121 lines
14 KiB
Markdown
121 lines
14 KiB
Markdown
# 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 |
|
||
| `ConstantsOf` | engine-carried species constants: `incomeFactor` (Zuul 1.1, Morrigi 0.8, else 1), `hazardCostFactor` (Zuul 0.7, else 1), `systemBonusEligible` (Zuul false) | high on the three fields; the rest of the engine's species table is not modelled |
|
||
|
||
## 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` | per entry `minC = max(min, 0)`, `maxC = clamp(max, 0, 2e9)` with 0 → 2e9, `req = ftol(fraction x float(availPre)) − minC`, `take = min(max(req, 0), maxC − minC)`; total `Σ minC + min(max(Σ take, 0), availPre − Σ minC)` | high — the request is the slider fraction of the pre-expense available income (single-precision product) minus the mandatory minimum |
|
||
| `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 = net > 0 ? max(0, ftol((techIncomeMult − 1) x net)) : 0` where `net` is the full net so far (all income incl. interest and trade, minus maintenance, research money, construction, expenses, research aid); `savingsGiven = min(max(SatAdd(Sav, net + bonus), 0), max(aid, 0))` — capped by the projected treasury, not the turn net | high on the items, signs and both running-total readers; medium 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 (BnkEl) = max(ftol(maxIncome / −0.15), −2e9)` — the debt whose 15 % interest equals the maximum income; `protectionLimit (BnkPr) = max(−ftol(PROTECTION_LIMIT_FACTOR x maxIncome), BnkEl)`. The limits a turn's check reads are the ones computed at the end of the previous turn (and on load) | high — the 3.3 factor is on the protection limit; both expressions read with their constants |
|
||
| `BankruptcyLevel` | 2 if `Sav < BnkEl`, 1 if `Sav < BnkPr`, else 0 | high |
|
||
| `BankruptcyStep` | state first: any level change (0↔1, 1↔2 alike) stamps `startTurn = level ? turn : −1`; decisions on the *old* state: `costCutting = level != 0 && old.level != 0`, `eliminate = old.level == 2 && turn − old.startTurn >= BANKRUPTCY_ELIMINATION_TURNS` — both actions begin the turn after the level is reached | high — stamp-on-transition and act-on-old-state read from the code |
|
||
| `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 || p >= 1 || rand01() <= p))`; one draw only when 0 < p < 1 in Normal mode | high |
|
||
| `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 just-funded target is **not** excluded and only escapes by completing in the same pass (net gain of the current tech = spend − 5 % of cost) | high — confirmed against the loop |
|
||
| `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 |
|
||
|---|---|---|
|
||
| `SpeciesTechFlags` | per-species xenotech bits in family order: translation 1/2/3 (bits 0–2), incorporate 3, addict 4, temperance 5, subjugate 6, accommodate 7, proliferate 8; `FromBits` unpacks a flag word | high on the order |
|
||
| `HazardModifier` | `clamp01(1 − |suit − ideal| / (SuitTol + 0.1))` — linear, no exponent; SuitTol is the species start value plus the adaptation techs (+0.75 atmospheric, +1.5 gravitational); the caller passes 1 when it holds the accommodate xenotech for the species or is the rebel AI | high — read with its 0.1 constant |
|
||
| `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| / (1.5 x 1.2 / 20000)`; `points x 1.5 x 1.2 x TerraMod x sign / 20000` toward the ideal | high |
|
||
| `SlaveDeathRate` | `(SRs x BYOUTPUT + |ideal − suit| x BYHAZARD + DEATH_RATE) x ((translation1 ? 0.8 : 1) − 0.2 translation2 − 0.2 translation3)` | high |
|
||
| `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 |
|
||
| `SuitabilityCostMod` | `min(|ideal − suit|, SuitTol)`; 0 for the rebel AI; 20 when unowned — the tolerance techs cap the money cost as well as widening the habitable band | high |
|
||
| `SystemMoneyIncome` | `t = (trade − fmod(trade, 5)) x 5` (whole five-point blocks, five money each); `t += imperial + civilian + slave population income`; `t *= speciesIncomeFactor`; `t *= IncMod`; `t *= serverIncomeMod x difficultyIncomeMult`; `cost = speciesCostFactor x suitCostMod x 10000 x 1.5`; `money = ftol(t − cost)` | high on the chain and constants; the three population-income terms are inputs (their group-income tables, and the addiction factor inside them, are not modelled) |
|
||
| `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; `target = eligible ? ftol(max(POPBONUS, 0) x cap) : 0`; `pbon += min(max(ftol(POPBONUS_INC x cap), 0), max(target − pbon, 0))`; `ibon += min(max(INFRABONUS_INC, 0), max((eligible ? INFRABONUS : 0) − ibon, 0))`; Zuul are never eligible. The `*_HOME` keys are only read when a home system's bonus is initialised (not modelled) | high — increment, target and gating read with constants |
|
||
| `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)` — no clamp; only evaluated for chords inside a sphere, so `dist <= radius` by construction | high |
|
||
| `DistPointToSegment` | closest-point distance with the projection parameter clamped to [0, 1] | high |
|
||
| `BuildStutterSegments` | the travel line is intersected with every system's influence sphere; each chord (clipped to the line) becomes a segment, chords shorter than 0.01 are dropped, segments are sorted by start and overlaps are split at the midpoint of the overlap; the speed factor of a segment is `NodeLineSpeed(1, closest approach of the whole chord)` — per segment, not per position | high on chord/clip/drop/sort and per-chord speed; medium on the overlap rule (midpoint split; a chord swallowed by an earlier one is dropped) |
|
||
| `AdvanceAlongNodeLine` | piecewise integration: segment speed inside a sphere, plain `nodeSpeed` in the gaps, never past the line end | medium — written from the per-segment description |
|
||
| `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)
|
||
|
||
None. The five formulas that were low (bankruptcy limits, bankruptcy stamping, hazard
|
||
curve, money tail, system-bonus increment) are pinned; each header carries a one-line
|
||
rationale. Items still at **medium**: `TechCostMultiplier` (which three species techs
|
||
count), `RollLabAccident` (odds-from-boost function), `ApplyImperialGrowth` shrink floor,
|
||
`SplitLeftover`, `TradeRouteGrossIncome` multiplier truncation order, the movement pass
|
||
bucketing, the probabilistic-jump field identities, the stutter overlap rule and
|
||
`AdvanceAlongNodeLine`, and the meaning of the budget's secondary-manager slot.
|
||
|
||
Tech effects on the player's modifiers (`OutMod`, `PopMod`, `TerraMod`, `SuitTol`, ...)
|
||
live in `game/effects` — see `game-effects.md`.
|
||
|
||
Not modelled here (out of scope for pure formulas, or unresolved): base output from
|
||
population, the per-group population income tables (inputs to `SystemMoneyIncome`),
|
||
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, the home-system bonus
|
||
initialisation, and the game-setup handicap multipliers (output / income / research)
|
||
that feed `techIncomeMult` and friends.
|