Sandbox: WASM-per-bot model, drop what-if branching

Reworked sandbox to use separate WASM modules per bot instead of JS
function callbacks. Each bot compiles to its own WASM with a standard
init/compute_moves/free_result interface. Supports Go, Rust, TS natively;
PHP/Java bots reimplemented in Go for WASM. Memory budget 30-105MB
depending on player count. Two user modes: TS quick-start in Monaco
or upload pre-compiled .wasm file.

Dropped what-if replay branching — good in theory but the split-pane
counterfactual UI is a power-user feature that would confuse casual
visitors. Not worth the complexity.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
jedarden 2026-03-23 22:38:00 -04:00
parent 66fe718835
commit 46f5e2ac4a

View file

@ -1922,25 +1922,29 @@ within the first week of operation.
### Phase 8: Enhanced Features
**Deliverables:**
- WASM game engine build (`GOOS=js GOARCH=wasm`) with `loadState()` and
`simulate()` API for browser use
- In-browser sandbox: Monaco editor + WASM engine + JS bot execution + replay
viewer integration
- JS reimplementations of the 6 built-in strategy bots for sandbox opponents
- What-if replay branching: fork UI, state injection, split-pane comparison
- WASM game engine build (`GOOS=js GOARCH=wasm`) with `loadState()`,
`step()`, and `runMatch()` API for browser use
- WASM bot interface spec: `init()`, `compute_moves()`, `free_result()`
exports for bot-to-engine communication
- Pre-compiled WASM builds of the 6 built-in strategy bots (Go/Rust/TS
natively; PHP/Java reimplemented in Go for WASM)
- In-browser sandbox: Monaco editor (TS quick-start) + WASM upload mode +
opponent selector + replay viewer integration
- Win probability computation in the match worker (Monte Carlo rollout) +
critical moments detector + replay viewer sparkline graph
- Replay enrichment pipeline: selective AI commentary for featured matches
- Clip maker: GIF + MP4 export with social media format presets
- Rival detection query + rivalry pages
- Community replay feedback system: tagged annotations guiding evolution
- D1 schema additions: `replay_feedback`, `rivalries` tables
- Clip maker: GIF + MP4 export with 5 social media format presets
(landscape, square, portrait, compact GIF, square GIF)
- Rival detection query + rivalry pages with template-generated narratives
- Community replay feedback system: tagged annotations feeding evolution
- D1 schema additions: `replay_feedback` table
- Worker API addition: `POST /api/feedback` for submitting replay annotations
**Exit criteria:** users can write and test bots in the browser without
deploying anything, fork replays to explore alternate outcomes, watch
enriched replays with commentary and win probability, export clips for
social sharing, and submit tactical feedback that influences the evolution
pipeline.
**Exit criteria:** users can write and test bots in the browser (TS
quick-start or uploaded WASM) without deploying anything, watch enriched
replays with commentary and win probability, export clips for social
sharing, view rivalries, and submit tactical feedback that influences the
evolution pipeline.
---
@ -1948,80 +1952,117 @@ pipeline.
### 12.1 In-Browser WASM Game Sandbox
The game engine compiles to WebAssembly, enabling users to write bots and
play matches entirely in the browser — zero deployment, zero server setup.
The game engine and bots compile to WebAssembly, enabling users to develop
and test bots entirely in the browser against real opponents — zero
deployment, zero server setup.
**Architecture:**
**Architecture — WASM per module, not JS functions:**
A meaningful bot needs pathfinding, state tracking across turns, spatial
data structures, and threat assessment. That's a real program, not a
20-line JavaScript function. Limiting bots to JS callbacks would undermine
the platform's multi-language premise.
Instead, the sandbox loads **separate WASM modules** for the game engine
and each bot:
```
Browser
├── Game Engine (Go → WASM, single instance, ~15 MB)
├── Game Engine (Go → WASM, ~15 MB)
│ ├── loadState(json) → set engine to a specific turn state
│ ├── simulate(botMoves[]) → advance one turn
│ └── runMatch(botFunctions[]) → run full match, return replay
│ ├── step(moves[]) → advance one turn, return events
│ └── runMatch(config, map) → run full match coordinating bot WASMs
├── User Bot (JavaScript function)
│ └── computeMoves(gameState) → returns moves array
├── Bot WASMs (pre-compiled, loaded on demand)
│ ├── gatherer.wasm (Go → WASM, ~12 MB)
│ ├── rusher.wasm (Rust → WASM, ~3 MB)
│ ├── swarm.wasm (TypeScript → WASM via wasm-pack, ~5 MB)
│ ├── random.wasm (Go → WASM, ~10 MB) -- lightweight reimpl
│ ├── guardian.wasm (Go → WASM, ~12 MB) -- reimpl from PHP
│ └── hunter.wasm (Go → WASM, ~12 MB) -- reimpl from Java
├── Opponent Bots (JavaScript, built-in)
│ ├── randomBot(gameState) → random valid moves
│ ├── gathererBot(gameState) → BFS to nearest energy
│ ├── rusherBot(gameState) → BFS to nearest enemy core
│ ├── guardianBot(gameState) → perimeter defense
│ ├── swarmBot(gameState) → formation advance
│ └── hunterBot(gameState) → target isolated enemies
├── User's Bot WASM (compiled locally, uploaded as .wasm file)
│ └── or: user writes Go/Rust/TS, compiles in-browser via toolchain
├── Monaco Editor (code editing)
├── Monaco Editor (code editing for quick-start JS/TS mode)
└── Replay Viewer (Canvas, renders result)
```
**Why 1 WASM, not 8:**
**WASM communication interface:**
The game engine is the only WASM module. Bots run as plain JavaScript
functions called synchronously by the engine via JS interop. For a 6-player
match: 1 WASM instance + 6 JS function calls per turn. Memory: ~3050 MB
total. A 500-turn, 6-player match simulates in <2 seconds on modern hardware.
Each bot WASM exports a standard interface:
The built-in strategy bots (originally Python, Go, Rust, PHP, TypeScript,
Java) are **reimplemented in JavaScript** for the sandbox. They only need to
be behaviorally equivalent — same BFS, same combat logic, same decision
heuristics — not identical code. These JS versions are ~200500 lines each.
**WASM engine API:**
```typescript
interface WASMEngine {
// Run a full match, calling botFn[i](state) for each player each turn
runMatch(config: MatchConfig, map: MapData, bots: BotFunction[]): Replay
// Load a specific game state (for what-if branching)
loadState(state: GameState): void
// Simulate one turn with given moves
step(moves: PlayerMoves[]): TurnResult
// Simulate forward from current state using bot functions
simulateForward(bots: BotFunction[], remainingTurns: number): Replay
}
type BotFunction = (state: GameState) => Move[]
```
// Exported by every bot WASM module
fn init(config_json: *const u8, config_len: u32)
fn compute_moves(state_json: *const u8, state_len: u32) -> *const u8
fn free_result(ptr: *const u8)
```
**User flow:**
The engine WASM orchestrates the match: each turn, it serializes the
fog-filtered game state as JSON, calls each bot WASM's `compute_moves`,
deserializes the moves, and advances the simulation. Bots maintain their
own internal state across turns inside their WASM linear memory.
**Language support in the sandbox:**
| Language | WASM Compilation | Sandbox Support |
|----------|-----------------|-----------------|
| Go | `GOOS=js GOARCH=wasm` (native) | Full |
| Rust | `wasm32-unknown-unknown` (native) | Full |
| TypeScript | AssemblyScript or wasm-pack | Full |
| Python | Pyodide (~20 MB runtime) | Heavy but feasible |
| PHP | Not practical for WASM | HTTP ladder only |
| Java | Not practical for WASM | HTTP ladder only |
For the built-in opponents, GuardianBot (PHP) and HunterBot (Java) are
**reimplemented in Go** as sandbox-only WASM builds. They are behaviorally
equivalent — same BFS, same combat logic, same heuristics — not identical
code.
**Memory budget:**
| Configuration | Memory |
|--------------|--------|
| Engine + 1 user bot + 1 opponent | ~3040 MB |
| Engine + 1 user bot + 3 opponents (4-player) | ~5575 MB |
| Engine + 1 user bot + 5 opponents (6-player) | ~75105 MB |
| With Pyodide (Python user bot) | Add ~20 MB |
Desktop browsers typically have 24 GB available. Even the heaviest
configuration is <5% of available memory. Mobile is tighter but the
sandbox is a desktop-first dev tool.
A 500-turn 2-player match simulates in ~23 seconds (WASM-to-WASM calls
have overhead vs native, but each turn's computation is trivial).
**User flows (two modes):**
*Quick-start mode (JS/TS in Monaco):*
1. User visits `/sandbox`
2. Monaco editor pre-loaded with a starter bot (hold-all-units stub)
3. User selects opponent (dropdown: RandomBot, GathererBot, etc.) and map
4. Clicks "Run Match"
5. WASM engine executes the match locally (~12 seconds)
6. Replay viewer renders the result inline
7. User modifies code, re-runs — instant feedback loop
8. When ready for the real ladder, user follows a link to the starter kit
docs for deploying an HTTP server
2. Monaco editor pre-loaded with a TypeScript starter bot
3. User writes strategy code with full type hints and autocomplete
4. Code compiles to WASM in-browser via AssemblyScript
5. Selects opponent and map, clicks "Run Match"
6. Engine orchestrates match between user WASM and opponent WASM (~23s)
7. Replay viewer renders result inline
8. Modify, re-run — instant feedback loop
**Why this matters:** The current onboarding path is "set up a server, write
HTTP handlers, deploy, register." The sandbox makes it "open a page, write
20 lines of JS, click play." The difference between 50 users and 5,000.
*Full mode (upload compiled WASM):*
1. User develops a bot locally in Go, Rust, or any WASM-targeting language
2. Compiles to `.wasm` using their own toolchain
3. Uploads the `.wasm` file to the sandbox page
4. Sandbox validates the exported interface (`init`, `compute_moves`)
5. Runs match against selected opponents
6. When ready for the real ladder, deploys the same bot logic as an HTTP
server using a starter kit
**Why this matters:** The sandbox preserves the platform's multi-language
strength while eliminating the deployment barrier. Users can develop
substantial, stateful bots in real languages — not toy JS functions —
and iterate locally before committing to the HTTP ladder.
### 12.2 Win Probability Graph + Critical Moments
@ -2087,62 +2128,7 @@ large position changes). No LLM needed — template-based.
This transforms replay viewing from "press play and wait 5 minutes" to
"click the 3 interesting moments and watch 30 seconds of decisive action."
### 12.3 What-If Replay Branching
Pause a replay at any turn, modify one player's moves, and simulate an
alternate timeline. Compare the real outcome with the counterfactual.
**How it works (no WASM swapping):**
The WASM engine (same instance used by the sandbox) supports state injection.
The replay viewer already reconstructs the full game state at every turn.
```
1. User pauses replay at turn N
2. UI shows the current player's units with editable move arrows
3. User drags arrows to change moves for their chosen player
4. Click "What If?"
5. Viewer serializes turn-N game state → engine.loadState(stateN)
6. Engine applies:
- Modified moves for the user's chosen player
- Original replay moves for all other players
7. Engine simulates forward from turn N+1:
- Other players continue using their original replay moves
- The modified player uses a selectable strategy (hold all, random,
gatherer, or "repeat my change")
8. Result renders in split-pane view:
Left: original replay from turn N onward
Right: alternate timeline from turn N onward
Both share the same scrub bar and play/pause controls
```
**Key design decision:** Other players replay their *original moves* from
the replay data, not re-computed moves. This means the what-if shows "what
would have happened if I changed my move but my opponent didn't react
differently." This is a simplification — a true counterfactual would need
the opponent's bot to recompute — but it's useful for answering tactical
questions ("would moving east instead of north have avoided that ambush?")
and requires no access to opponent bot code.
**UI:**
```
┌────────────────────┬────────────────────┐
│ Original (real) │ What-If (branch) │
│ │ │
│ [canvas] │ [canvas] │
│ │ │
├────────────────────┴────────────────────┤
│ [win prob graph: solid=real, dashed=branch] │
│ ◄ ▶ ⏸ Turn 203/500 Speed: 4x │
└─────────────────────────────────────────┘
```
The win probability graph shows both timelines overlaid — the real outcome
as a solid line and the branched outcome as a dashed line. The divergence
point is marked with a vertical line at turn N.
### 12.4 Replay Enrichment (Selective AI Commentary)
### 12.3 Replay Enrichment (Selective AI Commentary)
Select replays receive AI-generated natural language commentary — a
play-by-play narration that makes matches accessible to casual viewers.
@ -2152,7 +2138,7 @@ play-by-play narration that makes matches accessible to casual viewers.
- **Featured matches**: matches flagged by the system as particularly
interesting (high win probability variance, close finishes, upsets where
a lower-rated bot wins)
- **Rivalry matches**: matches between detected rivals (§12.6)
- **Rivalry matches**: matches between detected rivals (§12.5)
- **Evolution milestones**: first match of a newly promoted evolved bot,
or matches where an evolved bot breaks into the top 10
- **User-requested**: participants can request enrichment for their own
@ -2207,7 +2193,7 @@ enriched matches/hour: ~$26/day, ~$60180/month. Reasonable.
- Toggle on/off via a "Commentary" button
- Enriched replays are badged on the match list ("Featured" / "Narrated")
### 12.5 Shareable Replay Clips
### 12.4 Shareable Replay Clips
One-click export of a replay segment as a GIF or video, formatted for
major social media platforms. This is the viral growth engine.
@ -2259,7 +2245,7 @@ The clip maker uses:
the browser. The share buttons use Web Share API where available, fallback
to window.open() with pre-composed URLs.
### 12.6 Automatic Rival Detection
### 12.5 Automatic Rival Detection
The platform automatically identifies **rivalries** — pairs of bots that
frequently play each other with close results — and surfaces them as
@ -2314,11 +2300,11 @@ The narrative is template-generated from the stats (no LLM needed):
- Rivalry widget on the landing page: "Top Rivalries" with head-to-head
records and links to key matches
- Bot profile pages show "Rivals" section listing detected rivalries
- Rivalry matches are auto-flagged for replay enrichment (§12.4)
- Rivalry matches are auto-flagged for replay enrichment (§12.3)
- Leaderboard can show "rivalry mode" — filter to matches between two
specific bots
### 12.7 Community Replay Feedback
### 12.6 Community Replay Feedback
Users can leave **tagged feedback** on specific moments in replays.
Feedback is anchored to a `(replay_id, turn)` pair and is visible to