How to set up the environment:https://mrpunkdasilva.github.io/16Games-in-Cpp/configuracao-ambiente.html
Repository: https://github.com/mrpunkdasilva/16Games-in-Cpp/tree/main/05%20Minesweeper
This tutorial delves into the implementation of the classic Minesweeper game using C++ and the SFML library. We will cover the game architecture, underlying data structures, core algorithms, and user interface management in a technical and detailed manner.
Technical Overview
Minesweeper is a grid-based logic game where the player must deduce the location of hidden mines. The implementation focuses on efficient state management, 2D grid manipulation, and a recursive flood fill algorithm to reveal areas of the board.
Game Organisation
Game States - A Finite State Machine
The game flow is controlled by a simple state machine, defined by an enum
and a global variable currentGameState
. This allows the program to behave differently depending on the context (menu, active game, end of game).
enum GameState {
MENU, // Initial screen, waiting for the player to start
PLAYING, // Game in progress, processing board interactions
GAME_OVER, // Game ended by defeat (mine detonated)
GAME_WON // Game ended by victory (all safe cells revealed)
};
GameState currentGameState = MENU; // The game always starts in the menu state
State transitions are triggered by user events (clicks) or game conditions (detonating a mine, revealing all safe cells).
graph LR
A[MENU] -->|Click Start Game| B[PLAYING]
B -->|Mine clicked| C[GAME_OVER]
B -->|All safe cells revealed| D[GAME_WON]
C -->|Any Click| B
D -->|Any Click| B
Data Structures - Board Representation
The Minesweeper board is modelled by two two-dimensional arrays of integers, both of size 12x12
to accommodate an invisible border of cells (indices 0 and 11) that simplifies the logic of checking neighbours.
-
int grid[12][12]
: This array stores the logical state of each cell: -
0
to8
: Indicates the number of adjacent mines. -
9
: Represents a mine.- The cells on the edge (indices 0 and 11) are usually initialised with
0
and are not displayed to the player.
- The cells on the edge (indices 0 and 11) are usually initialised with
int sgrid[12][12]
: This array stores the visible state of each cell to the player:-
10
: Cell covered (not revealed).-
11
: Cell marked with a flag.
-
0
to9
: Revealed cell, displaying the number of adjacent mines or a mine (if detonated).
int w = 32; // Size in pixels of each cell (width and height)
int grid[12][12]; // Stores the layout of mines and counts
int sgrid[12][12]; // Stores what is visible to the player
Coordinate System
The game operates with two coordinate systems:
- Logical Coordinates (Grid): Pairs of integers
(i, j)
that represent the position of the cell in the matrix (e.g.,grid[i][j]
). These are used for all game logic (mine calculation, neighbour checking). - Visual Coordinates (Pixel): Pairs of integers
(x, y)
in pixels, used for rendering in the SFML window. The conversion is done by multiplying the logical coordinates by the cell size (w
).
// Conversion from pixel coordinates to grid coordinates
int x_grid = pos.x / w;
int y_grid = pos.y / w;
// Conversion from grid coordinates to pixel coordinates for sprite positioning
s.setPosition(i * w, j * w);
Core Mechanics and Algorithms
resetGame()
- Initialisation and Board Reconfiguration
This function is called at the start of each new game. Its algorithm is:
- Initialisation of
sgrid
: All visible cells are set to10
(covered). - Initialisation of
grid
: All logical cells are set to0
(no mines). - Mine Placement: A double loop iterates over cells
(1,1)
to(10,10)
. For each cell, a random number is generated (rand() % 5 == 0
). If the condition is true (approximately 20% chance), the cell atgrid[i][j]
is set to9
(mine). - Number Calculation: Another double loop iterates over cells
(1,1)
to(10,10)
. If a cell does not contain a mine (grid[i][j] != 9
), it checks its 8 neighbours (including diagonals). For each neighbour that contains a mine (grid[neighbour_x][neighbour_y] == 9
), a counter is incremented. The final value of the counter is assigned togrid[i][j]
. The invisible edge (0
and11
) ensures that neighbour checks do not go beyond the limits of the array.
void resetGame() {
// ... Initialisation of the grid ...
for (int i = 1; i <= 10; i++) {
for (int j = 1; j <= 10; j++) {
if (grid[i][j] == 9) continue; // Ignore mines
int n = 0;
for (int dx = -1; dx <= 1; dx++) { // Iterate over neighbours
for (int dy = -1; dy <= 1; dy++) {
if (grid[i + dx][j + dy] == 9) n++; // Count neighbouring mines
}
}
grid[i][j] = n;
}
}
}
graph TD
A[resetGame Called] --> B{Initialise covered sgrid and empty grid}
B --> C{Loop: Position Mines Randomly}
C --> D{Loop: Calculate number of neighbouring mines}
D --> E[Number of neighbouring mines]
E --> F[Playing field ready for new game]
openCells()
– Recursive flood-fill algorithm
This function is the backbone of the mechanics for uncovering empty cells. It is a classic implementation of the flood-fill algorithm:
- Stop condition: The recursion when cell
(i, j)
is no longer covered (sgrid[i][j] != 10
). This prevents endless loops and the reprocessing of cells that have already been uncovered. - Reveal: The cell
sgrid[i][j]
is updated to the corresponding value ingrid[i][j]
, making it visible. - Spreading (for empty cells): If the uncovered cell is
0
(empty, with no neighbouring mines), the function recursively calls itself for all of its 8 neighbours (including diagonals). The recursive calls are protected by boundary checks (i + dx >= 1 && i + dx <= 10
, etc.) to ensure that they do not access invalid indices of the array.
void openCells(int i, int j) {
if (sgrid[i][j] == 10) { // If the cell is covered
sgrid[i][j] = grid[i][j]; // Uncover
if (grid[i][j] == 0) { // If empty, propagate
for (int dx = -1; dx <= 1; dx++) {
for (int dy = -1; dy <= 1; dy++) {
// Check boundaries and call recursively
if (i + dx >= 1 && i + dx <= 10 && j + dy >= 1 && j + dy <= 10) {
openCells(i + dx, j + dy);
}
}
}
}
}
}
graph TD
A[Called] --> B{Cell covered?}
B -- No --> F[Return]
B -- Yes --> C[Uncover cell]
C --> D{Empty cell}
D -- No --> F
D -- Yes --> E[For each neighbour nx, ny]
E --> G{Neighbour nx, ny valid and within boundaries?}
G -- Yes --> A
G -- No --> F
User Input Processing
The game responds to mouse clicks, with behaviour varying according to the currentGameState
:
-
MENU
state: A left click within the ‘Start Game’ text area transitions the game to thePLAYING
state and starts a new game viaresetGame()
. -
GAME_OVER
/GAME_WON
States: Any left click restarts the game, transitioning toPLAYING
and callingresetGame()
. -
PLAYING
State:-
Left Click: If the clicked cell is covered (
sgrid[x][y] == 10
):
-
Left Click: If the clicked cell is covered (
- If
grid[x][y] == 9
(mine): The state changes toGAME_OVER
. All mines are revealed insgrid
. - Ifgrid[x][y] == 0
(empty):openCells(x, y)
is called to start recursive revealing. - Otherwise (number
1-8
): The cell is simply revealed (sgrid[x][y] = grid[x][y]
).-
Right Click: Toggles the cell state between covered (
10
) and flagged (11
), but only if the cell is covered or already has a flag.
-
Right Click: Toggles the cell state between covered (
Win and Lose Conditions
-
Lose: Detected immediately when a left click reveals a mine (
grid[x][y] == 9
). ThecurrentGameState
is set toGAME_OVER
. - Victory: Checked every frame in the
PLAYING
state. The game is won if: -
The number of covered cells (
sgrid[i][j] == 10
orsgrid[i][j] == 11
) is equal to the total number of mines on the board.- And the number of mines correctly marked with flags (
sgrid[i][j] == 11 && grid[i][j] == 9
) is equal to the total number of mines.
This logic ensures that the player not only reveals all safe cells, but also correctly identifies all mines. If the condition is satisfied,
currentGameState
is set toGAME_WON
. - And the number of mines correctly marked with flags (
User Interface (UI) and Rendering
SFML is used to draw all visual elements of the game.
Using Sprite Sheet (images/tiles.jpg
)
The tiles.jpg
file is a sprite sheet containing all images for the different cell states. Each image is a 32x32
pixel square. The function s.setTextureRect(IntRect(sgrid[i][j] * w, 0, w, w))
is crucial here: it selects the correct portion of the sprite sheet (sgrid[i][j] * w
pixels from the left) to draw the cell corresponding to its visible state.
- Sprite Sheet Indices:
-
0
to8
: Sprites for cells revealed with 0 to 8 adjacent mines. -
9
: Mine sprite (displayed when losing the game). -
10
: Covered cell sprite.-
11
: Flag sprite.
-
Text Rendering (sf::Font
, sf::Text
)
The Carlito-Regular.ttf
font is loaded to render status messages (GAME OVER
, YOU WIN!
, Click to Play Again
) and menu elements (MINESWEEPER
, Start Game
).
-
sf::Font::loadFromFile()
: Loads the font file. -
sf::Text
: Object used to configure the text (string, font, size, colour, outline). -
setTextureRect()
: Not applicable to text, butsetPosition()
andgetGlobalBounds()
are used to centre and position the text dynamically in the window.
Main Loop Structure (main
function)
The main game loop is the heart of the SFML application, following the common game pattern:
graph TD
A[Start of Main Loop] --> B{Process Events app.pollEvent}
B --> C{currentGameState == MENU?}
C -- Yes --> D[Check Click on Start Game]
C -- No --> E{currentGameState == GAME_OVER or GAME_WON?}
E -- Yes --> F[Check Restart Click]
E -- No --> G{currentGameState == PLAYING?}
G -- Yes --> H[Process Left/Right Clicks]
H --> I[Check Win Conditions]
I --> J[Clear Screen app.clear]
J --> K{currentGameState == MENU?}
K -- Yes --> L[Draw Menu]
K -- No --> M[Draw Board]
M --> N{currentGameState == GAME_OVER?}
N -- Yes --> O[Draw Text GAME OVER]
N -- No --> P{currentGameState == GAME_WON?}
P -- Yes --> Q[Draw Text YOU WIN!]
Q --> R[Draw Text Click to Play Again]
R --> S[Display Frame app.display]
S --> A
Advanced Programming Concepts
1. Finite State Machines (FSM)
- The implementation of
GameState
is a clear example of FSM, a fundamental design pattern in game development for managing complexity and application flow.
2. Recursion and Search Algorithms (Flood Fill)
- The
openCells()
function is a practical and efficient application of recursion to implement the flood fill algorithm. It is essential for Minesweeper gameplay, revealing large areas of the board with a single click.
3. 2D Array Manipulation and Boundary Checking
- The use of
grid
andsgrid
demonstrates data manipulation in 2D structures. The inclusion of an invisible border (12x12
for a10x10
board) is a common technique to simplify neighbour checking logic, avoiding the need for multipleif
checks for the corners and edges of the actual board.
4. Event-Driven Programming
- The main SFML loop is a classic example of event-driven programming, where the programme reacts to user interactions and system events rather than following a linear flow.
5. Simple Procedural Generation
- The random placement of mines in
resetGame()
is a basic form of procedural content generation, where game elements are created algorithmically at runtime.
This Minesweeper project serves as an excellent case study for understanding the application of fundamental algorithms, design patterns, and rendering techniques in a game development context.
Top comments (0)