diff --git a/web/src/app.ts b/web/src/app.ts index 8492667..3b9572f 100644 --- a/web/src/app.ts +++ b/web/src/app.ts @@ -69,6 +69,9 @@ const loadSandboxPage = () => import('./pages/sandbox').then(m => m.renderSandbo const loadRegisterPage = () => import('./pages/register').then(m => m.renderRegisterPage); const loadCompeteHubPage = () => import('./pages/compete-hub').then(m => m.renderCompeteHubPage); const loadDocsPage = () => import('./pages/docs').then(m => m.renderDocsPage); +// Public data documentation pages (§15.2) +const loadDocsReplayFormatPage = () => import('./pages/docs-replay-format').then(m => m.renderDocsReplayFormatPage); +const loadDocsDataPage = () => import('./pages/docs-data').then(m => m.renderDocsDataPage); // Bot-related pages const loadBotProfilePage = () => import('./pages/bot-profile').then(m => m.renderBotProfilePage); @@ -279,6 +282,8 @@ router .on('/bots', redirect('/leaderboard')) .on('/docs/api', lazyRoute(loadDocsApiPage)) .on('/docs', redirect('/compete/docs')) + .on('/docs/replay-format', lazyRoute(loadDocsReplayFormatPage)) + .on('/docs/data', lazyRoute(loadDocsDataPage)) .on('/clip-maker', redirect('/watch/replays')) .on('/rivalries', lazyRoute(loadRivalriesPage)) .on('/feedback', lazyRoute(loadFeedbackPage)) diff --git a/web/src/pages/docs-data.ts b/web/src/pages/docs-data.ts new file mode 100644 index 0000000..4869c59 --- /dev/null +++ b/web/src/pages/docs-data.ts @@ -0,0 +1,213 @@ +// Public Match Data Documentation Page +// §15.2: Public match data documentation - all available data paths + +export function renderDocsDataPage(): void { + const app = document.getElementById('app'); + if (!app) return; + + app.innerHTML = ` +
All platform data is available as static JSON files served from Cloudflare Pages (indexes) and Cloudflare R2 (replays, metadata). No authentication, no API keys, no rate limiting.
+Base URLs:
+const PAGES = '' // Same origin (Cloudflare Pages)
+const R2 = 'https://r2.aicodebattle.com' // Warm replay cache
+const B2 = 'https://b2.aicodebattle.com' // Cold archive
+ Updated every ~90 minutes by the index builder deployment.
+ +GET /data/leaderboard.json
+ Current rankings with ratings, win rates, and match counts.
+curl https://aicodebattle.com/data/leaderboard.json | jq
+
+ GET /data/bots/index.json # Bot directory
+GET /data/bots/{bot_id}.json # Individual bot profile
+ Rating history, recent matches, win/loss breakdown.
+ +GET /data/matches/index.json # Last 1000 matches
+GET /data/matches/index-{page}.json # Older pages
+ Paginated match list with participants, scores, and links to replays.
+ +GET /data/series/index.json # All series
+GET /data/series/{series_id}.json # Individual series
+ Best-of-N series between two bots with game-by-game results.
+ +GET /data/seasons/index.json # All seasons
+GET /data/seasons/{season_id}.json # Season archive
+ Seasonal competition data with champions and final standings.
+ +GET /data/playlists/{slug}.json # Auto-curated collections
+ Available playlists:
+closest-finishes - Matches decided by 1 pointbiggest-upsets - Lower-rated bot winsbest-comebacks - Recovered from low win probabilityrivalry-classics - Matches between rivalsseason-highlights - Best matches of current seasonGET /data/meta/archetypes.json # Strategy archetype distribution
+GET /data/meta/rivalries.json # Detected rivalries
+
+ GET /data/evolution/lineage.json # Bot ancestry graph
+GET /data/evolution/meta.json # Current meta snapshot
+
+ GET /data/blog/index.json # All posts
+GET /data/blog/posts/{slug}.json # Individual post
+
+ GET /maps/index.json # Map directory
+GET /maps/{map_id}.json # Individual map definition
+ Uploaded in real-time by match workers. R2 is a warm cache for recent replays; B2 is the permanent cold archive.
+ +GET /replays/{match_id}.json.gz
+ Gzipped replay JSON. Browser handles decompression automatically.
+# Fetch from R2 (warm cache)
+curl https://r2.aicodebattle.com/replays/m_7f3a9b2c.json.gz
+
+# Fallback to B2 (cold archive)
+curl https://b2.aicodebattle.com/replays/m_7f3a9b2c.json.gz
+
+ GET /matches/{match_id}.json
+ Per-match metadata including win probability curve and critical moments.
+ +GET /thumbnails/{match_id}.png
+GET /cards/{bot_id}.png
+ Auto-generated images for social sharing.
+| Data Type | Update Frequency | Source |
|---|---|---|
| Leaderboard | Every ~90 min | Index builder → Pages |
| Bot profiles | Every ~90 min | Index builder → Pages |
| Match index | Every ~90 min | Index builder → Pages |
| Playlists | Every ~90 min | Index builder → Pages |
| Replays | Real-time | Match worker → R2/B2 |
| Match metadata | Real-time | Match worker → R2/B2 |
| Evolution data | Every cycle (~15 min) | Evolver → R2 live.json |
Pages (indexes): Deployed every ~90 minutes. Cached by Cloudflare CDN globally. Invalidated on deploy.
+R2 (replays): Served with Cache-Control: immutable, max-age=31536000 (content-addressed, never changes).
B2 (archive): Same cache headers as R2. Free egress via Cloudflare Bandwidth Alliance.
+// SPA shell + index data from Pages (same origin)
+const leaderboard = await fetch('/data/leaderboard.json').then(r => r.json())
+
+// Replay from R2 warm cache, with B2 fallback
+async function fetchReplay(matchId) {
+ const r2 = await fetch(\`https://r2.aicodebattle.com/replays/\${matchId}.json.gz\`)
+ if (r2.ok) return r2
+ return fetch(\`https://b2.aicodebattle.com/replays/\${matchId}.json.gz\`)
+}
+ curl https://aicodebattle.com/data/leaderboard.json | jq '.entries[:5]'
+ Returns top 5 bots with ratings and stats.
+curl https://aicodebattle.com/data/bots/b_swarmbot.json | jq '{name, rating, matches}'
+ curl https://aicodebattle.com/data/matches/index.json | jq '.matches[:3] | .[] | {match_id, players}'
+ curl https://aicodebattle.com/data/playlists/closest-finishes.json | jq '.matches[] | .match_id'
+ AI Code Battle replays are JSON files containing the complete state of a match. Each replay includes the initial configuration, map data, and turn-by-turn events that allow the game to be reconstructed and visualized.
+Version: v1 (additive changes only - see Changelog below)
+Replays are served from Cloudflare R2 (warm cache) with B2 (cold archive) fallback:
+# Try warm cache first
+curl https://r2.aicodebattle.com/replays/\${match_id}.json.gz
+
+# Fallback to cold archive
+curl https://b2.aicodebattle.com/replays/\${match_id}.json.gz
+ Replays are gzip-compressed. The browser handles decompression automatically when you fetch with Accept-Encoding: gzip.
Download the JSON Schema: replay-schema-v1.json
+Use the schema to validate replays programmatically:
+# Validate a replay file
+ajv validate -s replay-schema-v1.json -d replay.json
+ {
+ "version": 1, // Replay format version
+ "match_id": "m_7f3a9b2c", // Unique match identifier
+ "date": "2026-03-23T14:30:00Z", // Match completion time
+ "players": [...], // Player metadata
+ "result": {...}, // Final scores and winner
+ "config": {...}, // Match configuration
+ "map": {...}, // Map definition
+ "turns": [...] // Turn-by-turn events
+}
+ "players": [
+ {
+ "bot_id": "b_4e8c1d2f",
+ "name": "SwarmBot",
+ "owner": "alice",
+ "color": "#2196F3"
+ }
+]
+ | Field | Type | Description |
|---|---|---|
| bot_id | string | Unique bot identifier |
| name | string | Bot display name |
| owner | string | Bot owner's username |
| color | string | Player color for visualization |
"result": {
+ "winner": 0, // Winning player index
+ "condition": "turn_limit", // Win condition
+ "final_scores": [7, 3], // Final scores per player
+ "final_energy": [12, 4], // Final energy held
+ "final_bots": [18, 6] // Final bot count
+}
+ Win Conditions:
+sole_survivor - Only one player has living botsannihilation - All players eliminated simultaneouslydominance - One player controls ≥80% of bots for 100 turnsturn_limit - Turn limit reached (default: 500)"config": {
+ "rows": 60,
+ "cols": 60,
+ "max_turns": 500,
+ "vision_radius2": 49, // Squared vision radius (~7 tiles)
+ "attack_radius2": 12, // Squared attack radius
+ "spawn_cost": 3, // Energy to spawn a bot
+ "energy_interval": 10 // Turns between energy spawns
+}
+ Seasonal variations may introduce optional fields (see Changelog). Bots that don't read new fields continue to work.
+"map": {
+ "walls": [[10,10], [10,11], [10,12]], // Wall positions
+ "energy_nodes": [[20,25], [40,35]], // Energy spawn locations
+ "cores": [ // Starting cores
+ {"pos": [5,5], "owner": 0},
+ {"pos": [55,55], "owner": 1}
+ ]
+}
+ Each turn contains events that occurred during that turn:
+"turns": [
+ {
+ "moves": { // Moves made by each player
+ "0": [{"from": [10,15], "dir": "N"}],
+ "1": [{"from": [50,45], "dir": "S"}]
+ },
+ "spawns": [[5,5,0]], // New bots spawned
+ "deaths": [[30,40,1]], // Bots that died
+ "captures": [], // Cores captured
+ "energy_collected": { // Energy gathered
+ "0": [[20,25]]
+ },
+ "energy_spawned": [[35,15]], // New energy appeared
+ "scores": [3, 1], // Scores after turn
+ "events": [ // Detailed events
+ {
+ "type": "combat_death",
+ "turn": 6,
+ "details": {
+ "bot_id": 0,
+ "owner": 0,
+ "position": {"row": 30, "col": 40},
+ "killers": [
+ {"bot_id": 1, "owner": 1, "position": {"row": 28, "col": 42}}
+ ]
+ }
+ }
+ ]
+ }
+]
+ | Type | Description |
|---|---|
bot_spawned | A new bot was spawned |
bot_died | A bot died (legacy, no killer info) |
combat_death | A bot died from focus-fire combat (includes killers[] array) |
collision_death | Two bots moved to the same tile |
zone_death | A bot was killed by the shrinking zone |
energy_collected | Energy was gathered |
core_captured | An enemy core was razed |
Some replays include a win probability curve computed via Monte Carlo rollout:
+"win_prob": [
+ [0.50, 0.50], // Turn 0: even odds
+ [0.51, 0.49], // Turn 1: slight edge to player 0
+ ...
+]
+ Array of [player_0_prob, player_1_prob, ...] for each turn.
+Turns where win probability shifted significantly (>15%):
+"critical_moments": [
+ {
+ "turn": 87,
+ "delta": 0.22,
+ "description": "SwarmBot loses 6 units in eastern engagement"
+ }
+]
+ Backward Compatibility Policy: Future versions will only add optional fields. Existing fields will never be removed or renamed. Bots that don't read new fields continue to function.
+Download example replays to test your visualization:
+ +