**Wesley Kerfoot**3 years ago

**1 changed files**with

**222 additions**and

**0 deletions**

`@ -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)` |

Loading…

Reference in new issue