# 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 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>`, `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 struct Impl; #include std::unique_ptr 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 #include #include 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& robots, int round, const std::vector& log); bool waitForStep(); void setAutoAdvanceMs(int ms); bool isOpen() const; private: struct Impl; std::unique_ptr impl_; }; ``` ### Why pass `std::vector`? `Game` owns robots as `std::unique_ptr`. 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 robotsInCell; for (int i = 0; i < static_cast(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(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(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 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 Game::robotView() const { std::vector 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 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 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 lk(turnMutex_); renderFrameLocked(); } #endif // launch and join threads #ifdef WITH_SDL2_GUI if (renderer_ && renderer_->isOpen()) { renderer_->setAutoAdvanceMs(0); std::lock_guard 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 #include 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 `` 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 ```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 #include 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 #include #include class World; class Robot; class Renderer { public: Renderer(); ~Renderer(); bool init(); void render(const World& world, const std::vector& robots, int round, const std::vector& log); bool waitForStep(); void setAutoAdvanceMs(int ms); bool isOpen() const; private: struct Impl; std::unique_ptr impl_; }; ``` ### `src/Renderer.cpp` ```cpp #include "Renderer.h" #include "Robot.h" #include "World.h" #include #include #include #include #include #include #include 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(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(70 + clamped * 15); Uint8 r = static_cast(20 + clamped * 22); return rgba(r, g, 55); } void drawGrid(const World& world, const std::vector& 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((CELL_W - 12) * (static_cast(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((i % 3) * 18); int py = GRID_Y + r->getY() * CELL_H + 14 + static_cast((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& robots, int round, const std::vector& 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(log.size()) - 15); for (int i = start; i < static_cast(log.size()); ++i) { drawText(log[i], PANEL_X + 18, y, rgba(210, 210, 210)); y += 20; } } void render(const World& world, const std::vector& robots, int round, const std::vector& 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(autoAdvanceMs)) return true; SDL_Delay(10); } return false; } }; Renderer::Renderer() : impl_(std::make_unique()) {} Renderer::~Renderer() { impl_->shutdown(); } bool Renderer::init() { return impl_->init(); } void Renderer::render(const World& world, const std::vector& robots, int round, const std::vector& 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 guiLog_; void renderFrameLocked(); std::vector 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 Game::robotPointers() const { std::vector 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(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 lock(turnMutex_); renderFrameLocked(); } #endif // launch and join threads here #ifdef WITH_SDL2_GUI if (renderer_ && renderer_->isOpen()) { renderer_->setAutoAdvanceMs(0); std::lock_guard lock(turnMutex_); guiLog_.push_back("=== GAME OVER ==="); renderFrameLocked(); } #endif ```