A match run is a different kind of run, and stops polluting the gore stats

The two modes share no currency: gore counts kills, multi-kills and weapons;
zombie tennis counts opponents felled and has exactly one kill per opponent,
always credited to 'bomb' (matchGameWon's explode). Every one of those was
landing in splatterRuns as a gore run, on the roster as THE CLUB PRO, and on
the KILLS world board — where a five-body match would post a row next to a
300-kill massacre and mean something else entirely by it.

One character on the record fixes all of it: `m`, absent meaning gore (which
is what every existing record is, so no migration). read() takes a mode and
so does endRun, which now compares you only against your own mode and refuses
a name prompt from a match.

The record's two headline slots carry each MODE's two headline numbers — gore
g=games s=sets, match g=felled s=games — so `g` stays the thing everything
ranks on and the match best is derived from the record instead of living in a
second key that drifts. That retires the matchBest key from the last commit.

Found on the way, pre-existing and visible: endRun returned `{...best, kills,
games}` and the later key won, so G.best.games was the run that had just died.
The gore death screen has been printing "BEST <this run> GAMES · <the record>
SETS". The return now carries the best and nothing else.

And stats.check() — the module's own fixture check — was exported and called
by nothing. Wired into the suite, with the mode split added to it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Opus 5 committed 2026-08-12 08:58:04 +02:00
1 parent 8011186cda
commit 66fdad3d74
6 files changed
+84 -34

No files matched your search

+15 -6
View File
@@ -91,6 +91,7 @@ const { zombies, spawnZombie, pellets } = await import('./src/zombies.js');
const { killPlayer } = await import('./src/zombies.js');
const { drillQueue, drillLegal, arrival, drillMult, DRILL_TYPES } = await import('./src/drill.js');
const { TUNE, bossArt, COURT } = await import('./src/config.js');
const { read: readStats, check: statsCheck } = await import('./src/stats.js');
const DT = 1 / 60;
function freshSplatter() {
@@ -750,26 +751,34 @@ test('regicide: killing the body mid-game wins the game outright', () => {
assert.ok(oppOf(), 'and the next opponent is on');
});
test('the match measures OPPONENTS FELLED, with its own best', () => {
localStorage.removeItem('matchBest');
test('the match measures OPPONENTS FELLED, banked as its own kind of run', () => {
localStorage.removeItem('splatterRuns');
startMatch();
for (let k = 0; k < 8; k++) resolveInterlude('P'); // two games = two bodies down
assert.equal(G.match.opp, 3, 'on the third rung');
G.hp = 1; resolveInterlude('O');
assert.equal(G.mode, 'eaten', 'bled out');
assert.equal(G.match.felled, 2, 'the summary counts bodies, not games');
assert.ok(G.match.newBest, 'first run of the day is a record');
assert.equal(localStorage.getItem('matchBest'), '2', '...and it is written down');
assert.ok(G.newBest, 'first run of the day is a record');
// a shorter second run must NOT overwrite the mark, and must know what it is chasing
G.mode = 'splatter'; endMatch(); startMatch();
for (let k = 0; k < 4; k++) resolveInterlude('P');
G.hp = 1; resolveInterlude('O');
assert.equal(G.match.felled, 1);
assert.equal(G.match.best, 2, 'the mark to beat is shown');
assert.ok(!G.match.newBest, 'and one body does not beat two');
assert.equal(localStorage.getItem('matchBest'), '2', 'the record survives a worse run');
assert.ok(!G.newBest, 'and one body does not beat two');
// ...and none of it lands in the gore mode's stats, which is where it all used to go: a felled
// opponent is a kill credited to 'bomb', and three of them are not a massacre.
const runs = JSON.parse(localStorage.getItem('splatterRuns'));
assert.ok(runs.every(r => r.m === 'z'), 'both runs are tagged as match runs');
assert.equal(readStats('all').kills, 0, 'the gore screen shows nothing from a match');
assert.equal(readStats('all', undefined, 'z').longest, 2, '...and the match view knows the best');
G.mode = 'splatter'; endMatch();
});
// stats.js ships its own fixture-based check — the period filter, the mode split and the name
// normalisation that has to agree with server.py. It was exported and never called by anything.
test("stats.js's own check", () => { assert.equal(statsCheck(), 'ok'); });
console.log(`\n${passed} tests passed`);
process.exit(0); // render.js schedules viewport timers at load; don't let them outlive the report