Skip to content
KIRAZINAPublic

About

High-performance Nation-State Simulator in C# (.NET 10). Features a zero-allocation hot path, deterministic PRNG, hybrid DDD + Arch ECS architecture, regional graph economy, interactive Spectre.Console REPL, and a headless WebSocket/REST server.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

1 Commit

Folders and files

Repository files navigation

NationSim

A deterministic nation simulation built as a hybrid modular monolith that combines DDD (aggregates, invariants, domain events) with DOD/ECS (micro-agents, systems, spatial grid) on top of Clean Architecture layering.

The simulation is a long running socio-economic model: a population of micro-agents lives on a spatial grid, produces wealth, ages, drifts in well-being, and is governed by a parliament, an executive ministry and a set of laws. A macroeconomic engine turns that activity into GDP, inflation, unemployment and treasury income, and the institutions can react by passing bills that change the policy parameters of the economy.

The simulation can be driven from an interactive console, replayed exactly from a recorded session, or served to an external front end over a WebSocket and a small HTTP API.

Documentation

Document Content
Architecture The layers, the tick pipeline, the three invariants, the emergent economy, the do-not list
API The WebSocket stream and the HTTP REST contract for an external front end
CLI The complete command line reference with examples
Development How to build, test, benchmark and extend the simulation
Release audit The verification report of the release

Solution layout

NationSim.sln
├── src/
│   ├── NationSim.Core            Simulation core: tick lifecycle, zero-allocation EventBus,
│   │                             deterministic PRNG (Xoshiro256**), macroeconomic math,
│   │                             global modifier registry, command queue, metrics ring buffer,
│   │                             replay model, hashing
│   ├── NationSim.Domain          DDD aggregates: Parliament, Treasury, ExecutiveMinistry, Law,
│   │                             MacroPolicy, NationalAccounts, executive ministries, bills, factions
│   ├── NationSim.Simulation      ECS systems (Arch), micro-agents, firms, regional graph,
│   │                             parallel agent engine, emergent aggregation, state capture/restore
│   ├── NationSim.Infrastructure  Binary serialization (MemoryPack), snapshot and replay file
│   │                             I/O, telemetry adapters, channel command queue, engine host
│   ├── NationSim.Server          Headless server: WebSocket telemetry stream and HTTP API
│   └── NationSim.UI.Console      Interactive CLI (Spectre.Console)
└── tests/
    └── NationSim.Core.Tests      xUnit tests for determinism, macro economics, emergent economy,
                                  regional distribution, telemetry, replay, server and time travel,
                                  plus BenchmarkDotNet

Dependency direction

NationSim.UI.Console
        │
        ├──▶ NationSim.Infrastructure ──▶ NationSim.Simulation ──▶ NationSim.Domain ──▶ NationSim.Core
        │                                        │                       │
        └────────────────────────────────────────┴───────────────────────┴──▶ NationSim.Core

Dependencies point inwards only. NationSim.Core has no project references at all.

Project References Responsibility
NationSim.Core – Tick lifecycle, event bus, PRNG, macro math, commands, metrics, replay model
NationSim.Domain Core Aggregates, invariants, domain events, bills, factions, fiscal models
NationSim.Simulation Core, Domain ECS world, systems, micro-agents, regional graph, agent engine, state capture/restore
NationSim.Infrastructure Core, Domain, Simulation MemoryPack snapshots and sessions, file I/O, telemetry, channel queue, engine host
NationSim.Server Infrastructure The headless server: WebSocket telemetry stream and HTTP API
NationSim.UI.Console Core, Domain, Simulation, Infrastructure, Server CLI, dashboard, replay and server commands
NationSim.Core.Tests Core, Domain, Simulation, Infrastructure, Server Unit tests, integration tests, benchmarks

Architectural rules

These rules are enforced by the code, the test suite and the build configuration (TreatWarningsAsErrors is on: the solution must compile with zero warnings).

1. Zero-allocation hot path

