Browse Source

Implementation of a minesweeper board

master
Wesley Kerfoot 3 years ago
parent
commit
4e495608aa
  1. 222
      minesweeper_board.py

222
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)
Loading…
Cancel
Save