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"(requiresmaxto 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¶
- Copy an existing definition as a starting point.
- Change
nameanddescription. - Adjust
baseConfigto match your commons model. - Define
baseAgents— keep stableidvalues for override targeting. - Define
sweep.points— each point is one configuration. UseconfigOverridefor parameter sweeps,agentOverridesfor strategy composition sweeps,_perturbationsfor perturbation timing sweeps. - Set
runsPerPoint(5–10 for exploration, 20–30 for publication). - Run with
--runs 1first to verify the definition parses and runs without error.