diff --git a/PROGRESS.md b/PROGRESS.md index 6cae1df..088802f 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -4,9 +4,18 @@ **Status: 🔄 In Progress** -**Last Updated: 2026-03-29** (Live evolution observatory) +**Last Updated: 2026-03-29** (Public API documentation) ### Recent Changes (2026-03-29) +- **Phase 10 Public Match Data Documentation** (`web/src/pages/docs-api.ts`): + - New `/docs/api` route with OpenAPI-style documentation + - Documents all Pages endpoints (leaderboard, bots, matches, playlists, blog) + - Documents R2 endpoints (live evolution, replays, thumbnails, cards) + - Documents B2 endpoints (cold archive for all data) + - Includes JSON Schema for replay format + - Recommended fetching pattern with R2-then-B2 fallback + - Cache behavior documentation for each endpoint type + - Added link from Getting Started page to API Reference - **Phase 10 Live Evolution Observatory** (`cmd/acb-evolver/internal/live/r2.go`): - R2 client for S3-compatible uploads to Cloudflare R2 - `UploadLiveJSON()` uploads evolution state to `evolution/live.json` @@ -436,7 +445,12 @@ - Cache-Control: max-age=10 for near-real-time updates - Tests for R2 config validation and credential handling - [ ] Narrative engine (weekly story arc detection + LLM chronicles) -- [ ] Public match data documentation (OpenAPI-style) +- [x] Public match data documentation (OpenAPI-style) + - New `/docs/api` route with comprehensive endpoint documentation + - Documents Pages, R2, and B2 static JSON endpoints + - Includes JSON Schema for replay format + - Fetching pattern with R2-then-B2 fallback + - Cache behavior documentation ### Phase 4 Completed diff --git a/web/src/app.ts b/web/src/app.ts index 622fc6d..8b7d313 100644 --- a/web/src/app.ts +++ b/web/src/app.ts @@ -13,6 +13,7 @@ import { renderRivalriesPage } from './pages/rivalries'; import { renderFeedbackPage } from './pages/feedback'; import { renderPlaylistsPage } from './pages/playlists'; import { renderBlogPage, renderBlogPostPage } from './pages/blog'; +import { renderDocsApiPage } from './pages/docs-api'; import { ReplayViewer } from './replay-viewer'; import type { Replay } from './types'; @@ -34,6 +35,7 @@ router .on('/blog/:slug', renderBlogPostPage) .on('/replay', renderReplayPage) .on('/docs', renderDocsPage) + .on('/docs/api', renderDocsApiPage) .notFound(renderNotFoundPage); // Update active nav link on route change @@ -675,6 +677,12 @@ function renderDocsPage(): void {
See the example bots in various languages for reference implementations.
+ +All match data (leaderboards, replays, bot profiles) is exposed as static JSON files served from CDN.
+ +This week...
", + "stats": { + "matches_played": 1520, + "top_bot": "SwarmBot", + "top_bot_rating": 1847 + } +}`, + }, + ], + }, + { + title: 'R2 Endpoints (Warm Cache)', + description: 'Recent replays and real-time data served from Cloudflare R2. Free tier capped at 10GB. Try R2 first, fall back to B2 for older data.', + endpoints: [ + { + method: 'GET', + path: '/evolution/live.json', + description: 'Real-time evolution observatory data. Updated every evolution cycle (~5 min) with Cache-Control: max-age=10.', + cache: '10 seconds', + responseExample: `{ + "updated_at": "2026-03-29T12:05:00Z", + "total_programs": 1247, + "promoted_count": 12, + "islands": { + "alpha": {"count": 312, "best_fitness": 0.85, "avg_fitness": 0.62} + }, + "generation_log": [...], + "lineage": [...], + "meta_snapshots": [...] +}`, + }, + { + method: 'GET', + path: '/replays/{match_id}.json.gz', + description: 'Compressed replay file for recent matches. Contains full turn-by-turn game state.', + cache: 'immutable (content-addressed)', + schemaLink: '#replay-schema', + }, + { + method: 'GET', + path: '/matches/{match_id}.json', + description: 'Per-match metadata including win probability curve and critical moments.', + cache: 'immutable (content-addressed)', + responseExample: `{ + "match_id": "match_xyz789", + "completed_at": "2026-03-29T11:45:00Z", + "map_id": "map_2p_001", + "config": {"rows": 60, "cols": 60}, + "participants": [...], + "result": {"winner": 0, "reason": "dominance", "turns": 247}, + "win_prob": [[0.5, 0.5], [0.52, 0.48], ...], + "critical_moments": [ + {"turn": 87, "delta": 0.22, "description": "Decisive engagement"} + ] +}`, + }, + { + method: 'GET', + path: '/cards/{bot_id}.png', + description: 'Canvas-rendered bot profile card image (1200x630) for Open Graph social sharing.', + cache: 'max-age=86400 (1 day)', + }, + { + method: 'GET', + path: '/thumbnails/{match_id}.png', + description: 'Auto-generated match thumbnail for embed previews.', + cache: 'max-age=86400 (1 day)', + }, + ], + }, + { + title: 'B2 Endpoints (Cold Archive)', + description: 'Permanent archive for ALL replays and match data. Free egress via Cloudflare Bandwidth Alliance. Use as fallback when R2 returns 404.', + endpoints: [ + { + method: 'GET', + path: '/replays/{match_id}.json.gz', + description: 'Compressed replay file. All replays are archived permanently on B2.', + cache: 'immutable (content-addressed)', + schemaLink: '#replay-schema', + }, + { + method: 'GET', + path: '/matches/{match_id}.json', + description: 'Per-match metadata. Same structure as R2 endpoint.', + cache: 'immutable (content-addressed)', + }, + { + method: 'GET', + path: '/cards/{bot_id}.png', + description: 'Bot profile card images. All cards archived permanently.', + cache: 'immutable', + }, + { + method: 'GET', + path: '/thumbnails/{match_id}.png', + description: 'Match thumbnails. All thumbnails archived permanently.', + cache: 'immutable', + }, + ], + }, +]; + +// Replay JSON Schema section +const replaySchema = `{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://aicodebattle.com/schemas/replay.json", + "title": "Match Replay", + "description": "Complete replay of an AI Code Battle match", + "type": "object", + "required": ["match_id", "config", "map", "players", "turns", "result"], + "properties": { + "match_id": { + "type": "string", + "description": "Unique match identifier" + }, + "config": { + "$ref": "#/definitions/GameConfig" + }, + "map": { + "$ref": "#/definitions/Map" + }, + "players": { + "type": "array", + "items": {"$ref": "#/definitions/Player"} + }, + "turns": { + "type": "array", + "items": {"$ref": "#/definitions/Turn"} + }, + "result": { + "$ref": "#/definitions/Result" + }, + "win_prob": { + "type": "array", + "description": "Per-turn win probability for each player (computed post-match)", + "items": { + "type": "array", + "items": {"type": "number"} + } + }, + "critical_moments": { + "type": "array", + "description": "Turns with significant win probability shifts", + "items": { + "type": "object", + "properties": { + "turn": {"type": "integer"}, + "delta": {"type": "number"}, + "description": {"type": "string"} + } + } + } + }, + "definitions": { + "GameConfig": { + "type": "object", + "properties": { + "rows": {"type": "integer", "minimum": 30, "maximum": 120}, + "cols": {"type": "integer", "minimum": 30, "maximum": 120}, + "max_turns": {"type": "integer", "default": 500}, + "vision_radius2": {"type": "integer", "default": 49}, + "attack_radius2": {"type": "integer", "default": 5}, + "spawn_cost": {"type": "integer", "default": 3}, + "energy_interval": {"type": "integer", "default": 10} + } + }, + "Position": { + "type": "object", + "properties": { + "row": {"type": "integer"}, + "col": {"type": "integer"} + } + }, + "Map": { + "type": "object", + "properties": { + "rows": {"type": "integer"}, + "cols": {"type": "integer"}, + "walls": { + "type": "array", + "items": {"$ref": "#/definitions/Position"} + }, + "cores": { + "type": "array", + "items": { + "type": "object", + "properties": { + "position": {"$ref": "#/definitions/Position"}, + "owner": {"type": "integer"} + } + } + }, + "energy_nodes": { + "type": "array", + "items": {"$ref": "#/definitions/Position"} + } + } + }, + "Player": { + "type": "object", + "properties": { + "id": {"type": "integer"}, + "name": {"type": "string"}, + "bot_id": {"type": "string"} + } + }, + "Turn": { + "type": "object", + "properties": { + "turn": {"type": "integer"}, + "bots": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": {"type": "integer"}, + "owner": {"type": "integer"}, + "position": {"$ref": "#/definitions/Position"}, + "alive": {"type": "boolean"} + } + } + }, + "cores": { + "type": "array", + "items": { + "type": "object", + "properties": { + "position": {"$ref": "#/definitions/Position"}, + "owner": {"type": "integer"}, + "active": {"type": "boolean"} + } + } + }, + "energy": { + "type": "array", + "items": {"$ref": "#/definitions/Position"}, + "description": "Energy positions visible this turn" + }, + "scores": { + "type": "array", + "items": {"type": "integer"} + }, + "energy_held": { + "type": "array", + "items": {"type": "integer"} + }, + "events": { + "type": "array", + "items": { + "type": "object", + "properties": { + "type": {"type": "string", "enum": [ + "bot_spawned", "bot_died", "energy_collected", + "core_captured", "combat_death", "collision_death" + ]}, + "turn": {"type": "integer"}, + "details": {"type": "object"} + } + } + } + } + }, + "Result": { + "type": "object", + "properties": { + "winner": {"type": "integer", "description": "Player index, -1 for draw"}, + "reason": {"type": "string", "enum": [ + "sole_survivor", "dominance", "turn_limit", "annihilation" + ]}, + "turns": {"type": "integer"}, + "scores": {"type": "array", "items": {"type": "integer"}}, + "energy": {"type": "array", "items": {"type": "integer"}}, + "bots_alive": {"type": "array", "items": {"type": "integer"}} + } + } + } +}`; + +export function renderDocsApiPage(): void { + const app = document.getElementById('app'); + if (!app) return; + + app.innerHTML = ` ++ All match data is exposed as static JSON files. There is no live API for data access — + everything is pre-computed and served from CDN. This architecture enables unlimited + read scale with zero server cost. +
+ +The replay format is versioned. The current version is v1.
${escapeHtml(replaySchema)}
+ For replays and match metadata, always try R2 first and fall back to B2:
+async function fetchReplay(matchId: string): Promise {
+ // Try R2 warm cache first
+ const r2Url = \`https://r2.aicodebattle.com/replays/\${matchId}.json.gz\`;
+ const r2Resp = await fetch(r2Url);
+ if (r2Resp.ok) {
+ return decompress(await r2Resp.arrayBuffer());
+ }
+
+ // Fall back to B2 cold archive
+ const b2Url = \`https://b2.aicodebattle.com/replays/\${matchId}.json.gz\`;
+ const b2Resp = await fetch(b2Url);
+ if (!b2Resp.ok) throw new Error(\`Replay not found: \${matchId}\`);
+ return decompress(await b2Resp.arrayBuffer());
+}
+
+ There are no rate limits on static file access. The CDN handles unlimited concurrent requests.
+${section.description}
+ ${section.endpoints.map(e => renderEndpoint(e, section.title)).join('')} +${baseUrl}${endpoint.path}
+ ${endpoint.description}
+Cache: ${endpoint.cache}
+ ${endpoint.responseExample ? `${escapeHtml(endpoint.responseExample)}` : ''}
+ ${endpoint.schemaLink ? `` : ''}
+