This guide explains how to build the Minesweeper game in the Python terminal. We’ll break down the code step by step—from importing modules and defining helper functions to creating the main classes and implementing the game loop. Finally, you’ll learn how to run the game in your terminal.

This program is built for the Python terminal environment using only Python’s standard libraries. It does not depend on any external libraries and is compatible with Python 3.10 and above.

For the complete code, please visit the “oyna” project and then go to the “Minesweeper” section to view the code. Feel free to contribute to its improvement and development.

minesweeper gameplay

1. Importing Modules and Helper Function

The code begins by importing required modules and defining a helper function to capture a single character from user input. This function works across different operating systems.

import itertools
import random
import typing
from enum import Enum

def getch() -> str:
    """Gets a single character"""
    try:
        import msvcrt
        return str(msvcrt.getch().decode("utf-8"))  # type: ignore
    except ImportError:
        import sys
        import termios
        import tty

        fd = sys.stdin.fileno()
        oldsettings = termios.tcgetattr(fd)
        try:
            tty.setraw(fd)
            ch = sys.stdin.read(1)
        finally:
            termios.tcsetattr(fd, termios.TCSADRAIN, oldsettings)
        return ch

Imports:

  • itertools and random for processing iterators and generating random values.
  • typing to support type hints.
  • Enum from the enum module is used to define various states and actions.

getch Function:
Captures a single character without waiting for the Enter key.
Uses msvcrt on Windows and termios/tty on Unix-like systems.

2. Defining States and Actions

Two enums are defined to manage cell states and user actions.

class State(Enum):
    BLOCK = "🟪"
    BOMB = "💣"
    DEAD = "💥"
    EIGHT = " 8"
    FIVE = " 5"
    FLAG = "❓"
    FOUR = " 4"
    NINE = " 9"
    ONE = " 1"
    PLAYER = "🟦"
    SEVEN = " 7"
    SIX = " 6"
    THREE = " 3"
    TWO = " 2"
    WALL = "🔹"
    WIN = "🏆"
    ZERO = "  "

    @classmethod
    def create_from_number(cls, number: int) -> "State":
        return {
            0: cls.ZERO,
            1: cls.ONE,
            2: cls.TWO,
            3: cls.THREE,
            4: cls.FOUR,
            5: cls.FIVE,
            6: cls.SIX,
            7: cls.SEVEN,
            8: cls.EIGHT,
            9: cls.NINE,
        }[number]

class Action(Enum):
    CLICK = "click"
    MOVE_DOWN = "down"
    MOVE_LEFT = "left"
    MOVE_RIGHT = "right"
    MOVE_UP = "up"
    SET_FLAG = "flag"

State Enum:
Contains various visual states for cells, including hidden blocks, bombs, numbers (from ZERO to NINE), flags, walls, and end-game states (WIN and DEAD).

The create_from_number class method maps a numeric value to its corresponding state.

Action Enum:
Lists possible actions such as clicking a cell, moving the player in four directions, and setting a flag.

3. The Cell Class

Each cell on the Minesweeper board is represented by the Cell class. This class holds cell-specific properties and methods to handle actions like clicking and flagging.

class Cell:
    def __init__(self, state: State = State.BLOCK) -> None:
        self.player_is_here = False  # Indicates if the player is on this cell.
        self.seen = False            # Whether the cell has been revealed.
        self.state = state           # The display state of the cell.
        self.value = 0               # Holds the bomb count (or -1 for a bomb).
        self.down: "Cell"
        self.up: "Cell"
        self.right: "Cell"
        self.left: "Cell"

    def __str__(self) -> str:
        # If the player is here, show the PLAYER icon; otherwise, show the current state.
        return State.PLAYER.value if self.player_is_here else self.state.value

    def set_neighbors(
        self, left: "Cell", right: "Cell", up: "Cell", down: "Cell"
    ) -> None:
        self.down = down
        self.up = up
        self.right = right
        self.left = left

    def set_state(self, action: Action) -> "Cell":
        match action:
            case Action.SET_FLAG:
                self._set_flag()
                return self
            case Action.CLICK:
                self._click()
                return self
            case _:
                return self._move_tile(action)

    def _move_tile(self, action: Action) -> "Cell":
        side_: "Cell" = getattr(self, action.value)
        if side_.state == State.WALL:
            return self
        else:
            self.player_is_here = False
            side_.player_is_here = True
            return side_

    def _click(self) -> None:
        # Process a click only if the cell hasn't been seen and is not a wall.
        if self._is_unseen_and_not_wall():
            # If the cell is not a bomb, update its state based on the value.
            self.state = (
                State.create_from_number(self.value) if self.value > -1 else State.BOMB
            )
            self.seen = True
            # If the cell is empty (no bombs around), automatically reveal adjacent cells.
            if self._is_empty():
                self._continue(Action.MOVE_DOWN)
                self._continue(Action.MOVE_LEFT)
                self._continue(Action.MOVE_RIGHT)
                self._continue(Action.MOVE_UP)

    def _is_unseen_and_not_wall(self) -> bool:
        return not self.seen and self.state != State.WALL

    def _is_empty(self) -> bool:
        return self.value == 0

    def _set_flag(self) -> None:
        # Toggle flag if the cell has not been revealed.
        if not self.seen:
            self.state = State.BLOCK if self.state == State.FLAG else State.FLAG

    def _continue(self, side: Action) -> None:
        side_: typing.Union["Cell", None] = getattr(self, side.value)
        if side_ is not None:
            side_.set_state(Action.CLICK)

