import numpy as np from random import sample def linear_to_pair(i, s): x = i // s y = i - x * s return x, y def offset_inside(x, y, ox, oy, s): _x = (x + ox) % s _y = (y + oy) % s return (_x, _y) def reveal_around(x, y, mf): unchecked_interior = set() boundary = set() offsets = [(-1, 0), (1, 0), (0, -1), (0, 1)] for ox, oy in offsets: _x, _y = offset_inside(x, y, ox, oy, mf.shape[0]) if mf[_x, _y] == 0: unchecked_interior.add((_x, _y)) else: boundary.add((_x, _y)) return unchecked_interior, boundary def generate_revelation_matrix(x, y, mf): reveal = np.zeros(mf.shape) if mf[x, y] == -1: return None elif mf[x, y] > 0: reveal[x, y] = 1 return reveal else: checked_interior = set() checked_interior.add((x, y)) unchecked_interor, boundary = reveal_around(x, y, mf) while len(unchecked_interor) > 0: x, y = unchecked_interor.pop() int_nbhd, bdry_nbdh = reveal_around(x, y, mf) unchecked_interor.update(int_nbhd.difference(checked_interior)) boundary.update(bdry_nbdh) checked_interior.add((x, y)) interior = list(checked_interior) boundary = list(boundary) for x, y in interior: reveal[x, y] = 1 for x, y in boundary: if mf[x, y] > 0: reveal[x, y] = 1 else: print("Mine in boundary") return reveal class Minesweeper: def __init__(self, size, mines): self.m = mines self.s = size self.f = mines self.lost = False self.initialize_field() def initialize_field(self): minefield = np.zeros((self.s, self.s)) fog_of_war = np.zeros((self.s, self.s)) flagged = set() mine_positions = sample(range(self.s*self.s), self.m) xs = [-1, 0, 1] ys = [-1, 0, 1] for i in mine_positions: x, y = linear_to_pair(i, self.s) minefield[x, y] = -1 for i in range(self.s*self.s): x, y = linear_to_pair(i, self.s) if minefield[x, y] == -1: continue else: sub = np.zeros((3, 3)) for ox in xs: for oy in ys: sub[ox+1, oy+1] = minefield[offset_inside(x, y, ox, oy, self.s)] num_mines = np.sum(sub < 0) minefield[x, y] = num_mines self.mf = minefield self.fow = fog_of_war self.flagged = flagged def flag_at(self, x, y) -> None: self.flagged.add((x, y)) def unflag_at(self, x, y) -> None: self.flagged.remove((x, y)) def reveal_at(self, x, y): r = generate_revelation_matrix(x, y, self.mf) if r is None: self.lost = True else: self.fow = np.logical_or(self.fow, r) def show_minefield(self): field = np.abs(self.mf * self.fow) return '\n'.join(' '.join(self.colorize(int(i)) for i in row) for row in field) @classmethod def colorize(cls, i: int) -> str: return [ lambda x: f'\033[0m{x}\033[0m', lambda x: f'\033[31m{x}\033[0m', lambda x: f'\033[32m{x}\033[0m', lambda x: f'\033[33m{x}\033[0m', lambda x: f'\033[34m{x}\033[0m', ][i](i) def __repr__(self): field = np.abs(self.mf * self.fow) s = "" upper = " |" lower = " |" divider = "---" for x in range(self.s): u, l = numeral_vertical(x, len(str(self.s)), "b") upper = upper + u + "|" lower = lower + l + "|" divider += "--" s = s + upper + "\n" + lower + "\n" + divider + "\n" for x in range(self.s): s = s + numeral_horizontal(x, len(str(self.s)), "r") + "|" for y in range(self.s): # if self.mf[x, y] == -1: # s += "m " if (x, y) in self.flagged: s += "f " elif self.fow[x, y] == 0: s += "x " else: s += self.colorize(int(field[x, y])) s += " " s = s[:-1] s += "|\n" return s[:-1] + "\n" + divider def check_victory(self): return ( len(self.flagged) == self.m # all mines are flagged and int(np.sum(self.fow)) == (self.s**2 - self.m) # no stone unturned ) def numeral_horizontal(number, space, justification): n = str(number) if justification == "l": s = n + " " * (space - len(n)) elif justification == "r": s = (n[::-1] + " " * (space - len(n)))[::-1] elif justification == "c": pre = (space - len(n)) // 2 s = " " * pre + n + " " * (space - len(n) - pre) return s def numeral_vertical(number, space, justification): just_map = {"t": "l", "b": "r", "c": "c"} n = numeral_horizontal(number, space, just_map[justification]) return list(n) def dispatch_choice(ms: Minesweeper, choice) -> None: cmd = choice.split(" ") if cmd[0] == "f": x = int(cmd[1]) y = int(cmd[2]) ms.flag_at(x, y) elif cmd[0] == "u": x = int(cmd[1]) y = int(cmd[2]) ms.unflag_at(x, y) elif cmd[0] == "r": ms.s = int(cmd[1]) ms.m = int(cmd[2]) ms.initialize_field() elif cmd[0] == "o": x = int(cmd[1]) y = int(cmd[2]) ms.reveal_at(x, y) else: print("""Legal actions are: "f x y" to place a flag "u x y" to remove a flag "o x y" to open a tile "r s m" o reset the board with size s and m mines "q" to quit """) if __name__ == "__main__": # ms = Minesweeper(20, 1) is_running = True has_won = False print("You're playing minesweeper on the torus.") size = int(input("What size of board would you like to play? ")) num_mines = int(input("How many mines would you like? ")) ms = Minesweeper(size, num_mines) while is_running: if has_won: print("You won!") has_won = False else: print(ms) print(f"Flagged {len(ms.flagged)} of {ms.m} mines. Board size is {ms.s}.") print(f"Revealed {int(np.sum(ms.fow))} of {ms.s**2} tiles.") cmd = input("What would you like to do? ") if cmd == "q": is_running = False else: dispatch_choice(ms, cmd) has_won = ms.check_victory()