Creating the Grid
Building a two-dimensional grid of interactive minesweeper cells
In this lesson, we'll focus on creating the foundational structure for our Minesweeper game. We'll implement the grid system and individual cells, setting the stage for bomb placement and game logic in future lessons.
We'll start by setting up the necessary parameters in our global configuration, then move on to creating the MinesweeperCell
and MinesweeperGrid
classes.
Finally, we'll implement basic interaction functionality, allowing players to clear cells and prepare our system for future bomb placement.
Updating Globals
We'll begin by updating our Globals.h
file with new variables that will define the structure and appearance of our game:
GRID_COLUMNS
andGRID_ROWS
determine the number of cells in each dimension. We're creating an 8-column by 4-row grid in this example, but the project can easily adapt to different grid sizes by adjusting these values.PADDING
represents the visual spacing between elements in our UI.CELL_SIZE
represents the width and height of each cell in pixels. In this case, each cell will be 50x50 pixels
We'll also add GRID_HEIGHT
and GRID_WIDTH
variables to calculate the visual size of our grid based on these options.
Finally, we'll update the WINDOW_HEIGHT
and WINDOW_WIDTH
variables to be large enough to contain our grid, with additional padding around the edge.
These variables will provide a flexible foundation for our game, allowing easy adjustments to the game's appearance and structure.
// Globals.h
// ...
namespace Config{
inline const std::string GAME_NAME{
"Minesweeper"};
inline constexpr int GRID_COLUMNS{8};
inline constexpr int GRID_ROWS{4};
// Size and Positioning
inline constexpr int PADDING{5};
inline constexpr int CELL_SIZE{50};
inline constexpr int GRID_HEIGHT{
CELL_SIZE * GRID_ROWS
+ PADDING * (GRID_ROWS - 1)
};
inline constexpr int GRID_WIDTH{
CELL_SIZE * GRID_COLUMNS +
PADDING * (GRID_COLUMNS - 1)};
inline constexpr int WINDOW_HEIGHT{
GRID_HEIGHT + PADDING * 2
};
inline constexpr int WINDOW_WIDTH{
GRID_WIDTH + PADDING * 2
};
// ...
}
// ...
Creating Cells
Now, we'll create a class for our Minesweeper Cell. Here are some key points about the implementation:
- We expect this class to become quite large, so we'll preemptively separate it into a header (.h) and implementation (.cpp) file.
- The cell inherits from
Engine::Button
, which in turn inherits fromEngine::Rectangle
. As such, our constructor will acceptx
andy
arguments to control the position of the cell, andw
andh
arguments to control the size. - We'll also accept
Row
andCol
arguments, so the cell knows where it is within the grid. - We'll override the HandleEvent and Render methods. For now, they just call the base implementation on the
Engine::Button
class, but we'll expand them soon.
This structure allows our cells to behave like buttons while also maintaining their position within the Minesweeper grid.
// Minesweeper/Cell.h
#pragma once
#include "Engine/Button.h"
class MinesweeperCell : public Engine::Button {
public:
MinesweeperCell(
int X, int Y, int W, int H, int Row, int Col
);
void HandleEvent(const SDL_Event& E) override;
void Render(SDL_Surface* Surface) override;
[[nodiscard]]
int GetRow() const{ return Row; }
[[nodiscard]]
int GetCol() const{ return Col; }
private:
int Row;
int Col;
};
// Minesweeper/Cell.cpp
#include <iostream>
#include "Minesweeper/Cell.h"
#include "Globals.h"
MinesweeperCell::MinesweeperCell(
int x, int y, int w, int h, int Row, int Col)
: Button{x, y, w, h}, Row{Row}, Col{Col} {};
void MinesweeperCell::HandleEvent(
const SDL_Event& E){
Button::HandleEvent(E);
}
void MinesweeperCell::Render(
SDL_Surface* Surface){
Button::Render(Surface);
}
The Grid Class
Next, we'll implement our MinesweeperGrid
class. Here are the important aspects of this class:
- The constructor accepts
x
andy
arguments, controlling where the grid should be drawn on the surface. - The constructor creates a two-dimensional grid of
MinesweeperCell
objects, based on the configuration options in ourGlobals.h
file. - It stores all these cells in a
std::vector
calledChildren
. - The
x
,y
,w
,h
,row
, andcol
arguments for each invocation of theMinesweeperCell
constructor are being calculated in the loop body. This calculation ensures that each cell is positioned correctly within the grid, taking into account the cell size and padding. - It has public
Render()
andHandleEvent()
methods. These methods iterate over all theMinesweeperCell
objects in the grid, and call theirRender()
andHandleEvent()
methods respectively.
This grid class will manage the collection of cells and handle the rendering and event distribution for the entire Minesweeper board.
// Minesweeper/Grid.h
#pragma once
#include <vector>
#include "Globals.h"
#include "Minesweeper/Cell.h"
class MinesweeperGrid {
public:
MinesweeperGrid(int x, int y){
using namespace Config;
Children.reserve(GRID_COLUMNS * GRID_ROWS);
for (int Col{1}; Col <= GRID_COLUMNS; ++Col) {
for (int Row{1}; Row <= GRID_ROWS; ++Row) {
constexpr int Spacing{CELL_SIZE + PADDING};
Children.emplace_back(
x + (Spacing) * (Col - 1),
y + (Spacing) * (Row - 1),
CELL_SIZE, CELL_SIZE, Row, Col
);
}
}
}
void Render(SDL_Surface* Surface){
for (auto& Child : Children) {
Child.Render(Surface);
}
}
void HandleEvent(const SDL_Event& E){
for (auto& Child : Children) {
Child.HandleEvent(E);
}
}
std::vector<MinesweeperCell> Children;
};
Passing Through Events
We'll now update our MinesweeperUI
class to construct the MinesweeperGrid
. Here's what you need to know:
- The
MinesweeperUI
hasRender()
andHandleEvent()
methods, which are being called from the main event loop. - The class is now forwarding those calls to the
MinesweeperGrid
component. - As we saw above, the
MinesweeperGrid
component is then forwarding thoseHandleEvent
andRender()
calls to all of itsChildren
- i.e., theMinesweeperCell
objects. - The
MinesweeperGrid
is being constructed by passing thePadding
value from our config, causing theGrid
to be drawn slightly offset from the top left edge of the window surface.
This structure ensures that events are properly distributed throughout our game components, and that everything is rendered in the correct position.
// Minesweeper/UI.h
#pragma once
#include "Globals.h"
#include "Minesweeper/Grid.h"
class MinesweeperUI {
public:
void Render(SDL_Surface* Surface){
Grid.Render(Surface);
}
void HandleEvent(const SDL_Event& E){
Grid.HandleEvent(E);
}
private:
MinesweeperGrid Grid{
Config::PADDING, Config::PADDING
};
};
Running our program, we should now see a grid of buttons, responding to our cursor:

