Skip to content

Experiment Definition Format

BDPD experiments are declared as JSON files stored in experiments/definitions/. Each file fully describes a parametric sweep: the commons model, agent pool, observability settings, perturbations, sweep axis, and replication count. The runner (experiments/runner.js) reads the file, instantiates Arena objects in-process, and aggregates results across replicates.


Running a Definition

# Run by name (looks in experiments/definitions/<name>.json)
node experiments/experiment.js run aggressive_fraction

# Run by file path
node experiments/experiment.js run /path/to/my_experiment.json

# Override replication count at the command line
node experiments/experiment.js run aggressive_fraction --runs 5

# Save results to a custom directory
node experiments/experiment.js run aggressive_fraction --out experiments/results/

# List all available definitions
node experiments/experiment.js list

# Print a definition without running it
node experiments/experiment.js show aggressive_fraction

Full Schema

{
  "name":         string          // unique identifier; used as filename prefix for results
  "description":  string          // one-line description of the experiment

  "baseConfig":   ModelConfig     // arena model defaults (see §ModelConfig)
  "observability": ObsConfig      // default observability for all points (see §ObsConfig)
  "baseAgents":   AgentSpec[]     // agent pool; each point may patch this list
  "perturbations": Perturbation[] // (optional) default perturbations applied to every point

  "sweep": {
    "axis":   string              // human-readable label for the sweep dimension
    "points": SweepPoint[]        // list of parameter combinations to test
  }

  "runsPerPoint": integer         // independent replicates per sweep point (default: 1)
  "metrics":      string[]        // dotted paths into arena summary to aggregate
}

ModelConfig

Merged with per-point configOverride before creating each arena. All keys are optional; the runner fills missing keys with the defaults shown.

Field Type Default Description
commonsInitial number 150 Initial commons stock
commonsCapacity number 150 Carrying capacity \(K\) (logistic); ignored by Seneca engine
regenRate number 0.12 Logistic growth rate \(r\); Seneca ODE uses k1, k2 instead
maxTurns number 60 Maximum turns before forced close
threshold number 10 Collapse threshold (logistic legacy; ignored when useHiddenReserve: true)
scheduler string "simultaneous" Turn order: simultaneous / sequential_random / wealth_weighted
engineMode string "logistic" Resource dynamics: logistic or seneca
useHiddenReserve boolean false Enable stochastic hidden-reserve collapse (logistic only)
hiddenReserveInitial number 20 Initial hidden reserve size
collapseDie object {sides:6, successThreshold:4} Stochastic collapse die configuration
seneca object {} Seneca ODE parameters: k1, k2, l1, l2, dt, steps
tickInterval number|null null Auto-tick interval in ms (null = manual tick)
maxPlayers number 20 Maximum allowed players
minPlayers number 1 Minimum players required to start

ObsConfig

Controls what each agent perceives. Applied to all sweep points unless a point provides its own _obs override. Each entry has the same structure:

"<variable>": {
  "visible":    true | false,
  "noise":      0.0,            // Gaussian σ as fraction of true value (0 = exact)
  "resolution": "exact"         // "exact" | "bucket" | "sign"
}

Available variables:

Variable Default visible Notes
commonsStock true Commons stock \(S_t\)
regenRate false Regeneration rate (hidden by default — must be inferred)
nPlayers true Number of players in the arena
othersWealth true Other players' current wealth
othersHarvest true Other players' last harvest
capitalStock true Seneca engine only: capital \(C_t\)
pollutionLevel false Seneca engine only: pollution \(P_t\) (hidden by default)

resolution values:

  • "exact" — full numeric value (after noise)
  • "bucket" — maps to "low" / "medium" / "high" (requires max to be inferrable from capacity)
  • "sign" — returns only the direction of change (+1 / -1 / 0)

AgentSpec

Each entry in baseAgents describes one player. The id field is used by agentOverrides to patch individual agents per sweep point.

Field Type Required Description
id string yes Stable identifier for override targeting
agentType string yes "builtin" / "http" / "code"
strategy string builtin only aggressive / conservative / adaptive / random / rcp
privateResource number no Starting wealth (default: 30)
displayName string no Label shown in reports (defaults to strategy name)
strategyParams object no Strategy-specific tuning (see below)
callbackUrl string http only Endpoint for HTTP agents
strategyCode string code only JavaScript decide(obs, memory) function source

strategyParams

Strategy Parameter Type Default Effect
aggressive intensity number ∈ (0, 1] 0.5 Fraction of visible stock requested per turn
adaptive reductionFactor number ≥ 0 1.0 Scales the adaptive harvest cut; 0 = no adaptation, >1 = over-reactive

SweepPoint

Each point is one configuration on the sweep axis. The runner runs runsPerPoint independent replicates and aggregates them.

Field Type Description
label string Human-readable point label (appears in logs and result JSON)
axisValue number Numeric value on the sweep axis (used for plotting)
configOverride object Fields to merge into baseConfig for this point
agentOverrides array Patches applied to baseAgents by id (partial update)
_agents array Replaces the entire agent list for this point
_obs object Replaces the entire observability config for this point
_perturbations array Replaces perturbations for this point (empty [] = no perturbations)

Override precedence (highest wins):

point._agents       > baseAgents + agentOverrides
point._obs          > definition-level observability
point._perturbations > definition-level perturbations
point.configOverride > baseConfig

PLAYER_N alias

