// B4 adapter tests: a ServerSystem snapshot -> sim::ColonyTurnState/Inputs -> one colony turn // -> back into the snapshot, which is what the hook lays into its scratch regions. // // Hand-written fixtures only. The "reference" one is the turn-2 shape of the human player's // home system in the reference save (a stable, owned, home colony with no plague, no // rebellion and no slaves), so the mapping is exercised on the numbers the live compare will // also see. #include "shim/hooks/colony_inputs.h" #include "check.h" using namespace shim::hooks; using sots::sim::AddictionPhase; using sots::sim::AddictionPhaseOf; using sots::sim::ColonyCountdowns; using sots::sim::ColonyTurnInputs; using sots::sim::ColonyTurnResult; using sots::sim::ColonyTurnState; using sots::sim::ProcessColonyTurn; namespace { ColonyTuning Tuning() { ColonyTuning t; t.SYSTEMBONUS_MINTURNS = 10; t.SYSTEMBONUS_POPBONUS = 0.1; t.SYSTEMBONUS_POPBONUS_INC = 0.01; t.SYSTEMBONUS_INFRABONUS = 0.2; t.SYSTEMBONUS_INFRABONUS_INC = 0.05; return t; } // A stable, owned, home colony well past the bonus gates. ColonySnapshot Reference() { ColonySnapshot s; s.systemIndex = 7; s.ownerIndex = 0; s.ownerSpecies = 6; // Morrigi s.owned = true; s.homeSystem = true; s.stable = true; s.playerCount = 8; s.currentTurn = 2; s.turnAcquired = 0; s.infra = 1.0f; s.infraBonus = 0.0f; s.pop = 2000000000; s.popBonus = 0; s.turnsDeveloping = 2; s.totalResources = 5000; s.imperialCapacity = 2000000000; s.addictionPhase2Start = 10; s.addictionPhase3Start = 15; return s; } void test_state_round_trip() { ColonySnapshot s = Reference(); s.infra = 0.25f; s.infraBonus = 0.125f; s.popBonus = 4242; s.totalResources = 99; s.growthHalted[1] = 1; s.battles = 0x00000000000000f3ull; s.battlesMask = 0x5u; s.recon = 0x0000000000000021ull; s.reconMask = 0x3u; const ColonyTurnState st = ToColonyTurnState(s); CHECK_NEAR(st.infra, 0.25, 0.0); CHECK_NEAR(st.infraBonus, 0.125, 0.0); CHECK_EQ(st.popBonus, std::int64_t{4242}); CHECK_EQ(st.totalResources, 99); CHECK(!st.growthHalted[0] && st.growthHalted[1] && !st.growthHalted[2]); CHECK_EQ(st.battles.counters, 0x00000000000000f3ull); CHECK_EQ(st.battles.active, 0x5u); ColonySnapshot back = Reference(); ApplyColonyTurnState(st, back); CHECK_NEAR(back.infra, 0.25f, 0.0); CHECK_EQ(back.popBonus, 4242); CHECK_EQ(back.battles, 0x00000000000000f3ull); CHECK_EQ(back.reconMask, 0x3u); } void test_inputs_mapping() { ColonySnapshot s = Reference(); s.currentTurn = 30; s.turnAcquired = 4; s.temperance[2] = 1; s.civilianPresent[2] = 1; s.addictionStart[2] = 11; const ColonyTurnInputs in = ToColonyTurnInputs(s); CHECK_EQ(in.turnsOwned, 26); // ModCount - TAcq CHECK(in.owned && in.homeSystem && in.stable); CHECK(in.temperance[2] && in.civilianPresent[2]); CHECK_EQ(in.addictionStart[2], 11); CHECK_EQ(in.playerCount, 8); CHECK_EQ(in.imperialCapacity, std::int64_t{2000000000}); } // The reference colony's turn: nothing to do but the housekeeping the dispatcher owns. void test_reference_turn() { const sots::sim::TuningTable t = ToTuningTable(Tuning()); ColonySnapshot s = Reference(); s.battles = 0x0000000000000231ull; // players 0, 1, 2 counting down from 1, 3, 2 s.battlesMask = 0x7u; s.recon = 0; s.reconMask = 0x2u; // stale bit with no counter: the sweep is skipped ColonyTurnState st = ToColonyTurnState(s); const ColonyTurnResult r = ProcessColonyTurn(st, ToColonyTurnInputs(s), t); CHECK_EQ(r.rngDraws, 0); // a colony turn draws only from ProcessRebellion CHECK(r.moraleEvents.empty()); // no civilians recorded, so no addiction sweep CHECK_EQ(st.totalResources, 0); // TRes is reset every turn CHECK(!st.growthHalted[0] && !st.growthHalted[1] && !st.growthHalted[2]); CHECK_EQ(st.turnsDeveloping, 3); // stable: ntdev++ CHECK_NEAR(st.infra, 1.0, 0.0); // already built out, no pending bonus CHECK_EQ(st.battles.counters, 0x0000000000000120ull); // 1->0, 3->2, 2->1 // The mask bit is cleared on the turn a counter is FOUND at zero, not on the turn it // reaches zero -- player 0's counter is 0 now, so its bit goes next turn. CHECK_EQ(st.battles.active, 0x7u); ColonyTurnState st2 = st; sots::sim::TickCountdowns(st2.battles, 8); CHECK_EQ(st2.battles.active, 0x6u); CHECK_EQ(st.recon.counters, 0ull); CHECK_EQ(st.recon.active, 0x2u); // the whole rcex sweep is skipped when the word is 0 // an unstable colony resets the counter instead ColonySnapshot u = Reference(); u.stable = false; u.turnsDeveloping = 9; ColonyTurnState us = ToColonyTurnState(u); ProcessColonyTurn(us, ToColonyTurnInputs(u), t); CHECK_EQ(us.turnsDeveloping, 0); } void test_bonus_and_accrual() { const sots::sim::TuningTable t = ToTuningTable(Tuning()); ColonySnapshot s = Reference(); s.currentTurn = 40; s.turnAcquired = 0; s.turnsDeveloping = 30; s.infra = 0.5f; s.infraBonus = 0.1f; s.pop = 1000; s.popBonus = 500; s.imperialCapacity = 1000000; ColonyTurnState st = ToColonyTurnState(s); ProcessColonyTurn(st, ToColonyTurnInputs(s), t); // the pending pools drain first, then the accrual tops them back up CHECK_NEAR(st.infra, 0.6, 1e-6); CHECK_NEAR(st.infraBonus, 0.05, 1e-6); // drained to 0, then + INFRABONUS_INC CHECK_EQ(st.pop, std::int64_t{1500}); CHECK_EQ(st.popBonus, std::int64_t{10000}); // drained to 0, then + 1e6 x 0.01 // a non-home colony absorbing a bonus has its ntdev reset, which then costs it the // system-bonus gate on the very same turn ColonySnapshot n = s; n.homeSystem = false; ColonyTurnState ns = ToColonyTurnState(n); ProcessColonyTurn(ns, ToColonyTurnInputs(n), t); CHECK_EQ(ns.turnsDeveloping, 1); CHECK_EQ(ns.popBonus, std::int64_t{0}); // the accrual gate failed CHECK_NEAR(ns.infraBonus, 0.0, 1e-6); } void test_addiction_sweep() { const sots::sim::TuningTable t = ToTuningTable(Tuning()); ColonySnapshot s = Reference(); s.currentTurn = 40; s.civilianPresent[0] = 1; // never addicted -> no event s.civilianPresent[1] = 1; // onset s.addictionStart[1] = 35; // elapsed 5 <= 10 s.civilianPresent[2] = 1; // established -> no event at all s.addictionStart[2] = 28; // elapsed 12, > 10 and <= 15 s.civilianPresent[3] = 1; // terminal s.addictionStart[3] = 20; // elapsed 20 > 15 s.civilianPresent[4] = 1; // temperance wins regardless of the phase s.addictionStart[4] = 20; s.temperance[4] = 1; s.civilianPresent[5] = 0; // no civilians: never looked at s.addictionStart[5] = 20; ColonyTurnState st = ToColonyTurnState(s); const ColonyTurnResult r = ProcessColonyTurn(st, ToColonyTurnInputs(s), t); CHECK_EQ(r.moraleEvents.size(), std::size_t{3}); CHECK_EQ(r.moraleEvents[0].species, 1); CHECK_EQ(r.moraleEvents[0].eventId, sots::sim::kMoraleEventAddictionOnset); CHECK_EQ(r.moraleEvents[0].moraleDelta, 1); CHECK_EQ(r.moraleEvents[1].species, 3); CHECK_EQ(r.moraleEvents[1].eventId, sots::sim::kMoraleEventAddictionTerminal); CHECK_EQ(r.moraleEvents[1].moraleDelta, -2); CHECK_EQ(r.moraleEvents[2].species, 4); CHECK_EQ(r.moraleEvents[2].eventId, sots::sim::kMoraleEventAddictionSuppressed); CHECK_EQ(r.moraleEvents[2].moraleDelta, -1); // an independent colony does not sweep at all ColonySnapshot i = s; i.independent = true; ColonyTurnState is = ToColonyTurnState(i); CHECK(ProcessColonyTurn(is, ToColonyTurnInputs(i), t).moraleEvents.empty()); // phase boundaries are strict CHECK(AddictionPhaseOf(0, 40, 10, 15) == AddictionPhase::None); CHECK(AddictionPhaseOf(30, 40, 10, 15) == AddictionPhase::Onset); // elapsed 10 CHECK(AddictionPhaseOf(29, 40, 10, 15) == AddictionPhase::Established); // elapsed 11 CHECK(AddictionPhaseOf(25, 40, 10, 15) == AddictionPhase::Established); // elapsed 15 CHECK(AddictionPhaseOf(24, 40, 10, 15) == AddictionPhase::Terminal); // elapsed 16 } void test_unowned_colony() { const sots::sim::TuningTable t = ToTuningTable(Tuning()); ColonySnapshot s = Reference(); s.owned = false; s.homeSystem = false; s.stable = false; s.infra = 0.5f; s.infraBonus = 0.25f; s.popBonus = 900; ColonyTurnState st = ToColonyTurnState(s); ProcessColonyTurn(st, ToColonyTurnInputs(s), t); // decay first (0.5 - 0.02f), then the pending infrastructure pool drains into it CHECK_NEAR(st.infra, 0.48 + 0.25, 1e-6); CHECK_NEAR(st.infraBonus, 0.0, 1e-6); CHECK_EQ(st.popBonus, std::int64_t{0}); // an unowned system drops the population pool CHECK_EQ(st.turnsDeveloping, 0); } void test_countdown_edges() { ColonyCountdowns c; c.counters = 0; c.active = 0xffffu; sots::sim::TickCountdowns(c, 8); CHECK_EQ(c.active, 0xffffu); // an all-zero word skips the sweep entirely c.counters = 0xfULL << (4 * 14); // player 14 is the last one with a nibble c.active = 0xffffu; sots::sim::TickCountdowns(c, 16); CHECK_EQ(sots::sim::CountdownFor(c, 14), 14); CHECK_EQ(c.active & (1u << 15), 1u << 15); // player 15 has no nibble and is untouched CHECK_EQ(sots::sim::CountdownFor(c, 15), 0); sots::sim::SetCountdown(c, 3, 99); CHECK_EQ(sots::sim::CountdownFor(c, 3), 15); // clamped sots::sim::SetCountdown(c, 20, 5); // out of range: ignored CHECK_EQ(sots::sim::CountdownFor(c, 20), 0); } } // namespace int main() { test_state_round_trip(); test_inputs_mapping(); test_reference_turn(); test_bonus_and_accrual(); test_addiction_sweep(); test_unowned_colony(); test_countdown_edges(); return simtest::finish("shim_colony_unit"); }