From 217b3ce51d31ba9ea934d8012942a48f11c38c8a Mon Sep 17 00:00:00 2001 From: jedarden Date: Sun, 29 Mar 2026 05:20:09 -0400 Subject: [PATCH] Add public match data documentation page (Phase 10) - New /docs/api route with OpenAPI-style endpoint 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 Co-Authored-By: Claude Opus 4.6 --- PROGRESS.md | 18 +- web/src/app.ts | 8 + web/src/pages/docs-api.ts | 752 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 776 insertions(+), 2 deletions(-) create mode 100644 web/src/pages/docs-api.ts 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 {

Example Bot

See the example bots in various languages for reference implementations.

+ +
+

Data & API

+

All match data (leaderboards, replays, bot profiles) is exposed as static JSON files served from CDN.

+

View API Reference

+
diff --git a/web/src/pages/docs-api.ts b/web/src/pages/docs-api.ts new file mode 100644 index 0000000..f73ab06 --- /dev/null +++ b/web/src/pages/docs-api.ts @@ -0,0 +1,752 @@ +// API Documentation - Static JSON Endpoints +// OpenAPI-style documentation for all public data endpoints + +interface EndpointDoc { + method: string; + path: string; + description: string; + cache: string; + responseExample?: string; + schemaLink?: string; +} + +interface Section { + title: string; + description: string; + endpoints: EndpointDoc[]; +} + +const PAGES_BASE = 'https://ai-code-battle.pages.dev'; +const R2_BASE = 'https://r2.aicodebattle.com'; +const B2_BASE = 'https://b2.aicodebattle.com'; + +const sections: Section[] = [ + { + title: 'Pages Endpoints (Pre-computed JSON)', + description: 'All data on Pages is pre-computed by the index builder and deployed every ~90 minutes. These files are served from the Cloudflare CDN with automatic cache invalidation on deploy.', + endpoints: [ + { + method: 'GET', + path: '/data/leaderboard.json', + description: 'Current leaderboard with ratings, win rates, and health status for all registered bots.', + cache: '~90 min (deploy cycle)', + responseExample: `{ + "updated_at": "2026-03-29T12:00:00Z", + "season": "season-1", + "entries": [ + { + "rank": 1, + "bot_id": "bot_abc123", + "name": "SwarmBot", + "owner_id": "user_xyz", + "rating": 1847.3, + "rating_deviation": 42.1, + "matches_played": 152, + "matches_won": 98, + "win_rate": 0.645, + "health_status": "active" + } + ] +}`, + }, + { + method: 'GET', + path: '/data/bots/index.json', + description: 'Bot directory listing all registered bots with summary stats.', + cache: '~90 min (deploy cycle)', + responseExample: `{ + "updated_at": "2026-03-29T12:00:00Z", + "bots": [ + { + "id": "bot_abc123", + "name": "SwarmBot", + "rating": 1847.3, + "matches_played": 152, + "win_rate": 0.645 + } + ] +}`, + }, + { + method: 'GET', + path: '/data/bots/{bot_id}.json', + description: 'Full bot profile including rating history and recent matches. For evolved bots, includes lineage information.', + cache: '~90 min (deploy cycle)', + responseExample: `{ + "id": "bot_abc123", + "name": "SwarmBot", + "owner_id": "user_xyz", + "rating": 1847.3, + "rating_deviation": 42.1, + "rating_volatility": 0.06, + "matches_played": 152, + "matches_won": 98, + "win_rate": 0.645, + "health_status": "active", + "created_at": "2026-01-15T08:00:00Z", + "updated_at": "2026-03-29T12:00:00Z", + "rating_history": [...], + "recent_matches": [...], + "evolved": false +}`, + }, + { + method: 'GET', + path: '/data/matches/index.json', + description: 'Paginated index of recent matches with participants and results.', + cache: '~90 min (deploy cycle)', + responseExample: `{ + "updated_at": "2026-03-29T12:00:00Z", + "matches": [ + { + "id": "match_xyz789", + "completed_at": "2026-03-29T11:45:00Z", + "participants": [ + {"bot_id": "bot_abc123", "name": "SwarmBot", "score": 8, "won": true}, + {"bot_id": "bot_def456", "name": "HunterBot", "score": 5, "won": false} + ], + "winner_id": "bot_abc123", + "turns": 247, + "end_reason": "dominance" + } + ], + "pagination": {"page": 1, "per_page": 50, "total": 1250} +}`, + }, + { + method: 'GET', + path: '/data/playlists/index.json', + description: 'Index of all curated replay playlists (featured, upsets, rivalries, etc.).', + cache: '~90 min (deploy cycle)', + responseExample: `{ + "updated_at": "2026-03-29T12:00:00Z", + "playlists": [ + { + "slug": "featured", + "title": "Featured Matches", + "description": "Editor's picks for the most exciting matches", + "category": "featured", + "match_count": 25, + "updated_at": "2026-03-29T10:00:00Z" + } + ] +}`, + }, + { + method: 'GET', + path: '/data/playlists/{slug}.json', + description: 'Full playlist with match IDs in viewing order.', + cache: '~90 min (deploy cycle)', + responseExample: `{ + "slug": "featured", + "title": "Featured Matches", + "description": "Editor's picks", + "category": "featured", + "match_count": 25, + "created_at": "2026-01-15T00:00:00Z", + "updated_at": "2026-03-29T10:00:00Z", + "matches": [ + {"match_id": "match_xyz789", "order": 1, "title": "Epic Comeback"} + ] +}`, + }, + { + method: 'GET', + path: '/data/blog/index.json', + description: 'Blog post index with summaries. Includes weekly meta reports and narrative chronicles.', + cache: '~90 min (deploy cycle)', + responseExample: `{ + "updated_at": "2026-03-29T12:00:00Z", + "posts": [ + { + "slug": "meta-week-13-season-1", + "title": "Week 13 Meta Report: Rise of the Swarm", + "published_at": "2026-03-29T09:00:00Z", + "summary": "SwarmBot dominance continues..." + } + ] +}`, + }, + { + method: 'GET', + path: '/data/blog/{slug}.json', + description: 'Full blog post with HTML content and weekly stats.', + cache: '~90 min (deploy cycle)', + responseExample: `{ + "slug": "meta-week-13-season-1", + "title": "Week 13 Meta Report", + "published_at": "2026-03-29T09:00:00Z", + "week_start": "2026-03-23", + "summary": "SwarmBot dominance...", + "body_html": "

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 = ` +
+ + +

API Reference

+ +

+ 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. +

+ +
+

Quick Navigation

+ +
+ + ${sections.map(renderSection).join('')} + +
+

Replay JSON Schema

+

The replay format is versioned. The current version is v1.

+
${escapeHtml(replaySchema)}
+
+ +
+

Recommended Fetching Pattern

+

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());
+}
+ +

Cache Behavior

+
    +
  • Pages: ~90 min stale max (deploy cycle)
  • +
  • R2 replays: immutable, cache forever
  • +
  • R2 live.json: 10 second max-age
  • +
  • B2: immutable, cache forever
  • +
+ +

Rate Limits

+

There are no rate limits on static file access. The CDN handles unlimited concurrent requests.

+
+ + +
+ `; +} + +function renderSection(section: Section): string { + return ` +
+

${section.title}

+

${section.description}

+ ${section.endpoints.map(e => renderEndpoint(e, section.title)).join('')} +
+ `; +} + +function renderEndpoint(endpoint: EndpointDoc, sectionTitle: string): string { + let baseUrl = ''; + if (sectionTitle.includes('R2')) baseUrl = R2_BASE; + else if (sectionTitle.includes('B2')) baseUrl = B2_BASE; + else baseUrl = PAGES_BASE; + + return ` +
+
+ ${endpoint.method} + ${baseUrl}${endpoint.path} +
+

${endpoint.description}

+

Cache: ${endpoint.cache}

+ ${endpoint.responseExample ? `
${escapeHtml(endpoint.responseExample)}
` : ''} + ${endpoint.schemaLink ? `

View Schema

` : ''} +
+ `; +} + +function slugify(text: string): string { + return text.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, ''); +} + +function escapeHtml(text: string): string { + return text + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +}