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.
| 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 |
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
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 |
These rules are enforced by the code, the test suite and the build configuration
(TreatWarningsAsErrors is on: the solution must compile with zero warnings).
No heap allocation is allowed while a tick is executed. The rules that make this hold:
- The
EventBusresolves channels through a type keyed cache and stores subscribers in a pre-sized array.EventChannel<T>.Publishpasses payloads byinreference, so nothing is boxed and nothing is allocated. Every macro payload (FiscalFlow,MacroState,PolicyEnactedEvent,GlobalModifier,MeasuredEconomyEvent) is a value type. - The
GlobalModifierRegistryowns 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:
RegionGraphholds theRegionNode[], a flatbool[]adjacency matrix and the pre-computed neighbour lists. The update loops walk the nodes bySpan<RegionNode>and the neighbour lists byReadOnlySpan<int>; the population aggregation writes into a pre-allocatedint[]. - The micro-agents work on a structure of arrays:
MicroAgentStoremirrors 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 - unlikeParallel.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
Enumerableallocates a boxed enumerator (ExecutiveMinistry.Approval,Parliament.LawsInForce). - The macroeconomic engine is pure
decimalarithmetic 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
NationalAccountsinstead 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.GetAllocatedBytesForCurrentThreadbased 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 withGC.GetTotalAllocatedBytesto catch the allocations of the worker threads.
DateTime.Now,DateTimeOffset.NowandSystem.Randomare banned. There is no ambient clock:SimulationClockderives 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. InvariantGlobalizationis enabled so that formatting and parsing cannot depend on the host locale.
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, andSimulationWorldhas noDespawnmethod at all. - The working set never uses the iteration order as an identity. The slot of a
citizen in
MicroAgentStoreis 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.
Aggregates must not reference ECS entities, and the ministries must not reference the spatial regions.
NationSim.Domaindoes not referenceNationSim.Simulationand knows nothing aboutArch.Core.Entity,RegionNodeor 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,FirmandDemographicsare plain value types that only carry the state of an agent. NationSim.Simulationis the only layer allowed to translate between the two worlds.ECSAggregationSystemaggregates the micro-agents and pushes the result into theRegionNodeand theNationalAccounts; 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
GlobalModifierstructs. 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
SimulationKerneland the systems, again through plain data.
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 |
| 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 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.
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.
| 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.
| 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.
Parliamentpasses aTaxLawor anInfrastructureLawand publishes the immutableLawPassedEvent(kind, target value and enforcement intensity).- The
MinistryRegistryroutes the law to the ministry that has to enforce it. The ministry registers the law and sets its enforcement target. - The spatial systems read the published modifiers during the execution phase and update
the region states deterministically, in ascending
RegionIdorder.
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.
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 |
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.
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:
Parliament.ProposeBill(bill)creates aLawcarrying the bill.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.- If the supporting seats reach the majority threshold, the bill is applied to the
MacroPolicyaggregate, which publishes an immutablePolicyEnactedEvent. - The
MacroeconomicSystempicks 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.
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.
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.
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.
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.Channelsqueue that the tick loop drains in its input phase; the core knows nothing about sockets. - The telemetry travels through a bounded channel of
TelemetryFramevalues; a slow client drops the oldest frame instead of blocking the engine. SimulationEngineHostowns the kernel and the queues; it is the only piece that knows both the engine and the transport.
| 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 |
# 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| 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 |
| 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.
# 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}'# 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.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 –
BenchmarkDotNetwith a memory diagnoser measures the cost of a single tick and of a batch of 100 ticks.
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.
- .NET 10 (SDK pinned in
global.json) - Arch 2.1 — ECS framework
- MemoryPack 1.21 — binary snapshots
- Spectre.Console 0.57 — terminal UI
- BenchmarkDotNet 0.15 — performance baselines
- xUnit 2.9 — test framework