10 KiB
game/sim — strategic-layer formula catalog
Module: src/game/sim/. Pure functions over plain structs — no game state, no I/O.
Every RNG-consuming roll takes an IRandom& (rng.h); the engine injects its
MT19937-compatible server generator, tests inject a scripted sequence. Tuning constants
come in through a TuningTable (tuning.h) whose fields are the data-file keys verbatim;
nothing in the module hard-codes a shipped value.
Numeric conventions (numeric.h): Ftol/Ftoi64 truncate toward zero like the original
float-to-int helper; RoundToInt rounds half away from zero; the treasury is a saturating
32-bit int clamped to +/-2,000,000,000.
Build/test: tests/game_sim/build_and_run.sh (plain g++, -Wall -Wextra -Werror), or
-DSOTS_GAME_SIM_TESTS=ON once src/game/sim is added to the root CMake. The real-data
smoke test runs only when SOTS_SAVES_JSON points at a save_reader.py --json dump and
asserts nothing (compare-mode is future work).
Confidence legend — high: formula verified in the RE notes against the code path;
medium: shape and inputs established, one term or the truncation order inferred;
low: shape only, marked CONFIDENCE: low — see sots-re open questions in the header.
Species (species.h)
| item | formula / rule | confidence |
|---|---|---|
| enum order | Human 0, Hiver 1, Tarkas 2, Liir 3, NPC 4, Zuul 5, Morrigi 6 — index of every per-species table |
high |
Economy (economy.h)
| function | formula | confidence |
|---|---|---|
SavingsInterest |
Sav >= 0 && ownsSystems ? ftol(Sav x 0.01) : 0 |
high |
DebtInterest |
Sav < 0 ? ftol(-Sav x 0.15) : 0 |
high |
MaintenanceCost |
Maint / ftol(difficultyDivisor) |
high |
ExpenseTotal |
sum(min) + min(sum(clamp(req - min, 0, max - min)), availBefore - sum(min)) |
medium — the per-entry request term is unresolved |
ResearchPointsFromMoney |
ftol(difficulty x (money/50 x 1.15 x 0.5 x 0.85) x (ResMod + shrm + TRM) x techMult x srv.ResMod x ResScl) ≈ money x 0.009775 x multipliers |
high |
ComputeBudget |
line items: +system income (positive part), +trade, +ship-carried population, +secondary manager, +savings interest, +tech bonus; −negative system income, −maintenance, −research kept, −debt interest, −construction, −expenses, −research aid, −savings aid. avail = max(0, running); construction = min(demand, avail) for humans; researchMoney = max(0, ftol((avail − construction) x ResRate)) (0 when projected); totalRP = max(0, RP + TRA + TRP); aid: given = x pct/100; bonus = ftol((techIncomeMult − 1) x running); savingsGiven = min(max(running, 0), aid) |
high on the items and signs; medium on which running total the bonus and savings aid read and on the meaning of the secondary-manager slot |
TradeRoutesSupported |
max(1, ceil(civ/REQ_CIV) + ceil(imp/REQ_IMP)) |
high |
TradeRouteGrossIncome |
age < STARTUP_TURNS → STARTUP_INCOME; else MIN_INCOME + Σ_class min(n, capLeft) x PERFREIGHTER[class] (CRQ, CR, DE; capLeft from MAX_FREIGHTERS) x (1 + STATION_BONUS_TRADE_INCOME x stations) x ADDICTION_TRADE_MOD if addicted |
high on the sum; medium on truncation order of the multipliers |
TradeRouteIncome |
owner x OWNERS_SHARE (clamped 0..1), partner x (1 − share), x AI difficulty trade multiplier |
high |
ComputeBankruptcyLimits |
eliminationFloor = −ftol(PROTECTION_LIMIT_FACTOR x maxIncome); protectionLimit = max(floor, −maxIncome) |
low — "debt floor ≈ −3.3 x max income" is established; which limit carries the factor and the other limit's exact form are not |
BankruptcyLevel |
2 if Sav < floor, 1 if Sav < protection, else 0 |
high |
BankruptcyStep |
non-zero level differing from the stored one restamps the start turn; eliminate when level 2 and turn − start >= BANKRUPTCY_ELIMINATION_TURNS; level 0 clears |
low — elimination condition established; restamp rule inferred |
SaturatingAdd |
clamp to ±2e9 | high |
Research (research.h)
| function | formula | confidence |
|---|---|---|
EdgeAvailability |
per-species chance parsed as pct/100; unlisted species default 1.0; explicit 0 excludes | high |
RollEdgeAvailable |
include iff mode == Everything or `(p > 0 && (mode == NoRoll |
|
TechCostMultiplier |
max(0.25, 1 − 0.25 x n) |
medium — which three species techs count is unresolved |
TechCost |
INT_MAX stays; else max(1, ftol(base x mult)) |
high |
ApplyResearchPoints |
lo = cost x 50/100, hi = cost x 150/100 (integer); spend = min(points, hi − progress); below hi: odds = (progress − lo)/hi (0 at 50 %, 1/3 at 100 %, 2/3 at 150 %), roll = rand01(), Zuul keep the lower of two rolls, zero spend → odds 0/roll 1; at hi: guaranteed; complete iff odds >= roll; crossing 100 % without completing → over-budget event (flag 2); completing below 80 % → "completed early" (flag 0) |
high |
DecayResearchProgress |
max(0, progress − ftol(cost x 0.05)) |
high |
DecayAllResearch |
applies to every Available node with progress, after the target was processed (the target decays too: net gain = spend − 5 %) | high |
RollLabAccident |
randint(100) < odds |
medium — odds-from-boost function unresolved (caller supplies odds) |
LabAccidentLossPercent |
ceil(clamp01(rand01() x (max − min) + min) x 100) |
high |
Colonies (colony.h)
| function | formula | confidence |
|---|---|---|
HazardModifierShape |
placeholder: `clamp01(1 − | suit − ideal |
CarryingCapacity |
ftoi64(Size x 1e8 x groupMult x speciesFactor x crossSpecies x hazard) + arcology (1e8 imperial / 2e8 civilian); clamp to group max; x INDSYS_IMPERIAL_POPULATION_MOD for NPC owners; NPC species or uninhabitable → 0 |
high |
PopulationGrowthDelta |
g = clamp01((1 − clamp01(pop/cap))^EXP); if g > 0: x MOD x PopMod x hazard/species x groupMult (if > 0); delta = ftoi64(pop x g), min 1 when g > 0, max 50,000,000; blockade → 0 |
high |
ApplyImperialGrowth |
over cap: shrink by min(5e7, pop − cap) but not below min(pop, 100); else min(cap, pop + delta) |
medium — shrink floor read from a terse note |
InfrastructurePointsNeeded / InfrastructureGain |
ceil((1 − infra)/3.3e-5); points x (1/500) x 0.01 x 1.65 = points x 3.3e-5 (≈30,300 points for 0→1) |
high |
DecayUnownedInfrastructure |
max(0, infra − 0.02) |
high |
TerraformPointsNeeded / TerraformDelta |
` | ideal − suit |
SlaveDeathRate |
`(SRs x BYOUTPUT + | ideal − suit |
SlaveDeaths |
clamp(ftoi64(slaves x rate), MIN_DEATHS, MAX_DEATHS), MAX −1 = uncapped, never more than present |
high |
NormaliseOutputRates |
negatives → 0; terraform → 0 at ideal; infra → 0 when full; rescale to Σ 1, equal split when all zero | high (the tiny positive threshold is treated as 0) |
MoraleOutputMultiplier |
>= INCREASE_OUTPUT → x INCREASE_MOD; <= DECREASE_OUTPUT → x DECREASE_MOD |
high |
TotalSystemOutput |
round(base x morale x (1 + STATION_BONUS_IMPERIAL_OUTPUT x stations) x addiction x ScOutMod x RebOutMod x techOut x sys.OutMod x OutMod) |
high on the chain; the base-from-population term is an input (unresolved) |
SplitOutput |
round(total x rate) per channel |
high |
ConstructionPoints |
round(cons x (1 + STATION_BONUS_SHIPCON x stations)) |
high |
SplitLeftover |
unspent construction over trade/terraform/infra by their rates, or 1 / (suit != ideal) / (infra != 1) when construction was the only slider |
medium |
SystemMoneyIncomeShape |
ftol(trade x speciesIncomeFactor x playerIncomeMult − costTerm) |
low — the income tail's FP chain is unresolved |
ApplyPopulationBonus / ApplyInfrastructureBonus |
pop += min(bonus, cap − pop); infra += min(bonus, 1 − infra); bonus reduced by the same |
high |
AccrueSystemBonus |
gated on stable, owned > MINTURNS, no rebellion > MINTURNS; pbon += min(ftoi64(cap x POPBONUS_INC), cap x POPBONUS(_HOME) − pbon); ibon += min(INFRABONUS_INC, INFRABONUS(_HOME) − ibon) |
low — the POPBONUS_INC-derived increment is not fully resolved; caps and gating are |
ProcessBuildQueue |
FIFO: points < conleft → conleft −= points, stop; else complete, points −= conleft, charge money cost, continue |
high |
Movement (movement.h)
| function | formula | confidence |
|---|---|---|
| pass schedule | departing/in-transit sets: two dt = 0.5 passes; everything else one dt = 1.0 pass |
medium — constants established, bucketing semantics not fully |
StraightStep |
speed x dt |
high |
NodeLineSpeed |
speed x ((STUTTER_MAX − STUTTER_MIN) x (dist / INFLUENCE_RADIUS) + STUTTER_MIN), ratio clamped to [0, 1] here |
high on the formula; medium on the clamp (assumed) |
ResolveMoveStep |
range = minShipRange − 0.05; no range at all and range < distance → step 0 (stranded); move = min(step, range, distance) ≥ 0; arrival when move == distance |
high |
ConsumeShipRange |
max(0, range − moved) unless exempt |
high |
RemainingPassTime |
fraction < 0.9999 ? (1 − fraction) x dt : 0 (multi-waypoint recursion) |
high |
RollProbabilisticJump |
roll = rand01() x castEfficiency; roll > castThreshold → stop at fraction roll; else arrive |
medium — identity of the two player fields inferred |
Low-confidence list (flagged in headers)
ComputeBankruptcyLimits— protection-limit expression.BankruptcyStep— when the bankruptcy start turn is stamped.HazardModifierShape— suitability-to-capacity curve.SystemMoneyIncomeShape— trade points → money tail.AccrueSystemBonus— the population-bonus increment.
Not modelled here (out of scope for pure formulas, or unresolved): base output from
population, civilian seeding rules, morale event deltas, rebellion rolls, plague, the
research-boost → accident-odds function, the fleet speed (FPsp2) derivation from
engine ftlspeed/nodespeed, gate traffic capacity.