Skip to content

Quickstart — Play with an AI

You have an LLM, an RL model, or any program that can read a JSON observation and emit a number. You want it to play BDPD against the built-in heuristics, in an Arena you control. This guide walks the shortest path from zero to first move, in about 10 minutes.

Choose a path

If you have ... Best path Time
an OpenAI-compatible API key (OpenAI, DeepSeek, Together, ...) Path A — middle-man Python 5 min
a local llama.cpp / Ollama / vLLM endpoint Path A — middle-man Python 5 min
an RL model in PyTorch / TensorFlow / JAX Path B — your own HTTP agent 10 min
a tiny rule-based bot, anything else Path C — sandboxed JS code agent 2 min

All three paths plug into the same arena: BDPD only needs an HTTP endpoint (or, for Path C, a small JS function) that answers POST /decide → { harvest: number }. Everything else is your decision.

A Flask server, already in the repo as agents/bdpd_agent.py, sits between BDPD and an OpenAI-compatible API. It handles conversation history, archetype nudges, fallback on parse failure.

# 1. start the BDPD platform server (Terminal 1)
node main.js                                    # listens on :3000

# 2. start the middle-man Python agent (Terminal 2)
source venv_py_bdpd/bin/activate
python agents/bdpd_agent.py \
    --port 5001 \
    --model-url https://api.deepseek.com \
    --model-name deepseek-v4-flash \
    --api-key-env DEEPSEEK_API_KEY \
    --archetype conservative \
    --temperature 0.4

# 3. create an arena (Terminal 3)
curl -X POST http://localhost:3000/api/v1/arenas \
  -H "Content-Type: application/json" \
  -d @- <<'JSON'
{
  "model":   { "name": "first-run", "engineMode": "logistic", "maxPlayers": 2, "minPlayers": 2 },
  "ownerKey":  "owner_demo",
  "arenaKey":  "arena_demo",
  "spectatorToken": "spec_demo"
}
JSON

# 4. attach the LLM agent (uses the arenaKey returned above)
curl -X POST "http://localhost:3000/api/v1/arenas/<arenaId>/join" \
  -H "X-Arena-Key: arena_demo" \
  -H "Content-Type: application/json" \
  -d '{ "agentType": "http", "displayName": "Claude", "privateResource": 30,
        "callbackUrl": "http://localhost:5001/decide" }'

# 5. add a built-in opponent and start
curl -X POST "http://localhost:3000/api/v1/arenas/<arenaId>/join" \
  -H "X-Arena-Key: arena_demo" \
  -H "Content-Type: application/json" \
  -d '{ "agentType": "builtin", "strategy": "aggressive",
        "displayName": "Bot",  "privateResource": 30 }'

curl -X POST "http://localhost:3000/api/v1/arenas/<arenaId>/start" \
  -H "X-Owner-Key: owner_demo"

Open http://localhost:3000/arena.html?id=<arenaId>&token=spec_demo in a browser to watch the live dashboard.

For other providers, swap --model-url, --model-name, --api-key-env. The middle-man supports any OpenAI-compatible API out of the box.

Path B — your own HTTP agent

BDPD only needs an endpoint that answers POST /decide. The minimal contract:

POST /decide
Content-Type: application/json
Body:
  {
    "playerId":  "<uuid>",
    "observation": {
      "turn":             <int>,
      "myWealth":         <float>,
      "myLastHarvest":    <float>,
      "commonsStock":     <float or null>,
      "commonsRatio":     <float or null>,
      "regenRate":        <float or null>,
      "nPlayers":         <int>,
      "othersWealth":     [{"id": "...", "wealth": <float>}, ...] | null,
      "othersLastHarvest":[{"id": "...", "harvest": <float>}, ...] | null
    }
  }
Response:
  {
    "harvest": <float, non-negative>
  }

The simplest Python skeleton, no dependencies beyond Flask:

from flask import Flask, request, jsonify
app = Flask(__name__)

@app.post('/decide')
def decide():
    obs = request.get_json()['observation']
    stock = obs.get('commonsStock') or 0
    capacity = 1.0 + 0.05 * obs['myWealth']
    # Toy policy: harvest 30% of capacity, scaled by perceived health
    target = capacity * 0.3 * (stock / 150.0)
    return jsonify({'harvest': max(0.0, target)})

app.run(port=5002)

Same arena spec as Path A; just point callbackUrl at your server.

Path C — sandboxed JS code agent

Skip the HTTP server entirely: write a JS function as a string and POST it. The function runs in a sandbox with a 50ms timeout per turn and gets a persistent memory object.

curl -X POST "http://localhost:3000/api/v1/arenas/<arenaId>/join" \
  -H "X-Arena-Key: arena_demo" \
  -H "Content-Type: application/json" \
  -d '{"agentType":"code","displayName":"SimpleBot","privateResource":30,"strategyCode":"function decide(o, m) { return Math.max(0.5, (o.myCapacity || 1) * 0.3); }"}'

Keep the -d argument on a single line. Some terminals — notably zsh with bracketed paste — insert literal newlines inside long quoted strings on paste, which Express then rejects with a JSON parse error on a control character. The one-line form above is safe to copy.

The memory argument (m) persists across turns within a single arena run. It's private — not shared with other agents, not exposed by the API. For richer strategies that exploit memory, save the function in a .js file and pass it as a JSON-escaped string:

cat > strategy.js <<'JS'
function decide(o, m) {
  if (!m.h) m.h = [];
  m.h.push(o.commonsStock || 0);
  if (m.h.length > 5) m.h.shift();
  const trend = m.h.length < 2 ? 0 : m.h[m.h.length - 1] - m.h[0];
  return Math.max(0.3, (o.myCapacity || 1) * 0.4 + trend * 0.05);
}
JS

node -e '
  const fs = require("fs");
  const body = {
    agentType: "code", displayName: "TrendBot", privateResource: 30,
    strategyCode: fs.readFileSync("strategy.js", "utf8")
  };
  fs.writeFileSync("join.json", JSON.stringify(body));
'

curl -X POST "http://localhost:3000/api/v1/arenas/<arenaId>/join" \
  -H "X-Arena-Key: arena_demo" \
  -H "Content-Type: application/json" \
  -d @join.json

Errors thrown inside the sandbox are caught: the arena keeps running and a fallback harvest is applied for that turn. Inspect the per-turn trace via GET /api/v1/arenas/<arenaId>/owner to see the decisions[].error field if your code is silently misbehaving.

Where to go next

  • Tune your prompt or policy by inspecting the per-turn trace at GET /api/v1/arenas/<arenaId>/owner (owner key required), which returns the full state including each agent's last observation and decision.
  • Run statistical sweeps instead of single arenas: see Design an experiment.
  • Plug in custom resource dynamics (engine), perturbations, metrics, or victory functions: see Extend the engine.
  • Audit provenance: every sweep writes a manifest sibling (<results>.manifest.json) with content-addressed hashes; verify it with node tools/manifest.mjs verify <manifest>. See the v0.6.5 notes for figure-side provenance.