Spaces:
Sleeping
Visualizer β Game-engine contract
This document describes what the React visualizer in visualizer/ expects from
the rest of the project. The viewer can render episodes from two sources:
- Live streaming (default): the FastAPI server in
server/visualizer_server.pyruns LLM- or rule-based agents through the parliamentary loop and streams per-round records over Server-Sent Events. - Static JSON files: drag-and-drop a
.jsonfile (or click "Load episode JSON") with the shape described below.
The two paths share the same per-round schema, so anything the live runner produces can be dropped onto disk and replayed later.
1. The episode JSON contract
Top-level shape:
{
"episode_id": "string",
"seed": 42,
"policy": "string e.g. 'gpt-4o-mini' or 'PPO/checkpoint-12000'",
"config": {
"sectors": [
{ "name": "Social", "full_name": "Social/Municipal", "baseline": 60 },
...
],
"pop_0": 1000000,
"initial_treasury": 1000,
"baseline_tax": 100,
"productivity_bounds": [0.5, 2.0],
"max_rounds": 50
},
"rounds": [ /* see Β§1.1 below */ ],
"summary": {
"rounds_survived": 50,
"total_reward": 12345.6,
"final_treasury": 2200,
"final_prosperity": 0.0021,
"final_productivity": 1.42,
"final_population": 1080000,
"termination_reason": "MAX_ROUNDS"
}
}
1.1 Per-round shape
Each entry in rounds[] is one full step (Phases 1β9 of the spec):
{
"round_num": 1,
"year": 1,
"quarter": 1,
// Phase 1 β events
"events": [
{
"round": 1,
"id": "war",
"name": "War",
"severity": 4,
"affected_sectors": { "Defense": 2.5, "Agriculture": 1.3 },
"category": "moderate",
"narrative": "Enemy forcesβ¦",
"treasury_injection": 0,
"is_positive": false
}
],
"crisis_occurred": false,
"treasury_injection": 0,
// Phase 2 β debate
"debate": [
{ "agent_id": "Health", "department": "Health", "message": "β¦" }
],
// Phases 3β4 β proposals + voting
"proposal_order": ["Defense", "Health", "Education", "Commerce", "Social", "Agriculture"],
"proposals": [
{
"proposal_id": "r1_Defense",
"agent_id": "Defense",
"department": "Defense",
"amount": 320,
"justification": "β¦",
"status": "approved",
"rejection_reason": null,
"votes": { "Health": "YES", "Defense": "ABSTAIN", ... }
}
],
"votes": [
{
"proposal_id": "r1_Defense",
"agent_id": "Health",
"department": "Health",
"vote": "YES"
}
],
"vote_results": [
{
"proposal_id": "r1_Defense",
"department": "Defense",
"amount": 320,
"yes": 4, "no": 1, "abstain": 1,
"status": "APPROVED"
}
],
// Phases 5β8 β execution + revenue + surplus
"allocations": { "Defense": 320, "Health": 95, ... },
"consumptions": { "Defense": 250, "Health": 90, ... },
"revenues": { "Defense": 480, "Health": 130, ... },
"revenue_factors": { "Defense": 1.50, "Health": 1.36, ... },
"thresholds": {
"Defense": { "critical": 100, "demand": 250, "surplus": 375, "wastage": 625 },
...
},
"event_multipliers": { "Defense": 2.5, "Agriculture": 1.3, ... },
"treasury_before": 1000.0,
"treasury_after": 1180.4,
"total_allocation": 740,
"total_revenue": 1020.4,
"surplus_returned": 60.0,
"population": 1004000,
"productivity": 1.04,
"avg_revenue_factor":1.21,
"prosperity": 0.00102,
// Phase 9 β reward + termination
"reward": {
"base_reward": 0.00102,
"productivity_bonus": 2.0,
"survival_bonus": 10.0,
"over_alloc_penalty": 0,
"under_alloc_penalty": -10,
"critical_penalty": 0,
"total": 2.001
},
"cumulative_reward": 2.001,
"done": false,
"termination_reason": null,
"critical_failure_in_budget": false
}
1.2 Validation rules the loader enforces
App.jsx::onLoadFile requires:
parsed.roundsis an array (may be empty for a freshly-started live run).parsed.config.sectorsexists.
Everything else is run through src/utils/normalizeEpisode.js, which derives
the optional fields below from the spec formulas in
specification/04_ECONOMY_MODEL.md. The engine's lean output is enough.
1.3 Required vs derived fields (per round)
| Field | Required | If missing, derived as |
|---|---|---|
round_num |
β (or implied by index) | index + 1 |
treasury or treasury_after |
β | uses whichever is present |
treasury_before |
β | previous round's treasury_after (config.initial_treasury for round 1) |
allocations[s], revenues[s], revenue_factors[s], consumptions[s] |
β | β |
demands[s] or thresholds[s] |
β (one of) | thresholds derived as {0.4, 1.0, 1.5, 2.5} Γ demand (spec 04) |
event_multipliers[s] |
β | aggregated multiplicatively from events[].affected_sectors |
total_allocation, total_revenue, surplus_returned |
β | summed from per-sector dicts |
prosperity |
β | total_revenue / population (spec 09 base reward) |
avg_revenue_factor |
β | mean of revenue_factors[s] |
cumulative_reward |
β | running sum of reward.total |
events[].is_positive |
β | true iff treasury_injection > 0 or category == "positive" or every multiplier < 1 |
events[].treasury_injection |
β | 0 |
debate, proposal_order, proposals, votes, vote_results |
β | empty (panel renders gracefully) |
done, termination_reason, reward.* |
β | falsy / zero |
Critical-failure semantics (per core/game.py::_terminate() + spec 04):
the engine returns the round with revenues = 0, revenue_factors = 0,
consumptions = 0, surplus_returned = 0 for every sector and does not
debit the treasury (the check fires in Phase 5a, before Phase 5b debit).
The visualizer detects this with done && termination_reason.startsWith("CRITICAL_FAILURE"),
flags the offending sector(s) (those with allocation < critical), greys out
the rest, and labels the treasury cell as "frozen β failure before debit".
2. Live streaming
The visualizer talks to server/visualizer_server.py over a tiny REST + SSE
surface. The Vite dev server already proxies /api/* to
http://127.0.0.1:8001 (override with VIZ_BACKEND_URL).
2.1 Endpoints
| Method | Path | Purpose |
|---|---|---|
GET |
/api/health |
Liveness check. |
GET |
/api/config |
Visualizer-shaped config block (sectors, baselines, β¦). |
GET |
/api/modes |
Available inference modes (llm + rule-based baselines). |
GET |
/api/runs |
List recent runs. |
POST |
/api/runs |
Start a new run. Body: {mode, model_id?, seed, max_rounds, temperature}. Returns {run_id, ...}. |
GET |
/api/runs/{id}/snapshot |
Full episode-so-far in the Β§1 shape. |
GET |
/api/runs/{id}/stream |
SSE stream β see Β§2.2. |
2.2 SSE event types
Each event arrives as event: <type>\ndata: <json>\n\n. The browser client in
src/utils/api.js dispatches them into typed callbacks.
event |
Payload | Notes |
|---|---|---|
start |
{run_id, policy, mode, seed, config, max_rounds} |
Sent once on connection. |
round |
A single per-round record (Β§1.1). | One per completed round. |
summary |
The Β§1 summary block. |
Sent once when the episode finishes. |
error |
{message: string} |
Adapter or engine failure. |
done |
{reason: "complete" | "error" | "already_closed"} |
Always the last event. |
Late subscribers receive the full event history first (so refreshing the page mid-run replays from the beginning), then live updates.
2.3 Inference modes
Modes returned from /api/modes:
llmβ drives every minister with a Hugging Face Inference API model. RequiresHF_TOKENin the server.env; readsHF_MODEL_IDas the default model id (the form lets you override it per-run).equal_split,optimal_zone,conservative,greedyβ rule-based baselines fromagents/rule_based/. Useful for offline demos.
2.4 Running locally
Two terminals:
# Terminal 1 β backend (FastAPI + SSE on :8001)
python -m scripts.run_visualizer_server
# Terminal 2 β frontend (Vite dev server on :5173)
cd visualizer
npm install # one time
npm run dev
Open http://localhost:5173, pick a policy in the Live inference card, and press Start run. Rounds will stream into the dashboard as they're produced.
3. Static JSON files
The static loader is unchanged. To export a recorded run, hit
GET /api/runs/{id}/snapshot and save the response β the file matches the
shape in Β§1 and can be dragged back into the visualizer at any time.
4. Development notes
vite.config.jshasserver.watch.usePolling: truebecause the project lives on a WSL-mounted Windows drive wherefs.watch()throwsEISDIR. Remove it on a normal filesystem to lower idle CPU.- For the very first run on Windows + WSL, install dependencies inside WSL
(
wsl npm install) to avoidEPERM/EISDIRcleanup errors, then run the build/dev server from either side. - The proxy block in
vite.config.jsstrips response buffering for the/streamendpoint so SSE chunks arrive immediately even through the dev proxy.