Files
deepMiner/guide/PARALLEL_DEEP_MINER_GUIDE.md
fegger 2a79d42bd6 World updates
Update World API and implementation:
rename size fields to sizeX_/sizeY_/sizeZ_, add helpers such as
mineAllPositive, positiveAverage, topPositiveSum and a sort helper,
and fix related surface/mine semantics for dynamic columns.
2026-04-26 17:48:28 +02:00

65 KiB
Raw Permalink Blame History

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
  2. Architecture
  3. Core Components
  4. Build and Execution
  5. Assignment Levels
  6. Writing Tests
  7. Design Principles and Best Practices
  8. Common Pitfalls and Solutions
  9. Advanced Extension Ideas
  10. Project Structure
  11. 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 510, 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

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

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

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<std::unique_ptr<Robot>>.
  • 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:

std::vector<std::vector<std::vector<int>>> 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.

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<int> 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<std::vector<std::vector<int>>> grid_;
};

Surface level

int World::getSurfaceLevel(int x, int y) const {
    validateXY(x, y);
    const auto& col = grid_[x][y];
    return col.empty() ? -1 : static_cast<int>(col.size()) - 1;
}

This is O(1), because the column vector already knows its size.

Mining

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.

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:

int World::collectPositiveColumn(int x, int y) {
    validateXY(x, y);
    auto& col = grid_[x][y];

    int total = 0;
    std::vector<int> 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

void World::clear() {
    for (auto& row : grid_)
        for (auto& col : row)
            col.clear();
}

void World::setColumn(int x, int y, std::vector<int> values) {
    validateXY(x, y);
    if (static_cast<int>(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.

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.

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:

0 = stay
1 = x + 1
2 = x - 1
3 = y + 1
4 = y - 1

Clamp movement to world boundaries:

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.

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.

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.

int RandomBot::mine(World& world) {
    static thread_local std::mt19937 rng{std::random_device{}()};
    std::uniform_int_distribution<int> 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.

// include/ScopedTimer.h
#pragma once

#include <chrono>

class ScopedTimer {
public:
    using Clock = std::chrono::steady_clock;
    using Duration = std::chrono::duration<double>;

    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.

// include/Game.h
#pragma once

#include <chrono>
#include <memory>
#include <mutex>
#include <string>
#include <thread>
#include <vector>

#include "Robot.h"
#include "ScopedTimer.h"
#include "World.h"

class Game {
public:
    Game();
    void run();

private:
    using Duration = ScopedTimer::Duration;

    World world_;
    std::vector<std::unique_ptr<Robot>> robots_;

    std::vector<int> deadRobotScores_;
    std::vector<bool> deathRecorded_;

    std::mutex turnMutex_;
    int lastThreshold_ = 0;

    std::vector<Duration> 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<Robot> 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_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:

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()

void Game::setup() {
    const int n = validateInput("Number of robots (5-10): ", 5, 10);

    const std::vector<std::pair<int, int>> 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()

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<std::thread> threads;
        threads.reserve(robots_.size());

        for (int i = 0; i < static_cast<int>(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<int>(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:

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()

void Game::robotLoop(int idx) {
    ScopedTimer threadTimer(threadTimes_[idx]);
    Robot& robot = *robots_[idx];

    while (true) {
        {
            std::lock_guard<std::mutex> lk(turnMutex_);

            if (!robot.isAlive()) {
                recordDeath(idx, "killed earlier");
                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()

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

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:

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

using Duration = ScopedTimer::Duration;

std::vector<Duration> threadTimes_;
Duration totalTime_{};

Thread measurement

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

{
    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()

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<int>::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()

void Game::fightNearby(Robot& attacker) {
    static std::mt19937 rng{std::random_device{}()};
    std::uniform_int_distribution<int> 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

#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

#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<double>(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

// setup() prompt
int type = validateInput(
    "  Type (1=SortBot  2=DigDeepBot  3=RandomBot  4=SmartBot): ", 1, 4);

// createRobot()
case 4:
    return std::make_unique<SmartBot>(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

#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

#include "../include/LookaheadBot.h"
#include "../include/World.h"

#include <algorithm>

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

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().

static World makeEmptyWorld(int x = 5, int y = 5, int z = 10) {
    World w(x, y, z);
    w.clear();
    return w;
}

HP tests

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

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

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<Robot> releases robots automatically.
  • std::lock_guard<std::mutex> releases the mutex automatically.
  • ScopedTimer stores elapsed time automatically.

Const-correctness

Make read-only methods const:

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.

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<std::unique_ptr<Robot>>. The terminal guide should not mention player_, computer_, autoMode_, or play(Robot&, bool).


9. Advanced Extension Ideas

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

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:

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_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

#include "Game.h"
#include <exception>
#include <iostream>

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

#pragma once
#include <string>
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

#pragma once
#include <chrono>

class ScopedTimer {
public:
    using Clock = std::chrono::steady_clock;
    using Duration = std::chrono::duration<double>;

    explicit ScopedTimer(Duration& output);
    ~ScopedTimer();

    ScopedTimer(const ScopedTimer&) = delete;
    ScopedTimer& operator=(const ScopedTimer&) = delete;

private:
    Duration& output_;
    Clock::time_point start_;
};

src/ScopedTimer.cpp

#include "ScopedTimer.h"

ScopedTimer::ScopedTimer(Duration& output)
    : output_(output), start_(Clock::now()) {}

ScopedTimer::~ScopedTimer() {
    output_ = Clock::now() - start_;
}

include/World.h

#pragma once
#include <memory>
#include <vector>
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<int>& values);
    std::vector<int> 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<std::unique_ptr<Robot>>& robots) const;

private:
    int sizeX_;
    int sizeY_;
    int sizeZ_;
    std::vector<std::vector<std::vector<int>>> grid_;

    void init();
    void validateXY(int x, int y) const;
    void validateZ(int z) const;
};

src/World.cpp

#include "World.h"
#include "Robot.h"

#include <algorithm>
#include <iomanip>
#include <iostream>
#include <random>
#include <stdexcept>
#include <utility>

World::World(int x, int y, int z)
    : sizeX_(x), sizeY_(y), sizeZ_(z), grid_(x, std::vector<std::vector<int>>(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<int> valueDist(1, 9);
    std::uniform_int_distribution<int> effectChance(1, 10);
    std::uniform_int_distribution<int> 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<int>(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<int>(col.size())) col.erase(col.begin() + z);
        return;
    }
    if (z > static_cast<int>(col.size())) throw std::logic_error("setValue would create holes; use setColumn instead.");
    if (z == static_cast<int>(col.size())) col.push_back(value);
    else col[z] = value;
}

void World::setColumn(int x, int y, const std::vector<int>& values) {
    validateXY(x, y);
    if (static_cast<int>(values.size()) > sizeZ_) throw std::out_of_range("Column is too deep.");
    grid_[x][y] = values;
}

std::vector<int> 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<int>(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<int> 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<double>(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<int> 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<int> opDist(0, 2);
    for (auto& row : grid_) for (auto& col : row) {
        std::vector<int> 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<std::unique_ptr<Robot>>& 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

#pragma once
#include "Robot.h"
#include <string>

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

#include "BaseRobot.h"
#include "World.h"
#include <algorithm>
#include <cstdlib>
#include <limits>
#include <utility>

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<int>::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<int>::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

#pragma once
#include "BaseRobot.h"
class SortBot : public BaseRobot {
public:
    SortBot(int startX, int startY);
    int mine(World& world) override;
};

src/SortBot.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

#pragma once
#include "BaseRobot.h"
class DigDeepBot : public BaseRobot {
public:
    DigDeepBot(int startX, int startY);
    int mine(World& world) override;
};

src/DigDeepBot.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

#pragma once
#include "BaseRobot.h"
class RandomBot : public BaseRobot {
public:
    RandomBot(int startX, int startY);
    int mine(World& world) override;
};

src/RandomBot.cpp

#include "RandomBot.h"
#include "World.h"
#include <random>

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<int> 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

#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

#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

#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

#include "LookaheadBot.h"
#include "World.h"
#include <algorithm>

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

#pragma once
#include <memory>
#include <mutex>
#include <string>
#include <vector>
#include "Robot.h"
#include "ScopedTimer.h"
#include "World.h"

class Game {
public:
    Game();
    void run();

private:
    World world_;
    std::vector<std::unique_ptr<Robot>> robots_;
    std::vector<int> deadRobotScores_;
    std::mutex turnMutex_;
    int lastThreshold_ = 0;
    int round_ = 0;
    ScopedTimer::Clock::time_point programStart_;
    std::vector<ScopedTimer::Duration> 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<Robot> createRobot(int choice, int x, int y) const;
};

src/Game.cpp

#include "Game.h"
#include "DigDeepBot.h"
#include "LookaheadBot.h"
#include "RandomBot.h"
#include "SmartBot.h"
#include "SortBot.h"

#include <chrono>
#include <cstdlib>
#include <iomanip>
#include <iostream>
#include <limits>
#include <numeric>
#include <random>
#include <sstream>
#include <stdexcept>
#include <thread>
#include <utility>

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<std::thread> threads;
    threads.reserve(robots_.size());
    for (int i = 0; i < static_cast<int>(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<std::pair<int,int>> 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<std::mutex> 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<int> 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<int>::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<int>(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<Robot> Game::createRobot(int choice, int x, int y) const {
    if (choice == 1) return std::make_unique<SortBot>(x, y);
    if (choice == 2) return std::make_unique<DigDeepBot>(x, y);
    if (choice == 3) return std::make_unique<RandomBot>(x, y);
    if (choice == 4) return std::make_unique<SmartBot>(x, y);
    if (choice == 5) return std::make_unique<LookaheadBot>(x, y);
    throw std::invalid_argument("Unknown robot type.");
}

tests/test_all.cpp

#include "DigDeepBot.h"
#include "LookaheadBot.h"
#include "SmartBot.h"
#include "SortBot.h"
#include "World.h"

#include <exception>
#include <iostream>
#include <sstream>
#include <stdexcept>
#include <string>

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 <typename A, typename B>
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;
}