stuff
This commit is contained in:
@@ -0,0 +1,301 @@
|
||||
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<PlayerInput>
|
||||
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<void> {
|
||||
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,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user