In _perturbations, the payload.playerId field accepts the alias "PLAYER_0", "PLAYER_1", … (zero-indexed position in the agent list) in addition to the actual UUID assigned at runtime. This makes definitions portable across runs.


Perturbation

Perturbations fire mid-game based on a trigger condition.

{
  "id":          "unique-id",
  "type":        "strategy_override",
  "trigger":     { "kind": "turn", "turn": 15 },
  "payload":     { "playerId": "PLAYER_0", "strategy": "aggressive" },
  "repeatable":  false,
  "description": "TheMule defects at turn 15"
}

Trigger kinds:

kind Extra fields Fires when
turn turn: N Arena turn equals N
threshold variable, below or above Metric crosses the threshold
manual Fired explicitly via API; never fires from definitions

variable options for threshold triggers: commonsRatio, commonsStock, gini, welfareScore.

Perturbation types:

type Payload fields Effect
regen_shock factor Multiply regenRate by factor
regen_set value Set regenRate to exact value
commons_shock delta Add delta to commons stock (negative = deplete)
capacity_shock factor Multiply carrying capacity by factor
wealth_shock playerId?, factor?, delta? Change player wealth (null playerId → all players)
strategy_override playerId, strategy Replace a builtin agent's strategy mid-game
observability_flip variable, visible Toggle a variable's visibility mid-game
threshold_shift newThreshold Move the collapse threshold

metrics

A list of dotted-path strings into the arena's fullSummary() object. The aggregator computes mean, min, max, and standard deviation across replicates.

Common values:

Path Description
turns Number of turns played
metrics.gini Gini coefficient of final wealth distribution
metrics.welfareScore Sum of all players' final wealth
metrics.meanWealth Mean final wealth
metrics.totalWealth Total wealth accumulated
metrics.commonsRatio Final commons stock / capacity

Annotated Example

The following is experiments/definitions/mule_strategy_override.json, fully annotated. It sweeps the turn at which a cooperative agent defects (converts to aggressive), implementing the "Mule" experiment (P11).

{
  // Unique name — becomes the filename prefix for saved results.
  "name": "mule_strategy_override",

  // One-line description shown in logs and the experiment list.
  "description": "The Mule: cooperative agent switches to aggressive at different turns.",

  // Commons model — shared across all sweep points.
  "baseConfig": {
    "commonsInitial":  150,
    "commonsCapacity": 150,
    "regenRate":       0.14,    // slightly higher than default to allow longer games
    "maxTurns":        60,
    "threshold":       10,
    "scheduler":       "simultaneous"
  },

  // What each agent perceives — applies to all points (no _obs overrides here).
  "observability": {
    "commonsStock":  { "visible": true,  "noise": 0 },
    "regenRate":     { "visible": false },
    "nPlayers":      { "visible": true,  "noise": 0 },
    "othersWealth":  { "visible": true,  "noise": 0 },
    "othersHarvest": { "visible": true,  "noise": 0 }
  },

  // Base agent pool: four conservatives, one named "TheMule".
  "baseAgents": [
    { "id": "a0", "agentType": "builtin", "strategy": "conservative",
      "privateResource": 30, "displayName": "TheMule" },
    { "id": "a1", "agentType": "builtin", "strategy": "conservative",
      "privateResource": 30, "displayName": "Cons-1" },
    { "id": "a2", "agentType": "builtin", "strategy": "conservative",
      "privateResource": 30, "displayName": "Cons-2" },
    { "id": "a3", "agentType": "builtin", "strategy": "conservative",
      "privateResource": 30, "displayName": "Cons-3" }
  ],

  "sweep": {
    "axis": "turn at which agent a0 defects (conservative → aggressive)",
    "points": [

      // Baseline: no perturbation — TheMule stays cooperative.
      {
        "label": "no defection",
        "axisValue": 999,
        "_perturbations": []        // empty list overrides any definition-level perturbations
      },

      // Point: TheMule defects at turn 5.
      {
        "label": "defect turn 5",
        "axisValue": 5,
        "_perturbations": [{
          "id":      "mule",
          "type":    "strategy_override",
          "trigger": { "kind": "turn", "turn": 5 },
          "payload": { "playerId": "PLAYER_0", "strategy": "aggressive" },
          "description": "TheMule defects at turn 5"
        }]
        // PLAYER_0 resolves to the first agent in the list (a0 = TheMule).
      }

      // ... additional points for turns 15, 25, 35, 45 ...
    ]
  },

  // Run each point 20 times with different random seeds.
  "runsPerPoint": 20,

  // Metrics to collect and aggregate across replicates.
  "metrics": [
    "turns",
    "metrics.gini",
    "metrics.welfareScore",
    "metrics.commonsRatio"
  ]
}

Writing a New Definition

  1. Copy an existing definition as a starting point.
  2. Change name and description.
  3. Adjust baseConfig to match your commons model.
  4. Define baseAgents — keep stable id values for override targeting.
  5. Define sweep.points — each point is one configuration. Use configOverride for parameter sweeps, agentOverrides for strategy composition sweeps, _perturbations for perturbation timing sweeps.
  6. Set runsPerPoint (5–10 for exploration, 20–30 for publication).
  7. Run with --runs 1 first to verify the definition parses and runs without error.
# Quick smoke test (1 run per point)
node experiments/experiment.js run my_experiment --runs 1

# Full run
node experiments/experiment.js run my_experiment