sots-engine/tests/game_sim/test_research.cpp

238 lines
8.9 KiB
C++

#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<double>(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<double>(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<ResearchNode> 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<ResearchNode> 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<int> 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<int>(rng.floatDraws()));
return trace;
};
CHECK(run() == run());
const std::vector<int> 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");
}