Clearing Cells
Now we'll expand our MinesweeperCell
objects to let users "clear" them by left-clicking. We'll implement this using a ClearCell()
method and an isCleared
member variable. Our Engine::Button
class has a virtual HandleLeftClick()
method which we can override.
// Minesweeper/Cell.h
#pragma once
// ...
class MinesweeperCell : public Engine::Button {
public:
// ...
protected:
void HandleLeftClick() override;
private:
void ClearCell();
bool isCleared{false};
// ...
};
In the implementation of these functions:
SetIsDisabled()
is inherited fromEngine::Button
, and will cause our object to stop reacting to mouse events.SetColor
is inherited fromEngine::Rectangle
(viaEngine::Button
) and will change the color of our button. We change the color to a new value that we'll add to theConfig
namespace in ourGlobals.h
.
This implementation allows users to interact with our cells, changing their state when clicked.
// Minesweeper/Cell.cpp
// ...
void MinesweeperCell::ClearCell(){
if (isCleared) return;
isCleared = true;
SetIsDisabled(true);
SetColor(Config::BUTTON_CLEARED_COLOR);
}
void MinesweeperCell::HandleLeftClick(){
ClearCell();
}
// ...
// Globals.h
// ...
namespace Config{
// ...
inline constexpr SDL_Color BUTTON_CLEARED_COLOR{
240, 240, 240, 255};
// ...
}
// ...
Reporting Cell Cleared
A cell being cleared is an important action in the context of the wider game - for example, it can trigger a game-over state if the cell contains a mine, and adjacent cells may also need to react. To implement the gameplay, we'll need to notify other components every time a cell is cleared, so we'll use the SDL event loop.
We'll add our first custom event type to our UserEvents
namespace:
// Globals.h
// ...
namespace UserEvents{
inline Uint32 CELL_CLEARED =
SDL_RegisterEvents(1);
}
// ...
As we develop our game, cells will need to report various events. To streamline this process, let's create a ReportEvent()
helper method.
This method will accept an event type parameter, which SDL represents as a 32-bit unsigned integer. Here's how we'll define it in our MinesweeperCell
class:
// Minesweeper/Cell.h
// ...
#pragma once
// ...
class MinesweeperCell : public Engine::Button {
public:
// ...
protected:
// ...
private:
void ReportEvent(uint32_t EventType);
// ...
};
In the implementation, we'll create an event of that type. We'll also attach a pointer to ourselves using the this
keyword, so consumers of the event can understand which cell was cleared. We'll finally dispatch the event into the queue using SDL_PushEvent()
:
// Minesweeper/Cell.cpp
// ...
void MinesweeperCell::ReportEvent(
uint32_t EventType){
SDL_Event event{EventType};
event.user.data1 = this;
SDL_PushEvent(&event);
}
// ...
Finally, we'll update our ClearCell()
implementation to call our ReportEvent()
function, passing the correct type.
We'll also update HandleEvent()
to react to a cell being cleared. For now, we'll just log out a message, but we'll build upon this later:
// Minesweeper/Cell.cpp
// ...
void MinesweeperCell::ClearCell(){
if (isCleared) return;
isCleared = true;
SetIsDisabled(true);
SetColor(Config::BUTTON_CLEARED_COLOR);
ReportEvent(UserEvents::CELL_CLEARED);
}
void MinesweeperCell::HandleEvent(
const SDL_Event& E){
if (E.type == UserEvents::CELL_CLEARED) {
// TODO
std::cout << "A Cell Was Cleared\n";
}
Button::HandleEvent(E);
}
// ...
Testing
At this point, you should test the application to verify it's working correctly. You should see the grid as before, but now, left-clicking on a cell will clear it, causing it to change color and stop reacting to future mouse events.

