feat: show rejoin link when bot is already in a fight
- Track active fight IDs per bot in orchestrator (Set → Map) - Return fightId in "already in fight" error responses (409) - Frontend shows "REJOIN FIGHT" link instead of generic error Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
64273cf145
commit
e31d49898b
@@ -21,6 +21,7 @@ const step = ref<string>('login')
|
|||||||
const isHumanMode = ref(false)
|
const isHumanMode = ref(false)
|
||||||
const selectedHumanSeed = ref('baby_fighter_1')
|
const selectedHumanSeed = ref('baby_fighter_1')
|
||||||
const error = ref('')
|
const error = ref('')
|
||||||
|
const activeFightLink = ref('')
|
||||||
const rateLimitCountdown = ref(0)
|
const rateLimitCountdown = ref(0)
|
||||||
let rateLimitTimer: ReturnType<typeof setInterval> | null = null
|
let rateLimitTimer: ReturnType<typeof setInterval> | null = null
|
||||||
const isJoining = ref(false)
|
const isJoining = ref(false)
|
||||||
@@ -105,6 +106,7 @@ const archetypeList = [
|
|||||||
function handleError(e: unknown, fallback: string) {
|
function handleError(e: unknown, fallback: string) {
|
||||||
const msg = e instanceof Error ? e.message : fallback
|
const msg = e instanceof Error ? e.message : fallback
|
||||||
error.value = msg
|
error.value = msg
|
||||||
|
activeFightLink.value = ''
|
||||||
// Parse "Slow down" with retry seconds from the error message or check for countdown pattern
|
// Parse "Slow down" with retry seconds from the error message or check for countdown pattern
|
||||||
if (msg.toLowerCase().includes('too many requests') || msg.toLowerCase().includes('slow down')) {
|
if (msg.toLowerCase().includes('too many requests') || msg.toLowerCase().includes('slow down')) {
|
||||||
startRateLimitTimer(msg)
|
startRateLimitTimer(msg)
|
||||||
@@ -394,9 +396,14 @@ async function fight() {
|
|||||||
return // Don't reset flag — navigation will unmount component
|
return // Don't reset flag — navigation will unmount component
|
||||||
} else {
|
} else {
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
|
if (data.fightId) {
|
||||||
|
activeFightLink.value = data.fightId
|
||||||
|
error.value = 'You\'re already in a fight!'
|
||||||
|
} else {
|
||||||
const msg = data.retryAfterSec ? `${data.error} (${data.retryAfterSec}s)` : (data.error || 'Failed to join.')
|
const msg = data.retryAfterSec ? `${data.error} (${data.retryAfterSec}s)` : (data.error || 'Failed to join.')
|
||||||
handleError(new Error(msg), 'Failed to join.')
|
handleError(new Error(msg), 'Failed to join.')
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
error.value = 'Network error.'
|
error.value = 'Network error.'
|
||||||
}
|
}
|
||||||
@@ -443,9 +450,14 @@ async function practice() {
|
|||||||
return // Don't reset flag — navigation will unmount component
|
return // Don't reset flag — navigation will unmount component
|
||||||
} else {
|
} else {
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
|
if (data.fightId) {
|
||||||
|
activeFightLink.value = data.fightId
|
||||||
|
error.value = 'You\'re already in a fight!'
|
||||||
|
} else {
|
||||||
const msg = data.retryAfterSec ? `${data.error} (${data.retryAfterSec}s)` : (data.error || 'Failed to start practice fight.')
|
const msg = data.retryAfterSec ? `${data.error} (${data.retryAfterSec}s)` : (data.error || 'Failed to start practice fight.')
|
||||||
handleError(new Error(msg), 'Practice fight failed.')
|
handleError(new Error(msg), 'Practice fight failed.')
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
error.value = 'Network error.'
|
error.value = 'Network error.'
|
||||||
}
|
}
|
||||||
@@ -1144,6 +1156,14 @@ function handleSignOut() {
|
|||||||
<!-- Error display -->
|
<!-- Error display -->
|
||||||
<div v-if="error" class="mt-4 p-3 border-2 border-ko/30 bg-ko/5 text-center">
|
<div v-if="error" class="mt-4 p-3 border-2 border-ko/30 bg-ko/5 text-center">
|
||||||
<p class="font-mono text-xs text-ko">{{ error }}</p>
|
<p class="font-mono text-xs text-ko">{{ error }}</p>
|
||||||
|
<RouterLink
|
||||||
|
v-if="activeFightLink"
|
||||||
|
:to="`/arena/${activeFightLink}`"
|
||||||
|
class="inline-block mt-2 px-6 py-2 bg-neon-cyan/10 border-2 border-neon-cyan text-neon-cyan
|
||||||
|
font-display font-black text-xs tracking-widest hover:bg-neon-cyan/20 transition-all neon-border-cyan"
|
||||||
|
>
|
||||||
|
REJOIN FIGHT
|
||||||
|
</RouterLink>
|
||||||
<div v-if="rateLimitCountdown > 0" class="mt-2 flex items-center justify-center gap-2">
|
<div v-if="rateLimitCountdown > 0" class="mt-2 flex items-center justify-center gap-2">
|
||||||
<div class="w-4 h-4 border-2 border-neon-purple/60 border-t-neon-purple rounded-full animate-spin" />
|
<div class="w-4 h-4 border-2 border-neon-purple/60 border-t-neon-purple rounded-full animate-spin" />
|
||||||
<p class="font-display font-bold text-sm text-neon-purple tracking-wider">
|
<p class="font-display font-bold text-sm text-neon-purple tracking-wider">
|
||||||
|
|||||||
@@ -47,7 +47,8 @@ interface WebhookResponse {
|
|||||||
import { MAX_ROUNDS, KO_THRESHOLD, MAX_RESPONSE_BYTES, STARTING_HP, ELO_K_FACTOR, ELO_K_FACTOR_MOCK, MAX_ANSWER_LENGTH, MAX_TRASH_TALK_LENGTH } from '../lib/constants.js'
|
import { MAX_ROUNDS, KO_THRESHOLD, MAX_RESPONSE_BYTES, STARTING_HP, ELO_K_FACTOR, ELO_K_FACTOR_MOCK, MAX_ANSWER_LENGTH, MAX_TRASH_TALK_LENGTH } from '../lib/constants.js'
|
||||||
|
|
||||||
// Track bots currently in a fight to prevent concurrent fights
|
// Track bots currently in a fight to prevent concurrent fights
|
||||||
const activeFighters = new Set<string>()
|
// Maps botId → fightId so we can direct users to their active fight
|
||||||
|
const activeFighters = new Map<string, string>()
|
||||||
|
|
||||||
export function getActiveFighterCount(): number {
|
export function getActiveFighterCount(): number {
|
||||||
return activeFighters.size
|
return activeFighters.size
|
||||||
@@ -57,6 +58,10 @@ export function isInFight(botId: string): boolean {
|
|||||||
return activeFighters.has(botId)
|
return activeFighters.has(botId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getActiveFightId(botId: string): string | undefined {
|
||||||
|
return activeFighters.get(botId)
|
||||||
|
}
|
||||||
|
|
||||||
function emit(fightId: string, type: string, data: Record<string, unknown>) {
|
function emit(fightId: string, type: string, data: Record<string, unknown>) {
|
||||||
fightEvents.emit({
|
fightEvents.emit({
|
||||||
fightId,
|
fightId,
|
||||||
@@ -621,13 +626,13 @@ export async function runFight(botAId: string, botBId: string, mode: 'free' | 'r
|
|||||||
if (activeFighters.has(botAId)) throw new Error(`Bot ${botAId} is already in a fight`)
|
if (activeFighters.has(botAId)) throw new Error(`Bot ${botAId} is already in a fight`)
|
||||||
if (activeFighters.has(botBId)) throw new Error(`Bot ${botBId} is already in a fight`)
|
if (activeFighters.has(botBId)) throw new Error(`Bot ${botBId} is already in a fight`)
|
||||||
|
|
||||||
activeFighters.add(botAId)
|
|
||||||
activeFighters.add(botBId)
|
|
||||||
|
|
||||||
try {
|
|
||||||
const [botA, botB] = await loadBots(botAId, botBId)
|
const [botA, botB] = await loadBots(botAId, botBId)
|
||||||
const arena = randomArena()
|
const arena = randomArena()
|
||||||
const fightId = await createFightRecord(botA, botB, arena, mode)
|
const fightId = await createFightRecord(botA, botB, arena, mode)
|
||||||
|
activeFighters.set(botAId, fightId)
|
||||||
|
activeFighters.set(botBId, fightId)
|
||||||
|
|
||||||
|
try {
|
||||||
await executeFightRounds(fightId, botA, botB, arena, mode)
|
await executeFightRounds(fightId, botA, botB, arena, mode)
|
||||||
return fightId
|
return fightId
|
||||||
} finally {
|
} finally {
|
||||||
@@ -644,12 +649,11 @@ export async function runFightAsync(botAId: string, botBId: string, mode: 'free'
|
|||||||
if (activeFighters.has(botAId)) throw new Error(`Bot ${botAId} is already in a fight`)
|
if (activeFighters.has(botAId)) throw new Error(`Bot ${botAId} is already in a fight`)
|
||||||
if (activeFighters.has(botBId)) throw new Error(`Bot ${botBId} is already in a fight`)
|
if (activeFighters.has(botBId)) throw new Error(`Bot ${botBId} is already in a fight`)
|
||||||
|
|
||||||
activeFighters.add(botAId)
|
|
||||||
activeFighters.add(botBId)
|
|
||||||
|
|
||||||
const [botA, botB] = await loadBots(botAId, botBId)
|
const [botA, botB] = await loadBots(botAId, botBId)
|
||||||
const arena = randomArena()
|
const arena = randomArena()
|
||||||
const fightId = await createFightRecord(botA, botB, arena, mode)
|
const fightId = await createFightRecord(botA, botB, arena, mode)
|
||||||
|
activeFighters.set(botAId, fightId)
|
||||||
|
activeFighters.set(botBId, fightId)
|
||||||
|
|
||||||
executeFightRounds(fightId, botA, botB, arena, mode)
|
executeFightRounds(fightId, botA, botB, arena, mode)
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { db, schema } from '../db/index.js'
|
import { db, schema } from '../db/index.js'
|
||||||
import { logger } from '../lib/logger.js'
|
import { logger } from '../lib/logger.js'
|
||||||
import { eq } from 'drizzle-orm'
|
import { eq } from 'drizzle-orm'
|
||||||
import { runFightAsync, isInFight } from './orchestrator.js'
|
import { runFightAsync, isInFight, getActiveFightId } from './orchestrator.js'
|
||||||
import { seedMockBots } from './mock.js'
|
import { seedMockBots } from './mock.js'
|
||||||
|
|
||||||
interface QueueEntry {
|
interface QueueEntry {
|
||||||
@@ -55,9 +55,12 @@ export async function joinQueue(botId: string): Promise<string> {
|
|||||||
throw new Error(`Cooldown active. Wait ${waitSec}s.`)
|
throw new Error(`Cooldown active. Wait ${waitSec}s.`)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if already in a fight
|
// Check if already in a fight — return the fight ID so frontend can redirect
|
||||||
if (isInFight(botId)) {
|
if (isInFight(botId)) {
|
||||||
throw new Error('Bot is already in a fight.')
|
const activeFightId = getActiveFightId(botId)
|
||||||
|
const err = new Error('Bot is already in a fight.')
|
||||||
|
;(err as any).fightId = activeFightId
|
||||||
|
throw err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load bot
|
// Load bot
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { eq, desc, inArray } from 'drizzle-orm'
|
|||||||
import { ARENAS } from '../engine/arenas.js'
|
import { ARENAS } from '../engine/arenas.js'
|
||||||
import { runMockFight, isClassicBot } from '../engine/mock.js'
|
import { runMockFight, isClassicBot } from '../engine/mock.js'
|
||||||
import { startFightLoop } from '../engine/fight-loop.js'
|
import { startFightLoop } from '../engine/fight-loop.js'
|
||||||
import { runFight, runFightAsync, isInFight } from '../engine/orchestrator.js'
|
import { runFight, runFightAsync, isInFight, getActiveFightId } from '../engine/orchestrator.js'
|
||||||
import { fightEvents } from '../engine/events.js'
|
import { fightEvents } from '../engine/events.js'
|
||||||
import { botRateLimit } from '../middleware/rate-limit.js'
|
import { botRateLimit } from '../middleware/rate-limit.js'
|
||||||
import { getPendingChallenge, getPendingAnswers, submitHumanResponse } from '../engine/human-responses.js'
|
import { getPendingChallenge, getPendingAnswers, submitHumanResponse } from '../engine/human-responses.js'
|
||||||
@@ -233,7 +233,7 @@ fightsRouter.post('/matchmake/:botId', botRateLimit(10_000), async (c) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isInFight(botId)) {
|
if (isInFight(botId)) {
|
||||||
return c.json({ error: 'Bot is already in a fight.' }, 400)
|
return c.json({ error: 'Bot is already in a fight.', fightId: getActiveFightId(botId) }, 409)
|
||||||
}
|
}
|
||||||
|
|
||||||
const allBots = await db.select()
|
const allBots = await db.select()
|
||||||
@@ -285,7 +285,7 @@ fightsRouter.post('/practice/:botId', botRateLimit(10_000), async (c) => {
|
|||||||
const bot = botRows[0]
|
const bot = botRows[0]
|
||||||
|
|
||||||
if (isInFight(botId)) {
|
if (isInFight(botId)) {
|
||||||
return c.json({ error: 'Bot is already in a fight.' }, 400)
|
return c.json({ error: 'Bot is already in a fight.', fightId: getActiveFightId(botId) }, 409)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find all classic bots
|
// Find all classic bots
|
||||||
|
|||||||
@@ -31,9 +31,10 @@ queueRouter.post('/join/:botId', async (c) => {
|
|||||||
try {
|
try {
|
||||||
const fightId = await joinQueue(botId)
|
const fightId = await joinQueue(botId)
|
||||||
return c.json({ fightId, message: 'Matched! Fight starting.' })
|
return c.json({ fightId, message: 'Matched! Fight starting.' })
|
||||||
} catch (err) {
|
} catch (err: any) {
|
||||||
const message = err instanceof Error ? err.message : 'Queue error'
|
const message = err instanceof Error ? err.message : 'Queue error'
|
||||||
return c.json({ error: message }, 500)
|
const status = message.includes('already in a fight') ? 409 : 500
|
||||||
|
return c.json({ error: message, fightId: err?.fightId || undefined }, status)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user