Architecture¶
The BDPD Platform is a stateless Node.js HTTP server (Express) that manages one or more concurrent arenas, each an independent game instance with its own commons, players, scheduler, and event log, and one or more concurrent worlds, each a federation of arenas with cross-arena resource links, treaties, agent migration, and world-level meta-agents.
Arenas remain the unit of harvest, pact, and sanction. Worlds add a nested layer above arenas — they compose without cloning, hold their members by reference, and tick all running arenas in parallel each global round. v0.9 use cases (single arena, single policy) keep working unchanged; v1.0 use cases (Mule-style polycentric, treaty enforcement, junk arena) live at the World layer.
System Diagram¶
flowchart TD
subgraph CLIENT["Client Layer"]
WEB["Web Dashboard\n(arena.html)"]
SCRIPT["Orchestration Script\n(my_arena.sh / Python)"]
EXT["External Agent\n(bdpd_agent.py / RL model)"]
end
subgraph SERVER["Node.js Server"]
API["REST API\n/api/arenas/*\n/api/v1/worlds/*"]
REG["Arena Registry\n(in-memory index)"]
WREG["World Registry\n(in-memory index)"]
STR["Streamer\n(SSE broadcast)"]
subgraph WORLD["World (v1.0)"]
LINKS["Resource Links\n(rate × source.stock)"]
TREATIES["Treaties\n(cross-arena constraints)"]
WMETA["World Meta-Agents\n(C3.b: world_sanctioner, treaty_enforcer)"]
end
subgraph ARENA["Arena Instance"]
REG2["Engine Registry\nbuildEngine(engineMode)"]
ENG["Engine\n(logistic | seneca | …)"]
SCHED["Scheduler\n(simultaneous / sequential / wealth-weighted)"]
PERT["Perturbation Engine\n(shocks / strategy override)"]
OBS["Observability Layer\n(noise / visibility / resolution)"]
REG2 --> ENG
end
subgraph AGENTS["Agent Runners"]
BUILTIN["Built-in Heuristics\n(aggressive / conservative / adaptive / rcp / random)"]
SANDBOX["Sandboxed JS\n(user code, 50ms timeout)"]
HTTP["HTTP Dispatcher\n(async fetch + AbortController)"]
end
end
subgraph STORAGE["Persistence"]
JSONL["turns.jsonl\n(per-tick log)"]
META["meta.json\n(arena metadata)"]
SUMM["summary.json\n(final report)"]
end
SCRIPT -->|"POST /arena"| API
WEB -->|"GET /api/arenas/:id/stream (SSE)"| STR
EXT -->|"POST /decide (callback)"| HTTP
API --> REG
API --> WREG
WREG --> WORLD
WORLD --> ARENA
REG --> ARENA
ENG --> SCHED
SCHED --> OBS
OBS --> AGENTS
AGENTS --> ENG
ENG --> PERT
ENG --> STR
ENG --> JSONL
REG --> META
REG --> SUMM
Key Subsystems¶
Arena Registry¶
A singleton in-memory index that maps arena IDs, owner keys, arena keys,
and spectator tokens to live Arena objects. All authentication is
performed here before dispatching to the arena instance.
- Owner key (
owner_*): administrative operations (start, close, delete) - Arena key: player join
- Spectator token (
spec_*): read-only SSE streaming
On server restart, closed arenas are not reloaded into memory, but their files on disk remain accessible for historical exploration.
World Registry (≥ v1.0)¶
A second singleton in-memory index, parallel to the Arena Registry,
that owns live World instances. A World composes N arenas
by reference (no clone), holds the cross-arena
Links and Treaties, and
hosts world-level meta-agents (C3.b — see Governance).
The World's tick() ticks every running arena in parallel
(Promise.all), then applies end-of-round link flows, then invokes
the world-level meta-agents over the resulting snapshot. The per-arena
schedulers stay invariant — World never reaches inside an arena's
decide/apply loop.
Storage follows data/worlds/<wid>/arenas/<aid>/, so each member
arena's JSONL log is naturally namespaced by its world. The legacy
default world keeps its v0.9 layout for backwards compatibility.
See World for the full surface.
Engine Plugin Registry (≥ v0.5.0)¶
Resource dynamics are implemented as pluggable engine classes behind a
registry. arena.js calls buildEngine(this.model) once at construction and
delegates every resource mutation to this._engine — it never inspects
engineMode again.
Harvests are rationed proportionally if demand exceeds supply. The hidden reserve and Forest Die provide stochastic collapse when the visible stock is exhausted.
Bardi (2011) three-variable ODE model:
Capital (\(C\)) drives pollution (\(P\)) with a delay, creating false stability before a tipping point. Resources (\(R\)) decline monotonically; the Seneca cliff appears when \(l_3 = 0\) and \(k_2 > 0\).
Adding a new model requires one new file (engine/my-engine.js) and
one line in engine/engine-registry.js. See Engines for
the full interface contract and developer guide.
All state mutations are synchronous within a single tick; concurrent ticks on the same arena are serialised.
Scheduler¶
Three pluggable turn-order strategies (≥ v0.4.0):
| Scheduler | Behaviour | Research Purpose |
|---|---|---|
simultaneous |
All agents observe same state, decide concurrently | Baseline (closest to continuous-time) |
sequential_random |
Agents act in random order; later agents see updated state | Information cascades |
wealth_weighted |
Richer agents act first | Regulatory capture / lobbying |
Permutation tests (B5) confirm that scheduler choice has no statistically significant effect on outcome — strategy composition dominates turn-order institutional rules.
Observability Layer¶
Intercepts the raw game state before passing it to any agent, applying per-variable transformations. See Observability for detailed configuration.
Perturbation Engine¶
A rule-based event scheduler that fires at configurable turns or when threshold conditions are met. See Perturbations for the full catalogue of 12 perturbation types.
Streamer¶
Maintains a list of open SSE connections per arena, broadcasting
tick, player_joined, arena_started, and arena_closed events. The
init event sent on connection includes a compact history of all past
ticks, allowing late-connecting clients to reconstruct the full trajectory.
Persistence¶
| File | Contents | Written |
|---|---|---|
turns.jsonl |
Per-tick state snapshot (agent decisions, commons state, metrics) | Real-time, appended each tick |
meta.json |
Lightweight arena metadata (keys, agent roster, config) | On arena close |
summary.json |
Full game report (victory evaluation, rankings, final metrics) | On arena close |
Data is written atomically so that no information is lost on server crash.
Arena Lifecycle¶
CREATE → JOIN → START → TICK (×N) → CLOSE
│ │ │ │ │
│ │ │ │ └→ summary.json + meta.json written
│ │ │ └→ 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 and is
accessible via /api/arenas/history and /api/arenas/:id/summary.
Engine Registry¶
spec.model.engineMode selects the resource model at arena creation:
| Mode | Variables | Collapse Mechanism | Default |
|---|---|---|---|
logistic |
Single stock \(S_t\) + hidden reserve | Forest Die + gate threshold | Yes |
seneca |
Resources \(R_t\), Capital \(C_t\), Pollution \(P_t\) | Seneca cliff via \(C\) and \(P\) ODE | No |
The rcp built-in agent strategy is designed specifically for
Seneca mode: it tracks capitalStock and pollutionLevel observations to
anticipate the cliff before resources visibly decline.
See Engines for the full parameter reference and instructions for implementing new engine plugins.