257 lines
9.1 KiB
C++
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");
|
|
}
|