# 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; } ```