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:
parent
149fbe4edf
commit
d27aafc532
3 changed files with 437 additions and 0 deletions
|
|
@ -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
213
web/src/pages/docs-data.ts
Normal 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>
|
||||
`;
|
||||
}
|
||||
219
web/src/pages/docs-replay-format.ts
Normal file
219
web/src/pages/docs-replay-format.ts
Normal 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>
|
||||
`;
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue