From 6f0eb92ebb2dd271755f77ecf294c1175da6f78b Mon Sep 17 00:00:00 2001 From: Dorian Date: Thu, 12 Mar 2026 20:09:11 +0000 Subject: [PATCH] fix: both-wrong draws + double TTS entrance fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- frontend/src/components/FightViewer.vue | 10 +++---- server/src/engine/scoring.ts | 35 +++++++++++-------------- 2 files changed, 18 insertions(+), 27 deletions(-) diff --git a/frontend/src/components/FightViewer.vue b/frontend/src/components/FightViewer.vue index 22bc8c4..b781110 100644 --- a/frontend/src/components/FightViewer.vue +++ b/frontend/src/components/FightViewer.vue @@ -3,7 +3,7 @@ import { ref, onMounted, onUnmounted, nextTick } from 'vue' import { createFightScene, type FightSceneController } from '../game/FightScene' import { fanfareRound, fanfareFight, announce, announceDeep, announceFast, - announceDeepIntro, announceRandomHype, announceRoundHype, + announceRandomHype, announceRoundHype, announceFinishHim, announceFlawlessVictory, sfxCrowdCheer, sfxCrowdGasp, sfxCrowdOoh, sfxApplause, sfxDrumRoll, setMusicIntensity, stopAllAudio, @@ -382,8 +382,7 @@ async function _doReplay() { } await sleep(300) - // Deep movie trailer intro - announceDeepIntro() + // Arena intro overlay (deep voice intro plays inside playEntrance) await showOverlay(props.fight.arenaInfo?.name || 'THE RING', '#b83dff', 900) logItems.value.push( { type: 'system', round: 0, text: `ARENA: ${props.fight.arenaInfo?.name || 'THE RING'}`, color: 'neon-purple' }, @@ -393,12 +392,9 @@ async function _doReplay() { scrollLog() await sleep(200) - // Entrance animations + // Entrance animations (positions fighters at home, no reset needed after) if (scene) { 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 await sleep(300) } diff --git a/server/src/engine/scoring.ts b/server/src/engine/scoring.ts index bdfac7a..608b760 100644 --- a/server/src/engine/scoring.ts +++ b/server/src/engine/scoring.ts @@ -4,7 +4,7 @@ import { MARGIN_TO_DAMAGE_SCALE, LOSER_DAMAGE_BASE, ARENA_DAMAGE_MULTIPLIER, MAX_COMBO_STACKS, COMBO_DAMAGE_PER_STACK, 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, LARGE_WIN_MARGIN, CLOSE_MATCH_MARGIN, WHIFF_NARRATION_THRESHOLD, TIMEOUT_WINNER_SCORE, SCORE_ROUNDING_FACTOR, @@ -145,11 +145,10 @@ export function scoreRound( scoreA = NO_ANSWER_SCORE + (responseA.answer ? 1 : 0) scoreB = ONE_CORRECT_WINNER_BASE + correctB * CONFIDENCE_BONUS } else { - // Both wrong -- speed tiebreaker in low range + // Both wrong -- DRAW, nobody wins for getting it wrong resultType = 'both-wrong' - const aFaster = responseA.timeMs <= responseB.timeMs - scoreA = aFaster ? BOTH_WRONG_FASTER : BOTH_WRONG_SLOWER - scoreB = aFaster ? BOTH_WRONG_SLOWER : BOTH_WRONG_FASTER + scoreA = BOTH_WRONG_SLOWER + scoreB = BOTH_WRONG_SLOWER } } else { // No answers defined — both get base score (all challenges should be factual) @@ -175,6 +174,17 @@ export function scoreRound( const narration = winnerId ? 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([ `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!`, @@ -240,21 +250,6 @@ function generateNarration( const critPrefix = isCritical ? 'CRITICAL HIT! ' : '' 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) { const bigWins = [ `${critPrefix}${winner} NAILS IT! ${loser} didn't even come close. Embarrassing, honestly.`,