nation_optimizer / visualizer /EPISODE_FORMAT.md
Kr0issant
(visualizer) implement the whole thing
f2e28ca
|
Raw
History Blame Contribute Delete
9.39 kB

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:

  1. Live streaming (default): the FastAPI server in server/visualizer_server.py runs LLM- or rule-based agents through the parliamentary loop and streams per-round records over Server-Sent Events.
  2. Static JSON files: drag-and-drop a .json file (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.rounds is an array (may be empty for a freshly-started live run).
  • parsed.config.sectors exists.

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. Requires HF_TOKEN in the server .env; reads HF_MODEL_ID as the default model id (the form lets you override it per-run).
  • equal_split, optimal_zone, conservative, greedy β€” rule-based baselines from agents/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.js has server.watch.usePolling: true because the project lives on a WSL-mounted Windows drive where fs.watch() throws EISDIR. 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 avoid EPERM/EISDIR cleanup errors, then run the build/dev server from either side.
  • The proxy block in vite.config.js strips response buffering for the /stream endpoint so SSE chunks arrive immediately even through the dev proxy.