fix: both-wrong draws + double TTS entrance fix

Scoring: both-wrong is now a DRAW — equal scores (3/3), no winner,
symmetric minimal damage. Garbage answers no longer beat reasonable
ones just by being faster. Both-wrong narrations reflect the draw.

Entrance: removed duplicate announceDeepIntro() call from FightViewer
(was already called inside playEntrance). Removed _resetPositions()
after entrance (entrance already places fighters at home positions,
the extra reset caused a visible snap/reset).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-12 20:09:11 +00:00
co-authored by Claude Opus 4.6
parent 017d0e3e4c
commit 6f0eb92ebb
2 changed files with 18 additions and 27 deletions
+3 -7
View File
@@ -3,7 +3,7 @@ import { ref, onMounted, onUnmounted, nextTick } from 'vue'
import { createFightScene, type FightSceneController } from '../game/FightScene' import { createFightScene, type FightSceneController } from '../game/FightScene'
import { import {
fanfareRound, fanfareFight, announce, announceDeep, announceFast, fanfareRound, fanfareFight, announce, announceDeep, announceFast,
announceDeepIntro, announceRandomHype, announceRoundHype, announceRandomHype, announceRoundHype,
announceFinishHim, announceFlawlessVictory, announceFinishHim, announceFlawlessVictory,
sfxCrowdCheer, sfxCrowdGasp, sfxCrowdOoh, sfxApplause, sfxDrumRoll, sfxCrowdCheer, sfxCrowdGasp, sfxCrowdOoh, sfxApplause, sfxDrumRoll,
setMusicIntensity, stopAllAudio, setMusicIntensity, stopAllAudio,
@@ -382,8 +382,7 @@ async function _doReplay() {
} }
await sleep(300) await sleep(300)
// Deep movie trailer intro // Arena intro overlay (deep voice intro plays inside playEntrance)
announceDeepIntro()
await showOverlay(props.fight.arenaInfo?.name || 'THE RING', '#b83dff', 900) await showOverlay(props.fight.arenaInfo?.name || 'THE RING', '#b83dff', 900)
logItems.value.push( logItems.value.push(
{ type: 'system', round: 0, text: `ARENA: ${props.fight.arenaInfo?.name || 'THE RING'}`, color: 'neon-purple' }, { type: 'system', round: 0, text: `ARENA: ${props.fight.arenaInfo?.name || 'THE RING'}`, color: 'neon-purple' },
@@ -393,12 +392,9 @@ async function _doReplay() {
scrollLog() scrollLog()
await sleep(200) await sleep(200)
// Entrance animations // Entrance animations (positions fighters at home, no reset needed after)
if (scene) { if (scene) {
await scene.playEntrance() await scene.playEntrance()
// Safety: ensure fighters are visible after entrance (prevents invisible characters if entrance times out)
// Re-check: scene could be destroyed during async playEntrance()
scene?._resetPositions()
if (!scene) return if (!scene) return
await sleep(300) await sleep(300)
} }
+15 -20
View File
@@ -4,7 +4,7 @@ import {
MARGIN_TO_DAMAGE_SCALE, LOSER_DAMAGE_BASE, ARENA_DAMAGE_MULTIPLIER, MARGIN_TO_DAMAGE_SCALE, LOSER_DAMAGE_BASE, ARENA_DAMAGE_MULTIPLIER,
MAX_COMBO_STACKS, COMBO_DAMAGE_PER_STACK, MAX_COMBO_STACKS, COMBO_DAMAGE_PER_STACK,
FACTUAL_FASTER_BASE, FACTUAL_SLOWER_BASE, SPEED_ADVANTAGE_MULTIPLIER, SPEED_RATIO_MULTIPLIER, FACTUAL_FASTER_BASE, FACTUAL_SLOWER_BASE, SPEED_ADVANTAGE_MULTIPLIER, SPEED_RATIO_MULTIPLIER,
ONE_CORRECT_WINNER_BASE, CONFIDENCE_BONUS, NO_ANSWER_SCORE, BOTH_WRONG_FASTER, BOTH_WRONG_SLOWER, ONE_CORRECT_WINNER_BASE, CONFIDENCE_BONUS, NO_ANSWER_SCORE, BOTH_WRONG_SLOWER,
RETRO_SPEED_BONUS_CAP, RETRO_SPEED_BONUS_CAP,
LARGE_WIN_MARGIN, CLOSE_MATCH_MARGIN, WHIFF_NARRATION_THRESHOLD, LARGE_WIN_MARGIN, CLOSE_MATCH_MARGIN, WHIFF_NARRATION_THRESHOLD,
TIMEOUT_WINNER_SCORE, SCORE_ROUNDING_FACTOR, TIMEOUT_WINNER_SCORE, SCORE_ROUNDING_FACTOR,
@@ -145,11 +145,10 @@ export function scoreRound(
scoreA = NO_ANSWER_SCORE + (responseA.answer ? 1 : 0) scoreA = NO_ANSWER_SCORE + (responseA.answer ? 1 : 0)
scoreB = ONE_CORRECT_WINNER_BASE + correctB * CONFIDENCE_BONUS scoreB = ONE_CORRECT_WINNER_BASE + correctB * CONFIDENCE_BONUS
} else { } else {
// Both wrong -- speed tiebreaker in low range // Both wrong -- DRAW, nobody wins for getting it wrong
resultType = 'both-wrong' resultType = 'both-wrong'
const aFaster = responseA.timeMs <= responseB.timeMs scoreA = BOTH_WRONG_SLOWER
scoreA = aFaster ? BOTH_WRONG_FASTER : BOTH_WRONG_SLOWER scoreB = BOTH_WRONG_SLOWER
scoreB = aFaster ? BOTH_WRONG_SLOWER : BOTH_WRONG_FASTER
} }
} else { } else {
// No answers defined — both get base score (all challenges should be factual) // No answers defined — both get base score (all challenges should be factual)
@@ -175,6 +174,17 @@ export function scoreRound(
const narration = winnerId const narration = winnerId
? generateNarration(challenge, winnerName!, loserName!, margin, isCritical, resultType) ? generateNarration(challenge, winnerName!, loserName!, margin, isCritical, resultType)
: resultType === 'both-wrong'
? pick([
`Both bots WHIFFED! ${botA.name} and ${botB.name} got it wrong. Nobody wins this one!`,
`DOUBLE MISS! Neither ${botA.name} nor ${botB.name} could answer that. Embarrassing for everyone.`,
`${botA.name} and ${botB.name} both hallucinated! Nobody scores. The crowd throws things.`,
`Two wrong answers! ${botA.name} and ${botB.name} cancel each other out in wrongness.`,
`NOBODY got it right! ${botA.name} and ${botB.name} both swing and miss. Draw!`,
`${botA.name}: wrong. ${botB.name}: also wrong. The real winner is whoever stopped watching.`,
`Both bots confidently stated incorrect answers! ${botA.name} and ${botB.name} share the L equally.`,
`WRONG and WRONG! Neither ${botA.name} nor ${botB.name} had any idea. The crowd demands smarter bots.`,
])
: pick([ : pick([
`Dead even! ${botA.name} and ${botB.name} are perfectly matched. Like two politicians blaming each other.`, `Dead even! ${botA.name} and ${botB.name} are perfectly matched. Like two politicians blaming each other.`,
`IT'S A TIE! ${botA.name} and ${botB.name} cancel each other out like Congress!`, `IT'S A TIE! ${botA.name} and ${botB.name} cancel each other out like Congress!`,
@@ -240,21 +250,6 @@ function generateNarration(
const critPrefix = isCritical ? 'CRITICAL HIT! ' : '' const critPrefix = isCritical ? 'CRITICAL HIT! ' : ''
const isFactual = challenge.scoring === 'factual' const isFactual = challenge.scoring === 'factual'
// Both bots got it WRONG — narration must reflect that
if (isFactual && resultType === 'both-wrong') {
const bothWrong = [
`${critPrefix}Both bots WHIFFED! Nobody got it right but ${winner} failed FASTER. Is that even a win?`,
`${critPrefix}DOUBLE MISS! ${winner} and ${loser} both got it wrong! ${winner} wins on speed alone. Participation trophy energy.`,
`${critPrefix}Two wrong answers! ${winner} was wrong FASTER than ${loser}. Speed isn't everything, but here we are.`,
`${critPrefix}Neither bot had a clue! ${winner} guessed quicker. ${loser} took their time being equally wrong.`,
`${critPrefix}BOTH WRONG! The only thing ${winner} beat ${loser} at was being wrong first. The crowd boos.`,
`${critPrefix}${winner} AND ${loser} both hallucinated! ${winner} at least hallucinated with conviction. Speed win.`,
`${critPrefix}NOBODY got the answer! ${winner} earns the world's saddest victory by responding faster. Tragic.`,
`${critPrefix}Wrong and wrong! ${winner} speedran being incorrect! ${loser} at least took time to think about it. Both failed.`,
]
return pick(bothWrong)
}
if (isFactual && margin > LARGE_WIN_MARGIN) { if (isFactual && margin > LARGE_WIN_MARGIN) {
const bigWins = [ const bigWins = [
`${critPrefix}${winner} NAILS IT! ${loser} didn't even come close. Embarrassing, honestly.`, `${critPrefix}${winner} NAILS IT! ${loser} didn't even come close. Embarrassing, honestly.`,