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.
Path A — middle-man Python (recommended for LLMs)¶
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 withnode tools/manifest.mjs verify <manifest>. See the v0.6.5 notes for figure-side provenance.