sots-engine/docs/game-sim.md

14 KiB
Raw Blame History

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
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
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
SuitabilityCostMod `min( ideal − suit
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.