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