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() /** * 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 { 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 }