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.
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
andrandom
for processing iterators and generating random values. -
typing
to support type hints. -
Enum
from theenum
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
andBoard
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!