No heap allocation is allowed while a tick is executed. The rules that make this hold:

  • The EventBus resolves channels through a type keyed cache and stores subscribers in a pre-sized array. EventChannel<T>.Publish passes payloads by in reference, so nothing is boxed and nothing is allocated. Every macro payload (FiscalFlow, MacroState, PolicyEnactedEvent, GlobalModifier, MeasuredEconomyEvent) is a value type.
  • The GlobalModifierRegistry owns one pre-allocated slot per modifier kind, so a ministry never allocates when it publishes its modifiers.
  • The regional layer is backed by pre-allocated arrays: RegionGraph holds the RegionNode[], a flat bool[] adjacency matrix and the pre-computed neighbour lists. The update loops walk the nodes by Span<RegionNode> and the neighbour lists by ReadOnlySpan<int>; the population aggregation writes into a pre-allocated int[].
  • The micro-agents work on a structure of arrays: MicroAgentStore mirrors the components of the ECS into pre-allocated arrays, the agent systems run on those arrays and the result is written back once per tick. The regional buckets are rebuilt with a counting sort, so no bucket ever grows.
  • The parallel execution uses RegionScheduler, a pool of worker threads that is created once. A tick only signals the workers and waits for them - unlike Parallel.For, whose task machinery allocates about ten kilobytes on every call.
  • ECS query descriptions are built once (NationSim.Simulation.Querying.QueryDescriptions) and query delegates are cached in fields instead of being re-created as closures per tick.
  • Aggregates avoid LINQ in members that are read from the tick loop, because Enumerable allocates a boxed enumerator (ExecutiveMinistry.Approval, Parliament.LawsInForce).
  • The macroeconomic engine is pure decimal arithmetic over value types. It never allocates, never uses the clock and never uses randomness.
  • Fiscal reports are written into a pre-allocated ring buffer inside NationalAccounts instead of being collected into a growing list.
  • Structural changes (spawning agents, creating a law) are cold path operations. The tick loop reports the need for them as events instead of performing them inline. For the same reason a death is a state flag (Citizen.IsDead) and not a removal: destroying an entity reorders the internal chunk layout of the ECS and would change the order in which the random source is consumed.
  • GC.GetAllocatedBytesForCurrentThread based tests assert that a tick, the event bus and the state hash allocate exactly zero bytes, the benchmark memory diagnoser confirms it end to end, and a diagnostic measures the process wide allocation of a tick with GC.GetTotalAllocatedBytes to catch the allocations of the worker threads.

2. Deterministic PRNG

  • DateTime.Now, DateTimeOffset.Now and System.Random are banned. There is no ambient clock: SimulationClock derives simulated time from the tick index and a fixed step. One tick equals one simulated day.
  • All randomness comes from Xoshiro256StarStar (NationSim.Core.Primitives), seeded explicitly. The generator state is 256 bits and can be captured and restored (RandomState), which is what makes replays and branching timelines possible.
  • Every region owns a private random stream. A worker of the parallel execution never shares a random source with another worker, and the regions are processed independently, so the parallel run is bit for bit identical to a serial one.
  • The random source is split into independent streams: the population layout uses a dedicated spawn stream so that changing the number of agents does not shift the stream consumed by the systems.
  • The state hash (DeterministicHash, FNV-1a) folds the complete state - the random states, the treasury, the policy, the macro state, the laws, the agents, the firms and the regions - so two runs of the same seed with the same commands produce byte identical fingerprints.
  • InvariantGlobalization is enabled so that formatting and parsing cannot depend on the host locale.

3. LIFO chunk stability: the IsDead flag

