Last active
October 14, 2018 22:31
-
-
Save timorthi/3b5f115392920663bc05fb787ae7e77b to your computer and use it in GitHub Desktop.
Python implementation of Minesweeper
This file contains 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
import random | |
class Cell: | |
''' | |
Helper class to encapsulate the state of a given cell in the Minesweeper grid. | |
''' | |
def __init__(self, row, col, revealed=False, is_bomb=False, value=0): | |
self.row = row | |
self.col = col | |
self.revealed = revealed | |
self.is_bomb = is_bomb | |
self.value = value | |
def __repr__(self): | |
if self.revealed: | |
return '*' if self.is_bomb else str(self.value) | |
else: | |
return '-' | |
class Minesweeper: | |
BOMB_PCT = 0.15 | |
DELTAS = [ | |
(-1, -1), (0, 1), (1, 1), | |
(-1, 0), (1, 0), | |
(-1, 1), (0, -1), (1, -1), | |
] | |
def __init__(self, rows, cols): | |
Minesweeper.__validate(rows, cols) | |
self.rows = int(rows) | |
self.cols = int(cols) | |
self.has_bombed = False | |
self.revealed_count = 0 | |
self.num_bombs = 0 | |
self.grid = self.__generate_grid() | |
def print_grid(self): | |
''' | |
Helper method to print the grid in its current state with row and column labels. | |
''' | |
col_labels = "\t".join(map(str, [x for x in range(1, self.cols+1)])) | |
print(f'\t{col_labels}\n') | |
for i in range(0, self.rows): | |
row_vals = "\t".join(map(str, self.grid[i])) | |
print(f'{i+1}\t{row_vals}') | |
def sweep(self, row, col): | |
''' | |
Accepts a row and column to select and reveal a cell. If the cell is a bomb, game is over. | |
Otherwise, recursively reveal until all revealed cells are non-blank. | |
''' | |
row = int(row) - 1 | |
col = int(col) - 1 | |
cell = self.grid[row][col] | |
if cell.revealed: | |
print("Please select a cell that isn't revealed yet.") | |
return | |
if cell.is_bomb: | |
self.grid[row][col].revealed = True | |
self.revealed_count += 1 | |
self.has_bombed = True | |
else: | |
self.__expand_blanks(cell) | |
def is_game_over(self): | |
''' | |
Returns whether the game is over. | |
''' | |
return self.has_bombed or self.__all_cells_revealed() | |
def reveal_all_cells(self): | |
''' | |
Helper method to reveal all cells at the end of a game. | |
''' | |
for i in range(0, self.rows): | |
row = self.grid[i] | |
for j in range(0, self.cols): | |
row[j].revealed = True | |
def print_end_message(self): | |
''' | |
Helper method to print message depending on game end condition. | |
''' | |
if self.has_bombed: | |
print('You swept a mine. Game over!') | |
else: | |
print('Congratulations! You swept the field without blowing something up.') | |
@staticmethod | |
def __validate(rows, cols): | |
''' | |
Validates number of rows and columns during initialization | |
''' | |
rows = int(rows) | |
cols = int(cols) | |
if rows < 5 or cols < 5: | |
raise ValueError('Rows or columns cannot be less than 5.') | |
elif rows > 20 or cols > 20: | |
raise ValueError('Rows or columns cannot be larger than 20.') | |
def __generate_grid(self): | |
''' | |
Returns a `self.rows x self.cols` 2D array with about self.BOMB_PCT chance for each cell | |
to have a bomb. | |
''' | |
grid = [None] * self.rows | |
# Populate grid with bombs | |
for i in range(0, self.rows): | |
row = [None] * self.cols | |
for j in range(0, self.cols): | |
is_bomb = (random.random() <= self.BOMB_PCT) | |
if is_bomb: | |
self.num_bombs += 1 | |
row[j] = Cell(i, j, is_bomb=is_bomb) | |
grid[i] = row | |
# Compute adjacent values for non-bomb cells | |
for i in range(0, self.rows): | |
row = grid[i] | |
for j in range(0, self.cols): | |
cell = row[j] | |
if not cell.is_bomb: | |
cell.value = self.__get_adjacent_bomb_count(grid, i, j) | |
return grid | |
def __get_adjacent_bomb_count(self, grid, row, col): | |
''' | |
Returns the number of bombs adjacent to the given cell. | |
''' | |
count = 0 | |
for delta in self.DELTAS: | |
d_row, d_col = row + delta[1], col + delta[0] | |
if self.__is_in_range(d_row, d_col) and grid[d_row][d_col].is_bomb: | |
count += 1 | |
return count | |
def __expand_blanks(self, root): | |
''' | |
Recursively reveal non-blank, non-revealed cells in the grid starting from root. | |
''' | |
if root.revealed: | |
return | |
root.revealed = True | |
self.revealed_count += 1 | |
if root.value == 0: | |
for delta in self.DELTAS: | |
d_row, d_col = root.row + delta[1], root.col + delta[0] | |
if self.__is_in_range(d_row, d_col): | |
self.__expand_blanks(self.grid[d_row][d_col]) | |
def __is_in_range(self, row, col): | |
''' | |
Returns whether row and col fall within the grid's range. | |
''' | |
return (0 <= row < self.rows) and (0 <= col < self.cols) | |
def __all_cells_revealed(self): | |
''' | |
Returns whether all non-bomb cells have been revealed | |
''' | |
return self.revealed_count >= (self.rows * self.cols) - self.num_bombs | |
def main(): | |
print('Welcome to Minesweeper!') | |
print('Please enter grid size in the format "rows cols"') | |
user_input = input('--> ') | |
ms = Minesweeper(*user_input.split(' ')) | |
while not ms.is_game_over(): | |
ms.print_grid() | |
print('Please enter the cell you wish to sweep in the format "row col"') | |
user_input = input('--> ') | |
ms.sweep(*user_input.split(' ')) | |
ms.print_grid() | |
ms.print_end_message() | |
print('\n\n\nFinal Grid:') | |
ms.reveal_all_cells() | |
ms.print_grid() | |
if __name__ == '__main__': | |
main() | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment