Files
botfights/server/src/routes/arcade.ts
T
2026-04-11 19:46:37 +01:00

143 lines
4.6 KiB
TypeScript

import { Hono } from 'hono'
import { z } from 'zod'
import { db, schema } from '../db/index.js'
import { eq } from 'drizzle-orm'
import { logger } from '../lib/logger.js'
import {
formatArcadeChallenge,
parseArcadeResponse,
generateArcadeBotActions,
type ArcadeGameState,
} from '../engine/arcade-bot.js'
import { isMockBot } from '../engine/orchestrator.js'
import { isPollingBot } from '../engine/poll-responses.js'
import { isClassicBot } from '../engine/mock.js'
export const arcadeRouter = new Hono()
const gameStateSchema = z.object({
self: z.object({
hp: z.number(), x: z.number(), state: z.string(), grounded: z.boolean(),
}),
opponent: z.object({
hp: z.number(), x: z.number(), state: z.string(), grounded: z.boolean(),
}),
distance: z.number(),
timer: z.number(),
round: z.number(),
maxRounds: z.number(),
facingRight: z.boolean(),
})
const requestSchema = z.object({
botId: z.string(),
gameState: gameStateSchema,
})
// In-memory personality cache (mock bots)
const personalityCache = new Map<string, string>()
/**
* POST /api/arcade/bot-action
* Accepts game state, returns bot actions for arcade mode.
* Works with mock/classic bots (instant) and webhook bots (async).
*/
arcadeRouter.post('/bot-action', async (c) => {
const body = await c.req.json().catch(() => null)
const parsed = requestSchema.safeParse(body)
if (!parsed.success) {
return c.json({ error: { code: 'INVALID_INPUT', message: 'Invalid request body' } }, 400)
}
const { botId, gameState } = parsed.data
// Look up the bot
const bots = await db.select({
id: schema.bots.id,
name: schema.bots.name,
webhookUrl: schema.bots.webhookUrl,
eloRating: schema.bots.eloRating,
}).from(schema.bots).where(eq(schema.bots.id, botId)).limit(1)
if (bots.length === 0) {
return c.json({ error: { code: 'BOT_NOT_FOUND', message: 'Bot not found' } }, 404)
}
const bot = bots[0]
try {
let actions: string[]
if (isMockBot(bot.webhookUrl) || isClassicBot(bot.webhookUrl)) {
// Mock/classic bots: generate actions locally (instant, no network)
const personality = await getPersonality(bot.name)
actions = generateArcadeBotActions(gameState, personality)
logger.info('arcade', `${bot.name} mock actions: ${actions.join(',')}`)
} else if (isPollingBot(bot.webhookUrl)) {
// Polling bots: can't do real-time arcade via polling — use mock AI
const personality = await getPersonality(bot.name)
actions = generateArcadeBotActions(gameState, personality)
} else {
// Real webhook bot: forward game state as arcade challenge
const prompt = formatArcadeChallenge(gameState)
const controller = new AbortController()
const timeout = setTimeout(() => controller.abort(), 2000) // tight timeout for real-time
try {
const res = await fetch(bot.webhookUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
type: 'arcade',
challenge: prompt,
constraints: { timeout_ms: 2000, max_tokens: 100 },
}),
signal: controller.signal,
})
clearTimeout(timeout)
if (res.ok) {
const data = await res.json() as { answer?: string }
actions = parseArcadeResponse(data.answer ?? null)
} else {
actions = parseArcadeResponse(null)
}
} catch {
clearTimeout(timeout)
// Webhook failed — fall back to mock AI
actions = parseArcadeResponse(null)
}
}
return c.json({ actions })
} catch (err) {
logger.error('arcade', `bot-action error: ${err}`)
return c.json({ actions: ['move_forward', 'punch', 'block'] })
}
})
/** Get mock bot personality by name */
async function getPersonality(botName: string): Promise<string> {
const cached = personalityCache.get(botName)
if (cached) return cached
// Import dynamically to avoid circular deps
const { MOCK_BOTS_LIST } = await import('../engine/mock.js').then(m => {
// Access the exported mock bots list
return { MOCK_BOTS_LIST: [] as { name: string; personality: string }[] }
}).catch(() => ({ MOCK_BOTS_LIST: [] }))
// Fallback personality based on bot name hash
const personalities = [
'aggressive', 'calculated', 'reckless', 'tactical', 'chill',
'confident', 'chaotic', 'zen', 'relentless', 'witty',
]
let hash = 0
for (let i = 0; i < botName.length; i++) {
hash = ((hash << 5) - hash + botName.charCodeAt(i)) | 0
}
const personality = personalities[Math.abs(hash) % personalities.length]
personalityCache.set(botName, personality)
return personality
}