Skip to content

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.

spec.model.engineMode  →  ENGINE_REGISTRY  →  Engine instance
                           { logistic, seneca, … }
\[S_{t+1} = \max\left(0, \min\left(K, (S_t - H_t) + r\,(S_t - H_t) \left(1 - \frac{S_t - H_t}{K}\right)\right)\right)\]

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:

\[ \begin{aligned} \dot{R} &= -k_1 R C - l_3 R \\ \dot{C} &= k_1 R C - k_2 C P - l_1 C \\ \dot{P} &= k_2 C P - l_2 P \end{aligned} \]

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.