Skip to content

Perturbations

The BDPD Platform's perturbation engine injects structured mid-game shocks — modelling exogenous events, policy interventions, and structural breaks that test the fragility of cooperative arrangements.


Overview

A perturbation is a scheduled or triggered change to the arena's state or configuration during a running game. Perturbations model:

  • Exogenous shocks — climate events, market crashes, policy changes
  • Structural breaks — The Mule: an agent whose strategy changes mid-game, invalidating models built on stable populations
  • Endogenous warnings — triggered when metrics cross thresholds (e.g. Gini > 0.4)

Perturbation Spec

{
  "id": "p1",
  "type": "regen_shock",
  "trigger": {
    "kind": "turn",
    "turn": 20
  },
  "payload": {
    "factor": 0.5
  },
  "repeatable": false,
  "description": "Drought: regen rate halved at turn 20"
}
Field Type Description
id string Unique identifier
type string One of 12 perturbation types (see below)
trigger.kind "turn" | "threshold" | "manual" | "pact_violation" How the perturbation fires
trigger.turn number Fire at this specific turn
trigger.variable string Metric to monitor: commonsRatio, gini, welfareScore
trigger.above / trigger.below number Fire when metric crosses this threshold
payload object Type-specific parameters
repeatable boolean If true, fires on every matching turn (default: false)
description string Human-readable explanation (appears in reports)

Perturbation Types

regen_shock

Multiplies the regeneration rate by a factor.

Payload Description
factor ∈ [0, ∞) \(r_\text{new} = r_\text{old} \times \text{factor}\)
factor Effect Research Use
< 1.0 Drought, environmental degradation P10: Seneca asymmetry
0.0 Complete regeneration collapse B4: pure Seneca shock
> 1.0 Boom, technological improvement P10: asymmetric recovery

regen_set (≥ v0.4.1)

Sets the regeneration rate to an exact value, ignoring the prior rate.

Payload Description
value New \(r\), clamped to ≥ 0

Useful for precisely controlling the regeneration regime without compounding from the previous value.

commons_shock

Adds a step change to the commons stock.

Payload Description
delta \(\Delta S_t\) (negative = depletion, positive = windfall)
delta Effect Research Use
−30 Sudden depletion event Testing recovery dynamics
+30 Resource discovery Testing abundance effects

capacity_shock

Multiplies the commons carrying capacity by a factor.

Payload Description
factor \(K_\text{new} = K_\text{old} \times \text{factor}\)

If the new capacity is lower than current stock, stock is capped to the new capacity. This models ecosystem contraction (e.g. habitat loss).

wealth_shock

Modifies agent wealth — multiplicatively, additively, or both.

Payload Description
playerId Target player ID, or null for all players
factor Multiply wealth by this
delta Add this amount after multiplication
Example Effect
{ "factor": 0.5 } Halve all players' wealth
{ "playerId": "...", "delta": 20 } Give +20 to one player
{ "factor": 1.2 } 20% wealth increase for all

strategy_override (The Mule)

Replaces a built-in agent's strategy live during the game.

Payload Description
playerId Target player (use PLAYER_N for Nth player, 0-indexed)
strategy New strategy: aggressive, conservative, adaptive, rcp, random

Only works on builtin agent types. HTTP and code agents cannot be overridden.

The Mule

Named after the character in Asimov's Foundation — an unpredictable structural agent whose appearance breaks models built on the assumption of stable populations. In P11, a conservative agent is overridden to aggressive at turn 20, testing whether cooperative equilibria can survive a single well-timed defection.

observability_flip

Changes what agents can see mid-game.

Payload Description
variable Variable name: commonsStock, regenRate, othersWealth, etc.
visible true or false — new visibility
Example Effect
Hide commonsStock at turn 15 Agents lose sight of the resource mid-game (information blackout)
Reveal regenRate at turn 10 Agents gain knowledge of regeneration function (transparency shock)

capital_shock (≥ v1.1)

Modifies the engine's capital state (Seneca engine only).

Payload Description
factor Multiply capital by this (optional)
delta Add this amount after multiplication (optional)

Returns applied: false if the engine has no setCapital method. Used in Seneca polycentric experiments (S3) to model external capital injection or destruction.

threshold_shift

Moves the collapse threshold — the stock level below which the gate fails.

Payload Description
newThreshold New gate threshold, clamped to ≥ 0
Example Effect
{ "newThreshold": 30 } Raise threshold — collapse becomes more likely
{ "newThreshold": 0 } Remove threshold entirely

sanction (≥ v0.9)

Applies a one-shot wealth penalty to a player.

Payload Description
playerId Target player ID
factor Multiply wealth by this (default: 1)
amount Subtract this from wealth after factor (default: 0)
reason Optional human-readable justification

Typically fired by the pact_violation trigger when a governance pact is breached. The result is max(0, wealth × factor − amount).

sanction_graduated (≥ v0.9)

Graduated sanction following Ostrom's design principle #5: the penalty escalates with repeated violations.

Payload Description
playerId Target player ID
ladder Array of amounts, e.g. [1, 3, 10]; the k-th violation draws ladder[min(k−1, len−1)]

Counts prior violations of the same (playerId, termType) from the pact registry to determine the step. Used in D2/D3 ladder sweeps.

exclude (≥ v1.0)

