sots-engine/tests/game_effects/test_effects.cpp

377 lines
15 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(TechIdName(TechId::Unresolved_052) == nullptr);
CHECK(TechIdName(TechId::None) == nullptr);
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, 89); // recovered data-file names so far; bump when more are resolved
// 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)), 10163);
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-12);
CHECK_NEAR(s.conMod[1], 0.90, 1e-12);
CHECK_NEAR(s.conMod[2], 0.90, 1e-12);
CHECK_NEAR(s.outMod, 1.15, 1e-12);
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-12);
ApplyTechEffect(s, TechId::IND_CyberInt, ctx); // -0.05 / +0.20
CHECK_NEAR(s.conMod[0], 0.85, 1e-12);
CHECK_NEAR(s.outMod, 1.35, 1e-12);
ApplyTechEffect(s, TechId::IND_ExpSys, ctx); // -0.10 / +0.15
CHECK_NEAR(s.conMod[2], 0.75, 1e-12);
CHECK_NEAR(s.outMod, 1.50, 1e-12);
ApplyTechEffect(s, TechId::IND_OrbDry, ctx); // classes 1 and 2 only
CHECK_NEAR(s.conMod[0], 0.75, 1e-12);
CHECK_NEAR(s.conMod[1], 0.70, 1e-12);
CHECK_NEAR(s.conMod[2], 0.70, 1e-12);
ApplyTechEffect(s, TechId::DRN_AdvRob, ctx);
CHECK_NEAR(s.conMod[0], 0.70, 1e-12);
CHECK_NEAR(s.conMod[1], 0.65, 1e-12);
ApplyTechEffect(s, TechId::IND_OrbFound, ctx);
CHECK_NEAR(s.savMod[0], 0.95, 1e-12);
CHECK_NEAR(s.savMod[2], 0.95, 1e-12);
ApplyTechEffect(s, TechId::IND_GravCon, ctx); // 1.80
ApplyTechEffect(s, TechId::IND_HvyPlat, ctx); // 1.90
CHECK_NEAR(s.outMod, 1.90, 1e-12);
ApplyTechEffect(s, TechId::IND_HrdStrct, ctx); // multiplicative
CHECK_NEAR(s.outMod, 1.71, 1e-12);
CHECK_NEAR(s.defenceDamageMod, 0.25, 1e-12);
// 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-12); // 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.1, 0.0);
CHECK_NEAR(s.miningRate, 1.0, 0.0);
PlayerEconomyState q;
q.maxOverharvest = 0.3;
ApplyTechEffect(q, TechId::IND_MsMine, ctx);
CHECK_NEAR(q.maxOverharvest, 0.3, 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-12);
ApplyTechEffect(s, TechId::BIO_AtmoAd, ctx);
CHECK_NEAR(s.suitTol, 0.82, 1e-12);
CHECK_NEAR(s.popMod, 1.16, 1e-12);
CHECK_NEAR(s.terraMod, 1.35, 1e-12);
ApplyTechEffect(s, TechId::BIO_GrvAdpt, ctx);
CHECK_NEAR(s.suitTol, 2.32, 1e-12);
CHECK_NEAR(s.popMod, 1.26, 1e-12);
CHECK_NEAR(s.terraMod, 1.70, 1e-12);
ApplyTechEffect(s, TechId::BIO_EnvTail, ctx);
CHECK_NEAR(s.popMod, 1.46, 1e-12);
CHECK_NEAR(s.terraMod, 2.15, 1e-12);
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-12);
TechApplyOutcome o = ApplyTechEffect(s, TechId::IND_ArcCon, ctx);
CHECK(s.Flag(PlayerFlag::Arcology));
CHECK_NEAR(s.popMod, 1.61, 1e-12);
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-12);
ApplyTechEffect(s, TechId::CCC_AIAdmin, ctx);
CHECK_NEAR(s.incMod, 1.3, 1e-12);
ApplyTechEffect(s, TechId::CCC_AIFac, ctx);
CHECK_NEAR(s.outMod, 1.4, 1e-12);
SetAiBenefit(s, false, ctx); // rebellion: bonuses withdrawn
CHECK_NEAR(s.resMod, 1.0, 1e-12);
CHECK_NEAR(s.incMod, 1.0, 1e-12);
CHECK_NEAR(s.outMod, 1.0, 1e-12);
SetAiBenefit(s, false, ctx); // idempotent
CHECK_NEAR(s.outMod, 1.0, 1e-12);
SetAiBenefit(s, true, ctx);
CHECK_NEAR(s.resMod, 1.2, 1e-12);
CHECK_NEAR(s.outMod, 1.4, 1e-12);
PlayerEconomyState off; // researched while off: nothing
off.aiBenefit = false;
ApplyTechEffect(off, TechId::CCC_AIFac, ctx);
CHECK_NEAR(off.outMod, 1.0, 1e-12);
TechApplyOutcome o = ApplyTechEffect(off, TechId::CCC_AISlv, ctx); // slave AI: benefit back on
CHECK(off.aiBenefit);
CHECK_NEAR(off.outMod, 1.4, 1e-12);
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; // unknown table values: 0
ApplyTechEffect(none, TechId::CCC_AI, ApplyContext{});
CHECK_NEAR(none.resMod, 1.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));
ApplyTechEffect(s, TechId::XNC_Proliferate_Zuul, ctx);
CHECK_EQ(s.speciesFlags[static_cast<int>(Species::Zuul)], 0x100u);
// 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-12);
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-12);
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-12);
}
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::Unresolved_052).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);
// 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);
}
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();
return simtest::finish("test_effects");
}