238 lines
8.9 KiB
C++
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");
|
|
}
|