Attributes:

  • player_is_here tracks the player’s current position.
  • seen indicates whether the cell has been clicked and revealed.
  • state holds the visual representation (like a hidden block, number, flag, or bomb).
  • value stores the underlying number (bomb count) or -1 if the cell contains a bomb.
  • Neighbor pointers (up, down, left, right) are set later.

Methods:

  • __str__: Displays the appropriate icon depending on whether the player is on the cell.
  • set_neighbors: Sets the cell’s adjacent neighbors.
  • set_state: Dispatches actions (click, flag, or move) based on the user input.
  • _click: Reveals the cell; if it is empty, recursively reveals neighboring cells.
  • _set_flag: Toggles a flag on the cell.
  • _continue: Automatically continues revealing adjacent cells if the current cell is empty.

4. The Board Class

The Board class manages the game grid, sets up walls, places bombs, and assigns neighboring cells.

class Board:
    def __init__(self, size: int) -> None:
        self.start_player_position = size // 2
        self.size = size
        self.cells = self._cells()
        self.set_initial()
        self.player = self.cells[self.start_player_position][self.start_player_position]

    def _cells(self) -> list[list[Cell]]:
        return [[Cell() for _ in range(self.main_size)] for _ in range(self.main_size)]

    @property
    def main_size(self) -> int:
        # The main board size includes a border (walls) around the game area.
        return self.size + 2

    def set_initial(self) -> None:
        self.set_horizontal_walls()
        self.set_vertical_walls()
        self.set_cells_neighboring()
        self.set_player()
        self.set_bombs()

    def set_horizontal_walls(self) -> None:
        for j in range(self.main_size):
            self.cells[0][j].state = State.WALL
            self.cells[self.main_size - 1][j].state = State.WALL

    def set_vertical_walls(self) -> None:
        for i in range(self.main_size):
            self.cells[i][0].state = State.WALL
            self.cells[i][self.main_size - 1].state = State.WALL

    def set_cells_neighboring(self) -> None:
        for i in range(1, self.main_size - 1):
            for j in range(1, self.main_size - 1):
                self.cells[i][j].set_neighbors(
                    self.cells[i][j - 1],
                    self.cells[i][j + 1],
                    self.cells[i - 1][j],
                    self.cells[i + 1][j],
                )

    def set_player(self) -> None:
        self.cells[self.start_player_position][self.start_player_position].player_is_here = True

    def set_bombs(self) -> None:
        # Place a certain number of bombs randomly on the board.
        for _ in range(self.size + 2):
            cell = self.cells[random.randint(2, self.size - 1)][random.randint(2, self.size - 1)]
            if cell.value != -1:
                cell.value = -1
                # Update neighboring cells by increasing their bomb count.
                self.increase_value(cell.down)
                self.increase_value(cell.down.left)
                self.increase_value(cell.down.right)
                self.increase_value(cell.up)
                self.increase_value(cell.up.left)
                self.increase_value(cell.up.right)
                self.increase_value(cell.left)
                self.increase_value(cell.right)

    @staticmethod
    def increase_value(cell: Cell) -> None:
        # Increment the cell's value if it is not a bomb.
        cell.value += 1 if cell.value != -1 else 0

    def action(self, ch: str) -> None:
        # Map key inputs to actions on the current player cell.
        match ch:
            case "w":
                self.player = self.player.set_state(Action.MOVE_UP)
            case "a":
                self.player = self.player.set_state(Action.MOVE_LEFT)
            case "s":
                self.player = self.player.set_state(Action.MOVE_DOWN)
            case "d":
                self.player = self.player.set_state(Action.MOVE_RIGHT)
            case "e":
                self.player = self.player.set_state(Action.CLICK)
            case "q":
                self.player = self.player.set_state(Action.SET_FLAG)
            case " ":
                self.player.state = State.BOMB
            case _:
                pass

    def __str__(self) -> str:
        return "\n".join(["".join([str(cell) for cell in rows]) for rows in self.cells])

    def player_win(self) -> bool:
        # Determine if the player has won by checking if every non-bomb, non-wall cell has been revealed.
        for cell in itertools.chain(*self.cells):
            if cell.value >= 0 and cell.state != State.WALL and not cell.seen:
                return False
        return True

