feat(scoring): rescale scores to 0.0-1.0 with 0.05 resolution

Per-question score is now a float in [0.0, 1.0] snapped to a 21-level
0.05 grid, replacing the previous 0-1000 integer scale. Easier to read
on a leaderboard, ties become acceptable rather than vanishingly rare,
and small clock-skew differences no longer split rankings.

DB schema: score is REAL now (SQLite type affinity is loose enough that
existing rows still read fine, but new inserts go in as floats).

Frontend: added fmtScore() helpers in admin.js and quiz.js to render
two decimal places consistently (0.85, 1.20, 5.00) so float-arithmetic
sums never display as 0.8500000000000001.

Tests: linear_decay/flat/exponential_decay assertions updated; added
a snap-to-grid invariant test.
This commit is contained in:
ameer
2026-05-03 00:09:35 +08:00
parent 8e8d5cfff0
commit 7a483ad3ee
6 changed files with 87 additions and 35 deletions

View File

@@ -3,24 +3,35 @@ from app.scoring import SCORE_FNS
def test_linear_decay_values():
fn = SCORE_FNS["linear_decay"]
assert fn(True, 0, 60_000) == 1000
assert fn(True, 30_000, 60_000) == 750
assert fn(True, 60_000, 60_000) == 500
assert fn(True, 90_000, 60_000) == 500
assert fn(False, 0, 60_000) == 0
assert fn(True, 0, 60_000) == 1.0
assert fn(True, 30_000, 60_000) == 0.75
assert fn(True, 60_000, 60_000) == 0.5
# Past the deadline the score floors at 0.5 (still correct, fully decayed).
assert fn(True, 90_000, 60_000) == 0.5
assert fn(False, 0, 60_000) == 0.0
def test_linear_decay_snaps_to_grid():
"""Every score is on the 0.05 grid (21 distinct values)."""
fn = SCORE_FNS["linear_decay"]
for elapsed in range(0, 60_001, 137): # arbitrary irrational-ish step
s = fn(True, elapsed, 60_000)
# multiplied by 20, must be an integer
assert abs(s * 20 - round(s * 20)) < 1e-9, (elapsed, s)
def test_flat_values():
fn = SCORE_FNS["flat"]
assert fn(True, 0, 60_000) == 1000
assert fn(True, 60_000, 60_000) == 1000
assert fn(True, 90_000, 60_000) == 1000
assert fn(False, 0, 60_000) == 0
assert fn(True, 0, 60_000) == 1.0
assert fn(True, 60_000, 60_000) == 1.0
assert fn(True, 90_000, 60_000) == 1.0
assert fn(False, 0, 60_000) == 0.0
def test_exponential_decay_values():
fn = SCORE_FNS["exponential_decay"]
assert fn(True, 0, 60_000) == 1000
assert 560 < fn(True, 60_000, 60_000) < 570
assert fn(True, 0, 60_000) == 1.0
# At deadline: 0.5 + 0.5 * e^-2 ≈ 0.5677 → snaps to 0.55
assert fn(True, 60_000, 60_000) == 0.55
assert fn(True, 90_000, 60_000) == fn(True, 60_000, 60_000)
assert fn(False, 0, 60_000) == 0
assert fn(False, 0, 60_000) == 0.0