A House Divided A House DividedDocumentation
Changelog
Engineering/Architecture

The Turn Processor (as shipped)

Last updated 2026-08-20
Source files

Every hour, one function advances the entire game world by one turn: processTurn() in . It acquires a lock on the singleton gameState document, runs a registry of phase adapters covering the economy, parties, legislation, elections, government formation, and telemetry, then advances the game clock and writes a turnLog. This document describes the processor as it actually runs today, grounded in the current code. It supersedes the older design/turn-processing.md where the two disagree.

Time model#

Constants live in : 48 turns = 1 game year (TURNS_PER_YEAR), 1 turn = 1 game week, and MS_PER_TURN maps 1 turn to 1 real hour. STARTING_YEAR is 2019 by default; each world's gameState.startingYear plus preset (one of SEED_PRESET_IDS: 1953, 1979, 1991, 1999, 2007, 2019, 2023 defaults) anchors its calendar. Fast mode halves the interval to 30 minutes (toggleFastMode).

Cron entry#

The entry point is GET /api/cron/turn (), fired hourly. Details that matter operationally:

Other cron routes under src/app/api/cron/ (price-update, stock-exchange-refresh, index-fund, fog-update, patreon-reconcile) are separate jobs, not part of the turn.

Locking, recovery, and failure#

The lock is a set of fields on the gameState _id: "current" document (isProcessing, processingKind, processingStartedAt, processingTargetTurn, processingHeartbeatAt, processingPhase, processingPhaseStatuses). Acquisition is a single findOneAndUpdate that succeeds only if no lock is held or the held lock is stale. Constants come from :

Safety layers around the lock:

On success the processor advances the clock, clears the lock, writes the turnLog (turn, year, durationMs, warnings, phaseStatuses, per-phase phases results), and emits turn_start/turn_complete events.

Phase registry and ordering#

Phases are organized as adapters returned by getTurnPhaseRegistry() in . Each adapter's execute(context, runtime) runs one thematic group; inside a group, individual phases run through runtime.runPhase(name, fn). The canonical name list is BASE_TURN_PHASE_NAMES in : 123 base phases, plus 57 per-country election phases contributed by COUNTRY_ELECTION_PHASES in (that file also registers 16 per-country bill-lifecycle phases, which are in the base list), for roughly 180 named phases per turn.

Groups execute strictly in registry order:

# Group (adapter key) Purpose, representative phases
1 expiredBannedShareholderCleanup, inactiveShareholderShareRelease Release shares held by banned/inactive users and inactive-CEO corps before dividends settle.
2 resourceAndFinanceStart The economy: disaster/crisis spawners, autoSectorSeed, extractionAutoStrategy, actionRefresh, fundGeneration, corporationTurn (the whole sector engine), nppCorporateAttacks, unionsTurn, nppUnionBehavior, decolonization, partyInfluenceTurn, caucusTax, treasuryTurn, nppFundGeneration, savingsInterestTurn, prospectingResolution, macroCountryTurn, sphereSponsorTurn, bondTurn, commodityPrices, contractSettlement, lineOfCreditTurn, recomputeSharePrices, financialSuspectScan.
3 demographicsAndPartySetup Turnout decay and GOTV, partyOrgTurn, Reg/pressure/support decay and accrual, partyTierTurn, state/national/committee party elections, partyActionGeneration, charter expiry and empty-party cleanup, NPP relationship maintenance, governorLegislationQueue, nppBillSponsorship, generateChallengers, nppBehavior (NPP candidacies and votes).
4 billsCampaignsAndActivity billLifecycle plus the 16 country bill lifecycles (UK/JP/IE/DE, CN and the one-party bloc RU/DD/PL/CS/HU/RO/BG/YU/UKR/BLR/BAL), stateBillTimers, cabinetNominations, scotusTurn, ukJrSurpriseTurn, fomcNominations, then sequentially socialAxisDrift, governor's-office phases (officeStateSeed, governorAPRegen, executive orders, address expiry, endorsements), campaignTurn, playerRandomEvents, world-events maintenance/scheduler, nppActionProcessing, activityLogging.
5 electionResolutionAndGovernment Strictly sequential: candidatePartySweep, primaryResolution, voteAccumulation, campaignSpendReset, electionTimers, primarySnapshots, electionResolution, clearResolvedSupport, leadershipVacate, then parliamentary government formation/votes/vacancy watcher and nppGovernmentPhases. Reordering here corrupts elections (dropped final-turn votes, offices resolved from stale tallies).
6 electionCoverageAndSuccession detectPreIterationComplete, withdrawInactiveCandidates, then the 57 per-country ensure*Elections spawner phases plus perpetualElections in parallel (suppressed while the founding phase is active), byElectionWatcher, leadershipElections, staleCandidateCleanup, internationalOrganizations, then alignment, autoReelectionEntry, impeachmentLifecycle, presidentialSuccession.
7 fiscalYearBoundary fiscalYear, only on year-boundary turns (isFiscalYearEnd); marked skipped otherwise.
8 stateEffectsAndNationalAggregation () The largest group: crisisTurn, ministerialOrders, policyEffects, demographicEffects (followed sequentially by the era-checkpoint pull, see the granular-electorate doc), decay phases, regional budgets, metricEngine, demographicFlows, census, eraCrossing, nationalMetrics, economicModel, inflationRecalc, commandEconomy, forexTurn, central-bank/FOMC phases, referendumLifecycle, then the snapshot battery (metricHistory, approvalSnapshot, portfolio/stock-exchange/wealth snapshots), auditAnomalyScan, suspiciousDetection, gameHealthSnapshot.
9 indexFunds, moneySupplySnapshot Index-fund NAV/rebalance cron (flag-gated) and the per-currency money-supply snapshot.
10 ledgerBalanceSnapshot, ledgerReconcile Shadow ledger, registered last so it snapshots after every value-affecting phase. Flag-gated (ledgerShadow, on by default in prod seeds).

Parallelism and its constraints#

Parallelism is used only inside a group, via Promise.all, and only where phases touch disjoint state. Documented constraints in the registry:

Conditional and skipped phases#

Phases can be skipped with a recorded reason (markPhaseSkipped): manualPause (admin paused corporation actions skips corporationTurn and the disaster spawners), featureDisabled (prospecting, contract settlement, index funds, shadow ledger), conditional (fiscal year off-boundary), and simElectionsOnly (headless worldsim profiles via gameConfig.simTurnPhaseMode, inert in production). Bond servicing deliberately still runs while corporation actions are paused: existing coupons and maturities are contracts, not corporation actions.

Telemetry#

createTurnPhaseRuntime () wraps every phase with:

The turndiag MCP tooling reads turnLogs for phase timings, regressions, and lock status; gameHealthSnapshot (written even on crash) is the per-turn health record.