sots-engine/tests/game_sim/test_colony.cpp

501 lines
22 KiB
C++

#include "game/sim/colony.h"
#include "check.h"
#include "game/sim/numeric.h"
using namespace sots::sim;
static TuningTable tuning() {
TuningTable t;
t.POPULATION_GROWTH_MOD = 1.2;
t.POPULATION_GROWTH_EXP = 2.0;
t.INDSYS_IMPERIAL_POPULATION_MOD = 0.1;
t.SLAVES_DEATH_RATE = 0.05;
t.SLAVES_DEATH_RATE_BYHAZARD = 0.5;
t.SLAVES_DEATH_RATE_BYOUTPUT = 0.1;
t.SLAVES_MIN_DEATHS = 0;
t.SLAVES_MAX_DEATHS = -1;
t.MORALE_INCREASE_OUTPUT = 75;
t.MORALE_INCREASE_OUTPUT_MOD = 1.1;
t.MORALE_DECREASE_OUTPUT = 25;
t.MORALE_DECREASE_OUTPUT_MOD = 0.9;
t.STATION_BONUS_IMPERIAL_OUTPUT = 0.1;
t.STATION_BONUS_SHIPCON = 0.25;
t.ADDICTION_OUTPUT_MOD = 0.5;
t.SYSTEMBONUS_MINTURNS = 10;
t.SYSTEMBONUS_POPBONUS = 0.1;
t.SYSTEMBONUS_POPBONUS_HOME = 0.2;
t.SYSTEMBONUS_POPBONUS_INC = 0.01;
t.SYSTEMBONUS_INFRABONUS = 0.2;
t.SYSTEMBONUS_INFRABONUS_HOME = 0.5;
t.SYSTEMBONUS_INFRABONUS_INC = 0.05;
return t;
}
static void test_capacity() {
TuningTable t = tuning();
CapacityInputs c;
c.planetSize = 5;
CHECK_EQ(CarryingCapacity(c, t), std::int64_t{500000000});
c.hazardMod = 0.5;
CHECK_EQ(CarryingCapacity(c, t), std::int64_t{250000000});
c.arcologyTech = true;
CHECK_EQ(CarryingCapacity(c, t), std::int64_t{350000000});
c.group = PopGroup::Civilian;
CHECK_EQ(CarryingCapacity(c, t), std::int64_t{450000000});
c.group = PopGroup::Slaves;
CHECK_EQ(CarryingCapacity(c, t), std::int64_t{250000000}); // no arcology bonus for slaves
c.group = PopGroup::Imperial;
c.groupMaxEnabled = true;
c.groupMax = 300000000;
CHECK_EQ(CarryingCapacity(c, t), std::int64_t{300000000});
c.ownerIsNpc = true;
CHECK_EQ(CarryingCapacity(c, t), std::int64_t{30000000});
c.ownerIsNpc = false;
c.ownerIsDifferentSpecies = true;
c.crossSpeciesMod = 0.5;
CHECK_EQ(CarryingCapacity(c, t), std::int64_t{225000000}); // 5e8 x 0.5 x 0.5 + 1e8
c.species = Species::NPC;
CHECK_EQ(CarryingCapacity(c, t), std::int64_t{0});
c.species = Species::Liir;
c.speciesCanLive = false;
CHECK_EQ(CarryingCapacity(c, t), std::int64_t{0});
c.speciesCanLive = true;
c.planetSize = 0;
CHECK_EQ(CarryingCapacity(c, t), std::int64_t{100000000}); // arcology alone
// hazard = clamp01(1 - |suit - ideal| / (tol + 0.1))
CHECK_NEAR(HazardModifier(0.5, 0.5, 0.2), 1.0, 0.0);
CHECK_NEAR(HazardModifier(0.6, 0.5, 0.2), 1.0 - 0.1 / 0.3, 1e-12);
CHECK_NEAR(HazardModifier(0.5, 0.65, 0.2), 0.5, 1e-12); // symmetric
CHECK_NEAR(HazardModifier(0.8, 0.5, 0.2), 0.0, 0.0); // at the band edge
CHECK_NEAR(HazardModifier(0.9, 0.5, 0.2), 0.0, 0.0);
CHECK_NEAR(HazardModifier(0.55, 0.5, 0.0), 0.5, 1e-12); // zero tolerance keeps a 0.1 band
CHECK_NEAR(HazardModifier(0.7, 0.5, 0.0), 0.0, 0.0);
// both adaptation techs: 0.2 + 0.75 + 1.5 -> band 2.55
CHECK_NEAR(HazardModifier(0.8, 0.5, 2.45), 1.0 - 0.3 / 2.55, 1e-12);
}
static void test_growth() {
TuningTable t = tuning();
// B4: the curve is driven by suitability, not by how full the colony is. `tol` is the
// owner's SuitTol and doubles as the divisor, so d/tol is the fraction of the habitable
// band the planet is off by.
GrowthInputs g;
g.suitTolerance = 1.0;
g.idealSuitability = 1.0;
g.suitability = 0.5; // half a band off: base 0.5, ^2 = 0.25, x1.2 = 0.3
g.pop = 500000;
CHECK_EQ(PopulationGrowthDelta(g, t), std::int64_t{150000});
g.playerPopMod = 0.5; // 75000
CHECK_EQ(PopulationGrowthDelta(g, t), std::int64_t{75000});
g.playerPopMod = 1.0;
g.groupGrowthMult = 2.0; // 300000
CHECK_EQ(PopulationGrowthDelta(g, t), std::int64_t{300000});
g.groupGrowthMult = 0.0; // a zero column is ignored, not applied
CHECK_EQ(PopulationGrowthDelta(g, t), std::int64_t{150000});
g.extraFactor = 0.0; // so is a zero extra factor
CHECK_EQ(PopulationGrowthDelta(g, t), std::int64_t{150000});
g.extraFactor = 1.0;
g.pop = 0; // an empty group does not grow at all
CHECK_EQ(PopulationGrowthDelta(g, t), std::int64_t{0});
g.pop = 1; // trunc(1 x 0.3) == 0 -> forced to 1
CHECK_EQ(PopulationGrowthDelta(g, t), std::int64_t{1});
g.pop = 500000;
g.suitability = 1.0; // exactly at the ideal: base 1
CHECK_EQ(PopulationGrowthDelta(g, t), std::int64_t{600000}); // 1 x 1.2 x 5e5
g.suitability = 0.0; // a whole band off: base 0 -> no growth
CHECK_EQ(PopulationGrowthDelta(g, t), std::int64_t{0});
g.suitability = -5.0; // clamped up to 0 first, so still a full band
CHECK_EQ(PopulationGrowthDelta(g, t), std::int64_t{0});
g.suitability = 0.5;
g.accommodated = true; // suitability ignored entirely: base 1
CHECK_EQ(PopulationGrowthDelta(g, t), std::int64_t{600000});
g.accommodated = false;
g.blockaded = true;
CHECK_EQ(PopulationGrowthDelta(g, t), std::int64_t{0});
g.blockaded = false;
// the suitability distance clamps the planet's own value into [0, 20] before the
// difference, and is itself capped by the tolerance
CHECK_NEAR(GrowthSuitabilityDistance(25.0, 0.0, 100.0, false), 20.0, 1e-6);
CHECK_NEAR(GrowthSuitabilityDistance(-3.0, 5.0, 100.0, false), 5.0, 1e-6);
CHECK_NEAR(GrowthSuitabilityDistance(0.0, 5.0, 2.0, false), 2.0, 1e-6);
CHECK_NEAR(GrowthSuitabilityDistance(0.0, 5.0, 2.0, true), 0.0, 0.0);
// the exponent is clamped into [0.01f, 1000] before pow()
TuningTable big = t;
big.POPULATION_GROWTH_EXP = 100000.0;
TuningTable capped = t;
capped.POPULATION_GROWTH_EXP = 1000.0;
CHECK_NEAR(PopulationGrowthFraction(g, big), PopulationGrowthFraction(g, capped), 0.0);
TuningTable tiny = t;
tiny.POPULATION_GROWTH_EXP = 1e-9;
TuningTable floored = t;
floored.POPULATION_GROWTH_EXP = kGrowthExponentMin;
CHECK_NEAR(PopulationGrowthFraction(g, tiny), PopulationGrowthFraction(g, floored), 0.0);
CHECK_EQ(ApplyImperialGrowth(500000, 1000000, 150000), std::int64_t{650000});
CHECK_EQ(ApplyImperialGrowth(999999, 1000000, 5), std::int64_t{1000000}); // lands on the cap
CHECK_EQ(ApplyImperialGrowth(2000000, 1000000, 0), std::int64_t{1000000});
CHECK_EQ(ApplyImperialGrowth(1000000000, 100000000, 0), std::int64_t{950000000}); // shrink capped
CHECK_EQ(ApplyImperialGrowth(50, 10, 0), std::int64_t{50}); // floor min(pop, 100)
CHECK_EQ(ApplyImperialGrowth(500, 10, 0), std::int64_t{100});
// the 50,000,000 cap is on the delta, and it lives in the apply, not in the fraction
CHECK_EQ(ApplyImperialGrowth(1000, 2000000000, 100000000), std::int64_t{50001000});
CHECK_EQ(ApplyImperialGrowth(1000, 2000000000, -100), std::int64_t{900});
}
static void test_infra_terraform() {
CHECK_NEAR(InfrastructurePointsNeeded(0.0), 30304.0, 0.0); // ceil(30303.03)
CHECK_NEAR(InfrastructurePointsNeeded(1.0), 0.0, 0.0);
CHECK_NEAR(InfrastructureGain(500), 0.0165, 1e-7);
CHECK_NEAR(InfrastructureGain(1000), 0.033, 1e-7);
CHECK_NEAR(InfrastructureGain(-100), 0.0, 0.0); // clamped at zero
double unused = -1;
double delta = ApplyInfrastructurePoints(0.5, 100000, &unused);
CHECK_NEAR(unused, 100000.0 - 15152.0, 0.0); // ceil(0.5 / 3.3e-5)
CHECK_NEAR(ApplyInfrastructureDelta(0.5, delta), 1.0, 0.0); // clamped to exactly 1
delta = ApplyInfrastructurePoints(0.5, 1000, &unused);
CHECK_NEAR(unused, 0.0, 0.0);
CHECK_NEAR(ApplyInfrastructureDelta(0.5, delta), 0.533, 1e-7);
CHECK_NEAR(ApplyInfrastructureDelta(1.0, 0.5), 1.0, 0.0); // already built out: no-op
CHECK_NEAR(DecayUnownedInfrastructure(0.5), 0.48, 1e-7);
CHECK_NEAR(DecayUnownedInfrastructure(0.01), 0.0, 0.0);
// the decay constant is the widened float literal, not the decimal 0.02
CHECK(DecayUnownedInfrastructure(0.5) != 0.5 - 0.02);
// the terraforming modifier is inside the point count, and the result is a ceil
CHECK_NEAR(TerraformPointsNeeded(0.5, 0.8, 1.0), 3334.0, 0.0);
CHECK_NEAR(TerraformPointsNeeded(0.8, 0.5, 1.0), 3334.0, 0.0);
CHECK_NEAR(TerraformPointsNeeded(0.5, 0.8, 2.0), 1667.0, 0.0); // twice the modifier, half the points
CHECK_NEAR(TerraformPointsNeeded(0.5, 0.5, 1.0), 0.0, 0.0);
CHECK_NEAR(TerraformDelta(1000, 1.0, 0.5, 0.8), 0.09, 1e-7);
CHECK_NEAR(TerraformDelta(1000, 1.0, 0.8, 0.5), -0.09, 1e-7);
CHECK_NEAR(TerraformDelta(1000, 1.0, 0.5, 0.5), 0.09, 1e-7); // at the ideal the sign is +1
CHECK_NEAR(TerraformDelta(1000, 2.0, 0.5, 0.8), 0.18, 1e-7);
CHECK_NEAR(TerraformDelta(0, 2.0, 0.5, 0.8), 0.0, 0.0);
// suitability stops at the ideal from whichever side it came
CHECK_NEAR(ApplyTerraformDelta(0.5, 0.09, 0.8), 0.59, 1e-7);
CHECK_NEAR(ApplyTerraformDelta(0.75, 0.09, 0.8), 0.8, 1e-7);
CHECK_NEAR(ApplyTerraformDelta(0.85, -0.09, 0.8), 0.8, 1e-7);
CHECK_NEAR(ApplyTerraformDelta(0.8, 0.09, 0.8), 0.8, 0.0);
}
static void test_slaves() {
TuningTable t = tuning();
SpeciesTechFlags f;
// 0.5 x 0.1 + 0.2 x 0.5 + 0.05 = 0.2
CHECK_NEAR(SlaveDeathRate(0.5, 0.3, 0.5, f, t), 0.2, 1e-6);
f.translation1 = true;
CHECK_NEAR(SlaveDeathRate(0.5, 0.3, 0.5, f, t), 0.16, 1e-6);
f.translation2 = f.translation3 = true;
CHECK_NEAR(SlaveDeathRate(0.5, 0.3, 0.5, f, t), 0.08, 1e-6);
SpeciesTechFlags none;
CHECK_NEAR(SlaveDeathRate(0.0, 0.5, 0.5, none, t), 0.05, 1e-6); // base rate only
// an unowned system short-circuits to a rate of 1, not 0
CHECK_NEAR(SlaveDeathRate(0.5, 0.3, 0.5, none, t, false), 1.0, 0.0);
SpeciesTechFlags bits = SpeciesTechFlags::FromBits(0x087); // bits 0,1,2,7
CHECK(bits.translation1 && bits.translation2 && bits.translation3 && bits.accommodate);
CHECK(!bits.incorporate && !bits.addict && !bits.temperance && !bits.subjugate && !bits.proliferate);
CHECK_NEAR(SlaveDeathRate(0.5, 0.3, 0.5, bits, t), 0.08, 1e-6);
CHECK(SpeciesTechFlags::FromBits(0x100).proliferate);
CHECK(SpeciesTechFlags::FromBits(0x020).temperance);
CHECK_EQ(SlaveDeaths(1000, 0.2, 0.0, t), std::int64_t{200});
CHECK_EQ(SlaveDeaths(0, 0.2, 0.0, t), std::int64_t{0});
CHECK_EQ(SlaveDeaths(1000, 0.2, 0.1, t), std::int64_t{300}); // the plague rate ADDS
t.SLAVES_MIN_DEATHS = 300;
CHECK_EQ(SlaveDeaths(1000, 0.2, 0.0, t), std::int64_t{300});
CHECK_EQ(SlaveDeaths(100, 0.2, 0.0, t), std::int64_t{100}); // never more than present
t.SLAVES_MAX_DEATHS = 150;
CHECK_EQ(SlaveDeaths(1000, 0.2, 0.0, t), std::int64_t{150});
t.SLAVES_MIN_DEATHS = -1; // any negative disables it
t.SLAVES_MAX_DEATHS = -7;
CHECK_EQ(SlaveDeaths(1000, 0.2, 0.0, t), std::int64_t{200});
t.SLAVES_MIN_DEATHS = 0;
t.SLAVES_MAX_DEATHS = -1;
CHECK_EQ(SlaveDeaths(7, 0.2, 0.0, t), std::int64_t{1}); // 1.4 truncates
}
static void test_output() {
TuningTable t = tuning();
// B4: the trade slider is PINNED. Everything else is rescaled to what is left of 1.
OutputRates r = NormaliseOutputRates({1, 1, 1, 1}, false, false);
CHECK_NEAR(r.trade, 1.0, 0.0);
CHECK_NEAR(r.construction, 0.0, 0.0); // nothing left over for the other three
CHECK_NEAR(r.terraform, 0.0, 0.0);
CHECK_NEAR(r.infra, 0.0, 0.0);
r = NormaliseOutputRates({0.25, 0.25, 0.25, 0.25}, false, false);
CHECK_NEAR(r.trade, 0.25, 0.0); // untouched
CHECK_NEAR(r.construction, 0.25, 1e-7);
CHECK_NEAR(r.terraform, 0.25, 1e-7);
CHECK_NEAR(r.infra, 0.25, 1e-7);
r = NormaliseOutputRates({0, 1, 1, 1}, true, false);
CHECK_NEAR(r.terraform, 0.0, 0.0);
CHECK_NEAR(r.construction, 0.5, 1e-7);
CHECK_NEAR(r.infra, 0.5, 1e-7);
// all-zero: the three unpinned channels split evenly, trade stays at zero
r = NormaliseOutputRates({0, 0, 0, 0}, false, false);
CHECK_NEAR(r.trade, 0.0, 0.0);
CHECK_NEAR(r.construction, 1.0 / 3.0, 1e-6);
CHECK_NEAR(r.terraform, 1.0 / 3.0, 1e-6);
CHECK_NEAR(r.infra, 1.0 / 3.0, 1e-6);
// ... and the suppressions still apply inside the fallback
r = NormaliseOutputRates({0, 0, 0, 0}, true, true);
CHECK_NEAR(r.construction, 1.0, 1e-7);
CHECK_NEAR(r.terraform, 0.0, 0.0);
CHECK_NEAR(r.infra, 0.0, 0.0);
// a slider at or below the threshold counts as off
r = NormaliseOutputRates({kOutputRateThreshold, 1, 0, 0}, false, false);
CHECK_NEAR(r.trade, 0.0, 0.0);
CHECK_NEAR(r.construction, 1.0, 1e-7);
r = NormaliseOutputRates({-1, 3, 0, 1}, false, true);
CHECK_NEAR(r.trade, 0.0, 0.0);
CHECK_NEAR(r.construction, 1.0, 1e-7);
CHECK_NEAR(r.infra, 0.0, 0.0);
CHECK_NEAR(MoraleOutputMultiplier(80, t), 1.1, 0.0);
CHECK_NEAR(MoraleOutputMultiplier(75, t), 1.1, 0.0);
CHECK_NEAR(MoraleOutputMultiplier(50, t), 1.0, 0.0);
CHECK_NEAR(MoraleOutputMultiplier(25, t), 0.9, 0.0);
OutputModifiers m;
m.baseOutput = 1000;
m.morale = 80;
m.stations = 1;
CHECK_NEAR(TotalSystemOutput(m, t), 1210.0, 0.0); // 1000 x 1.1 x 1.1
m.addictionPhase3 = true;
CHECK_NEAR(TotalSystemOutput(m, t), 605.0, 0.0);
m.addictionPhase3 = false;
m.playerOutMod = 0.5;
m.systemOutMod = 0.5;
// B4: the engine's round is ties-to-EVEN, so 302.5 goes DOWN to 302 (it used to be 303)
CHECK_NEAR(TotalSystemOutput(m, t), 302.0, 0.0);
m.baseOutput = 0;
CHECK_NEAR(TotalSystemOutput(m, t), 0.0, 0.0);
CHECK_NEAR(RoundHalfEven(0.5), 0.0, 0.0);
CHECK_NEAR(RoundHalfEven(1.5), 2.0, 0.0);
CHECK_NEAR(RoundHalfEven(-2.5), -2.0, 0.0);
OutputSplit s = SplitOutput(1000, {0.5, 0.25, 0.125, 0.125});
CHECK_NEAR(s.trade, 500.0, 0.0);
CHECK_NEAR(s.construction, 250.0, 0.0);
CHECK_NEAR(s.terraform, 125.0, 0.0);
CHECK_NEAR(s.infra, 125.0, 0.0);
CHECK_EQ(ConstructionPoints(250, 2, t), 375);
CHECK_EQ(ConstructionPoints(250, 0, t), 250);
CHECK_EQ(ConstructionPoints(3, 1, t), 3); // 3.75 TRUNCATES, it does not round
OutputSplit l = SplitLeftover(100, {0.5, 0.25, 0.125, 0.125}, false, false);
CHECK_NEAR(l.trade, 67.0, 0.0);
CHECK_NEAR(l.terraform, 17.0, 0.0);
CHECK_NEAR(l.infra, 17.0, 0.0); // and 67+17+17 != 100
l = SplitLeftover(100, {0, 1, 0, 0}, true, false); // construction rate exactly 1
CHECK_NEAR(l.trade, 50.0, 0.0);
CHECK_NEAR(l.terraform, 0.0, 0.0);
CHECK_NEAR(l.infra, 50.0, 0.0);
l = SplitLeftover(100, {0, 1, 0, 0}, true, true);
CHECK_NEAR(l.trade, 100.0, 0.0);
l = SplitLeftover(0, {0.5, 0.25, 0.125, 0.125}, false, false);
CHECK_NEAR(l.trade, 0.0, 0.0);
}
static void test_system_money() {
CHECK_NEAR(SuitabilityCostMod(0.3, 0.5, 0.15, false, true), 0.15, 0.0); // capped by SuitTol
CHECK_NEAR(SuitabilityCostMod(0.45, 0.5, 0.15, false, true), 0.05, 1e-12);
CHECK_NEAR(SuitabilityCostMod(0.3, 0.5, 0.15, true, true), 0.0, 0.0); // rebel AI pays nothing
CHECK_NEAR(SuitabilityCostMod(0.5, 0.5, 0.15, false, false), 20.0, 0.0); // unowned
SystemMoneyInputs m;
m.tradePoints = 20; // 4 blocks x 5 = 100
CHECK_EQ(SystemMoneyIncome(m), 100);
m.tradePoints = 4.9; // no whole block
CHECK_EQ(SystemMoneyIncome(m), 0);
m.tradePoints = 123; // 24 blocks -> 600
CHECK_EQ(SystemMoneyIncome(m), 600);
m.popIncomeImperial = 100;
m.popIncomeCivilian = 50;
m.slaveIncome = 10;
m.speciesIncomeFactor = 1.1; // Zuul
m.speciesCostFactor = 0.7;
m.suitCostMod = 0.2;
// (600 + 160) x 1.1 = 836.0000000000001; cost 0.7 x 0.2 x 15000 = 2100;
// -1263.9999999999998 truncates toward zero
CHECK_EQ(SystemMoneyIncome(m), -1263);
m.speciesIncomeFactor = 1.0; // 760 - 0.25 x 15000 = -2990 exactly
m.speciesCostFactor = 1.0;
m.suitCostMod = 0.25;
CHECK_EQ(SystemMoneyIncome(m), -2990);
SystemMoneyInputs n;
n.tradePoints = 10;
n.speciesIncomeFactor = 0.8; // Morrigi
CHECK_EQ(SystemMoneyIncome(n), 40);
n.speciesIncomeFactor = 1.0;
n.tradePoints = 100; // 500
n.playerIncMod = 1.2; // 600
n.serverIncomeMod = 0.5;
n.difficultyIncomeMult = 2.0; // x1 net
CHECK_EQ(SystemMoneyIncome(n), 600);
SystemMoneyInputs unowned;
unowned.suitCostMod = 20.0; // 20 x 15000
CHECK_EQ(SystemMoneyIncome(unowned), -300000);
// the cost is not scaled by the income multipliers
SystemMoneyInputs c;
c.tradePoints = 10; // 50
c.playerIncMod = 3.0; // 150
c.suitCostMod = 0.01; // cost 150
CHECK_EQ(SystemMoneyIncome(c), 0);
CHECK_NEAR(ConstantsOf(Species::Zuul).incomeFactor, 1.1, 0.0);
CHECK_NEAR(ConstantsOf(Species::Zuul).hazardCostFactor, 0.7, 0.0);
CHECK_NEAR(ConstantsOf(Species::Morrigi).incomeFactor, 0.8, 0.0);
CHECK_NEAR(ConstantsOf(Species::Human).incomeFactor, 1.0, 0.0);
CHECK(!ConstantsOf(Species::Zuul).systemBonusEligible);
CHECK(ConstantsOf(Species::Hiver).systemBonusEligible);
}
static void test_bonuses() {
TuningTable t = tuning();
std::int64_t pop = 900, bonus = 500;
ApplyPopulationBonus(pop, 1000, bonus);
CHECK_EQ(pop, std::int64_t{1000});
CHECK_EQ(bonus, std::int64_t{400});
pop = 1200; // over cap: nothing applied
ApplyPopulationBonus(pop, 1000, bonus);
CHECK_EQ(pop, std::int64_t{1200});
CHECK_EQ(bonus, std::int64_t{400});
double infra = 0.95, ibon = 0.1;
BonusApplyResult br = ApplyInfrastructureBonus(infra, ibon);
CHECK_NEAR(infra, 1.0, 0.0); // the pool covered the remainder: exactly 1, not 0.999...
CHECK_NEAR(ibon, 0.05, 1e-7);
CHECK(br.applied);
CHECK(!br.resetTurnsDeveloping); // a home system is not reset
br = ApplyInfrastructureBonus(infra, ibon, /*homeSystem=*/false);
CHECK(!br.applied); // already at 1: nothing happens, no reset either
infra = 0.5;
ibon = 0.1;
br = ApplyInfrastructureBonus(infra, ibon, /*homeSystem=*/false);
CHECK_NEAR(infra, 0.6, 1e-7);
CHECK_NEAR(ibon, 0.0, 1e-7);
CHECK(br.resetTurnsDeveloping); // a non-home colony absorbing a bonus resets ntdev
std::int64_t up = 10, ub = 500;
ApplyPopulationBonus(up, 1000, ub, /*owned=*/false);
CHECK_EQ(up, std::int64_t{10}); // an unowned system drops the whole pool
CHECK_EQ(ub, std::int64_t{0});
SystemBonusInputs in;
in.stable = true;
in.turnsOwned = 11;
in.turnsDeveloping = 11;
in.capacity = 1000000;
std::int64_t pbon = 0;
double ibonus = 0;
AccrueSystemBonus(in, pbon, ibonus, t);
CHECK_EQ(pbon, std::int64_t{10000}); // 1e6 x 0.01
CHECK_NEAR(ibonus, 0.05, 1e-12);
for (int i = 0; i < 20; ++i) AccrueSystemBonus(in, pbon, ibonus, t);
CHECK_EQ(pbon, std::int64_t{100000}); // capped at 1e6 x 0.1
CHECK_NEAR(ibonus, 0.2, 1e-12); // capped at INFRABONUS
// the increment truncates: 12345 x 0.005 = 61.725 -> 61; target 1234.5 -> 1234
TuningTable small = t;
small.SYSTEMBONUS_POPBONUS_INC = 0.005;
in.capacity = 12345;
std::int64_t p3 = 0;
double i3 = 0;
AccrueSystemBonus(in, p3, i3, small);
CHECK_EQ(p3, std::int64_t{61});
for (int i = 0; i < 30; ++i) AccrueSystemBonus(in, p3, i3, small);
CHECK_EQ(p3, std::int64_t{1234});
p3 = 5000; // already above the target: untouched
AccrueSystemBonus(in, p3, i3, small);
CHECK_EQ(p3, std::int64_t{5000});
in.capacity = 1000000;
// Zuul never accrue either bonus
std::int64_t pz = 0;
double iz = 0;
in.ownerSpeciesEligible = false;
AccrueSystemBonus(in, pz, iz, t);
CHECK_EQ(pz, std::int64_t{0});
CHECK_NEAR(iz, 0.0, 0.0);
in.ownerSpeciesEligible = true;
std::int64_t p2 = 0;
double i2 = 0;
in.turnsOwned = 10; // not strictly more than MINTURNS
AccrueSystemBonus(in, p2, i2, t);
CHECK_EQ(p2, std::int64_t{0});
in.turnsOwned = 11;
in.stable = false;
AccrueSystemBonus(in, p2, i2, t);
CHECK_EQ(p2, std::int64_t{0});
in.stable = true;
in.turnsDeveloping = 10;
AccrueSystemBonus(in, p2, i2, t);
CHECK_EQ(p2, std::int64_t{0});
}
static void test_build_queue() {
std::vector<BuildOrder> q = {{1, 11, 100, 100, 50}, {2, 12, 200, 200, 0}, {3, 13, 300, 300, 70}};
BuildQueueResult r = ProcessBuildQueue(q, 250);
CHECK_EQ(r.completedOrderIds.size(), std::size_t{1});
CHECK_EQ(r.completedOrderIds[0], 11);
CHECK_EQ(r.moneyCharged, 50);
CHECK_EQ(r.pointsLeft, 0);
CHECK_EQ(q.size(), std::size_t{2});
CHECK_EQ(q[0].orderId, 12);
CHECK_EQ(q[0].constructionLeft, 50);
CHECK_EQ(q[1].constructionLeft, 300);
r = ProcessBuildQueue(q, 700);
CHECK_EQ(r.completedOrderIds.size(), std::size_t{2});
CHECK_EQ(r.moneyCharged, 70);
CHECK_EQ(r.pointsLeft, 350);
CHECK(q.empty());
r = ProcessBuildQueue(q, 100); // empty queue: points pass through
CHECK_EQ(r.pointsLeft, 100);
std::vector<BuildOrder> exact = {{1, 21, 100, 100, 0}};
r = ProcessBuildQueue(exact, 100); // exactly enough completes
CHECK_EQ(r.completedOrderIds.size(), std::size_t{1});
CHECK(exact.empty());
std::vector<BuildOrder> zero = {{1, 31, 100, 100, 0}};
r = ProcessBuildQueue(zero, 0);
CHECK(r.completedOrderIds.empty());
CHECK_EQ(zero[0].constructionLeft, 100);
}
int main() {
test_capacity();
test_growth();
test_infra_terraform();
test_slaves();
test_output();
test_system_money();
test_bonuses();
test_build_queue();
return simtest::finish("test_colony");
}