Skip to content

Arena & World Lifecycle & Persistence

This page covers the arena lifecycle, the World composition layer (≥ v1.0), the registries and persistence model, SSE streaming, automated report generation, and scheduler types.


Arena Lifecycle

CREATE  →  JOIN  →  START  →  TICK (×N)  →  CLOSE
  │          │        │          │              │
  │          │        │          │              └→ summary.json + meta.json + report.md
  │          │        │          └→ turns.jsonl appended each tick
  │          │        └→ transition waiting → running
  │          └→ agents added (builtin | code | HTTP)
  └→ owner key + arena key + spectator token returned

Once closed, an arena cannot be restarted. Its data remains on disk at data/worlds/<worldId>/arenas/<id>/ and is accessible via history and summary endpoints.

When arenas are composed into a World, the per-arena lifecycle stays the same — but the storage layout becomes data/worlds/<wid>/arenas/<aid>/, namespaced by the parent world. The legacy default world keeps the data/arenas/<id>/ layout for backwards compatibility.


World Lifecycle (≥ v1.0)

A World moves through idle → running → closed, parallel to but independent from its member arenas:

CREATE WORLD  →  ADD ARENAS / LINKS / TREATIES  →  START  →  TICK (×N)  →  CLOSE
                 │                                  │          │
                 │                                  │          ├→ each running arena ticks in parallel
                 │                                  │          ├→ end-of-round Link flows applied
                 │                                  │          └→ world meta-agents (C3.b) observe
                 │                                  └→ status → running
                 └→ arenas held by reference (no clone)

The World's tick() is not a substitute for the arena's tick() — it orchestrates them. Per-arena schedulers stay invariant; the World never reaches inside an arena's decide/apply loop. Arenas that are already closed are skipped (arena.tick() short-circuits).

Auto-close: when every composed arena has reached a non-running status, the World closes itself with reason: 'all_arenas_ended'. world.summary() remains queryable after close.

See World for the full surface (addArena, addLink, addTreaty, addMetaAgent, migrateAgent, routeCrossArenaMessage, runRounds).


Arena Registry

The ArenaRegistry is a singleton in-memory index that manages all concurrent arenas. It maintains a primary store plus three lookup indices:

Index Maps Used For
_arenas arenaId → Arena Direct arena resolution
_ownerIndex ownerKey → Set<arenaId> Owner authentication, listing owned arenas
_arenaKeyIndex arenaKey → arenaId Player join authentication
_spectatorIndex spectatorToken → arenaId SSE stream authentication

Authentication Flow

Client request → Registry._resolveArena(id)
              → Registry.checkOwner(id, ownerKey)    // for admin ops
              → Registry.checkPlayer(arenaKey)        // for join
              → Registry.checkSpectator(token)         // for SSE

All keys are generated via crypto.randomBytes(16) and are cryptographically random. The ownerKey can be reused across multiple arena creations, allowing a single orchestrator script to manage multiple concurrent games.

World Registry (≥ v1.0)

WorldRegistry is the parallel singleton for live worlds. It exposes createWorld, getWorld, listWorlds, and checkOwner with the same in-memory pattern as the Arena Registry. Owner keys are independent: a world owner is not automatically the owner of the arenas it composes (World.addArena only re-points an existing arena; the arena keeps its own owner key).

Persistence of the world shell itself (a worlds.json index) is intentionally out of scope for v1.0 — the per-arena JSONL logs under data/worlds/<wid>/arenas/<aid>/ already give us replay, and a world "exists" as long as at least one of its arenas does.


Persistence Model

All data is stored under data/worlds/<worldId>/arenas/<arenaId>/:

File Format Written Contents
turns.jsonl JSONL, appended each tick During game Per-turn state: agent decisions, commons state, metrics, observability-filtered observations
meta.json JSON, atomic write On arena close Lightweight metadata: ID, status, end reason, model, turn count, player count, timestamps, all three authentication tokens
summary.json JSON, atomic write On arena close Full game report: victory evaluation, final rankings, per-agent wealth/harvest history, turn-by-turn metrics, commons trajectory
report/report.md Markdown On arena close (async) Human-readable narrative: outcome, rankings, player analysis, commons health timeline, key moments
report/report.json JSON On arena close (async) Structured machine-readable report

Turns are written to turns.jsonl in real-time as they occur, ensuring that no data is lost on server crash. The summary and metadata are written atomically on arena close. The report is generated asynchronously (via setImmediate) so it does not block the close path.

Historical Arena Access

# List all closed arenas from disk
curl http://localhost:3000/api/arenas/history

# Replay a specific closed arena's summary directly from disk (no auth)
curl "http://localhost:3000/api/arenas/history/${ARENA_ID}/summary"

# Read a live arena's summary (requires owner key)
curl "http://localhost:3000/api/arenas/${ARENA_ID}/summary" \
  -H "X-Owner-Key: ${OWNER_KEY}"

Historical arenas are loaded from meta.json files; they are not reconstructed as live Arena objects.


SSE Streaming

The Streamer maintains a list of open Server-Sent Events connections per arena, broadcasting game events to all connected clients.

Connection

GET /api/arenas/:id/stream?token=SPECTATOR_TOKEN

The client receives an init event immediately on connection containing the full history of past ticks, followed by real-time tick events as the game progresses.

Events

Event Payload Trigger
init { turns: [...], arenaId, players: [...], config } On SSE connection
player_joined { playerId, type, displayName } Agent joins arena
arena_started { turn: 0, commons: {...} } Arena transitions to running
tick Full turn record Each game tick
arena_closed { victory, rankings, endReason } Arena closes

Late Connection Support

The init event includes a compact history of all past ticks, allowing a web dashboard to connect mid-game and reconstruct the full trajectory without additional API calls. The history is stored in memory and serialised once per connection.


Automated Report Generation

When an arena closes, the Reporter module (platform/reporter.js) generates an automated analysis. The report is written to data/worlds/<worldId>/arenas/<id>/report/ and contains:

  • report.md — Human-readable narrative: gate outcome, rankings, per-agent wealth and harvest evolution, commons health timeline, Gini trajectory, key strategic moments
  • report.json — Machine-readable structured data: all metrics, rankings, turn-level aggregates
  • figures/ — PNG charts generated via Python/matplotlib subprocess: harvest per turn, wealth per turn, commons stock trajectory, Gini evolution

The reporter reads from the arena's fullSummary() output and does not require the arena to be live.


Scheduler Types

Three pluggable turn-order strategies control the sequence in which agents observe state and submit decisions (≥ v0.4.0):

Scheduler Behaviour Research Purpose
simultaneous All agents observe the same state and decide concurrently. Decisions cannot influence each other within the same turn. Baseline — closest to continuous-time differential game
sequential_random Agents act in random order. Later agents observe the updated state after earlier agents' harvests are applied. Information cascades, first-mover advantage
wealth_weighted Agents act in descending order of wealth. Richer agents decide first, observing the pre-harvest state; poorer agents see the residual stock after the rich have taken their share. Regulatory capture, lobbying, power asymmetry

Permutation tests (B5 experiment, 10,000 permutations) confirm that scheduler choice has no statistically significant effect on gate pass rate, game length, or welfare score (\(p > 0.05\)). Strategy composition dominates turn-order institutional rules.


Arena Deletion

curl -X DELETE "http://localhost:3000/api/arenas/${ARENA_ID}" \
  -H "X-Owner-Key: ${OWNER_KEY}"

This removes the arena from the in-memory registry and deletes its directory under data/worlds/<worldId>/arenas/. Use with caution — data is irrecoverable.