sots-engine/src/game/sim/research.h

172 lines
8.3 KiB
C++

// Research: tech-tree race gating, per-turn progress and completion, cost decay,
// lab-accident rolls.
#pragma once
#include <climits>
#include <vector>
#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<int>(s)] = percent / 100.f; }
float Chance(Species s) const { return chance[static_cast<int>(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<ResearchNode>& 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<ResearchStepResult> 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<ResearchNode>& nodes,
const std::vector<ResearchAllocEntry>& 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