"""Score functions. Scores are floats in [0.0, 1.0] snapped to a 0.05 grid (21 distinct levels). The discrete grid keeps display readable and ties common enough that small clock-skew differences don't decide a leaderboard, while still rewarding faster correct answers. """ from __future__ import annotations import math from collections.abc import Callable ScoreFn = Callable[[bool, int, int], float] SCORE_FNS: dict[str, ScoreFn] = {} GRID = 0.05 def _snap(value: float) -> float: """Snap to the 0.05 grid and clamp to [0.0, 1.0].""" snapped = round(value / GRID) * GRID snapped = max(0.0, min(1.0, snapped)) # Round to two decimals so the wire / display values are always # exactly e.g. 0.85, never 0.8500000000000001. return round(snapped, 2) def register(name: str) -> Callable[[ScoreFn], ScoreFn]: def decorator(func: ScoreFn) -> ScoreFn: SCORE_FNS[name] = func return func return decorator @register("linear_decay") def linear_decay(correct: bool, elapsed_ms: int, time_limit_ms: int) -> float: """Correct answers earn 1.0 instantly, decaying linearly to 0.5 at the deadline. Wrong (or missed) answers earn 0.0.""" if not correct: return 0.0 elapsed_ms = max(0, min(elapsed_ms, time_limit_ms)) raw = 1.0 - 0.5 * (elapsed_ms / time_limit_ms) return _snap(raw) @register("flat") def flat(correct: bool, elapsed_ms: int, time_limit_ms: int) -> float: """All correct answers earn 1.0 regardless of speed.""" return 1.0 if correct else 0.0 @register("exponential_decay") def exponential_decay(correct: bool, elapsed_ms: int, time_limit_ms: int) -> float: """Correct answers earn 1.0 instantly, decaying exponentially to ~0.57 at the deadline (e^{-2}/2 + 0.5).""" if not correct: return 0.0 elapsed_ms = max(0, min(elapsed_ms, time_limit_ms)) decay = math.exp(-2 * elapsed_ms / time_limit_ms) raw = 0.5 + 0.5 * decay return _snap(raw)