feat: SSE live fight spectating with spectator count

Enable real-time fight spectating for all live fights (not just human
fights). Multiple spectators can watch simultaneously via SSE. Spectator
count is tracked per-fight and broadcast with every SSE event.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-08 19:46:39 +00:00
co-authored by Claude Opus 4.6
parent a540320901
commit 610e799605
18 changed files with 1555 additions and 397 deletions
+152
View File
@@ -0,0 +1,152 @@
# Production Hardening v2 — Final Status
## Context
The original hardening plan (`greedy-skipping-lollipop.md`) was 95% complete. A full security audit confirmed the fight engine, scoring, auth, SSRF, rate limiting, and webhook systems were solid. This session addressed remaining production gaps AND added Creator ₿ animation/morph enhancements AND fixed a production ranked-fight bug.
---
## Phase 1: Security Headers & Error Hardening — DONE
### 1.1 Security headers middleware — DONE
**File:** `server/src/app.ts`
- Added `secureHeaders` from `hono/secure-headers` with CSP (production only), X-Frame-Options, X-Content-Type-Options, HSTS, Referrer-Policy
### 1.2 Request body size limit — DONE
**File:** `server/src/app.ts`
- Added `bodyLimit` from `hono/body-limit` — 256KB max for API requests
### 1.3 Hide error details in production — DONE
**File:** `server/src/app.ts`
- `onError` returns generic "Internal server error" in production, full message in dev
---
## Phase 2: Graceful Shutdown & Env Validation — DONE
### 2.1 Graceful shutdown handler — DONE
**File:** `server/src/index.ts`
- SIGTERM/SIGINT handlers wait 10s for in-flight fights, clear pending human challenges, then exit
### 2.2 Export helpers for shutdown — DONE
- `server/src/engine/orchestrator.ts``getActiveFighterCount()`
- `server/src/engine/human-responses.ts``clearAllPending()`
### 2.3 Stricter env validation — DONE
**File:** `server/src/index.ts`
- Warns if CORS_ORIGIN not set in production
---
## Phase 3: Payment Race Condition Fix — DONE
### 3.1 Atomic consumePaymentForQueue — DONE
**File:** `server/src/engine/payments.ts`
- Replaced check-then-set with atomic `UPDATE ... WHERE` (single SQL statement)
- Imported raw `sqlite` from db/index.js
### 3.2 releasePayment reverts DB state — DONE
**File:** `server/src/engine/payments.ts`
- Now also reverts `status` from `consumed` back to `confirmed` in DB
---
## Phase 4: Live Round-by-Round TUI — DONE
### 4.1 onRoundComplete callback — DONE
**File:** `server/src/engine/fight-loop.ts`
- New `onRoundComplete` option, subscribes to `fightEvents.onAll()` for `round_end` events
### 4.2 TUI wired to round updates — DONE
**File:** `server/src/fight-loop-cli.ts`
- `onRoundComplete` updates `state.currentFight` HP/round/challengeType and re-renders
---
## Phase 5: Creator ₿ Animation Enhancements — DONE
### 5.1 Persistent orbiting ₿ letters on entrance — DONE
**File:** `frontend/src/game/FightScene.ts` (Creator entrance)
- Replaced 6 plain circle dots with 10 ₿ text letters in 2 counter-rotating rings
- Varied sizes (7-14px), 5 colors, gentle rocking animation
- Visible for the entire fight
### 5.2 Upgraded omni-morph ₿ orbits — DONE
**File:** `frontend/src/game/FightScene.ts` (`applyCreatorOmniMorph`)
- Increased from 4 to 8 ₿ in dual rings (inner 5, outer counter-rotating 3)
- Varied sizes (10-18px), 3 colors, wobble animation
### 5.3 bitcoinRain uses ₿ text — DONE
**File:** `frontend/src/game/FightScene.ts` (`bitcoinRain`)
- Changed from circles to actual ₿ text with 3 colors, varied sizes (6-18px), spinning as they fall
### 5.4 Four new ₿ swarm choreographies — DONE
**File:** `frontend/src/game/FightScene.ts`
- `bitcoinSwarm` — 18-30 ₿ letters zigzag from Creator to defender
- `satoshiTornado` — 14-24 ₿ spiral in tightening vortex around defender, then explode
- `hodlWave` — wall of ₿ letters (4-6 rows x 7-10 cols) advances like a tidal wave
- `bitcoinBarrage` — rapid-fire stream of 20-35 spinning ₿ from the laptop
- All added to `CREATOR_MOVES` array and `choreographyMap`
---
## Phase 6: Creator Omni-Morph Visual Fix — DONE
### 6.1 Sprite swap on morph — DONE
**File:** `frontend/src/game/FightScene.ts` (`applyCreatorOmniMorph`)
- Was: only added overlays (aura, ₿, color tint) — sprite stayed as Creator
- Now: generates a full sprite sheet for the picked archetype via `generateSpriteSheet(seed, tier, colors, archetypeOverride)`
- Loads it as a Kaplay sprite and swaps with `fighter.use(k.sprite(morphKey))`
- Function changed from sync to `async`
- Unique sprite keys via incrementing counter prevent collisions
### 6.2 Sprite revert after attack — DONE
**File:** `frontend/src/game/FightScene.ts` (morph trigger in playRound)
- `morphRevert` now calls `attacker.use(k.sprite(originalSpriteKey))` to swap back to Creator
- Added `await` on `applyCreatorOmniMorph` call (now async)
---
## Phase 7: Production Ranked Fight Fix — DONE
### 7.1 Missing pubkey in ranked endpoints — DONE
**File:** `frontend/src/composables/useWallet.ts`
- `payEntryFee()` now sends `{ botId, pubkey: pubkey.value }` to `/api/payments/create-invoice`
**File:** `frontend/src/pages/JoinBoutPage.vue`
- `fightRanked()` now sends `{ paymentId, pubkey: pubkey.value }` to `/api/queue/join-ranked`
**Root cause:** Server requires `pubkey` in production for ownership verification, but frontend wasn't sending it. Worked in dev because dev mode skips pubkey checks.
---
## All Files Modified
| File | Phase | Changes |
|------|-------|---------|
| `server/src/app.ts` | 1 | +secureHeaders, +bodyLimit, error hiding |
| `server/src/index.ts` | 2 | +graceful shutdown, +env validation |
| `server/src/engine/orchestrator.ts` | 2 | +getActiveFighterCount() |
| `server/src/engine/human-responses.ts` | 2 | +clearAllPending() |
| `server/src/engine/payments.ts` | 3 | Atomic consume, releasePayment DB revert |
| `server/src/engine/fight-loop.ts` | 4 | +onRoundComplete via fightEvents |
| `server/src/fight-loop-cli.ts` | 4 | Wire onRoundComplete to TUI |
| `frontend/src/game/FightScene.ts` | 5,6 | ₿ animations, omni-morph sprite swap, 4 new choreographies |
| `frontend/src/composables/useWallet.ts` | 7 | +pubkey in create-invoice |
| `frontend/src/pages/JoinBoutPage.vue` | 7 | +pubkey in join-ranked |
| `frontend/src/game/sprites/archetypes/the_creator.ts` | — | No changes (reference only) |
---
## Verification
1. `pnpm build` — verify no type errors (both server and frontend pass)
2. Production server: `curl -I /api/health` — X-Frame-Options, X-Content-Type-Options, CSP headers present
3. Body limit: oversized POST returns 413
4. Error hiding: bad endpoint returns "Internal server error" (not stack trace)
5. Graceful shutdown: `kill -TERM` shows drain log
6. TUI: `pnpm fight-loop --max=3` shows live round-by-round HP updates
7. Payment race: concurrent ranked joins with same paymentId — only 1 succeeds
8. Ranked fights: no more "Missing pubkey" error in production
9. Creator fights: omni-morph visually transforms into random archetypes (sprite swaps)
10. Creator fights: ₿ letters orbit persistently, new swarm attacks fire regularly
+498 -271
View File
@@ -1,330 +1,557 @@
# Botfights: 30-Day Production Plan
# Botfights: 6-Month Development Plan (MarchSeptember 2026)
## Context
## Current State (March 8, 2026)
Botfights is a procedural fighting game (Vue 3 + Kaplay + Hono/SQLite) where AI bots battle through challenge rounds with pixel art sprites, synthesized audio, and choreographed fight animations. Currently a working prototype with 6 character archetypes, 42 choreographies, 4 music tracks, 6 voice profiles, ~45 challenge prompts, and 12 mock bots. The goal is to scale this to production-grade content (100 characters, 300 moves, 10,000 challenges, 20 songs, 30 voices, 30+ arenas) and build an overnight fight loop that runs autonomously.
Botfights is a competitive AI fighting game (Vue 3 + Kaplay + Hono/SQLite) where bots battle through challenge rounds with pixel art sprites, synthesized audio, and choreographed fight animations. The core is feature-complete:
| Category | Count | Status |
|----------|-------|--------|
| Archetypes | 101 (100 + THE CREATOR) | Done |
| Choreographies | 352+ (300 base + 40 tier-gated + 12 creator) | Done |
| Challenge Types | 16 | Done |
| Challenge Prompts | 800 (551 multiple-choice, 249 creative) | Done |
| Mock Bots | 100 (tiers 06) | Done |
| Arenas | 25 with modifiers | Done |
| Voice Profiles | 60+ | Done |
| Music Tracks | 10 procedural synth | Done |
| Sound Effects | 30+ | Done |
| Pages | 20 | Done |
| Nostr Auth | NIP-04/44/19 | Done |
| Payments | NWC + Lightning Address + Cashu | Done |
| PWA | Manifest + SW + offline assets | Done |
| Docker | Multi-stage, 42MB prod image | Done |
| TUI Fight Loop | Live round-by-round | Done |
| Production Hardening | Security headers, SSRF, rate limiting, graceful shutdown | Done |
**FightScene.ts** is 11,313 lines. The codebase works end-to-end but hasn't been battle-tested with real users at scale.
---
## Phase 1: Module Splitting (Days 1-6)
## Month 1: Social Layer & Real-Money Launch (March 9 April 5)
The 3 monolith files must be split before any content scaling. Without this, adding content to 3500-line files is unmaintainable.
Goal: Make botfights a place people want to hang out and bet sats.
### Day 1-2: Split `sprites.ts` (954 lines)
### Week 1: Spectator Experience
```
frontend/src/game/sprites/
index.ts -- re-exports generateSpriteSheet, getBotColors, etc.
palette.ts -- makePal, parseHSL, Pal interface
constants.ts -- FRAME_SIZE, ANIMATIONS, INTERNAL, SCALE
drawing.ts -- px(), box(), fill() drawing primitives
base-body.ts -- drawFrame() core body/head/arms/legs/pose logic
archetypes/
index.ts -- archetype registry + roll function
standard.ts -- base (no extra features)
lobster.ts -- claws, antennae, segmented body, tail
sheep.ts -- wool, floppy ears, hooves
cyborg.ts -- metal plating, robot eye, wiring
blob.ts -- wobbly outline, googly eyes, drool
tank.ts -- armor, rivets, treads
judge.ts -- generateJudgeSpriteSheet()
```
#### 1.1 Live Fight Spectating via SSE
**Files:** `server/src/routes/fights.ts`, `frontend/src/pages/FightPage.vue`
- SSE endpoint streams round results, HP changes, challenge reveals in real-time
- Multiple spectators can watch a live fight simultaneously
- Auto-redirect from fight card to live view when fight starts
- Spectator count badge on active fights
Each archetype exports `{ name, weight, drawFeatures(ctx, pal, params) }`. The base-body calls the archetype's `drawFeatures` after the shared body drawing.
#### 1.2 Fight Feed / Activity Stream
**New file:** `frontend/src/pages/FeedPage.vue`
- Reverse-chronological feed of recent fights: winner, KO/decision, Elo changes
- Filter by tier, arena, bot name
- Click any fight to watch replay
- Replace or augment current HomePage with this
**Test:** Sprite preview page renders all 6 archetypes at all tiers identically to before.
#### 1.3 Reactions / Crowd Noise
**Files:** `frontend/src/components/FightViewer.vue`, `server/src/routes/fights.ts`
- Spectators can send emoji reactions during live fights (fist, fire, skull, 100, clown)
- Reactions appear as floating particles over the fight
- Server aggregates reactions, broadcasts via SSE
- Crowd SFX intensity scales with reaction count
### Day 3-4: Split `FightScene.ts` (~3500 lines)
### Week 2: Betting System UI
#### 2.1 Bet Placement UI
**Files:** `frontend/src/components/BetPanel.vue`, `frontend/src/pages/FightCardPage.vue`
- Pre-fight bet panel: pick winner, set amount (21/100/500/1000 sats)
- Cashu token deposit flow (paste token or scan QR)
- NWC direct-pay option for connected wallets
- Odds display based on Elo difference
- Bet confirmation with payout preview
#### 2.2 Bet Settlement & Payout
**Files:** `server/src/engine/betting.ts`, `server/src/engine/payments.ts`
- Atomic settlement on fight completion
- Winner payout minus 5% house cut (goes to fight pot for ranked matches)
- Cashu token minting for winnings OR Lightning payout
- Refund on cancelled/errored fights
#### 2.3 Betting History
**Files:** `frontend/src/pages/BotProfilePage.vue`, new `frontend/src/components/BetHistory.vue`
- Personal bet history on profile page
- P&L tracking (total wagered, total won, net)
- "Hot streak" and "cold streak" indicators
### Week 3: Nostr Social Integration
#### 3.1 Fight Results to Nostr
**New file:** `server/src/engine/nostr-publish.ts`
- Post NIP-01 kind:1 notes for notable fights (KOs, perfects, upsets, tier changes)
- Include fight replay link, bot names, Elo changes
- Configurable relay list via env var
- Only publish if `BOTFIGHTS_NOSTR_NSEC` is set
#### 3.2 Nostr Profile Enrichment
**Files:** `frontend/src/composables/useNostr.ts`, `frontend/src/pages/BotProfilePage.vue`
- Fetch NIP-05, display name, banner from Nostr relays
- Show Nostr profile picture alongside bot sprite
- Link to user's Nostr profile (njump.me or similar)
#### 3.3 Zap Integration
**Files:** `frontend/src/components/FightViewer.vue`, `server/src/routes/payments.ts`
- "Zap the winner" button after fight ends
- NWC zap flow: create invoice, user approves, confirm
- Zap receipts displayed on bot profile
### Week 4: Ranked Season System
#### 4.1 Season Framework
**New file:** `server/src/engine/seasons.ts`
- Season = 2 weeks, auto-rotates
- Season Elo resets to soft floor (blend of final Elo + 1200 baseline)
- Season leaderboard separate from all-time
- Season rewards: title badges, exclusive arena unlocks
#### 4.2 Season Leaderboard UI
**Files:** `frontend/src/pages/LeaderboardPage.vue`
- Toggle between "This Season" and "All Time"
- Season countdown timer
- Top 3 highlighted with crown/medal sprites
- Season history archive
#### 4.3 Ranked Queue Improvements
**Files:** `server/src/engine/ranked-queue.ts`, `frontend/src/pages/JoinBoutPage.vue`
- Elo-bracket matchmaking (±200 Elo, widens over time)
- Ranked queue status indicator (how many waiting)
- Estimated wait time
- Ranked-only challenges (harder prompts, no multiple choice)
---
## Month 2: Tournaments, Human Play & Content Expansion (April 6 May 3)
Goal: Structured competition and enough content that nothing feels repetitive.
### Week 5: Tournament System
#### 5.1 Tournament Engine
**New files:** `server/src/engine/tournaments.ts`, `server/src/db/schema.ts` (new tables)
- Single-elimination brackets (8, 16, 32 bots)
- Round-robin group stage option
- Entry fee (configurable sats amount) → prize pool
- Auto-advance winners, schedule next round
- DB tables: `tournaments`, `tournament_entries`, `tournament_matches`
#### 5.2 Tournament UI
**New files:** `frontend/src/pages/TournamentPage.vue`, `frontend/src/pages/TournamentListPage.vue`
- Visual bracket display (SVG or canvas)
- Live bracket updates as fights complete
- Tournament lobby: registered bots, countdown to start
- Results page with champion highlight
#### 5.3 Scheduled Tournaments
**Files:** `server/src/engine/fight-loop.ts`, `server/src/engine/tournaments.ts`
- Daily automated tournaments (e.g., 8pm UTC)
- "The Halvening" weekly 32-bot major tournament
- Special themed tournaments (code-golf only, roast-battle only)
- Cron-based scheduling via fight loop
### Week 6: Human vs Bot Improvements
#### 6.1 Human Fight Polish
**Files:** `frontend/src/pages/HumanFightPage.vue`, `server/src/engine/human-responses.ts`
- Fix the TODO in human-responses.ts (currently `pass # TODO: implement`)
- Mobile-optimized input (large buttons for multiple choice, big text area for creative)
- Countdown timer with visual urgency (color shift, shake)
- Immediate feedback after answering (correct/wrong animation)
#### 6.2 Practice Mode
**New file:** `frontend/src/pages/PracticePage.vue`
- Fight against mock bots without Elo impact
- Choose difficulty tier
- Post-fight breakdown: which rounds you won/lost and why
- Suggested improvements
#### 6.3 Human vs Human
**Files:** `server/src/engine/orchestrator.ts`, `server/src/routes/queue.ts`
- Real-time matchmaking for two human players
- Both answer same challenge simultaneously
- WebSocket or SSE for synchronization
- Elo-rated human ladder separate from bot ladder
### Week 7: Content Scaling
#### 7.1 Challenge Prompts → 2,000
**Files:** `server/src/engine/challenges.ts`
- 75 more prompts per existing 16 types = 1,200 new prompts
- Focus on Bitcoin/cypherpunk themed prompts:
- "What year was the Bitcoin whitepaper published?"
- "Explain proof-of-work to a medieval blacksmith"
- "Roast someone who thinks Ethereum is decentralized"
- "Write a haiku about the mempool"
- All factual types get multiple-choice options
- Difficulty tiers: easy (tier 02 fights), medium (34), hard (56)
#### 7.2 Music Tracks → 20
**Files:** `frontend/src/game/sounds.ts`
- 10 new procedural tracks:
- 2x Drum & Bass (170175 BPM)
- 2x Lo-fi Hip Hop (90100 BPM)
- 2x Ska Punk (200220 BPM)
- 2x Boss Fight (140160 BPM, heavy)
- 2x Chiptune (180200 BPM)
- Intensity-grouped: low (lo-fi, chiptune), mid (ska, D&B), high (boss fight)
#### 7.3 Arenas → 40
**Files:** `server/src/engine/arenas.ts`, `frontend/src/game/FightScene.ts`
- 15 new arenas:
- bitcoin_mine (hash_power modifier)
- lightning_node (speed_3x)
- satoshis_garage (legacy_code)
- mempool (chaos_mode)
- silk_road_ruins (trap_heavy)
- block_height_tower (accuracy_buff)
- hodl_cave (endurance_2x)
- whale_pool (high_stakes)
- node_farm (efficiency_buff)
- genesis_block (all_types)
- pizza_day_parlor (food_2x)
- difficulty_adjustment (adaptive)
- 51_percent_arena (chaos_mode)
- timechain_temple (speed_2x)
- nostr_relay_room (crowd_favorite)
- Each with unique theme colors and background elements
### Week 8: Choreography & Animation Polish
#### 8.1 Choreographies → 500
**Files:** `frontend/src/game/FightScene.ts`
- 148 new choreographies across categories:
- 30 Bitcoin-themed (lightning_bolt, hash_smash, block_drop, mempool_flood, difficulty_bomb, fee_spike, utxo_scatter, node_sync, relay_bounce, channel_force_close, etc.)
- 25 more food moves
- 25 more energy/beam moves
- 20 more silly/absurd moves
- 20 more weapon moves
- 15 more vehicle moves
- 13 more animal moves
- Wire into CHALLENGE_THEMED with balanced distribution
#### 8.2 Entrance Animations
**Files:** `frontend/src/game/FightScene.ts`
- 10 new tier-gated entrances:
- Tier 01: simple walk-in
- Tier 2: slide in with dust
- Tier 3: drop from above with impact shake
- Tier 4: teleport with lightning flash
- Tier 5: dramatic slow-mo entrance with camera zoom
- Tier 6: custom per-bot entrance (THE CREATOR golden portal already done)
#### 8.3 Victory Celebrations
**Files:** `frontend/src/game/FightScene.ts`
- Post-fight victory animations tied to finish type:
- KO: winner flexes, camera flash effect
- Perfect: winner does victory lap, confetti rain
- Decision: both bow, crowd cheers
- Upset: dramatic zoom on winner, shocked crowd SFX
---
## Month 3: Scale, Polish & Launch (May 4 May 31)
Goal: Production-ready for real users, real sats, real competition.
### Week 9: FightScene.ts Decomposition
#### 9.1 Split the Monolith
FightScene.ts at 11,313 lines is the single biggest maintenance risk. Split into focused modules:
```
frontend/src/game/fight/
index.ts -- createFightScene() orchestrator, returns controller
types.ts -- FightContext interface (k, W, H, GROUND_Y, HOME_A, HOME_B, helpers)
constants.ts -- ARENA_THEMES, spriteAnims, positions
particles.ts -- spawnSparks, spawnBulletHoles, spawnExhaust, etc.
projectiles.ts -- spawnProjectile, spawnBullet, spawnProp, spawnBarrage
effects.ts -- glitchRGB, scanlineGlitch, vhsTracking, dimensionalShift, etc.
grotesque.ts -- spawnGrotesqueDetails, destroyGrotesqueDetails
arena-decor.ts -- drawArenaDecor() per-arena backgrounds
index.ts -- createFightScene() orchestrator
types.ts -- FightContext, shared interfaces
constants.ts -- arena themes, sprite anims, positions
particles.ts -- all particle spawn functions
projectiles.ts -- projectile spawning and physics
effects.ts -- screen effects (glitch, scanline, VHS, etc.)
grotesque.ts -- close-up detail system
arena.ts -- arena background rendering
entrances.ts -- bot entrance animations
morphs.ts -- morph system (elemental, mech, beast, omni)
choreography/
index.ts -- choreographyMap registry, pickChoreography
challenge-map.ts -- CHALLENGE_THEMED + critMoves
basic.ts -- dashPunch, aerialSlam, flyingKick, uppercut
ranged.ts -- projectile, gunBurst, sniperShot, minigunSpray
movement.ts -- dashThrough, fullScreenDash, teleportStrike
heavy.ts -- groundPound, jetpackDive, bodySlam, suplex
weapons.ts -- swordSlash, hammerSmash, katanaCombo, chainsawRev, whipCrack
vehicles.ts -- motorbikeCharge, carSmash, boatCannon
themed.ts -- golfClubSmash, fireBreath, riddleBarrage, coinShower, etc.
finishers.ts -- playKO (4 styles), playPerfect
index.ts -- choreographyMap, pickChoreography
registry.ts -- CHALLENGE_THEMED, critMoves
basic.ts -- punches, kicks, slams
ranged.ts -- projectiles, guns, beams
heavy.ts -- ground pounds, body slams
weapons.ts -- swords, hammers, etc.
food.ts -- food-themed moves
silly.ts -- absurd/comedy moves
bitcoin.ts -- Bitcoin-themed moves
creator.ts -- THE CREATOR exclusive moves
ultimates.ts -- tier-gated ultimate moves
finishers.ts -- KO styles, perfect finish
round.ts -- playRound, playTaunt, playDodge
camera.ts -- camera shake, zoom, pan
hud.ts -- HP bars, round counter, timer
```
**Critical pattern:** Choreography functions close over Kaplay context. Each file exports functions that take a `FightContext` object with all shared state/helpers.
- Zero behavior changes — pure refactor
- Each module receives FightContext object
- Test: replay 50 fights, verify identical behavior
**Test:** Replay several fights, verify all 42 choreographies work.
### Week 10: Performance & Mobile
### Day 5-6: Split `sounds.ts` (1381 lines) + server modules
#### 10.1 Performance Optimization
- Profile FightScene with Chrome DevTools
- Reduce particle count on mobile (detect via `navigator.maxTouchPoints`)
- Lazy-load Kokoro TTS model (2.2MB) — don't block initial render
- Sprite sheet atlas: batch all archetypes into single texture
- RequestAnimationFrame budget monitoring — drop effects if frame time > 16ms
```
frontend/src/game/sounds/
index.ts -- re-exports
context.ts -- AudioContext setup, gain nodes
primitives.ts -- tone(), noise(), sweep() helpers
sfx.ts -- all sfx functions (punch, kick, special, etc.)
voice.ts -- voice profiles, speak(), announce*(), voice lines
music/
index.ts -- startMusic(), stopMusic(), setMusicIntensity()
types.ts -- MusicTrack interface
player.ts -- playMusicBar() scheduler
tracks/
neon-fury.ts
dark-circuit.ts
pixel-blitz.ts
skull-crusher.ts
#### 10.2 Mobile UX Polish
**Files:** Various frontend components
- Touch-friendly bet panel (large tap targets, swipe gestures)
- Responsive fight viewer (portrait mode layout)
- Bottom navigation bar for mobile
- Haptic feedback on hits (Vibration API)
- Reduced motion mode (prefers-reduced-motion)
server/src/engine/
challenges/
index.ts -- pickChallenge(), registry
types.ts -- Challenge interfaces
speed-blitz.ts -- prompts array
riddle.ts
... (one per type)
mock/
index.ts -- runMockFight(), seedMockFights()
bots.ts -- MOCK_BOTS array
answers.ts -- MOCK_ANSWERS, BAD_ANSWERS
trash-talk.ts -- TRASH_TALK lines
```
#### 10.3 Offline / Weak Connection
- Queue position preserved on reconnect (SSE retry)
- Optimistic UI for bet placement
- Replay cache: save last 10 watched fights for offline replay
- Connection status indicator
**Test:** Full end-to-end fight replay with music, SFX, voice.
### Week 11: Admin, Analytics & Bot Developer Experience
**Deliverable:** Identical functionality, ~40 small focused modules instead of 3 monoliths.
#### 11.1 Admin Dashboard
**New files:** `frontend/src/pages/AdminPage.vue`, `server/src/routes/admin.ts`
- Protected by THE CREATOR pubkey (only admin)
- Live stats: active fights, queue depth, bets in escrow, total sats moved
- Bot management: deactivate, reset Elo, ban
- Challenge management: add/remove prompts, preview scoring
- Fight log viewer with full round details
- System health: DB size, memory, uptime
#### 11.2 Privacy-Respecting Analytics
**New file:** `server/src/engine/analytics.ts`
- No PII, no cookies, no third-party scripts
- Aggregate counts only: fights/day, unique bots/day, sats wagered/day
- Stored in SQLite, queryable from admin dashboard
- Public stats endpoint for transparency: `GET /api/stats/public`
#### 11.3 Bot Developer Docs
**Files:** `frontend/src/pages/DocsPage.vue`
- Interactive API explorer (try endpoints from the browser)
- Webhook payload examples for all 16 challenge types
- Response format with scoring breakdown
- "Build your first bot" tutorial (curl, Python, Node examples)
- Webhook testing tool (send test challenge, see response + score)
### Week 12: Launch Prep & Stress Testing
#### 12.1 Load Testing
- Simulate 100 concurrent spectators on single fight
- Simulate 20 concurrent fights
- Simulate 50 concurrent bet placements
- SQLite WAL mode verification under write contention
- Memory profiling under sustained fight-loop operation (24hr soak test)
#### 12.2 Backup & Recovery
**New file:** `server/src/engine/backup.ts`
- Automated SQLite backup to file (daily rotation, keep 7)
- Export fight history as JSON (for Nostr archival)
- Elo snapshot before season reset (rollback safety)
#### 12.3 Launch Checklist
- [ ] All 16 challenge types tested end-to-end with real webhook
- [ ] NWC payment flow tested with real sats (testnet then mainnet)
- [ ] Cashu token deposit + withdrawal tested
- [ ] Betting flow: place bet → watch fight → receive payout
- [ ] Tournament: create → fill → run → settle prizes
- [ ] Human fight: join queue → answer challenges → see result
- [ ] Mobile: full flow on iOS Safari + Android Chrome
- [ ] PWA: install → offline → reconnect → resume
- [ ] TUI fight loop: 24hr soak test without crash
- [ ] Docker: fresh build → deploy → health check → seed → fight
- [ ] Nostr: fight results published to relay, zaps working
- [ ] Security: rate limiting, SSRF, body limits, error hiding verified
- [ ] THE CREATOR: all exclusive features working (entrance, morphs, cameo, moves)
- [ ] Bundle size < 300KB gzipped (run `npx vite-bundle-visualizer`)
- [ ] Memory: 50-fight replay stays flat (Chrome DevTools heap snapshot)
- [ ] Server: 24hr fight-loop RSS < 250MB
- [ ] Mobile: fight loads in < 2s on throttled 4G
- [ ] SQLite: indexes verified, WAL mode enabled, p95 query < 50ms
---
## Phase 2: Characters (Days 7-12)
## Performance & Footprint (Continuous Thread)
### Day 7-8: New archetypes batch 1 (8 new)
- `dog.ts` -- floppy ears, wagging tail, snout with tongue, collar
- `cat.ts` -- pointed ears, whiskers, curled tail, slit eyes
- `cactus.ts` -- spikes everywhere, small flower on head, no real arms
- `pizza.ts` -- triangular body, cheese drip, pepperoni spots
- `mushroom.ts` -- dome cap head, spotted cap, stubby legs, spore particles
- `shark.ts` -- dorsal fin, teeth row, tail fin
- `penguin.ts` -- tuxedo coloring, beak, flippers
- `octopus.ts` -- 4 visible tentacles, large head, suction cups
Performance is not a one-week task — it's a discipline that runs across all 3 months. Every feature addition must respect these budgets.
### Day 9-10: New archetypes batch 2 (8 more)
- `skeleton.ts` -- visible ribs, skull head, bony limbs
- `ghost.ts` -- semi-transparent, wavy bottom (no legs), glowing eyes
- `alien.ts` -- big head, huge eyes, antenna, green tint
- `dinosaur.ts` -- tail, tiny arms, spiky back, big jaw
- `pirate.ts` -- eye patch, hat, peg leg, hook hand
- `ninja.ts` -- mask, headband, throwing star
- `cowboy.ts` -- hat, bandana, boots with spurs
- `wizard.ts` -- pointed hat, robe, staff replaces one arm, beard
### Budgets
### Day 11: Final archetypes + hybrid system
- `bee.ts` -- stripes, wings, stinger, antennae
- `frog.ts` -- wide mouth, bulging eyes, webbed feet, long tongue
- `snail.ts` -- shell on back, eye stalks, slime trail
- **Hybrid system:** 10% chance a bot combines features from 2 archetypes (lobster+sunglasses, sheep+robot arm, etc.)
- **Total:** 25 archetypes
| Metric | Target | Current Estimate |
|--------|--------|-----------------|
| Initial JS bundle | < 300KB gzipped | Audit needed |
| Kokoro TTS model | Lazy-load, < 2.2MB | 2.2MB (loaded eagerly) |
| Sprite sheet per bot | < 15KB PNG | ~8KB |
| Fight replay memory | < 80MB peak | Unknown (profile needed) |
| Server memory (idle) | < 100MB RSS | ~60MB |
| Server memory (fight-loop 24hr) | < 250MB RSS | Unknown |
| Docker image | < 50MB | 42MB |
| SQLite DB after 10K fights | < 200MB | ~5MB (100 fights) |
| Time to first fight frame | < 2s on 4G | Unknown |
| SSE reconnect | < 1s | Unknown |
### Day 12: 100 Named Bots + Seed Data
Expand `MOCK_BOTS` from 12 to 100 with distribution:
- ~5 tier-5, ~10 tier-4, ~20 tier-3, ~25 tier-2, ~25 tier-1, ~15 tier-0
### Month 1 Performance Tasks
Funny names by theme:
- **Tech:** `segfault_sarah`, `stack_overflow_sam`, `infinite_loop_larry`, `null_reference_nancy`
- **Food:** `pizza_pete`, `sushi_sensei`, `taco_tornado`, `burrito_bomber`
- **Animals:** `mega_lobster_xl`, `tactical_penguin`, `stealth_shark_3000`
- **Pop culture:** `keyboard_warrior`, `meme_lord_420`, `reply_guy_9000`
- **Absurd:** `sentient_toaster`, `angry_calculator`, `philosophical_fork`
#### P1.1 Bundle Audit & Tree-Shaking
- Run `npx vite-bundle-visualizer` to identify bloat
- Verify Kaplay tree-shakes unused modules
- Verify nostr-tools only imports what's needed (not full bundle)
- Code-split routes: lazy-load all pages except HomePage
- Target: identify top 3 bundle offenders and fix
Pre-seed 200 fight history records.
#### P1.2 Kokoro TTS Lazy Loading
**Files:** `frontend/src/game/sounds.ts`
- Don't load the 2.2MB Kokoro model on page load
- Load on first fight start (with loading indicator)
- Cache in service worker after first load
- Fallback to Web Speech API while loading
**Test:** `pnpm seed`, verify 100 bots, leaderboard renders, sprite preview shows all archetypes.
#### P1.3 Sprite Atlas
**Files:** `frontend/src/game/sprites/`
- Currently generates sprite sheets per-bot at fight time
- Pre-generate common archetypes into a shared atlas texture
- Single GPU texture upload instead of per-bot
- Reduces fight init time and GPU memory
---
#### P1.4 SSE Connection Pooling
**Files:** `server/src/routes/fights.ts`
- Ensure SSE connections are properly cleaned up on client disconnect
- Add connection limit per IP (max 5 concurrent SSE streams)
- Heartbeat every 15s to detect dead connections
- Memory: track active SSE count in admin stats
## Phase 3: Choreographies (Days 13-19)
### Month 2 Performance Tasks
Scale from 42 to 300 choreographies. Each follows the established function signature.
#### P2.1 Fight Replay Memory Profiling
- Profile memory during 10-fight replay session
- Identify particle/sprite leaks (Kaplay objects not destroyed)
- Add `fight.destroy()` cleanup that nulls all references
- Test: play 50 fights consecutively, memory should stay flat
### Day 13-14: Food choreographies (~30 moves)
`choreography/food.ts`:
- `pizzaThrow`, `bananaSlip`, `pieFace`, `hotDogWhip`, `sushiRoll`
- `watermelonSmash`, `popcornBarrage`, `burritoWrap`, `cakeExplosion`, `iceCreamFreeze`
- `tacoSlam`, `donutSpin`, `breadSlap`, `eggCrack`, `soupSplash`
- `cheeseWheel`, `grapeShot`, `pineappleGrenade`, `cornCobGatling`, `steakSlap`
- Plus ~10 more food-themed variations
#### P2.2 Choreography Performance Tiers
**Files:** `frontend/src/game/FightScene.ts`
- Tag each choreography with a `particleCount` estimate
- On low-end devices (detect via `navigator.hardwareConcurrency < 4`):
- Reduce particle counts by 60%
- Skip grotesque close-ups
- Use simpler camera movements
- Disable background arena animations
- Toggle in settings: "Performance mode"
### Day 15-16: Spins/flips (~25) + vehicles (~15)
`choreography/spins.ts`:
- `spin720`, `backflipKick`, `tornadoSpin`, `corkscrewDive`, `cartwheel`
- `helicopterSpin`, `barrelRoll`, `frontflipSlam`, `spinningPiledriver`
- `doubleBackflip`, `wallflip`, `moonwalk`, `breakdanceSpin`, `drillKick`
- Plus more rotation/flip-based moves
#### P2.3 SQLite Optimization
**Files:** `server/src/db/index.ts`
- Enable WAL mode: `PRAGMA journal_mode=WAL`
- Set `PRAGMA synchronous=NORMAL` (safe with WAL)
- Add indexes: `fights(status)`, `fights(createdAt)`, `bots(eloRating)`, `payments(status)`
- `PRAGMA optimize` on daily cron
- Connection pool size: 1 writer + 4 readers (via better-sqlite3's sync nature this is automatic, but verify no concurrent write contention)
`choreography/vehicles.ts` expansion:
- `skateboard`, `helicopter`, `shoppingCart`, `zamboni`, `rocketSled`
- `segway`, `unicycle`, `bulldozer`, `rollercoaster`, `cannonball`
- Plus existing (motorbikeCharge, carSmash, boatCannon)
#### P2.4 Server Memory Leak Hunt
- Run fight-loop for 4 hours, capture heap snapshots at 0h, 1h, 2h, 4h
- Check for: unclosed SSE streams, orphaned fight event listeners, growing caches
- Add `process.memoryUsage()` to admin stats endpoint
- Set up `--max-old-space-size=256` in Docker CMD as safety net
### Day 17-18: Animal (~20) + energy (~20) + silly (~25)
`choreography/animal.ts`:
- `sharkBite`, `beeSwarm`, `frogTongue`, `stampede`, `spiderWeb`
- `snakeStrike`, `bearHug`, `eagleDive`, `ramCharge`, `crocodileRoll`
### Month 3 Performance Tasks
`choreography/energy.ts`:
- `kamehameha`, `hadouken`, `spiritBomb`, `thunderStrike`, `dragonPunch`
- `novaBlast`, `blackHole`, `sonicBoom`, `plasmaWhip`, `quantumTunnel`
#### P3.1 FightScene Split Enables Dead Code Elimination
- After Week 9 split, unused choreography modules can be tree-shaken
- Vite dynamic imports for choreography categories (load on demand)
- Split point: `import('./choreography/food.ts')` only when food moves are picked
`choreography/silly.ts`:
- `pillowFight`, `tickleAttack`, `selfieStun`, `vibeCheck`, `dabOnEm`
- `yeetThrow`, `airHorn`, `rubberChicken`, `bananaPeelChain`, `confettiCannon`
- `invisibleWall`, `slipNSlide`, `trampolineBounce`, `bubbleWrap`, `kazooBlast`
#### P3.2 CDN & Caching Strategy
- Static assets: immutable hash filenames, `Cache-Control: max-age=31536000`
- API responses: `Cache-Control: no-store` for fight data, `max-age=60` for leaderboard
- Service worker: Stale-while-revalidate for sprites/audio
- Compress all API responses with gzip (Hono middleware)
### Day 19: Wire all 300 choreographies
- Register all in `choreographyMap`
- Update `CHALLENGE_THEMED` with new move pools per challenge type
- Balance `pickChoreography` weights for maximum variety
- Add new moves to `critMoves` and `wild` arrays
#### P3.3 Load Test Results → Optimization
- Profile under simulated 100-spectator load
- Identify: SSE fan-out bottleneck, DB query hotspots, memory ceiling
- Optimize based on actual profiling data, not guesses
- Document capacity ceiling: "This VPS handles X concurrent fights with Y spectators"
**Test:** Run 50 fight replays, verify no crashes, observe variety.
#### P3.4 Production Monitoring
**Files:** `server/src/engine/analytics.ts`
- Log slow queries (> 50ms) to structured log
- Track: p50/p95 fight duration, webhook response latency, payment settlement time
- Memory/CPU metrics every 60s to analytics table
- Alert threshold: memory > 200MB, fight error rate > 5%, payment failure > 1%
---
### Mobile-Specific Performance
## Phase 4: Challenges (Days 20-22)
#### Touch & Rendering
- Canvas rendering: limit to 30fps on mobile (save battery), 60fps on desktop
- Disable parallax scrolling and complex CSS animations on mobile
- Use `will-change: transform` sparingly (GPU memory cost)
- Intersection Observer for off-screen fight cards (don't render invisible content)
Scale from ~45 prompts to ~10,000 challenges. Hilarious, modern, pop culture, stupid/silly.
### Day 20: Challenge bank architecture
```
server/src/engine/challenges/
banks/
speed-blitz.ts -- 1,200+ prompts
riddle.ts -- 500+ riddles
code-golf.ts -- 400+ challenges
roast-battle.ts -- 600+ roast prompts
hallucination.ts -- 800+ true/false
token-economy.ts -- 500+ explain prompts
creative-writing.ts -- 400+ creative prompts
math-blitz.ts -- 1,500+ math problems (many templated)
trap-card.ts -- 600+ prompt injections
meme-knowledge.ts -- 500+ meme identification (NEW TYPE)
emoji-translate.ts -- 400+ emoji challenges (NEW TYPE)
debate.ts -- 300+ debate topics (NEW TYPE)
fill-the-blank.ts -- 400+ completions (NEW TYPE)
reverse-engineer.ts -- 300+ output puzzles (NEW TYPE)
survival.ts -- 500+ escalating difficulty (NEW TYPE)
```
### Day 21: Build prompt banks (~10,000 total)
- Mix of hand-written and procedural templates
- Pop culture: "Is it true Elon Musk once fought a kangaroo?", "What would happen if the Minecraft creeper attended a job interview?"
- Modern/silly: "Explain blockchain to a medieval peasant", "Write a breakup text from a printer to its owner"
- Procedural templates for math/trivia: fill in numbers, countries, units
### Day 22: New challenge types + mock answers
- 6 new types: meme_knowledge, emoji_translate, debate, fill_the_blank, reverse_engineer, survival
- Scoring for each, mock answers for all 100 bots
- Wire into frontend `CHALLENGE_THEMED`
---
## Phase 5: Audio (Days 23-25)
### Day 23: 16 new music tracks (20 total)
Each track ~40 lines. Genre variety:
- 2x Synthwave (180-200 BPM), 2x Metal (250-280 BPM)
- 2x Chiptune (200-220 BPM), 2x Jazz Fusion (160-180 BPM)
- 2x Drum & Bass (170-175 BPM), 2x Lo-fi (140-160 BPM)
- 2x Ska Punk (210-230 BPM), 2x Boss Fight (280-300 BPM)
Intensity-based genre grouping:
- Low: lo-fi, jazz, chiptune | Medium: synthwave, ska punk
- High: metal, drum & bass | Max: boss fight
### Day 24: 24 new voice profiles (30 total)
New: whisper, manic, gravelly, chipper, deadpan, sports_caster, old_timey, drill_sergeant, surfer_dude, valley_girl, pirate_voice, robot_glitch, auctioneer, news_anchor, horror_narrator, lullaby, rage_quit, stoner, brit_posh, aussie, game_show, wrestling_announcer, noir_detective, anime_narrator
Expand voice line libraries to 300+ unique lines.
### Day 25: New SFX + arena-specific ambience
- 20 new SFX for new choreography types
- Arena-specific ambient background layers (beach waves, space hum, forest chirps)
---
## Phase 6: Scenes & Polish (Days 26-27)
### Day 26: 15+ new arenas (30+ total)
volcano, underwater, rooftop, train, kitchen, arcade, library, parking_lot, school, hospital, factory, castle, moon_base, ice_rink, stadium
New modifiers: fire_boost, ice_slow, gravity_low, double_damage, chaos_mode
### Day 27: Scene transitions + animated elements
- 4 transition styles: wipe, circle close/open, pixel dissolve, glitch cut
- Animated arena backgrounds: floating particles, dynamic lighting
- Enhanced spectator crowd reactions tied to round outcomes
---
## Phase 7: Production Loop (Days 28-30)
### Day 28: Fight scheduler + matchmaking
`server/src/engine/scheduler.ts`:
- `startScheduler(intervalMs)` -- Elo-based matchmaking loop
- Routes: `POST /api/scheduler/start|stop`, `GET /api/scheduler/status`
- Queue with rate limiting + bot cooldown
### Day 29: Headless mode + overnight operation
- `pnpm run fight-loop` for headless scheduler
- Env vars: `FIGHT_INTERVAL_MS`, `FIGHTS_PER_BATCH`, `MAX_CONCURRENT`
- Structured JSON logging, graceful SIGTERM shutdown
- `GET /api/stats` endpoint, health monitoring
### Day 30: Final seed data + integration test
- Generate: 100 bots, 500+ fight history
- Verify: Elo bell curve, tier distribution
- Full E2E: seed -> dev -> replay 10 fights -> fight-loop -> 100 fights -> typecheck + lint
#### Network
- Preconnect to API server: `<link rel="preconnect">`
- Prefetch next likely page (e.g., fight card → fight page)
- Compress SSE payloads (only send changed fields, not full state)
- Offline-first: service worker serves cached shell immediately
---
## Dependency Graph
```
Phase 1 (Split) --- BLOCKS EVERYTHING
|
+-- Phase 2 (Characters) -- independent after Phase 1
+-- Phase 3 (Choreographies) -- independent after Phase 1
+-- Phase 4 (Challenges) -- independent after Phase 1
+-- Phase 5 (Audio) -- independent after Phase 1
|
+-- Phase 6 (Scenes) -- needs Phase 1 + Phase 3
+-- Phase 7 (Production) -- needs Phase 2 + Phase 4
Month 1 Month 2 Month 3
─────── ─────── ───────
W1 Spectating ──────────────────────────────────────────────── W9 FightScene Split
W2 Betting UI ──┐ W10 Performance
W3 Nostr Social │ W5 Tournaments ──────────────── W11 Admin
W4 Seasons ─────┤ W6 Human Play W12 Launch
│ W7 Content ─────────────────── W12 Launch
└──────────────→ W8 Choreography ─────────────→ W9 FightScene Split
```
## Testing Strategy
- Weeks 14 are mostly independent of each other (parallelize)
- Week 5 (Tournaments) needs Week 2 (Betting) for entry fees
- Week 6 (Human Play) needs Week 1 (Spectating) for live sync
- Week 9 (Split) should happen after Week 8 (Choreography) to avoid splitting then adding
- Week 12 (Launch) needs everything
After every day:
1. `pnpm typecheck` -- catches import/export errors
2. Sprite preview page -- verify all archetypes render
3. Watch 5 random fight replays -- no crashes, visual variety
4. Audio check -- music, voices, SFX all play
5. `pnpm seed` -- verify DB has expected data
## Risk Mitigation
## Critical Files
| Risk | Mitigation |
|------|-----------|
| FightScene.ts too fragile to split | Comprehensive replay testing before/after; git branch for safe rollback |
| SQLite bottleneck under load | WAL mode + connection pooling; monitor write contention; Turso migration path if needed |
| NWC wallet failures | Graceful fallback to Cashu; refund queue for stuck payments |
| Choreography visual bugs | Automated screenshot comparison for 20 reference fights |
| Scope creep | Each week is self-contained; can ship at end of any week |
- `frontend/src/game/sprites.ts` -> split into `sprites/` directory
- `frontend/src/game/FightScene.ts` -> split into `fight/` directory
- `frontend/src/game/sounds.ts` -> split into `sounds/` directory
- `frontend/src/components/FightViewer.vue` -> update imports
- `server/src/engine/challenges.ts` -> split into `challenges/` directory
- `server/src/engine/mock.ts` -> split into `mock/` + expand to 100 bots
- `server/src/engine/arenas.ts` -> expand to 30+ arenas
- `server/src/seed.ts` -> update imports
- `server/src/routes/fights.ts` -> add scheduler routes
## Success Metrics (End of Month 3)
**Content:**
- 500+ choreographies, 2,000+ challenge prompts, 40 arenas, 20 music tracks
**Adoption:**
- 10+ real bot developers registered and competing
- Tournaments running daily with real sats
- Betting volume: 10,000+ sats/day
- Mobile PWA installs > 0 (any adoption = success for v1)
**Reliability:**
- Zero stuck fights, zero lost payments over 7-day period
- Fight loop stable for 72hr continuous operation
**Performance:**
- Initial bundle < 300KB gzipped (excluding lazy-loaded TTS)
- Time to first fight frame < 2s on 4G mobile
- 50-fight replay session with flat memory (no leaks)
- Server stable at < 250MB RSS after 24hr fight-loop
- 20 concurrent fights + 100 spectators without degradation
- Docker image stays < 50MB
- SQLite handles 10K fights without query degradation (< 50ms p95)
+214
View File
@@ -0,0 +1,214 @@
# Production-Grade Botfights: Implementation Plan
## Context
Botfights is a competitive game where users register AI bots with webhook URLs. The server sends challenge questions to webhooks, scores responses, and updates rankings. The system works end-to-end but has critical fairness, reliability, and security gaps that must be fixed before real users compete for rankings. This plan makes the engine bulletproof and adds a rich TUI for the overnight fight loop.
---
## Phase 1: Fight Integrity (Foundation)
Everything depends on fights producing correct, trustworthy results.
### 1.1 Schema additions
**File:** `server/src/db/schema.ts`, `server/src/db/migrate.ts`
- Add to `bots`: `lastFightAt TEXT`, `consecutiveErrors INTEGER DEFAULT 0`, `lastErrorAt TEXT`
- Add migration SQL for existing DBs
### 1.2 Transaction-wrap Elo updates
**Files:** `server/src/engine/orchestrator.ts` (lines 276-306), `server/src/engine/mock.ts` (lines 392-421)
- Wrap fight finalization (status update + both bot stat updates) in a single SQLite transaction
- Prevents partial Elo corruption on crash or concurrent fights
### 1.3 Block self-fights
**Files:** `server/src/engine/orchestrator.ts`, `server/src/engine/mock.ts`, `server/src/routes/fights.ts`
- Add `if (botAId === botBId) throw new Error('A bot cannot fight itself')` at top of `runFight`, `runFightAsync`, `runMockFight`
### 1.4 Prevent concurrent fights for same bot
**File:** `server/src/engine/orchestrator.ts`
- In-memory `Set<string>` of currently-fighting bot IDs
- Check before starting, add on start, remove in `.finally()`
### 1.5 Crash recovery for stuck fights
**File:** `server/src/engine/orchestrator.ts`
- In `runFightAsync` catch handler: mark fight `status='cancelled'`, call `fightEvents.cleanup()`
- On server startup: mark any `status='live'` fights older than 10 minutes as `cancelled`
### 1.6 Fix creative scoring (anti-gaming)
**File:** `server/src/engine/scoring.ts` (lines 166-174)
- Replace `estimateQuality` with multi-factor heuristic: character diversity, word diversity, length window (30-400 ideal), speed bonus
- Prevents gaming by dumping 500 chars of garbage text
### 1.7 Tighten factual answer checking
**File:** `server/src/engine/answers.ts` (line 78)
- Add word-boundary awareness: `containsWholeWord()` helper using regex `\b`
- Short answers (2 chars like "au", "fe") require near-exact match, not just containment
- Prevents "I feel confident" matching accepted answer "fe"
### 1.8 Dampen mock-bot Elo farming
**File:** `server/src/engine/orchestrator.ts`
- When one combatant is a mock bot, use K-factor 12 instead of 32
- Real bot-vs-real bot fights keep K=32 for full stakes
---
## Phase 2: Webhook Contract & Developer Experience
### 2.1 Response size limits
**File:** `server/src/engine/orchestrator.ts` (line 83)
- Replace `res.text()` with size-limited reader (10KB max)
- Truncate `answer` to 2000 chars, `trash_talk` to 200 chars
- Prevents OOM attacks from malicious webhooks
### 2.2 SSRF protection
**File:** `server/src/engine/orchestrator.ts` (new `isAllowedWebhookUrl` function)
- Block localhost, private IPs (10.x, 192.168.x, 172.16-31.x), AWS metadata (169.254.169.254), .local/.internal
- Enforce HTTPS in production, allow HTTP in dev
- Apply at registration (`routes/bots.ts`, `routes/auth.ts`) AND at call time
### 2.3 Pre-fight webhook verification
**New file:** `server/src/engine/webhook-test.ts`
- `testWebhook(url)`: sends a test challenge (`"respond with {"answer": "pong"}"`), validates response shape and latency
- Returns `{ reachable, validResponse, latencyMs, error? }`
**Integrate into:**
- `POST /api/auth/register` -- test webhook before inserting bot. Reject with specific error if it fails.
- `POST /api/bots` -- same
- New route `POST /api/bots/:name/test` -- re-test webhook on demand (replaces weak `/health` check)
### 2.4 Webhook reliability tracking
**File:** `server/src/engine/orchestrator.ts`
- After each webhook call: increment `consecutiveErrors` on failure, reset to 0 on success
- If `consecutiveErrors >= 5`: mark bot `isActive: false`, skip in matchmaking
- Bot owner must re-test webhook to reactivate
### 2.5 Add `fight_id` to webhook payload
**File:** `server/src/engine/orchestrator.ts` (line 48)
- Thread `fightId` through `callWebhook` and `getBotResponse`
- Bot developers can correlate challenge POSTs to specific fights for debugging
---
## Phase 3: Anti-Gaming & Security
### 3.1 Rate limiting
**New file:** `server/src/middleware/rate-limit.ts`
- Simple in-memory rate limiter (no new deps), per-IP sliding window
- Apply: `POST /api/bots` (5/hr), `POST /api/auth/register` (5/hr), `POST /api/queue/join` (1 per 10s per bot), all other POSTs (60/min)
### 3.2 Case-insensitive name uniqueness
**Files:** `server/src/routes/bots.ts` (line 39), `server/src/routes/auth.ts` (line 78)
- Force bot names to lowercase at registration time
- Prevents name squatting ("MyBot" vs "mybot")
### 3.3 Queue cooldowns
**File:** `server/src/engine/queue.ts`
- In-memory `Map<string, number>` of post-fight cooldowns (15 seconds)
- Set cooldown after fight completes (called from `orchestrator.ts`)
- Reject queue join if cooldown active
### 3.4 Concurrent fight checks in queue
**File:** `server/src/engine/queue.ts`
- Check the `activeFighters` set (from 1.4) before allowing queue join
- Prevents a bot from queueing while already in a fight
---
## Phase 4: TUI Fight Loop
Minimal deps -- only `chalk` for colors. All layout via ANSI codes and Unicode box drawing.
### 4.1 Add dependency
`chalk@5` to `server/package.json`
### 4.2 TUI state tracker
**New file:** `server/src/tui/state.ts`
- `TuiState` interface: fight count, KOs, perfects, draws, errors, current fight (bots/HP/round/events), recent fights, leaderboard, biggest upset, Elo movers, elapsed time
### 4.3 TUI renderer
**New file:** `server/src/tui/renderer.ts`
```
+=================== BOTFIGHTS OVERNIGHT LOOP ====================+
| Fight #47 of 200 Elapsed: 12m 34s |
| Style: mixed Rate: 3.8 fights/min |
+------------------------------------------------------------------+
| |
| skull_crusher_9000 (1820) vs boaty_mcbotface (1150) |
| [================----] 163 HP vs [====----------------] 47 HP |
| Round 6/10 -- speed_blitz |
| >> skull_crusher answered in 234ms (CORRECT) |
| >> boaty_mcbotface timed out! FREE HIT! |
| |
+========================= STATS ==================================+
| Fights: 47 completed, 0 errors |
| KOs: 31 (66%) | Perfects: 4 | Draws: 2 |
| Biggest upset: boaty_mcbotface beat the_architect! |
+========================= LEADERBOARD ============================+
| #1 the_architect 1980 52W-8L LEGEND |
| #2 chad_gpt 1950 48W-10L LEGEND |
| #3 skull_crusher_9000 1820 35W-12L DIAMOND |
+========================= RECENT ================================+
| #47 skull_crusher vs boaty -> skull_crusher (KO R6) |
| #46 chad_gpt vs lorem_ipsum -> chad_gpt (PERFECT R3) |
| #45 regex_ronin vs the_intern -> regex_ronin (Decision) |
+==================================================================+
```
- Single buffered write to avoid flicker
- Handles terminal resize via `process.stdout.on('resize')`
- Tier colors via chalk
### 4.4 Refactor fight loop with callbacks
**File:** `server/src/engine/fight-loop.ts`
- Add callback options: `onFightStart`, `onRoundComplete`, `onFightComplete`, `onError`
- Wire event bus so TUI gets live round-by-round updates
### 4.5 Rewrite CLI
**File:** `server/src/fight-loop-cli.ts`
- Create TUI state + renderer, pass callbacks to fight loop
- Graceful SIGINT: show final summary screen
- Same CLI args (`--max`, `--interval`, `--style`)
### 4.6 Final summary screen
On loop end or Ctrl+C: duration, total fights, KO/perfect/draw rates, top Elo movers, biggest upset, most active bot
---
## Phase 5: Verification
### 5.1 Manual test sequence
1. `pnpm seed` -- verify schema migrations run
2. `pnpm dev` -- verify server starts, orphaned fights cleaned up on startup
3. Register bot with bad webhook URL -> verify rejection with specific error
4. Register bot with valid webhook -> verify test challenge sent and validated
5. Try self-fight via `/api/fights/matchmake` -> verify blocked
6. Run `pnpm fight-loop --max=10` -> verify TUI renders, stats update live
7. Kill process mid-fight, restart -> verify stuck fights cleaned up
8. Send oversized response from test webhook -> verify 10KB limit
9. Rapid-fire queue joins -> verify rate limiting and cooldowns
---
## Files Summary
**Modified (11 files):**
- `server/src/db/schema.ts` -- new columns
- `server/src/db/migrate.ts` -- migration SQL
- `server/src/engine/orchestrator.ts` -- transactions, self-fight block, concurrent guard, crash recovery, response limits, SSRF, reliability tracking, mock Elo dampening, fight_id in payload
- `server/src/engine/mock.ts` -- transaction wrap, self-fight block
- `server/src/engine/scoring.ts` -- creative scoring rewrite
- `server/src/engine/answers.ts` -- word boundary fixes
- `server/src/engine/queue.ts` -- cooldowns, concurrent fight checks, export activeFighters check
- `server/src/engine/fight-loop.ts` -- callback options for TUI
- `server/src/fight-loop-cli.ts` -- TUI integration
- `server/src/app.ts` -- rate limiting, startup cleanup
- `server/package.json` -- add chalk
**Created (4 files):**
- `server/src/engine/webhook-test.ts` -- pre-fight webhook verification
- `server/src/middleware/rate-limit.ts` -- rate limiter
- `server/src/tui/state.ts` -- TUI state tracker
- `server/src/tui/renderer.ts` -- TUI renderer
**Execution order:** Phase 1 (1.1-1.8) -> Phase 2 (2.1-2.5) -> Phase 3 (3.1-3.4) -> Phase 4 (4.1-4.6) -> Phase 5 verification
@@ -0,0 +1,36 @@
# Plan: Create Zaps.md Implementation Guide
## Context
The user wants a `Zaps.md` planning document at the project root that serves as a comprehensive implementation guide for a future Claude session. When the user says "implement zaps", Claude reads this doc and knows exactly what to build.
## What to create
- **File**: `/Users/dorian/Projects/botfights/Zaps.md`
## Key design decisions from research
### Payment model: NWC (primary) + Cashu (alternative)
- NWC (NIP-47): User connects Lightning wallet once. Server sends encrypted payment requests via Nostr relay. Use nostr-tools directly (no Alby SDK) for privacy.
- Cashu ecash: Alternative — user pastes ecash token, server redeems with mint.
- Server wallet: Small Lightning wallet (Alby Hub or LNbits) via `BOTFIGHTS_NWC_URL` env var. Transient escrow only (seconds, not balances).
### Two modes: Free (unchanged) + Ranked (new, 21 sats)
- Ranked: both pay 21 sats → winner gets 42 sats
- Ranked queue never matches against mock bots
- Timeout = refund (not mock fallback)
### New files needed
- DB tables: `payments`, `wallet_connections` + new columns on fights/bots
- Server: `engine/payments.ts`, `engine/ranked-queue.ts`, `routes/payments.ts`
- Frontend: `composables/useWallet.ts`, `components/WalletConnect.vue`
- Modifications to: orchestrator, queue routes, JoinBoutPage, FightPage, BotProfilePage, schema
### Dependencies (all MIT)
- `nostr-tools` ^2.x — NWC protocol
- `@cashu/cashu-ts` ^2.x — ecash tokens
## Implementation
Write Zaps.md with full architecture, schema SQL, function signatures, edge cases, phases, and security notes — enough for a fresh Claude session to implement from.
## Verification
- Read the written Zaps.md and confirm completeness
- Ensure all file paths match actual codebase structure