MineSweeper game - OO / code structure feedback
Anonymous User
218

Hi everyone,

It's not returning won or lost yet. I'd just like some OO / code structure feedback on this.
Any suggestions welcome

from enum import Enum
from random import sample
import numpy as np

# 8 coordinates around a point
COORDINATES = [(0, 1), (1, 0), (-1, 0), (0, -1), (1, 1), (-1, -1), (1, -1), (-1, 1)]


class TileType(Enum):
    """
    'M' mine, unrevealed
    'E' empty square, revealed
    'B' blank square, revealed, that has no adjacent minde
    '1' - '8' represents how many mines are adjacent to this revealed square
    'X' represents a revealed exploded mine.
    """

    MINE = "M"
    EMPTY = "E"
    REVEALED_EMPTY = "B"
    REVEALED_MINE = "X"


class Tile:
    """ A single tile in the grid. 
    See TileType Enum for all possible values.
    """

    def __init__(self, value: TileType = TileType.EMPTY) -> None:
        self._value = value
        self._visible = False

    @property
    def visible(self):
        return self._visible

    @property
    def value(self):
        return self._value

    @value.setter
    def value(self, value):
        self._value = value
        if value in [TileType.REVEALED_MINE, TileType.REVEALED_EMPTY]:
            self._visible = True

    def is_mine(self) -> bool:
        return self._value == TileType.MINE

    def is_empty(self) -> bool:
        return self._value == TileType.EMPTY

    def is_revealed_empty(self) -> bool:
        return self._value == TileType.REVEALED_EMPTY

    def __repr__(self) -> str:
        if type(self._value) is int:
            return str(self._value)
        return str(self._value.value)


class Board:
    """Board class for the MineSweeper game"""

    def __init__(self, rows: int, cols: int) -> None:
        self.rows = rows
        self.cols = cols
        self._board = [[Tile() for _ in range(cols)] for _ in range(rows)]

    @property
    def data(self):
        return self._board

    def at(self, row: int, col: int) -> Tile:
        return self._board[row][col]

    def in_bounds(self, row: int, col: int) -> bool:
        """Checks row and col are in bound in the grid"""
        if not (0 <= row < self.rows and 0 <= col < self.cols):
            return False
        return True

    def place_mines(self, num_mines: int) -> None:
        """Generate random mines coordinates and place them on the board"""
        candidates = [(x, y) for y in range(self.cols) for x in range(self.rows)]
        mines_coordinates = sample(candidates, num_mines)
        for x, y in mines_coordinates:
            self._board[x][y] = Tile(TileType.MINE)

    def update_board(self, row: int, col: int) -> None:
        """Trigger the explore() and update properties of affected Tiles"""
        if self._board[row][col].is_mine():
            self._board[row][col].value = TileType.REVEALED_MINE
            return
        self.explore(row, col)

    def explore(self, row: int, col: int) -> None:
        """DFS to check surrounding mines and reveal all visible tiles"""
        if not self.in_bounds(row, col) or not self._board[row][col].is_empty():
            return

        mines = self.find_surrounding_mines(row, col)
        if mines > 0:
            self._board[row][col].value = mines
            return

        self._board[row][col].value = TileType.REVEALED_EMPTY
        for x, y in COORDINATES:
            self.explore(row + x, col + y)  # DFS

    def find_surrounding_mines(self, row: int, col: int):
        mines_around = 0
        for x, y in COORDINATES:
            if (
                self.in_bounds(row + x, col + y)
                and self._board[row + x][col + y].is_mine()
            ):
                mines_around += 1
        return mines_around


class MineSweeper:
    """The MineSweeper game class, initiates the board, handle on_click and print board"""

    def __init__(self, rows: int, cols: int, num_mines: int) -> None:
        self._board = Board(rows, cols)
        self._board.place_mines(num_mines)

    def on_click(self, row: int, col: int) -> bool:
        """Handles click validation and updates the board"""
        if not self._board.in_bounds(row, col):
            # raise Exception("Not in bounds")
            print("Not in bounds")
            return False
        if self._board.at(row, col).visible:
            # raise Exception("Already visible")
            print("Already visible")
            return False

        self._board.update_board(row, col)

        return False

    def print_board(self) -> None:
        # print(np.matrix(self._board._board)) # how the board looks under the hood
        for row in range(self._board.rows):
            for col in range(self._board.cols):
                if (
                    self._board._board[row][col].is_mine()
                    or self._board._board[row][col].is_empty()
                ):
                    print("-", end=" ")
                elif self._board._board[row][col].is_revealed_empty():
                    print("0", end=" ")
                else:
                    print(self._board._board[row][col], end=" ")
            print("")
        print("-" * (self._board.rows + self._board.cols))


num_mines = 7

mine_sweeper = MineSweeper(10, 10, num_mines)
mine_sweeper.on_click(5, 1)
mine_sweeper.print_board()
# mine_sweeper.on_click(8, 5);
# mine_sweeper.print_board()
Comments (0)