DEV Community

Cover image for Building Minesweeper in C++
Mr Punk da Silva
Mr Punk da Silva

Posted on • Edited on

Building Minesweeper in C++

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 to 8: 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.
  • 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 to 9: 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
Enter fullscreen mode Exit fullscreen mode

Coordinate System

The game operates with two coordinate systems:

  1. 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).
  2. 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);
Enter fullscreen mode Exit fullscreen mode

Core Mechanics and Algorithms

resetGame() - Initialisation and Board Reconfiguration

This function is called at the start of each new game. Its algorithm is:

  1. Initialisation of sgrid: All visible cells are set to 10 (covered).
  2. Initialisation of grid: All logical cells are set to 0 (no mines).
  3. 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 at grid[i][j] is set to 9 (mine).
  4. 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 to grid[i][j]. The invisible edge (0 and 11) 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;
             }
          }
}
Enter fullscreen mode Exit fullscreen mode
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]
Enter fullscreen mode Exit fullscreen mode

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:

  1. 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.
  2. Reveal: The cell sgrid[i][j] is updated to the corresponding value in grid[i][j], making it visible.
  3. 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);
                    }
                }
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode

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 the PLAYING state and starts a new game via resetGame().
  • GAME_OVER / GAME_WON States: Any left click restarts the game, transitioning to PLAYING and calling resetGame().
  • PLAYING State:
    • Left Click: If the clicked cell is covered (sgrid[x][y] == 10):
  • If grid[x][y] == 9 (mine): The state changes to GAME_OVER. All mines are revealed in sgrid. - If grid[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.

Win and Lose Conditions

  • Lose: Detected immediately when a left click reveals a mine (grid[x][y] == 9). The currentGameState is set to GAME_OVER.
  • Victory: Checked every frame in the PLAYING state. The game is won if:
  • The number of covered cells (sgrid[i][j] == 10 or sgrid[i][j] == 11) is equal to the total number of mines on the board.

    1. 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 to GAME_WON.


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 to 8: 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, but setPosition() and getGlobalBounds() 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
Enter fullscreen mode Exit fullscreen mode

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 and sgrid demonstrates data manipulation in 2D structures. The inclusion of an invisible border (12x12 for a 10x10 board) is a common technique to simplify neighbour checking logic, avoiding the need for multiple if 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)