Files
fegger 1680388189 Add SDL2 GUI support and fix World surface logic
- Integrate SDL2 GUI rendering into the Game class
- Update World::getSurfaceLevel to correctly identify surface height
- Improve ASSERT_EQ macro to avoid multiple evaluation of arguments
- Fix typo in main_gui.cpp include
- Update CMakeLists.txt to include Threads package
2026-04-29 02:57:41 +02:00

945 lines
31 KiB
C++

#include "../include/World.h"
#include "../include/BaseRobot.h"
#include "../include/SortBot.h"
#include "../include/DigDeepBot.h"
#include "../include/RandomBot.h"
#include "../include/SmartBot.h"
#include "../include/LookaheadBot.h"
#include <iostream>
#include <sstream>
#include <streambuf>
#include <numeric>
static int s_passed = 0;
static int s_failed = 0;
#define TEST(name, body) \
do { \
try { \
body \
std::cout << " [PASS] " name "\n"; \
++s_passed; \
} catch ( const std::exception& e ) { \
std::cout << " [FAIL] " name ": " << e.what() << "\n"; \
++s_failed; \
} catch ( ... ) { \
std::cout << " [FAIL] " name ": unknown exception\n"; \
++s_failed; \
} \
} while(0)
#define ASSERT(expr) \
if ( !(expr) ) throw std::runtime_error ( "ASSERT failed: " #expr )
#define ASSERT_EQ(a, b) \
do { \
auto _a = (a); auto _b = (b); \
if ( _a != _b ) { \
std::ostringstream _msg; \
_msg << #a << " == " << #b << " (" << _a << " != " << _b << ")"; \
throw std::runtime_error ( "ASSERT_EQ failed: " + _msg.str() ); \
} \
} while(0)
/* Redirect std::cout to a sink for the duration of the scope.
Useful for robot methods that print as a side-effect. */
struct SilentOut {
std::streambuf* saved;
SilentOut() { saved = std::cout.rdbuf ( nullptr ); }
~SilentOut() { std::cout.rdbuf ( saved ); }
};
/* Create a World with every cell zeroed so tests start from a known state.
init() is called by the constructor; we then overwrite all cells to 0. */
static World makeEmptyWorld ( int x = 5, int y = 5, int z = 10 ) {
World w ( x, y, z );
w.clear();
return w;
}
/* ── World: getValue / setValue ── */
static void test_world_get_set () {
std::cout << "\n── World: getValue / setValue ──\n";
TEST ( "set and get round-trips correctly", {
World w = makeEmptyWorld ( 3, 3, 5 );
w.setValue ( 1, 2, 0, 42 );
ASSERT_EQ ( w.getValue ( 1, 2, 0 ), 42 );
} );
TEST ( "initial zeroed cell returns 0", {
World w = makeEmptyWorld ( 3, 3, 5 );
ASSERT_EQ ( w.getValue ( 0, 0, 0 ), 0 );
} );
TEST ( "overwrite cell with new value", {
World w = makeEmptyWorld ( 3, 3, 5 );
w.setValue ( 0, 0, 0, 5 );
w.setValue ( 0, 0, 0, 9 );
ASSERT_EQ ( w.getValue ( 0, 0, 0 ), 9 );
} );
TEST ( "getValue throws on x out of range", {
World w = makeEmptyWorld ( 3, 3, 5 );
bool threw = false;
try { w.getValue ( 3, 0, 0 ); } catch ( const std::out_of_range& ) { threw = true; }
ASSERT ( threw );
} );
TEST ( "getValue throws on y out of range", {
World w = makeEmptyWorld ( 3, 3, 5 );
bool threw = false;
try { w.getValue ( 0, 3, 0 ); } catch ( const std::out_of_range& ) { threw = true; }
ASSERT ( threw );
} );
TEST ( "getValue throws on z out of range", {
World w = makeEmptyWorld ( 3, 3, 5 );
bool threw = false;
try { w.getValue ( 0, 0, 5 ); } catch ( const std::out_of_range& ) { threw = true; }
ASSERT ( threw );
} );
TEST ( "setValue throws on negative x", {
World w = makeEmptyWorld ( 3, 3, 5 );
bool threw = false;
try { w.setValue ( -1, 0, 0, 5 ); } catch ( const std::out_of_range& ) { threw = true; }
ASSERT ( threw );
} );
TEST ( "setValue throws on negative z", {
World w = makeEmptyWorld ( 3, 3, 5 );
bool threw = false;
try { w.setValue ( 0, 0, -1, 5 ); } catch ( const std::out_of_range& ) { threw = true; }
ASSERT ( threw );
} );
}
/* ── World: getSurfaceLevel ── */
static void test_world_surface_level () {
std::cout << "\n── World: getSurfaceLevel ──\n";
TEST ( "empty column returns -1", {
World w = makeEmptyWorld ( 3, 3, 5 );
ASSERT_EQ ( w.getSurfaceLevel ( 0, 0 ), -1 );
} );
TEST ( "single block at z=0 returns 0", {
World w = makeEmptyWorld ( 3, 3, 5 );
w.setValue ( 0, 0, 0, 7 );
ASSERT_EQ ( w.getSurfaceLevel ( 0, 0 ), 0 );
} );
TEST ( "returns highest positive z", {
World w = makeEmptyWorld ( 3, 3, 5 );
w.setColumn ( 0, 0, { 3, 0, 5, 0, 8 } ); // positives at z=0,2,4
ASSERT_EQ ( w.getSurfaceLevel ( 0, 0 ), 4 );
} );
TEST ( "ignores negative effect cells", {
World w = makeEmptyWorld ( 3, 3, 5 );
w.setColumn ( 0, 0, { 0, 6, 0, -2 } ); // positive at z=1, effect at z=3
ASSERT_EQ ( w.getSurfaceLevel ( 0, 0 ), 1 );
} );
TEST ( "effect above positive block is ignored; block is still surface", {
World w = makeEmptyWorld ( 3, 3, 5 );
w.setColumn ( 0, 0, { 0, 4, 0, -1 } ); // positive at z=1, effect above at z=3
ASSERT_EQ ( w.getSurfaceLevel ( 0, 0 ), 1 );
} );
TEST ( "all-effect column returns -1", {
World w = makeEmptyWorld ( 3, 3, 5 );
w.setValue ( 0, 0, 0, -1 );
w.setValue ( 0, 0, 1, -2 );
w.setValue ( 0, 0, 2, -3 );
ASSERT_EQ ( w.getSurfaceLevel ( 0, 0 ), -1 );
} );
TEST ( "does not bleed across columns", {
World w = makeEmptyWorld ( 3, 3, 5 );
w.setValue ( 0, 0, 0, 9 );
ASSERT_EQ ( w.getSurfaceLevel ( 1, 0 ), -1 );
} );
}
/* ── World: mine ── */
static void test_world_mine () {
std::cout << "\n── World: mine ──\n";
TEST ( "returns 0 on empty column", {
World w = makeEmptyWorld ( 3, 3, 5 );
ASSERT_EQ ( w.mine ( 0, 0 ), 0 );
} );
TEST ( "returns surface value and zeroes that cell", {
World w = makeEmptyWorld ( 3, 3, 5 );
w.setValue ( 0, 0, 0, 9 );
ASSERT_EQ ( w.mine ( 0, 0 ), 9 );
ASSERT_EQ ( w.getValue ( 0, 0, 0 ), 0 ); // column now empty, virtual 0
} );
TEST ( "lower block becomes new surface after mining", {
World w = makeEmptyWorld ( 3, 3, 5 );
w.setColumn ( 0, 0, { 4, 7 } ); // z=0:4, z=1:7
w.mine ( 0, 0 ); // mines z=1 (value 7)
ASSERT_EQ ( w.getSurfaceLevel ( 0, 0 ), 0 );
} );
TEST ( "column empty after mining sole block", {
World w = makeEmptyWorld ( 3, 3, 5 );
w.setValue ( 0, 0, 0, 3 );
w.mine ( 0, 0 );
ASSERT_EQ ( w.getSurfaceLevel ( 0, 0 ), -1 );
} );
}
/* ── World: checkEffects ── */
static void test_world_check_effects () {
std::cout << "\n── World: checkEffects ──\n";
TEST ( "returns 0 when no effects in column", {
World w = makeEmptyWorld ( 3, 3, 5 );
w.setValue ( 0, 0, 0, 5 );
ASSERT_EQ ( w.checkEffects ( 0, 0 ), 0 );
} );
TEST ( "returns the effect value when present", {
World w = makeEmptyWorld ( 3, 3, 5 );
w.setValue ( 0, 0, 0, -2 );
ASSERT_EQ ( w.checkEffects ( 0, 0 ), -2 );
} );
TEST ( "consumes the effect cell (becomes 0)", {
World w = makeEmptyWorld ( 3, 3, 5 );
w.setValue ( 0, 0, 0, -3 );
w.checkEffects ( 0, 0 );
ASSERT_EQ ( w.getValue ( 0, 0, 0 ), 0 ); // column now empty, virtual 0
} );
TEST ( "second effect still present after first is consumed", {
World w = makeEmptyWorld ( 3, 3, 5 );
w.setColumn ( 0, 0, { -1, -2 } ); // -1 at z=0, -2 at z=1 (top)
w.checkEffects ( 0, 0 ); // consumes -2 (top)
const int remaining = w.checkEffects ( 0, 0 ); // -1 is now top
ASSERT ( remaining < 0 );
} );
TEST ( "returns 0 for fully empty column", {
World w = makeEmptyWorld ( 3, 3, 5 );
ASSERT_EQ ( w.checkEffects ( 0, 0 ), 0 );
} );
}
/* ── World: rearrange (effect semantics) ── */
static void test_world_rearrange_effects () {
std::cout << "\n── World: rearrange (effect semantics) ──\n";
TEST ( "effect cells stay at their original z-position across rearrangements", {
World w = makeEmptyWorld ( 3, 3, 5 );
w.setValue ( 0, 0, 0, -1 ); // effect at z=0
w.setValue ( 0, 0, 1, 5 ); // minable blocks
w.setValue ( 0, 0, 2, 3 );
for ( int i = 0; i < 20; ++i ) {
SilentOut s;
w.rearrange ();
ASSERT_EQ ( w.getValue ( 0, 0, 0 ), -1 );
}
} );
TEST ( "positive values are preserved (same sum) after rearrange", {
World w = makeEmptyWorld ( 3, 3, 5 );
w.setColumn ( 0, 0, { 3, 7 } ); // positives at z=0,1; sum=10
SilentOut s;
w.rearrange ();
int sum = 0;
for ( int z = 0; z < w.getSizeZ (); ++z )
if ( w.getValue ( 0, 0, z ) > 0 )
sum += w.getValue ( 0, 0, z );
ASSERT_EQ ( sum, 10 );
} );
TEST ( "effect value is preserved after rearrange", {
World w = makeEmptyWorld ( 3, 3, 5 );
w.setColumn ( 0, 0, { 3, 4, -3, 5, 6 } ); // effect at z=2, positives around it
SilentOut s;
w.rearrange ();
ASSERT_EQ ( w.getValue ( 0, 0, 2 ), -3 );
} );
}
/* ── BaseRobot: move ── */
static void test_baserobot_move () {
std::cout << "\n── BaseRobot: move ──\n";
TEST ( "direction 1 moves right (+x)", {
World w = makeEmptyWorld ();
SortBot bot ( 0, 0 );
bot.move ( 1, w );
ASSERT_EQ ( bot.getX (), 1 );
} );
TEST ( "direction 2 moves left (-x)", {
World w = makeEmptyWorld ();
SortBot bot ( 2, 2 );
bot.move ( 2, w );
ASSERT_EQ ( bot.getX (), 1 );
} );
TEST ( "direction 3 moves down (+y)", {
World w = makeEmptyWorld ();
SortBot bot ( 0, 0 );
bot.move ( 3, w );
ASSERT_EQ ( bot.getY (), 1 );
} );
TEST ( "direction 4 moves up (-y)", {
World w = makeEmptyWorld ();
SortBot bot ( 2, 2 );
bot.move ( 4, w );
ASSERT_EQ ( bot.getY (), 1 );
} );
TEST ( "direction 0 stays in place", {
World w = makeEmptyWorld ();
SortBot bot ( 2, 3 );
bot.move ( 0, w );
ASSERT_EQ ( bot.getX (), 2 );
ASSERT_EQ ( bot.getY (), 3 );
} );
TEST ( "clamps at right boundary (x = sizeX - 1)", {
World w = makeEmptyWorld ();
SortBot bot ( 4, 0 );
bot.move ( 1, w );
ASSERT_EQ ( bot.getX (), 4 );
} );
TEST ( "clamps at left boundary (x = 0)", {
World w = makeEmptyWorld ();
SortBot bot ( 0, 0 );
bot.move ( 2, w );
ASSERT_EQ ( bot.getX (), 0 );
} );
TEST ( "clamps at bottom boundary (y = sizeY - 1)", {
World w = makeEmptyWorld ();
SortBot bot ( 0, 4 );
bot.move ( 3, w );
ASSERT_EQ ( bot.getY (), 4 );
} );
TEST ( "clamps at top boundary (y = 0)", {
World w = makeEmptyWorld ();
SortBot bot ( 0, 0 );
bot.move ( 4, w );
ASSERT_EQ ( bot.getY (), 0 );
} );
}
/* ── BaseRobot: decideNextMove ── */
static void test_baserobot_decide () {
std::cout << "\n── BaseRobot: decideNextMove ──\n";
TEST ( "returns 0 (stay) when world is fully empty", {
World w = makeEmptyWorld ();
SortBot bot ( 2, 2 );
ASSERT_EQ ( bot.decideNextMove ( w ), 0 );
} );
TEST ( "chooses neighbour with highest surface value", {
World w = makeEmptyWorld ();
w.setValue ( 3, 2, 0, 9 ); // right neighbour of (2,2): value 9
w.setValue ( 1, 2, 0, 2 ); // left neighbour of (2,2): value 2
SortBot bot ( 2, 2 );
ASSERT_EQ ( bot.decideNextMove ( w ), 1 ); // direction 1 = right (+x)
} );
TEST ( "stays on current cell if it has highest value", {
World w = makeEmptyWorld ();
w.setValue ( 2, 2, 0, 9 ); // current position: highest
w.setValue ( 3, 2, 0, 3 );
w.setValue ( 1, 2, 0, 2 );
SortBot bot ( 2, 2 );
ASSERT_EQ ( bot.decideNextMove ( w ), 0 ); // direction 0 = stay
} );
TEST ( "fallback: steps right toward the only non-empty column", {
World w = makeEmptyWorld ();
w.setValue ( 4, 4, 0, 5 ); // only non-empty column is at (4,4)
SortBot bot ( 0, 0 ); // no mineable neighbours
ASSERT_EQ ( bot.decideNextMove ( w ), 1 ); // tx=4 > x_=0, so direction 1 (right)
} );
TEST ( "fallback: steps down when target is directly below", {
World w = makeEmptyWorld ();
w.setValue ( 0, 4, 0, 5 ); // only non-empty column is at (0,4)
SortBot bot ( 0, 0 );
ASSERT_EQ ( bot.decideNextMove ( w ), 3 ); // tx==x_ and ty=4 > y_=0, so direction 3 (down)
} );
}
/* ── DigDeepBot: mine ── */
static void test_digdeepbot_mine () {
std::cout << "\n── DigDeepBot: mine ──\n";
TEST ( "returns 0 on empty column", {
World w = makeEmptyWorld ();
DigDeepBot bot ( 0, 0 );
ASSERT_EQ ( bot.mine ( w ), 0 );
} );
TEST ( "mines the only block in the column", {
World w = makeEmptyWorld ();
w.setValue ( 0, 0, 0, 5 );
DigDeepBot bot ( 0, 0 );
ASSERT_EQ ( bot.mine ( w ), 5 );
ASSERT_EQ ( w.getValue ( 0, 0, 0 ), 0 ); // column empty, virtual 0
} );
TEST ( "mines top 3 blocks and returns their sum", {
World w = makeEmptyWorld ( 3, 3, 5 );
w.setValue ( 0, 0, 0, 1 ); // z=0: below limit, should remain
w.setValue ( 0, 0, 1, 2 ); // z=1: mined 3rd
w.setValue ( 0, 0, 2, 3 ); // z=2: mined 2nd
w.setValue ( 0, 0, 3, 4 ); // z=3: mined 1st (top)
DigDeepBot bot ( 0, 0 );
ASSERT_EQ ( bot.mine ( w ), 4 + 3 + 2 );
ASSERT_EQ ( w.getValue ( 0, 0, 0 ), 1 ); // z=0 untouched
} );
TEST ( "mines fewer than 3 if column has only 2 blocks", {
World w = makeEmptyWorld ( 3, 3, 5 );
w.setColumn ( 0, 0, { 3, 6 } ); // z=0:3, z=1:6
DigDeepBot bot ( 0, 0 );
ASSERT_EQ ( bot.mine ( w ), 6 + 3 );
} );
TEST ( "skips effect cells (negative values)", {
World w = makeEmptyWorld ( 3, 3, 5 );
w.setColumn ( 0, 0, { -1, 7 } ); // effect at z=0, positive at z=1 (top)
DigDeepBot bot ( 0, 0 );
ASSERT_EQ ( bot.mine ( w ), 7 );
} );
TEST ( "score accumulates correctly across two mines", {
World w = makeEmptyWorld ();
w.setValue ( 0, 0, 0, 4 );
w.setValue ( 0, 0, 1, 6 );
DigDeepBot bot ( 0, 0 );
bot.mine ( w ); // score = 10
w.setValue ( 0, 0, 0, 3 ); // replenish column
bot.mine ( w ); // score = 13
ASSERT_EQ ( bot.getScore (), 13 );
} );
}
/* ── SortBot: mine ── */
static void test_sortbot_mine () {
std::cout << "\n── SortBot: mine ──\n";
TEST ( "returns 0 on empty column", {
World w = makeEmptyWorld ();
SortBot bot ( 0, 0 );
ASSERT_EQ ( bot.mine ( w ), 0 );
} );
TEST ( "returns the single block value", {
World w = makeEmptyWorld ();
w.setValue ( 0, 0, 0, 7 );
SortBot bot ( 0, 0 );
ASSERT_EQ ( bot.mine ( w ), 7 );
} );
TEST ( "always mines the highest value in the column", {
World w = makeEmptyWorld ( 3, 3, 5 );
w.setColumn ( 0, 0, { 3, 7, 1 } ); // values 3, 7, 1 at z=0,1,2
SortBot bot ( 0, 0 );
ASSERT_EQ ( bot.mine ( w ), 7 );
} );
TEST ( "column has correct remaining sum after mining highest", {
World w = makeEmptyWorld ( 3, 3, 5 );
w.setValue ( 0, 0, 0, 2 );
w.setValue ( 0, 0, 1, 5 );
w.setValue ( 0, 0, 2, 8 ); // mined
SortBot bot ( 0, 0 );
bot.mine ( w );
int remaining = 0;
for ( int z = 0; z < w.getSizeZ (); ++z )
if ( w.getValue ( 0, 0, z ) > 0 )
remaining += w.getValue ( 0, 0, z );
ASSERT_EQ ( remaining, 2 + 5 );
} );
TEST ( "score accumulates correctly", {
World w = makeEmptyWorld ( 3, 3, 5 );
w.setValue ( 0, 0, 0, 4 );
w.setValue ( 0, 0, 1, 9 );
SortBot bot ( 0, 0 );
bot.mine ( w ); // sort:[4,9], mines 9; col=[4]
w.setValue ( 0, 0, 1, 6 ); // col=[4,6]
bot.mine ( w ); // sort:[4,6], mines 6 (new highest)
ASSERT_EQ ( bot.getScore (), 15 );
} );
}
/* ── RandomBot: mine ── */
static void test_randombot_mine () {
std::cout << "\n── RandomBot: mine ──\n";
TEST ( "returns 0 on empty column regardless of limit", {
World w = makeEmptyWorld ();
RandomBot bot ( 0, 0 );
for ( int i = 0; i < 10; ++i )
ASSERT_EQ ( bot.mine ( w ), 0 );
} );
TEST ( "mined value is always non-negative", {
World w = makeEmptyWorld ( 3, 3, 5 );
for ( int z = 0; z < 5; ++z )
w.setValue ( 0, 0, z, z + 1 );
RandomBot bot ( 0, 0 );
for ( int i = 0; i < 20; ++i ) {
/* Restore column before each mine so the test is repeatable. */
for ( int z = 0; z < 5; ++z )
w.setValue ( 0, 0, z, z + 1 );
ASSERT ( bot.mine ( w ) >= 0 );
}
} );
TEST ( "never mines more than 9 blocks per turn", {
World w = makeEmptyWorld ( 3, 3, 10 );
for ( int z = 0; z < 10; ++z )
w.setValue ( 0, 0, z, 1 ); // 10 blocks each worth 1
RandomBot bot ( 0, 0 );
const int mined = bot.mine ( w );
ASSERT ( mined <= 9 ); // limit drawn from [0, 9]
} );
TEST ( "score is always non-negative after mining", {
World w = makeEmptyWorld ( 3, 3, 5 );
for ( int z = 0; z < 5; ++z )
w.setValue ( 0, 0, z, 3 );
RandomBot bot ( 0, 0 );
bot.mine ( w );
ASSERT ( bot.getScore () >= 0 );
} );
}
/* ── BaseRobot: score / position utilities ── */
static void test_baserobot_misc () {
std::cout << "\n── BaseRobot: score / position utilities ──\n";
TEST ( "initial score is 0", {
SortBot bot ( 0, 0 );
ASSERT_EQ ( bot.getScore (), 0 );
} );
TEST ( "addScore adds positive points", {
SortBot bot ( 0, 0 );
bot.addScore ( 10 );
ASSERT_EQ ( bot.getScore (), 10 );
} );
TEST ( "addScore with negative value decreases score", {
SortBot bot ( 0, 0 );
bot.addScore ( 20 );
bot.addScore ( -10 );
ASSERT_EQ ( bot.getScore (), 10 );
} );
TEST ( "setPosition updates x and y", {
SortBot bot ( 0, 0 );
bot.setPosition ( 3, 4 );
ASSERT_EQ ( bot.getX (), 3 );
ASSERT_EQ ( bot.getY (), 4 );
} );
TEST ( "getName returns correct name for SortBot", {
SortBot bot ( 0, 0 );
ASSERT ( bot.getName () == "SortBot" );
} );
TEST ( "getName returns correct name for DigDeepBot", {
DigDeepBot bot ( 0, 0 );
ASSERT ( bot.getName () == "DigDeepBot" );
} );
TEST ( "getName returns correct name for RandomBot", {
RandomBot bot ( 0, 0 );
ASSERT ( bot.getName () == "RandomBot" );
} );
}
/* ── World extra: hasPositiveValues, remainingPositiveSum, positiveAverage,
topPositiveSum, setColumn/getColumn, mineAllPositive,
sortPositiveValues ── */
static void test_world_extra () {
std::cout << "\n── World: hasPositiveValues / remainingPositiveSum ──\n";
TEST ( "empty world has no positive values", {
World w = makeEmptyWorld ( 3, 3, 5 );
ASSERT ( !w.hasPositiveValues () );
} );
TEST ( "world with one positive block has positive values", {
World w = makeEmptyWorld ( 3, 3, 5 );
w.setValue ( 0, 0, 0, 7 );
ASSERT ( w.hasPositiveValues () );
} );
TEST ( "remainingPositiveSum sums all positives across columns", {
World w = makeEmptyWorld ( 3, 3, 5 );
w.setValue ( 0, 0, 0, 3 );
w.setValue ( 1, 1, 0, 5 );
w.setValue ( 2, 2, 0, 8 );
ASSERT_EQ ( w.remainingPositiveSum (), 16 );
} );
TEST ( "remainingPositiveSum ignores negative effect cells", {
World w = makeEmptyWorld ( 3, 3, 5 );
w.setValue ( 0, 0, 0, -2 );
w.setValue ( 0, 0, 1, 4 );
ASSERT_EQ ( w.remainingPositiveSum (), 4 );
} );
std::cout << "\n── World: positiveAverage ──\n";
TEST ( "returns 0.0 for column with no positives", {
World w = makeEmptyWorld ( 3, 3, 5 );
ASSERT_EQ ( w.positiveAverage ( 0, 0 ), 0.0 );
} );
TEST ( "averages only positive values, skips effects", {
World w = makeEmptyWorld ( 3, 3, 5 );
w.setValue ( 0, 0, 0, -1 ); // effect; excluded from average
w.setValue ( 0, 0, 1, 4 );
w.setValue ( 0, 0, 2, 6 );
ASSERT_EQ ( w.positiveAverage ( 0, 0 ), 5.0 ); // (4+6)/2
} );
TEST ( "single positive value is its own average", {
World w = makeEmptyWorld ( 3, 3, 5 );
w.setValue ( 0, 0, 0, 9 );
ASSERT_EQ ( w.positiveAverage ( 0, 0 ), 9.0 );
} );
std::cout << "\n── World: topPositiveSum ──\n";
TEST ( "returns 0 for empty column", {
World w = makeEmptyWorld ( 3, 3, 5 );
ASSERT_EQ ( w.topPositiveSum ( 0, 0, 3 ), 0 );
} );
TEST ( "returns sum of top N positive blocks", {
World w = makeEmptyWorld ( 3, 3, 5 );
w.setValue ( 0, 0, 0, 2 );
w.setValue ( 0, 0, 1, 5 );
w.setValue ( 0, 0, 2, 8 );
ASSERT_EQ ( w.topPositiveSum ( 0, 0, 2 ), 13 ); // top 2 from surface: 8+5
} );
TEST ( "blocks=0 returns 0", {
World w = makeEmptyWorld ( 3, 3, 5 );
w.setValue ( 0, 0, 0, 7 );
ASSERT_EQ ( w.topPositiveSum ( 0, 0, 0 ), 0 );
} );
TEST ( "blocks > column size returns all positives", {
World w = makeEmptyWorld ( 3, 3, 5 );
w.setValue ( 0, 0, 0, 3 );
w.setValue ( 0, 0, 1, 4 );
ASSERT_EQ ( w.topPositiveSum ( 0, 0, 10 ), 7 );
} );
std::cout << "\n── World: setColumn / getColumn ──\n";
TEST ( "getColumn returns exactly what setColumn set", {
World w = makeEmptyWorld ( 3, 3, 5 );
w.setColumn ( 0, 0, { 3, 5, 7 } );
ASSERT ( w.getColumn ( 0, 0 ) == ( std::vector<int>{ 3, 5, 7 } ) );
} );
TEST ( "setColumn throws when vector too long", {
World w = makeEmptyWorld ( 3, 3, 5 ); // sizeZ_ = 5
bool threw = false;
try { w.setColumn ( 0, 0, { 1, 2, 3, 4, 5, 6 } ); } // size 6 > sizeZ_ 5
catch ( const std::out_of_range& ) { threw = true; }
ASSERT ( threw );
} );
TEST ( "setColumn of empty vector clears the column", {
World w = makeEmptyWorld ( 3, 3, 5 );
w.setValue ( 0, 0, 0, 7 );
w.setColumn ( 0, 0, {} );
ASSERT_EQ ( w.getSurfaceLevel ( 0, 0 ), -1 );
} );
std::cout << "\n── World: mineAllPositive ──\n";
TEST ( "returns 0 on empty column", {
World w = makeEmptyWorld ( 3, 3, 5 );
ASSERT_EQ ( w.mineAllPositive ( 0, 0 ), 0 );
} );
TEST ( "returns sum of all positive blocks", {
World w = makeEmptyWorld ( 3, 3, 5 );
w.setValue ( 0, 0, 0, 3 );
w.setValue ( 0, 0, 1, 7 );
w.setValue ( 0, 0, 2, 4 );
ASSERT_EQ ( w.mineAllPositive ( 0, 0 ), 14 );
} );
TEST ( "column empty after mineAllPositive with no effects", {
World w = makeEmptyWorld ( 3, 3, 5 );
w.setValue ( 0, 0, 0, 3 );
w.setValue ( 0, 0, 1, 7 );
w.setValue ( 0, 0, 2, 4 );
w.mineAllPositive ( 0, 0 );
ASSERT_EQ ( w.getSurfaceLevel ( 0, 0 ), -1 );
} );
TEST ( "effect cells remain after mineAllPositive", {
World w = makeEmptyWorld ( 3, 3, 5 );
w.setValue ( 0, 0, 0, -2 );
w.setValue ( 0, 0, 1, 5 );
ASSERT_EQ ( w.mineAllPositive ( 0, 0 ), 5 );
ASSERT_EQ ( w.checkEffects ( 0, 0 ), -2 );
} );
TEST ( "clear() empties every column", {
World w = makeEmptyWorld ( 3, 3, 5 );
w.setValue ( 0, 0, 0, 4 );
w.setValue ( 1, 1, 0, 7 );
w.clear ();
ASSERT_EQ ( w.getSurfaceLevel ( 0, 0 ), -1 );
ASSERT_EQ ( w.getSurfaceLevel ( 1, 1 ), -1 );
} );
std::cout << "\n── World: sortPositiveValues ──\n";
TEST ( "sort puts highest positive at the surface (top index)", {
World w = makeEmptyWorld ( 3, 3, 5 );
w.setValue ( 0, 0, 0, 7 );
w.setValue ( 0, 0, 1, 2 );
w.setValue ( 0, 0, 2, 5 );
// col = [7,2,5]; sorted ascending in-place -> [2,5,7]; col.back()=7
w.sortPositiveValues ( 0, 0 );
ASSERT_EQ ( w.getSurfaceValue ( 0, 0 ), 7 );
} );
TEST ( "negative cells are not moved by sort", {
World w = makeEmptyWorld ( 3, 3, 5 );
w.setValue ( 0, 0, 0, -3 ); // effect stays at z=0
w.setValue ( 0, 0, 1, 6 );
w.setValue ( 0, 0, 2, 1 );
w.sortPositiveValues ( 0, 0 );
ASSERT_EQ ( w.getValue ( 0, 0, 0 ), -3 );
} );
TEST ( "total sum unchanged after sort", {
World w = makeEmptyWorld ( 3, 3, 5 );
w.setValue ( 0, 0, 0, 4 );
w.setValue ( 0, 0, 1, 8 );
w.setValue ( 0, 0, 2, 2 );
w.sortPositiveValues ( 0, 0 );
int sum = 0;
for ( int z = 0; z < w.getSizeZ (); ++z )
if ( w.getValue ( 0, 0, z ) > 0 )
sum += w.getValue ( 0, 0, z );
ASSERT_EQ ( sum, 14 );
} );
}
/* ── BaseRobot: hp / takeDamage / isAlive ── */
static void test_baserobot_hp () {
std::cout << "\n── BaseRobot: hp / takeDamage / isAlive ──\n";
TEST ( "initial HP is MaxHp (100)", {
SortBot bot ( 0, 0 );
ASSERT_EQ ( bot.getHp (), 100 );
} );
TEST ( "isAlive() true when HP > 0", {
SortBot bot ( 0, 0 );
ASSERT ( bot.isAlive () );
} );
TEST ( "takeDamage reduces HP", {
SortBot bot ( 0, 0 );
bot.takeDamage ( 30 );
ASSERT_EQ ( bot.getHp (), 70 );
} );
TEST ( "takeDamage clamps HP to 0, never negative", {
SortBot bot ( 0, 0 );
bot.takeDamage ( 200 );
ASSERT_EQ ( bot.getHp (), 0 );
} );
TEST ( "isAlive() false when HP reaches 0", {
SortBot bot ( 0, 0 );
bot.takeDamage ( 200 );
ASSERT ( !bot.isAlive () );
} );
TEST ( "takeDamage(0) leaves HP unchanged", {
SortBot bot ( 0, 0 );
bot.takeDamage ( 0 );
ASSERT_EQ ( bot.getHp (), 100 );
} );
}
/* ── SmartBot: mine ── */
static void test_smartbot_mine () {
std::cout << "\n── SmartBot: mine ──\n";
TEST ( "returns 0 when positiveAverage is at or below threshold", {
World w = makeEmptyWorld ();
w.setValue ( 0, 0, 0, 3 ); // average = 3.0 <= threshold 5 → skip
SmartBot bot ( 0, 0, 5 );
ASSERT_EQ ( bot.mine ( w ), 0 );
} );
TEST ( "mines all positives when average exceeds threshold", {
World w = makeEmptyWorld ();
w.setValue ( 0, 0, 0, 6 );
w.setValue ( 0, 0, 1, 8 ); // average = 7.0 > threshold 5
SmartBot bot ( 0, 0, 5 );
ASSERT_EQ ( bot.mine ( w ), 14 );
} );
TEST ( "score accumulates after successful mine", {
World w = makeEmptyWorld ();
w.setValue ( 0, 0, 0, 6 );
w.setValue ( 0, 0, 1, 8 );
SmartBot bot ( 0, 0, 5 );
bot.mine ( w );
ASSERT_EQ ( bot.getScore (), 14 );
} );
TEST ( "does nothing on empty column (average=0)", {
World w = makeEmptyWorld ();
SmartBot bot ( 0, 0, 5 );
ASSERT_EQ ( bot.mine ( w ), 0 );
} );
}
/* ── SmartBot: decideNextMove ── */
static void test_smartbot_decide () {
std::cout << "\n── SmartBot: decideNextMove ──\n";
TEST ( "returns 0 on empty world", {
World w = makeEmptyWorld ();
SmartBot bot ( 2, 2 );
ASSERT_EQ ( bot.decideNextMove ( w ), 0 );
} );
TEST ( "prefers neighbour with highest average", {
World w = makeEmptyWorld ();
w.setValue ( 3, 2, 0, 9 ); // right of (2,2): positiveAverage = 9.0
w.setValue ( 1, 2, 0, 2 ); // left of (2,2): positiveAverage = 2.0
SmartBot bot ( 2, 2, 0 ); // threshold=0; decideNextMove picks by average
ASSERT_EQ ( bot.decideNextMove ( w ), 1 ); // direction 1 = right (+x)
} );
}
/* ── LookaheadBot: mine ── */
static void test_lookaheadbot_mine () {
std::cout << "\n── LookaheadBot: mine ──\n";
TEST ( "returns 0 on empty column", {
World w = makeEmptyWorld ();
LookaheadBot bot ( 0, 0 );
ASSERT_EQ ( bot.mine ( w ), 0 );
} );
TEST ( "mines up to 3 positive blocks", {
World w = makeEmptyWorld ();
w.setValue ( 0, 0, 0, 1 );
w.setValue ( 0, 0, 1, 2 );
w.setValue ( 0, 0, 2, 3 );
w.setValue ( 0, 0, 3, 4 ); // 4 blocks; only top 3 are mined
LookaheadBot bot ( 0, 0 );
ASSERT_EQ ( bot.mine ( w ), 4 + 3 + 2 ); // top 3: 9
} );
TEST ( "score accumulates", {
World w = makeEmptyWorld ();
w.setValue ( 0, 0, 0, 1 );
w.setValue ( 0, 0, 1, 2 );
w.setValue ( 0, 0, 2, 3 );
w.setValue ( 0, 0, 3, 4 );
LookaheadBot bot ( 0, 0 );
bot.mine ( w );
ASSERT_EQ ( bot.getScore (), 9 );
} );
}
/* ── LookaheadBot: decideNextMove ── */
static void test_lookaheadbot_decide () {
std::cout << "\n── LookaheadBot: decideNextMove ──\n";
TEST ( "returns 0 on empty world", {
World w = makeEmptyWorld ();
LookaheadBot bot ( 2, 2 );
ASSERT_EQ ( bot.decideNextMove ( w ), 0 );
} );
TEST ( "moves toward column with higher topPositiveSum", {
World w = makeEmptyWorld ();
w.setValue ( 3, 2, 0, 9 );
w.setValue ( 3, 2, 1, 8 );
w.setValue ( 3, 2, 2, 7 ); // right of (2,2): topPositiveSum(3)=24
w.setValue ( 1, 2, 0, 2 ); // left of (2,2): topPositiveSum(3)=2
LookaheadBot bot ( 2, 2 );
ASSERT_EQ ( bot.decideNextMove ( w ), 1 ); // direction 1 = right (+x)
} );
}
int main () {
std::cout << "=== Deep Miner Tests ===\n";
test_world_get_set ();
test_world_surface_level ();
test_world_mine ();
test_world_check_effects ();
test_world_rearrange_effects ();
test_baserobot_move ();
test_baserobot_decide ();
test_digdeepbot_mine ();
test_sortbot_mine ();
test_randombot_mine ();
test_baserobot_misc ();
test_world_extra ();
test_baserobot_hp ();
test_smartbot_mine ();
test_smartbot_decide ();
test_lookaheadbot_mine ();
test_lookaheadbot_decide ();
std::cout << "\n=== Results: "
<< s_passed << " passed, "
<< s_failed << " failed ===\n";
return s_failed > 0 ? 1 : 0;
}