sots-engine/tests/shim_budget/unit_tests.cpp

257 lines
9.1 KiB
C++

// B1 adapter tests: ServerPlayer snapshot -> sim::BudgetInputs -> the game's 22-int budget array.
//
// Hand-written fixtures only. Two of them are the turn-2 state of the reference save's two
// landed players, taken from the save dump's own field names, so the mapping is exercised on
// numbers the live compare will also see.
#include "shim/hooks/budget_inputs.h"
#include <cstring>
#include "check.h"
using namespace shim::hooks;
using sots::sim::Budget;
using sots::sim::BudgetInputs;
namespace {
BudgetSnapshot Blank() {
BudgetSnapshot s;
s.playerIndex = 0;
s.species = 0;
s.ownedSystems = 1;
return s;
}
void test_aid_aggregation() {
BudgetSnapshot s = Blank();
s.aidCount = 3;
s.aid[0] = {30, 1, 500, 1}; // active on both channels
s.aid[1] = {40, 0, 700, 0}; // inactive: contributes nothing
s.aid[2] = {50, 2, 900, 3}; // active (gate is "> 0", not "== 1")
CHECK_EQ(AidResearchPercent(s), 80);
CHECK_EQ(AidSavings(s), 1400);
s.aid[0].researchPercent = 90; // 90 + 50 = 140 clamps to 100
CHECK_EQ(AidResearchPercent(s), 100);
s.aidCount = 0;
CHECK_EQ(AidResearchPercent(s), 0);
CHECK_EQ(AidSavings(s), 0);
}
void test_input_mapping() {
BudgetSnapshot s = Blank();
s.savings = 289688;
s.resRate = 0.25f;
s.resMod = 1.0f;
s.resScl = 1.0f;
s.trm = 0.f;
s.shrm = 0.f;
s.tra = 3;
s.trp = 4;
s.setupIncomeMult = 1.5f;
s.setupResearchMult = 2.0f;
s.isAI = true;
s.hasResearchTarget = true;
s.aggSystemIncomePositive = 239189;
s.aggSystemIncomeNegative = 17;
s.aggTradeIncome = 11;
s.aggShipCarriedPopIncome = 22;
s.aggSecondaryManagerIncome = 33;
s.maintenance = 500;
s.maintenanceDivisor = 2.0;
s.aggConstructionSpend = 4000;
s.expenseCount = 2;
s.expenses[0] = {7, 100, 900, 0.10f};
s.expenses[1] = {8, 0, 0, 0.25f};
const BudgetInputs in = ToBudgetInputs(s);
CHECK_EQ(in.savings, 289688);
CHECK(in.ownsSystems);
CHECK_EQ(static_cast<int>(in.systemIncome.size()), 2);
CHECK_EQ(in.systemIncome[0], 239189);
CHECK_EQ(in.systemIncome[1], -17);
CHECK_EQ(in.tradeIncome, 11);
CHECK_EQ(in.shipCarriedPopIncome, 22);
CHECK_EQ(in.secondaryManagerIncome, 33);
CHECK_EQ(in.maintenance, 500);
CHECK_NEAR(in.maintenanceDivisor, 2.0, 1e-12);
CHECK_EQ(in.constructionDemand, 4000);
CHECK(in.isAI);
CHECK(in.hasResearchTarget);
CHECK_NEAR(in.researchRate, 0.25, 1e-9);
CHECK_NEAR(in.techIncomeMult, 1.5, 1e-9);
CHECK_NEAR(in.techResearchMult, 2.0, 1e-9);
CHECK_EQ(in.tra, 3);
CHECK_EQ(in.trp, 4);
CHECK_EQ(static_cast<int>(in.expenses.size()), 2);
CHECK_EQ(in.expenses[0].minimum, 100);
CHECK_EQ(in.expenses[0].maximum, 900);
CHECK_NEAR(in.expenses[0].fraction, 0.10f, 1e-7);
// No systems -> no savings interest.
s.ownedSystems = 0;
CHECK(!ToBudgetInputs(s).ownsSystems);
// A zero aggregate must not become a phantom system in the list.
s.ownedSystems = 1;
s.aggSystemIncomeNegative = 0;
CHECK_EQ(static_cast<int>(ToBudgetInputs(s).systemIncome.size()), 1);
}
void test_slot_layout() {
// The six slots the milestone consumes as inputs.
const int inputs[] = {1, 2, 3, 4, 7, 11};
for (int i = 0; i < kBudgetSlots; ++i) {
bool expected = false;
for (int k : inputs) expected = expected || (k == i);
CHECK_EQ(IsInputSlot(i), expected);
}
}
// Player 0 of the reference save at turn 2: human, one system, no research target, no expense
// sliders, no aid, treasury 289,688, ResRate 0.25.
void test_reference_player0() {
BudgetSnapshot s = Blank();
s.savings = 289688;
s.resRate = 0.25f;
s.resMod = 1.0f;
s.resScl = 1.0f;
s.isAI = false;
s.hasResearchTarget = false;
s.aggSystemIncomePositive = 239189;
s.aggConstructionSpend = 0;
const Budget b = sots::sim::ComputeBudget(ToBudgetInputs(s), s.projected);
std::int32_t out[kBudgetSlots];
FillSlots(b, s, out);
CHECK_EQ(out[kSlotSavings], 289688);
CHECK_EQ(out[kSlotSystemIncomePositive], 239189);
CHECK_EQ(out[kSlotSavingsInterest], 2896); // ftol(289688 x 0.01)
CHECK_EQ(out[kSlotDebtInterest], 0);
CHECK_EQ(out[kSlotExpenses], 0);
// available = 239189 + 2896
CHECK_EQ(out[kSlotAvailable], 242085);
CHECK_EQ(out[kSlotConstruction], 0);
CHECK_EQ(out[kSlotResearchMoney], 60521); // ftol(242085 x 0.25)
// 60521/50 x 1.15 x 0.5 x 0.85 = 591.59... -> 591
CHECK_EQ(out[kSlotResearchPoints], 591);
CHECK_EQ(out[kSlotResearchMoneyGiven], 0);
CHECK_EQ(out[kSlotTotalResearchPoints], 591);
CHECK_EQ(out[kSlotBonusIncome], 0); // setup income multiplier is 1
CHECK_EQ(out[kSlotSavingsGiven], 0);
// No research target: the research money is reported but never charged, so the whole
// income lands in the treasury (the B1 trace's player 0).
CHECK_EQ(out[kSlotResearchMoneyKept], 0);
CHECK_EQ(b.net, 242085);
}
// Player 1: the AI, treasury 92,651, ResRate 0.8, ResMod 0.9, Maint 500, a research target.
void test_reference_player1() {
BudgetSnapshot s = Blank();
s.playerIndex = 1;
s.species = 2;
s.isAI = true;
s.savings = 92651;
s.resRate = 0.800000011920929f;
s.resMod = 0.8999999761581421f;
s.resScl = 1.0f;
s.hasResearchTarget = true;
s.aggSystemIncomePositive = 100000;
s.maintenance = 500;
s.maintenanceDivisor = kDifficultyAI.maintenanceDivisor;
s.researchDifficultyMult = kDifficultyAI.researchMult;
const Budget b = sots::sim::ComputeBudget(ToBudgetInputs(s), s.projected);
std::int32_t out[kBudgetSlots];
FillSlots(b, s, out);
CHECK_EQ(out[kSlotSavingsInterest], 926);
CHECK_EQ(out[kSlotMaintenance], 166); // 500 / ftol(3.0)
CHECK_EQ(out[kSlotAvailable], 100760); // 100000 + 926 - 166
CHECK_EQ(out[kSlotConstruction], 0); // AI: the construction slot stays 0 here
CHECK(b.hasResearchAllocation);
CHECK(out[kSlotResearchMoney] > 0);
CHECK_EQ(out[kSlotResearchMoneyKept], out[kSlotResearchMoney]);
}
// The difficulty row the original picks: the AI row only for a player that is AI and not NPC.
void test_difficulty_row() {
CHECK_NEAR(DifficultyRowFor(false, false).maintenanceDivisor, 1.0, 1e-12);
CHECK_NEAR(DifficultyRowFor(false, false).researchMult, 1.0, 1e-12);
CHECK_NEAR(DifficultyRowFor(true, false).maintenanceDivisor, 3.0, 1e-12);
CHECK_NEAR(DifficultyRowFor(true, false).researchMult, 1.5, 1e-12);
CHECK_NEAR(DifficultyRowFor(true, true).maintenanceDivisor, 1.0, 1e-12); // NPC: human row
CHECK_NEAR(DifficultyRowFor(false, true).researchMult, 1.0, 1e-12);
}
// The research-allocation vector at the tail of the Budget object: one 8-byte element exactly
// when the player has a research target.
void test_alloc_vector() {
BudgetSnapshot s = Blank();
s.aggSystemIncomePositive = 1000;
std::uint32_t words[3] = {0xdeadbeef, 0xdeadbeef, 0xdeadbeef};
s.hasResearchTarget = false;
FillAllocVector(sots::sim::ComputeBudget(ToBudgetInputs(s), false), words);
CHECK_EQ(AllocElementCount(words), 0);
s.hasResearchTarget = true;
FillAllocVector(sots::sim::ComputeBudget(ToBudgetInputs(s), false), words);
CHECK_EQ(AllocElementCount(words), 1);
// The original's words are heap pointers; only the span matters.
const std::uint32_t live[3] = {888397232u, 888397240u, 888397240u};
CHECK_EQ(AllocElementCount(live), 1);
const std::uint32_t empty[3] = {0u, 0u, 0u};
CHECK_EQ(AllocElementCount(empty), 0);
const std::uint32_t bad[3] = {8u, 0u, 0u};
CHECK_EQ(AllocElementCount(bad), -1);
const shim::trace::Tv d = DescribeAllocVector(live, sizeof live, 256);
CHECK_EQ(static_cast<int>(d.items[0].i), 1);
}
void test_describers() {
std::int32_t slots[kBudgetSlots];
for (int i = 0; i < kBudgetSlots; ++i) slots[i] = i * 10;
const shim::trace::Tv t = DescribeBudgetSlots(slots, sizeof slots, 256);
CHECK_EQ(static_cast<int>(t.keys.size()), static_cast<int>(kBudgetSlots));
CHECK(t.keys[kSlotResearchPoints] == "researchPoints");
CHECK_EQ(t.items[kSlotResearchPoints].i, 170);
BudgetSnapshot s = Blank();
s.savings = 42;
s.expenseCount = 1;
s.expenses[0] = {1, 2, 3, 0.5f};
s.aidCount = 1;
s.aid[0] = {25, 1, 60, 1};
const shim::trace::Tv d = DescribeSnapshot(&s, sizeof s, 256);
bool sawSavings = false, sawAidPct = false;
for (std::size_t i = 0; i < d.keys.size(); ++i) {
if (d.keys[i] == "savings") { sawSavings = true; CHECK_EQ(d.items[i].i, 42); }
if (d.keys[i] == "aidResearchPercent") { sawAidPct = true; CHECK_EQ(d.items[i].i, 25); }
}
CHECK(sawSavings);
CHECK(sawAidPct);
// A short region must not read past the buffer.
const shim::trace::Tv bad = DescribeSnapshot(&s, 4, 256);
CHECK_EQ(static_cast<int>(bad.keys.size()), 1);
}
} // namespace
int main() {
test_aid_aggregation();
test_input_mapping();
test_slot_layout();
test_reference_player0();
test_reference_player1();
test_difficulty_row();
test_alloc_vector();
test_describers();
return simtest::finish("shim_budget");
}