Files
deepMiner/guide/SDL2_GUI_EXTENSION_GUIDE.md
fegger 2a79d42bd6 World updates
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.
2026-04-26 17:48:28 +02:00

966 lines
26 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
1. [Purpose](#1-purpose)
2. [Design Goals](#2-design-goals)
3. [Prerequisites](#3-prerequisites)
4. [Renderer Architecture](#4-renderer-architecture)
5. [Renderer Public Interface](#5-renderer-public-interface)
6. [Implementation Notes](#6-implementation-notes)
7. [Integrating Renderer with Game](#7-integrating-renderer-with-game)
8. [GUI CMake Target](#8-gui-cmake-target)
9. [GUI Entry Point](#9-gui-entry-point)
10. [Common Pitfalls](#10-common-pitfalls)
11. [Appendix A: Complete GUI Source Code](#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 510 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
1. **Optional dependency**: the terminal build must not require SDL2 headers or libraries.
2. **Parallel-compatible**: rendering works with the robot vector and does not assume exactly two robots.
3. **No ownership confusion**: `Game` owns the robots; `Renderer` only observes them.
4. **Stable snapshots**: render calls happen while the game state is stable.
5. **Simple build split**: `deep_miner` remains terminal-only; `deep_miner_gui` links SDL2.
---
## 3. Prerequisites
### Arch Linux
```bash
sudo pacman -S sdl2 sdl2_ttf
```
### Ubuntu / Debian
```bash
sudo apt-get install libsdl2-dev libsdl2-ttf-dev
```
### Fedora
```bash
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.
```text
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
```cpp
// 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:
```text
+-------------------------------+----------------------------+
| grid title / round | scores and HP |
| | |
| 5×5 grid | legend |
| each cell shows surface value | message log |
| and robot markers | controls |
+-------------------------------+----------------------------+
```
Recommended constants:
```cpp
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`.
```cpp
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()
```cpp
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
```cpp
// 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
```cpp
#ifdef WITH_SDL2_GUI
#include "../include/Renderer.h"
#endif
```
### Message logging
```cpp
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
```cpp
#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()
```cpp
#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()
```cpp
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.
```cpp
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.
```cmake
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:
```bash
mkdir build
cd build
cmake -DBUILD_GUI=ON ..
cmake --build .
./deep_miner_gui
```
---
## 9. GUI Entry Point
```cpp
// 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 510 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
```text
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`
```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`
```cpp
#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`
```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
```cmake
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:
```cpp
class Renderer;
```
Add this public method:
```cpp
#ifdef WITH_SDL2_GUI
void setRenderer(Renderer* renderer);
#endif
```
Add these private members and helper declarations:
```cpp
#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:
```cpp
#ifdef WITH_SDL2_GUI
#include "Renderer.h"
#endif
```
Add these methods:
```cpp
#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:
```cpp
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:
```cpp
// 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()`:
```cpp
#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
```