From 2a79d42bd66fd67e5d1a59f398f87849e85810db Mon Sep 17 00:00:00 2001 From: Florian Egger Date: Sun, 26 Apr 2026 17:48:28 +0200 Subject: [PATCH] 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. --- guide/PARALLEL_DEEP_MINER_GUIDE.md | 2426 ++++++++++++++++++++++++++++ guide/SDL2_GUI_EXTENSION_GUIDE.md | 965 +++++++++++ include/World.h | 8 +- src/World.cpp | 48 + 4 files changed, 3443 insertions(+), 4 deletions(-) create mode 100644 guide/PARALLEL_DEEP_MINER_GUIDE.md create mode 100644 guide/SDL2_GUI_EXTENSION_GUIDE.md diff --git a/guide/PARALLEL_DEEP_MINER_GUIDE.md b/guide/PARALLEL_DEEP_MINER_GUIDE.md new file mode 100644 index 0000000..72e6a5f --- /dev/null +++ b/guide/PARALLEL_DEEP_MINER_GUIDE.md @@ -0,0 +1,2426 @@ +# Programming Guide: Parallel Deep Miner + +> **Basis:** Fourth Example – *Deep Miner* – a parallel mining simulation on a 3-D grid where multiple robots run concurrently in separate threads and compete to accumulate the highest score. All play is automated; there is no manual player mode. +> +> **Scope of this guide:** terminal/C++ implementation, threading, timing, combat, tests, and non-GUI extensions. The SDL2 renderer has been split into `SDL2_GUI_EXTENSION_GUIDE.md`. + +--- + +## Table of Contents + +1. [Project Overview](#1-project-overview) +2. [Architecture](#2-architecture) +3. [Core Components](#3-core-components) + - 3.1 [World](#31-world) + - 3.2 [Robot Interface](#32-robot-interface) + - 3.3 [BaseRobot](#33-baserobot) + - 3.4 [Concrete Robots](#34-concrete-robots) + - 3.5 [ScopedTimer](#35-scopedtimer) + - 3.6 [Game](#36-game) +4. [Build and Execution](#4-build-and-execution) +5. [Assignment Levels](#5-assignment-levels) + - 5.1 [Stufe 1 – Parallel Threads and Conservation Check](#51-stufe-1--parallel-threads-and-conservation-check) + - 5.2 [Stufe 2 – RAII Thread Timing](#52-stufe-2--raii-thread-timing) + - 5.3 [Stufe 3 – Robot Combat](#53-stufe-3--robot-combat) + - 5.4 [Adding SmartBot](#54-adding-smartbot) + - 5.5 [Adding LookaheadBot](#55-adding-lookaheadbot) +6. [Writing Tests](#6-writing-tests) +7. [Design Principles and Best Practices](#7-design-principles-and-best-practices) +8. [Common Pitfalls and Solutions](#8-common-pitfalls-and-solutions) +9. [Advanced Extension Ideas](#9-advanced-extension-ideas) +10. [Project Structure](#10-project-structure) +11. [Appendix A: Complete Core Source Code](#appendix-a-complete-core-source-code) + +--- + +## 1. Project Overview + +**Deep Miner** is a parallel strategy simulation implemented in C++17. A configurable number of robots, usually 5–10, each run in their own `std::thread` on a shared three-dimensional grid. Robots move, trigger effects, fight, and mine blocks to accumulate points. + +A single mutex, `turnMutex_`, protects the complete logical turn. This keeps the implementation safe and easy to reason about: only one robot can read or mutate the `World` at a time. + +### Grid concept + +```text +5 × 5 × 10 grid world (X × Y × Z) + +Each column (x, y) has up to 10 layers. +z = 0 bottom / deepest layer +z = column.size() - 1 surface / top layer + +Positive values 1..9 mineable blocks +-1 blocked turn effect +-2 teleport effect +-3 HP damage effect +``` + +### Assignment levels + +| Level | Title | Key feature | +|---|---|---| +| Stufe 1 | Parallel Threads and Conservation Check | One thread per robot, one mutex, point conservation check | +| Stufe 2 | RAII Thread Timing | `ScopedTimer` starts in constructor and stores elapsed time in destructor | +| Stufe 3 | Robot Combat | HP/death system, combat, preserved dead-robot scores | + +### Per-thread turn sequence + +```text +Thread for robot i wakes up: + 1. Acquire turnMutex_ + 2. If this robot is dead: record score, exit thread + 3. If the world is empty: exit thread + 4. decideNextMove(world_) + 5. move(direction, world_) + 6. checkEffects(x, y) + 7. applyEffect(effect), if any + 8. If effect killed this robot: record score, exit thread + 9. fightNearby(robot) +10. If this robot somehow died during the turn: record score, exit thread +11. If effect was not -1: mine(world_) +12. checkRearrange(robot) +13. Release turnMutex_ +14. sleep_for(10 ms) +``` + +The death check must happen **before** the game-over check. A robot may have been killed by another robot on a previous turn. It still needs a chance to record its final score before its thread exits. + +--- + +## 2. Architecture + +```text +Robot interface + ↑ +BaseRobot abstract base class + ↑ +SortBot / DigDeepBot / RandomBot / optional extensions + +World + owns the dynamic 3-D grid + +Game + owns World, robots, mutex, score bookkeeping, timing data, and thread lifecycle + +ScopedTimer + small RAII utility used by Game and robot threads for elapsed-time measurement +``` + +### Main ownership rules + +- `Game` owns all robots through `std::vector>`. +- The robot threads are launched in `Game::run()` and joined before `Game::run()` returns. +- `World` is accessed only while `turnMutex_` is held. +- Robot HP and score mutations happen only while `turnMutex_` is held. +- Timing slots are pre-sized before threads start; each thread writes only to its own slot. + +--- + +## 3. Core Components + +### 3.1 World + +The `World` class stores each `(x, y)` column as a dynamic stack: + +```cpp +std::vector>> grid_; +// grid_[x][y] is one vertical column. +// grid_[x][y].back() is the current surface block. +``` + +This design replaces the older fixed-depth grid that used `0` as a mined-cell sentinel. In the dynamic-column design, mined blocks are removed from the vector. No zero-filled holes should be introduced by robot code. + +#### Recommended public API + +```cpp +class World { +public: + World(int x = 5, int y = 5, int z = 10); + + int getSizeX() const; + int getSizeY() const; + int getSizeZ() const; + + int getSurfaceLevel(int x, int y) const; // -1 when empty + int getSurfaceValue(int x, int y) const; // 0 when empty + int getValue(int x, int y, int z) const; // throws on invalid coordinates + + int mine(int x, int y); // pops the surface block + int checkEffects(int x, int y); // removes and returns one effect, or 0 + int collectPositiveColumn(int x, int y); // extension helper; keeps effects + + void setColumn(int x, int y, std::vector values); // tests/examples + void clear(); // tests/examples + void rearrange(); + void display() const; + +private: + void validateXY(int x, int y) const; + void validateXYZ(int x, int y, int z) const; + + int sizeX_ = 5; + int sizeY_ = 5; + int sizeZ_ = 10; + std::vector>> grid_; +}; +``` + +#### Surface level + +```cpp +int World::getSurfaceLevel(int x, int y) const { + validateXY(x, y); + const auto& col = grid_[x][y]; + return col.empty() ? -1 : static_cast(col.size()) - 1; +} +``` + +This is O(1), because the column vector already knows its size. + +#### Mining + +```cpp +int World::mine(int x, int y) { + validateXY(x, y); + auto& col = grid_[x][y]; + if (col.empty()) return 0; + + const int value = col.back(); + col.pop_back(); + return value > 0 ? value : 0; +} +``` + +`mine()` transfers a positive surface value from the world to a robot. It never creates a `0` sentinel. + +#### Effect handling + +A simple version checks only the current surface. That keeps effects intuitive: a buried effect is triggered only when mining/movement exposes it. + +```cpp +int World::checkEffects(int x, int y) { + validateXY(x, y); + auto& col = grid_[x][y]; + if (col.empty()) return 0; + + const int value = col.back(); + if (value >= 0) return 0; + + col.pop_back(); + return value; +} +``` + +If your assignment requires effects to be found anywhere in the column, document that explicitly. Do not mix both interpretations. + +#### Extension helper for full-column mining + +Some extension robots, such as `SmartBot`, want to mine all positive values in a column. Do not implement that by setting cells to `0`. Add an explicit stack-aware helper: + +```cpp +int World::collectPositiveColumn(int x, int y) { + validateXY(x, y); + auto& col = grid_[x][y]; + + int total = 0; + std::vector kept; + kept.reserve(col.size()); + + for (int v : col) { + if (v > 0) { + total += v; + } else { + kept.push_back(v); // keep effects instead of destroying them + } + } + + col = std::move(kept); + return total; +} +``` + +#### Test helpers + +```cpp +void World::clear() { + for (auto& row : grid_) + for (auto& col : row) + col.clear(); +} + +void World::setColumn(int x, int y, std::vector values) { + validateXY(x, y); + if (static_cast(values.size()) > sizeZ_) + throw std::out_of_range("column exceeds configured depth"); + grid_[x][y] = std::move(values); +} +``` + +These helpers solve the test problem cleanly. A dynamic column is empty only when its vector is empty; filling it with zeros is not equivalent. + +--- + +### 3.2 Robot Interface + +`Robot` is a pure virtual interface. `Game` talks to robots only through this interface. + +```cpp +class Robot { +public: + virtual ~Robot() = default; + + virtual void move(int direction, const World& world) = 0; + virtual int mine(World& world) = 0; + virtual int decideNextMove(const World& world) const = 0; + + virtual void setPosition(int x, int y) = 0; + virtual int getScore() const = 0; + virtual void addScore(int points) = 0; + virtual int getX() const = 0; + virtual int getY() const = 0; + virtual std::string getName() const = 0; + + virtual int getHp() const = 0; + virtual bool isAlive() const = 0; + virtual void takeDamage(int dmg) = 0; +}; +``` + +--- + +### 3.3 BaseRobot + +`BaseRobot` implements shared robot state and behavior. It remains abstract because `mine()` is still strategy-specific. + +```cpp +class BaseRobot : public Robot { +public: + BaseRobot(std::string name, int startX, int startY) + : x_(startX), y_(startY), name_(std::move(name)) {} + + void move(int direction, const World& world) override; + int decideNextMove(const World& world) const override; + + void setPosition(int x, int y) override { x_ = x; y_ = y; } + int getScore() const override { return score_; } + void addScore(int points) override { score_ += points; } + int getX() const override { return x_; } + int getY() const override { return y_; } + std::string getName() const override { return name_; } + + int getHp() const override { return hp_; } + bool isAlive() const override { return hp_ > 0; } + + void takeDamage(int dmg) override { + hp_ -= dmg; + if (hp_ < 0) hp_ = 0; + } + +protected: + int x_ = 0; + int y_ = 0; + int score_ = 0; + std::string name_; + + int hp_ = 100; + static constexpr int kMaxHp = 100; +}; +``` + +Movement uses direction codes: + +```text +0 = stay +1 = x + 1 +2 = x - 1 +3 = y + 1 +4 = y - 1 +``` + +Clamp movement to world boundaries: + +```cpp +void BaseRobot::move(int direction, const World& world) { + int nx = x_; + int ny = y_; + + switch (direction) { + case 1: ++nx; break; + case 2: --nx; break; + case 3: ++ny; break; + case 4: --ny; break; + default: break; + } + + x_ = std::clamp(nx, 0, world.getSizeX() - 1); + y_ = std::clamp(ny, 0, world.getSizeY() - 1); +} +``` + +--- + +### 3.4 Concrete Robots + +Each concrete robot implements only its mining strategy. + +#### SortBot + +Sorts the current column so the highest positive value reaches the surface, then mines one block. With dynamic columns, sorting must not create zeros. + +```cpp +int SortBot::mine(World& world) { + // If you expose a World::sortPositiveColumnToSurface helper, call it here. + // Then mine the surface once. + const int points = world.mine(x_, y_); + score_ += points; + return points; +} +``` + +#### DigDeepBot + +Mines up to three surface blocks. + +```cpp +int DigDeepBot::mine(World& world) { + int total = 0; + for (int i = 0; i < 3; ++i) { + const int points = world.mine(x_, y_); + if (points <= 0) break; + total += points; + } + score_ += total; + return total; +} +``` + +#### RandomBot + +Mines a random number of surface blocks from 0 to 9. + +```cpp +int RandomBot::mine(World& world) { + static thread_local std::mt19937 rng{std::random_device{}()}; + std::uniform_int_distribution countDist(0, 9); + + const int n = countDist(rng); + int total = 0; + + for (int i = 0; i < n; ++i) { + const int points = world.mine(x_, y_); + if (points <= 0) break; + total += points; + } + + score_ += total; + return total; +} +``` + +The caller must **not** call `addScore()` after `mine()`. Each `mine()` implementation credits its own score. + +--- + +### 3.5 ScopedTimer + +Stufe 2 uses a small RAII timing class. Its constructor starts the timer. Its destructor stops the timer and writes the elapsed duration into a target variable. + +```cpp +// include/ScopedTimer.h +#pragma once + +#include + +class ScopedTimer { +public: + using Clock = std::chrono::steady_clock; + using Duration = std::chrono::duration; + + explicit ScopedTimer(Duration& target) noexcept + : target_(target), start_(Clock::now()) {} + + ~ScopedTimer() noexcept { + target_ = Clock::now() - start_; + } + + ScopedTimer(const ScopedTimer&) = delete; + ScopedTimer& operator=(const ScopedTimer&) = delete; + +private: + Duration& target_; + Clock::time_point start_; +}; +``` + +Use `std::chrono::steady_clock` for elapsed-time measurement. It is monotonic, so it is not affected by system clock adjustments. + +--- + +### 3.6 Game + +`Game` owns the simulation and coordinates all threads. + +```cpp +// include/Game.h +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "Robot.h" +#include "ScopedTimer.h" +#include "World.h" + +class Game { +public: + Game(); + void run(); + +private: + using Duration = ScopedTimer::Duration; + + World world_; + std::vector> robots_; + + std::vector deadRobotScores_; + std::vector deathRecorded_; + + std::mutex turnMutex_; + int lastThreshold_ = 0; + + std::vector threadTimes_; + Duration totalTime_{}; + + void setup(); + void robotLoop(int idx); + void recordDeath(int idx, const std::string& reason); + void fightNearby(Robot& attacker); + + int computeWorldSum() const; + int computeLivingScore() const; + int computeDeadScore() const; + + void checkRearrange(Robot& robot); + void applyEffect(Robot& robot, int effect); + bool isGameOver() const; + void log(const std::string& msg); + void printScores() const; + void printResult() const; + + std::unique_ptr createRobot(int choice, int x, int y) const; +}; +``` + +`deathRecorded_` prevents accidental double-recording if later code paths call `recordDeath()` more than once for the same robot. + +--- + +## 4. Build and Execution + +### Requirements + +- C++17-compatible compiler +- CMake 3.15 or newer +- Thread library resolved through CMake's `Threads::Threads` + +### CMakeLists.txt + +```cmake +cmake_minimum_required(VERSION 3.15) +project(deep_miner) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +find_package(Threads REQUIRED) + +include_directories(include) + +set(SOURCES + src/BaseRobot.cpp + src/SortBot.cpp + src/DigDeepBot.cpp + src/RandomBot.cpp + src/World.cpp + src/Game.cpp +) + +add_executable(deep_miner + ${SOURCES} + main.cpp +) + +add_executable(deep_miner_tests + ${SOURCES} + tests/test_all.cpp +) + +target_link_libraries(deep_miner Threads::Threads) +target_link_libraries(deep_miner_tests Threads::Threads) +``` + +Build and run: + +```bash +mkdir build +cd build +cmake .. +cmake --build . +./deep_miner +./deep_miner_tests +``` + +Every new `.cpp` file must be added to `SOURCES` so it is compiled into both the game and the test target. + +--- + +## 5. Assignment Levels + +### 5.1 Stufe 1 – Parallel Threads and Conservation Check + +Stufe 1 replaces the old sequential two-robot loop with one thread per robot. + +#### setup() + +```cpp +void Game::setup() { + const int n = validateInput("Number of robots (5-10): ", 5, 10); + + const std::vector> startTable = { + {0,0}, {4,4}, {0,4}, {4,0}, {2,2}, + {0,2}, {4,2}, {2,0}, {2,4}, {1,1} + }; + + robots_.reserve(n); + threadTimes_.resize(n); + deathRecorded_.assign(n, false); + + for (int i = 0; i < n; ++i) { + std::cout << "Robot " << (i + 1) << ":\n"; + int type = validateInput( + " Type (1=SortBot 2=DigDeepBot 3=RandomBot): ", 1, 3); + + auto [sx, sy] = startTable[i % startTable.size()]; + robots_.push_back(createRobot(type, sx, sy)); + } +} +``` + +#### run() + +```cpp +void Game::run() { + std::cout << "=== PARALLEL DEEP MINER ===\n\n"; + setup(); + + const int initialWorldSum = computeWorldSum(); + std::cout << "Initial world sum: " << initialWorldSum << "\n"; + world_.display(); + + { + ScopedTimer totalTimer(totalTime_); + + std::vector threads; + threads.reserve(robots_.size()); + + for (int i = 0; i < static_cast(robots_.size()); ++i) + threads.emplace_back(&Game::robotLoop, this, i); + + for (auto& t : threads) + t.join(); + } // totalTimer destructor stores totalTime_ + + printScores(); + + const int liveScore = computeLivingScore(); + const int deadScore = computeDeadScore(); + const int remainingWorldSum = computeWorldSum(); + const int conservedTotal = liveScore + deadScore + remainingWorldSum; + + std::cout << "\n--- Conservation Check ---\n" + << "Initial world sum : " << initialWorldSum << "\n" + << "Living robot scores : " << liveScore << "\n" + << "Dead robot scores : " << deadScore << "\n" + << "Remaining world sum : " << remainingWorldSum << "\n" + << "Conserved total : " << conservedTotal << "\n" + << (initialWorldSum == conservedTotal + ? "Conservation check: OK\n" + : "Conservation check: MISMATCH\n"); + + std::cout << "\n--- Thread Timing ---\n"; + for (int i = 0; i < static_cast(robots_.size()); ++i) { + std::cout << "Thread " << i + << " [" << robots_[i]->getName() << "]: " + << std::fixed << std::setprecision(3) + << threadTimes_[i].count() << " s\n"; + } + + std::cout << "Total wall-clock time: " + << std::fixed << std::setprecision(3) + << totalTime_.count() << " s\n"; + + printResult(); +} +``` + +This version checks true conservation: + +```text +initialWorldSum == livingRobotScores + deadRobotScores + remainingWorldSum +``` + +If all robots die before the world is depleted, the check can still pass because the remaining world value is counted explicitly. + +#### 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"); + break; + } + + if (isGameOver()) { + break; + } + + 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"); + break; + } + + fightNearby(robot); + + if (!robot.isAlive()) { + recordDeath(idx, "killed in combat"); + break; + } + + if (effect != -1) { + const int mined = robot.mine(world_); + log(robot.getName() + " mined " + std::to_string(mined) + " points."); + } + + checkRearrange(robot); + } + + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } +} // threadTimer destructor stores threadTimes_[idx] +``` + +#### recordDeath() + +```cpp +void Game::recordDeath(int idx, const std::string& reason) { + if (deathRecorded_[idx]) return; + + Robot& robot = *robots_[idx]; + deadRobotScores_.push_back(robot.getScore()); + deathRecorded_[idx] = true; + + log("[DEAD] " + robot.getName() + " " + reason + + " – final score " + std::to_string(robot.getScore()) + " recorded."); +} +``` + +`recordDeath()` is called only while `turnMutex_` is held. + +#### Score helpers + +```cpp +int Game::computeLivingScore() const { + int sum = 0; + for (const auto& r : robots_) { + if (r->isAlive()) sum += r->getScore(); + } + return sum; +} + +int Game::computeDeadScore() const { + int sum = 0; + for (int s : deadRobotScores_) sum += s; + return sum; +} +``` + +Dead robots remain inside `robots_`, so summing all robot scores plus `deadRobotScores_` would double-count dead robots. Either sum only living robot scores plus dead scores, or skip `deadRobotScores_` and sum every robot directly. The version above is clearer because it documents the death handoff. + +--- + +### 5.2 Stufe 2 – RAII Thread Timing + +Stufe 2 uses `ScopedTimer` instead of manually writing start/stop code. + +#### Why RAII? + +RAII makes timing hard to forget: + +```cpp +void someFunction() { + ScopedTimer timer(durationSlot); + + // Work happens here. + // All returns and breaks still run the destructor. +} +``` + +When the scope exits, the destructor writes the elapsed time. This works even if the function leaves through an early `return` or a `break` exits a loop inside the scope. + +#### Game data members + +```cpp +using Duration = ScopedTimer::Duration; + +std::vector threadTimes_; +Duration totalTime_{}; +``` + +#### Thread measurement + +```cpp +void Game::robotLoop(int idx) { + ScopedTimer threadTimer(threadTimes_[idx]); + // full robot loop +} +``` + +Each thread writes only to `threadTimes_[idx]`. The vector is resized before any thread starts, so this is safe. + +#### Total measurement + +```cpp +{ + ScopedTimer totalTimer(totalTime_); + // launch and join all threads +} +``` + +The braces are intentional. They force the destructor to run before timing is printed. + +--- + +### 5.3 Stufe 3 – Robot Combat + +Stufe 3 adds HP, combat, and permanent death. + +#### applyEffect() + +```cpp +void Game::applyEffect(Robot& robot, int effect) { + std::ostringstream oss; + + switch (effect) { + case -1: + oss << "[EFFECT -1] " << robot.getName() + << " is blocked and may not mine this turn."; + break; + + case -2: { + int bestX = robot.getX(); + int bestY = robot.getY(); + int bestValue = std::numeric_limits::max(); + + for (int x = 0; x < world_.getSizeX(); ++x) { + for (int y = 0; y < world_.getSizeY(); ++y) { + const int value = world_.getSurfaceValue(x, y); + if (value < bestValue) { + bestValue = value; + bestX = x; + bestY = y; + } + } + } + + robot.setPosition(bestX, bestY); + oss << "[EFFECT -2] " << robot.getName() + << " teleported to (" << bestX << ", " << bestY << ")."; + break; + } + + case -3: + robot.takeDamage(30); + oss << "[EFFECT -3] " << robot.getName() + << " takes 30 HP damage. HP=" << robot.getHp(); + break; + + default: + return; + } + + log(oss.str()); +} +``` + +Effect `-3` damages HP only. It must not subtract score, because score destruction breaks point conservation. + +#### fightNearby() + +```cpp +void Game::fightNearby(Robot& attacker) { + static std::mt19937 rng{std::random_device{}()}; + std::uniform_int_distribution dmgDist(5, 25); + + for (auto& targetPtr : robots_) { + Robot& target = *targetPtr; + + if (&target == &attacker) continue; + if (!target.isAlive()) continue; + + const int dx = std::abs(attacker.getX() - target.getX()); + const int dy = std::abs(attacker.getY() - target.getY()); + + if (dx <= 1 && dy <= 1) { + const int dmg = dmgDist(rng); + target.takeDamage(dmg); + + std::ostringstream oss; + oss << attacker.getName() << " attacked " + << target.getName() << " for " << dmg + << " damage. HP=" << target.getHp(); + + if (!target.isAlive()) oss << " [DEAD]"; + log(oss.str()); + } + } +} +``` + +This function is called while `turnMutex_` is held, so robot HP reads/writes are data-race free. + +A target killed in `fightNearby()` records its score when its own thread next acquires the mutex. That is why the top of `robotLoop()` checks `!robot.isAlive()` before checking `isGameOver()`. + +--- + +### 5.4 Adding SmartBot + +`SmartBot` mines the entire positive content of a column only when the average positive value exceeds a threshold. + +#### include/SmartBot.h + +```cpp +#pragma once +#include "BaseRobot.h" + +class SmartBot : public BaseRobot { +public: + explicit SmartBot(int startX, int startY, int threshold = 5); + + int mine(World& world) override; + int decideNextMove(const World& world) const override; + +private: + int threshold_; + double columnAverage(const World& world, int x, int y) const; +}; +``` + +#### src/SmartBot.cpp + +```cpp +#include "../include/SmartBot.h" +#include "../include/World.h" + +SmartBot::SmartBot(int startX, int startY, int threshold) + : BaseRobot("SmartBot", startX, startY), threshold_(threshold) {} + +double SmartBot::columnAverage(const World& world, int x, int y) const { + int sum = 0; + int count = 0; + + const int surface = world.getSurfaceLevel(x, y); + for (int z = 0; z <= surface; ++z) { + const int v = world.getValue(x, y, z); + if (v > 0) { + sum += v; + ++count; + } + } + + return count == 0 ? 0.0 : static_cast(sum) / count; +} + +int SmartBot::mine(World& world) { + if (columnAverage(world, x_, y_) <= threshold_) { + return 0; + } + + const int total = world.collectPositiveColumn(x_, y_); + score_ += total; + return total; +} + +int SmartBot::decideNextMove(const World& world) const { + int bestDir = 0; + double bestAvg = -1.0; + + auto check = [&](int dx, int dy, int dir) { + const int nx = x_ + dx; + const int ny = y_ + dy; + if (nx < 0 || nx >= world.getSizeX()) return; + if (ny < 0 || ny >= world.getSizeY()) return; + + const double avg = columnAverage(world, nx, ny); + if (avg > bestAvg) { + bestAvg = avg; + bestDir = dir; + } + }; + + check( 0, 0, 0); + check( 1, 0, 1); + check(-1, 0, 2); + check( 0, 1, 3); + check( 0, -1, 4); + + return bestDir; +} +``` + +This version does not call `setValue(..., 0)`. It stays compatible with dynamic columns. + +#### Register SmartBot + +```cpp +// setup() prompt +int type = validateInput( + " Type (1=SortBot 2=DigDeepBot 3=RandomBot 4=SmartBot): ", 1, 4); + +// createRobot() +case 4: + return std::make_unique(x, y); +``` + +Add `src/SmartBot.cpp` to `SOURCES`. + +--- + +### 5.5 Adding LookaheadBot + +`LookaheadBot` chooses a move by estimating the best two-turn yield without mutating the world. + +#### include/LookaheadBot.h + +```cpp +#pragma once +#include "BaseRobot.h" + +class LookaheadBot : public BaseRobot { +public: + LookaheadBot(int startX, int startY); + + int mine(World& world) override; + int decideNextMove(const World& world) const override; + +private: + int lookaheadScore(const World& world, int x, int y, int depth) const; + int columnTopValue(const World& world, int x, int y, int blocks) const; +}; +``` + +#### src/LookaheadBot.cpp + +```cpp +#include "../include/LookaheadBot.h" +#include "../include/World.h" + +#include + +LookaheadBot::LookaheadBot(int startX, int startY) + : BaseRobot("LookaheadBot", startX, startY) {} + +int LookaheadBot::columnTopValue(const World& world, int x, int y, int blocks) const { + int sum = 0; + int grabbed = 0; + + for (int z = world.getSurfaceLevel(x, y); z >= 0 && grabbed < blocks; --z) { + const int v = world.getValue(x, y, z); + if (v > 0) { + sum += v; + ++grabbed; + } + } + + return sum; +} + +int LookaheadBot::lookaheadScore(const World& world, int x, int y, int depth) const { + if (depth == 0) { + return columnTopValue(world, x, y, 3); + } + + int best = 0; + + auto tryMove = [&](int nx, int ny) { + if (nx < 0 || nx >= world.getSizeX()) return; + if (ny < 0 || ny >= world.getSizeY()) return; + + const int score = columnTopValue(world, nx, ny, 3) + + lookaheadScore(world, nx, ny, depth - 1); + best = std::max(best, score); + }; + + tryMove(x, y); + tryMove(x + 1, y); + tryMove(x - 1, y); + tryMove(x, y + 1); + tryMove(x, y - 1); + + return best; +} + +int LookaheadBot::decideNextMove(const World& world) const { + int bestDir = 0; + int bestScore = -1; + + auto check = [&](int dx, int dy, int dir) { + const int nx = x_ + dx; + const int ny = y_ + dy; + if (nx < 0 || nx >= world.getSizeX()) return; + if (ny < 0 || ny >= world.getSizeY()) return; + + const int score = lookaheadScore(world, nx, ny, 1); + if (score > bestScore) { + bestScore = score; + bestDir = dir; + } + }; + + check( 0, 0, 0); + check( 1, 0, 1); + check(-1, 0, 2); + check( 0, 1, 3); + check( 0, -1, 4); + + return bestDir; +} + +int LookaheadBot::mine(World& world) { + int total = 0; + + for (int i = 0; i < 3; ++i) { + const int points = world.mine(x_, y_); + if (points <= 0) break; + total += points; + } + + score_ += total; + return total; +} +``` + +This version estimates using `getValue()` but mutates with `world.mine()`, preserving the dynamic-column model. + +--- + +## 6. Writing Tests + +The project uses a small custom test framework in `tests/test_all.cpp`. + +### Test macros + +```cpp +TEST("Description", { + ASSERT(condition); + ASSERT_EQ(actual, expected); +}); +``` + +### Deterministic test worlds + +Do not create an empty world by setting every cell to `0`. With dynamic columns, that creates full columns containing zero values. Use `clear()` and `setColumn()`. + +```cpp +static World makeEmptyWorld(int x = 5, int y = 5, int z = 10) { + World w(x, y, z); + w.clear(); + return w; +} +``` + +### HP tests + +```cpp +static void test_baserobot_combat() { + TEST("Full HP at construction", { + SortBot bot(0, 0); + ASSERT_EQ(bot.getHp(), 100); + ASSERT_EQ(bot.isAlive(), true); + }); + + TEST("takeDamage reduces HP", { + SortBot bot(0, 0); + bot.takeDamage(20); + ASSERT_EQ(bot.getHp(), 80); + ASSERT_EQ(bot.isAlive(), true); + }); + + TEST("Fatal damage clamps HP to zero", { + SortBot bot(0, 0); + bot.takeDamage(200); + ASSERT_EQ(bot.getHp(), 0); + ASSERT_EQ(bot.isAlive(), false); + }); +} +``` + +### SmartBot tests + +```cpp +static void test_smartbot_mine() { + TEST("Does not mine when average is too low", { + World w = makeEmptyWorld(3, 3, 5); + w.setColumn(1, 1, {3}); + + SmartBot bot(1, 1, 5); + ASSERT_EQ(bot.mine(w), 0); + ASSERT_EQ(bot.getScore(), 0); + ASSERT_EQ(w.getSurfaceValue(1, 1), 3); + }); + + TEST("Mines positive column when average is high enough", { + World w = makeEmptyWorld(3, 3, 5); + w.setColumn(1, 1, {7, 8}); + + SmartBot bot(1, 1, 5); + ASSERT_EQ(bot.mine(w), 15); + ASSERT_EQ(bot.getScore(), 15); + ASSERT_EQ(w.getSurfaceLevel(1, 1), -1); + }); + + TEST("Keeps effects when collecting positives", { + World w = makeEmptyWorld(3, 3, 5); + w.setColumn(1, 1, {4, -2, 8}); + + SmartBot bot(1, 1, 5); + ASSERT_EQ(bot.mine(w), 12); + ASSERT_EQ(w.getSurfaceLevel(1, 1), 0); + ASSERT_EQ(w.getSurfaceValue(1, 1), -2); + }); +} +``` + +### Timing tests + +```cpp +static void test_scoped_timer() { + TEST("ScopedTimer writes elapsed duration on destruction", { + ScopedTimer::Duration elapsed{}; + { + ScopedTimer timer(elapsed); + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + ASSERT(elapsed.count() > 0.0); + }); +} +``` + +--- + +## 7. Design Principles and Best Practices + +### Single Responsibility Principle + +| Class | Responsibility | +|---|---| +| `World` | Manage grid data and stack operations | +| `Robot` | Define the robot contract | +| `BaseRobot` | Shared robot state, movement, HP/death | +| `*Bot` | Concrete mining and movement strategy | +| `Game` | Thread lifecycle, turns, effects, combat, score/timing output | +| `ScopedTimer` | Elapsed-time measurement through RAII | + +### Open/Closed Principle + +Adding a robot type should not require changes to `World`, `BaseRobot`, or the thread model. Update only the new robot files, the robot factory, the menu prompt, and CMake. + +### RAII + +Use RAII for both resource management and timing: + +- `std::unique_ptr` releases robots automatically. +- `std::lock_guard` releases the mutex automatically. +- `ScopedTimer` stores elapsed time automatically. + +### Const-correctness + +Make read-only methods `const`: + +```cpp +int getScore() const override; +int decideNextMove(const World& world) const override; +int getHp() const override; +bool isAlive() const override; +``` + +### Thread safety + +The safe rule is simple: + +> Every read or write of `world_`, robot score, robot position, or robot HP happens while `turnMutex_` is held. + +The 10 ms sleep must stay outside the lock. Otherwise one sleeping thread would block every other robot. + +--- + +## 8. Common Pitfalls and Solutions + +### Pitfall 1: Missing `.cpp` file in CMake + +**Symptom:** linker error such as `undefined reference to SmartBot::mine`. + +**Solution:** add the file to `SOURCES`. + +### Pitfall 2: Adding score twice + +**Symptom:** scores are too high. + +**Cause:** `mine()` already updates `score_`, and `robotLoop()` also calls `addScore()`. + +**Solution:** `robotLoop()` should log the returned value only. + +```cpp +const int mined = robot.mine(world_); +log(robot.getName() + " mined " + std::to_string(mined) + " points."); +``` + +### Pitfall 3: Using zero sentinels in dynamic columns + +**Symptom:** empty columns are not really empty, surface levels are wrong, and tests behave strangely. + +**Solution:** use `clear()`, `setColumn()`, `mine()`, and stack-aware helpers. Do not use `setValue(..., 0)` as a mining operation. + +### Pitfall 4: Dead scores are double-counted or lost + +**Double-counted:** summing all `robots_` scores and also summing `deadRobotScores_`. + +**Lost:** checking `isGameOver()` before checking whether the current robot is dead. + +**Solution:** at the top of `robotLoop()`, record death first, then check game over. At the end, compute `livingScore + deadScore + remainingWorldSum`. + +### Pitfall 5: Non-monotonic timing clock + +**Symptom:** rare negative or odd elapsed times on systems where wall clock changes. + +**Solution:** use `std::chrono::steady_clock` inside `ScopedTimer`. + +### Pitfall 6: Holding the lock while sleeping + +**Symptom:** one robot monopolizes the game. + +**Solution:** keep `sleep_for(10 ms)` after the lock-guard scope. + +### Pitfall 7: GUI snippets reference old two-robot state + +The GUI has been moved to a separate guide and rewritten to use `std::vector>`. The terminal guide should not mention `player_`, `computer_`, `autoMode_`, or `play(Robot&, bool)`. + +--- + +## 9. Advanced Extension Ideas + +### Monte Carlo Tree Search + +Replace the simple lookahead with Monte Carlo Tree Search. Simulations should operate on copied/snapshot world data, not the shared `world_` object. + +### Achievement System + +Track milestones such as: + +- first 50-point threshold +- three blocks mined in one turn +- first effect triggered +- first robot defeated +- surviving with 1 HP + +### Persistent Highscore Table + +Save the top 10 results to a local file. Include robot types, total collected score, remaining world sum, and game duration. + +### GUI Renderer + +Use the separate `SDL2_GUI_EXTENSION_GUIDE.md`. It documents the optional renderer, pimpl interface, CMake target, and integration points for the parallel robot vector. + +--- + +## 10. Project Structure + +```text +deep_miner/ +|-- CMakeLists.txt +|-- main.cpp +|-- include/ +| |-- Robot.h +| |-- BaseRobot.h +| |-- SortBot.h +| |-- DigDeepBot.h +| |-- RandomBot.h +| |-- World.h +| |-- ScopedTimer.h +| `-- Game.h +|-- src/ +| |-- BaseRobot.cpp +| |-- SortBot.cpp +| |-- DigDeepBot.cpp +| |-- RandomBot.cpp +| |-- World.cpp +| `-- Game.cpp +|-- tests/ +| `-- test_all.cpp +`-- docs/ + |-- PARALLEL_DEEP_MINER_GUIDE.md + `-- SDL2_GUI_EXTENSION_GUIDE.md +``` + +The optional GUI target adds: + +```text +include/Renderer.h +src/Renderer.cpp +main_gui.cpp +``` + +Keep GUI dependencies out of the terminal target. + +--- + +*This revised guide documents the fully parallel terminal implementation. It fixes score accounting, dynamic-column examples, deterministic tests, and timing by using an RAII `ScopedTimer` whose constructor starts measurement and destructor stores the elapsed duration.* + +--- + +## Appendix A: Complete Core Source Code + +This appendix contains a complete terminal-only version of the project. It uses dynamic stack columns, `ScopedTimer` for RAII timing, living/dead/remaining score conservation, and includes `SmartBot` and `LookaheadBot` so the menu and build file are self-contained. + +### `CMakeLists.txt` + +```cmake +cmake_minimum_required(VERSION 3.15) +project(deep_miner) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +find_package(Threads REQUIRED) +include_directories(include) + +set(SOURCES + src/BaseRobot.cpp + src/SortBot.cpp + src/DigDeepBot.cpp + src/RandomBot.cpp + src/SmartBot.cpp + src/LookaheadBot.cpp + src/ScopedTimer.cpp + src/World.cpp + src/Game.cpp +) + +add_executable(deep_miner ${SOURCES} main.cpp) +add_executable(deep_miner_tests ${SOURCES} tests/test_all.cpp) + +target_link_libraries(deep_miner Threads::Threads) +target_link_libraries(deep_miner_tests Threads::Threads) +``` + +### `main.cpp` + +```cpp +#include "Game.h" +#include +#include + +int main() { + try { + Game game; + game.run(); + return 0; + } catch (const std::exception& e) { + std::cerr << "Fatal error: " << e.what() << "\n"; + return 1; + } +} +``` + +### `include/Robot.h` + +```cpp +#pragma once +#include +class World; + +class Robot { +public: + virtual ~Robot() = default; + virtual void move(int direction, const World& world) = 0; + virtual int mine(World& world) = 0; + virtual int decideNextMove(const World& world) const = 0; + virtual void setPosition(int x, int y) = 0; + virtual int getScore() const = 0; + virtual void addScore(int points) = 0; + virtual int getX() const = 0; + virtual int getY() const = 0; + virtual std::string getName() const = 0; + virtual int getHp() const = 0; + virtual bool isAlive() const = 0; + virtual void takeDamage(int damage) = 0; +}; +``` + +### `include/ScopedTimer.h` + +```cpp +#pragma once +#include + +class ScopedTimer { +public: + using Clock = std::chrono::steady_clock; + using Duration = std::chrono::duration; + + explicit ScopedTimer(Duration& output); + ~ScopedTimer(); + + ScopedTimer(const ScopedTimer&) = delete; + ScopedTimer& operator=(const ScopedTimer&) = delete; + +private: + Duration& output_; + Clock::time_point start_; +}; +``` + +### `src/ScopedTimer.cpp` + +```cpp +#include "ScopedTimer.h" + +ScopedTimer::ScopedTimer(Duration& output) + : output_(output), start_(Clock::now()) {} + +ScopedTimer::~ScopedTimer() { + output_ = Clock::now() - start_; +} +``` + +### `include/World.h` + +```cpp +#pragma once +#include +#include +class Robot; + +class World { +public: + World(int x = 5, int y = 5, int z = 10); + + int getSizeX() const { return sizeX_; } + int getSizeY() const { return sizeY_; } + int getSizeZ() const { return sizeZ_; } + + int getValue(int x, int y, int z) const; + void setValue(int x, int y, int z, int value); + void setColumn(int x, int y, const std::vector& values); + std::vector getColumn(int x, int y) const; + void clear(); + + int getSurfaceLevel(int x, int y) const; + int getSurfaceValue(int x, int y) const; + bool hasPositiveValues() const; + int remainingPositiveSum() const; + + int checkEffects(int x, int y); + int mine(int x, int y); + int mineAllPositive(int x, int y); + + double positiveAverage(int x, int y) const; + int topPositiveSum(int x, int y, int blocks) const; + void sortPositiveValuesInColumnAscending(int x, int y); + void rearrange(); + + void display() const; + void display(const std::vector>& robots) const; + +private: + int sizeX_; + int sizeY_; + int sizeZ_; + std::vector>> grid_; + + void init(); + void validateXY(int x, int y) const; + void validateZ(int z) const; +}; +``` + +### `src/World.cpp` + +```cpp +#include "World.h" +#include "Robot.h" + +#include +#include +#include +#include +#include +#include + +World::World(int x, int y, int z) + : sizeX_(x), sizeY_(y), sizeZ_(z), grid_(x, std::vector>(y)) { + if (x <= 0 || y <= 0 || z <= 0) throw std::invalid_argument("World dimensions must be positive."); + init(); +} + +void World::init() { + std::mt19937 rng{std::random_device{}()}; + std::uniform_int_distribution valueDist(1, 9); + std::uniform_int_distribution effectChance(1, 10); + std::uniform_int_distribution effectDist(1, 3); + for (int x = 0; x < sizeX_; ++x) + for (int y = 0; y < sizeY_; ++y) { + auto& col = grid_[x][y]; + col.clear(); + col.reserve(sizeZ_); + for (int z = 0; z < sizeZ_; ++z) + col.push_back(effectChance(rng) == 1 ? -effectDist(rng) : valueDist(rng)); + } +} + +void World::validateXY(int x, int y) const { + if (x < 0 || x >= sizeX_ || y < 0 || y >= sizeY_) throw std::out_of_range("World coordinate out of range."); +} + +void World::validateZ(int z) const { + if (z < 0 || z >= sizeZ_) throw std::out_of_range("World depth out of range."); +} + +int World::getValue(int x, int y, int z) const { + validateXY(x, y); validateZ(z); + const auto& col = grid_[x][y]; + return z < static_cast(col.size()) ? col[z] : 0; +} + +void World::setValue(int x, int y, int z, int value) { + validateXY(x, y); validateZ(z); + auto& col = grid_[x][y]; + if (value == 0) { + if (z < static_cast(col.size())) col.erase(col.begin() + z); + return; + } + if (z > static_cast(col.size())) throw std::logic_error("setValue would create holes; use setColumn instead."); + if (z == static_cast(col.size())) col.push_back(value); + else col[z] = value; +} + +void World::setColumn(int x, int y, const std::vector& values) { + validateXY(x, y); + if (static_cast(values.size()) > sizeZ_) throw std::out_of_range("Column is too deep."); + grid_[x][y] = values; +} + +std::vector World::getColumn(int x, int y) const { + validateXY(x, y); + return grid_[x][y]; +} + +void World::clear() { + for (auto& row : grid_) for (auto& col : row) col.clear(); +} + +int World::getSurfaceLevel(int x, int y) const { + validateXY(x, y); + const auto& col = grid_[x][y]; + return col.empty() ? -1 : static_cast(col.size()) - 1; +} + +int World::getSurfaceValue(int x, int y) const { + validateXY(x, y); + const auto& col = grid_[x][y]; + return col.empty() ? 0 : col.back(); +} + +int World::remainingPositiveSum() const { + int sum = 0; + for (const auto& row : grid_) for (const auto& col : row) for (int v : col) if (v > 0) sum += v; + return sum; +} + +bool World::hasPositiveValues() const { + return remainingPositiveSum() > 0; +} + +int World::checkEffects(int x, int y) { + validateXY(x, y); + auto& col = grid_[x][y]; + if (!col.empty() && col.back() < 0) { + int effect = col.back(); + col.pop_back(); + return effect; + } + return 0; +} + +int World::mine(int x, int y) { + validateXY(x, y); + auto& col = grid_[x][y]; + if (col.empty()) return 0; + int value = col.back(); + col.pop_back(); + return value > 0 ? value : 0; +} + +int World::mineAllPositive(int x, int y) { + validateXY(x, y); + auto& col = grid_[x][y]; + int total = 0; + std::vector kept; + kept.reserve(col.size()); + for (int v : col) { + if (v > 0) total += v; + else kept.push_back(v); + } + col = std::move(kept); + return total; +} + +double World::positiveAverage(int x, int y) const { + validateXY(x, y); + int sum = 0, count = 0; + for (int v : grid_[x][y]) if (v > 0) { sum += v; ++count; } + return count == 0 ? 0.0 : static_cast(sum) / count; +} + +int World::topPositiveSum(int x, int y, int blocks) const { + validateXY(x, y); + int sum = 0, count = 0; + const auto& col = grid_[x][y]; + for (auto it = col.rbegin(); it != col.rend() && count < blocks; ++it) + if (*it > 0) { sum += *it; ++count; } + return sum; +} + +void World::sortPositiveValuesInColumnAscending(int x, int y) { + validateXY(x, y); + auto& col = grid_[x][y]; + std::vector positives; + for (int v : col) if (v > 0) positives.push_back(v); + std::sort(positives.begin(), positives.end()); + auto it = positives.begin(); + for (int& v : col) if (v > 0) v = *it++; +} + +void World::rearrange() { + std::mt19937 rng{std::random_device{}()}; + std::uniform_int_distribution opDist(0, 2); + for (auto& row : grid_) for (auto& col : row) { + std::vector positives; + for (int v : col) if (v > 0) positives.push_back(v); + int op = opDist(rng); + if (op == 0) std::shuffle(positives.begin(), positives.end(), rng); + else if (op == 1) std::sort(positives.begin(), positives.end()); + else std::sort(positives.rbegin(), positives.rend()); + auto it = positives.begin(); + for (int& v : col) if (v > 0) v = *it++; + } +} + +void World::display() const { + std::cout << "\nWorld surface values:\n"; + for (int y = 0; y < sizeY_; ++y) { + for (int x = 0; x < sizeX_; ++x) { + int v = getSurfaceValue(x, y); + if (v == 0) std::cout << std::setw(4) << "--"; + else std::cout << std::setw(4) << v; + } + std::cout << "\n"; + } +} + +void World::display(const std::vector>& robots) const { + display(); + std::cout << "Robots:\n"; + for (const auto& r : robots) + std::cout << " " << r->getName() << " @ (" << r->getX() << "," << r->getY() << ")" + << " score=" << r->getScore() << " hp=" << r->getHp() + << (r->isAlive() ? "" : " [DEAD]") << "\n"; +} +``` + +### `include/BaseRobot.h` + +```cpp +#pragma once +#include "Robot.h" +#include + +class BaseRobot : public Robot { +public: + BaseRobot(std::string name, int startX, int startY); + ~BaseRobot() override = default; + + void move(int direction, const World& world) override; + int decideNextMove(const World& world) const override; + void setPosition(int x, int y) override; + + int getScore() const override { return score_; } + void addScore(int points) override { score_ += points; } + int getX() const override { return x_; } + int getY() const override { return y_; } + std::string getName() const override { return name_; } + int getHp() const override { return hp_; } + bool isAlive() const override { return hp_ > 0; } + void takeDamage(int damage) override; + +protected: + int x_ = 0; + int y_ = 0; + int score_ = 0; + std::string name_; + int hp_ = 100; + static constexpr int kMaxHp = 100; +}; +``` + +### `src/BaseRobot.cpp` + +```cpp +#include "BaseRobot.h" +#include "World.h" +#include +#include +#include +#include + +BaseRobot::BaseRobot(std::string name, int startX, int startY) + : x_(startX), y_(startY), name_(std::move(name)) {} + +void BaseRobot::move(int direction, const World& world) { + int nx = x_, ny = y_; + if (direction == 1) ++nx; + else if (direction == 2) --nx; + else if (direction == 3) ++ny; + else if (direction == 4) --ny; + x_ = std::clamp(nx, 0, world.getSizeX() - 1); + y_ = std::clamp(ny, 0, world.getSizeY() - 1); +} + +int BaseRobot::decideNextMove(const World& world) const { + const int dirs[5][3] = {{0,0,0},{1,0,1},{-1,0,2},{0,1,3},{0,-1,4}}; + int bestDir = 0, bestValue = -1; + for (const auto& d : dirs) { + int nx = x_ + d[0], ny = y_ + d[1]; + if (nx < 0 || nx >= world.getSizeX() || ny < 0 || ny >= world.getSizeY()) continue; + int v = world.getSurfaceValue(nx, ny); + if (v > bestValue) { bestValue = v; bestDir = d[2]; } + } + if (bestValue > 0) return bestDir; + + int bestDistance = std::numeric_limits::max(); + int targetX = x_, targetY = y_; + for (int x = 0; x < world.getSizeX(); ++x) + for (int y = 0; y < world.getSizeY(); ++y) + if (world.topPositiveSum(x, y, 1) > 0) { + int dist = std::abs(x - x_) + std::abs(y - y_); + if (dist < bestDistance) { bestDistance = dist; targetX = x; targetY = y; } + } + if (bestDistance == std::numeric_limits::max()) return 0; + if (targetX > x_) return 1; + if (targetX < x_) return 2; + if (targetY > y_) return 3; + if (targetY < y_) return 4; + return 0; +} + +void BaseRobot::setPosition(int x, int y) { x_ = x; y_ = y; } + +void BaseRobot::takeDamage(int damage) { + hp_ -= damage; + if (hp_ < 0) hp_ = 0; +} +``` + +### `include/SortBot.h` + +```cpp +#pragma once +#include "BaseRobot.h" +class SortBot : public BaseRobot { +public: + SortBot(int startX, int startY); + int mine(World& world) override; +}; +``` + +### `src/SortBot.cpp` + +```cpp +#include "SortBot.h" +#include "World.h" + +SortBot::SortBot(int startX, int startY) : BaseRobot("SortBot", startX, startY) {} + +int SortBot::mine(World& world) { + world.sortPositiveValuesInColumnAscending(x_, y_); + int mined = world.mine(x_, y_); + score_ += mined; + return mined; +} +``` + +### `include/DigDeepBot.h` + +```cpp +#pragma once +#include "BaseRobot.h" +class DigDeepBot : public BaseRobot { +public: + DigDeepBot(int startX, int startY); + int mine(World& world) override; +}; +``` + +### `src/DigDeepBot.cpp` + +```cpp +#include "DigDeepBot.h" +#include "World.h" + +DigDeepBot::DigDeepBot(int startX, int startY) : BaseRobot("DigDeepBot", startX, startY) {} + +int DigDeepBot::mine(World& world) { + int total = 0; + for (int i = 0; i < 3; ++i) { + int mined = world.mine(x_, y_); + if (mined <= 0) break; + total += mined; + } + score_ += total; + return total; +} +``` + +### `include/RandomBot.h` + +```cpp +#pragma once +#include "BaseRobot.h" +class RandomBot : public BaseRobot { +public: + RandomBot(int startX, int startY); + int mine(World& world) override; +}; +``` + +### `src/RandomBot.cpp` + +```cpp +#include "RandomBot.h" +#include "World.h" +#include + +RandomBot::RandomBot(int startX, int startY) : BaseRobot("RandomBot", startX, startY) {} + +int RandomBot::mine(World& world) { + static thread_local std::mt19937 rng{std::random_device{}()}; + std::uniform_int_distribution countDist(0, 9); + int attempts = countDist(rng); + int total = 0; + for (int i = 0; i < attempts; ++i) { + int mined = world.mine(x_, y_); + if (mined <= 0) break; + total += mined; + } + score_ += total; + return total; +} +``` + +### `include/SmartBot.h` + +```cpp +#pragma once +#include "BaseRobot.h" +class SmartBot : public BaseRobot { +public: + explicit SmartBot(int startX, int startY, int threshold = 5); + int mine(World& world) override; + int decideNextMove(const World& world) const override; +private: + int threshold_; +}; +``` + +### `src/SmartBot.cpp` + +```cpp +#include "SmartBot.h" +#include "World.h" + +SmartBot::SmartBot(int startX, int startY, int threshold) + : BaseRobot("SmartBot", startX, startY), threshold_(threshold) {} + +int SmartBot::mine(World& world) { + if (world.positiveAverage(x_, y_) <= threshold_) return 0; + int total = world.mineAllPositive(x_, y_); + score_ += total; + return total; +} + +int SmartBot::decideNextMove(const World& world) const { + const int dirs[5][3] = {{0,0,0},{1,0,1},{-1,0,2},{0,1,3},{0,-1,4}}; + int bestDir = 0; + double bestAverage = -1.0; + for (const auto& d : dirs) { + int nx = x_ + d[0], ny = y_ + d[1]; + if (nx < 0 || nx >= world.getSizeX() || ny < 0 || ny >= world.getSizeY()) continue; + double avg = world.positiveAverage(nx, ny); + if (avg > bestAverage) { bestAverage = avg; bestDir = d[2]; } + } + return bestDir; +} +``` + +### `include/LookaheadBot.h` + +```cpp +#pragma once +#include "BaseRobot.h" +class LookaheadBot : public BaseRobot { +public: + LookaheadBot(int startX, int startY); + int mine(World& world) override; + int decideNextMove(const World& world) const override; +private: + int lookaheadScore(const World& world, int x, int y, int depth) const; +}; +``` + +### `src/LookaheadBot.cpp` + +```cpp +#include "LookaheadBot.h" +#include "World.h" +#include + +LookaheadBot::LookaheadBot(int startX, int startY) : BaseRobot("LookaheadBot", startX, startY) {} + +int LookaheadBot::lookaheadScore(const World& world, int x, int y, int depth) const { + int here = world.topPositiveSum(x, y, 3); + if (depth == 0) return here; + int bestNext = 0; + const int moves[5][2] = {{0,0},{1,0},{-1,0},{0,1},{0,-1}}; + for (const auto& m : moves) { + int nx = x + m[0], ny = y + m[1]; + if (nx < 0 || nx >= world.getSizeX() || ny < 0 || ny >= world.getSizeY()) continue; + bestNext = std::max(bestNext, lookaheadScore(world, nx, ny, depth - 1)); + } + return here + bestNext; +} + +int LookaheadBot::decideNextMove(const World& world) const { + const int dirs[5][3] = {{0,0,0},{1,0,1},{-1,0,2},{0,1,3},{0,-1,4}}; + int bestDir = 0, bestScore = -1; + for (const auto& d : dirs) { + int nx = x_ + d[0], ny = y_ + d[1]; + if (nx < 0 || nx >= world.getSizeX() || ny < 0 || ny >= world.getSizeY()) continue; + int score = lookaheadScore(world, nx, ny, 1); + if (score > bestScore) { bestScore = score; bestDir = d[2]; } + } + return bestDir; +} + +int LookaheadBot::mine(World& world) { + int total = 0; + for (int i = 0; i < 3; ++i) { + int mined = world.mine(x_, y_); + if (mined <= 0) break; + total += mined; + } + score_ += total; + return total; +} +``` + +### `include/Game.h` + +```cpp +#pragma once +#include +#include +#include +#include +#include "Robot.h" +#include "ScopedTimer.h" +#include "World.h" + +class Game { +public: + Game(); + void run(); + +private: + World world_; + std::vector> robots_; + std::vector deadRobotScores_; + std::mutex turnMutex_; + int lastThreshold_ = 0; + int round_ = 0; + ScopedTimer::Clock::time_point programStart_; + std::vector threadTimes_; + + void setup(); + void robotLoop(int idx); + void fightNearby(Robot& attacker); + int computeWorldSum() const; + bool isGameOver() const; + void log(const std::string& message) const; + void checkRearrange(Robot& robot); + void applyEffect(Robot& robot, int effect); + void printScores() const; + void printResult() const; + std::unique_ptr createRobot(int choice, int x, int y) const; +}; +``` + +### `src/Game.cpp` + +```cpp +#include "Game.h" +#include "DigDeepBot.h" +#include "LookaheadBot.h" +#include "RandomBot.h" +#include "SmartBot.h" +#include "SortBot.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { +int validateInput(const std::string& prompt, int min, int max) { + int value = 0; + while (true) { + std::cout << prompt; + if (std::cin >> value && value >= min && value <= max) return value; + std::cout << "Please enter a number from " << min << " to " << max << ".\n"; + std::cin.clear(); + std::cin.ignore(10000, '\n'); + } +} +} + +Game::Game() : world_(5, 5, 10) {} + +void Game::run() { + std::cout << "=== PARALLEL DEEP MINER ===\n\n"; + setup(); + int initialSum = computeWorldSum(); + std::cout << "Initial world sum: " << initialSum << "\n"; + world_.display(robots_); + + programStart_ = ScopedTimer::Clock::now(); + std::vector threads; + threads.reserve(robots_.size()); + for (int i = 0; i < static_cast(robots_.size()); ++i) + threads.emplace_back(&Game::robotLoop, this, i); + for (auto& t : threads) t.join(); + + printScores(); + printResult(); + + int livingScore = 0; + for (const auto& r : robots_) if (r->isAlive()) livingScore += r->getScore(); + int deadScore = std::accumulate(deadRobotScores_.begin(), deadRobotScores_.end(), 0); + int remainingWorld = computeWorldSum(); + int conservedTotal = livingScore + deadScore + remainingWorld; + + std::cout << "\n--- Conservation Check ---\n" + << "Initial world sum : " << initialSum << "\n" + << "Living robot scores : " << livingScore << "\n" + << "Dead robot scores : " << deadScore << "\n" + << "Remaining world sum : " << remainingWorld << "\n" + << "Conserved total : " << conservedTotal << "\n" + << (initialSum == conservedTotal ? "Conservation check: OK\n" : "Conservation check: MISMATCH\n"); +} + +void Game::setup() { + int n = validateInput("Number of robots (5-10): ", 5, 10); + const std::vector> starts = { + {0,0}, {4,4}, {0,4}, {4,0}, {2,2}, {0,2}, {4,2}, {2,0}, {2,4}, {1,1} + }; + robots_.reserve(n); + threadTimes_.resize(n); + for (int i = 0; i < n; ++i) { + std::cout << "Robot " << (i + 1) << ":\n"; + int type = validateInput(" Type (1=SortBot 2=DigDeepBot 3=RandomBot 4=SmartBot 5=LookaheadBot): ", 1, 5); + auto [x, y] = starts[i % starts.size()]; + robots_.push_back(createRobot(type, x, y)); + } +} + +void Game::robotLoop(int idx) { + ScopedTimer timer(threadTimes_[idx]); + Robot& robot = *robots_[idx]; + while (true) { + { + std::lock_guard lock(turnMutex_); + if (!robot.isAlive()) { + deadRobotScores_.push_back(robot.getScore()); + log("[DEAD] " + robot.getName() + " final score " + std::to_string(robot.getScore()) + " recorded."); + break; + } + if (isGameOver()) break; + + ++round_; + int dir = robot.decideNextMove(world_); + robot.move(dir, world_); + + int effect = world_.checkEffects(robot.getX(), robot.getY()); + if (effect < 0) applyEffect(robot, effect); + if (!robot.isAlive()) { + deadRobotScores_.push_back(robot.getScore()); + log("[DEAD] " + robot.getName() + " killed by effect; score recorded."); + break; + } + + fightNearby(robot); + + if (effect != -1) { + int mined = robot.mine(world_); + log(robot.getName() + " mined " + std::to_string(mined) + " points."); + } else { + log(robot.getName() + " is blocked and cannot mine this turn."); + } + checkRearrange(robot); + } + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } +} + +void Game::fightNearby(Robot& attacker) { + static std::mt19937 rng{std::random_device{}()}; + std::uniform_int_distribution damageDist(5, 25); + for (auto& targetPtr : robots_) { + Robot& target = *targetPtr; + if (&target == &attacker || !target.isAlive()) continue; + int dx = std::abs(attacker.getX() - target.getX()); + int dy = std::abs(attacker.getY() - target.getY()); + if (dx <= 1 && dy <= 1) { + int damage = damageDist(rng); + target.takeDamage(damage); + std::ostringstream oss; + oss << attacker.getName() << " attacked " << target.getName() + << " for " << damage << " dmg. HP=" << target.getHp() + << (target.isAlive() ? "" : " [DEAD]"); + log(oss.str()); + } + } +} + +int Game::computeWorldSum() const { return world_.remainingPositiveSum(); } +bool Game::isGameOver() const { return !world_.hasPositiveValues(); } +void Game::log(const std::string& message) const { std::cout << message << "\n"; } + +void Game::checkRearrange(Robot& robot) { + int threshold = robot.getScore() / 50; + if (threshold > lastThreshold_) { + lastThreshold_ = threshold; + world_.rearrange(); + log("*** " + robot.getName() + " reached a 50-point threshold. World rearranged. ***"); + } +} + +void Game::applyEffect(Robot& robot, int effect) { + std::ostringstream oss; + if (effect == -1) { + oss << "[EFFECT -1] " << robot.getName() << " is blocked this turn."; + } else if (effect == -2) { + int bestX = robot.getX(), bestY = robot.getY(); + int lowest = std::numeric_limits::max(); + for (int x = 0; x < world_.getSizeX(); ++x) + for (int y = 0; y < world_.getSizeY(); ++y) + if (world_.getSurfaceValue(x, y) < lowest) { + lowest = world_.getSurfaceValue(x, y); + bestX = x; bestY = y; + } + robot.setPosition(bestX, bestY); + oss << "[EFFECT -2] " << robot.getName() << " teleported to (" << bestX << "," << bestY << ")."; + } else if (effect == -3) { + robot.takeDamage(30); + oss << "[EFFECT -3] " << robot.getName() << " takes 30 HP damage. HP=" << robot.getHp(); + } + log(oss.str()); +} + +void Game::printScores() const { + std::cout << "\n--- Scores ---\n"; + for (const auto& r : robots_) + std::cout << r->getName() << " score=" << r->getScore() << " hp=" << r->getHp() + << (r->isAlive() ? "" : " [DEAD]") << "\n"; +} + +void Game::printResult() const { + ScopedTimer::Duration total = ScopedTimer::Clock::now() - programStart_; + std::cout << "\n--- Thread Timing ---\n"; + for (int i = 0; i < static_cast(robots_.size()); ++i) + std::cout << "Thread " << i << " [" << robots_[i]->getName() << "]: " + << std::fixed << std::setprecision(3) << threadTimes_[i].count() << " s\n"; + std::cout << "Total wall-clock time: " << std::fixed << std::setprecision(3) << total.count() << " s\n"; +} + +std::unique_ptr Game::createRobot(int choice, int x, int y) const { + if (choice == 1) return std::make_unique(x, y); + if (choice == 2) return std::make_unique(x, y); + if (choice == 3) return std::make_unique(x, y); + if (choice == 4) return std::make_unique(x, y); + if (choice == 5) return std::make_unique(x, y); + throw std::invalid_argument("Unknown robot type."); +} +``` + +### `tests/test_all.cpp` + +```cpp +#include "DigDeepBot.h" +#include "LookaheadBot.h" +#include "SmartBot.h" +#include "SortBot.h" +#include "World.h" + +#include +#include +#include +#include +#include + +namespace { +int s_passed = 0; +int s_failed = 0; + +void assertTrue(bool value, const std::string& expr, int line) { + if (!value) throw std::runtime_error("Assertion failed at line " + std::to_string(line) + ": " + expr); +} + +template +void assertEq(const A& actual, const B& expected, const std::string& expr, int line) { + if (!(actual == expected)) { + std::ostringstream oss; + oss << "Assertion failed at line " << line << ": " << expr + << " actual=" << actual << " expected=" << expected; + throw std::runtime_error(oss.str()); + } +} + +#define ASSERT(expr) assertTrue((expr), #expr, __LINE__) +#define ASSERT_EQ(actual, expected) assertEq((actual), (expected), #actual " == " #expected, __LINE__) +#define TEST(name, body) do { try { body; ++s_passed; std::cout << "[PASS] " << name << "\n"; } catch (const std::exception& e) { ++s_failed; std::cout << "[FAIL] " << name << ": " << e.what() << "\n"; } } while (false) + +World makeEmptyWorld(int x = 5, int y = 5, int z = 10) { + World world(x, y, z); + world.clear(); + return world; +} + +void test_world_stack_model() { + TEST("mine pops surface values", { + World world = makeEmptyWorld(2, 2, 5); + world.setColumn(0, 0, {1, 2, 3}); + ASSERT_EQ(world.mine(0, 0), 3); + ASSERT_EQ(world.mine(0, 0), 2); + ASSERT_EQ(world.mine(0, 0), 1); + ASSERT_EQ(world.mine(0, 0), 0); + }); + TEST("surface effect is removed by checkEffects", { + World world = makeEmptyWorld(2, 2, 5); + world.setColumn(0, 0, {5, -3}); + ASSERT_EQ(world.checkEffects(0, 0), -3); + ASSERT_EQ(world.getSurfaceValue(0, 0), 5); + }); +} + +void test_hp_and_movement() { + TEST("damage clamps at zero", { + SortBot bot(0, 0); + bot.takeDamage(200); + ASSERT_EQ(bot.getHp(), 0); + ASSERT(!bot.isAlive()); + }); + TEST("movement clamps to boundaries", { + World world = makeEmptyWorld(3, 3, 3); + SortBot bot(0, 0); + bot.move(2, world); + bot.move(4, world); + ASSERT_EQ(bot.getX(), 0); + ASSERT_EQ(bot.getY(), 0); + }); +} + +void test_robot_mining() { + TEST("SortBot mines highest after sorting", { + World world = makeEmptyWorld(3, 3, 5); + world.setColumn(1, 1, {2, 9, 4}); + SortBot bot(1, 1); + ASSERT_EQ(bot.mine(world), 9); + ASSERT_EQ(bot.getScore(), 9); + }); + TEST("DigDeepBot mines up to three blocks", { + World world = makeEmptyWorld(3, 3, 5); + world.setColumn(1, 1, {1, 2, 3, 4}); + DigDeepBot bot(1, 1); + ASSERT_EQ(bot.mine(world), 9); + }); + TEST("SmartBot uses average threshold", { + World world = makeEmptyWorld(3, 3, 5); + world.setColumn(1, 1, {8, -1, 7}); + SmartBot bot(1, 1, 5); + ASSERT_EQ(bot.mine(world), 15); + }); +} + +void test_lookahead() { + TEST("LookaheadBot moves toward best yield", { + World world = makeEmptyWorld(3, 3, 5); + world.setColumn(2, 1, {9}); + LookaheadBot bot(1, 1); + ASSERT_EQ(bot.decideNextMove(world), 1); + }); +} +} + +int main() { + std::cout << "=== Deep Miner Tests ===\n"; + test_world_stack_model(); + test_hp_and_movement(); + test_robot_mining(); + test_lookahead(); + std::cout << "\n=== Results: " << s_passed << " passed, " << s_failed << " failed ===\n"; + return s_failed > 0 ? 1 : 0; +} +``` diff --git a/guide/SDL2_GUI_EXTENSION_GUIDE.md b/guide/SDL2_GUI_EXTENSION_GUIDE.md new file mode 100644 index 0000000..0b4ece9 --- /dev/null +++ b/guide/SDL2_GUI_EXTENSION_GUIDE.md @@ -0,0 +1,965 @@ +# 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 +``` diff --git a/include/World.h b/include/World.h index ee8bc60..d893435 100644 --- a/include/World.h +++ b/include/World.h @@ -10,9 +10,9 @@ class World { public: explicit World ( int x = 5, int y = 5, int z = 10 ); - int getSizeX () const { return _sizeX; } - int getSizeY () const { return _sizeY; } - int getSizeZ () const { return _sizeZ; } + int getSizeX () const { return sizeX_; } + int getSizeY () const { return sizeY_; } + int getSizeZ () const { return sizeZ_; } int getValue ( int x, int y, int z ) const; void setValue ( int x, int y, int z, int value ); @@ -30,7 +30,7 @@ class World { int checkEffects ( int x, int y ); int mineAllPositive ( int x, int y ); - double posiveAverage ( int x, int y ) const; + double positiveAverage ( int x, int y ) const; int topPositiveSum ( int x, int y, int blocks ) const; void sortPositiveValuesInColumnAscending ( int x, int y ); void rearrange (); diff --git a/src/World.cpp b/src/World.cpp index 6a66dc9..79e9c95 100644 --- a/src/World.cpp +++ b/src/World.cpp @@ -143,6 +143,54 @@ int World::mine ( int x, int y ) { return value > 0 ? value : 0; } +int World::mineAllPositive( int x, int y ) { + validateXY(x, y ); + auto& col = grid_[x][y]; + int total = 0; + std::vector kept; + kept.reserve( col.size() ); + for ( int v : col ) { + if ( v > 0 ) { + total += v; + } else { + kept.push_back( v ); + } + } + col = std::move(kept ); + return total; +} + +double World::positiveAverage(int x, int y ) const { + validateXY(x, y ); + int sum = 0, count = 0; + for ( int v : grid_[x][y] ) { + if ( v > 0 ) { + sum += v; + count++; + } + } + return count ==0 ? 0.0f : static_cast (sum) / count; +} + +int World::topPositiveSum(int x, int y, int blocks ) const { + validateXY( x, y ); + int sum = 0, count =0; + const auto& col = grid_[x][z]; + for ( auto it = col.rbegin(); it != col.rend() && count < blocks; ++it ) { + if ( *it > 0 ) { + sum += *it; + ++count; + } + } + return sum; +} + +void World::sortPositiveValuesInColumnAscending( int x, int y ) { + validateXY ( x, y ); + auto + +} + /* Print a 2D surface view with optional player/computer markers. Marker legend is shown when at least one robot position is provided. Complexity: O(x × y × z) due to per-cell surface lookup */