Arch stores the components of a chunk in LIFO order: a query returns the entities in the reverse order of their creation. Two rules follow from it, and both are load bearing.

  • An entity is never destroyed inside the tick. Destroying one moves the last element of the chunk into the hole and permutes the iteration order, so the random draws of the agent systems would land on different citizens and the run would no longer be reproducible. A death is therefore a state flag: Citizen.IsDead. The entity stays where it is, the systems skip it, and SimulationWorld has no Despawn method at all.
  • The working set never uses the iteration order as an identity. The slot of a citizen in MicroAgentStore is its agent identifier and the slot of a firm is its firm identifier. When a snapshot is restored, the entities are re-created in the order of the snapshot, the LIFO reverses that order again, and the working set ends up exactly as it was. This is what makes a restored run hash identical to the original - the time travel test failed until the rule was introduced.

The full story, including the failure that motivated it, is in the architecture reference.

4. DDD vs ECS boundary

Aggregates must not reference ECS entities, and the ministries must not reference the spatial regions.

  • NationSim.Domain does not reference NationSim.Simulation and knows nothing about Arch.Core.Entity, RegionNode or the micro-agent components. It works exclusively with plain value objects (Faction, MinistryOffice, BudgetLine, Law, PolicyBill).
  • The micro-agent components never reference an aggregate. Citizen, Firm and Demographics are plain value types that only carry the state of an agent.
  • NationSim.Simulation is the only layer allowed to translate between the two worlds. ECSAggregationSystem aggregates the micro-agents and pushes the result into the RegionNode and the NationalAccounts; the macro engine and the treasury consume those bottom-up metrics.
  • The executive ministries are pure domain aggregates: they consume a budget, react to the laws they enforce and publish GlobalModifier structs. They never touch a region or a micro-agent - the spatial and ECS systems read the published modifiers and apply the effects. A reflection test enforces this rule.
  • The reverse direction — reading domain state into the ECS world — happens exclusively in SimulationKernel and the systems, again through plain data.

The micro-agent engine

NationSim.Simulation owns the population and the firms of the nation. The components are lightweight value types that never reference an aggregate.

Component Fields
Citizen int Age, float Wealth, byte Happiness, ushort RegionId, bool IsEmployed, bool IsDead
Firm int FirmId, ushort RegionId, float Capital, int EmployeeCapacity, int CurrentEmployees, float WageRate, float ProductionOutput
Demographics byte HealthStatus, byte SkillLevel

The agent systems

System Phase Responsibility
AgentAgingAndMortalitySystem Tick Ages the citizens once a year, drifts the happiness, applies the mortality rate of the health ministry
EmploymentSystem Tick Matches the unemployed citizens with the firms of their region; dismisses the retired and the surplus
ProductionAndWageSystem Tick Lets the firms produce from capital and labour, pay the wages, accumulate the capital and pay the corporate tax
ConsumptionAndTaxSystem Tick Lets the citizens consume their wealth, receive the public services and pay the income tax
ECSAggregationSystem Post-tick Sums the micro-agents per region and feeds the RegionNode and the NationalAccounts

The systems run on the MicroAgentStore, a structure of arrays that mirrors the ECS. The regions are partitioned over the worker threads of the RegionScheduler; every worker owns the private random stream of its region, so the parallel execution is deterministic.

The emergent economy

The macroeconomic engine no longer invents the output of the nation: it adopts the values that were measured on the micro-agents.

citizens consume  ──▶  firms produce  ──▶  wages and profits  ──▶  the citizens earn
citizens pay tax  ──▶  the treasury fills  ──▶  the public services return to the citizens
firms hire        ──▶  the employment rate  ──▶  the unemployment rate
  • The output of a region is the value its firms produced and its citizens earned.
  • The unemployment rate is the share of the labour force that is not employed.
  • The tax revenue is the sum of the income tax the citizens paid and the corporate tax the firms paid.
  • The engine keeps what only the model can provide: the inflation (a Phillips curve driven by the fiscal stance and the labour market), the government debt and the price stability.
  • A higher tax rate therefore lowers the wealth and the demand of the citizens, which changes the hiring of the firms - the behaviour emerges from the agents, it is not scripted.

The regional layer

NationSim.Simulation.Spatial holds the spatial state of the nation.

