172 lines
8.3 KiB
C++
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
|