You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('') and can be up to 35 characters long.
222 lines
6.9 KiB
222 lines
6.9 KiB
#! /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, y1),


(x, y+1), (x, y1),


(x1, y+1), (x1, y), (x1, y1)


]


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 breadthfirst 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)


