// Research: tech-tree race gating, per-turn progress and completion, cost decay, // lab-accident rolls. #pragma once #include #include #include "game/sim/rng.h" #include "game/sim/species.h" namespace sots::sim { // Sentinel cost of a node that has no researched parent yet ("no cost known"). constexpr int kNoResearchCost = INT_MAX; // --------------------------------------------------------------------------------------- // Tree creation // --------------------------------------------------------------------------------------- // How the per-species tree is built. Normal rolls the race percentages; NoRoll includes // every edge with a non-zero chance; Everything includes every edge regardless. enum class TreeBuildMode : int { Normal = 0, NoRoll = 1, Everything = 2 }; // Per-species availability on a tech-tree edge as parsed from an `allows` line. // A species not named on the line keeps the default of 1.0 (always available); an // explicit 0 excludes it. CONFIDENCE: high. struct EdgeAvailability { float chance[kSpeciesCount] = {1.f, 1.f, 1.f, 1.f, 1.f, 1.f, 1.f}; // Set one species' chance from a percentage as written in the data file. void SetPercent(Species s, int percent) { chance[static_cast(s)] = percent / 100.f; } float Chance(Species s) const { return chance[static_cast(s)]; } }; // Whether an edge is included in one species' tree. Exactly one RNG draw is consumed // iff the mode is Normal and 0 < chance < 1: // include iff mode == Everything // or (chance > 0 and (mode == NoRoll or chance >= 1 or rand01() <= chance)) // CONFIDENCE: high. bool RollEdgeAvailable(const EdgeAvailability& edge, Species species, TreeBuildMode mode, IRandom& rng); // --------------------------------------------------------------------------------------- // Cost // --------------------------------------------------------------------------------------- // Cost multiplier from the species' research-bonus techs: 1 - 0.25 per applicable tech // owned, floored at 0.25. CONFIDENCE: medium (which three techs apply is unresolved). double TechCostMultiplier(int applicableBonusTechsOwned); // Effective research cost: INT_MAX stays INT_MAX (no cost known); otherwise at least 1. // CONFIDENCE: high. int TechCost(int baseCost, double multiplier); // --------------------------------------------------------------------------------------- // Per-turn progress // --------------------------------------------------------------------------------------- // Node states. 3 is the selected research target: it behaves as Available everywhere except // the decay sweep, whose guard is an equality test against Available, so the funded tech does // not lose 5 % of its cost the turn it is funded. Confirmed on the live game (docs/B3.md). enum class TechState : int { Hidden = 0, ParentResearched = 1, Available = 2, CurrentTarget = 3, Researched = 4, }; // Completion-flag values the game stamps on a node. enum class TechFlag : int { CompletedEarly = 0, Default = 1, OverBudgetNotified = 2 }; struct ResearchNode { TechState state = TechState::Available; int cost = kNoResearchCost; // effective cost in RP (already multiplied) int progress = 0; TechFlag flag = TechFlag::Default; }; struct ResearchStepResult { int spent = 0; // points actually applied to the node int overbudget = 0; // points that could not be applied (cap at 150 % of cost) bool wasCompleteBefore = false; bool completed = false; // research finished this turn bool completedEarly = false; // finished below 80 % of cost bool overbudgetEvent = false; // crossed 100 % without finishing -> notify the owner float odds = 0; // completion odds used (narrowed to float32, as the original) float roll = 0; // roll compared against the odds (likewise float32) }; // The 50 % / 150 % bounds of the spend window, as the original computes them: the // multiply is a plain 32-bit signed multiply (it wraps for a cost near INT_MAX) and the // division by 100 truncates toward zero. `lo` is then floored at 0 and `hi` raised to at // least `lo`. CONFIDENCE: high. int ResearchSpendFloor(int cost); int ResearchSpendCeiling(int cost); // Completion odds for a partially funded tech: (progress - lo) / hi, evaluated in double // and narrowed to a 32-bit float because the original stores it in a float slot before // comparing. 0 at 50 % of cost, 1/3 at 100 %, 2/3 at 150 %. CONFIDENCE: high. float ResearchCompletionOdds(int progress, int lo, int hi); // Apply one turn of research points to the current target: // lo = cost x 50 / 100, hi = cost x 150 / 100 (32-bit, lo >= 0, hi >= lo) // spend = min(points, hi - progress) -- signed min, NOT floored at 0 // progress += spend // progress < hi: spend == 0 -> odds 0, roll 1 (cannot complete) // else odds = ResearchCompletionOdds(...), roll = rand01(); // Zuul roll twice and keep the lower // progress >= hi: odds 1, roll 0 (guaranteed) // completes iff !(odds < roll), compared as float32. Crossing 100 % without completing // raises the over-budget notification; completing below 80 % of cost marks the node // "completed early". // On completion the node's state becomes Researched. This models only what the original // function itself writes to the node -- the unlock cascade that follows (turn/order stamps, // child states, the owner's tech-effect callback) belongs to SetResearched and is not // reproduced here. CONFIDENCE: high. ResearchStepResult ApplyResearchPoints(ResearchNode& node, int points, Species owner, IRandom& rng); // Progress decay on a partially researched tech: lose 5 % of cost per turn, floored at 0. // The 5 % constant is a float literal widened to double (0.05f), which is what the image // holds; using the exact double 0.05 would round differently at the truncation boundary. // CONFIDENCE: high. int DecayResearchProgress(int progress, int cost); // Apply the decay to every node in state Available whose progress is non-zero. This runs after // the allocation pass. The current target is state CurrentTarget, not Available, so it keeps // its whole gain; only idle partially-researched techs decay. CONFIDENCE: high. void DecayAllResearch(std::vector& nodes); // --------------------------------------------------------------------------------------- // The whole per-turn research pass // --------------------------------------------------------------------------------------- // One entry of the allocation the budget builds: which node gets how many research points. // The original stores the tech itself and looks the node up by its id; here the caller has // already resolved the index into `nodes`. struct ResearchAllocEntry { int nodeIndex = -1; int points = 0; }; struct ResearchTurnResult { int overbudget = 0; // total to add to the caller's counter std::vector steps; // one per allocation entry, in order }; // The per-turn pass: apply every allocation entry to its node in order, then decay every // available tech that has progress. `nodes[i].cost` must already hold the node's effective // cost; a slot that does not exist in the tree should be left in state Hidden so the decay // pass skips it. An entry whose index is out of range is skipped without consuming a draw. // CONFIDENCE: high. ResearchTurnResult ProcessResearchTurn(std::vector& nodes, const std::vector& alloc, Species owner, IRandom& rng); // --------------------------------------------------------------------------------------- // Lab accidents // --------------------------------------------------------------------------------------- // Whether a lab accident happens: roll = randint(100), uniform on [0, 100] inclusive; // accident iff roll < odds. // One RNG draw. CONFIDENCE: medium (the odds-from-boost function is unresolved -- the // caller supplies the odds). bool RollLabAccident(int oddsPercent, IRandom& rng); // Progress lost by a non-catastrophic accident, as a whole percentage: // ceil(clamp01(rand01() x (maxLoss - minLoss) + minLoss) x 100) // One RNG draw. CONFIDENCE: high. int LabAccidentLossPercent(double minLoss, double maxLoss, IRandom& rng); } // namespace sots::sim