Type Purpose
RegionNode The state of an administrative region: population, local GDP, industrial and agricultural capacity, infrastructure level, unemployment rate, local tax collector, trade volume
LocalTaxCollector The collection capacity, the collected tax and the outstanding tax of a region
RegionGraph The regions, the rectangular topology and the transport adjacency

The default topology is a 4x4 grid of 16 administrative regions, each with its own private deterministic stream for the initial capacities. The adjacency matrix is a flat bool[] and the neighbour lists are pre-computed, so the topology is fixed and the update order is always the ascending RegionId.

The spatial systems

System Phase Responsibility
RegionalPopulationSystem Post-tick Aggregates the micro-agents per region into a pre-allocated counter array
RegionalInfrastructureSystem Execution Improves the infrastructure level with the investment authorised by the ministry
RegionalTaxSystem Execution Collects the tax that is due on the output of the last period
RegionalTradeSystem Execution Computes the trade volume between the connected regions
RegionalReconciliationSystem Post-tick Distributes the national GDP over the regions and sets the local unemployment rates

The reconciliation is exact: the national gross domestic product is distributed according to the economic weight of the regions and the rounding residual is booked on the last region, so the regional output adds up to the national figure to the last decimal.

The executive branch

Ministry Consumes Publishes
MinistryOfHealth Its annual budget HealthQuality and MortalityRate - the health quality lowers the mortality rate of the population
MinistryOfInfrastructure Its annual budget InfrastructureInvestment and TradeEfficiency - the investment improves the regions over time
MinistryOfFinance Its annual budget and the enforcement intensity of the tax laws TaxCollectionEfficiency - the efficacy of the regional tax collection

The ministries are domain aggregates. They are driven by MinistrySystem in the pre-tick phase, which publishes their modifiers into the pre-allocated GlobalModifierRegistry. The ECS and spatial systems read the registry during the execution phase, so the ministries always publish before their effects are enforced.

The legal enforcement pipeline

  1. Parliament passes a TaxLaw or an InfrastructureLaw and publishes the immutable LawPassedEvent (kind, target value and enforcement intensity).
  2. The MinistryRegistry routes the law to the ministry that has to enforce it. The ministry registers the law and sets its enforcement target.
  3. The spatial systems read the published modifiers during the execution phase and update the region states deterministically, in ascending RegionId order.

The tick pipeline

Pre-tick     MinistrySystem          -> publishes the global modifiers
             MicroEconomySetupSystem -> refreshes the micro economic parameters
Execution    HealthSystem            -> latches the mortality rate
             RegionalInfrastructure  -> improves the regions
             RegionalTaxSystem       -> aligns the tax collectors with the infrastructure
             RegionalTradeSystem     -> computes the trade volumes
Tick         MovementSystem          -> agents move inside the territory
             MicroAgentSyncSystem    -> mirrors the agents into the working set
             AgentAgingAndMortality  -> ages the citizens, applies the mortality
             EmploymentSystem        -> matches the citizens with the firms
             ProductionAndWage       -> the firms produce and pay the wages
             ConsumptionAndTax       -> the citizens consume and pay the tax
Post-tick    MicroAgentCommitSystem  -> writes the agents back into the ECS
             ECSAggregationSystem    -> aggregates the agents into the regions and the accounts
             MacroeconomicSystem     -> GDP, inflation, unemployment, fiscal flows
             RegionalReconciliation  -> national GDP distributed over the regions
             DomainProjection        -> treasury, government approval, policy pressure
             MetricsSystem           -> pushes the telemetry frame into the ring buffer

The input phase comes first: the commands of the operators and of the remote clients are applied before the ministries publish their modifiers, which is what makes a recorded session replayable.


The macroeconomic engine

NationSim.Core.MacroEconomy holds the pure mathematics, NationSim.Domain.Economics holds the aggregates that own the resulting state.

