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_COLUMNSandGRID_ROWSdetermine 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.PADDINGrepresents the visual spacing between elements in our UI.CELL_SIZErepresents 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 acceptxandyarguments to control the position of the cell, andwandharguments to control the size. - We'll also accept
RowandColarguments, 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::Buttonclass, 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 <SDL.h>
#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 <SDL.h>
#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
xandyarguments, controlling where the grid should be drawn on the surface. - The constructor creates a two-dimensional grid of
MinesweeperCellobjects, based on the configuration options in ourGlobals.hfile. - It stores all these cells in a
std::vectorcalledChildren. - The
x,y,w,h,row, andcolarguments for each invocation of theMinesweeperCellconstructor 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 theMinesweeperCellobjects 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 <SDL.h>
#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
MinesweeperUIhasRender()andHandleEvent()methods, which are being called from the main event loop. - The class is now forwarding those calls to the
MinesweeperGridcomponent. - As we saw above, the
MinesweeperGridcomponent is then forwarding thoseHandleEventandRender()calls to all of itsChildren- i.e., theMinesweeperCellobjects. - The
MinesweeperGridis being constructed by passing thePaddingvalue from our config, causing theGridto 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 <SDL.h>
#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.SetColoris 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 theConfignamespace 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_Eventis pushed into the queue. - The event loop (in
main.cpp) receives this event and forwards it to theHandleEvent()method of ourMinesweeperUIobject. - The
MinesweeperUIforwards the event to theHandleEvent()method of ourMinesweeperGridobject. - The
MinesweeperGridforwards the event to theHandleEvent()method of everyMinesweeperCellobject in itsChildrenarray, 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
MinesweeperCellclass with clearing functionality - Implementing the
MinesweeperGridclass 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.