feat(web): add /docs/replay-format and /docs/data documentation pages

Implements plan §15.2 public match data documentation:

- /docs/replay-format: Complete replay schema v1 specification
  with field-by-field documentation, event types, win probability
  and critical moments format, example replays, and changelog

- /docs/data: Comprehensive guide to all public data endpoints
  including leaderboard, bots, matches, series, seasons, playlists,
  meta, evolution, blog posts, and maps with update frequencies
  and curl examples

- Added lazy loaders and routes for both pages in app.ts

Closes: bf-ckcj

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
jedarden 2026-05-26 13:01:57 -04:00
parent 149fbe4edf
commit d27aafc532
3 changed files with 437 additions and 0 deletions

View file

@ -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))

213
web/src/pages/docs-data.ts Normal file
View file

@ -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 = `
<div class="docs-page">
<h1 class="page-title">Public Match Data</h1>
<div class="docs-content">
<section>
<h2>Overview</h2>
<p>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.</p>
<p><strong>Base URLs:</strong></p>
<pre><code>const PAGES = '' // Same origin (Cloudflare Pages)
const R2 = 'https://r2.aicodebattle.com' // Warm replay cache
const B2 = 'https://b2.aicodebattle.com' // Cold archive</code></pre>
</section>
<section>
<h2>Index Files (Cloudflare Pages)</h2>
<p>Updated every ~90 minutes by the index builder deployment.</p>
<h3>Leaderboard</h3>
<pre><code>GET /data/leaderboard.json</code></pre>
<p>Current rankings with ratings, win rates, and match counts.</p>
<pre><code>curl https://aicodebattle.com/data/leaderboard.json | jq</code></pre>
<h3>Bot Profiles</h3>
<pre><code>GET /data/bots/index.json # Bot directory
GET /data/bots/{bot_id}.json # Individual bot profile</code></pre>
<p>Rating history, recent matches, win/loss breakdown.</p>
<h3>Matches</h3>
<pre><code>GET /data/matches/index.json # Last 1000 matches
GET /data/matches/index-{page}.json # Older pages</code></pre>
<p>Paginated match list with participants, scores, and links to replays.</p>
<h3>Series</h3>
<pre><code>GET /data/series/index.json # All series
GET /data/series/{series_id}.json # Individual series</code></pre>
<p>Best-of-N series between two bots with game-by-game results.</p>
<h3>Seasons</h3>
<pre><code>GET /data/seasons/index.json # All seasons
GET /data/seasons/{season_id}.json # Season archive</code></pre>
<p>Seasonal competition data with champions and final standings.</p>
<h3>Playlists</h3>
<pre><code>GET /data/playlists/{slug}.json # Auto-curated collections</code></pre>
<p>Available playlists:</p>
<ul class="inline-list">
<li><code>closest-finishes</code> - Matches decided by 1 point</li>
<li><code>biggest-upsets</code> - Lower-rated bot wins</li>
<li><code>best-comebacks</code> - Recovered from low win probability</li>
<li><code>rivalry-classics</code> - Matches between rivals</li>
<li><code>season-highlights</code> - Best matches of current season</li>
</ul>
<h3>Meta</h3>
<pre><code>GET /data/meta/archetypes.json # Strategy archetype distribution
GET /data/meta/rivalries.json # Detected rivalries</code></pre>
<h3>Evolution</h3>
<pre><code>GET /data/evolution/lineage.json # Bot ancestry graph
GET /data/evolution/meta.json # Current meta snapshot</code></pre>
<h3>Blog</h3>
<pre><code>GET /data/blog/index.json # All posts
GET /data/blog/posts/{slug}.json # Individual post</code></pre>
<h3>Maps</h3>
<pre><code>GET /maps/index.json # Map directory
GET /maps/{map_id}.json # Individual map definition</code></pre>
</section>
<section>
<h2>Replay Data (Cloudflare R2)</h2>
<p>Uploaded in real-time by match workers. R2 is a warm cache for recent replays; B2 is the permanent cold archive.</p>
<h3>Replay Files</h3>
<pre><code>GET /replays/{match_id}.json.gz</code></pre>
<p>Gzipped replay JSON. Browser handles decompression automatically.</p>
<pre><code># 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</code></pre>
<h3>Match Metadata</h3>
<pre><code>GET /matches/{match_id}.json</code></pre>
<p>Per-match metadata including win probability curve and critical moments.</p>
<h3>Thumbnails & Bot Cards</h3>
<pre><code>GET /thumbnails/{match_id}.png
GET /cards/{bot_id}.png</code></pre>
<p>Auto-generated images for social sharing.</p>
</section>
<section>
<h2>Update Frequency</h2>
<table class="update-table">
<tr><th>Data Type</th><th>Update Frequency</th><th>Source</th></tr>
<tr><td>Leaderboard</td><td>Every ~90 min</td><td>Index builder Pages</td></tr>
<tr><td>Bot profiles</td><td>Every ~90 min</td><td>Index builder Pages</td></tr>
<tr><td>Match index</td><td>Every ~90 min</td><td>Index builder Pages</td></tr>
<tr><td>Playlists</td><td>Every ~90 min</td><td>Index builder Pages</td></tr>
<tr><td>Replays</td><td>Real-time</td><td>Match worker R2/B2</td></tr>
<tr><td>Match metadata</td><td>Real-time</td><td>Match worker R2/B2</td></tr>
<tr><td>Evolution data</td><td>Every cycle (~15 min)</td><td>Evolver R2 live.json</td></tr>
</table>
</section>
<section>
<h2>Cache Behavior</h2>
<p><strong>Pages (indexes):</strong> Deployed every ~90 minutes. Cached by Cloudflare CDN globally. Invalidated on deploy.</p>
<p><strong>R2 (replays):</strong> Served with <code>Cache-Control: immutable, max-age=31536000</code> (content-addressed, never changes).</p>
<p><strong>B2 (archive):</strong> Same cache headers as R2. Free egress via Cloudflare Bandwidth Alliance.</p>
</section>
<section>
<h2>Data Loading Pattern</h2>
<pre><code>// 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\`)
}</code></pre>
</section>
<section>
<h2>Example: Fetch Leaderboard</h2>
<pre><code>curl https://aicodebattle.com/data/leaderboard.json | jq '.entries[:5]'</code></pre>
<p>Returns top 5 bots with ratings and stats.</p>
</section>
<section>
<h2>Example: Fetch Bot Profile</h2>
<pre><code>curl https://aicodebattle.com/data/bots/b_swarmbot.json | jq '{name, rating, matches}'</code></pre>
</section>
<section>
<h2>Example: Fetch Match Index</h2>
<pre><code>curl https://aicodebattle.com/data/matches/index.json | jq '.matches[:3] | .[] | {match_id, players}'</code></pre>
</section>
<section>
<h2>Example: Fetch Playlist</h2>
<pre><code>curl https://aicodebattle.com/data/playlists/closest-finishes.json | jq '.matches[] | .match_id'</code></pre>
</section>
<section>
<h2>Related Documentation</h2>
<ul>
<li><a href="/docs/replay-format">Replay Format Specification</a> - Replay JSON schema</li>
<li><a href="/docs">API Documentation</a> - HTTP protocol reference</li>
<li><a href="/compete/docs">Getting Started</a> - Build your own bot</li>
</ul>
</section>
</div>
<style>
.schema-table {
width: 100%;
border-collapse: collapse;
margin: 1rem 0;
}
.schema-table th, .schema-table td {
border: 1px solid var(--border-muted);
padding: 0.5rem;
text-align: left;
}
.schema-table th {
background: var(--bg-secondary);
}
.update-table {
width: 100%;
border-collapse: collapse;
margin: 1rem 0;
}
.update-table th, .update-table td {
border: 1px solid var(--border-muted);
padding: 0.75rem;
text-align: left;
}
.update-table th {
background: var(--bg-secondary);
}
.inline-list {
display: flex;
flex-wrap: wrap;
gap: 1rem;
list-style: none;
padding: 0;
}
.inline-list li {
background: var(--bg-secondary);
padding: 0.25rem 0.5rem;
border-radius: 4px;
}
.inline-list code {
background: transparent;
padding: 0;
}
</style>
</div>
`;
}

