#include "game/sim/scriptobjects.h" #include "check.h" using namespace sots::sim; // The tier scan, at the boundaries the corpus cannot reach. Rule 23: the reference pair only // ever exercises frame 1 and 2, so every other row here is hand-computed from the table the // handler builds and is the only check those rows will ever get. static void test_slaver_tier() { // Below the first threshold the scan stops at index 0 and returns before storing. CHECK_EQ(SlaverDifficultyTier(-5), -1); CHECK_EQ(SlaverDifficultyTier(0), -1); // 1..49 is tier 0. Both ends by hand. CHECK_EQ(SlaverDifficultyTier(1), 0); CHECK_EQ(SlaverDifficultyTier(2), 0); CHECK_EQ(SlaverDifficultyTier(49), 0); // 50..99 is tier 1. Both ends by hand. CHECK_EQ(SlaverDifficultyTier(50), 1); CHECK_EQ(SlaverDifficultyTier(99), 1); // And at 100 the scan runs off the end of a three-record table, so it writes NOTHING. // Tier 2 is unreachable through this path. This is what the instructions do; a reader // who assumed "last tier wins" would have it wrong from turn 100 on. CHECK_EQ(SlaverDifficultyTier(100), -1); CHECK_EQ(SlaverDifficultyTier(1000), -1); } static void test_slaver_store_is_conditional() { // The reference pair: -1 at frame 1 becomes 0. SlaverTierResult r = SlaversTurnEnd(-1, 1); CHECK(r.wrote); CHECK_EQ(r.tier, 0); // The turn after: already 0, so no store. This is why the second reference pair moves // nothing here, and that asymmetry is the check that the rule is one-shot per tier. r = SlaversTurnEnd(0, 2); CHECK(!r.wrote); CHECK_EQ(r.tier, 0); // Past 100 the scan writes nothing even though the stored tier is stale. r = SlaversTurnEnd(1, 250); CHECK(!r.wrote); CHECK_EQ(r.tier, 1); // A save that somehow carries a tier ahead of its frame is written back DOWN, because // the store is on inequality, not on ordering. r = SlaversTurnEnd(1, 5); CHECK(r.wrote); CHECK_EQ(r.tier, 0); } static void test_refugee_latch() { bool ini = false; RefugeesTurnBeginResult r = RefugeesTurnBegin(ini); CHECK(r.latched); CHECK(ini); CHECK(r.convoyOwed); // Second delivery: the latch holds and nothing is owed again. r = RefugeesTurnBegin(ini); CHECK(!r.latched); CHECK(!r.convoyOwed); CHECK(ini); } // The reference pair's map: two systems tagged with the swarm's scenario id, and three // tagged with someone else's. Only the two get hives. static void test_hive_registration() { const std::vector ids = {64, 336, 400, 448, 480}; const std::vector tags = {5, 3, 3, 4, 4}; std::vector hives; std::vector owed = HivesToRegister(ids, tags, hives); CHECK_EQ(owed.size(), std::size_t{2}); CHECK_EQ(owed[0], std::int32_t{336}); CHECK_EQ(owed[1], std::int32_t{400}); hives.push_back(Hive{336, 0, 30}); hives.push_back(Hive{400, 0, 28}); // Registration is idempotent: nothing is owed once the hives exist, which is why the // second reference pair moves no hive leaf. owed = HivesToRegister(ids, tags, hives); CHECK(owed.empty()); } static void test_hive_prune() { const std::vector ids = {336, 400}; std::vector tags = {3, 3}; std::vector hives = {Hive{336, 0, 30}, Hive{400, 0, 28}}; CHECK_EQ(PruneHives(hives, ids, tags), 0); CHECK_EQ(hives.size(), std::size_t{2}); // The swarm loses one system: its hive goes. tags[1] = -1; CHECK_EQ(PruneHives(hives, ids, tags), 1); CHECK_EQ(hives.size(), std::size_t{1}); CHECK_EQ(hives[0].systemId, std::int32_t{336}); // A hive whose system is not in the map at all goes the same way. hives.push_back(Hive{9999, 0, 5}); CHECK_EQ(PruneHives(hives, ids, tags), 1); CHECK_EQ(hives.size(), std::size_t{1}); } // The whole explanation of a number that looked impossible: 31 after turn 1, 32 after turn // 2. It is not re-rolled -- it walks forward one per turn the gates stay shut. static void test_hive_tick_slips() { std::vector hives = {Hive{336, 0, 30}, Hive{400, 0, 28}}; HiveTickResult t = TickHives(hives, 1); CHECK_EQ(t.slipped, 2); CHECK_EQ(hives[0].nextQueenTurn, std::int32_t{31}); CHECK_EQ(hives[1].nextQueenTurn, std::int32_t{29}); t = TickHives(hives, 2); CHECK_EQ(t.slipped, 2); CHECK_EQ(hives[0].nextQueenTurn, std::int32_t{32}); CHECK_EQ(hives[1].nextQueenTurn, std::int32_t{30}); // A hive that already has a queen is not ticked at all. hives[0].queenId = 77; t = TickHives(hives, 3); CHECK_EQ(t.slipped, 1); CHECK_EQ(hives[0].nextQueenTurn, std::int32_t{32}); } // The arm no corpus save has ever taken, stated so the shape of the rule is on record: with // the gates open a hive whose date has arrived spawns, and one whose date has not does NOT // slip -- the slip belongs to the failed-gate arm alone. static void test_hive_tick_gates_open() { std::vector hives = {Hive{336, 0, 3}, Hive{400, 0, 99}}; const HiveTickResult t = TickHives(hives, 10, /*gatesOpen=*/true); CHECK_EQ(t.spawnsOwed, 1); CHECK_EQ(t.slipped, 0); CHECK_EQ(hives[0].nextQueenTurn, std::int32_t{3}); CHECK_EQ(hives[1].nextQueenTurn, std::int32_t{99}); } int main() { test_slaver_tier(); test_slaver_store_is_conditional(); test_refugee_latch(); test_hive_registration(); test_hive_prune(); test_hive_tick_slips(); test_hive_tick_gates_open(); return simtest::finish("scriptobjects"); }