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¶
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 momentsreport.json— Machine-readable structured data: all metrics, rankings, turn-level aggregatesfigures/— 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¶
This removes the arena from the in-memory registry and deletes its
directory under data/worlds/<worldId>/arenas/. Use with caution — data is irrecoverable.