Created
December 12, 2018 23:51
-
-
Save Jwely/909d23420e9aac3e1642117d7647fd68 to your computer and use it in GitHub Desktop.
Minesweeper implementation under 120 minutes
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from typing import Tuple | |
import numpy as np | |
class MineSweeper(object): | |
def __init__(self, shape: Tuple, n_mines: int): | |
""" | |
:param shape: | |
:param n_mines: | |
""" | |
self.shape = shape | |
self.size = shape[0] * shape[1] | |
self.n_mines = n_mines | |
self._board = self.generate_board() | |
self._mines = self._board == -1 # mines are -1, positive integers are adjacent mines. | |
self.mask = self.generate_mask() # 1 indicates mask = True, 0 indicates space is revealed | |
self.flags = self.generate_flags() | |
self.game_over = False | |
assert self.size > n_mines, f"not enough space for {n_mines} mines!" | |
def generate_board(self): | |
def get_adjacent(arr: np.ndarray, i: int,j: int): | |
# end index is not inclusive, so + 2 is used on j values | |
x1 = max([i - 1, 0]) | |
x2 = min([i + 2, arr.shape[0]]) | |
y1 = max([j - 1, 0]) | |
y2 = min([j + 2, arr.shape[1]]) | |
return arr[x1: x2, y1: y2] | |
# generate random mine positions with n_mines. | |
flat = np.array([-1] * self.n_mines + | |
[0] * (self.size - self.n_mines)) | |
np.random.shuffle(flat) | |
board = np.reshape(flat, self.shape) | |
new_board = board * 0 # empty blank new board. | |
# now populate the adjacency numbers for each area on the board | |
for i in range(self.shape[0]): | |
for j in range(self.shape[1]): | |
adjacent = get_adjacent(board, i, j) | |
new_board[i, j] = (adjacent == -1).sum() | |
# now change mine locations on the new board to -1 | |
new_board[board == -1] = -1 | |
return new_board | |
def generate_mask(self): | |
mask = np.zeros(self.shape) + 1.0 | |
return mask.astype(bool) | |
def generate_flags(self): | |
flags = np.zeros(self.shape) | |
return flags.astype(bool) | |
def render(self) -> str: | |
str_hidden = " - " | |
str_flag = " P " | |
str_cleared = " + " | |
str_mine = " X " | |
def format_row(board: np.ndarray, mask: np.ndarray, flags: np.ndarray): | |
assert board.shape == mask.shape | |
for b, m, f in zip(board, mask, flags): | |
if m: | |
yield str_hidden | |
elif f: | |
yield str_flag | |
elif b == 0: | |
yield str_cleared | |
elif b == 0: | |
yield str_cleared | |
elif b == -1: | |
yield str_mine | |
else: | |
yield f" {b} " | |
# row by row, print the board | |
# TODO: hacky trial and error visual formatting here | |
row_strings = [ | |
"\t\t\t columns", | |
" row " + "".join([f"{i}".ljust(3) for i in range(self.shape[1])]) | |
] | |
for row_num in range(self.shape[0]): | |
row_strings.append( | |
f" {str(row_num).ljust(3)} |" + | |
"".join( | |
format_row( | |
self._board[row_num, :], | |
self.mask[row_num, :], | |
self.flags[row_num, :] | |
) | |
) | |
) | |
return "\n".join(row_strings) | |
def uncover(self, x, y) -> (bool, str): | |
""" returns false if player has lost the game by selecting a mine""" | |
if self.flags[x, y]: | |
return True, f"{x} {y} has a flag on it! remove the flag to uncover this space!" | |
self.mask[x, y] = 0 # punch hole in the mask revealing what's underneath | |
if self._mines[x, y]: | |
self.game_over = True | |
return False, f"{x} {y} was a mine! Game Over!" | |
else: | |
return True, f"uncovered {x} {y}" | |
def toggle_flag(self, x, y) -> str: | |
# remove existing flag | |
if self.flags[x, y] == 1: | |
self.flags[x, y] = 0 | |
return f"removed flag from {x} {y}" | |
# place a new flag | |
else: | |
self.flags[x, y] = 1 | |
return f"placed new flag on {x} {y}" | |
@staticmethod | |
def instructions(): | |
return "" # TODO: finish | |
@staticmethod | |
def help(): | |
doc = \ | |
""" help | |
Command description | |
------- ----------- | |
help see instructions | |
u [x] [y] uncovers the space at coordinates x and y | |
f [x] [y] places a flag at coordinates x and y, marking it as a mine. | |
or removes an existing flag. | |
""" | |
return doc | |
def move(self): | |
""" prompts user for input and alters the game state accordingly. """ | |
move = input("Whats your next move?") | |
if move == "help": | |
print(self.help()) | |
parsed = move.strip().split(" ") | |
if len(parsed) == 3: | |
move, x, y = parsed | |
x = int(x) | |
y = int(y) | |
if move == "u": | |
result, message = self.uncover(x, y) | |
return message | |
elif move == "f": | |
return self.toggle_flag(x, y) | |
else: | |
print(f"did not understand input {move}") | |
def game_end_report(self): | |
print("board looked like this!") | |
self.mask = self.mask * 0 # unmask the board! | |
print(self.render()) | |
if __name__ == "__main__": | |
# main game loop | |
# start the game | |
# TODO: allow user input board instantiation | |
mf = MineSweeper((15, 15), 30) | |
# print instructions and help | |
print(mf.instructions()) | |
print(mf.help()) | |
# print the game map | |
print(mf.render()) | |
while not mf.game_over: | |
print(mf.move()) | |
print(mf.render()) | |
mf.game_end_report() | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment