sots-engine/tests/shim_colony/unit_tests.cpp

263 lines
9.8 KiB
C++

// 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");
}