From 276cbd6e311630de52135e86a593c43488cf6db4 Mon Sep 17 00:00:00 2001 From: Dorian Date: Mon, 9 Mar 2026 07:26:50 +0000 Subject: [PATCH] refactor: replace all hardcoded numbers in scoring.ts with named constants All magic numbers in scoring.ts now reference constants from lib/constants.ts: damage multipliers, score thresholds, quality parameters, narration margins, and rounding factors. Co-Authored-By: Claude Opus 4.6 --- server/src/engine/scoring.ts | 105 ++++++++++++++++++----------------- server/src/lib/constants.ts | 5 ++ 2 files changed, 58 insertions(+), 52 deletions(-) diff --git a/server/src/engine/scoring.ts b/server/src/engine/scoring.ts index 84ea8d6..20cf101 100644 --- a/server/src/engine/scoring.ts +++ b/server/src/engine/scoring.ts @@ -12,6 +12,7 @@ import { RETRO_SPEED_BONUS_CAP, LARGE_WIN_MARGIN, CLOSE_MATCH_MARGIN, WHIFF_NARRATION_THRESHOLD, DEFAULT_CHALLENGE_TIMEOUT_MS, + TIMEOUT_WINNER_SCORE, CREATIVE_TOTAL_SCORE, SCORE_ROUNDING_FACTOR, } from '../lib/constants.js' import type { Challenge } from './challenges.js' import { pick } from '../lib/utils.js' @@ -69,9 +70,9 @@ export function scoreRound( } if (responseA.timedOut || responseA.error) { - const dmg = applyModifiers(challenge.baseDamage * 1.5, challenge, arenaModifier, comboB) + const dmg = applyModifiers(challenge.baseDamage * TIMEOUT_DAMAGE_MULTIPLIER, challenge, arenaModifier, comboB) return { - botAScore: 0, botBScore: 10, + botAScore: 0, botBScore: TIMEOUT_WINNER_SCORE, botADamage: 0, botBDamage: Math.round(dmg), winnerId: botB.id, narration: responseA.timedOut @@ -92,9 +93,9 @@ export function scoreRound( } if (responseB.timedOut || responseB.error) { - const dmg = applyModifiers(challenge.baseDamage * 1.5, challenge, arenaModifier, comboA) + const dmg = applyModifiers(challenge.baseDamage * TIMEOUT_DAMAGE_MULTIPLIER, challenge, arenaModifier, comboA) return { - botAScore: 10, botBScore: 0, + botAScore: TIMEOUT_WINNER_SCORE, botBScore: 0, botADamage: Math.round(dmg), botBDamage: 0, winnerId: botA.id, narration: responseB.timedOut @@ -133,31 +134,31 @@ export function scoreRound( const confB = Math.min(correctB, 1) if (aFaster) { - scoreA = 7 + (1 - speedRatio) * 2 + confA - scoreB = 5 + speedRatio * 1.5 + confB * 0.5 + scoreA = FACTUAL_FASTER_BASE + (1 - speedRatio) * SPEED_ADVANTAGE_MULTIPLIER + confA + scoreB = FACTUAL_SLOWER_BASE + speedRatio * SPEED_RATIO_MULTIPLIER + confB * CONFIDENCE_BONUS } else { - scoreA = 5 + speedRatio * 1.5 + confA * 0.5 - scoreB = 7 + (1 - speedRatio) * 2 + confB + scoreA = FACTUAL_SLOWER_BASE + speedRatio * SPEED_RATIO_MULTIPLIER + confA * CONFIDENCE_BONUS + scoreB = FACTUAL_FASTER_BASE + (1 - speedRatio) * SPEED_ADVANTAGE_MULTIPLIER + confB } } else if (correctA > 0 && correctB === 0) { - scoreA = 9 + correctA * 0.5 - scoreB = 1 + (responseB.answer ? 1 : 0) + scoreA = ONE_CORRECT_WINNER_BASE + correctA * CONFIDENCE_BONUS + scoreB = NO_ANSWER_SCORE + (responseB.answer ? 1 : 0) } else if (correctB > 0 && correctA === 0) { - scoreA = 1 + (responseA.answer ? 1 : 0) - scoreB = 9 + correctB * 0.5 + scoreA = NO_ANSWER_SCORE + (responseA.answer ? 1 : 0) + scoreB = ONE_CORRECT_WINNER_BASE + correctB * CONFIDENCE_BONUS } else { // Both wrong -- speed tiebreaker in low range const aFaster = responseA.timeMs <= responseB.timeMs - scoreA = aFaster ? 4 : 3 - scoreB = aFaster ? 3 : 4 + scoreA = aFaster ? BOTH_WRONG_FASTER : BOTH_WRONG_SLOWER + scoreB = aFaster ? BOTH_WRONG_SLOWER : BOTH_WRONG_FASTER } } else { // === CREATIVE SCORING === const qualA = estimateQuality(responseA) const qualB = estimateQuality(responseB) const total = qualA + qualB || 1 - scoreA = (qualA / total) * 10 - scoreB = (qualB / total) * 10 + scoreA = (qualA / total) * CREATIVE_TOTAL_SCORE + scoreB = (qualB / total) * CREATIVE_TOTAL_SCORE } // Determine winner @@ -166,14 +167,14 @@ 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 - const isCritical = margin > 4 + const isCritical = margin > CRITICAL_MARGIN_THRESHOLD - let winnerDamage = challenge.baseDamage + margin * 2 - if (isCritical) winnerDamage *= 1.5 + let winnerDamage = challenge.baseDamage + margin * MARGIN_TO_DAMAGE_SCALE + if (isCritical) winnerDamage *= CRITICAL_DAMAGE_MULTIPLIER const winnerCombo = winnerId === botA.id ? comboA : comboB winnerDamage = applyModifiers(winnerDamage, challenge, arenaModifier, winnerCombo) - const loserDamage = Math.max(0, challenge.baseDamage * 0.3 - margin) + const loserDamage = Math.max(0, challenge.baseDamage * LOSER_DAMAGE_BASE - margin) const narration = winnerId ? generateNarration(challenge, winnerName!, loserName!, margin, isCritical) @@ -186,8 +187,8 @@ export function scoreRound( ]) return { - botAScore: Math.round(scoreA * 10) / 10, - botBScore: Math.round(scoreB * 10) / 10, + botAScore: Math.round(scoreA * SCORE_ROUNDING_FACTOR) / SCORE_ROUNDING_FACTOR, + botBScore: Math.round(scoreB * SCORE_ROUNDING_FACTOR) / SCORE_ROUNDING_FACTOR, botADamage: winnerId === botA.id ? Math.round(winnerDamage) : Math.round(loserDamage), botBDamage: winnerId === botB.id ? Math.round(winnerDamage) : Math.round(loserDamage), winnerId, @@ -221,45 +222,45 @@ function applyModifiers( combo: number, ): number { let d = damage - // Arena modifier: 2x damage when challenge type matches + // Arena modifier: bonus damage when challenge type matches if (arenaModifier && ARENA_MODIFIER_TYPES[arenaModifier]?.includes(challenge.type)) { - d *= 2 + d *= ARENA_DAMAGE_MULTIPLIER } if (combo > 0) { - d *= 1 + Math.min(combo, 5) * 0.2 + d *= 1 + Math.min(combo, MAX_COMBO_STACKS) * COMBO_DAMAGE_PER_STACK } return d } function estimateQuality(response: BotResponse): number { - if (!response.answer) return 0.5 + if (!response.answer) return EMPTY_RESPONSE_QUALITY const text = response.answer.trim() const len = text.length - if (len < 10) return 1 + if (len < MIN_QUALITY_LENGTH) 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 + if (charRatio < SPAM_CHAR_RATIO) return EMPTY_RESPONSE_QUALITY // 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 + // Ideal length window 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 + if (len >= QUALITY_LENGTH_IDEAL_MIN && len <= QUALITY_LENGTH_IDEAL_MAX) lengthScore = QUALITY_SCORE_IDEAL + else if (len > QUALITY_LENGTH_IDEAL_MAX && len <= QUALITY_LENGTH_SECONDARY_MAX) lengthScore = QUALITY_SCORE_SECONDARY + else if (len > QUALITY_LENGTH_SECONDARY_MAX) lengthScore = QUALITY_SCORE_LONG + else lengthScore = QUALITY_SCORE_LONG // Diversity bonus (prevents repetitive text) - const diversityScore = Math.min(wordDiversity * 4, 3) + const diversityScore = Math.min(wordDiversity * WORD_DIVERSITY_SCALE, WORD_DIVERSITY_CAP) // Speed bonus (faster is slightly better) - const speedBonus = Math.max(0, 2 - response.timeMs / 8000) + const speedBonus = Math.max(0, SPEED_BONUS_BASE - response.timeMs / DEFAULT_CHALLENGE_TIMEOUT_MS) return lengthScore + diversityScore + speedBonus } @@ -274,7 +275,7 @@ function generateNarration( const critPrefix = isCritical ? 'CRITICAL HIT! ' : '' const isFactual = challenge.scoring === 'factual' - if (isFactual && margin > 5) { + if (isFactual && margin > LARGE_WIN_MARGIN) { const bigWins = [ `${critPrefix}${winner} NAILS IT! ${loser} didn't even come close. Embarrassing, honestly.`, `${critPrefix}${winner} knows their stuff! ${loser} needs to hit the books. Or just hit something.`, @@ -289,7 +290,7 @@ function generateNarration( return pick(bigWins) } - if (isFactual && margin <= 3) { + if (isFactual && margin <= CLOSE_MATCH_MARGIN) { const closeOnes = [ `${critPrefix}Both bots got it right, but ${winner} was FASTER! ${loser} needs more coffee.`, `${critPrefix}Correct on both sides! ${winner} edges it out by milliseconds. That's BRUTAL.`, @@ -439,17 +440,17 @@ function scoreRetroRound( } } if (responseA.timedOut || responseA.error) { - const dmg = applyModifiers(challenge.baseDamage * 1.5, challenge, arenaModifier, comboB) + const dmg = applyModifiers(challenge.baseDamage * TIMEOUT_DAMAGE_MULTIPLIER, challenge, arenaModifier, comboB) return { - botAScore: 0, botBScore: 10, botADamage: 0, botBDamage: dmg, winnerId: botB.id, + botAScore: 0, botBScore: TIMEOUT_WINNER_SCORE, botADamage: 0, botBDamage: dmg, winnerId: botB.id, narration: `${botA.name}'s controller disconnected! ${botB.name} lands free hits!`, isCritical: false, } } if (responseB.timedOut || responseB.error) { - const dmg = applyModifiers(challenge.baseDamage * 1.5, challenge, arenaModifier, comboA) + const dmg = applyModifiers(challenge.baseDamage * TIMEOUT_DAMAGE_MULTIPLIER, challenge, arenaModifier, comboA) return { - botAScore: 10, botBScore: 0, botADamage: dmg, botBDamage: 0, winnerId: botA.id, + botAScore: TIMEOUT_WINNER_SCORE, botBScore: 0, botADamage: dmg, botBDamage: 0, winnerId: botA.id, narration: `${botB.name}'s controller disconnected! ${botA.name} lands free hits!`, isCritical: false, } @@ -463,20 +464,20 @@ function scoreRetroRound( let scoreA = resultA.score let scoreB = resultB.score const maxTime = challenge.timeout_ms - if (scoreA > 0) scoreA *= 1 + Math.max(0, (maxTime - responseA.timeMs) / maxTime) * 0.2 - if (scoreB > 0) scoreB *= 1 + Math.max(0, (maxTime - responseB.timeMs) / maxTime) * 0.2 + if (scoreA > 0) scoreA *= 1 + Math.max(0, (maxTime - responseA.timeMs) / maxTime) * RETRO_SPEED_BONUS_CAP + if (scoreB > 0) scoreB *= 1 + Math.max(0, (maxTime - responseB.timeMs) / maxTime) * RETRO_SPEED_BONUS_CAP const margin = Math.abs(scoreA - scoreB) const winnerId = scoreA > scoreB ? botA.id : scoreB > scoreA ? botB.id : null 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 - const isCritical = margin > 4 + const isCritical = margin > CRITICAL_MARGIN_THRESHOLD - let winnerDamage = challenge.baseDamage + margin * 2 - if (isCritical) winnerDamage *= 1.5 + let winnerDamage = challenge.baseDamage + margin * MARGIN_TO_DAMAGE_SCALE + if (isCritical) winnerDamage *= CRITICAL_DAMAGE_MULTIPLIER const winnerCombo = winnerId === botA.id ? comboA : comboB winnerDamage = applyModifiers(winnerDamage, challenge, arenaModifier, winnerCombo) - const loserDamage = Math.max(0, challenge.baseDamage * 0.3 - margin) + const loserDamage = Math.max(0, challenge.baseDamage * LOSER_DAMAGE_BASE - margin) const winnerResult = winnerId === botA.id ? resultA : resultB const loserResult = winnerId === botA.id ? resultB : resultA @@ -504,7 +505,7 @@ function scoreRetroRound( `HIDDEN MOVE FOUND! ${winnerName} unleashes ${discoveryNames.join(' + ')} for MASSIVE damage!`, ) } - if (loserWhiffs >= 2) { + if (loserWhiffs >= WHIFF_NARRATION_THRESHOLD) { narrations.push( `${loserName} mashes random buttons and WHIFFS ${loserWhiffs} times! ${winnerName} capitalizes with [${moveSummary(winnerResult)}]!`, ) @@ -522,8 +523,8 @@ function scoreRetroRound( } return { - botAScore: Math.round(scoreA * 10) / 10, - botBScore: Math.round(scoreB * 10) / 10, + botAScore: Math.round(scoreA * SCORE_ROUNDING_FACTOR) / SCORE_ROUNDING_FACTOR, + botBScore: Math.round(scoreB * SCORE_ROUNDING_FACTOR) / SCORE_ROUNDING_FACTOR, botADamage: winnerId === botA.id ? Math.round(winnerDamage) : Math.round(loserDamage), botBDamage: winnerId === botB.id ? Math.round(winnerDamage) : Math.round(loserDamage), winnerId, @@ -542,8 +543,8 @@ export function calculateElo( const expectedLoser = 1 - expectedWinner return { - newWinnerElo: Math.round((winnerElo + k * (1 - expectedWinner)) * 10) / 10, - newLoserElo: Math.round((loserElo + k * (0 - expectedLoser)) * 10) / 10, + newWinnerElo: Math.round((winnerElo + k * (1 - expectedWinner)) * SCORE_ROUNDING_FACTOR) / SCORE_ROUNDING_FACTOR, + newLoserElo: Math.round((loserElo + k * (0 - expectedLoser)) * SCORE_ROUNDING_FACTOR) / SCORE_ROUNDING_FACTOR, } } diff --git a/server/src/lib/constants.ts b/server/src/lib/constants.ts index b68f56b..fd162e6 100644 --- a/server/src/lib/constants.ts +++ b/server/src/lib/constants.ts @@ -74,3 +74,8 @@ export const RETRO_SPEED_BONUS_CAP = 0.2 // Retro mode speed bonus c export const LARGE_WIN_MARGIN = 5 // Margin for "big win" narration export const CLOSE_MATCH_MARGIN = 3 // Margin for "close match" narration export const WHIFF_NARRATION_THRESHOLD = 2 // Whiff count for narration trigger + +// --- Scoring: score ranges --- +export const TIMEOUT_WINNER_SCORE = 10 // Score awarded to winner when opponent times out +export const CREATIVE_TOTAL_SCORE = 10 // Total score pool for creative challenges +export const SCORE_ROUNDING_FACTOR = 10 // Multiply/divide for rounding to 1 decimal