From 4e495608aab1a58b5c3fb62cb6f453cff8017757 Mon Sep 17 00:00:00 2001 From: Wesley Kerfoot Date: Fri, 7 Feb 2020 02:45:18 -0500 Subject: [PATCH] Implementation of a minesweeper board --- minesweeper_board.py | 222 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 222 insertions(+) create mode 100755 minesweeper_board.py diff --git a/minesweeper_board.py b/minesweeper_board.py new file mode 100755 index 0000000..a0517b9 --- /dev/null +++ b/minesweeper_board.py @@ -0,0 +1,222 @@ +#! /usr/bin/env python3 + +from itertools import product +from random import choices +from collections import deque +from sys import stdout + +import attr, cattr + +# Serialization helpers for persisting games +def serialize_board(board): + """ + Returns a dict representation of the game board + """ + return { + "cell_rows" : [[cattr.unstructure(cell) for cell in row] for row in board.cells], + "height" : board.height, + "width" : board.width, + "probability" : board.board_probability + } + +def deserialize_board(board_data): + """ + Returns a Board object given a dict serialized from it + """ + return Board( + cells=[[cattr.structure(cell, Cell) for cell in row] + for row in board_data.get("cell_rows", [])], + width=board_data.get("width", 8), + height=board_data.get("height", 8), + board_probability=board_data.get("probability", 0.10) + ) + +def gen_board(width, height, p): + """ + Generate a fresh board + """ + return [ + [Cell((w, h), is_mine=gen_cell(p)) for w in range(width)] for + h in range(height) + ] + +def gen_cell(probability): + """ + Generate a single cell with probability p that it is a mine + True = has a mine + False = is clear + """ + return choices((True, False), (probability, (1 - probability)))[0] + +@attr.s +class Cell: + location = attr.ib() + is_mine = attr.ib(default=False) + + @property + def x(self): + return self.location[0] + + @property + def y(self): + return self.location[1] + +@attr.s +class Board: + height = attr.ib(default=20) + width = attr.ib(default=20) + cells = attr.ib(factory=list) + board_probability = attr.ib(default=0.10) + + def __iter__(self): + return iter(self.cells) + + def print_board(self): + for y in range(self.height): + for x in range(self.width): + stdout.write("x" if self.get_cell(x, y).is_mine else "0") + stdout.write("\n") + + def show_board(self): + """ + Convert to a representation we can show on the frontend + """ + return [ + [(0, self.get_cell(x, y).is_mine) for x in range(self.width)] + for y in range(self.height) + ] + + + def get_cell(self, x, y): + """ + Get the value of an individual cell + """ + # Handle boundary conditions + if (x >= self.width or + y >= self.height or + x < 0 or y < 0): + return None + try: + return self.cells[y][x] + except IndexError: + return None + + def get_adjacent(self, x, y): + """ + Get a list of all cells adjacent to this location + """ + return [ + self.get_cell(x, y) for x, y in [ + (x+1, y), (x+1, y+1), (x+1, y-1), + (x, y+1), (x, y-1), + (x-1, y+1), (x-1, y), (x-1, y-1) + ] + if self.get_cell(x, y) + ] + + def count_adjacent(self, cells): + """ + How many mines are adjacent to this cell? + """ + return sum([c.is_mine for c in cells]) + + def flip_cell(self, x, y): + """ + Flip over a cell + Three potential cases: + Uncovering a mine + A clear cell with 1 or more adjacent mines + A clear cell with no adjacent mines, then we keep clearing + """ + clicked_cell = self.get_cell(x, y) + + if clicked_cell is None: + return [] + + if clicked_cell.is_mine: + # if it's a mine, we're done + return [(0, clicked_cell)] + + cells = deque([clicked_cell]) + uncovered = [] + processed_locations = set() + + # do a breadth-first search of the surrounding cells + while cells: + for cell in list(cells): + cells.popleft() + adjacent_cells = self.get_adjacent(cell.x, cell.y) + num_adjacent = self.count_adjacent(adjacent_cells) + + if not (cell.location in processed_locations): + if not cell.is_mine: + # This is the mine we "clicked" on + uncovered.append((num_adjacent, cell)) + + # add to the set of processed cells + processed_locations.add(cell.location) + + # skip processing the surrounding ones + # if it has at least one adjacent mine + if num_adjacent > 0: + continue + else: + # Process surrounding cells that themselves have 0 adjacent mines + # This adds them to the queue of cells to be processed + cells.extend([c for c in adjacent_cells if self.count_adjacent(self.get_adjacent(*c.location)) == 0]) + + # Add the surrounding adjacent cells to the uncovered list + for cell in adjacent_cells: + adjacent_count = self.count_adjacent(self.get_adjacent(*cell.location)) + + # small optimization to prevent it from processing mines + if cell.is_mine: + processed_locations.add(cell.location) + + elif adjacent_count > 0 and (not (cell.location in processed_locations)): + processed_locations.add(cell.location) + uncovered.append((adjacent_count, cell)) + return uncovered + +# Helper functions to handle playing the game +def click_cell(game_board, display_board, x, y): + """ + Game board: the full board object + Display board: the board displayed to users + x, y: coordinates ot the cell to be uncovered + Mutates `display_board` and returns it, or False if it is a mine. + """ + uncovered = game_board.flip_cell(x, y) + for cell in uncovered: + if cell[1].is_mine: + return False + + for adjacent_num, cell in uncovered: + display_board[cell.y][cell.x] = (adjacent_num, cell.is_mine) + + return display_board + +def won(game_board, display_board): + """ + Determine if the only remaining uncovered cells are mines + This determins if the player has won + """ + won = True + for y, row in enumerate(display_board): + for x, cell in enumerate(row): + if (cell is None) and (not game_board.get_cell(x, y).is_mine): + # if it's uncovered and it's not a mine + # then they haven't won yet + won = False + return won + +def new_board(x=8, y=8, p=0.10): + """ + Generate a new game board, and display board + The game board never changes + The display board gets updated each time a cell is revealed + """ + game_board = Board(cells=gen_board(x, y, p), width=x, height=y, board_probability=p) + display_board = [[None for _ in range(x)] for _ in range(y)] + + return (game_board, display_board)