Grid Initialization:
The board is created with an extra border to act as walls.

Walls:
set_horizontal_walls and set_vertical_walls add walls around the game area.

Neighboring Cells:
set_cells_neighboring connects each cell to its adjacent neighbors.

Bomb Placement:
set_bombs randomly places bombs and updates the numbers of adjacent cells.

User Input:
The action method maps key presses (such as “w”, “a”, “s”, “d” for movement and “e” for clicking) to corresponding actions.

Win Condition:
The player_win method checks if all non-bomb cells have been revealed.

5. The Game Class and Game Loop

The Game class encapsulates the main game loop, managing the display and processing user input until the game ends.

class Game:
    def __init__(self) -> None:
        self.board = Board(15)

    def run(self) -> None:
        self._bold_font()
        while self.allow_continue():
            self._print_board()
            self.board.action(getch())
        self.print_result()

    @staticmethod
    def _bold_font() -> None:
        print("\033[1;10m")

    @staticmethod
    def clear_screen() -> None:
        print("\033[H\033[J", end="")

    def _print_board(self) -> None:
        self.clear_screen()
        print(self.board)

    def allow_continue(self) -> bool:
        # Continue the game if the player hasn't hit a bomb and hasn't yet won.
        return self.board.player.state != State.BOMB and not self.board.player_win()

    def print_result(self) -> None:
        # Once the game is over, update bomb cells to reflect win or loss.
        for cell in filter(lambda c: c.value < 0, itertools.chain(*self.board.cells)):
            cell.state = State.WIN if self.board.player_win() else State.DEAD
            cell.player_is_here = False
        self._print_board()

def run() -> None:
    Game().run()

if __name__ == "__main__":
    run()

Game Initialization:
A Game object is created with a board of size 15.

Game Loop:
The loop continues while the player hasn't triggered a bomb or met the win condition. Each iteration clears the screen, prints the board, and processes a key input.

Display Settings:
The _bold_font method sets a bold font for improved visual appeal.

Result Handling:
After the game ends, print_result updates bomb cells to indicate whether the player won (WIN state) or lost (DEAD state) and then prints the final board.

6. How to Run the Game

To run this Minesweeper game in your Python terminal:

Prerequisites:
Python 3.10 or above. No external libraries are needed as the game uses only standard Python libraries.

Download the Code:
Save the entire code in a file (for example, minesweeper_game.py) or download the grid_base.py file.

Run the Game:
Open your terminal and navigate to the directory containing the file. Execute the command:

python3 minesweeper_game.py

7. How to Play

Once the game starts, the Minesweeper board will be displayed in your terminal. Use the following controls:

Movement:

  • w: Move Up
  • a: Move Left
  • s: Move Down
  • d: Move Right
  • e: Click (reveal a cell)
  • q: Set/Toggle Flag (to mark a suspected bomb)
  • Space: Exit the game

Objective:
Reveal all cells that do not contain bombs. If you reveal a bomb, the game is over. When all safe cells are revealed, you win!

8. Conclusion

This comprehensive guide provided a step-by-step explanation of the Minesweeper game code in the Python terminal. We covered:

  • How to handle user input with a cross-platform getch function.
  • The use of Enum classes to manage cell states and actions.
  • Detailed explanations of the Cell and Board classes, including neighbor assignment, bomb placement, and win condition checking.
  • The game loop and how the Game class manages the overall flow.
  • Instructions on running the game and the controls for playing.

Enjoy the game!