THE RUSH: two tickets on the rail, and the pass is a clock

Endless days past the card now deal a rush about one day in four
(deterministic by day number, never on a Critic day — one cruelty at a
time). Ticket one serves into silence: 'on the pass. The second is
already up — NEXT. Cook.' Nothing is written. The same day re-deals from
a twisted seed — a different order, still reload-proof — and while you
cook it, ticket one is dying: -0.5 per minute on the pass, capped at
1.5. The pair closes as ONE ledger line, the mean of what you served
and what survived the wait.

The write block behind every verdict (total, best, save) lived in nine
identical copies at nine call sites; it now lives once, inside
recordVerdict, which is what made deferring ticket one a three-line
change instead of a surgery.

Verified live on day 25: rush banner, ticket one 7.6 parked unwritten,
TICKET TWO banner on the re-deal (a prep day — different order
confirmed), 155s on the pass decayed 7.6 to 6.3, pair scored 6.7,
written once. Rush state clears on setDay, startDaily, and day advance.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
type-two 2026-07-21 01:51:01 +10:00
parent ef66e1aa7a
commit 3ecb6b59a3

View File

@ -98,6 +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;
private demoSnapshot: string | null = null;
private rng = new Rng(20260716);
private order!: Order;
@ -204,6 +206,12 @@ export class Game {
this.beginDay();
return;
}
if (this.rushFirst) {
// 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();
return;
}
this.save.day++;
this.save.maxDay = Math.max(this.save.maxDay, this.save.day);
writeSave(this.save);
@ -452,10 +460,6 @@ export class Game {
const v = judgeBruschetta(slice, this.order, this.kitchen.toolId, seconds, result);
this.lastBruschetta = { result, verdict: v };
this.recordVerdict(v);
this.save.total += v.total;
const key = `day${this.order.day}`;
this.save.best[key] = Math.max(this.save.best[key] ?? 0, v.total);
writeSave(this.save);
this.hideTempToggle();
this.hideAllScenes();
@ -514,6 +518,7 @@ export class Game {
/** Test hook: jump straight to a day, through the real order table. */
setDay(day: number): void {
this.rushFirst = null;
this.save.day = day;
this.save.maxDay = Math.max(this.save.maxDay ?? day, day);
writeSave(this.save);
@ -586,6 +591,7 @@ export class Game {
}
startDaily(): void {
this.rushFirst = null;
const date = new Date().toISOString().slice(0, 10);
this.dailyDate = date;
let h = 2166136261;
@ -642,10 +648,13 @@ export class Game {
// re-deals the identical order (no reroll-scumming), and the harness can
// reproduce any day by number.
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 o =
day <= DAYS.length
? DAYS[day - 1]
: proceduralOrder(day, () => dayRng.next());
: proceduralOrder(day, () => rng2.next());
if (!o.text) o.text = `${o.browningName}, ${o.amount} ${o.spread === 'mitey' ? 'MITEY' : o.spread}. ${o.noChar ? 'No burnt bits.' : ''}`;
this.order = o;
this.orderSeconds = 0;
@ -734,10 +743,6 @@ export class Game {
const slice = this.kitchen.currentSlice;
const v = judgeBenedict(slice, this.order, poach, seconds);
this.recordVerdict(v);
this.save.total += v.total;
const key = `day${this.order.day}`;
this.save.best[key] = Math.max(this.save.best[key] ?? 0, v.total);
writeSave(this.save);
this.hideAllScenes();
this.judgeView.root.visible = true;
this.judgeView.show(slice, v, this.order, this.kitchen.toolId, () => this.rng.next());
@ -765,10 +770,6 @@ export class Game {
const slice = this.kitchen.currentSlice;
const v = judgePoach(result, this.order, seconds);
this.recordVerdict(v);
this.save.total += v.total;
const key = `day${this.order.day}`;
this.save.best[key] = Math.max(this.save.best[key] ?? 0, v.total);
writeSave(this.save);
this.hideAllScenes();
this.judgeView.root.visible = true;
this.judgeView.show(slice, v, this.order, this.kitchen.toolId, () => this.rng.next());
@ -797,10 +798,6 @@ export class Game {
const slice = this.kitchen.currentSlice;
const v = judgeEggs(result, this.order, seconds);
this.recordVerdict(v);
this.save.total += v.total;
const key = `day${this.order.day}`;
this.save.best[key] = Math.max(this.save.best[key] ?? 0, v.total);
writeSave(this.save);
this.hideAllScenes();
this.judgeView.root.visible = true;
this.judgeView.show(slice, v, this.order, this.kitchen.toolId, () => this.rng.next());
@ -840,10 +837,6 @@ export class Game {
const slice = this.kitchen.currentSlice;
const v = judgeSteak(result, this.order, seconds);
this.recordVerdict(v);
this.save.total += v.total;
const key = `day${this.order.day}`;
this.save.best[key] = Math.max(this.save.best[key] ?? 0, v.total);
writeSave(this.save);
this.hideAllScenes();
this.judgeView.root.visible = true;
this.judgeView.show(slice, v, this.order, this.kitchen.toolId, () => this.rng.next());
@ -874,10 +867,6 @@ export class Game {
this.save.fridge = serializeStore(result.store);
const v = judgeFridge(result, this.order, seconds);
this.recordVerdict(v);
this.save.total += v.total;
const key = `day${this.order.day}`;
this.save.best[key] = Math.max(this.save.best[key] ?? 0, v.total);
writeSave(this.save);
this.hideAllScenes();
this.judgeView.root.visible = true;
this.judgeView.show(slice, v, this.order, this.kitchen.toolId, () => this.rng.next());
@ -926,10 +915,6 @@ export class Game {
const slice = this.kitchen.currentSlice;
const v = judgePan(result, this.order, seconds, this.panIngredient ?? undefined);
this.recordVerdict(v);
this.save.total += v.total;
const key = `day${this.order.day}`;
this.save.best[key] = Math.max(this.save.best[key] ?? 0, v.total);
writeSave(this.save);
this.hideAllScenes();
this.judgeView.root.visible = true;
this.judgeView.show(slice, v, this.order, this.kitchen.toolId, () => this.rng.next());
@ -957,10 +942,6 @@ export class Game {
const slice = this.kitchen.currentSlice;
const v = judgeGrill(result, this.order, seconds);
this.recordVerdict(v);
this.save.total += v.total;
const key = `day${this.order.day}`;
this.save.best[key] = Math.max(this.save.best[key] ?? 0, v.total);
writeSave(this.save);
this.hideAllScenes();
this.judgeView.root.visible = true;
@ -1065,8 +1046,38 @@ 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;
const key = `day${this.order.day}`;
this.save.best[key] = Math.max(this.save.best[key] ?? 0, v.total);
writeSave(this.save);
}
/** Endless-only, deterministic by day number: ~1 in 4 days past the card
* are a rush, never on a Critic day (one cruelty at a time). */
private rushDay(day: number): boolean {
return day > DAYS.length && day % 10 !== 0 && ((day * 2654435761) >>> 0) % 4 === 1;
}
private renderTicket(): void {
@ -1075,6 +1086,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.');
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');
@ -1150,10 +1162,6 @@ export class Game {
const extra = this.juiceResult ? juiceCriteria(this.juiceResult) : [];
const v = judge(slice, this.order, this.kitchen.toolId, seconds, this.prepResult ?? undefined, extra);
this.recordVerdict(v);
this.save.total += v.total;
const key = `day${this.order.day}`;
this.save.best[key] = Math.max(this.save.best[key] ?? 0, v.total);
writeSave(this.save);
this.kitchen.root.visible = false;
this.judgeView.root.visible = true;