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 (
excluderequires nested composition) - target is the same as the source arena
- underlying
World.migrateAgenterror (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.
Threshold-Based¶
Fires when a metric crosses a boundary. Evaluated after each tick.
| 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.
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.
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.