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
+37 -25
View File
@@ -29,7 +29,7 @@ export function scoreRound(
comboA: number,
comboB: number,
): RoundResult {
// Handle timeouts/errors instant loss for the failing bot
// Handle timeouts/errors -- instant loss for the failing bot
if (responseA.timedOut && responseB.timedOut) {
return {
botAScore: 0, botBScore: 0,
@@ -70,19 +70,17 @@ export function scoreRound(
let scoreB: number
if (challenge.answers && challenge.answers.length > 0) {
// ═══ FACTUAL SCORING ═══
// Check correctness against known answers
// === FACTUAL SCORING ===
const correctA = checkAnswer(responseA.answer, challenge.answers)
const correctB = checkAnswer(responseB.answer, challenge.answers)
if (correctA > 0 && correctB > 0) {
// Both correct speed is tiebreaker
// Both correct -- speed is tiebreaker
const faster = Math.min(responseA.timeMs, responseB.timeMs)
const slower = Math.max(responseA.timeMs, responseB.timeMs)
const speedRatio = slower > 0 ? faster / slower : 1
const aFaster = responseA.timeMs <= responseB.timeMs
// Confidence bonus (full match vs partial)
const confA = Math.min(correctA, 1)
const confB = Math.min(correctB, 1)
@@ -94,22 +92,19 @@ export function scoreRound(
scoreB = 7 + (1 - speedRatio) * 2 + confB
}
} else if (correctA > 0 && correctB === 0) {
// A correct, B wrong — A wins big
scoreA = 9 + correctA * 0.5
scoreB = 1 + (responseB.answer ? 1 : 0) // tiny credit for trying
scoreB = 1 + (responseB.answer ? 1 : 0)
} else if (correctB > 0 && correctA === 0) {
// B correct, A wrong — B wins big
scoreA = 1 + (responseA.answer ? 1 : 0)
scoreB = 9 + correctB * 0.5
} else {
// Both wrong speed tiebreaker in low range
// Both wrong -- speed tiebreaker in low range
const aFaster = responseA.timeMs <= responseB.timeMs
scoreA = aFaster ? 4 : 3
scoreB = aFaster ? 3 : 4
}
} else {
// ═══ CREATIVE SCORING ═══
// Heuristic: response quality estimation (length + speed)
// === CREATIVE SCORING ===
const qualA = estimateQuality(responseA)
const qualB = estimateQuality(responseB)
const total = qualA + qualB || 1
@@ -123,10 +118,8 @@ export function scoreRound(
const winnerName = winnerId === botA.id ? botA.name : winnerId === botB.id ? botB.name : null
const loserName = winnerId === botA.id ? botB.name : winnerId === botB.id ? botA.name : null
// Critical hit on big margin
const isCritical = margin > 4
// Calculate damage
let winnerDamage = challenge.baseDamage + margin * 2
if (isCritical) winnerDamage *= 1.5
const winnerCombo = winnerId === botA.id ? comboA : comboB
@@ -156,7 +149,6 @@ function applyModifiers(
combo: number,
): number {
let d = damage
// Combo multiplier (caps at 2x)
if (combo > 0) {
d *= 1 + Math.min(combo, 5) * 0.2
}
@@ -164,13 +156,36 @@ function applyModifiers(
}
function estimateQuality(response: BotResponse): number {
if (!response.answer) return 1
const len = response.answer.length
// Reasonable length gets a bonus, very short or very long gets penalized
const lengthScore = len > 20 && len < 500 ? 5 : len > 500 ? 3 : 2
// Faster is slightly better
const speedBonus = Math.max(0, 3 - response.timeMs / 5000)
return lengthScore + speedBonus
if (!response.answer) return 0.5
const text = response.answer.trim()
const len = text.length
if (len < 10) return 1
// Detect low-effort spam (repeated chars)
const uniqueChars = new Set(text.toLowerCase()).size
const charRatio = uniqueChars / Math.min(len, 100)
if (charRatio < 0.1) return 0.5
// Word diversity (unique words / total words)
const words = text.split(/\s+/)
const uniqueWords = new Set(words.map(w => w.toLowerCase()))
const wordDiversity = uniqueWords.size / Math.max(words.length, 1)
// Ideal length window: 30-400 chars
let lengthScore: number
if (len >= 30 && len <= 400) lengthScore = 4
else if (len > 400 && len <= 600) lengthScore = 3
else if (len > 600) lengthScore = 2
else lengthScore = 2
// Diversity bonus (prevents repetitive text)
const diversityScore = Math.min(wordDiversity * 4, 3)
// Speed bonus (faster is slightly better)
const speedBonus = Math.max(0, 2 - response.timeMs / 8000)
return lengthScore + diversityScore + speedBonus
}
function generateNarration(
@@ -183,7 +198,6 @@ function generateNarration(
const critPrefix = isCritical ? 'CRITICAL HIT! ' : ''
const isFactual = challenge.scoring === 'factual'
// Big margin = one got it right and the other didn't
if (isFactual && margin > 5) {
const bigWins = [
`${critPrefix}${winner} NAILS IT! ${loser} didn't even come close.`,
@@ -195,18 +209,16 @@ function generateNarration(
return bigWins[Math.floor(Math.random() * bigWins.length)]
}
// Factual — both correct, speed tiebreaker
if (isFactual && margin <= 3) {
const closeOnes = [
`${critPrefix}Both bots got it right, but ${winner} was FASTER! ${loser} needs to pick up the pace.`,
`${critPrefix}Correct on both sides! ${winner} edges it out with lightning speed.`,
`${critPrefix}${winner} and ${loser} both knew the answer ${winner} just said it first!`,
`${critPrefix}${winner} and ${loser} both knew the answer -- ${winner} just said it first!`,
`${critPrefix}A battle of speed! ${winner} fires back a fraction faster than ${loser}.`,
]
return closeOnes[Math.floor(Math.random() * closeOnes.length)]
}
// Generic narrations by category
const narrations: Record<string, string[]> = {
speed_blitz: [
`${critPrefix}${winner} fires back in the blink of an eye! ${loser} is still loading.`,