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:
1 parent
8011186cda
commit
66fdad3d74
6 files changed
+84
-34
No files matched your search
+50
-11
@@ -76,8 +76,24 @@ export function multiKill() { run.mk++; }
|
||||
// death from a tab closed mid-match — both are worth recording, only one is a death.
|
||||
// Idempotent: a run with nothing in it writes nothing, so death-then-close cannot double-count and
|
||||
// closing the tab at the menu does not invent an empty match.
|
||||
export function endRun(games, sets, secs, dead = true) {
|
||||
const prev = load();
|
||||
//
|
||||
// `g` AND `s` ARE THE MODE'S OWN TWO HEADLINE NUMBERS, not literally games and sets:
|
||||
// gore — g = games survived, s = sets banked (what it has always been)
|
||||
// match — g = OPPONENTS FELLED, s = games played
|
||||
// `g` is the one everything ranks on (best, newBest, `longest`), so putting each mode's real
|
||||
// measure in that slot is what lets one record shape serve both without a second best written
|
||||
// somewhere else and left to drift — which is the mistake splatterBest already made once.
|
||||
// WHICH MODE WROTE THIS RUN. One character on the record, because the two modes do not share a
|
||||
// single currency: the gore mode counts kills, multi-kills and weapons; zombie tennis counts
|
||||
// opponents felled and has exactly one kill per opponent, by definition, always credited to 'bomb'
|
||||
// (matchGameWon's explode). Un-tagged records predate the split and ARE gore runs, which is why the
|
||||
// default is 'g' everywhere and no migration is needed.
|
||||
export const MODE_GORE = 'g', MODE_MATCH = 'z';
|
||||
const modeOf = r => r.m || MODE_GORE;
|
||||
|
||||
export function endRun(games, sets, secs, dead = true, mode = MODE_GORE) {
|
||||
const all = load();
|
||||
const prev = all.filter(r => modeOf(r) === mode); // you are only ever compared against your own mode
|
||||
const best = prev.reduce((b, r) => r.g > b.games ? { games: r.g, sets: r.s } : b, { games: 0, sets: 0 });
|
||||
// TWO bests, because they answer different questions. `newBest` is GAMES and belongs to the death
|
||||
// summary, which has always celebrated how long you lasted. `newKillBest` is KILLS, which is what
|
||||
@@ -86,21 +102,32 @@ export function endRun(games, sets, secs, dead = true) {
|
||||
const kills = run.k;
|
||||
if (banked || (!run.k && !games)) { run = bag(); return { ...best, newBest: false, newKillBest: false }; }
|
||||
const newBest = games > best.games;
|
||||
const newKillBest = kills > bestKills;
|
||||
prev.push({ t: Date.now(), g: games, s: sets, d: Math.round(secs || 0),
|
||||
k: kills, mk: run.mk, dead: dead ? 1 : 0, z: run.z, w: run.w });
|
||||
save(prev);
|
||||
// THE WORLD BOARD IS THE GORE MODE'S. It ranks kills in a run, and a match run has exactly one
|
||||
// kill per opponent felled — a five-body match would post a "5 kills" row next to a 300-kill
|
||||
// massacre and mean something completely different by it. No name prompt from a match.
|
||||
// ponytail: when zombie tennis earns a board of its own it ranks FELLED (with points won as the
|
||||
// tiebreak — felled alone is a small integer and would be a wall of ties), and that is a server
|
||||
// change, not this line.
|
||||
const newKillBest = mode === MODE_GORE && kills > bestKills;
|
||||
all.push({ t: Date.now(), g: games, s: sets, d: Math.round(secs || 0), m: mode,
|
||||
k: kills, mk: run.mk, dead: dead ? 1 : 0, z: run.z, w: run.w });
|
||||
save(all); // ...the WHOLE record, not the filtered view — that would delete the other mode
|
||||
run = bag(); banked = true;
|
||||
last = { kills, games, newKillBest };
|
||||
return { ...best, newBest, newKillBest, kills, games };
|
||||
// `games`/`sets` here are THE BEST RUN'S, which is the only thing a caller wants back — the
|
||||
// caller already knows what the run it just handed over did. They used to be trailed by the
|
||||
// ENDED run's own `kills, games`, and the later key won: `G.best.games` was the run that had just
|
||||
// died, so the death screen printed "BEST 3 GAMES · 7 SETS" — this run's games, the record's sets.
|
||||
return { ...best, newBest, newKillBest };
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------- aggregation ----
|
||||
// `runs` is injectable so check() can assert against a fixture without touching localStorage.
|
||||
export function read(period, runs = load()) {
|
||||
const rs = period === 'last' ? runs.slice(-1)
|
||||
: period === '7d' ? runs.filter(r => Date.now() - r.t < 7 * 864e5)
|
||||
: runs;
|
||||
export function read(period, runs = load(), mode = MODE_GORE) {
|
||||
const own = runs.filter(r => modeOf(r) === mode); // ...and only ever one mode's runs at a time
|
||||
const rs = period === 'last' ? own.slice(-1)
|
||||
: period === '7d' ? own.filter(r => Date.now() - r.t < 7 * 864e5)
|
||||
: own;
|
||||
const out = { kills: 0, multi: 0, deaths: 0, longest: 0, matches: rs.length, z: {}, w: {} };
|
||||
for (const r of rs) {
|
||||
out.kills += r.k || 0; out.multi += r.mk || 0; out.deaths += r.dead || 0;
|
||||
@@ -235,9 +262,21 @@ export function check() {
|
||||
{ t: now - 9 * day, g: 3, s: 0, d: 60, k: 10, mk: 1, dead: 1, z: { a: 8, g: 2 }, w: { ball: 10 } },
|
||||
{ t: now - 2 * day, g: 7, s: 1, d: 90, k: 5, mk: 0, dead: 1, z: { g: 5 }, w: { bomb: 5 } },
|
||||
{ t: now - 1000, g: 1, s: 0, d: 10, k: 1, mk: 2, dead: 0, z: { a: 1 }, w: { ball: 1 } },
|
||||
// a zombie-tennis run sitting in the same array: 4 opponents felled over 9 games. Its four
|
||||
// kills are 'r' bodies credited to the game-winning blast and must never reach a gore figure.
|
||||
{ t: now - 500, g: 4, s: 9, d: 40, m: 'z', k: 4, mk: 0, dead: 1, z: { r: 4 }, w: { bomb: 4 } },
|
||||
];
|
||||
const all = read('all', fx), wk = read('7d', fx), last = read('last', fx);
|
||||
const eq = (got, want, what) => { if (got !== want) throw new Error(`${what}: ${got} !== ${want}`); };
|
||||
// THE MODE SPLIT. Un-tagged records are gore runs (that is what they were before the tag), so the
|
||||
// three above still answer every gore question exactly as they did — and the match run answers none
|
||||
// of them. 'last' is the last run OF THAT MODE, not the last run overall.
|
||||
const mz = read('all', fx, 'z');
|
||||
eq(all.matches, 3, 'gore sees only gore runs');
|
||||
eq(all.z.r || 0, 0, 'a felled opponent is not a shambler on the gore roster');
|
||||
eq(mz.matches, 1, 'and the match view sees only its own');
|
||||
eq(mz.longest, 4, 'the match measure is OPPONENTS FELLED, in the record`s ranking slot');
|
||||
eq(read('last', fx, 'z').kills, 4, 'last-of-mode, not last-overall');
|
||||
eq(all.kills, 16, 'all kills');
|
||||
eq(all.deaths, 2, 'all deaths'); // the abandoned run is recorded but is not a death
|
||||
eq(all.longest, 7, 'all longest');
|
||||
|
||||
Reference in new issue
Block a user