Update World API and implementation: rename size fields to sizeX_/sizeY_/sizeZ_, add helpers such as mineAllPositive, positiveAverage, topPositiveSum and a sort helper, and fix related surface/mine semantics for dynamic columns.
26 KiB
SDL2 GUI Extension Guide: Parallel Deep Miner
Scope: optional SDL2/SDL2_ttf renderer for the parallel Deep Miner project. This guide assumes the core implementation from
PARALLEL_DEEP_MINER_GUIDE.md.The GUI is intentionally separated from the terminal/threading guide so the base project remains buildable without SDL2.
Table of Contents
- Purpose
- Design Goals
- Prerequisites
- Renderer Architecture
- Renderer Public Interface
- Implementation Notes
- Integrating Renderer with Game
- GUI CMake Target
- GUI Entry Point
- Common Pitfalls
- Appendix A: Complete GUI Source Code
1. Purpose
The SDL2 renderer provides a live window beside the terminal output. It visualizes:
- the 5×5 grid
- surface values and effects
- column depth bars
- all 5–10 robots, not just two robots
- live HP and scores
- recent game messages
- optional step-by-step or auto-advance playback
This guide replaces the old two-robot GUI design that referenced player_, computer_, autoMode_, and play(Robot&, bool). The parallel architecture uses std::vector<std::unique_ptr<Robot>>, robotLoop(idx), and a complete-turn mutex.
2. Design Goals
- Optional dependency: the terminal build must not require SDL2 headers or libraries.
- Parallel-compatible: rendering works with the robot vector and does not assume exactly two robots.
- No ownership confusion:
Gameowns the robots;Rendereronly observes them. - Stable snapshots: render calls happen while the game state is stable.
- Simple build split:
deep_minerremains terminal-only;deep_miner_guilinks SDL2.
3. Prerequisites
Arch Linux
sudo pacman -S sdl2 sdl2_ttf
Ubuntu / Debian
sudo apt-get install libsdl2-dev libsdl2-ttf-dev
Fedora
sudo dnf install SDL2-devel SDL2_ttf-devel
The code can probe common system font paths at runtime. Do not bundle font files unless your project explicitly permits it.
4. Renderer Architecture
Use the pimpl idiom so SDL2 headers stay out of normal project headers.
include/Renderer.h src/Renderer.cpp
------------------ ----------------
class Renderer { #include <SDL2/SDL.h>
struct Impl; #include <SDL2/SDL_ttf.h>
std::unique_ptr<Impl> impl_; struct Renderer::Impl { ... };
};
Only src/Renderer.cpp includes SDL2 headers. Game.h can forward-declare Renderer.
5. Renderer Public Interface
// include/Renderer.h
#pragma once
#include <memory>
#include <string>
#include <vector>
class World;
class Robot;
class Renderer {
public:
Renderer();
~Renderer();
Renderer(const Renderer&) = delete;
Renderer& operator=(const Renderer&) = delete;
bool init();
void render(const World& world,
const std::vector<const Robot*>& robots,
int round,
const std::vector<std::string>& log);
bool waitForStep();
void setAutoAdvanceMs(int ms);
bool isOpen() const;
private:
struct Impl;
std::unique_ptr<Impl> impl_;
};
Why pass std::vector<const Robot*>?
Game owns robots as std::unique_ptr<Robot>. The renderer should not know or care about ownership. A vector of raw const Robot* is a lightweight read-only view.
6. Implementation Notes
Layout
A practical 900×680 window layout:
+-------------------------------+----------------------------+
| grid title / round | scores and HP |
| | |
| 5×5 grid | legend |
| each cell shows surface value | message log |
| and robot markers | controls |
+-------------------------------+----------------------------+
Recommended constants:
static constexpr int WIN_W = 900;
static constexpr int WIN_H = 680;
static constexpr int HDR_H = 56;
static constexpr int GRID_OFF_X = 32;
static constexpr int GRID_OFF_Y = 82;
static constexpr int CELL_W = 96;
static constexpr int CELL_H = 86;
static constexpr int PANEL_X = 536;
static constexpr int PANEL_PAD = 10;
Drawing multiple robots in one cell
A single cell may contain several robots. Draw small indexed markers instead of assuming one P and one C.
std::vector<int> robotsInCell;
for (int i = 0; i < static_cast<int>(robots.size()); ++i) {
if (robots[i]->isAlive()
&& robots[i]->getX() == x
&& robots[i]->getY() == y) {
robotsInCell.push_back(i);
}
}
for (int k = 0; k < static_cast<int>(robotsInCell.size()); ++k) {
const int robotIndex = robotsInCell[k];
const int cx = cellX + 18 + (k % 3) * 22;
const int cy = cellY + 20 + (k / 3) * 22;
fillCircle(cx, cy, 9, robotColor(robotIndex));
drawText(std::to_string(robotIndex + 1), cx - 4, cy - 7, white, false);
}
Controls
| Key / action | Effect |
|---|---|
SPACE / ENTER |
advance immediately |
| mouse click | advance immediately |
ESC |
close the GUI window |
| window close button | close the GUI window |
| auto-advance | continue after configured milliseconds |
waitForStep()
bool Renderer::waitForStep() {
const Uint32 start = SDL_GetTicks();
while (impl_->open) {
SDL_Event event;
while (SDL_PollEvent(&event)) {
if (event.type == SDL_QUIT) {
impl_->open = false;
return false;
}
if (event.type == SDL_KEYDOWN) {
const auto key = event.key.keysym.sym;
if (key == SDLK_ESCAPE) {
impl_->open = false;
return false;
}
if (key == SDLK_SPACE || key == SDLK_RETURN) {
return true;
}
}
if (event.type == SDL_MOUSEBUTTONDOWN) {
return true;
}
}
if (impl_->autoAdvanceMs > 0
&& static_cast<int>(SDL_GetTicks() - start) >= impl_->autoAdvanceMs) {
return true;
}
SDL_Delay(8);
}
return false;
}
7. Integrating Renderer with Game
Game.h additions
// include/Game.h
class Renderer;
class Game {
public:
Game();
void run();
#ifdef WITH_SDL2_GUI
void setRenderer(Renderer* renderer) { renderer_ = renderer; }
#endif
private:
// existing members
#ifdef WITH_SDL2_GUI
Renderer* renderer_ = nullptr; // non-owning
std::vector<std::string> msgLog_; // capped message buffer
int currentRound_ = 0;
void pushMsg(const std::string& msg);
void renderFrameLocked();
#endif
};
The pointer is non-owning. main_gui.cpp creates the renderer and passes its address to Game.
Game.cpp includes
#ifdef WITH_SDL2_GUI
#include "../include/Renderer.h"
#endif
Message logging
void Game::log(const std::string& msg) {
std::cout << msg << '\n';
#ifdef WITH_SDL2_GUI
if (renderer_) {
msgLog_.push_back(msg);
if (msgLog_.size() > 120) {
msgLog_.erase(msgLog_.begin());
}
}
#endif
}
If you prefer to keep log() terminal-only, use a separate pushMsg() helper. The important point is that the GUI log must be updated from the same serialized turn path.
Build a read-only robot view
#ifdef WITH_SDL2_GUI
std::vector<const Robot*> Game::robotView() const {
std::vector<const Robot*> view;
view.reserve(robots_.size());
for (const auto& robot : robots_) {
view.push_back(robot.get());
}
return view;
}
#endif
If you add this helper, declare it in Game.h under the same #ifdef.
renderFrameLocked()
#ifdef WITH_SDL2_GUI
void Game::renderFrameLocked() {
if (!renderer_ || !renderer_->isOpen()) return;
std::vector<const Robot*> view;
view.reserve(robots_.size());
for (const auto& robot : robots_) {
view.push_back(robot.get());
}
renderer_->render(world_, view, currentRound_, msgLog_);
renderer_->waitForStep();
}
#endif
The name renderFrameLocked() is deliberate: call it only while turnMutex_ is held. This guarantees the renderer sees a consistent snapshot. In GUI step mode, the game intentionally pauses while the user views the frame.
Calling the renderer from robotLoop()
void Game::robotLoop(int idx) {
ScopedTimer threadTimer(threadTimes_[idx]);
Robot& robot = *robots_[idx];
while (true) {
{
std::lock_guard<std::mutex> lk(turnMutex_);
if (!robot.isAlive()) {
recordDeath(idx, "killed earlier");
#ifdef WITH_SDL2_GUI
renderFrameLocked();
#endif
break;
}
if (isGameOver()) {
break;
}
++currentRound_;
const int dir = robot.decideNextMove(world_);
robot.move(dir, world_);
const int effect = world_.checkEffects(robot.getX(), robot.getY());
if (effect < 0) applyEffect(robot, effect);
if (!robot.isAlive()) {
recordDeath(idx, "killed by effect");
#ifdef WITH_SDL2_GUI
renderFrameLocked();
#endif
break;
}
fightNearby(robot);
if (effect != -1 && robot.isAlive()) {
const int mined = robot.mine(world_);
log(robot.getName() + " mined " + std::to_string(mined) + " points.");
}
checkRearrange(robot);
#ifdef WITH_SDL2_GUI
renderFrameLocked();
#endif
}
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
}
This preserves the base thread model. The GUI mode slows the simulation because it pauses after turns, but it does not reintroduce player_, computer_, or a sequential play() loop.
Initial and final frames
Render once after setup and once after all threads join. Both should happen while no robot thread is mutating the game state.
void Game::run() {
setup();
#ifdef WITH_SDL2_GUI
{
std::lock_guard<std::mutex> lk(turnMutex_);
renderFrameLocked();
}
#endif
// launch and join threads
#ifdef WITH_SDL2_GUI
if (renderer_ && renderer_->isOpen()) {
renderer_->setAutoAdvanceMs(0);
std::lock_guard<std::mutex> lk(turnMutex_);
log("=== GAME OVER ===");
renderFrameLocked();
}
#endif
}
8. GUI CMake Target
Keep the terminal target independent from SDL2.
option(BUILD_GUI "Build SDL2 GUI target" OFF)
if (BUILD_GUI)
find_package(SDL2 REQUIRED)
find_package(SDL2_ttf REQUIRED)
add_executable(deep_miner_gui
${SOURCES}
src/Renderer.cpp
main_gui.cpp
)
target_compile_definitions(deep_miner_gui PRIVATE WITH_SDL2_GUI)
target_include_directories(deep_miner_gui PRIVATE ${SDL2_INCLUDE_DIRS})
target_link_libraries(deep_miner_gui
Threads::Threads
SDL2::SDL2
SDL2_ttf::SDL2_ttf
)
endif()
Depending on your platform's CMake package files, the imported targets may be named differently. If SDL2::SDL2 or SDL2_ttf::SDL2_ttf is unavailable, use the variables exported by your SDL2 CMake package.
Build:
mkdir build
cd build
cmake -DBUILD_GUI=ON ..
cmake --build .
./deep_miner_gui
9. GUI Entry Point
// main_gui.cpp
#include "include/Game.h"
#include "include/Renderer.h"
#include <exception>
#include <iostream>
int main() {
try {
Renderer renderer;
if (!renderer.init()) {
std::cerr << "Could not initialise SDL2 renderer.\n";
return 1;
}
renderer.setAutoAdvanceMs(900);
Game game;
game.setRenderer(&renderer);
game.run();
return 0;
} catch (const std::exception& e) {
std::cerr << "Fatal error: " << e.what() << "\n";
return 1;
}
}
The normal terminal main.cpp remains unchanged and does not include Renderer.h.
10. Common Pitfalls
Pitfall 1: Including SDL2 in Game.h
Do not include <SDL2/SDL.h> in Game.h. Forward-declare Renderer and include SDL2 only in Renderer.cpp.
Pitfall 2: Rendering two hard-coded robots
Do not use player_, computer_, P, or C as architectural concepts. The game has 5–10 robots. Render indexed markers or abbreviated robot names.
Pitfall 3: Waiting for input without a stable snapshot
If the renderer waits while other threads mutate the world, the displayed state can become inconsistent. The simple solution is to call renderFrameLocked() while turnMutex_ is held. For a more advanced solution, copy a lightweight snapshot under the lock and let the renderer wait after the lock is released.
Pitfall 4: Linking SDL2 into the terminal target
Only deep_miner_gui should link SDL2. The base deep_miner target should link only Threads::Threads.
Pitfall 5: GUI timing changes thread measurements
Step mode and auto-advance delays are part of the measured runtime if you call the renderer inside robotLoop(). This is acceptable for visualization. For benchmark runs, use the terminal target.
Minimal Project Additions
deep_miner/
|-- main_gui.cpp
|-- include/
| `-- Renderer.h
`-- src/
`-- Renderer.cpp
The GUI guide deliberately stays separate from the base programming guide. The core project remains a clean terminal-based C++17 threading assignment, while this extension adds visualization without changing the ownership model or robot architecture.
Appendix A: Complete GUI Source Code
This appendix contains the complete GUI-specific files plus the small Game integration points required by the renderer. The core project files remain in PARALLEL_DEEP_MINER_GUIDE.md.
main_gui.cpp
#include "Game.h"
#include "Renderer.h"
#include <exception>
#include <iostream>
int main() {
try {
Renderer renderer;
if (!renderer.init()) {
std::cerr << "Could not initialise SDL2 renderer.\n";
return 1;
}
renderer.setAutoAdvanceMs(900);
Game game;
game.setRenderer(&renderer);
game.run();
return 0;
} catch (const std::exception& e) {
std::cerr << "Fatal error: " << e.what() << "\n";
return 1;
}
}
include/Renderer.h
#pragma once
#include <memory>
#include <string>
#include <vector>
class World;
class Robot;
class Renderer {
public:
Renderer();
~Renderer();
bool init();
void render(const World& world,
const std::vector<Robot*>& robots,
int round,
const std::vector<std::string>& log);
bool waitForStep();
void setAutoAdvanceMs(int ms);
bool isOpen() const;
private:
struct Impl;
std::unique_ptr<Impl> impl_;
};
src/Renderer.cpp
#include "Renderer.h"
#include "Robot.h"
#include "World.h"
#include <SDL2/SDL.h>
#include <SDL2/SDL_ttf.h>
#include <algorithm>
#include <cmath>
#include <sstream>
#include <string>
#include <vector>
namespace {
SDL_Color rgba(Uint8 r, Uint8 g, Uint8 b, Uint8 a = 255) { return SDL_Color{r, g, b, a}; }
}
struct Renderer::Impl {
SDL_Window* window = nullptr;
SDL_Renderer* sdl = nullptr;
TTF_Font* fontLg = nullptr;
TTF_Font* fontSm = nullptr;
bool open = false;
int autoAdvanceMs = 900;
static constexpr int WIN_W = 920;
static constexpr int WIN_H = 700;
static constexpr int GRID_X = 32;
static constexpr int GRID_Y = 84;
static constexpr int CELL_W = 92;
static constexpr int CELL_H = 82;
static constexpr int PANEL_X = 535;
bool init() {
if (SDL_Init(SDL_INIT_VIDEO) != 0) return false;
if (TTF_Init() != 0) return false;
window = SDL_CreateWindow("Parallel Deep Miner",
SDL_WINDOWPOS_CENTERED,
SDL_WINDOWPOS_CENTERED,
WIN_W, WIN_H,
SDL_WINDOW_SHOWN);
if (!window) return false;
sdl = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC);
if (!sdl) return false;
const char* candidates[] = {
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
"/usr/share/fonts/dejavu/DejaVuSans.ttf",
"/Library/Fonts/Arial.ttf",
"C:/Windows/Fonts/arial.ttf"
};
for (const char* path : candidates) {
if (!fontLg) fontLg = TTF_OpenFont(path, 20);
if (!fontSm) fontSm = TTF_OpenFont(path, 13);
if (fontLg && fontSm) break;
}
open = true;
return true;
}
void shutdown() {
if (fontLg) TTF_CloseFont(fontLg);
if (fontSm) TTF_CloseFont(fontSm);
if (sdl) SDL_DestroyRenderer(sdl);
if (window) SDL_DestroyWindow(window);
fontLg = nullptr;
fontSm = nullptr;
sdl = nullptr;
window = nullptr;
TTF_Quit();
SDL_Quit();
}
void setColor(SDL_Color c) {
SDL_SetRenderDrawColor(sdl, c.r, c.g, c.b, c.a);
SDL_SetRenderDrawBlendMode(sdl, c.a < 255 ? SDL_BLENDMODE_BLEND : SDL_BLENDMODE_NONE);
}
void fillRect(int x, int y, int w, int h, SDL_Color c) {
setColor(c);
SDL_Rect r{x, y, w, h};
SDL_RenderFillRect(sdl, &r);
}
void strokeRect(int x, int y, int w, int h, SDL_Color c) {
setColor(c);
SDL_Rect r{x, y, w, h};
SDL_RenderDrawRect(sdl, &r);
}
void fillCircle(int cx, int cy, int radius, SDL_Color c) {
setColor(c);
for (int dy = -radius; dy <= radius; ++dy) {
int dx = static_cast<int>(std::sqrt(radius * radius - dy * dy));
SDL_RenderDrawLine(sdl, cx - dx, cy + dy, cx + dx, cy + dy);
}
}
void drawText(const std::string& text, int x, int y, SDL_Color color, bool large = false) {
TTF_Font* font = large ? fontLg : fontSm;
if (!font || text.empty()) return;
SDL_Surface* surface = TTF_RenderUTF8_Blended(font, text.c_str(), color);
if (!surface) return;
SDL_Texture* texture = SDL_CreateTextureFromSurface(sdl, surface);
SDL_Rect dst{x, y, surface->w, surface->h};
SDL_FreeSurface(surface);
if (!texture) return;
SDL_RenderCopy(sdl, texture, nullptr, &dst);
SDL_DestroyTexture(texture);
}
SDL_Color cellColor(int value) const {
if (value == 0) return rgba(28, 31, 35);
if (value == -1) return rgba(176, 96, 28);
if (value == -2) return rgba(88, 55, 140);
if (value == -3) return rgba(170, 40, 55);
int clamped = std::clamp(value, 1, 9);
Uint8 g = static_cast<Uint8>(70 + clamped * 15);
Uint8 r = static_cast<Uint8>(20 + clamped * 22);
return rgba(r, g, 55);
}
void drawGrid(const World& world, const std::vector<Robot*>& robots) {
for (int y = 0; y < world.getSizeY(); ++y) {
for (int x = 0; x < world.getSizeX(); ++x) {
int px = GRID_X + x * CELL_W;
int py = GRID_Y + y * CELL_H;
int value = world.getSurfaceValue(x, y);
fillRect(px, py, CELL_W - 8, CELL_H - 8, cellColor(value));
strokeRect(px, py, CELL_W - 8, CELL_H - 8, rgba(230, 230, 230, 90));
std::string label = value == 0 ? "--" : std::to_string(value);
drawText(label, px + 32, py + 24, rgba(245, 245, 245), true);
int depth = world.getSurfaceLevel(x, y) + 1;
int barW = static_cast<int>((CELL_W - 12) * (static_cast<double>(depth) / world.getSizeZ()));
fillRect(px, py + CELL_H - 15, barW, 5, rgba(240, 240, 240, 150));
}
}
for (std::size_t i = 0; i < robots.size(); ++i) {
Robot* r = robots[i];
if (!r || !r->isAlive()) continue;
int px = GRID_X + r->getX() * CELL_W + 18 + static_cast<int>((i % 3) * 18);
int py = GRID_Y + r->getY() * CELL_H + 14 + static_cast<int>((i / 3) * 14);
fillCircle(px, py, 10, rgba(80, 190, 240));
drawText(std::to_string(i + 1), px - 4, py - 8, rgba(10, 10, 10));
}
}
void drawPanel(const std::vector<Robot*>& robots, int round, const std::vector<std::string>& log) {
fillRect(PANEL_X, 0, WIN_W - PANEL_X, WIN_H, rgba(20, 22, 26));
drawText("Round " + std::to_string(round), PANEL_X + 18, 24, rgba(245, 245, 245), true);
drawText("Robots", PANEL_X + 18, 70, rgba(230, 230, 230), true);
int y = 105;
for (std::size_t i = 0; i < robots.size(); ++i) {
Robot* r = robots[i];
if (!r) continue;
std::ostringstream oss;
oss << (i + 1) << ". " << r->getName() << " score=" << r->getScore() << " hp=" << r->getHp();
drawText(oss.str(), PANEL_X + 18, y, r->isAlive() ? rgba(235, 235, 235) : rgba(160, 160, 160));
y += 22;
}
y += 18;
drawText("Messages", PANEL_X + 18, y, rgba(230, 230, 230), true);
y += 34;
int start = std::max(0, static_cast<int>(log.size()) - 15);
for (int i = start; i < static_cast<int>(log.size()); ++i) {
drawText(log[i], PANEL_X + 18, y, rgba(210, 210, 210));
y += 20;
}
}
void render(const World& world, const std::vector<Robot*>& robots, int round, const std::vector<std::string>& log) {
if (!open) return;
setColor(rgba(12, 14, 18));
SDL_RenderClear(sdl);
drawText("DEEP MINER", 32, 24, rgba(245, 245, 245), true);
drawGrid(world, robots);
drawPanel(robots, round, log);
drawText("SPACE/click: step ESC/window close: quit", 32, WIN_H - 36, rgba(210, 210, 210));
SDL_RenderPresent(sdl);
}
bool waitForStep() {
if (!open) return false;
Uint32 start = SDL_GetTicks();
SDL_Event event;
while (open) {
while (SDL_PollEvent(&event)) {
if (event.type == SDL_QUIT) { open = false; return false; }
if (event.type == SDL_KEYDOWN) {
if (event.key.keysym.sym == SDLK_ESCAPE) { open = false; return false; }
if (event.key.keysym.sym == SDLK_SPACE || event.key.keysym.sym == SDLK_RETURN) return true;
}
if (event.type == SDL_MOUSEBUTTONDOWN) return true;
}
if (autoAdvanceMs > 0 && SDL_GetTicks() - start >= static_cast<Uint32>(autoAdvanceMs)) return true;
SDL_Delay(10);
}
return false;
}
};
Renderer::Renderer() : impl_(std::make_unique<Impl>()) {}
Renderer::~Renderer() { impl_->shutdown(); }
bool Renderer::init() { return impl_->init(); }
void Renderer::render(const World& world, const std::vector<Robot*>& robots, int round, const std::vector<std::string>& log) { impl_->render(world, robots, round, log); }
bool Renderer::waitForStep() { return impl_->waitForStep(); }
void Renderer::setAutoAdvanceMs(int ms) { impl_->autoAdvanceMs = ms; }
bool Renderer::isOpen() const { return impl_->open; }
CMakeLists.txt GUI additions
option(BUILD_GUI "Build SDL2 GUI target" OFF)
if (BUILD_GUI)
find_package(SDL2 REQUIRED)
find_package(SDL2_ttf REQUIRED)
add_executable(deep_miner_gui
${SOURCES}
src/Renderer.cpp
main_gui.cpp
)
target_compile_definitions(deep_miner_gui PRIVATE WITH_SDL2_GUI)
target_include_directories(deep_miner_gui PRIVATE ${SDL2_INCLUDE_DIRS})
target_link_libraries(deep_miner_gui
Threads::Threads
SDL2::SDL2
SDL2_ttf::SDL2_ttf
)
endif()
include/Game.h GUI integration additions
Add a forward declaration before the Game class:
class Renderer;
Add this public method:
#ifdef WITH_SDL2_GUI
void setRenderer(Renderer* renderer);
#endif
Add these private members and helper declarations:
#ifdef WITH_SDL2_GUI
Renderer* renderer_ = nullptr;
std::vector<std::string> guiLog_;
void renderFrameLocked();
std::vector<Robot*> robotPointers() const;
#endif
src/Game.cpp GUI integration additions
Include the renderer only for the GUI build:
#ifdef WITH_SDL2_GUI
#include "Renderer.h"
#endif
Add these methods:
#ifdef WITH_SDL2_GUI
void Game::setRenderer(Renderer* renderer) {
renderer_ = renderer;
}
std::vector<Robot*> Game::robotPointers() const {
std::vector<Robot*> out;
out.reserve(robots_.size());
for (const auto& robot : robots_) out.push_back(robot.get());
return out;
}
void Game::renderFrameLocked() {
if (!renderer_ || !renderer_->isOpen()) return;
renderer_->render(world_, robotPointers(), round_, guiLog_);
renderer_->waitForStep();
}
#endif
Modify Game::log so GUI messages are recorded while terminal output remains unchanged:
void Game::log(const std::string& message) const {
std::cout << message << "\n";
#ifdef WITH_SDL2_GUI
auto* self = const_cast<Game*>(this);
self->guiLog_.push_back(message);
if (self->guiLog_.size() > 120) self->guiLog_.erase(self->guiLog_.begin());
#endif
}
Call renderFrameLocked() after each atomic turn, before releasing the mutex:
// near the end of Game::robotLoop(), still inside the lock_guard scope
checkRearrange(robot);
#ifdef WITH_SDL2_GUI
renderFrameLocked();
#endif
Render an initial and final frame in Game::run():
#ifdef WITH_SDL2_GUI
{
std::lock_guard<std::mutex> lock(turnMutex_);
renderFrameLocked();
}
#endif
// launch and join threads here
#ifdef WITH_SDL2_GUI
if (renderer_ && renderer_->isOpen()) {
renderer_->setAutoAdvanceMs(0);
std::lock_guard<std::mutex> lock(turnMutex_);
guiLog_.push_back("=== GAME OVER ===");
renderFrameLocked();
}
#endif