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:
co-authored by
Claude Opus 4.6
parent
1d6826c6e2
commit
276cbd6e31
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user