feat: v3 — fight loop, expanded challenges, FightPage ownership guard
- Add fight-loop CLI for automated mock fights with elo-based matchmaking - Expand challenge types, scoring narrations, arenas, and mock bot pool - FightPage "Fight again" buttons now only show for your own bot - FightViewer async scene init, live fight polling with round counter - Extract mock answers into separate answers module Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
47d20fbe66
commit
2c0323d5fb
@@ -66,7 +66,7 @@ const logItems = ref<{ type: string; round: number; text: string; color: string
|
||||
onMounted(async () => {
|
||||
if (props.autoplay) {
|
||||
// Fresh fight — start replay immediately instead of showing static result
|
||||
initScene()
|
||||
await initScene()
|
||||
await nextTick()
|
||||
replay()
|
||||
} else {
|
||||
@@ -74,7 +74,7 @@ onMounted(async () => {
|
||||
displayHpA.value = mapHp(props.fight.botAHp, props.fight.winnerId, props.fight.botA?.id)
|
||||
displayHpB.value = mapHp(props.fight.botBHp, props.fight.winnerId, props.fight.botB?.id)
|
||||
for (const r of props.fight.rounds) addRoundToLog(r, false)
|
||||
initScene()
|
||||
await initScene()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -86,7 +86,7 @@ function mapHp(hp: number, winnerId: string | null, botId: string | undefined):
|
||||
|
||||
onUnmounted(() => { scene?.destroy(); scene = null })
|
||||
|
||||
function initScene() {
|
||||
async function initScene() {
|
||||
if (!canvasRef.value || !props.fight.botA || !props.fight.botB) return
|
||||
if (scene) { scene.k.go('fight'); return }
|
||||
|
||||
@@ -96,7 +96,7 @@ function initScene() {
|
||||
canvasRef.value.height = container.clientHeight
|
||||
}
|
||||
|
||||
scene = createFightScene({
|
||||
scene = await createFightScene({
|
||||
canvas: canvasRef.value,
|
||||
botA: { name: props.fight.botA.name, seed: props.fight.botA.avatarSeed || props.fight.botA.name, tier: props.fight.botA.tier, archetype: props.fight.botA.archetype },
|
||||
botB: { name: props.fight.botB.name, seed: props.fight.botB.avatarSeed || props.fight.botB.name, tier: props.fight.botB.tier, archetype: props.fight.botB.archetype },
|
||||
@@ -110,8 +110,15 @@ const challengeLabel = (type: string) => {
|
||||
roast_battle: 'ROAST BATTLE', hallucination_check: 'HALLUCINATION CHECK',
|
||||
token_economy: 'TOKEN ECONOMY', creative_writing: 'CREATIVE WRITING',
|
||||
math_blitz: 'MATH BLITZ', trap_card: 'TRAP CARD',
|
||||
food_fight: 'FOOD FIGHT', wrestling_match: 'WRESTLING MATCH',
|
||||
music_battle: 'MUSIC BATTLE', magic_duel: 'MAGIC DUEL',
|
||||
sports_showdown: 'SPORTS SHOWDOWN', nature_clash: 'NATURE CLASH',
|
||||
space_war: 'SPACE WAR', hack_battle: 'HACK BATTLE',
|
||||
meme_war: 'MEME WAR', animal_kingdom: 'ANIMAL KINGDOM',
|
||||
demolition: 'DEMOLITION DERBY', vehicle_mayhem: 'VEHICLE MAYHEM',
|
||||
medieval_combat: 'MEDIEVAL COMBAT',
|
||||
}
|
||||
return labels[type] || type.toUpperCase()
|
||||
return labels[type] || type.replace(/_/g, ' ').toUpperCase()
|
||||
}
|
||||
|
||||
const tierClass = (t: number) => `tier-${t}`
|
||||
@@ -148,8 +155,8 @@ function addRoundToLog(round: Round, stagger: boolean): Promise<void> {
|
||||
logItems.value.push(
|
||||
{ type: 'header', round: round.roundNumber, text: `ROUND ${round.roundNumber}: ${challengeLabel(round.challengeType)}`, color: 'neon-purple' },
|
||||
{ type: 'prompt', round: round.roundNumber, text: challenge.prompt, color: 'text-muted' },
|
||||
{ type: 'responseA', round: round.roundNumber, text: `${props.fight.botA?.name}: ${round.botAResponse?.slice(0, 120) || '[TIMEOUT]'} (${round.botATimeMs}ms)`, color: 'neon-cyan' },
|
||||
{ type: 'responseB', round: round.roundNumber, text: `${props.fight.botB?.name}: ${round.botBResponse?.slice(0, 120) || '[TIMEOUT]'} (${round.botBTimeMs}ms)`, color: 'neon-pink' },
|
||||
{ type: 'responseA', round: round.roundNumber, text: `${props.fight.botA?.name}: ${round.botAResponse?.slice(0, 120) || '[NO RESPONSE]'} (${round.botATimeMs}ms)`, color: 'neon-cyan' },
|
||||
{ type: 'responseB', round: round.roundNumber, text: `${props.fight.botB?.name}: ${round.botBResponse?.slice(0, 120) || '[NO RESPONSE]'} (${round.botBTimeMs}ms)`, color: 'neon-pink' },
|
||||
)
|
||||
if (round.narration) logItems.value.push({ type: 'narration', round: round.roundNumber, text: `>> ${round.narration}`, color: 'neon-yellow' })
|
||||
const winner = round.winnerId === props.fight.botA?.id ? props.fight.botA?.name : round.winnerId === props.fight.botB?.id ? props.fight.botB?.name : 'DRAW'
|
||||
@@ -163,11 +170,11 @@ function addRoundToLog(round: Round, stagger: boolean): Promise<void> {
|
||||
scrollLog(); await sleep(150)
|
||||
logItems.value.push({ type: 'prompt', round: round.roundNumber, text: challenge.prompt, color: 'text-muted' })
|
||||
scrollLog(); await sleep(200)
|
||||
logItems.value.push({ type: 'responseA', round: round.roundNumber, text: `${props.fight.botA?.name}: ${round.botAResponse?.slice(0, 120) || '[TIMEOUT]'}`, color: 'neon-cyan' })
|
||||
logItems.value.push({ type: 'responseA', round: round.roundNumber, text: `${props.fight.botA?.name}: ${round.botAResponse?.slice(0, 120) || '[NO RESPONSE]'}`, color: 'neon-cyan' })
|
||||
scrollLog(); await sleep(150)
|
||||
logItems.value.push({ type: 'time', round: round.roundNumber, text: ` ${round.botATimeMs}ms | Score: ${round.botAScore}`, color: 'text-muted' })
|
||||
scrollLog(); await sleep(150)
|
||||
logItems.value.push({ type: 'responseB', round: round.roundNumber, text: `${props.fight.botB?.name}: ${round.botBResponse?.slice(0, 120) || '[TIMEOUT]'}`, color: 'neon-pink' })
|
||||
logItems.value.push({ type: 'responseB', round: round.roundNumber, text: `${props.fight.botB?.name}: ${round.botBResponse?.slice(0, 120) || '[NO RESPONSE]'}`, color: 'neon-pink' })
|
||||
scrollLog(); await sleep(150)
|
||||
logItems.value.push({ type: 'time', round: round.roundNumber, text: ` ${round.botBTimeMs}ms | Score: ${round.botBScore}`, color: 'text-muted' })
|
||||
scrollLog()
|
||||
@@ -183,7 +190,7 @@ async function replay() {
|
||||
logItems.value = []
|
||||
currentRound.value = 0
|
||||
|
||||
initScene()
|
||||
await initScene()
|
||||
scene?.startMusic()
|
||||
await sleep(300)
|
||||
|
||||
@@ -198,6 +205,12 @@ async function replay() {
|
||||
scrollLog()
|
||||
await sleep(200)
|
||||
|
||||
// Entrance animations
|
||||
if (scene) {
|
||||
await scene.playEntrance()
|
||||
await sleep(300)
|
||||
}
|
||||
|
||||
for (const round of props.fight.rounds) {
|
||||
currentRound.value = round.roundNumber
|
||||
|
||||
|
||||
+1671
-57
File diff suppressed because it is too large
Load Diff
@@ -13,7 +13,9 @@ const isLoading = ref(true)
|
||||
const isRequeueing = ref(false)
|
||||
const isLive = ref(false)
|
||||
const liveRounds = ref(0)
|
||||
const fightError = ref('')
|
||||
let pollHandle: ReturnType<typeof setInterval> | null = null
|
||||
let pollCount = 0
|
||||
|
||||
async function loadFight(): Promise<string | null> {
|
||||
try {
|
||||
@@ -34,13 +36,25 @@ onMounted(async () => {
|
||||
const status = await loadFight()
|
||||
isLoading.value = false
|
||||
|
||||
if (status !== 'finished') {
|
||||
if (status === null) {
|
||||
// Fight doesn't exist yet — might still be creating. Poll briefly.
|
||||
isLive.value = true
|
||||
} else if (status !== 'finished') {
|
||||
isLive.value = true
|
||||
}
|
||||
|
||||
if (isLive.value) {
|
||||
pollHandle = setInterval(async () => {
|
||||
pollCount++
|
||||
const s = await loadFight()
|
||||
if (s === 'finished') {
|
||||
isLive.value = false
|
||||
if (pollHandle) { clearInterval(pollHandle); pollHandle = null }
|
||||
} else if (pollCount > 60) {
|
||||
// 90 seconds max wait
|
||||
isLive.value = false
|
||||
fightError.value = 'Fight took too long. It may still be running — try refreshing.'
|
||||
if (pollHandle) { clearInterval(pollHandle); pollHandle = null }
|
||||
}
|
||||
}, 1500)
|
||||
}
|
||||
@@ -78,6 +92,17 @@ async function fightAgain(botId: string) {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-else-if="fightError" class="flex-1 flex flex-col items-center justify-center gap-3">
|
||||
<p class="font-display text-ko text-sm tracking-wider">{{ fightError }}</p>
|
||||
<button
|
||||
class="px-6 py-2 border border-neon-cyan/40 text-neon-cyan font-display font-bold text-xs tracking-wider
|
||||
hover:bg-neon-cyan/10 transition-all"
|
||||
@click="$router.push('/join')"
|
||||
>
|
||||
BACK TO LOBBY
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-else-if="!fight" class="flex-1 flex items-center justify-center">
|
||||
<p class="font-display text-text-muted">Fight not found.</p>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user