// Arcade Bot — formats game state into challenge prompts and generates // mock/classic bot action responses for arcade mode. import { logger } from '../lib/logger.js' export interface ArcadeGameState { self: { hp: number; x: number; state: string; grounded: boolean } opponent: { hp: number; x: number; state: string; grounded: boolean } distance: number timer: number round: number maxRounds: number facingRight: boolean } const VALID_ACTIONS = [ 'idle', 'move_forward', 'move_back', 'jump', 'crouch', 'punch', 'kick', 'block', 'jump_punch', 'jump_kick', 'fireball', 'uppercut', 'dash_punch', 'spinning_kick', 'super_jump_kick', ] as const type BotAction = (typeof VALID_ACTIONS)[number] /** Format game state into a challenge prompt for webhook/polling bots */ export function formatArcadeChallenge(state: ArcadeGameState): string { const distLabel = state.distance > 250 ? 'far' : state.distance > 120 ? 'medium' : 'close' const selfHpPct = Math.round((state.self.hp / 1000) * 100) const oppHpPct = Math.round((state.opponent.hp / 1000) * 100) return `ARCADE FIGHT — Real-time 2D fighter. You are P2. ACTIONS (respond with comma-separated list, 3-8 actions): move_forward, move_back, jump, crouch, punch (50dmg), kick (70dmg), block, jump_punch, jump_kick, fireball (60dmg, ranged), uppercut (100dmg, launcher), dash_punch (80dmg, rush), spinning_kick (90dmg, multi-hit), super_jump_kick (110dmg) STATE: You: HP ${state.self.hp}/1000 (${selfHpPct}%), x=${state.self.x}, ${state.self.state}${state.self.grounded ? '' : ' (airborne)'} Opponent: HP ${state.opponent.hp}/1000 (${oppHpPct}%), x=${state.opponent.x}, ${state.opponent.state}${state.opponent.grounded ? '' : ' (airborne)'} Distance: ${state.distance}px (${distLabel}) | Timer: ${state.timer}s | Round ${state.round}/${state.maxRounds} Respond: {"answer":"action1, action2, action3, ..."}` } /** Parse a bot response into validated action list */ export function parseArcadeResponse(answer: string | null): BotAction[] { if (!answer) return generateFallbackActions() const parts = answer.split(',').map(s => s.trim().toLowerCase()) const actions: BotAction[] = [] for (const p of parts) { if ((VALID_ACTIONS as readonly string[]).includes(p)) { actions.push(p as BotAction) } } return actions.length > 0 ? actions.slice(0, 10) : generateFallbackActions() } /** Generate mock/classic bot arcade actions based on game state */ export function generateArcadeBotActions(state: ArcadeGameState, personality: string): BotAction[] { const actions: BotAction[] = [] const dist = state.distance const selfHp = state.self.hp const oppHp = state.opponent.hp const oppState = state.opponent.state const rng = () => Math.random() // Personality-based aggression (0 = defensive, 1 = aggressive) const aggression = getPersonalityAggression(personality) // React to opponent's state if (oppState === 'attacking' || oppState === 'kicking' || oppState === 'special') { // Opponent attacking — defensive response if (rng() < 0.4 + (1 - aggression) * 0.3) { actions.push('block') if (rng() < 0.3) actions.push('punch') // counter after block return actions } if (rng() < 0.3) { actions.push('move_back') return actions } } // Opponent in hitstun — press advantage if (oppState === 'hit' || oppState === 'knockback') { if (dist < 100) { if (rng() < 0.4 * aggression) actions.push('uppercut') else if (rng() < 0.5) actions.push('kick') else actions.push('punch') return actions } actions.push('move_forward') actions.push('punch') return actions } // Distance-based decisions if (dist > 250) { // Far range if (rng() < 0.35 * aggression) { actions.push('fireball') } else if (rng() < 0.5) { actions.push('move_forward') actions.push('move_forward') } else { actions.push('move_forward') if (rng() < 0.3) actions.push('jump') } } else if (dist > 120) { // Medium range if (rng() < 0.25 * aggression) { actions.push('dash_punch') } else if (rng() < 0.2 * aggression) { actions.push('fireball') } else if (rng() < 0.4) { actions.push('move_forward') actions.push(rng() < 0.5 ? 'punch' : 'kick') } else if (rng() < 0.3) { actions.push('jump_kick') } else { actions.push('move_forward') } } else { // Close range if (rng() < 0.15 * aggression) { actions.push('uppercut') } else if (rng() < 0.12 * aggression) { actions.push('spinning_kick') } else if (rng() < 0.35) { actions.push(rng() < 0.5 ? 'punch' : 'kick') if (rng() < 0.3 * aggression) actions.push('punch') // double tap } else if (rng() < 0.25) { actions.push('block') } else if (rng() < 0.2) { actions.push('crouch') actions.push('kick') // sweep } else { actions.push('move_back') if (rng() < 0.3) actions.push('fireball') } } // Low HP = more defensive if (selfHp < 300 && rng() < 0.3) { actions.push('block') actions.push('move_back') } // Opponent low HP = go for the kill if (oppHp < 200 && rng() < 0.4 * aggression) { actions.push('move_forward') actions.push(rng() < 0.3 ? 'super_jump_kick' : 'dash_punch') } // Ensure at least one action if (actions.length === 0) { actions.push(rng() < 0.6 ? 'move_forward' : 'idle') } return actions } function getPersonalityAggression(personality: string): number { const map: Record = { aggressive: 0.9, confident: 0.8, relentless: 0.95, intimidating: 0.85, unstoppable: 0.9, lethal: 0.85, destructive: 0.9, reckless: 0.95, chaotic: 0.8, calculated: 0.6, systematic: 0.55, precise: 0.5, tactical: 0.6, analytical: 0.5, logical: 0.45, disciplined: 0.55, steady: 0.5, resilient: 0.4, chill: 0.3, philosophical: 0.35, zen: 0.4, panicky: 0.7, buggy: 0.6, dramatic: 0.65, witty: 0.55, sarcastic: 0.5, based: 0.65, omniscient: 0.7, transcendent: 0.6, cosmic: 0.55, } return map[personality] ?? 0.6 } function generateFallbackActions(): BotAction[] { return ['move_forward', 'punch', 'block'] }