#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 #include #include #include 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) \ if ( (a) != (b) ) { \ std::ostringstream _msg; \ _msg << #a << " == " << #b << " (" << (a) << " != " << (b) << ")"; \ throw std::runtime_error ( "ASSERT_EQ failed: " + _msg.str() ); \ } /* 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 ); for ( int xi = 0; xi < x; ++xi ) for ( int yi = 0; yi < y; ++yi ) for ( int zi = 0; zi < z; ++zi ) w.setValue ( xi, yi, zi, 0 ); 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, 3, 42 ); ASSERT_EQ ( w.getValue ( 1, 2, 3 ), 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.setValue ( 0, 0, 0, 3 ); w.setValue ( 0, 0, 2, 5 ); w.setValue ( 0, 0, 4, 8 ); ASSERT_EQ ( w.getSurfaceLevel ( 0, 0 ), 4 ); } ); TEST ( "ignores negative effect cells", { World w = makeEmptyWorld ( 3, 3, 5 ); w.setValue ( 0, 0, 3, -2 ); // effect at z=3 w.setValue ( 0, 0, 1, 6 ); // mineable block at z=1 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.setValue ( 0, 0, 1, 4 ); w.setValue ( 0, 0, 3, -1 ); // effect above 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, 4, 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, 2, 9 ); ASSERT_EQ ( w.mine ( 0, 0 ), 9 ); ASSERT_EQ ( w.getValue ( 0, 0, 2 ), 0 ); } ); TEST ( "lower block becomes new surface after mining", { World w = makeEmptyWorld ( 3, 3, 5 ); w.setValue ( 0, 0, 1, 4 ); w.setValue ( 0, 0, 3, 7 ); w.mine ( 0, 0 ); // mines z=3 ASSERT_EQ ( w.getSurfaceLevel ( 0, 0 ), 1 ); } ); 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, 2, -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, 1, -3 ); w.checkEffects ( 0, 0 ); ASSERT_EQ ( w.getValue ( 0, 0, 1 ), 0 ); } ); TEST ( "second effect still present after first is consumed", { World w = makeEmptyWorld ( 3, 3, 5 ); w.setValue ( 0, 0, 0, -1 ); w.setValue ( 0, 0, 2, -2 ); w.checkEffects ( 0, 0 ); const int remaining = w.checkEffects ( 0, 0 ); 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.setValue ( 0, 0, 1, 3 ); w.setValue ( 0, 0, 3, 7 ); 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.setValue ( 0, 0, 2, -3 ); w.setValue ( 0, 0, 4, 6 ); 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, 2, 5 ); DigDeepBot bot ( 0, 0 ); ASSERT_EQ ( bot.mine ( w ), 5 ); ASSERT_EQ ( w.getValue ( 0, 0, 2 ), 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.setValue ( 0, 0, 1, 3 ); w.setValue ( 0, 0, 3, 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.setValue ( 0, 0, 2, -1 ); // effect; must not be counted w.setValue ( 0, 0, 3, 7 ); // positive block above 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, 5, 3 ); 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.setValue ( 0, 0, 0, 3 ); w.setValue ( 0, 0, 2, 7 ); // highest w.setValue ( 0, 0, 4, 1 ); 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 ); // mines 9 w.setValue ( 0, 0, 2, 6 ); bot.mine ( w ); // 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{ 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, 2, 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; }