View file

@ -0,0 +1,219 @@
// Replay Format Documentation Page
// §15.2: Public match data documentation - replay format specification
export function renderDocsReplayFormatPage(): void {
const app = document.getElementById('app');
if (!app) return;
app.innerHTML = `
<div class="docs-page">
<h1 class="page-title">Replay Format Specification</h1>
<div class="docs-content">
<section>
<h2>Overview</h2>
<p>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.</p>
<p><strong>Version:</strong> v1 (additive changes only - see <a href="#changelog">Changelog</a> below)</p>
</section>
<section>
<h2>Fetching Replays</h2>
<p>Replays are served from Cloudflare R2 (warm cache) with B2 (cold archive) fallback:</p>
<pre><code># 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</code></pre>
<p>Replays are gzip-compressed. The browser handles decompression automatically when you fetch with <code>Accept-Encoding: gzip</code>.</p>
</section>
<section>
<h2>Replay Schema</h2>
<p>Download the JSON Schema: <a href="/replay-schema-v1.json" target="_blank">replay-schema-v1.json</a></p>
<p>Use the schema to validate replays programmatically:</p>
<pre><code># Validate a replay file
ajv validate -s replay-schema-v1.json -d replay.json</code></pre>
</section>
<section>
<h2>Top-Level Structure</h2>
<pre><code>{
"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
}</code></pre>
</section>
<section>
<h2>Player Metadata</h2>
<pre><code>"players": [
{
"bot_id": "b_4e8c1d2f",
"name": "SwarmBot",
"owner": "alice",
"color": "#2196F3"
}
]</code></pre>
<table class="schema-table">
<tr><th>Field</th><th>Type</th><th>Description</th></tr>
<tr><td>bot_id</td><td>string</td><td>Unique bot identifier</td></tr>
<tr><td>name</td><td>string</td><td>Bot display name</td></tr>
<tr><td>owner</td><td>string</td><td>Bot owner's username</td></tr>
<tr><td>color</td><td>string</td><td>Player color for visualization</td></tr>
</table>
</section>
<section>
<h2>Match Result</h2>
<pre><code>"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
}</code></pre>
<p><strong>Win Conditions:</strong></p>
<ul>
<li><code>sole_survivor</code> - Only one player has living bots</li>
<li><code>annihilation</code> - All players eliminated simultaneously</li>
<li><code>dominance</code> - One player controls 80% of bots for 100 turns</li>
<li><code>turn_limit</code> - Turn limit reached (default: 500)</li>
</ul>
</section>
<section>
<h2>Match Configuration</h2>
<pre><code>"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
}</code></pre>
<p>Seasonal variations may introduce optional fields (see <a href="#changelog">Changelog</a>). Bots that don't read new fields continue to work.</p>
</section>
<section>
<h2>Map Definition</h2>
<pre><code>"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}
]
}</code></pre>
</section>
<section>
<h2>Turn Events</h2>
<p>Each turn contains events that occurred during that turn:</p>
<pre><code>"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}}
]
}
}
]
}
]</code></pre>
</section>
<section>
<h2>Event Types</h2>
<table class="schema-table">
<tr><th>Type</th><th>Description</th></tr>
<tr><td><code>bot_spawned</code></td><td>A new bot was spawned</td></tr>
<tr><td><code>bot_died</code></td><td>A bot died (legacy, no killer info)</td></tr>
<tr><td><code>combat_death</code></td><td>A bot died from focus-fire combat (includes killers[] array)</td></tr>
<tr><td><code>collision_death</code></td><td>Two bots moved to the same tile</td></tr>
<tr><td><code>zone_death</code></td><td>A bot was killed by the shrinking zone</td></tr>
<tr><td><code>energy_collected</code></td><td>Energy was gathered</td></tr>
<tr><td><code>core_captured</code></td><td>An enemy core was razed</td></tr>
</table>
</section>
<section>
<h2>Win Probability (Optional)</h2>
<p>Some replays include a win probability curve computed via Monte Carlo rollout:</p>
<pre><code>"win_prob": [
[0.50, 0.50], // Turn 0: even odds
[0.51, 0.49], // Turn 1: slight edge to player 0
...
]</code></pre>
<p>Array of [player_0_prob, player_1_prob, ...] for each turn.</p>
</section>
<section>
<h2>Critical Moments (Optional)</h2>
<p>Turns where win probability shifted significantly (>15%):</p>
<pre><code>"critical_moments": [
{
"turn": 87,
"delta": 0.22,
"description": "SwarmBot loses 6 units in eastern engagement"
}
]</code></pre>
</section>
<section id="changelog">
<h2>Changelog</h2>
<h3>Version 1 (Current)</h3>
<ul>
<li>Initial release</li>
<li>All core event types supported</li>
<li>Optional win_prob and critical_moments arrays</li>
</ul>
<p><strong>Backward Compatibility Policy:</strong> 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.</p>
</section>
<section>
<h2>Example Replays</h2>
<p>Download example replays to test your visualization:</p>
<ul>
<li><a href="/data/demo-replay-v2.json" download>2-Player Demo Replay</a></li>
<li><a href="/data/demo-replay-v2-6p.json" download>6-Player Demo Replay</a></li>
<li><a href="/data/real-replay.json" download>Full-Length Production Replay</a></li>
</ul>
</section>
<section>
<h2>Related Documentation</h2>
<ul>
<li><a href="/docs/data">Public Data Paths</a> - All available JSON endpoints</li>
<li><a href="/compete/docs">Getting Started</a> - Build your own bot</li>
<li><a href="/docs">API Documentation</a> - HTTP protocol reference</li>
</ul>
</section>
</div>
</div>
`;
}