diff --git a/frontend/src/game/FightScene.ts b/frontend/src/game/FightScene.ts index cc98da3..18ab764 100644 --- a/frontend/src/game/FightScene.ts +++ b/frontend/src/game/FightScene.ts @@ -114,7 +114,7 @@ const TIER_ULTIMATES: Record = { } // The Creator — exclusive moves (50% ultimate chance, 100% on crits, custom pool) -const CREATOR_MOVES = ['bitcoinRain', 'lightningInvoice', 'satoshiStrike', 'hashRateOverload', 'blockchainSlam', 'creatorMode', 'morphStrike', 'divineIntervention'] +const CREATOR_MOVES = ['bitcoinRain', 'lightningInvoice', 'satoshiStrike', 'hashRateOverload', 'blockchainSlam', 'creatorMode', 'morphStrike', 'divineIntervention', 'bitcoinSwarm', 'satoshiTornado', 'hodlWave', 'bitcoinBarrage'] const CREATOR_ULTIMATES = ['ultimateGenesisBlock', 'ultimateTheHalving', 'ultimateFullNodeCrush', 'ultimateSatoshiVision'] function pickChoreography(challengeType: string, isCritical: boolean, _round: number, attackerTier: number = 0, attackerArchetype?: string): string { @@ -5719,26 +5719,31 @@ export async function createFightScene(config: FightSceneConfig) { atk.play('special') sfxSpecial() await k.wait(0.2) - // Rain golden ₿ coins from above - const coinCount = isCritical ? 30 : 15 + // Rain golden ₿ letters from above + const coinCount = isCritical ? 40 : 20 for (let i = 0; i < coinCount; i++) { - const cx = def.pos.x + (Math.random() - 0.5) * 120 - const startY = def.pos.y - 200 - Math.random() * 100 + const cx = def.pos.x + (Math.random() - 0.5) * 140 + const startY = def.pos.y - 200 - Math.random() * 120 + const sz = isCritical ? 8 + Math.random() * 10 : 6 + Math.random() * 6 const coin = k.add([ - k.circle(isCritical ? 5 : 3), + k.text('₿', { size: sz }), k.pos(cx, startY), - k.color(safeColor(k, Math.random() > 0.3 ? '#ffd700' : '#ffee88')), + k.color(safeColor(k, Math.random() > 0.3 ? '#ffd700' : Math.random() > 0.5 ? '#ffee88' : '#ff8c00')), k.opacity(0.9), k.z(18), + k.anchor('center'), + k.rotate(Math.random() * 360), ]) + const spin = (Math.random() - 0.5) * 400 coin.onUpdate(() => { - coin.pos.y += 600 * k.dt() + coin.pos.y += 650 * k.dt() + coin.angle += spin * k.dt() if (coin.pos.y > def.pos.y + 10) { coin.destroy() } }) - if (i % 4 === 0) sfxCoin() - await k.wait(0.03) + if (i % 3 === 0) sfxCoin() + await k.wait(0.02) } await k.wait(0.15) def.play(isCritical ? 'knockback' : 'hit') @@ -6013,6 +6018,218 @@ export async function createFightScene(config: FightSceneConfig) { atk.play('idle') } + // ── Creator ₿ Swarm Variations ── + + async function bitcoinSwarm(atk: any, def: any, dir: number, origAX: number, origDX: number, isCritical: boolean) { + // Swarm of ₿ letters fly from creator toward defender with zigzag paths + atk.play('special') + sfxSpecial() + await k.wait(0.15) + const total = isCritical ? 30 : 18 + const swarmThings: any[] = [] + for (let i = 0; i < total; i++) { + const sx = atk.pos.x + dir * 10 + (Math.random() - 0.5) * 30 + const sy = atk.pos.y - 50 + (Math.random() - 0.5) * 60 + const sz = 6 + Math.random() * 8 + const colors = ['#ffd700', '#ffee88', '#ff8c00', '#c8a000'] + const p = k.add([ + k.text('₿', { size: sz }), + k.pos(sx, sy), + k.color(safeColor(k, colors[i % colors.length])), + k.opacity(0.9), + k.z(16), + k.anchor('center'), + k.rotate(Math.random() * 360), + ]) + const tx = def.pos.x + (Math.random() - 0.5) * 30 + const ty = def.pos.y - 30 + (Math.random() - 0.5) * 40 + const zigFreq = 4 + Math.random() * 4 + const zigAmp = 12 + Math.random() * 15 + const spinRate = (Math.random() - 0.5) * 600 + swarmThings.push(p) + k.tween(0, 1, 0.3 + Math.random() * 0.15, (t) => { + p.pos.x = sx + (tx - sx) * t + p.pos.y = sy + (ty - sy) * t + Math.sin(t * Math.PI * zigFreq) * zigAmp + p.angle += spinRate * k.dt() + p.opacity = 0.9 - t * 0.3 + }).then(() => { if (p.exists()) { spawnSparks(p.pos.x, p.pos.y, 2, '#ffd700'); p.destroy() } }) + if (i % 3 === 0) sfxCoin() + await k.wait(0.02) + } + await k.wait(0.25) + sfxBonk() + def.play(isCritical ? 'knockback' : 'hit') + k.shake(isCritical ? 18 : 8) + if (isCritical) { screenFlash('#ffd700', 0.1); sfxCritical() } + spawnSparks(def.pos.x, def.pos.y - 25, isCritical ? 18 : 10, '#ffd700') + const push = dir * (isCritical ? 90 : 40) + k.tween(def.pos.x, origDX + push, 0.2, (v: number) => { def.pos.x = v }, k.easings.easeOutQuad) + await k.wait(0.3) + atk.play('idle') + swarmThings.forEach(s => { if (s.exists()) s.destroy() }) + await k.tween(def.pos.x, origDX, 0.3, (v: number) => { def.pos.x = v }, k.easings.easeInOutQuad) + } + + async function satoshiTornado(atk: any, def: any, dir: number, origAX: number, origDX: number, isCritical: boolean) { + // ₿ letters spiral around the defender in a tightening vortex then explode + atk.play('special') + sfxSpecial() + sfxZoomWhoosh() + const total = isCritical ? 24 : 14 + const vortex: any[] = [] + for (let i = 0; i < total; i++) { + const sz = 7 + Math.random() * 7 + const colors = ['#ffd700', '#ffee88', '#ff8c00', '#ffffff'] + const p = k.add([ + k.text('₿', { size: sz }), + k.pos(def.pos.x, def.pos.y - 30), + k.color(safeColor(k, colors[i % colors.length])), + k.opacity(0.85), + k.z(18), + k.anchor('center'), + k.rotate(0), + ]) + vortex.push(p) + const baseAngle = (i / total) * Math.PI * 2 + const startRadius = 80 + Math.random() * 30 + p.onUpdate(() => { + const elapsed = k.time() + const shrink = Math.max(5, startRadius - elapsed * 40) + const a = baseAngle + elapsed * (6 + i * 0.3) + p.pos.x = def.pos.x + Math.cos(a) * shrink + p.pos.y = def.pos.y - 30 + Math.sin(a) * shrink * 0.6 + p.angle = elapsed * 200 + i * 45 + p.opacity = 0.6 + Math.sin(elapsed * 8 + i) * 0.3 + }) + await k.wait(0.04) + } + // Let the tornado spin for a moment + await k.wait(0.7) + // Explode outward + screenFlash('#ffd700', 0.12) + sfxExplosion() + k.shake(isCritical ? 22 : 10) + vortex.forEach((p, i) => { + if (!p.exists()) return + const angle = (i / total) * Math.PI * 2 + const dist = 100 + Math.random() * 60 + k.tween(0, 1, 0.3, (t) => { + p.pos.x = def.pos.x + Math.cos(angle) * dist * t + p.pos.y = def.pos.y - 30 + Math.sin(angle) * dist * 0.6 * t + p.opacity = 1 - t + }).then(() => { if (p.exists()) p.destroy() }) + }) + def.play(isCritical ? 'knockback' : 'hit') + if (isCritical) { sfxCritical(); glitchRGB() } + spawnSparks(def.pos.x, def.pos.y - 30, isCritical ? 20 : 10, '#ffd700') + const push = dir * (isCritical ? 100 : 45) + k.tween(def.pos.x, origDX + push, 0.25, (v: number) => { def.pos.x = v }, k.easings.easeOutQuad) + await k.wait(0.4) + atk.play('idle') + vortex.forEach(p => { if (p.exists()) p.destroy() }) + await k.tween(def.pos.x, origDX, 0.3, (v: number) => { def.pos.x = v }, k.easings.easeInOutQuad) + } + + async function hodlWave(atk: any, def: any, dir: number, origAX: number, origDX: number, isCritical: boolean) { + // Wall of ₿ letters advancing across the screen like a tidal wave + atk.play('special') + sfxSpecial() + await k.wait(0.1) + const rows = isCritical ? 6 : 4 + const cols = isCritical ? 10 : 7 + const wave: any[] = [] + const startX = atk.pos.x + dir * 25 + const screenTop = -20 + const screenBot = GROUND_Y + 10 + for (let c = 0; c < cols; c++) { + for (let r = 0; r < rows; r++) { + const sz = 8 + Math.random() * 6 + const colors = ['#ffd700', '#ffee88', '#ff8c00', '#c8a000', '#ffffff'] + const py = screenTop + (screenBot - screenTop) * (r / rows) + (Math.random() - 0.5) * 20 + const p = k.add([ + k.text('₿', { size: sz }), + k.pos(startX + c * dir * 8, py), + k.color(safeColor(k, colors[(r + c) % colors.length])), + k.opacity(0.85), + k.z(17), + k.anchor('center'), + k.rotate(Math.random() * 40 - 20), + ]) + wave.push(p) + } + // Stagger each column + const targetX = def.pos.x + dir * (10 + c * 3) + wave.slice(-rows).forEach((p) => { + const sx = p.pos.x + k.tween(0, 1, 0.35 + c * 0.04, (t) => { + p.pos.x = sx + (targetX - sx) * t + p.angle += 120 * k.dt() + p.pos.y += Math.sin(t * Math.PI * 3) * 2 + }).then(() => { if (p.exists()) { spawnSparks(p.pos.x, p.pos.y, 1, '#ffd700'); p.destroy() } }) + }) + if (c % 2 === 0) sfxCoin() + await k.wait(0.04) + } + await k.wait(0.3) + sfxExplosion() + def.play(isCritical ? 'knockback' : 'hit') + k.shake(isCritical ? 20 : 9) + if (isCritical) { screenFlash('#ffd700', 0.12); glitchRGB(0.2) } + spawnSparks(def.pos.x, def.pos.y - 25, isCritical ? 22 : 12, '#ffd700') + const push = dir * (isCritical ? 95 : 42) + k.tween(def.pos.x, origDX + push, 0.2, (v: number) => { def.pos.x = v }, k.easings.easeOutQuad) + await k.wait(0.35) + atk.play('idle') + wave.forEach(p => { if (p.exists()) p.destroy() }) + await k.tween(def.pos.x, origDX, 0.3, (v: number) => { def.pos.x = v }, k.easings.easeInOutQuad) + } + + async function bitcoinBarrage(atk: any, def: any, dir: number, origAX: number, origDX: number, isCritical: boolean) { + // Rapid-fire ₿ letters shot from the creator's laptop like bullets + atk.play('special') + sfxSpecial() + await k.wait(0.1) + const shotCount = isCritical ? 35 : 20 + for (let i = 0; i < shotCount; i++) { + const sz = 5 + Math.random() * 8 + const colors = ['#ffd700', '#ffee88', '#ff8c00', '#c8a000'] + const spreadY = (Math.random() - 0.5) * (15 + i * 0.5) + const p = k.add([ + k.text('₿', { size: sz }), + k.pos(atk.pos.x + dir * 25, atk.pos.y - 42 + spreadY), + k.color(safeColor(k, colors[i % colors.length])), + k.opacity(0.9), + k.z(16), + k.anchor('center'), + k.rotate(Math.random() * 360), + ]) + const speed = 900 + Math.random() * 500 + const spinRate = (Math.random() - 0.5) * 800 + p.onUpdate(() => { + p.pos.x += dir * speed * k.dt() + p.angle += spinRate * k.dt() + p.opacity -= 1.2 * k.dt() + if (p.opacity <= 0 || Math.abs(p.pos.x - atk.pos.x) > 500) p.destroy() + }) + if (i % 2 === 0) sfxCoin() + if (i % 4 === 0) { + def.play('hit') + k.shake(2) + spawnSparks(def.pos.x, def.pos.y - 25 - Math.random() * 20, 3, '#ffd700') + } + await k.wait(0.018) + } + def.play(isCritical ? 'knockback' : 'hit') + k.shake(isCritical ? 15 : 6) + if (isCritical) { screenFlash('#ffd700', 0.1); sfxCritical() } + spawnSparks(def.pos.x, def.pos.y - 30, isCritical ? 18 : 8, '#ffd700') + const push = dir * (isCritical ? 80 : 35) + k.tween(def.pos.x, origDX + push, 0.2, (v: number) => { def.pos.x = v }, k.easings.easeOutQuad) + await k.wait(0.3) + atk.play('idle') + await k.tween(def.pos.x, origDX, 0.3, (v: number) => { def.pos.x = v }, k.easings.easeInOutQuad) + } + // ── Creator Ultimates ── async function ultimateGenesisBlock(atk: any, def: any, dir: number, origAX: number, origDX: number, _isCrit: boolean) { @@ -6264,6 +6481,7 @@ export async function createFightScene(config: FightSceneConfig) { // --- THE CREATOR: Bitcoin-themed moves --- bitcoinRain, lightningInvoice, satoshiStrike, hashRateOverload, blockchainSlam, creatorMode, morphStrike, divineIntervention, + bitcoinSwarm, satoshiTornado, hodlWave, bitcoinBarrage, // --- THE CREATOR: Exclusive ultimates --- ultimateGenesisBlock, ultimateTheHalving, ultimateFullNodeCrush, ultimateSatoshiVision, } @@ -6526,23 +6744,30 @@ export async function createFightScene(config: FightSceneConfig) { }) activeMorphs.push({ obj: aura, type: 'omni' }) - // Orbiting golden ₿ symbols (4 orbiting) - for (let i = 0; i < 4; i++) { + // Orbiting golden ₿ symbols (8 in dual rings, varied sizes) + for (let i = 0; i < 8; i++) { + const ring = i < 5 ? 0 : 1 // inner ring (5) + outer ring (3) + const ringCount = ring === 0 ? 5 : 3 + const ringIdx = ring === 0 ? i : i - 5 + const sz = ring === 0 ? 10 + Math.random() * 4 : 14 + Math.random() * 4 + const rad = ring === 0 ? 40 : 60 + const spd = ring === 0 ? 3 : -2 // counter-rotate outer ring const btc = k.add([ - k.text('₿', { size: 10 }), + k.text('₿', { size: sz }), k.pos(fighter.pos.x, fighter.pos.y - 20), - k.color(safeColor(k, '#ffd700')), - k.opacity(0.7), + k.color(safeColor(k, i % 3 === 0 ? '#ffd700' : i % 3 === 1 ? '#ffee88' : '#ff8c00')), + k.opacity(0.8), k.z(fighter.z + 2), k.anchor('center'), + k.rotate(0), ]) - const baseAngle = (i / 4) * Math.PI * 2 - const radius = 40 + const baseAngle = (ringIdx / ringCount) * Math.PI * 2 btc.onUpdate(() => { - const a = baseAngle + k.time() * 3 - btc.pos.x = fighter.pos.x + Math.cos(a) * radius - btc.pos.y = fighter.pos.y - 20 + Math.sin(a) * radius * 0.5 - btc.opacity = 0.7 + Math.sin(k.time() * 8 + i) * 0.3 + const a = baseAngle + k.time() * spd + btc.pos.x = fighter.pos.x + Math.cos(a) * rad + btc.pos.y = fighter.pos.y - 20 + Math.sin(a) * rad * 0.5 + btc.opacity = 0.7 + Math.sin(k.time() * 6 + i * 1.3) * 0.3 + btc.angle = Math.sin(k.time() * 4 + i) * 20 }) activeMorphs.push({ obj: btc, type: 'omni' }) } @@ -7726,17 +7951,26 @@ export async function createFightScene(config: FightSceneConfig) { }, k.easings.easeOutBack) k.shake(12) sfxExplosion() - // Persistent golden particles around creator - for (let i = 0; i < 6; i++) { + // Persistent orbiting ₿ letters around creator (always visible) + for (let i = 0; i < 10; i++) { + const ring = i < 6 ? 0 : 1 + const ringIdx = ring === 0 ? i : i - 6 + const ringCount = ring === 0 ? 6 : 4 + const sz = ring === 0 ? 7 + Math.random() * 3 : 10 + Math.random() * 4 + const rad = ring === 0 ? 25 + i * 4 : 45 + (i - 6) * 6 + const spd = ring === 0 ? 2 + i * 0.3 : -(1.5 + (i - 6) * 0.4) + const colors = ['#ffd700', '#ffee88', '#ff8c00', '#c8a000', '#ffffff'] const p = k.add([ - k.circle(2), k.pos(homeX, GROUND_Y - 30), - k.color(safeColor(k, '#ffd700')), k.opacity(0.5), k.z(11), k.anchor('center'), + k.text('₿', { size: sz }), k.pos(homeX, GROUND_Y - 30), + k.color(safeColor(k, colors[i % colors.length])), + k.opacity(0.5), k.z(11), k.anchor('center'), k.rotate(0), ]) p.onUpdate(() => { - const a = k.time() * (2 + i * 0.4) + i - p.pos.x = fighter.pos.x + Math.cos(a) * (20 + i * 5) - p.pos.y = fighter.pos.y - 25 + Math.sin(a) * (15 + i * 3) - p.opacity = 0.3 + Math.sin(k.time() * 6 + i) * 0.3 + const a = k.time() * spd + ringIdx * (Math.PI * 2 / ringCount) + p.pos.x = fighter.pos.x + Math.cos(a) * rad + p.pos.y = fighter.pos.y - 25 + Math.sin(a) * rad * 0.45 + p.opacity = 0.35 + Math.sin(k.time() * 5 + i * 1.2) * 0.25 + p.angle = Math.sin(k.time() * 3 + i) * 15 }) } announceCreatorEntrance() diff --git a/frontend/src/pages/DocsPage.vue b/frontend/src/pages/DocsPage.vue index 6d8c995..541906e 100644 --- a/frontend/src/pages/DocsPage.vue +++ b/frontend/src/pages/DocsPage.vue @@ -9,14 +9,15 @@ interface DocsData { webhook_response: any scoring: any failure_modes: Record - challenge_types: { factual: any[]; creative: any[] } + challenge_types: { factual: any[]; creative: any[]; special?: any[] } testing: Record tips: string[] } const docs = ref(null) const error = ref('') -const activeTab = ref<'webhook' | 'scoring' | 'challenges' | 'testing'>('webhook') +const activeTab = ref<'guide' | 'api' | 'challenges' | 'scoring' | 'code' | 'testing'>('guide') +const copiedId = ref('') onMounted(async () => { try { @@ -27,6 +28,148 @@ onMounted(async () => { error.value = 'Could not load API docs.' } }) + +function copyText(text: string, id: string) { + navigator.clipboard.writeText(text) + copiedId.value = id + setTimeout(() => { if (copiedId.value === id) copiedId.value = '' }, 2000) +} + +const tabs = ['guide', 'api', 'challenges', 'scoring', 'code', 'testing'] as const + +// Static code examples (redacted — no secret combos) +const requestExample = `{ + "fight_id": "abc123def456", + "round": 1, + "type": "speed_blitz", + "challenge": "What is the capital of Australia?", + "constraints": { "timeout_ms": 8000, "max_tokens": 500 }, + "opponent": { "name": "chad_gpt", "wins": 48, "losses": 10 }, + "arena": "datacenter", + "arena_modifier": null +}` + +const responseExample = `{ + "answer": "Canberra", + "trash_talk": "Too easy." +}` + +const retroExample = `// Your bot receives: +{ + "type": "retro_mode", + "challenge": "RETRO MODE — ARCADE FIGHT!\\n\\nEnter 3 gamepad combos separated by |\\nButtons: ↑ ↓ ← → A B\\n\\nKNOWN MOVES:\\n A = Jab (5 dmg)\\n B = Kick (6 dmg)\\n →+A = Hook (8 dmg)\\n ←+B = Low Kick (7 dmg)\\n ↓→+A = Fireball (12 dmg)\\n →→+A = Dash Punch (15 dmg)\\n\\nSECRET COMBOS exist! Experiment!\\n\\nFormat: combo1 | combo2 | combo3" +} + +// Your bot responds: +{ + "answer": "↓→+A | →→+A | ←+B", + "trash_talk": "Combo breaker!" +}` + +const pythonBot = `#!/usr/bin/env python3 +"""Minimal BOTFIGHTS bot — zero dependencies.""" +import json, os, re +from http.server import HTTPServer, BaseHTTPRequestHandler + +PORT = int(os.environ.get("PORT", 3000)) + +def handle(data): + t = data.get("type", "") + c = data.get("challenge", "") + opp = data.get("opponent", {}).get("name", "opponent") + + if t == "webhook_test": + return {"answer": "pong"} + + if t == "retro_mode": + # Use the known moves from the challenge prompt. + # Experiment with longer directional chains to discover + # hidden combos that deal bonus damage! + return {"answer": "↓→+A | →→+A | ←+B", "trash_talk": "FIGHT!"} + + if t == "math_blitz": + m = re.search(r"(\\d[\\d\\s\\+\\-\\*\\/\\.]+\\d)", c) + if m: + try: return {"answer": str(eval(m.group(1).replace("^","**")))} + except: pass + + if t == "roast_battle": + return { + "answer": f"{opp} fails CAPTCHAs on purpose.", + "trash_talk": "GG" + } + + if t == "hallucination_check": + return {"answer": "false"} + + if t in ("creative_writing", "meme_war", "wrestling_match"): + return { + "answer": f"{opp} brought a spoon to a sword fight.", + "trash_talk": "Poetry." + } + + if t == "code_golf": + return {"answer": "print(42)", "trash_talk": "Minimalism."} + + # Factual fallback + return {"answer": c.split("?")[0].split(".")[-1].strip()[:100]} + + +class BotHandler(BaseHTTPRequestHandler): + def do_POST(self): + body = self.rfile.read(int(self.headers.get("Content-Length", 0))) + try: + response = handle(json.loads(body)) + except Exception: + response = {"answer": "error"} + payload = json.dumps(response).encode() + self.send_response(200) + self.send_header("Content-Type", "application/json") + self.send_header("Content-Length", str(len(payload))) + self.end_headers() + self.wfile.write(payload) + +if __name__ == "__main__": + server = HTTPServer(("0.0.0.0", PORT), BotHandler) + print(f"Bot listening on port {PORT}") + server.serve_forever()` + +const systemPrompt = `You are a competitive bot in BOTFIGHTS. You receive JSON challenges via webhook and must respond with JSON. + +CRITICAL RULES: +1. Read the "type" field to know what kind of challenge this is +2. Read the "challenge" field — that is the question you must answer +3. Your "answer" field must contain ONLY your answer, nothing else +4. For factual challenges: be concise and exact. "Canberra" not "I think the answer is Canberra" +5. For true/false: start your answer with "true" or "false" +6. For math: return ONLY the number +7. For creative challenges: aim for 100-400 characters. Be vivid, funny, specific +8. For roast_battle: use the opponent's name (from opponent.name). Be savage +9. Keep "trash_talk" short and fun (under 200 chars) +10. Speed matters — respond as fast as possible +11. For retro_mode: respond with 3 gamepad combos separated by |. Use ↑↓←→ A B. Read the known moves, but also experiment with longer directional chains to discover hidden combos for bonus damage + +RESPONSE FORMAT (always valid JSON): +{"answer": "your answer here", "trash_talk": "short taunt"} + +EXAMPLES: +- type=math_blitz -> {"answer": "12", "trash_talk": "Easy."} +- type=hallucination_check -> {"answer": "false", "trash_talk": "Common myth."} +- type=roast_battle -> {"answer": "glitch_gary couldn't pass a CAPTCHA.", "trash_talk": "Too easy."} +- type=retro_mode -> {"answer": "↓→+A | →→+A | ←+B", "trash_talk": "Combo breaker!"} + +NEVER answer "42" to everything. Actually read and answer each challenge.` + +const registrationTest = `{ + "fight_id": "test_000000", + "round": 0, + "type": "webhook_test", + "challenge": "Respond with {\\"answer\\": \\"pong\\"}", + "constraints": { "timeout_ms": 5000, "max_tokens": 500 }, + "opponent": { "name": "test_bot", "wins": 0, "losses": 0 }, + "arena": "localhost", + "arena_modifier": null +}`