sots-engine/tests/game_effects/test_effects.cpp

602 lines
26 KiB
C++

#include "game/effects/tech_effects.h"
#include "game/effects/tech_id.h"
#include <set>
#include <string>
#include "check.h" // shared with tests/game_sim
using namespace sots::effects;
using sots::sim::Species;
static ApplyContext ctx_with_tuning(sots::sim::TuningTable& t) {
ApplyContext c;
c.tuning = &t;
return c;
}
static void test_ids() {
CHECK_EQ(static_cast<int>(TechId::CCC_AdvSens), 10000);
CHECK_EQ(static_cast<int>(TechId::IND_HrdStrct), 10030);
CHECK_EQ(static_cast<int>(TechId::WEP_ACCAMP), 10192);
CHECK_EQ(static_cast<int>(TechId::None), 10197);
CHECK(IsValidTechId(10000) && IsValidTechId(10195));
CHECK(!IsValidTechId(9999) && !IsValidTechId(10196) && !IsValidTechId(TechId::None));
CHECK_EQ(TechIdIndex(TechId::DRV_RAD), 66);
CHECK(TechIdFromIndex(64) == TechId::DRV_RIP);
CHECK(std::string(TechIdName(TechId::IND_Waldo)) == "IND_Waldo");
CHECK(std::string(TechIdName(TechId::DRV_FUSN)) == "DRV_FUSN");
CHECK(TechIdName(TechId::None) == nullptr);
// The table is complete: every position has a name, and every name resolves back.
for (int i = 0; i < kTechIdCount; ++i) {
const char* n = TechIdName(TechIdFromIndex(i));
CHECK(n != nullptr && n[0] != '\0');
}
CHECK(TechIdFromName("IND_Waldo") == TechId::IND_Waldo);
CHECK(TechIdFromName("ind_waldo") == TechId::IND_Waldo); // case-insensitive like the game
CHECK(TechIdFromName("CCC_TRNSLIR") == TechId::XNC_Translation1_Liir);
CHECK(TechIdFromName("not_a_tech") == TechId::None);
CHECK(TechIdFromName("") == TechId::None);
// every resolved name round-trips, and the table has exactly 196 slots
int named = 0;
for (int i = 0; i < kTechIdCount; ++i) {
const char* n = TechIdName(TechIdFromIndex(i));
if (n == nullptr) continue;
++named;
CHECK(TechIdFromName(n) == TechIdFromIndex(i));
}
CHECK_EQ(named, kTechIdCount); // the whole table is recovered
// xenotech blocks
CHECK(XenoTechId(XenoLevel::Translation1, Species::Human) == TechId::XNC_Translation1_Human);
CHECK(XenoTechId(XenoLevel::Translation1, Species::Morrigi) == TechId::XNC_Translation1_Morrigi);
CHECK_EQ(static_cast<int>(XenoTechId(XenoLevel::Translation2, Species::Morrigi)), 10125);
CHECK_EQ(static_cast<int>(XenoTechId(XenoLevel::Incorporate, Species::Morrigi)), 10136);
CHECK(XenoTechId(XenoLevel::Incorporate, Species::Zuul) == TechId::None);
CHECK(XenoTechId(XenoLevel::Subjugate, Species::Zuul) == TechId::XNC_Subjugate_Zuul);
CHECK_EQ(static_cast<int>(XenoTechId(XenoLevel::Proliferate, Species::Morrigi)), 10162);
CHECK(XenoTechId(XenoLevel::Temperance, Species::NPC) == TechId::None);
CHECK(XenoTechId(XenoLevel::Translation3, Species::NPC) == TechId::None);
CHECK(std::string(NodeTrackTechName(Species::Human)) == "CCC_NDTRKHUM");
CHECK(std::string(NodeTrackTechName(Species::Zuul)) == "CCC_NDTRKZUL");
CHECK(NodeTrackTechName(Species::Hiver) == nullptr);
}
static void test_industrial() {
PlayerEconomyState s;
ApplyContext ctx;
TechApplyOutcome o = ApplyTechEffect(s, TechId::IND_Waldo, ctx);
CHECK(o.applied);
CHECK_NEAR(s.conMod[0], 0.90, 1e-6);
CHECK_NEAR(s.conMod[1], 0.90, 1e-6);
CHECK_NEAR(s.conMod[2], 0.90, 1e-6);
CHECK_NEAR(s.outMod, 1.15, 1e-6);
CHECK(s.HasResearched(TechId::IND_Waldo));
o = ApplyTechEffect(s, TechId::IND_Waldo, ctx); // no double application
CHECK(!o.applied);
CHECK_NEAR(s.outMod, 1.15, 1e-6);
ApplyTechEffect(s, TechId::IND_CyberInt, ctx); // -0.05 / +0.20
CHECK_NEAR(s.conMod[0], 0.85, 1e-6);
CHECK_NEAR(s.outMod, 1.35, 1e-6);
ApplyTechEffect(s, TechId::IND_ExpSys, ctx); // -0.10 / +0.15
CHECK_NEAR(s.conMod[2], 0.75, 1e-6);
CHECK_NEAR(s.outMod, 1.50, 1e-6);
ApplyTechEffect(s, TechId::IND_OrbDry, ctx); // classes 1 and 2 only
CHECK_NEAR(s.conMod[0], 0.75, 1e-6);
CHECK_NEAR(s.conMod[1], 0.70, 1e-6);
CHECK_NEAR(s.conMod[2], 0.70, 1e-6);
ApplyTechEffect(s, TechId::DRN_AdvRob, ctx);
CHECK_NEAR(s.conMod[0], 0.70, 1e-6);
CHECK_NEAR(s.conMod[1], 0.65, 1e-6);
ApplyTechEffect(s, TechId::IND_OrbFound, ctx);
CHECK_NEAR(s.savMod[0], 0.95, 1e-6);
CHECK_NEAR(s.savMod[2], 0.95, 1e-6);
ApplyTechEffect(s, TechId::IND_GravCon, ctx); // 1.80
ApplyTechEffect(s, TechId::IND_HvyPlat, ctx); // 1.90
CHECK_NEAR(s.outMod, 1.90, 1e-6);
ApplyTechEffect(s, TechId::IND_HrdStrct, ctx); // multiplicative
CHECK_NEAR(s.outMod, 1.71, 1e-6);
CHECK_NEAR(s.defenceDamageMod, 0.25, 1e-6);
// the order of additive and multiplicative effects matters
PlayerEconomyState r;
ApplyTechEffect(r, TechId::IND_HrdStrct, ctx);
ApplyTechEffect(r, TechId::IND_GravCon, ctx);
CHECK_NEAR(r.outMod, 1.20, 1e-6); // 0.9 + 0.3, not 1.3 x 0.9
ApplyTechEffect(s, TechId::IND_AstMine, ctx);
CHECK(s.Flag(PlayerFlag::AsteroidMining));
ApplyTechEffect(s, TechId::IND_MsMine, ctx);
CHECK_NEAR(s.maxOverharvest, 0.1f, 0.0);
CHECK_NEAR(s.miningRate, 1.0, 0.0);
PlayerEconomyState q;
q.maxOverharvest = 0.3f;
ApplyTechEffect(q, TechId::IND_MsMine, ctx);
CHECK_NEAR(q.maxOverharvest, 0.3f, 0.0); // max(), not assignment
ApplyTechEffect(s, TechId::CCC_AdvSens, ctx);
CHECK(s.Flag(PlayerFlag::AdvancedSensors));
}
static void test_biology() {
PlayerEconomyState s;
s.suitTol = 0.07;
ApplyContext ctx;
ApplyTechEffect(s, TechId::BIO_GnMod, ctx);
CHECK_NEAR(s.popMod, 1.10, 1e-6);
ApplyTechEffect(s, TechId::BIO_AtmoAd, ctx);
CHECK_NEAR(s.suitTol, 0.82, 1e-6);
CHECK_NEAR(s.popMod, 1.16, 1e-6);
CHECK_NEAR(s.terraMod, 1.35, 1e-6);
ApplyTechEffect(s, TechId::BIO_GrvAdpt, ctx);
CHECK_NEAR(s.suitTol, 2.32, 1e-6);
CHECK_NEAR(s.popMod, 1.26, 1e-6);
CHECK_NEAR(s.terraMod, 1.70, 1e-6);
ApplyTechEffect(s, TechId::BIO_EnvTail, ctx);
CHECK_NEAR(s.popMod, 1.46, 1e-6);
CHECK_NEAR(s.terraMod, 2.15, 1e-6);
ApplyTechEffect(s, TechId::IND_EleNans, ctx); // +0.60
ApplyTechEffect(s, TechId::BIO_TerBac, ctx); // +0.45
ApplyTechEffect(s, TechId::IND_AtProc, ctx); // +0.50
CHECK_NEAR(s.terraMod, 3.70, 1e-6);
TechApplyOutcome o = ApplyTechEffect(s, TechId::IND_ArcCon, ctx);
CHECK(s.Flag(PlayerFlag::Arcology));
CHECK_NEAR(s.popMod, 1.61, 1e-6);
CHECK(o.reevaluateCivilianCaps);
}
static void test_gates_and_casting() {
sots::sim::TuningTable t;
t.PERGATETRAFFIC_DRV_TpGate = 5;
t.PERGATETRAFFIC_DRV_GatAmp = 12;
ApplyContext ctx = ctx_with_tuning(t);
PlayerEconomyState s;
ApplyTechEffect(s, TechId::DRV_TpGate, ctx);
CHECK_NEAR(s.perGateTraffic, 5.0, 0.0);
ApplyTechEffect(s, TechId::DRV_GatAmp, ctx);
CHECK_NEAR(s.perGateTraffic, 12.0, 0.0);
PlayerEconomyState r; // reverse order: max wins
ApplyTechEffect(r, TechId::DRV_GatAmp, ctx);
ApplyTechEffect(r, TechId::DRV_TpGate, ctx);
CHECK_NEAR(r.perGateTraffic, 12.0, 0.0);
PlayerEconomyState n; // no tuning: nothing to raise
ApplyTechEffect(n, TechId::DRV_TpGate, ApplyContext{});
CHECK_NEAR(n.perGateTraffic, 0.0, 0.0);
ApplyTechEffect(s, TechId::DRV_FarCast, ctx);
CHECK_NEAR(s.castRange, 10.0, 0.0);
CHECK_NEAR(s.castEfficiency, 2.0, 0.0);
CHECK_NEAR(s.castThreshold, 1.0, 0.0);
ApplyTechEffect(s, TechId::DRV_GrvSyn, ctx);
CHECK(s.Flag(PlayerFlag::GravSynth));
}
static void test_ai() {
ApplyContext ctx;
ctx.aiBonus = {0.2, 0.3, 0.4};
PlayerEconomyState s;
CHECK(s.aiBenefit);
ApplyTechEffect(s, TechId::CCC_AI, ctx);
CHECK_NEAR(s.resMod, 1.2, 1e-6);
ApplyTechEffect(s, TechId::CCC_AIAdmin, ctx);
CHECK_NEAR(s.incMod, 1.3, 1e-6);
ApplyTechEffect(s, TechId::CCC_AIFac, ctx);
CHECK_NEAR(s.outMod, 1.4, 1e-6);
SetAiBenefit(s, false, ctx); // rebellion: bonuses withdrawn
CHECK_NEAR(s.resMod, 1.0, 1e-6);
CHECK_NEAR(s.incMod, 1.0, 1e-6);
CHECK_NEAR(s.outMod, 1.0, 1e-6);
SetAiBenefit(s, false, ctx); // idempotent
CHECK_NEAR(s.outMod, 1.0, 1e-6);
SetAiBenefit(s, true, ctx);
CHECK_NEAR(s.resMod, 1.2, 1e-6);
CHECK_NEAR(s.outMod, 1.4, 1e-6);
PlayerEconomyState off; // researched while off: nothing
off.aiBenefit = false;
ApplyTechEffect(off, TechId::CCC_AIFac, ctx);
CHECK_NEAR(off.outMod, 1.0, 1e-6);
TechApplyOutcome o = ApplyTechEffect(off, TechId::CCC_AISlv, ctx); // slave AI: benefit back on
CHECK(off.aiBenefit);
CHECK_NEAR(off.outMod, 1.4, 1e-6);
CHECK(o.flagSystemsAI);
CHECK(!AiRebellionPossible(off));
PlayerEconomyState v;
CHECK(AiRebellionPossible(v));
o = ApplyTechEffect(v, TechId::CCC_AIVrus, ctx);
CHECK(o.flagSystemsAI);
CHECK(AiRebellionPossible(v));
v.species = Species::NPC;
CHECK(!AiRebellionPossible(v));
PlayerEconomyState none; // caller overrides to 0
ApplyContext zero;
zero.aiBonus = AiBonusValues{0, 0, 0};
ApplyTechEffect(none, TechId::CCC_AI, zero);
CHECK_NEAR(none.resMod, 1.0, 0.0);
// The table's own values, recovered in B2: 0.5 into each of the three slots.
PlayerEconomyState dflt;
ApplyTechEffect(dflt, TechId::CCC_AI, ApplyContext{});
CHECK_NEAR(dflt.resMod, 1.5, 0.0);
ApplyTechEffect(dflt, TechId::CCC_AIAdmin, ApplyContext{});
CHECK_NEAR(dflt.incMod, 1.5, 0.0);
ApplyTechEffect(dflt, TechId::CCC_AIFac, ApplyContext{});
CHECK_NEAR(dflt.outMod, 1.5, 0.0);
CHECK_NEAR(AiRebellionOdds(TechId::CCC_AI), 0.1f, 0.0);
CHECK_NEAR(AiRebellionOdds(TechId::CCC_AIFRCON), 0.2f, 0.0);
CHECK_NEAR(AiRebellionOdds(TechId::CCC_AISlv), 0.0, 0.0);
}
static void test_flags_and_species() {
ApplyContext ctx;
PlayerEconomyState s;
ApplyTechEffect(s, TechId::CCC_FtlEcon, ctx);
CHECK(s.Flag(PlayerFlag::TradeAllowed));
PlayerEconomyState rebel;
rebel.rebelAI = true;
ApplyTechEffect(rebel, TechId::CCC_FtlEcon, ctx);
CHECK(!rebel.Flag(PlayerFlag::TradeAllowed));
ApplyTechEffect(s, TechId::CCC_ComRaid, ctx);
CHECK(s.Flag(PlayerFlag::CommerceRaiding));
ApplyTechEffect(s, TechId::CCC_DatCor, ctx);
CHECK(s.Flag(PlayerFlag::ViewIntel));
// design capture needs both techs, in either order
ApplyTechEffect(s, TechId::CCC_SpyBm, ctx);
CHECK(!s.Flag(PlayerFlag::CaptureDesigns));
ApplyTechEffect(s, TechId::IND_SlvgTech, ctx);
CHECK(s.Flag(PlayerFlag::CaptureDesigns));
PlayerEconomyState r;
ApplyTechEffect(r, TechId::IND_SlvgTech, ctx);
CHECK(!r.Flag(PlayerFlag::CaptureDesigns));
ApplyTechEffect(r, TechId::CCC_SpyBm, ctx);
CHECK(r.Flag(PlayerFlag::CaptureDesigns));
// Zuul get boarding pods with cruiser construction; nobody else does
TechApplyOutcome o = ApplyTechEffect(s, TechId::IND_CruisCon, ctx);
CHECK(o.grantedTech == TechId::None);
PlayerEconomyState z;
z.species = Species::Zuul;
o = ApplyTechEffect(z, TechId::IND_CruisCon, ctx);
CHECK(o.grantedTech == TechId::IND_BrdPod);
CHECK(!z.HasResearched(TechId::IND_BrdPod)); // the caller researches it
o = ApplyTechEffect(z, o.grantedTech, ctx);
CHECK(o.applied && z.HasResearched(TechId::IND_BrdPod));
// node-bore parameters: highest drive wins regardless of order
ApplyTechEffect(z, TechId::DRV_RIP, ctx);
CHECK_EQ(z.nodeBoreParams[0], 45);
CHECK_EQ(z.nodeBoreParams[2], 3);
o = ApplyTechEffect(z, TechId::DRV_RAD, ctx);
CHECK(o.nodeBoreParamsChanged);
CHECK_EQ(z.nodeBoreParams[0], 95);
CHECK_EQ(z.nodeBoreParams[1], 60);
o = ApplyTechEffect(z, TechId::DRV_REND, ctx);
CHECK(!o.nodeBoreParamsChanged);
CHECK_EQ(z.nodeBoreParams[0], 95);
}
static void test_plague() {
ApplyContext ctx;
PlayerEconomyState s;
CHECK_EQ(PlagueCureMask(TechId::BIO_PLGVAC), 1u);
CHECK_EQ(PlagueCureMask(TechId::BIO_CONNAN), 16u);
CHECK_EQ(PlagueCureMask(TechId::BIO_UNIANTI), 15u);
CHECK_EQ(PlagueCureMask(TechId::IND_Waldo), 0u);
TechApplyOutcome o = ApplyTechEffect(s, TechId::BIO_RTPLGVAC, ctx);
CHECK_EQ(s.hasVaccine, 2u);
CHECK_EQ(s.hasImmunity, 2u);
CHECK_EQ(o.plagueCuredMask, 2u);
o = ApplyTechEffect(s, TechId::BIO_UNIANTI, ctx);
CHECK_EQ(s.hasVaccine, 15u);
CHECK_EQ(o.plagueCuredMask, 15u);
ApplyTechEffect(s, TechId::BIO_CONNAN, ctx);
CHECK_EQ(s.hasImmunity, 31u);
}
static void test_xenotech() {
ApplyContext ctx;
PlayerEconomyState s;
TechApplyOutcome o = ApplyTechEffect(s, TechId::XNC_Translation1_Liir, ctx);
CHECK_EQ(s.speciesFlags[static_cast<int>(Species::Liir)], 0x001u);
CHECK_EQ(s.speciesFlags[static_cast<int>(Species::Human)], 0u);
CHECK_EQ(o.temperanceSpeciesMask, 0u);
ApplyTechEffect(s, TechId::XNC_Translation3_Liir, ctx);
CHECK_EQ(s.speciesFlags[static_cast<int>(Species::Liir)], 0x005u);
o = ApplyTechEffect(s, TechId::XNC_Temperance_Hiver, ctx);
CHECK_EQ(s.speciesFlags[static_cast<int>(Species::Hiver)], 0x020u);
CHECK_EQ(o.temperanceSpeciesMask, 1u << static_cast<int>(Species::Hiver));
o = ApplyTechEffect(s, TechId::XNC_Accommodate_Morrigi, ctx); // every completion reports temperance
CHECK_EQ(s.speciesFlags[static_cast<int>(Species::Morrigi)], 0x080u);
CHECK_EQ(o.temperanceSpeciesMask, 1u << static_cast<int>(Species::Hiver));
// Proliferate has no Zuul entry, so the Zuul word never gets bit 8.
ApplyTechEffect(s, TechId::XNC_Proliferate_Tarkas, ctx);
CHECK_EQ(s.speciesFlags[static_cast<int>(Species::Tarkas)], 0x100u);
ApplyTechEffect(s, TechId::XNC_Subjugate_Zuul, ctx);
CHECK_EQ(s.speciesFlags[static_cast<int>(Species::Zuul)], 0x040u);
// rebuild from the researched set alone (load path)
PlayerEconomyState l;
l.researched = s.researched;
RebuildSpeciesTechFlags(l);
for (int sp = 0; sp < sots::sim::kSpeciesCount; ++sp) CHECK_EQ(l.speciesFlags[sp], s.speciesFlags[sp]);
}
static void test_by_name() {
ApplyContext ctx;
PlayerEconomyState s;
TechApplyOutcome o = ApplyTechEffectByName(s, "ind_gravcon", ctx);
CHECK(o.applied);
CHECK_NEAR(s.outMod, 1.30, 1e-6);
o = ApplyTechEffectByName(s, "CCC_NDTRKHUM", ctx);
CHECK(o.applied);
CHECK_EQ(s.nodeTrackMask, 1u << static_cast<int>(Species::Human));
o = ApplyTechEffectByName(s, "ccc_ndtrkzul", ctx);
CHECK_EQ(s.nodeTrackMask, (1u << static_cast<int>(Species::Human)) | (1u << static_cast<int>(Species::Zuul)));
o = ApplyTechEffectByName(s, "WEP_SOMETHING_DATA_ONLY", ctx);
CHECK(!o.applied);
CHECK_NEAR(s.outMod, 1.30, 1e-6);
o = ApplyTechEffectByName(s, "IND_SPNLMNT", ctx); // in the table, no effect
CHECK(o.applied);
CHECK(s.HasResearched(TechId::IND_SPNLMNT));
CHECK_NEAR(s.outMod, 1.30, 1e-6);
}
static void test_table_shape() {
// Techs with a strategic effect have entries; unresolved and gate-only ids do not.
CHECK(!EffectsOf(TechId::IND_Waldo).empty());
CHECK_EQ(EffectsOf(TechId::IND_Waldo).size(), std::size_t{2});
CHECK(EffectsOf(TechId::DRV_FUSN).empty());
CHECK(EffectsOf(TechId::CCC_HYPCOM).empty());
CHECK(EffectsOf(TechId::None).empty());
int withEffects = 0;
for (int i = 0; i < kTechIdCount; ++i) {
if (!EffectsOf(TechIdFromIndex(i)).empty()) ++withEffects;
}
CHECK_EQ(withEffects, 44);
// The proliferate block is five entries and the two slots after it are the node-track
// techs -- the earlier reconstruction had a six-entry block running over them.
CHECK(XenoTechId(XenoLevel::Proliferate, Species::Zuul) == TechId::None);
CHECK(XenoTechId(XenoLevel::Proliferate, Species::Morrigi) == TechId::XNC_Proliferate_Morrigi);
CHECK_EQ(static_cast<int>(TechId::XNC_Proliferate_Morrigi), 10162);
CHECK_EQ(static_cast<int>(TechId::CCC_NDTRKHUM), 10163);
CHECK_EQ(static_cast<int>(TechId::CCC_NDTRKZUL), 10164);
CHECK(TechIdFromName(NodeTrackTechName(Species::Human)) == TechId::CCC_NDTRKHUM);
CHECK(TechIdFromName(NodeTrackTechName(Species::Zuul)) == TechId::CCC_NDTRKZUL);
CHECK(NodeTrackTechName(Species::Morrigi) == nullptr);
// design-option masks
std::set<std::string> have = {"IND_REFCOAT", "SLD_INTANG", "DRV_NODE", "WEP_HvyPmsl"};
DesignOptionMasks m = ComputeDesignOptionMasks([&](std::string_view n) { return have.count(std::string(n)) > 0; });
CHECK_EQ(m.a, (1u << 0) | (1u << 15));
CHECK_EQ(m.b, (1u << 0) | (1u << 28));
m = ComputeDesignOptionMasks([](std::string_view) { return false; });
CHECK_EQ(m.a, 0u);
CHECK_EQ(m.b, 0u);
m = ComputeDesignOptionMasks([](std::string_view) { return true; });
CHECK_EQ(m.a, 0xffffffffu);
CHECK_EQ(m.b, 0x1fffffffu);
}
// ---- B2: the corrections read off the completion callback's instruction stream --------
static void test_float32_state() {
ApplyContext ctx;
// The modifiers are float32 in the player object and every step rounds through float32.
// Six terraform techs in a row: accumulating in double and rounding once gives a
// different float from rounding at every step, which is what the original does.
PlayerEconomyState s;
const TechId chain[] = {TechId::BIO_AtmoAd, TechId::BIO_GrvAdpt, TechId::BIO_EnvTail,
TechId::BIO_TerBac, TechId::IND_AtProc, TechId::IND_EleNans};
for (TechId id : chain) ApplyTechEffect(s, id, ctx);
float stepwise = 1.f;
for (double k : {0.35f, 0.35f, 0.45f, 0.45f, 0.50f, 0.60f})
stepwise = static_cast<float>(static_cast<double>(stepwise) + k);
CHECK(s.terraMod == stepwise);
// The constants are widened float32 literals, not the exact decimals they look like.
CHECK(static_cast<double>(0.05f) != 0.05);
CHECK(static_cast<double>(0.45f) != 0.45);
CHECK(static_cast<double>(0.9f) != 0.9);
CHECK(static_cast<double>(0.75f) == 0.75); // exactly representable, unaffected
// The multiplicative tech runs on the float32 that the additive ones left behind.
PlayerEconomyState m;
ApplyTechEffect(m, TechId::IND_Waldo, ctx);
ApplyTechEffect(m, TechId::IND_HrdStrct, ctx);
const float expect = static_cast<float>(
static_cast<double>(static_cast<float>(1.0 + static_cast<double>(0.15f))) * static_cast<double>(0.9f));
CHECK(m.outMod == expect);
CHECK(m.defenceDamageMod == 0.25f);
}
static void test_gate_traffic_is_integer() {
sots::sim::TuningTable t;
t.PERGATETRAFFIC_DRV_TpGate = 5;
t.PERGATETRAFFIC_DRV_GatAmp = 12;
ApplyContext ctx = ctx_with_tuning(t);
PlayerEconomyState s;
ApplyTechEffect(s, TechId::DRV_GatAmp, ctx);
CHECK_EQ(s.perGateTraffic, 12);
ApplyTechEffect(s, TechId::DRV_TpGate, ctx);
CHECK_EQ(s.perGateTraffic, 12); // integer max, the smaller key loses
PlayerEconomyState r;
r.perGateTraffic = 20;
ApplyTechEffect(r, TechId::DRV_GatAmp, ctx);
CHECK_EQ(r.perGateTraffic, 20); // an already higher value is kept
}
static void test_node_bore() {
ApplyContext ctx;
PlayerEconomyState s;
CHECK(!s.hasNodeBoreParams);
TechApplyOutcome o = ApplyTechEffect(s, TechId::DRV_REND, ctx);
CHECK(o.nodeBoreParamsChanged);
CHECK(s.hasNodeBoreParams);
CHECK_EQ(s.nodeBoreParams[0], 65);
CHECK_EQ(s.nodeBoreParams[1], 35);
CHECK_EQ(s.nodeBoreParams[2], 4);
// Highest wins, whatever the order: a lower drive researched afterwards changes nothing.
o = ApplyTechEffect(s, TechId::DRV_RIP, ctx);
CHECK(!o.nodeBoreParamsChanged);
CHECK_EQ(s.nodeBoreParams[0], 65);
o = ApplyTechEffect(s, TechId::DRV_RAD, ctx);
CHECK(o.nodeBoreParamsChanged);
CHECK_EQ(s.nodeBoreParams[0], 95);
CHECK_EQ(s.nodeBoreParams[1], 60);
CHECK_EQ(s.nodeBoreParams[2], 5);
// The set is re-derived on every completion, not only on a bore tech's own.
PlayerEconomyState t;
t.researched.set(static_cast<std::size_t>(TechIdIndex(TechId::DRV_RIP)));
ApplyTechEffect(t, TechId::IND_Waldo, ctx);
CHECK(t.hasNodeBoreParams);
CHECK_EQ(t.nodeBoreParams[0], 45);
}
static void test_capture_designs_is_a_tail_check() {
ApplyContext ctx;
PlayerEconomyState s;
ApplyTechEffect(s, TechId::CCC_SpyBm, ctx);
CHECK(!s.Flag(PlayerFlag::CaptureDesigns));
ApplyTechEffect(s, TechId::IND_SlvgTech, ctx);
CHECK(s.Flag(PlayerFlag::CaptureDesigns));
// Both already researched but the flag not yet set (a loaded save, say): the next
// completion of any tech at all sets it, because the test is in the tail.
PlayerEconomyState t;
t.researched.set(static_cast<std::size_t>(TechIdIndex(TechId::CCC_SpyBm)));
t.researched.set(static_cast<std::size_t>(TechIdIndex(TechId::IND_SlvgTech)));
CHECK(!t.Flag(PlayerFlag::CaptureDesigns));
ApplyTechEffect(t, TechId::IND_HvyPlat, ctx);
CHECK(t.Flag(PlayerFlag::CaptureDesigns));
}
static void test_translation_known_mask() {
ApplyContext ctx;
PlayerEconomyState s;
CHECK_EQ(s.translationKnownMask, 0u);
ApplyTechEffect(s, XenoTechId(XenoLevel::Translation1, Species::Liir), ctx);
CHECK_EQ(s.translationKnownMask, 1u << static_cast<int>(Species::Liir));
ApplyTechEffect(s, XenoTechId(XenoLevel::Translation2, Species::Hiver), ctx);
CHECK_EQ(s.translationKnownMask, 1u << static_cast<int>(Species::Liir)); // level 2 alone: no bit
ApplyTechEffect(s, XenoTechId(XenoLevel::Translation1, Species::Hiver), ctx);
CHECK_EQ(s.translationKnownMask,
(1u << static_cast<int>(Species::Liir)) | (1u << static_cast<int>(Species::Hiver)));
// Sticky: clearing the researched bit and re-running the rebuild does not take it back.
s.researched.reset(static_cast<std::size_t>(TechIdIndex(XenoTechId(XenoLevel::Translation1, Species::Liir))));
RebuildSpeciesTechFlags(s);
CHECK(s.translationKnownMask & (1u << static_cast<int>(Species::Liir)));
CHECK_EQ(s.speciesFlags[static_cast<int>(Species::Liir)], 0u);
}
static void test_tail_runs_for_unkeyed_techs() {
// A tech outside the 196-name key space has no branch, but the callback still runs its
// whole tail: the capture-designs pair test, the flag words and the bore selection.
PlayerEconomyState s;
s.researched.set(static_cast<std::size_t>(TechIdIndex(TechId::CCC_SpyBm)));
s.researched.set(static_cast<std::size_t>(TechIdIndex(TechId::IND_SlvgTech)));
s.researched.set(static_cast<std::size_t>(TechIdIndex(TechId::DRV_REND)));
s.researched.set(static_cast<std::size_t>(TechIdIndex(XenoTechId(XenoLevel::Translation1, Species::Liir))));
const TechApplyOutcome o = RunCompletionTail(s);
CHECK(o.applied);
CHECK(s.Flag(PlayerFlag::CaptureDesigns));
CHECK(s.hasNodeBoreParams && s.nodeBoreParams[0] == 65);
CHECK(o.nodeBoreParamsChanged);
CHECK_EQ(s.speciesFlags[static_cast<int>(Species::Liir)], 1u);
CHECK_EQ(s.translationKnownMask, 1u << static_cast<int>(Species::Liir));
}
static void test_completion_has_no_guard() {
ApplyContext ctx;
// The callback runs after the node is already marked researched, so it cannot carry an
// already-researched guard. ApplyTechEffect (the caller-facing wrapper) still does.
PlayerEconomyState s;
ApplyTechCompletion(s, TechId::IND_HvyPlat, ctx);
const float once = s.outMod;
TechApplyOutcome o = ApplyTechCompletion(s, TechId::IND_HvyPlat, ctx);
CHECK(o.applied);
CHECK(s.outMod != once); // applied a second time
PlayerEconomyState g;
ApplyTechEffect(g, TechId::IND_HvyPlat, ctx);
const float g1 = g.outMod;
o = ApplyTechEffect(g, TechId::IND_HvyPlat, ctx);
CHECK(!o.applied);
CHECK(g.outMod == g1);
}
static void test_design_option_ids() {
// The two tables key on tech ids in the executable; the by-name and by-id builders must
// therefore agree bit for bit.
for (int i = 0; i < kDesignOptionCountA; ++i) {
const char* n = TechIdName(kDesignOptionIdsA[i]);
CHECK(n != nullptr && std::string(n) == kDesignOptionNamesA[i]);
}
for (int i = 0; i < kDesignOptionCountB; ++i) {
const char* n = TechIdName(kDesignOptionIdsB[i]);
CHECK(n != nullptr && std::string(n) == kDesignOptionNamesB[i]);
}
// Anchors that were known from unrelated readers before the tables were dumped.
CHECK(kDesignOptionIdsA[28] == TechId::CCC_AdvSens);
CHECK(kDesignOptionIdsB[12] == TechId::DRV_RIP);
CHECK(kDesignOptionIdsB[15] == TechId::BIO_CONNAN);
CHECK(kDesignOptionIdsB[21] == TechId::IND_TRKSTL);
CHECK(kDesignOptionIdsB[28] == TechId::WEP_HvyPmsl);
std::set<int> have = {static_cast<int>(TechId::IND_REFCOAT), static_cast<int>(TechId::SLD_INTANG),
static_cast<int>(TechId::DRV_NODE), static_cast<int>(TechId::WEP_HvyPmsl)};
DesignOptionMasks m = ComputeDesignOptionMasks(
[&](TechId id) { return have.count(static_cast<int>(id)) > 0; });
CHECK_EQ(m.a, (1u << 0) | (1u << 15));
CHECK_EQ(m.b, (1u << 0) | (1u << 28));
m = ComputeDesignOptionMasks([](TechId) { return true; });
CHECK_EQ(m.a, 0xffffffffu);
CHECK_EQ(m.b, 0x1fffffffu);
}
int main() {
test_ids();
test_industrial();
test_biology();
test_gates_and_casting();
test_ai();
test_flags_and_species();
test_plague();
test_xenotech();
test_by_name();
test_table_shape();
test_float32_state();
test_gate_traffic_is_integer();
test_node_bore();
test_capture_designs_is_a_tail_check();
test_translation_known_mask();
test_tail_runs_for_unkeyed_techs();
test_completion_has_no_guard();
test_design_option_ids();
return simtest::finish("test_effects");
}