#include "game/sim/research.h" #include "check.h" using namespace sots::sim; static void test_edge_roll() { EdgeAvailability e; e.SetPercent(Species::Human, 50); e.SetPercent(Species::Zuul, 0); { // roll at exactly the chance is included (<=) simtest::ScriptedRng rng({0.5f}); CHECK(RollEdgeAvailable(e, Species::Human, TreeBuildMode::Normal, rng)); CHECK_EQ(rng.floatDraws(), std::size_t{1}); } { simtest::ScriptedRng rng({0.51f}); CHECK(!RollEdgeAvailable(e, Species::Human, TreeBuildMode::Normal, rng)); } { // explicit 0 excludes without drawing simtest::ScriptedRng rng({0.0f}); CHECK(!RollEdgeAvailable(e, Species::Zuul, TreeBuildMode::Normal, rng)); CHECK_EQ(rng.floatDraws(), std::size_t{0}); } { // unlisted species default to 1.0: included, no draw simtest::ScriptedRng rng({0.99f}); CHECK(RollEdgeAvailable(e, Species::Morrigi, TreeBuildMode::Normal, rng)); CHECK(RollEdgeAvailable(e, Species::Hiver, TreeBuildMode::Normal, rng)); CHECK_EQ(rng.floatDraws(), std::size_t{0}); } { // NoRoll: any non-zero chance is in, zero stays out simtest::ScriptedRng rng; e.SetPercent(Species::Tarkas, 1); CHECK(RollEdgeAvailable(e, Species::Tarkas, TreeBuildMode::NoRoll, rng)); CHECK(!RollEdgeAvailable(e, Species::Zuul, TreeBuildMode::NoRoll, rng)); } { // Everything ignores the chance entirely simtest::ScriptedRng rng; CHECK(RollEdgeAvailable(e, Species::Zuul, TreeBuildMode::Everything, rng)); } CHECK_NEAR(e.Chance(Species::Human), 0.5, 1e-7); CHECK_NEAR(e.Chance(Species::Liir), 1.0, 0.0); } static void test_cost() { CHECK_NEAR(TechCostMultiplier(0), 1.0, 0.0); CHECK_NEAR(TechCostMultiplier(1), 0.75, 0.0); CHECK_NEAR(TechCostMultiplier(3), 0.25, 0.0); CHECK_NEAR(TechCostMultiplier(4), 0.25, 0.0); // floored CHECK_EQ(TechCost(1000, 0.75), 750); CHECK_EQ(TechCost(1, 0.25), 1); // never below 1 CHECK_EQ(TechCost(kNoResearchCost, 0.5), kNoResearchCost); CHECK_EQ(TechCost(999, 1.0), 999); } static ResearchNode node(int cost, int progress = 0) { ResearchNode n; n.cost = cost; n.progress = progress; return n; } static void test_progress_below_half() { // cost 1000: lo 500, hi 1500. 400 points -> odds (400-500)/1500 < 0, roll 0 still fails. ResearchNode n = node(1000); simtest::ScriptedRng rng({0.0f}); ResearchStepResult r = ApplyResearchPoints(n, 400, Species::Human, rng); CHECK_EQ(r.spent, 400); CHECK_EQ(r.overbudget, 0); CHECK(!r.completed); CHECK(!r.overbudgetEvent); CHECK_NEAR(r.odds, -100.0 / 1500.0, 1e-12); CHECK_EQ(n.progress, 400); CHECK(n.state == TechState::Available); CHECK_EQ(rng.floatDraws(), std::size_t{1}); } static void test_progress_at_cost() { // reaching 100 %: odds = 500/1500 = 1/3 { ResearchNode n = node(1000); simtest::ScriptedRng rng({0.3f}); ResearchStepResult r = ApplyResearchPoints(n, 1000, Species::Human, rng); CHECK(r.completed); CHECK(!r.completedEarly); CHECK_NEAR(r.odds, 1.0 / 3.0, 1e-12); CHECK(n.state == TechState::Researched); CHECK(n.flag == TechFlag::Default); } { ResearchNode n = node(1000); simtest::ScriptedRng rng({0.34f}); ResearchStepResult r = ApplyResearchPoints(n, 1000, Species::Human, rng); CHECK(!r.completed); CHECK(r.overbudgetEvent); // crossed 100 % without finishing CHECK(n.flag == TechFlag::OverBudgetNotified); CHECK(n.state == TechState::Available); } } static void test_progress_cap_150() { ResearchNode n = node(1000); simtest::ScriptedRng rng; // no draw expected ResearchStepResult r = ApplyResearchPoints(n, 2000, Species::Human, rng); CHECK_EQ(r.spent, 1500); CHECK_EQ(r.overbudget, 500); CHECK_NEAR(r.odds, 1.0, 0.0); CHECK(r.completed); CHECK(!r.completedEarly); CHECK_EQ(rng.floatDraws(), std::size_t{0}); CHECK_EQ(n.progress, 1500); // exactly 150 % also completes without a roll ResearchNode m = node(1000, 1400); ResearchStepResult r2 = ApplyResearchPoints(m, 100, Species::Human, rng); CHECK(r2.completed); CHECK_EQ(r2.overbudget, 0); } static void test_completed_early() { // progress 700 of 1000: odds 200/1500 = 0.1333; roll 0.1 completes, below 80 % -> early ResearchNode n = node(1000, 300); simtest::ScriptedRng rng({0.1f}); ResearchStepResult r = ApplyResearchPoints(n, 400, Species::Human, rng); CHECK(r.completed); CHECK(r.completedEarly); CHECK(n.flag == TechFlag::CompletedEarly); // wasCompleteBefore/nowComplete both false: no over-budget event CHECK(!r.overbudgetEvent); } static void test_zero_spend() { ResearchNode n = node(1000, 600); simtest::ScriptedRng rng; ResearchStepResult r = ApplyResearchPoints(n, 0, Species::Human, rng); CHECK_EQ(r.spent, 0); CHECK(!r.completed); CHECK_NEAR(r.odds, 0.0, 0.0); CHECK_NEAR(static_cast(r.roll), 1.0, 0.0); CHECK_EQ(rng.floatDraws(), std::size_t{0}); } static void test_zuul_double_roll() { { // Zuul: 0.9 then 0.1 -> keeps 0.1 -> completes at 100 % ResearchNode n = node(1000); simtest::ScriptedRng rng({0.9f, 0.1f}); ResearchStepResult r = ApplyResearchPoints(n, 1000, Species::Zuul, rng); CHECK(r.completed); CHECK_NEAR(static_cast(r.roll), 0.1, 1e-7); CHECK_EQ(rng.floatDraws(), std::size_t{2}); } { // same script for a Human: one draw of 0.9 -> fails ResearchNode n = node(1000); simtest::ScriptedRng rng({0.9f, 0.1f}); ResearchStepResult r = ApplyResearchPoints(n, 1000, Species::Human, rng); CHECK(!r.completed); CHECK_EQ(rng.floatDraws(), std::size_t{1}); } } static void test_decay() { CHECK_EQ(DecayResearchProgress(300, 1000), 250); CHECK_EQ(DecayResearchProgress(20, 1000), 0); // floored CHECK_EQ(DecayResearchProgress(10, 30), 9); // ftol(1.5) = 1 CHECK_EQ(DecayResearchProgress(0, 1000), 0); CHECK_EQ(DecayResearchProgress(50, kNoResearchCost), 50); std::vector nodes = {node(1000, 300), node(1000, 0), node(1000, 500)}; nodes[2].state = TechState::Researched; ResearchNode hidden = node(1000, 100); hidden.state = TechState::ParentResearched; nodes.push_back(hidden); DecayAllResearch(nodes); CHECK_EQ(nodes[0].progress, 250); CHECK_EQ(nodes[1].progress, 0); CHECK_EQ(nodes[2].progress, 500); // researched: untouched CHECK_EQ(nodes[3].progress, 100); // not available: untouched } static void test_lab_accident() { { simtest::ScriptedRng rng({}, {5, 10, 99}); CHECK(RollLabAccident(10, rng)); // 5 < 10 CHECK(!RollLabAccident(10, rng)); // 10 is not < 10 CHECK(!RollLabAccident(0, rng)); CHECK_EQ(rng.intDraws(), std::size_t{3}); } { // dyadic inputs so the ceil is not on a floating-point tie simtest::ScriptedRng rng({0.5f, 0.999f, 0.0f}); CHECK_EQ(LabAccidentLossPercent(0.125, 0.625, rng), 38); // 0.125 + 0.5 x 0.5 = 0.375 -> ceil(37.5) CHECK_EQ(LabAccidentLossPercent(0.125, 0.625, rng), 63); // 0.6245 -> ceil(62.45) CHECK_EQ(LabAccidentLossPercent(0.125, 0.625, rng), 13); // 0.125 -> ceil(12.5) CHECK_EQ(rng.floatDraws(), std::size_t{3}); } { simtest::ScriptedRng rng({0.5f}); CHECK_EQ(LabAccidentLossPercent(0.9, 1.5, rng), 100); // clamped to 1 } } static void test_determinism() { auto run = [] { std::vector nodes = {node(800), node(1200, 300)}; // Zuul: two draws per turn; the current target decays 40/turn too (cost 800). // Turn 1: 350, odds < 0. Turn 2: 310+350 = 660, odds 260/1200 vs min(0.8,0.5). // Turn 3: 620+350 = 970, odds 570/1200 = 0.475 vs min(0.3,0.9) -> completes. simtest::ScriptedRng rng({0.42f, 0.17f, 0.8f, 0.5f, 0.3f, 0.9f}); std::vector trace; for (int turn = 0; turn < 3; ++turn) { ResearchStepResult r = ApplyResearchPoints(nodes[0], 350, Species::Zuul, rng); trace.push_back(nodes[0].progress); trace.push_back(r.completed ? 1 : 0); DecayAllResearch(nodes); trace.push_back(nodes[1].progress); } trace.push_back(static_cast(rng.floatDraws())); return trace; }; CHECK(run() == run()); const std::vector expected = {350, 0, 240, 660, 0, 180, 970, 1, 120, 6}; CHECK(run() == expected); } int main() { test_edge_roll(); test_cost(); test_progress_below_half(); test_progress_at_cost(); test_progress_cap_150(); test_completed_early(); test_zero_spend(); test_zuul_double_roll(); test_decay(); test_lab_accident(); test_determinism(); return simtest::finish("test_research"); }