Adds mobile haptic feedback (50ms vibration pulse) at critical moments
during replay playback per plan §16.18:
- Added opt-in toggle in Accessibility panel (checked by default)
- Triggers vibration on:
- Combat deaths (bot_died events)
- Core captures (core_captured events)
- Win probability shifts >15%
- Imports hapticPulse, isHapticEnabled, setHapticEnabled from ambient.ts
- Integrated into onTurnChange callback for real-time feedback
Closes: bf-2m3wm
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Integrated WinProbSparkline component into playlist-carousel metadata panel
- Compute win probabilities on card load using WinProbabilityEngine
- Tap score bar now toggles metadata panel with win probability graph
- Display critical moments and turn-by-turn probabilities
- Sparkline is clickable to scrub to specific turns
Closes: bf-12nc7
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Add matches_today and active_bots fields to LiveData Totals (evolver)
- Query matches table for COUNT(*) WHERE completed_at >= today
- Query bots table for COUNT(*) WHERE status = 'active'
- Add fields to index builder EvolutionMeta struct
- Update homepage to render "X matches today · Y bots active · Gen #Z evolving"
- Add CSS styling for .home-live-stats section
Closes: bf-4m8mo
Plan §5055: Attack event arrows from killers to victim position.
Added 3 tests to verify:
1. Arrows are drawn from each killer to victim in combat_death events
2. Multiple arrows are drawn for multiple killers (2v1 situations)
3. Arrow colors are set based on attacker player color
The tests mock the canvas context and verify that the appropriate
drawing methods (moveTo, lineTo, stroke, fill) are called when
rendering turns with combat_death events.
Closes: bf-4o9fp
Replays are bundled into the Pages deploy as gzipped static assets (B2 stays
the private cold archive). Repoint all replay/card/thumbnail/live.json fetches
off the empty R2 cache and the non-resolving b2.aicodebattle.com onto
same-origin /data/, via a shared fetchReplayFromUrl helper that gunzips
.json.gz with DecompressionStream.
- new web/src/lib/replay-data.ts (REPLAY_BASE, replayUrl, fetchReplayFromUrl)
- replay.ts / embed.ts / pages/embed.ts / playlist-carousel.ts use the helper
- og-tags, bot-card, home, matches, bot-profile, playlists, feedback, ambient,
api-types: /r2/ -> /data/
- pages.json data_paths updated; friendlier 404 message preserved
- 21 web tests pass; npm run build OK
Closes: bf-5cwi
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Implements plan §14.6 Map Evolution with a dedicated /maps page for browsing all competitive maps. Users can now:
- Browse maps grouped by player count (2P, 3P, 4P, 6P)
- View map stats (engagement score, wall density, energy count, dimensions)
- Upvote/downvote maps directly from the browsing interface
- See net vote counts and their own vote state
The page integrates with existing map voting API and data structure from the index builder.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Update web/public/data/blog/posts/meta-week-13-season-1.json to show
what an LLM-enhanced meta report looks like. Adds:
- Counter-Strategy Spotlight section with narrative analysis
- Evolution Deep Dive subsection with generational insights
- Enhanced Looking Ahead section with specific predictions
- Structured tables for strategies, rising stars, rivalries, evolution
- Prediction standings leaderboard
- More detailed summary with key insights
This placeholder now accurately reflects the output of generateMetaReportWithLLM()
in cmd/acb-index-builder/blog.go, which adds these LLM-generated sections
to the template-based report.
Closes: bf-39q2
Add web/public/data/predictions/ directory with demo JSON files:
- leaderboard.json: demo predictor leaderboard entries
- open.json: demo open matches for predictions
These placeholder files follow the same pattern as other demo data
(e.g., bots/index.json, leaderboard.json) for local development
and consistency. The index builder generates the real versions at
runtime via generatePredictionsIndex() and generatePredictionsOpen().
Closes: bf-4w95
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>
Add comprehensive test coverage for the Director Mode (Adaptive Auto-Speed
Playback) implementation, verifying the action density formula and speed
mapping match the plan specification exactly.
Tests cover:
- Action density calculation (deaths×3.0 + captures×5.0 + energy×0.5 + spawns×1.0 + delta_win_prob×10.0)
- Speed mapping (0→16x, 0.1-1.0→8x, 1.0-3.0→4x, 3.0-5.0→2x, 5.0+→1x)
- Speed schedule computation with target duration scaling
- Win probability delta calculation
All 16 tests pass, confirming the Director Mode implementation in
director.ts correctly implements §16.10 of the plan.
Closes: bf-1p5y
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Regenerated demo-replay-v2.json (2-player) and demo-replay-v2-6p.json (6-player)
using acb-local with current engine settings. Both replays now show combat_death
events, demonstrating the focus-fire combat system is working.
- 2-player demo: 6 turns, 2 combat_deaths (seed 789)
- 6-player demo: 6 turns, 6 combat_deaths (seed 456)
Previous replays were generated before zone/spawn radius fixes and showed minimal
combat. Combat density tests confirm targets met: 86% for 2-player, 100% for
6-player (TestCombatDensityMetrics).
Closes: bf-3er8
The first test in replay.test.ts was failing due to a race condition:
the initial module import took longer than the 100ms timeout, causing
urlInput to be null. Subsequent tests passed because the module was
cached. Increased timeout to 500ms to ensure the first module load
completes before checking for DOM elements.
All tests now pass (5/5).
- Add generate-maps-index.go script to generate maps/index.json and maps/{map_id}.json from maps/ directory
- Update docs-api.ts with /data/maps/index.json and /data/maps/{map_id}.json endpoints
- Generate 200 maps (50 per player count: 2, 3, 4, 6) in web/dist/data/maps/
- Maps include full geometry: walls, cores, energy nodes
- Index builder's generateMapsIndex() function already integrated (line 169 of generator.go)
Acceptance criteria met:
1. Maps directory exists with 200 maps (50 per player count)
2. generateMapsIndex() generates maps/index.json and maps/{map_id}.json in outputDir
3. web/dist/data/maps/ appears after npm run build (201 files: 1 index + 200 map details)
4. Maps endpoints documented at /docs/api
Closes: bf-2xjg
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Per plan §3.7.1, the shrinking zone kills bots outside the safe radius.
The engine emits zone_death events (commit f0a0673), but the web viewer
only handled bot_died events, so zone kills weren't visualized correctly.
Changes:
- Add zone_death event collection in drawCombatEffects()
- Visual distinction: yellow-amber lightning bolt marker vs red X for combat
- Zone death animation: fast yellow particles + shockwave
- Screen reader transcript: "Bot X killed by zone"
- Separate summarizeZoneDeaths() for detailed transcripts
Closes: bf-4i44
Adds Top Rivalries widget to landing page and Rivals section to bot
profiles, completing the platform integration for the automatic rivalry
detection system.
## Changes
- web/src/pages/home.ts: fetch rivalries data and render Top Rivalries card
- web/src/pages/bot-profile.ts: add Rivals section filtered to this bot
- web/src/styles/components.css: add rivalry list/item styles
## Plan §13.5 Platform Integration
✅ Rivalry widget on landing page with head-to-head records
✅ Bot profile pages show Rivals section with filtered rivalries
✅ Narratives already implemented via buildRivalryNarrative()
Closes: bf-2quf
The window.matchMedia API (used for accessibility features) was not
mocked in tests, causing unhandled rejections when replay.ts tried to
check prefers-reduced-motion. Added the mock to both test-setup.ts
and the beforeEach hook in replay.test.ts to ensure it's available
before modules load.
Closes: bf-5cwi
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Updates showLoadError to display a friendly "not available yet" message
for HTTP 404 errors (common for unuploaded replays) vs generic "could
not load" for other HTTP errors. Adds the URL to error output and
maintains HTML escaping.
Also adds vitest testing infrastructure with 5 tests covering:
- 404 not-found message
- Generic HTTP error message
- Parse error handling
- HTML escaping (XSS protection)
- 404 vs error distinction
Closes: bf-5cwi
- Add combat_deaths field to MatchResult interface (types.ts, engine.ts)
- Display combat kills count in sandbox match result panel
- Combat deaths were already tracked in engine (CombatDeaths []int)
but not exposed in the web UI types or displayed to users
Closes: bf-2wjo
Regenerated demo replays using the latest combat density and zone mechanics.
Previous replays (May 10) had 0 combat_death events and didn't reflect the
current state where zone starts early (turn 15-20) and forces combat.
New replays:
- demo-replay-v2.json: 2-player (random vs idle), 15 turns, 2 combat_death events
- demo-replay-v2-6p.json: 6-player (swarm,hunter,gatherer,rusher,guardian,random), 41 turns, 8 combat_death events
Verification:
- grep for combat_death events: > 0 in both replays
- Replays show focus-fire combat working as intended
Closes: bf-a918
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- WASM engine now includes zone mechanics (ZoneStartTurn, ZoneMinRadius, etc.)
- Bot WASM modules rebuilt with latest strategy code
- Browser sandbox now matches production Go engine behavior
- Engine size increased 5.3MB→5.6MB due to zone code
Closes: bf-5uyv
Draws the active zone boundary and out-of-zone danger area using
per-turn zone_bounds data from the replay. The zone renders as:
- Red semi-transparent overlay outside the safe zone
- Solid red boundary circle with dashed inner ring
- Center cross marker
- Inactive zones show as subtle dashed outline
Changes:
- Add ZoneBounds type to types.ts
- Add zone_bounds field to ReplayTurn
- Implement drawZone() method in replay-viewer.ts
- Call drawZone() in renderStandardView()
- Update replay-schema-v1.json with ZoneBounds definition
Accepts: bf-k1oy
Closes: bf-k1oy
- Updated CombatDeathDetails type to include killers[] array with each killer's bot_id, owner, and position
- Modified drawCombatEffects() to handle combat_death events by drawing solid arrows from each killer to the victim
- Added drawArrow() helper method to draw arrows with arrowheads
- Maintained backward compatibility: old replays without combat_death events use proximity-inference lines
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Add display: flex and align-items: center to .participant on mobile
- Change .participant-name from flex-shrink: 1 to flex: 1 for proper space allocation
- Add margin-left spacing to .participant-score and .winner-badge
- Ensures all participant info (name, score, winner badge) is visible on mobile
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Change .match-participants to column layout on mobile for better width distribution
- Allow .participant elements to shrink (flex-shrink: 1)
- Remove max-width constraint from .participant-name
- Ensures participant names, scores, and Winner badge are all visible
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When navigating to /watch/replay/:id (e.g., from the "Closest Match" link in rivalries), params.url was undefined, causing the viewer to show "Enter a replay URL to load" message. Fixed by constructing the URL from params.id when params.url is not set, using the pattern /r2/replays/MATCHID.json.gz.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Fix /docs/api route to go directly to API docs page instead of redirecting
- Fix docs.ts link to point to /compete/docs/api instead of itself
- Add download button for replay-schema-v1.json in API docs
- Reorder router routes to ensure /docs/api is matched before /docs
The API documentation at /docs/api now correctly shows the OpenAPI-style
endpoint documentation for all static JSON file paths on Pages, R2, and B2,
including the versioned replay format specification.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Added comprehensive keyboard shortcuts overlay modal and missing shortcuts
for the replay viewer:
- New keyboard shortcuts overlay (activated by ? or / key) showing all
available shortcuts in an accessible modal dialog
- Added missing shortcuts: V (cycle view mode), E (toggle event timeline),
C (toggle commentary), Shift+arrows (jump 10 turns)
- Updated visible shortcuts list in sidebar to include all shortcuts
- Modal is fully accessible with ARIA roles, keyboard navigation, and
respects prefers-reduced-motion
- All shortcuts are WCAG 2.1 Level AA compliant
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The win probability sparkline component is now fully integrated:
1. Worker (engine/winprob.go): Monte Carlo rollout computes per-turn win
probabilities, detectCriticalMoments identifies turns where win prob
shifts >15% with template-based descriptions.
2. Replay storage (engine/replay.go): win_prob and critical_moments arrays
stored in replay JSON, written by match worker after each match.
3. Web component (web/src/components/win-prob.ts): WinProbSparkline class
renders the graph with critical moment markers (dashed vertical lines),
click-to-scrub interaction, and current turn indicator.
4. Replay page integration (web/src/pages/replay.ts): initWinProb() sets up
the sparkline with player colors, legend, prev/next critical moment
navigation buttons, and keyboard shortcuts ([/]).
The sparkline displays one line per player with area fill gradient,
percentage labels (0%, 50%, 100%), critical moment diamonds with
delta labels, and updates in real-time as the replay plays.
- Client-side event extraction from replay turn data
- Icon ribbon overlaid on replay viewer timeline
- Click-to-jump to event moment
- Computed events: mass death (5+ bots), spawn wave (3+ spawns),
momentum shift (win prob crosses 50%), critical moment (>15% shift)
- Energy milestone detection (3+ energy collected)
- Hover tooltips with event descriptions
- Updated icons matching plan §14.8 specification
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Per §14.10 of the plan, implemented shareable bot profile cards:
- Canvas-rendered PNG cards (800x450) with bot stats and branding
- Open Graph tags for social sharing (og:image points to /r2/cards/{bot_id}.png)
- "Share Card" button on bot profile page downloads the card as PNG
- Card displays: name, rating, rank badge, owner, archetype, win rate, stats
- Evolved badge, signature move, and recent rival info
- Responsive styles for desktop and mobile
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add optional debug field in move response schema with extended telemetry:
- values: key-value pairs for debug display (metrics, state flags)
- heatmap: 2D grid overlay for visualization (threat maps, influence maps)
Engine changes:
- Add Values and Heatmap fields to DebugInfo struct in bot_http.go
- Add DebugHeatmap struct with name and 2D data array
Web viewer changes:
- Extend DebugInfo interface in types.ts with values and heatmap
- Implement heatmap rendering with blue→red gradient overlay
- Add getHeatmapColor helper for normalized value visualization
- Update debug panel to display values as key-value table
- Show heatmap info with name and dimensions
Schema updates:
- Add DebugHeatmap definition to replay-schema-v1.json
- Extend DebugInfo with values and heatmap properties
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The reveal() function was trying to return the result of setXP()
which returns void. Fixed by setting XP first, then returning
the threshold value.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Implements §16.15 progressive disclosure system that reveals advanced
UI features gradually based on user engagement (XP tracked via localStorage).
Features:
- XP tracking system stored in localStorage
- reveal(featureKey) / isRevealed(featureKey) API
- 9 XP-gated features (event timeline, view modes, follow camera, etc.)
- Action-based features (predictions, sandbox, embed) unlocked by specific user actions
- Power user override to show all controls
- Engagement tracking (30+ second replay watch = 1 XP)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add comprehensive JSON Schema for replay format (v1) as specified in
plan §15.2. This enables third-party tooling to validate and understand
replay files programmatically.
Schema documents:
- Root replay object (format_version, match_id, config, timestamps)
- Match result (winner, reason, scores, stats)
- Player information
- Map data (walls, cores, energy nodes)
- Turn-by-turn state (bots, cores, energy, scores, events)
- Optional win probability curve and critical moments
- Event types (bot_spawned, bot_died, energy_collected, core_captured,
combat_death, collision_death)
- Debug telemetry for bot reasoning visualization
All fields include descriptions, types, constraints, and examples.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds combat_turns metric (distinct turns where ≥1 bot died from enemy
focus-fire, excluding self-collisions). Worker computes it after each
match; index builder sorts matches/index.json and the new most-combat
playlist descending by it, and bumps interest score for combat-heavy
matches so they surface in highlights.
Also switches homepage featured replay default view from influence to
standard so the actual bot-on-bot combat is visible.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two root causes prevented bots from making any moves:
1. SignRequest signing string included timestamp ({match_id}.{turn}.{timestamp}.{hash})
but all bots implement verifySignature without timestamp ({match_id}.{turn}.{hash}).
Fixed by dropping timestamp from the signing string; X-ACB-Timestamp header is still
sent for clock-skew checks but not in the HMAC.
2. The API stores bot secrets AES-GCM encrypted (184 hex chars) in the DB. The worker
was passing the ciphertext directly as the HMAC key, while bots use their plaintext
k8s secret (64 hex chars). Fixed by decrypting in the worker using ACB_ENCRYPTION_KEY.
Also tightens the home page winner filter to exclude winner_id="0" stalemates.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove the Choose File / file input from the Load Replay panel entirely
- Hide the Load Replay panel when a ?url= param is already in the route,
so navigating from a match link goes straight to the replay with no
form in the way
- Update no-replay overlay text: "Loading replay…" vs "Enter a replay URL"
- Remove the fileInput change listener (file uploads no longer supported)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Filter the match pool to those with winner_id set before selecting
the featured replay, so a stalemate is never shown as the hero replay.
Falls back to any completed match if no decided matches exist.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace native alert() calls in replay.ts URL/file load error handlers
with inline error display in the no-replay div. Add combat attack
direction visualization to §16.9 of the plan: engine emits combat_death
events with killer bot list; viewer draws directed arrows on kills.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Playlist card images were pointing at /replays/{id}.jpg which returns
the SPA shell HTML. Changed to /r2/thumbnails/{id}.png which routes
through the Pages Function proxy to R2.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace /replays/{id}.json.gz with /r2/replays/{id}.json.gz in all pages
(home, matches, bot-profile, playlists, feedback). The /replays/ path is not
served by Cloudflare Pages — it falls back to the SPA shell causing
"Unexpected token '<', <!DOCTYPE..." JSON parse errors. The R2 proxy at
/r2/ is the correct path for replay content.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Router: strip query string from hash path before route matching, and merge
hash query params (e.g. ?url=) into the params passed to route handlers.
Add /watch/replay route (without :id) so ?url= links work without a path ID.
Embed: fall back to demo replay when the match replay is not found in R2/B2
instead of showing "Failed to fetch" (handles test match IDs with no replay).
App: extend skeleton and PIP checks to match /watch/replay (with or without :id).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Cloudflare CDN strips Content-Encoding: gzip from Worker responses even when
explicitly set, leaving browsers with raw gzip bytes they cannot parse as JSON.
Fix by piping .gz objects through DecompressionStream('gzip') inside the Worker
so the response body is always plain JSON — no Content-Encoding header needed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
R2 Workers binding strips Content-Encoding: gzip from served objects even
when stored with that metadata — the Pages Function now re-applies it for
.gz keys so browsers decompress the body before parsing as JSON.
Change X-Frame-Options from DENY to SAMEORIGIN so the home page can embed
/embed.html in its featured-replay iframe (same origin is fine here).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>