// Retro Mode — arcade fighting game combo engine import type { Challenge } from './challenges.js' export interface RetroMove { input: string name: string damage: number tier: 'basic' | 'standard' | 'super' | 'ultra' } export const RETRO_MOVES: RetroMove[] = [ // Basic — always revealed { input: 'A', name: 'Jab', damage: 5, tier: 'basic' }, { input: 'B', name: 'Kick', damage: 6, tier: 'basic' }, { input: '→+A', name: 'Hook', damage: 8, tier: 'basic' }, { input: '←+B', name: 'Low Kick', damage: 7, tier: 'basic' }, // Standard — some revealed each fight { input: '↓→+A', name: 'Fireball', damage: 12, tier: 'standard' }, { input: '↓←+B', name: 'Spin Kick', damage: 14, tier: 'standard' }, { input: '→→+A', name: 'Dash Punch', damage: 15, tier: 'standard' }, { input: '↑↓+A', name: 'Uppercut', damage: 16, tier: 'standard' }, { input: '←→+B', name: 'Slide Kick', damage: 13, tier: 'standard' }, { input: '↑+A', name: 'Rising Fist', damage: 11, tier: 'standard' }, { input: '↓+B+A', name: 'Leg Sweep', damage: 10, tier: 'standard' }, { input: '→+B+A', name: 'Elbow Strike', damage: 12, tier: 'standard' }, // Super — never revealed, must discover { input: '↓→↓→+A', name: 'Hadouken', damage: 22, tier: 'super' }, { input: '←↓→+B', name: 'Dragon Kick', damage: 25, tier: 'super' }, { input: '↑↑↓↓+A', name: 'Power Surge', damage: 28, tier: 'super' }, { input: '→←→+A+B', name: 'Tiger Knee', damage: 24, tier: 'super' }, { input: '↓↓↑+B+A', name: 'Shoryuken', damage: 26, tier: 'super' }, { input: '←←→→+A', name: 'Sonic Boom', damage: 23, tier: 'super' }, { input: '↑→↓←+A+B', name: 'Cyclone', damage: 30, tier: 'super' }, // Ultra — the ultimate secret { input: '↑↑↓↓←→←→+B+A', name: 'KONAMI CODE', damage: 50, tier: 'ultra' }, ] // Build canonical form: arrows joined, then +buttons function canonicalize(raw: string): string { let s = raw s = s.replace(/\bup\b/gi, '↑') s = s.replace(/\bdown\b/gi, '↓') s = s.replace(/\bleft\b/gi, '←') s = s.replace(/\bright\b/gi, '→') const dirs: string[] = [] const buttons: string[] = [] for (const c of s) { if ('↑↓←→'.includes(c)) dirs.push(c) else if ('Aa'.includes(c)) buttons.push('A') else if ('Bb'.includes(c)) buttons.push('B') } if (dirs.length === 0 && buttons.length === 0) return '' let result = dirs.join('') if (buttons.length > 0) { if (result.length > 0) result += '+' result += buttons.join('+') } return result } // Precompute canonical lookup const MOVE_LOOKUP = new Map() for (const move of RETRO_MOVES) { MOVE_LOOKUP.set(canonicalize(move.input), move) } export function lookupMove(raw: string): RetroMove | null { return MOVE_LOOKUP.get(canonicalize(raw)) || null } export interface RetroMoveResult { input: string name: string | null damage: number discovered: boolean } export interface RetroScoreResult { totalDamage: number moves: RetroMoveResult[] score: number } export function scoreRetroResponse(answer: string | null, knownInputs: string[]): RetroScoreResult { if (!answer) return { totalDamage: 0, moves: [], score: 0 } const knownSet = new Set(knownInputs.map(canonicalize)) const parts = answer.split('|').map(s => s.trim()).filter(Boolean).slice(0, 3) const moves: RetroMoveResult[] = [] let totalDamage = 0 for (const raw of parts) { const move = lookupMove(raw) if (move) { const isDiscovered = !knownSet.has(canonicalize(move.input)) const dmg = isDiscovered ? Math.round(move.damage * 1.5) : move.damage totalDamage += dmg moves.push({ input: raw, name: move.name, damage: dmg, discovered: isDiscovered }) } else { moves.push({ input: raw, name: null, damage: 0, discovered: false }) } } return { totalDamage, moves, score: Math.min(10, totalDamage / 5) } } export function generateRetroChallenge(): Challenge { const basics = RETRO_MOVES.filter(m => m.tier === 'basic') const standards = RETRO_MOVES.filter(m => m.tier === 'standard') // Reveal 3-5 random standard moves const shuffled = [...standards].sort(() => Math.random() - 0.5) const revealCount = 3 + Math.floor(Math.random() * 3) const revealed = shuffled.slice(0, revealCount) const knownMoves = [...basics, ...revealed] const knownInputs = knownMoves.map(m => m.input) const moveList = knownMoves.map(m => ` ${m.input} = ${m.name} (${m.damage} dmg)`).join('\n') const displayPrompt = 'Binary Gamepads at the ready, GO!' const prompt = `ARCADE ROUND! Pick 3 moves:\n${moveList}\n\nSecret combos exist — longer chains hit harder.\nAnswer: combo1 | combo2 | combo3` return { type: 'retro_mode', label: 'Retro Mode', prompt, answers: knownInputs, timeout_ms: 12000, scoring: 'factual', baseDamage: 22, displayPrompt, } } // Generate mock retro response based on bot ELO export function generateMockRetroResponse(elo: number): string { const basics = RETRO_MOVES.filter(m => m.tier === 'basic') const standards = RETRO_MOVES.filter(m => m.tier === 'standard') const supers = RETRO_MOVES.filter(m => m.tier === 'super') const moves: string[] = [] for (let i = 0; i < 3; i++) { const roll = Math.random() const superChance = Math.max(0, (elo - 1400) / 1500) const standardChance = Math.max(0.3, (elo - 900) / 1200) const whiffChance = Math.max(0, (1300 - elo) / 2000) if (roll < whiffChance) { const gibberish = ['↓↓↓+A', '→←+A+A', '↑+B+B+A', '←↓↑→+A', '→↓←+B+B'][Math.floor(Math.random() * 5)] moves.push(gibberish) } else if (roll < whiffChance + superChance) { moves.push(supers[Math.floor(Math.random() * supers.length)].input) } else if (roll < whiffChance + superChance + standardChance) { moves.push(standards[Math.floor(Math.random() * standards.length)].input) } else { moves.push(basics[Math.floor(Math.random() * basics.length)].input) } } return moves.join(' | ') } // Get all known inputs as flat strings for reference export function getRetroMoveInputs(): string[] { return RETRO_MOVES.map(m => m.input) }