sots-engine/docs/game-sim.md

107 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 || 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 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| / tolerance)` | **low** — inputs known, curve shape not |
| `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 ((tech0 ? 0.8 : 1) − 0.2 tech1 − 0.2 tech2)` | 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 |
| `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)
1. `ComputeBankruptcyLimits` — protection-limit expression.
2. `BankruptcyStep` — when the bankruptcy start turn is stamped.
3. `HazardModifierShape` — suitability-to-capacity curve.
4. `SystemMoneyIncomeShape` — trade points → money tail.
5. `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.