Migrates a player from their home arena to a designated junk arena in the same World. The migration preserves wealth and identity (same PlayerRecord instance) but resets pact membership, reputation, and message inbox in the target.

Payload Description
playerId UUID of the player to migrate (required)
junkArenaId Target arena ID, must be in the same world (required)
reason Optional string surfaced in changes.exclude.reason
{
  "type": "exclude",
  "payload": {
    "playerId": "player-uuid",
    "junkArenaId": "arena-junk-uuid",
    "reason": "repeat_violator"
  }
}

Failure modes (return { applied: false, description }):

  • arena not in a world (exclude requires nested composition)
  • target is the same as the source arena
  • underlying World.migrateAgent error (player not found, target closed, …)

Junk arena pattern

A junk arena is an ordinary arena that the world owner designates as the migration target for exclude. It is otherwise unmodified — same engine, same scheduler, same observability. Migrated players keep their wealth and original identity, and continue to interact with whatever institutional structure the junk arena carries (or fails to carry). Empty junk arenas isolate violators from the commons; junk arenas with their own meta-agent let them try to self-govern.

The pattern's load-bearing experiment is D5: a world composes three mains under a voluntary_sanctioner and one shared junk; the mains' meta-agent emits exclude against repeat violators. With built-in agents the junk stays "pure" (no further violations); with LLM agents the junk gets contaminated by the migrated players who continue defecting — junk purity drops from 1.00 to 0.63 across N=5 seeds. See Paper 02 — Vignette V4 for the full reading.


Trigger Types

Turn-Based

Fires at a specific turn number. The simplest trigger.

{ "trigger": { "kind": "turn", "turn": 20 } }

Threshold-Based

Fires when a metric crosses a boundary. Evaluated after each tick.

{ "trigger": { "kind": "threshold", "variable": "gini", "above": 0.4 } }
Variable Meaning
commonsRatio \(S_t / K\)
commonsStock \(S_t\)
gini Gini coefficient of wealth distribution
welfareScore Aggregate welfare

Manual

Fired explicitly via the API. Not evaluated automatically.

{ "trigger": { "kind": "manual" } }

Pact-Violation (≥ v0.9)

Fires once per recorded breach of an active pact, evaluated after the harvest phase of each tick when PactRegistry.checkViolations runs. The optional termType filter restricts the trigger to a single pact type (omit it to fire on any pact breach). The triggered perturbation is dispatched once per violation event with the violating player ID threaded through so the handler (typically sanction or sanction_graduated) can target the right party.

{ "trigger": { "kind": "pact_violation", "termType": "harvest_cap" } }

See pacts for the pact lifecycle and the violation event shape, and the C1 pilot (scripts/pilot_c1.mjs) for a working end-to-end example pairing this trigger with the sanction perturbation.

# Fire by spec id (the spec must already be attached at creation)
curl -X POST "http://localhost:3000/api/arenas/${ARENA_ID}/perturb" \
  -H "Content-Type: application/json" \
  -H "X-Owner-Key: ${OWNER_KEY}" \
  -d '{"specId":"MANUAL_ID"}'

# Or fire an ad-hoc perturbation in one call (no pre-registration)
curl -X POST "http://localhost:3000/api/arenas/${ARENA_ID}/perturb" \
  -H "Content-Type: application/json" \
  -H "X-Owner-Key: ${OWNER_KEY}" \
  -d '{"spec":{"id":"adhoc","type":"regen_shock","payload":{"factor":0.5}}}'

Multiple Perturbations

Multiple perturbations can be scheduled on the same arena:

{
  "perturbations": [
    {
      "id": "drought_t20",
      "type": "regen_shock",
      "trigger": { "kind": "turn", "turn": 20 },
      "payload": { "factor": 0.5 },
      "description": "Climate event: drought halves regen rate"
    },
    {
      "id": "mule_at_gini",
      "type": "strategy_override",
      "trigger": { "kind": "threshold", "variable": "gini", "above": 0.3 },
      "payload": { "playerId": "PLAYER_1", "strategy": "aggressive" },
      "description": "Mule: Agent 1 defects when inequality rises"
    },
    {
      "id": "recovery_t30",
      "type": "regen_shock",
      "trigger": { "kind": "turn", "turn": 30 },
      "payload": { "factor": 2.0 },
      "description": "Policy response: regen rate doubled"
    }
  ]
}

Research Use Cases

Experiment Perturbation Types Finding
P10 regen_shock (factor < 1 vs > 1) Seneca asymmetry: negative shocks compound, positive shocks provide bounded relief
P11 strategy_override (conservative → aggressive) Cooperative equilibria structurally fragile to late defection
B4 regen_shock (no aggressor present) Seneca asymmetry is primarily a strategic artefact — near-symmetric without aggressor
C1 sanction (via pact_violation trigger) 1:1 violation-to-sanction ratio; governance metrics all dynamic
D2/D3 sanction_graduated (ladder sweeps) Graduation (not amount) is the key factor; soft ladder preserves + higher mule wealth
D5 exclude (junk arena migration) Policing via exclusion; junk purity 1.00
S3 capital_shock (Seneca polycentric) Leading-indicator governance (capital-triggered) prevents cascade

Persistence

Perturbation events are recorded in turns.jsonl with full before/after state. The PerturbationSchedule tracks which perturbations have fired (non-repeatable ones fire once) and emits a perturbation event via the arena's event emitter, which is broadcast to SSE clients.