import type { PlayerInput, FighterInstance } from './types' // ═══════════════════════════════════════════════════════════════ // Arcade Bot Bridge — communicates with server to get bot actions // and translates them into frame-level PlayerInput // ═══════════════════════════════════════════════════════════════ /** High-level actions a bot can respond with */ export type BotAction = | 'idle' | 'move_forward' | 'move_back' | 'jump' | 'crouch' | 'punch' | 'kick' | 'block' | 'jump_punch' | 'jump_kick' | 'fireball' | 'uppercut' | 'dash_punch' | 'spinning_kick' | 'super_jump_kick' /** Snapshot of game state sent to the bot */ 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 } interface ActionStep { input: Partial frames: number /** If true, direction keys are relative (forward/back resolved at execution time) */ relative?: boolean } // ═══════════════════════════════════════════════════════════════ // Action → frame-level input mapping // ═══════════════════════════════════════════════════════════════ function forwardKey(facingRight: boolean): 'right' | 'left' { return facingRight ? 'right' : 'left' } function backKey(facingRight: boolean): 'right' | 'left' { return facingRight ? 'left' : 'right' } /** Maps an action name to a sequence of frame-level input steps */ function actionToSteps(action: BotAction): ActionStep[] { switch (action) { case 'idle': return [{ input: {}, frames: 15 }] case 'move_forward': return [{ input: { _forward: true } as any, frames: 18, relative: true }] case 'move_back': return [{ input: { _back: true } as any, frames: 15, relative: true }] case 'jump': return [ { input: { up: true }, frames: 3 }, { input: {}, frames: 25 }, ] case 'crouch': return [{ input: { down: true }, frames: 18 }] case 'punch': return [ { input: { punch: true }, frames: 2 }, { input: {}, frames: 12 }, ] case 'kick': return [ { input: { kick: true }, frames: 2 }, { input: {}, frames: 16 }, ] case 'block': return [{ input: { _back: true } as any, frames: 25, relative: true }] case 'jump_punch': return [ { input: { up: true }, frames: 3 }, { input: {}, frames: 8 }, { input: { punch: true }, frames: 2 }, { input: {}, frames: 15 }, ] case 'jump_kick': return [ { input: { up: true }, frames: 3 }, { input: {}, frames: 8 }, { input: { kick: true }, frames: 2 }, { input: {}, frames: 15 }, ] // Combo sequences — produce frame-level inputs that match combo detection case 'fireball': return [ { input: { down: true }, frames: 3 }, { input: { _forward: true } as any, frames: 3, relative: true }, { input: { _forward: true, punch: true } as any, frames: 2, relative: true }, { input: {}, frames: 22 }, ] case 'uppercut': return [ { input: { down: true }, frames: 3 }, { input: { _forward: true } as any, frames: 3, relative: true }, { input: { _forward: true, kick: true } as any, frames: 2, relative: true }, { input: {}, frames: 22 }, ] case 'dash_punch': return [ { input: { _back: true } as any, frames: 3, relative: true }, { input: {}, frames: 2 }, { input: { _back: true } as any, frames: 3, relative: true }, { input: { _back: true, punch: true } as any, frames: 2, relative: true }, { input: {}, frames: 18 }, ] case 'spinning_kick': return [ { input: { _back: true } as any, frames: 3, relative: true }, { input: {}, frames: 2 }, { input: { _back: true } as any, frames: 3, relative: true }, { input: { _back: true, kick: true } as any, frames: 2, relative: true }, { input: {}, frames: 20 }, ] case 'super_jump_kick': return [ { input: { down: true }, frames: 3 }, { input: { up: true }, frames: 3 }, { input: { up: true, kick: true }, frames: 2 }, { input: {}, frames: 24 }, ] default: return [{ input: {}, frames: 10 }] } } /** Resolve relative direction markers into actual left/right keys */ function resolveStep(step: ActionStep, facingRight: boolean): { input: PlayerInput; frames: number } { const base: PlayerInput = { up: false, down: false, left: false, right: false, punch: false, kick: false } const raw = step.input as any if (step.relative) { if (raw._forward) base[forwardKey(facingRight)] = true if (raw._back) base[backKey(facingRight)] = true } if (raw.up) base.up = true if (raw.down) base.down = true if (raw.left) base.left = true if (raw.right) base.right = true if (raw.punch) base.punch = true if (raw.kick) base.kick = true return { input: base, frames: step.frames } } // ═══════════════════════════════════════════════════════════════ // Action Queue Executor // ═══════════════════════════════════════════════════════════════ interface QueueEntry { input: PlayerInput framesLeft: number } export interface BotBridge { /** Call once per frame to get the current PlayerInput for the bot */ getInput(facingRight: boolean): PlayerInput /** Feed new actions from the server */ enqueueActions(actions: BotAction[]): void /** Send game state to server and get new actions */ requestActions(state: ArcadeGameState): void /** Stop all polling */ destroy(): void } const EMPTY_INPUT: PlayerInput = { up: false, down: false, left: false, right: false, punch: false, kick: false } export function createBotBridge(botId: string): BotBridge { const queue: QueueEntry[] = [] const pendingActions: ActionStep[][] = [] let fetching = false let destroyed = false function enqueueActions(actions: BotAction[]): void { for (const action of actions) { const steps = actionToSteps(action) pendingActions.push(steps) } } function expandNextAction(facingRight: boolean): void { if (pendingActions.length === 0) return const steps = pendingActions.shift()! for (const step of steps) { const resolved = resolveStep(step, facingRight) queue.push({ input: resolved.input, framesLeft: resolved.frames }) } } function getInput(facingRight: boolean): PlayerInput { // Expand pending actions into resolved queue entries as needed if (queue.length === 0 && pendingActions.length > 0) { expandNextAction(facingRight) } if (queue.length === 0) return { ...EMPTY_INPUT } const current = queue[0] current.framesLeft-- const input = { ...current.input } if (current.framesLeft <= 0) { queue.shift() // Pre-expand next action if (queue.length === 0 && pendingActions.length > 0) { expandNextAction(facingRight) } } return input } async function requestActions(state: ArcadeGameState): Promise { if (fetching || destroyed) return fetching = true try { const res = await fetch('/api/arcade/bot-action', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ botId, gameState: state }), }) if (!res.ok) return const data = await res.json() as { actions?: string[] } if (data.actions && Array.isArray(data.actions)) { const validActions = data.actions .map(a => a.trim().toLowerCase()) .filter(isValidAction) as BotAction[] if (validActions.length > 0) { enqueueActions(validActions) } } } catch { // Network error — bot will idle until next poll } finally { fetching = false } } function destroy(): void { destroyed = true queue.length = 0 pendingActions.length = 0 } return { getInput, enqueueActions, requestActions, destroy } } function isValidAction(s: string): s is BotAction { return [ 'idle', 'move_forward', 'move_back', 'jump', 'crouch', 'punch', 'kick', 'block', 'jump_punch', 'jump_kick', 'fireball', 'uppercut', 'dash_punch', 'spinning_kick', 'super_jump_kick', ].includes(s) } /** Build ArcadeGameState from two fighter instances and match info */ export function buildGameState( self: FighterInstance, opponent: FighterInstance, timer: number, round: number, maxRounds: number, ): ArcadeGameState { return { self: { hp: self.combat.hp, x: Math.round(self.obj.pos.x), state: self.combat.state, grounded: self.physics.grounded, }, opponent: { hp: opponent.combat.hp, x: Math.round(opponent.obj.pos.x), state: opponent.combat.state, grounded: opponent.physics.grounded, }, distance: Math.round(Math.abs(self.obj.pos.x - opponent.obj.pos.x)), timer, round, maxRounds, facingRight: self.physics.facingRight, } }