Skip to content

Latest commit

 

History

History
232 lines (180 loc) · 7.24 KB

api.md

File metadata and controls

232 lines (180 loc) · 7.24 KB

The NationSim API

NationSim.Server runs the simulation as a background engine and exposes it to external front ends (Avalonia, React, Unity, a browser dashboard, ...). The transport is deliberately thin: the core simulation loop knows nothing about sockets.

dotnet run --project src/NationSim.Server -- --port 5000 --seed 42 --agents 2000
Option Default Description
--port <int> 5000 The port the server listens on (http://localhost:<port>)
--seed <ulong> 20260101 The seed of the deterministic random source
--agents <int> 1000 The number of citizens

The same server can be started from the console application:

dotnet run --project src/NationSim.UI.Console -- server start --port 5000

1. The endpoints

Method Route Purpose
GET /ws/state The WebSocket stream of the telemetry frames
POST /api/command Queues a command for the next tick
GET /api/regions/{id} The details of an administrative region
GET /api/state The current indicators of the nation
GET /health The liveness probe and the current tick

All the responses are JSON.

GET /health

{ "status": "ok", "tick": 1284 }

GET /api/state

{
  "tick": 1284,
  "paused": false,
  "gdp": 5810351.30185,
  "inflation": 0.010349115890519841,
  "unemployment": 0.29515418502202643,
  "treasuryBalance": 657445.9328158204,
  "population": 292,
  "regions": 16
}
Field Type Description
tick number The number of executed ticks; one tick is one simulated day
paused bool Whether the engine is paused
gdp number The nominal gross domestic product
inflation number The annual inflation rate as a fraction (0.02 is 2 %)
unemployment number The unemployment rate as a fraction
treasuryBalance number The balance of the treasury
population number The number of living citizens
regions number The number of administrative regions

GET /api/regions/{id}

{
  "regionId": 3,
  "population": 16,
  "employedCount": 10,
  "localGdp": 345124.72435,
  "infrastructureLevel": 0.5274,
  "unemploymentRate": 0.277283982751,
  "taxCollected": 34810.46823,
  "neighbours": [7, 2]
}

neighbours holds the identifiers of the regions that are connected by the transport network. An unknown region answers with 404:

{ "error": "Region 999 does not exist." }

POST /api/command

{ "kind": "bill", "target": "income-tax", "value": 0.3 }
Field Type Description
kind string bill, budget, pause or resume
target string The bill (income-tax, corp-tax, spending, rate, subsidy) or the ministry (health, infrastructure, finance)
value number The numeric payload of the command

The answer:

{ "accepted": true, "tick": 335, "message": "queued" }

An unknown command answers with 400:

{ "accepted": false, "tick": 335, "message": "Unknown command 'foo'." }

The command is queued: it is applied in the input phase of the next tick, exactly like the commands of the console application. A command that changes the state of the simulation (a bill or a budget) is recorded by the replay system, so a session that is driven from a front end can be replayed later.


2. The WebSocket stream

GET /ws/state upgrades the connection and streams the telemetry frames. Every frame is one JSON object followed by a newline (\n), so a client can read it line by line.

{
  "tick": 1284,
  "timestamp": "2026-05-15T00:00:00.0000000+00:00",
  "gdp": 5810351.30185,
  "inflation": 0.010349115890519841,
  "unemployment": 0.29515418502202643,
  "treasuryBalance": 657445.9328158204,
  "totalPopulation": 292,
  "activeFirmsCount": 148,
  "averageCitizenWealth": 2398.35
}
Field Type Description
tick number The tick the sample belongs to
timestamp string The simulated time of the sample (ISO 8601), never the wall clock
gdp number The nominal gross domestic product
inflation number The annual inflation rate as a fraction
unemployment number The unemployment rate as a fraction
treasuryBalance number The balance of the treasury
totalPopulation number The number of living citizens
activeFirmsCount number The number of firms that employ somebody
averageCitizenWealth number The average wealth of a citizen

The stream is fed by a bounded channel: when a client is too slow, the oldest frame is dropped instead of blocking the engine. The simulation never waits for a consumer.

A minimal browser client:

const socket = new WebSocket("ws://localhost:5000/ws/state");
socket.onmessage = (event) => {
  const frame = JSON.parse(event.data);
  console.log(frame.tick, frame.gdp, frame.unemployment);
};

A .NET client:

using var client = new ClientWebSocket();
await client.ConnectAsync(new Uri("ws://localhost:5000/ws/state"), CancellationToken.None);

var buffer = new byte[8192];
var result = await client.ReceiveAsync(buffer, CancellationToken.None);
string payload = Encoding.UTF8.GetString(buffer, 0, result.Count);

3. Driving the simulation from a front end

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

# fund the ministry of health
curl -X POST http://localhost:5000/api/command \
     -H "Content-Type: application/json" \
     -d '{"kind":"budget","target":"health","value":2500000}'

# stop and restart the engine
curl -X POST http://localhost:5000/api/command -H "Content-Type: application/json" -d '{"kind":"pause"}'
curl -X POST http://localhost:5000/api/command -H "Content-Type: application/json" -d '{"kind":"resume"}'

The pause and the resume are handled by the engine host itself, because a paused engine does not run a tick and would therefore never consume them from the queue.


4. The replay protocol

A session that is driven from the console application is recorded:

dotnet run --project src/NationSim.UI.Console -- --seed 42 replay record session.replay
# ... issue the commands ...
dotnet run --project src/NationSim.UI.Console -- replay play session.replay
# Recorded hash : 0x728C4D232ED79DE4
# Replayed hash : 0x728C4D232ED79DE4
# The replay reproduced the recorded session exactly.

The .replay file is a MemoryPack payload that holds the seed, the initial configuration and the commands with their ticks. A front end that wants reproducible sessions should send the same semantic commands (bill, budget) - the replay injects them at the tick they were issued at, so the reconstructed run is bit for bit identical.


5. Error semantics

Situation Answer
An unknown region 404 with {"error": "..."}
An unknown command 400 with {"accepted": false, ...}
A malformed body 400 from the framework
A WebSocket request to another route 400

The server never returns a 5xx for a client error: an invalid region or an invalid command is a normal answer.