Type Purpose
MacroState GDP, inflation, unemployment, government debt, profits, treasury income, savings
MacroParameters Income tax rate, corporate tax rate, policy rate, public spending, subsidy rate
MacroEngine.Step One deterministic differential update of the macro state (Okun's law, Phillips curve, fiscal flows)
FiscalFlow The daily tax revenue, spending, interest and net balance of a tick
MacroInputs The population inputs: labour force and total wealth
FiscalReport The monthly (30 tick) aggregate of the economy
MacroPolicy (Domain) The aggregate that owns the institutional parameters and emits PolicyEnactedEvent
NationalAccounts (Domain) The aggregate that owns the macro state and the fiscal report history

The pipeline of a tick

MovementSystem        → agents move inside the territory
DemographicsSystem    → agents age, happiness drifts (seeded PRNG)
EconomySystem         → population, labour force, wealth and happiness aggregate
MacroeconomicSystem   → MacroEngine.Step: GDP, inflation, unemployment, tax revenue, spending, interest
DomainProjectionSystem → treasury booking, government approval, policy pressure events

The fiscal report is closed every 30 ticks (one simulated month) and published as FiscalReportPublishedEvent; the report history lives in a fixed size ring buffer, so producing a report never allocates.

Policy pipeline

Predefined, parameterized bills live in NationSim.Domain.Policies:

Bill Effect
IncomeTaxBill(TargetRate) Sets the income tax rate
CorporateTaxBill(TargetRate) Sets the corporate tax rate
PublicInfrastructureSpendBill(BudgetAmount) Sets the annual spending budget
CentralBankRateBill(TargetInterestRate) Sets the central bank policy rate
SubsidyBill(TargetSubsidyRate) Sets the subsidy share of public spending
TaxLaw(TargetRate, EnforcementIntensity) Sets the income tax rate and how strictly it is enforced
InfrastructureLaw(BudgetAmount) Sets the annual infrastructure budget the ministries execute

A bill travels through the institutions:

  1. Parliament.ProposeBill(bill) creates a Law carrying the bill.
  2. Parliament.VoteBill(law, indicators, tick) asks every faction to score the bill against the current macro indicators. The score normalises all deltas to percentage points, so a tax increase, a spending cut and a rate hike are directly comparable. A faction supports the bill when its score is positive.
  3. If the supporting seats reach the majority threshold, the bill is applied to the MacroPolicy aggregate, which publishes an immutable PolicyEnactedEvent.
  4. The MacroeconomicSystem picks the new parameters up on the next tick.

Because the votes are a pure function of the macro state, the same bill submitted at the same economic situation always produces the same outcome. Example: a tax increase is rejected by every faction in a healthy economy, but passes once unemployment puts pressure on the budget.

Time travel

SimulationKernel.CaptureState() produces a complete, serializable SimulationState: the random state, every micro-agent, the parliament and its laws, the treasury, the government, the macro policy, the macro state, the fiscal report history, the state of every administrative region and the state of every ministry. SimulationKernel.RestoreState(state) puts it back verbatim.

NationSim.Infrastructure turns that state into a binary MemoryPack payload and back. The result is a snapshot that can be written to disk and resumed later, and the resumed run is bit for bit identical to the uninterrupted one - this is what DeterministicTimeTravelTests verifies.


Telemetry, replay and the headless server

The metrics ring buffer

MetricsStore (NationSim.Core.Metrics) is a fixed size ring buffer of TelemetryFrame values. The MetricsSystem pushes one frame at the end of every tick with the tick, the simulated timestamp, the GDP, the inflation, the unemployment, the treasury balance, the population, the number of active firms and the average wealth of a citizen.

  • The buffer is allocated once, so pushing a frame never allocates.
  • The history never grows: the oldest frame is overwritten when the store is full.
  • CopyLatest(Span<TelemetryFrame>) copies a window of the history into a span the caller owns, which is what a chart control needs - without a single allocation.
  • The timestamp is the simulated time, never the wall clock, so the telemetry stays reproducible.

The session recorder and the replay

A recorded session stores the inputs of a run, never its state:

Element Content
Seed The seed of the random source
Settings The initial configuration of the run
Commands The TickCommandRecord list: a command and the tick it was issued at
TotalTicks / ExpectedHash The length of the run and the hash it ended on

ReplayPlayer rebuilds the engine from the seed and the configuration, steps it forward and injects the recorded commands at the very tick they were issued at - the input phase of the tick applies them, so a bill that was passed before tick 300 also takes effect before tick 300 in the replay. The replay therefore reproduces the recorded run bit for bit, and a session file stays tiny compared to a sequence of snapshots.

The sessions are stored with MemoryPack in a .replay file.

The headless server

NationSim.Server runs the simulation as a background engine and exposes it to external clients (an Avalonia, React or Unity front end):

Method Route Purpose Answer
GET /ws/state The WebSocket stream of the telemetry frames (newline delimited JSON) A stream of frames
POST /api/command Queues a command: {"kind":"bill","target":"income-tax","value":0.3} {"accepted":true,"tick":335,"message":"queued"}
GET /api/regions/{id} The details of a region: population, employment, GDP, infrastructure, tax, neighbours 200 with the region, 404 when it is unknown
GET /api/state The current indicators of the nation The tick, the GDP, the inflation, the unemployment, the treasury, the population
GET /health The liveness probe and the current tick {"status":"ok","tick":1284}

The commands are bill (with the target income-tax, corp-tax, spending, rate or subsidy), budget (with the target health, infrastructure or finance), pause and resume. They are applied in the input phase of the next tick, exactly like the commands of the console application, which is what makes a session that is driven from a front end replayable.

dotnet run --project src/NationSim.Server -- --port 5000 --seed 42 --agents 2000

curl http://localhost:5000/api/state
curl http://localhost:5000/api/regions/3
curl -X POST http://localhost:5000/api/command -H "Content-Type: application/json" \
     -d '{"kind":"bill","target":"income-tax","value":0.3}'

The full contract, the payload schemas and a browser client are in the API reference.

The simulation loop stays completely decoupled from the transport:

client ──HTTP/WS──▶ NationSim.Server ──channel──▶ SimulationEngineHost ──▶ SimulationKernel
                                            ◀──channel── TelemetryFrame ◀──┘
  • The commands travel through a bounded System.Threading.Channels queue that the tick loop drains in its input phase; the core knows nothing about sockets.
  • The telemetry travels through a bounded channel of TelemetryFrame values; a slow client drops the oldest frame instead of blocking the engine.
  • SimulationEngineHost owns the kernel and the queues; it is the only piece that knows both the engine and the transport.

Core building blocks

Type Purpose
Xoshiro256StarStar Deterministic 256-bit state PRNG, capture/restore capable
EventBus / EventChannel<T> Zero-allocation typed publish/subscribe
TickLifecycleManager Runs the Input → PreTick → Execution → Tick → PostTick phases in registration order
SimulationClock Tick based simulated time, no ambient wall clock
DeterministicHash Stable, order sensitive state fingerprint
AllocationProbe Measures managed allocations of an operation
MacroEngine The deterministic macroeconomic model
GlobalModifier / GlobalModifierRegistry The immutable modifiers of the ministries and their pre-allocated registry
ICommandQueue / RingCommandQueue The queue of the commands that the input phase consumes
MetricsStore / TelemetryFrame The pre-allocated ring buffer of the telemetry frames
ReplaySession / ReplayRecorder / TickCommandRecord The recorded inputs of a session
ReplayPlayer / ReplayFileStore The deterministic replay of a session and its binary format
SimulationEngineHost The background engine of the headless server
Parliament / Treasury / ExecutiveMinistry / Law DDD aggregates with invariants
MacroPolicy / NationalAccounts Fiscal policy and macro state aggregates
Ministry / MinistryOfHealth / MinistryOfInfrastructure / MinistryOfFinance The executive ministries and their modifiers
Faction / FactionEconomicProfile Parliamentary blocs and their economic alignment
PolicyBill and the seven bill types Parameterized legislative proposals
SimulationWorld Arch ECS world with deterministic entity ordering
MovementSystem / AgentAgingAndMortalitySystem / EmploymentSystem / ProductionAndWageSystem / ConsumptionAndTaxSystem / ECSAggregationSystem The micro-agent and spatial systems
MicroAgentStore The structure of arrays the agent systems work on
RegionScheduler The persistent worker pool that partitions the regions
MicroEconomyParameters The parameters the agent systems work with
MinistrySystem / HealthSystem The ministry publisher and the health latch
RegionGraph / RegionNode / LocalTaxCollector The regional graph, its nodes and the local tax collectors
RegionalPopulationSystem / RegionalInfrastructureSystem / RegionalTaxSystem / RegionalTradeSystem / RegionalReconciliationSystem The spatial systems
SnapshotFileStore / SnapshotMapper MemoryPack binary snapshots with atomic writes
ITelemetrySink Telemetry abstraction with in-memory and composite adapters

Building and running

# restore, build and test the whole solution
dotnet restore
dotnet build
dotnet test

# run the console UI in non-interactive mode
dotnet run --project src/NationSim.UI.Console -- --seed 42 --agents 2000 --ticks 365 --once

# start the interactive console
dotnet run --project src/NationSim.UI.Console

CLI options

Option Description
--seed <ulong> Seed of the deterministic random source (default 20260101)
--agents <int> Number of micro-agents (default 1000)
--grid <int> Width and height of the world in cells (default 32)
--regions <int> Columns and rows of the administrative region grid (default 4, so 16 regions)
--ticks <long> Ticks to run before the interactive loop starts
--snapshots <path> Directory snapshots are written to (default snapshots)
--once Run the requested ticks and exit
replay record <file.replay> Record the commands of the session into a file
replay play <file.replay> Replay a recorded session and verify its hash
server start --port 5000 Start the headless server for external clients
-h, --help Show the usage text

Interactive commands

Command Description
run [ticks] / step [ticks] Advance the simulation
status Render the dashboard (tick, treasury, GDP, inflation, unemployment, laws, hash)
auto [on|off] [batch] Continuous runner: p pause, s step, d dashboard, +/- speed, q quit
pause / resume Pause or resume the simulation
autosave [on|off] [every] Write a snapshot every every ticks
bill <type> <value> Submit a bill: income-tax, corp-tax, spending, rate, subsidy
region list Show the breakdown of every administrative region
region inspect <id> Show the detailed state of a single region and its neighbours
agents count Show the census: active, deceased, employed and unemployed citizens, firms
agents sample <id> Show a sample of the citizens and the firms of a region
replay record <file> Start recording the commands of the session into a .replay file
replay play <file> Replay a recorded session and verify its state hash
replay stop Close the recording and write the session file
ministry list Show the ministries, their budgets and the active global modifiers
ministry budget <type> <amount> Fund health, infrastructure or finance
policy Show the macro policy and the parliamentary factions
reports Show the latest fiscal reports
laws List the laws known to the parliament
relief Enact the emergency relief act
telemetry Show the recorded telemetry
save [name] / load <file> Write or inspect a snapshot
help / quit Command list / leave the application

The REPL falls back to standard input when no terminal is attached, so the application can be scripted: echo "bill income-tax 0.30" | dotnet run --project src/NationSim.UI.Console.

The complete reference, with the examples of every command, is in the CLI documentation.

The headless server

# start the server for an external front end
dotnet run --project src/NationSim.Server -- --port 5000 --seed 42 --agents 2000

# the same from the console application
dotnet run --project src/NationSim.UI.Console -- server start --port 5000
# the state of the nation
curl http://localhost:5000/api/state

# a region
curl http://localhost:5000/api/regions/3

# a command
curl -X POST http://localhost:5000/api/command -H "Content-Type: application/json" \
     -d '{"kind":"bill","target":"income-tax","value":0.3}'

The replay

# record a session
dotnet run --project src/NationSim.UI.Console -- --seed 42 replay record session.replay

# replay it in another process and verify the hash
dotnet run --project src/NationSim.UI.Console -- replay play session.replay
# Recorded hash : 0x728C4D232ED79DE4
# Replayed hash : 0x728C4D232ED79DE4
# The replay reproduced the recorded session exactly.

Testing

dotnet test                                        # unit and integration tests
dotnet run -c Release --project tests/NationSim.Core.Tests -- --filter *Benchmark*

The suite has 125 tests in twelve suites:

Suite Tests Suite Tests
Primitives 8 Commands 6
Events 5 Metrics 6
Tick 4 Memory 2
Domain 24 Determinism 15
Economics 28 Replay 5
Regions 9 Server 4
Ministries 9

The suite covers:

  • Determinism – identical seeds produce identical state hashes, balances and laws; different seeds diverge; the PRNG stream can be captured and restored.
  • Time travel – a run of 1,000 ticks is snapshotted at tick 500, the snapshot is restored into a second instance and the resumed run ends on the exact same state hash. The snapshot is also verified across a file round trip and it carries the micro-agent state, the firms and the random streams of the regions.
  • Telemetry – the metrics store keeps the frames in order, overwrites the oldest one when it is full, copies a window into a caller owned span and never allocates.
  • Replay – a recorded session of 1,000 ticks with commands is replayed and ends on the exact same state hash; the session survives the binary round trip and the replay is deterministic.
  • Server – the WebSocket stream delivers telemetry frames, the command endpoint queues the commands, the region endpoint answers with the details of a region and the pause and the resume stop and restart the engine.
  • Emergent economy – the firms hire the citizens, the output and the unemployment rate emerge from the micro-agents, and a higher tax rate lowers the wealth and the demand of the citizens and changes the hiring of the firms.
  • Regional distribution – the aggregated regional GDP and population equal the global macro totals, the adjacency matrix and the neighbour lists agree, the region of a coordinate is stable and the regional update is deterministic.
  • Ministries – the ministries publish their modifiers, a well funded ministry improves its state, an acted law is routed to the right ministry and sets its enforcement target, and a reflection test proves that no ministry references the spatial layer or the ECS.
  • Macro economics – determinism of the engine, the effect of the interest rate, of the spending stance and of the labour force on output, the Phillips curve, the bounds of inflation and unemployment, and the monthly fiscal report.
  • Policy – bills update the macro policy, rejected bills leave it untouched, votes are driven by the economic indicators and are deterministic, and both the parliament and the executive ministry manage the policy parameters.
  • Tick loop – phase ordering, tick counters, simulated time and context contents.
  • Allocation budgets – event publishing, the tick lifecycle, a full kernel tick (including the macroeconomic engine, the regional systems and the parallel agent systems) and the state hash are asserted to allocate zero bytes. A separate diagnostic measures the process wide allocation of a tick to catch the worker threads.
  • Benchmarks – BenchmarkDotNet with a memory diagnoser measures the cost of a single tick and of a batch of 100 ticks.

Measured baseline

Short run of TickBenchmark on 12,000 citizens, 1,200 firms and 16 regions (Release, .NET 10, x86-64, 4 physical cores):

Method Mean Allocated
RunSingleTick 888.80 µs 0 B
RunHundredTicks 1,008.10 µs (10.08 µs per tick) 0 B

The telemetry is enabled in the benchmark, so the zero allocation figure covers the metrics ring buffer as well.

The Allocated column of the memory diagnoser confirms the zero allocation rule on the hot path: a tick of the full simulation, macroeconomic engine and parallel agent systems included, does not touch the managed heap.

The full verification report of the release is in the release audit.


Technology

About

High-performance Nation-State Simulator in C# (.NET 10). Features a zero-allocation hot path, deterministic PRNG, hybrid DDD + Arch ECS architecture, regional graph economy, interactive Spectre.Console REPL, and a headless WebSocket/REST server.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages