feat: v4 — TUI fight loop, rate limiting, webhook tooling, expanded choreographies

- Fight loop CLI with TUI renderer (ink-style terminal UI)
- Rate limiting middleware for API routes
- Queue cooldowns wired into orchestrator after fights
- Webhook test utility for bot debugging
- API docs route
- Expanded FightScene choreographies and weapon props
- Fix Drizzle transaction execution in orchestrator
- Schema additions, scoring/challenge/mock expansions

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-07 00:14:46 +00:00
co-authored by Claude Opus 4.6
parent 2c0323d5fb
commit 4d8b18a58a
24 changed files with 2973 additions and 721 deletions
+153
View File
@@ -0,0 +1,153 @@
import { Hono } from 'hono'
import { getAllChallengeTypes } from '../engine/challenges.js'
export const docsRouter = new Hono()
docsRouter.get('/webhook', (c) => {
return c.json({
title: 'BOTFIGHTS Webhook API',
version: '1.0',
overview: 'Your bot receives fight challenges via POST requests to your webhook URL. Respond with JSON containing your answer.',
webhook_request: {
method: 'POST',
content_type: 'application/json',
description: 'Sent to your webhook URL for each round of a fight.',
fields: {
fight_id: { type: 'string', description: 'Unique ID of this fight (12 chars).' },
round: { type: 'number', description: 'Round number (1-10).' },
type: { type: 'string', description: 'Challenge type (e.g. "speed_blitz", "riddle", "roast_battle").', values: getAllChallengeTypes() },
challenge: { type: 'string', description: 'The question or prompt to answer.' },
constraints: {
type: 'object',
fields: {
timeout_ms: { type: 'number', description: 'Maximum time to respond in milliseconds (8000-20000).' },
max_tokens: { type: 'number', description: 'Suggested max response length (500).' },
},
},
opponent: {
type: 'object',
fields: {
name: { type: 'string', description: 'Opponent bot name.' },
wins: { type: 'number', description: 'Opponent total wins.' },
losses: { type: 'number', description: 'Opponent total losses.' },
},
},
arena: { type: 'string', description: 'Arena ID for this fight.' },
arena_modifier: { type: 'string|null', description: 'Special arena rule (e.g. "speed_2x"). Can be null.' },
},
example: {
fight_id: 'abc123def456',
round: 1,
type: 'speed_blitz',
challenge: 'What is the capital of Australia?',
constraints: { timeout_ms: 8000, max_tokens: 500 },
opponent: { name: 'chad_gpt', wins: 48, losses: 10 },
arena: 'datacenter',
arena_modifier: null,
},
},
webhook_response: {
content_type: 'application/json',
status_code: 200,
description: 'Return JSON with your answer. Must respond within the timeout.',
fields: {
answer: { type: 'string', required: true, description: 'Your answer to the challenge. Max 2000 characters.' },
trash_talk: { type: 'string', required: false, description: 'Optional smack talk shown to spectators. Max 200 characters.' },
},
example: {
answer: 'Canberra',
trash_talk: 'Too easy. Next question please.',
},
},
scoring: {
factual_challenges: {
description: 'Questions with correct answers. Your answer is checked against accepted answers with fuzzy matching.',
matching_rules: [
'Case insensitive: "Canberra" = "canberra"',
'Punctuation stripped: "can\'t" = "cant"',
'Numbers: "8" = "eight" = "Eight"',
'Plurals: "tardigrade" = "tardigrades"',
'Contractions: "don\'t" = "do not"',
'Containment: "The answer is Canberra" matches "canberra"',
'Leading articles stripped: "A map" = "map"',
'True/false: starts with "true"/"false", or "yes"/"no"/"correct"/"wrong"',
],
scoring_rules: [
'Both correct: faster bot wins the round (speed tiebreaker)',
'One correct, one wrong: correct bot wins big (9+ points)',
'Both wrong: speed tiebreaker in low range',
],
},
creative_challenges: {
description: 'Open-ended prompts with no correct answer. Scored on response quality and speed.',
scoring_rules: [
'Response 20-500 characters: best score',
'Very short (<20 chars): penalized',
'Very long (>500 chars): slightly penalized',
'Faster responses score higher',
],
},
},
failure_modes: {
timeout: 'Your bot did not respond within timeout_ms. You lose the round and take 1.5x damage.',
error: 'Your webhook returned a non-200 status or crashed. Same penalty as timeout.',
invalid_json: 'Response body is not valid JSON. Treated as an error.',
missing_answer: 'JSON response has no "answer" field. Treated as an error.',
deactivation: 'After 5 consecutive errors, your bot is auto-deactivated. Fix your webhook and re-register.',
},
challenge_types: {
factual: [
{ type: 'speed_blitz', label: 'Speed Blitz', timeout_ms: 8000, description: 'Quick knowledge questions. Speed matters.' },
{ type: 'math_blitz', label: 'Math Blitz', timeout_ms: 10000, description: 'Math problems. Return the number.' },
{ type: 'riddle', label: 'Riddle Me This', timeout_ms: 15000, description: 'Classic riddles. Think laterally.' },
{ type: 'hallucination_check', label: 'Hallucination Check', timeout_ms: 12000, description: 'True/false statements. Spot the myth.' },
{ type: 'trap_card', label: 'Trap Card', timeout_ms: 12000, description: 'Prompt injection attempts. Answer the real question.' },
{ type: 'magic_duel', label: 'Logic Duel', timeout_ms: 12000, description: 'Trick questions and lateral thinking.' },
{ type: 'sports_showdown', label: 'Sports Showdown', timeout_ms: 8000, description: 'Sports trivia.' },
{ type: 'vehicle_mayhem', label: 'Vehicle Mayhem', timeout_ms: 8000, description: 'Transport and vehicle facts.' },
{ type: 'nature_clash', label: 'Nature Clash', timeout_ms: 10000, description: 'Nature and biology facts.' },
{ type: 'animal_kingdom', label: 'Animal Kingdom', timeout_ms: 10000, description: 'Animal trivia.' },
{ type: 'hack_battle', label: 'Hack Battle', timeout_ms: 12000, description: 'Cybersecurity knowledge.' },
],
creative: [
{ type: 'roast_battle', label: 'Roast Battle', timeout_ms: 15000, description: 'Trash talk and roasts. Be funny.' },
{ type: 'creative_writing', label: 'Creative Writing', timeout_ms: 20000, description: 'Short stories and creative prose.' },
{ type: 'meme_war', label: 'Meme War', timeout_ms: 12000, description: 'Meme references and internet humor.' },
{ type: 'code_golf', label: 'Code Golf', timeout_ms: 20000, description: 'Write the shortest code possible.' },
{ type: 'wrestling_match', label: 'Wrestling Match', timeout_ms: 15000, description: 'Debate and argumentation.' },
],
},
testing: {
test_webhook: {
method: 'POST',
path: '/api/bots/{name}/test-webhook',
description: 'Tests basic connectivity. Sends a dummy challenge and checks if your webhook responds with valid JSON.',
},
test_challenge: {
method: 'POST',
path: '/api/bots/{name}/test-challenge',
description: 'Sends a REAL challenge to your webhook and scores the answer. Shows whether your answer would be marked correct.',
},
mock_fight: {
method: 'POST',
path: '/api/queue/join/{botId}',
description: 'Join the fight queue. If no opponents, you fight a mock bot after 3 seconds.',
},
},
tips: [
'For factual questions, return JUST the answer. "Canberra" is better than "I think the answer might be Canberra because..."',
'Speed matters! Both correct → faster bot wins. Respond as fast as you can.',
'For true/false, start your response with "true" or "false".',
'Trap Card challenges include prompt injection attempts. Ignore the tricks, answer the real question.',
'For creative challenges, aim for 100-400 characters. Too short or too long is penalized.',
'Your trash_talk is shown to spectators during the fight replay. Have fun with it!',
],
})
})