THE RUSH, made honest: only the pair reaches the book

Adversarial pass over the freshly-threaded recordVerdict pipeline found
three real ordering bugs on rush days, all of the same species: ticket
one's RAW verdict was reaching systems that should only ever see the
pair. The streak moved twice per rush (and an S on ticket one extended
it even when the pair collapsed), the regulars' book logged two visits,
and — the exploit — the Shadow star was awarded off ticket one's raw
grade: ace the first ticket, flub the second, keep the S★ on a day the
ledger records as B.

The rush stash/combine now sits directly after the per-dish adjustments
(Critic and Mother judge each DISH — that part was right) and before
streak, book, star and daily, so ticket one returns early having touched
nothing, and the pair flows through everything else exactly once.

Also fixed the falsy-zero trap the stash simplification introduced: a
0.0 ticket one made `this.rushFirst ?` read as "no rush", re-dealing
the SAME order for ticket two — all three checks are `!== null` now.

Verified live on day 25: after ticket one, streak/visits/ledger byte-
identical to before it; after the pair, one visit, 41s decay 7.6→7.3,
one write.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
type-two 2026-07-21 02:03:37 +10:00
parent 3ecb6b59a3
commit 9ca713b5c9

View File

@ -98,8 +98,8 @@ export class Game {
private demoMode = false;
/** The Inspector's Shadow: the kitchen's words go dark; you cook by eye and ear. */
private shadowMode = false;
/** Rush hour: ticket one's verdict, parked on the pass while you cook two. */
private rushFirst: { total: number; seconds: number } | null = null;
/** Rush hour: ticket one's total, parked on the pass while you cook two. */
private rushFirst: number | null = null;
private demoSnapshot: string | null = null;
private rng = new Rng(20260716);
private order!: Order;
@ -206,7 +206,7 @@ export class Game {
this.beginDay();
return;
}
if (this.rushFirst) {
if (this.rushFirst !== null) {
// Ticket two of the rush: same day, different deal, the clock restarts
// but the FIRST ticket's decay is measured by ticket two's service.
this.beginDay();
@ -650,7 +650,7 @@ export class Game {
const dayRng = new Rng((day * 2654435761) >>> 0);
// Ticket two of a rush re-deals the same day from a twisted seed — a
// different order, deterministically, so reloads still can't scum it.
const rng2 = this.rushFirst ? new Rng(((day * 2654435761) ^ 0x9e3779b9) >>> 0) : dayRng;
const rng2 = this.rushFirst !== null ? new Rng(((day * 2654435761) ^ 0x9e3779b9) >>> 0) : dayRng;
const o =
day <= DAYS.length
? DAYS[day - 1]
@ -1014,6 +1014,28 @@ export class Game {
extras.push('The starter is lively — the crumb held. (+0.2. She did that, not you.)');
} else if (gap >= 3 && toastDay) extras.push('The jar says nothing today. It is sulking, and the crumb knows.');
}
// RUSH HOUR (endless only): the day dealt TWO tickets. The first verdict
// goes on the pass — no streak, no book, no star, nothing written — and
// the second closes the pair. The first decays for every second it sat
// while you cooked. Only the PAIR reaches the streak, the regulars'
// book, the Shadow star and the ledger: one service, one line.
if (this.rushDay(this.order.day) && !this.dailyDate) {
if (this.rushFirst === null) {
this.rushFirst = v.total;
extras.push('Ticket one, on the pass. The second is already up — NEXT. Cook.');
this.judgeView.extraLines = extras;
this.judgeView.shareText = '';
return; // nothing is written until the pair closes
}
const passSec = this.orderSeconds;
const decay = Math.min(1.5, (passSec / 60) * 0.5);
const aAdj = Math.max(0, Math.round((this.rushFirst - decay) * 10) / 10);
extras.push(`Ticket one sat ${Math.round(passSec)}s on the pass: ${this.rushFirst.toFixed(1)} fell to ${aAdj.toFixed(1)}.`);
v.total = Math.round(((aAdj + v.total) / 2) * 10) / 10;
v.grade = gradeOf(v.total);
extras.push('Two tickets, one verdict — the rush is scored as a pair.');
this.rushFirst = null;
}
// The streak. He notices. He hates that he notices.
const prev = this.save.streak ?? 0;
if (v.grade === 'S') {
@ -1046,26 +1068,6 @@ export class Game {
d[this.dailyDate] = Math.max(d[this.dailyDate] ?? 0, v.total);
extras.push('The Daily. Same deal for everyone today — come back tomorrow.');
}
// RUSH HOUR (endless only): the day dealt TWO tickets. The first verdict
// goes on the pass; the second closes the pair — and the first decays for
// every second it sat there while you cooked. One combined line in the book.
if (this.rushDay(this.order.day) && !this.dailyDate) {
if (!this.rushFirst) {
this.rushFirst = { total: v.total, seconds: this.orderSeconds };
extras.push('Ticket one, on the pass. The second is already up — NEXT. Cook.');
this.judgeView.extraLines = extras;
this.judgeView.shareText = '';
return; // nothing is written until the pair closes
}
const passSec = this.orderSeconds;
const decay = Math.min(1.5, (passSec / 60) * 0.5);
const aAdj = Math.max(0, Math.round((this.rushFirst.total - decay) * 10) / 10);
extras.push(`Ticket one sat ${Math.round(passSec)}s on the pass: ${this.rushFirst.total.toFixed(1)} fell to ${aAdj.toFixed(1)}.`);
v.total = Math.round(((aAdj + v.total) / 2) * 10) / 10;
v.grade = gradeOf(v.total);
extras.push('Two tickets, one verdict — the rush is scored as a pair.');
this.rushFirst = null;
}
this.judgeView.extraLines = extras;
this.judgeView.shareText = this.shareLine(v);
this.save.total += v.total;
@ -1086,7 +1088,7 @@ export class Game {
el('div', 'ticket-day', this.ticketEl, `DAY ${o.day}`);
el('div', 'ticket-who', this.ticketEl, o.who);
if (o.day % 10 === 0 && !this.dailyDate) el('div', 'ticket-critic', this.ticketEl, 'THE CRITIC is in today. The flaw will be the headline.');
if (this.rushDay(o.day) && !this.dailyDate) el('div', 'ticket-critic', this.ticketEl, this.rushFirst ? 'TICKET TWO — the first is dying on the pass.' : 'RUSH HOUR — two tickets on the rail. The first will wait for the second.');
if (this.rushDay(o.day) && !this.dailyDate) el('div', 'ticket-critic', this.ticketEl, this.rushFirst !== null ? 'TICKET TWO — the first is dying on the pass.' : 'RUSH HOUR — two tickets on the rail. The first will wait for the second.');
const demos = (window as unknown as { __t?: { demos?: Record<number, unknown> } }).__t?.demos;
if (!this.demoMode && demos && demos[o.day]) {
const watch = el('button', 'btn ghost watch-btn', this.ticketEl, '▶ watch him do it');