Additionally, check the terminal to verify the event handling is working correctly. You should see "A Cell Was Cleared" logged out multiple times whenever you clear a cell.
A Cell Was Cleared
A Cell Was Cleared
A Cell Was Cleared
// ...
It's important to understand why multiple log messages appear for each cleared cell:
- When
ClearCell()
is called within any of our cells, anSDL_Event
is pushed into the queue. - The event loop (in
main.cpp
) receives this event and forwards it to theHandleEvent()
method of ourMinesweeperUI
object. - The
MinesweeperUI
forwards the event to theHandleEvent()
method of ourMinesweeperGrid
object. - The
MinesweeperGrid
forwards the event to theHandleEvent()
method of everyMinesweeperCell
object in itsChildren
array, allowing each cell to react to the event if necessary.
This is why "A Cell Was Cleared" is logged out multiple times. A single left click clears a single cell, but every cell is notified of the action and can react accordingly.
Complete Code
Complete versions of the files we changed in this part are available below
Files not listed above have not been changed since the previous section.
Summary
In this lesson, we've laid the groundwork for our Minesweeper game by implementing the grid system and individual cells. We've set up the global configuration, created classes for cells and the grid, and implemented basic interaction functionality.
Key progress in this lesson includes:
- Defining the game's structure in
Globals.h
- Creating the
MinesweeperCell
class with clearing functionality - Implementing the
MinesweeperGrid
class to manage all cells - Setting up event handling and propagation through the game components
- Implementing a custom event system for cell clearing
In the next lesson, we'll build upon these foundations and place bombs randomly within our grid. We'll implement the logic for bomb placement, update our cell-clearing functionality to handle bombs, and begin implementing the core game mechanics.
Adding Bombs to the Grid
Updating the game to to place bombs randomly in the grid and render them when cells are cleared.