2a79d42bd6
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.
2427 lines
65 KiB
Markdown
2427 lines
65 KiB
Markdown
# 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<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:
|
||
|
||
```cpp
|
||
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.
|
||
|
||
#### 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<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
|
||
|
||
```cpp
|
||
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
|
||
|
||
```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<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
|
||
|
||
```cpp
|
||
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.
|
||
|
||
```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<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.
|
||
|
||
```cpp
|
||
// 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.
|
||
|
||
```cpp
|
||
// 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
|
||
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<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()
|
||
|
||
```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<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:
|
||
|
||
```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<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()
|
||
|
||
```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<Duration> 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<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()
|
||
|
||
```cpp
|
||
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
|
||
|
||
```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<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
|
||
|
||
```cpp
|
||
// 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
|
||
|
||
```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 <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
|
||
|
||
```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<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`:
|
||
|
||
```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<std::unique_ptr<Robot>>`. 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 <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`
|
||
|
||
```cpp
|
||
#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`
|
||
|
||
```cpp
|
||
#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`
|
||
|
||
```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 <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`
|
||
|
||
```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`
|
||
|
||
```cpp
|
||
#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`
|
||
|
||
```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`
|
||
|
||
```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 <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`
|
||
|
||
```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 <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`
|
||
|
||
```cpp
|
||
#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`
|
||
|
||
```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`
|
||
|
||
```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;
|
||
}
|
||
```
|