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 <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-09 07:26:50 +00:00
co-authored by Claude Opus 4.6
parent 1d6826c6e2
commit 276cbd6e31
2 changed files with 58 additions and 52 deletions
+53 -52
View File
@@ -12,6 +12,7 @@ import {
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,
DEFAULT_CHALLENGE_TIMEOUT_MS, DEFAULT_CHALLENGE_TIMEOUT_MS,
TIMEOUT_WINNER_SCORE, CREATIVE_TOTAL_SCORE, SCORE_ROUNDING_FACTOR,
} from '../lib/constants.js' } from '../lib/constants.js'
import type { Challenge } from './challenges.js' import type { Challenge } from './challenges.js'
import { pick } from '../lib/utils.js' import { pick } from '../lib/utils.js'
@@ -69,9 +70,9 @@ export function scoreRound(
} }
if (responseA.timedOut || responseA.error) { 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 { return {
botAScore: 0, botBScore: 10, botAScore: 0, botBScore: TIMEOUT_WINNER_SCORE,
botADamage: 0, botBDamage: Math.round(dmg), botADamage: 0, botBDamage: Math.round(dmg),
winnerId: botB.id, winnerId: botB.id,
narration: responseA.timedOut narration: responseA.timedOut
@@ -92,9 +93,9 @@ export function scoreRound(
} }
if (responseB.timedOut || responseB.error) { 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 { return {
botAScore: 10, botBScore: 0, botAScore: TIMEOUT_WINNER_SCORE, botBScore: 0,
botADamage: Math.round(dmg), botBDamage: 0, botADamage: Math.round(dmg), botBDamage: 0,
winnerId: botA.id, winnerId: botA.id,
narration: responseB.timedOut narration: responseB.timedOut
@@ -133,31 +134,31 @@ export function scoreRound(
const confB = Math.min(correctB, 1) const confB = Math.min(correctB, 1)
if (aFaster) { if (aFaster) {
scoreA = 7 + (1 - speedRatio) * 2 + confA scoreA = FACTUAL_FASTER_BASE + (1 - speedRatio) * SPEED_ADVANTAGE_MULTIPLIER + confA
scoreB = 5 + speedRatio * 1.5 + confB * 0.5 scoreB = FACTUAL_SLOWER_BASE + speedRatio * SPEED_RATIO_MULTIPLIER + confB * CONFIDENCE_BONUS
} else { } else {
scoreA = 5 + speedRatio * 1.5 + confA * 0.5 scoreA = FACTUAL_SLOWER_BASE + speedRatio * SPEED_RATIO_MULTIPLIER + confA * CONFIDENCE_BONUS
scoreB = 7 + (1 - speedRatio) * 2 + confB scoreB = FACTUAL_FASTER_BASE + (1 - speedRatio) * SPEED_ADVANTAGE_MULTIPLIER + confB
} }
} else if (correctA > 0 && correctB === 0) { } else if (correctA > 0 && correctB === 0) {
scoreA = 9 + correctA * 0.5 scoreA = ONE_CORRECT_WINNER_BASE + correctA * CONFIDENCE_BONUS
scoreB = 1 + (responseB.answer ? 1 : 0) scoreB = NO_ANSWER_SCORE + (responseB.answer ? 1 : 0)
} else if (correctB > 0 && correctA === 0) { } else if (correctB > 0 && correctA === 0) {
scoreA = 1 + (responseA.answer ? 1 : 0) scoreA = NO_ANSWER_SCORE + (responseA.answer ? 1 : 0)
scoreB = 9 + correctB * 0.5 scoreB = ONE_CORRECT_WINNER_BASE + correctB * CONFIDENCE_BONUS
} else { } else {
// Both wrong -- speed tiebreaker in low range // Both wrong -- speed tiebreaker in low range
const aFaster = responseA.timeMs <= responseB.timeMs const aFaster = responseA.timeMs <= responseB.timeMs
scoreA = aFaster ? 4 : 3 scoreA = aFaster ? BOTH_WRONG_FASTER : BOTH_WRONG_SLOWER
scoreB = aFaster ? 3 : 4 scoreB = aFaster ? BOTH_WRONG_SLOWER : BOTH_WRONG_FASTER
} }
} else { } else {
// === CREATIVE SCORING === // === CREATIVE SCORING ===
const qualA = estimateQuality(responseA) const qualA = estimateQuality(responseA)
const qualB = estimateQuality(responseB) const qualB = estimateQuality(responseB)
const total = qualA + qualB || 1 const total = qualA + qualB || 1
scoreA = (qualA / total) * 10 scoreA = (qualA / total) * CREATIVE_TOTAL_SCORE
scoreB = (qualB / total) * 10 scoreB = (qualB / total) * CREATIVE_TOTAL_SCORE
} }
// Determine winner // Determine winner
@@ -166,14 +167,14 @@ export function scoreRound(
const winnerName = winnerId === botA.id ? botA.name : winnerId === botB.id ? botB.name : 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 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 let winnerDamage = challenge.baseDamage + margin * MARGIN_TO_DAMAGE_SCALE
if (isCritical) winnerDamage *= 1.5 if (isCritical) winnerDamage *= CRITICAL_DAMAGE_MULTIPLIER
const winnerCombo = winnerId === botA.id ? comboA : comboB const winnerCombo = winnerId === botA.id ? comboA : comboB
winnerDamage = applyModifiers(winnerDamage, challenge, arenaModifier, winnerCombo) 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 const narration = winnerId
? generateNarration(challenge, winnerName!, loserName!, margin, isCritical) ? generateNarration(challenge, winnerName!, loserName!, margin, isCritical)
@@ -186,8 +187,8 @@ export function scoreRound(
]) ])
return { return {
botAScore: Math.round(scoreA * 10) / 10, botAScore: Math.round(scoreA * SCORE_ROUNDING_FACTOR) / SCORE_ROUNDING_FACTOR,
botBScore: Math.round(scoreB * 10) / 10, botBScore: Math.round(scoreB * SCORE_ROUNDING_FACTOR) / SCORE_ROUNDING_FACTOR,
botADamage: winnerId === botA.id ? Math.round(winnerDamage) : Math.round(loserDamage), botADamage: winnerId === botA.id ? Math.round(winnerDamage) : Math.round(loserDamage),
botBDamage: winnerId === botB.id ? Math.round(winnerDamage) : Math.round(loserDamage), botBDamage: winnerId === botB.id ? Math.round(winnerDamage) : Math.round(loserDamage),
winnerId, winnerId,
@@ -221,45 +222,45 @@ function applyModifiers(
combo: number, combo: number,
): number { ): number {
let d = damage 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)) { if (arenaModifier && ARENA_MODIFIER_TYPES[arenaModifier]?.includes(challenge.type)) {
d *= 2 d *= ARENA_DAMAGE_MULTIPLIER
} }
if (combo > 0) { 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 return d
} }
function estimateQuality(response: BotResponse): number { function estimateQuality(response: BotResponse): number {
if (!response.answer) return 0.5 if (!response.answer) return EMPTY_RESPONSE_QUALITY
const text = response.answer.trim() const text = response.answer.trim()
const len = text.length const len = text.length
if (len < 10) return 1 if (len < MIN_QUALITY_LENGTH) return 1
// Detect low-effort spam (repeated chars) // Detect low-effort spam (repeated chars)
const uniqueChars = new Set(text.toLowerCase()).size const uniqueChars = new Set(text.toLowerCase()).size
const charRatio = uniqueChars / Math.min(len, 100) 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) // Word diversity (unique words / total words)
const words = text.split(/\s+/) const words = text.split(/\s+/)
const uniqueWords = new Set(words.map(w => w.toLowerCase())) const uniqueWords = new Set(words.map(w => w.toLowerCase()))
const wordDiversity = uniqueWords.size / Math.max(words.length, 1) const wordDiversity = uniqueWords.size / Math.max(words.length, 1)
// Ideal length window: 30-400 chars // Ideal length window
let lengthScore: number let lengthScore: number
if (len >= 30 && len <= 400) lengthScore = 4 if (len >= QUALITY_LENGTH_IDEAL_MIN && len <= QUALITY_LENGTH_IDEAL_MAX) lengthScore = QUALITY_SCORE_IDEAL
else if (len > 400 && len <= 600) lengthScore = 3 else if (len > QUALITY_LENGTH_IDEAL_MAX && len <= QUALITY_LENGTH_SECONDARY_MAX) lengthScore = QUALITY_SCORE_SECONDARY
else if (len > 600) lengthScore = 2 else if (len > QUALITY_LENGTH_SECONDARY_MAX) lengthScore = QUALITY_SCORE_LONG
else lengthScore = 2 else lengthScore = QUALITY_SCORE_LONG
// Diversity bonus (prevents repetitive text) // 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) // 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 return lengthScore + diversityScore + speedBonus
} }
@@ -274,7 +275,7 @@ function generateNarration(
const critPrefix = isCritical ? 'CRITICAL HIT! ' : '' const critPrefix = isCritical ? 'CRITICAL HIT! ' : ''
const isFactual = challenge.scoring === 'factual' const isFactual = challenge.scoring === 'factual'
if (isFactual && margin > 5) { 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.`,
`${critPrefix}${winner} knows their stuff! ${loser} needs to hit the books. Or just hit something.`, `${critPrefix}${winner} knows their stuff! ${loser} needs to hit the books. Or just hit something.`,
@@ -289,7 +290,7 @@ function generateNarration(
return pick(bigWins) return pick(bigWins)
} }
if (isFactual && margin <= 3) { if (isFactual && margin <= CLOSE_MATCH_MARGIN) {
const closeOnes = [ const closeOnes = [
`${critPrefix}Both bots got it right, but ${winner} was FASTER! ${loser} needs more coffee.`, `${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.`, `${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) { 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 { 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!`, narration: `${botA.name}'s controller disconnected! ${botB.name} lands free hits!`,
isCritical: false, isCritical: false,
} }
} }
if (responseB.timedOut || responseB.error) { 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 { 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!`, narration: `${botB.name}'s controller disconnected! ${botA.name} lands free hits!`,
isCritical: false, isCritical: false,
} }
@@ -463,20 +464,20 @@ function scoreRetroRound(
let scoreA = resultA.score let scoreA = resultA.score
let scoreB = resultB.score let scoreB = resultB.score
const maxTime = challenge.timeout_ms const maxTime = challenge.timeout_ms
if (scoreA > 0) scoreA *= 1 + Math.max(0, (maxTime - responseA.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) * 0.2 if (scoreB > 0) scoreB *= 1 + Math.max(0, (maxTime - responseB.timeMs) / maxTime) * RETRO_SPEED_BONUS_CAP
const margin = Math.abs(scoreA - scoreB) const margin = Math.abs(scoreA - scoreB)
const winnerId = scoreA > scoreB ? botA.id : scoreB > scoreA ? botB.id : null 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 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 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 let winnerDamage = challenge.baseDamage + margin * MARGIN_TO_DAMAGE_SCALE
if (isCritical) winnerDamage *= 1.5 if (isCritical) winnerDamage *= CRITICAL_DAMAGE_MULTIPLIER
const winnerCombo = winnerId === botA.id ? comboA : comboB const winnerCombo = winnerId === botA.id ? comboA : comboB
winnerDamage = applyModifiers(winnerDamage, challenge, arenaModifier, winnerCombo) 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 winnerResult = winnerId === botA.id ? resultA : resultB
const loserResult = winnerId === botA.id ? resultB : resultA const loserResult = winnerId === botA.id ? resultB : resultA
@@ -504,7 +505,7 @@ function scoreRetroRound(
`HIDDEN MOVE FOUND! ${winnerName} unleashes ${discoveryNames.join(' + ')} for MASSIVE damage!`, `HIDDEN MOVE FOUND! ${winnerName} unleashes ${discoveryNames.join(' + ')} for MASSIVE damage!`,
) )
} }
if (loserWhiffs >= 2) { if (loserWhiffs >= WHIFF_NARRATION_THRESHOLD) {
narrations.push( narrations.push(
`${loserName} mashes random buttons and WHIFFS ${loserWhiffs} times! ${winnerName} capitalizes with [${moveSummary(winnerResult)}]!`, `${loserName} mashes random buttons and WHIFFS ${loserWhiffs} times! ${winnerName} capitalizes with [${moveSummary(winnerResult)}]!`,
) )
@@ -522,8 +523,8 @@ function scoreRetroRound(
} }
return { return {
botAScore: Math.round(scoreA * 10) / 10, botAScore: Math.round(scoreA * SCORE_ROUNDING_FACTOR) / SCORE_ROUNDING_FACTOR,
botBScore: Math.round(scoreB * 10) / 10, botBScore: Math.round(scoreB * SCORE_ROUNDING_FACTOR) / SCORE_ROUNDING_FACTOR,
botADamage: winnerId === botA.id ? Math.round(winnerDamage) : Math.round(loserDamage), botADamage: winnerId === botA.id ? Math.round(winnerDamage) : Math.round(loserDamage),
botBDamage: winnerId === botB.id ? Math.round(winnerDamage) : Math.round(loserDamage), botBDamage: winnerId === botB.id ? Math.round(winnerDamage) : Math.round(loserDamage),
winnerId, winnerId,
@@ -542,8 +543,8 @@ export function calculateElo(
const expectedLoser = 1 - expectedWinner const expectedLoser = 1 - expectedWinner
return { return {
newWinnerElo: Math.round((winnerElo + k * (1 - expectedWinner)) * 10) / 10, newWinnerElo: Math.round((winnerElo + k * (1 - expectedWinner)) * SCORE_ROUNDING_FACTOR) / SCORE_ROUNDING_FACTOR,
newLoserElo: Math.round((loserElo + k * (0 - expectedLoser)) * 10) / 10, newLoserElo: Math.round((loserElo + k * (0 - expectedLoser)) * SCORE_ROUNDING_FACTOR) / SCORE_ROUNDING_FACTOR,
} }
} }
+5
View File
@@ -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 LARGE_WIN_MARGIN = 5 // Margin for "big win" narration
export const CLOSE_MATCH_MARGIN = 3 // Margin for "close match" narration export const CLOSE_MATCH_MARGIN = 3 // Margin for "close match" narration
export const WHIFF_NARRATION_THRESHOLD = 2 // Whiff count for narration trigger 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