The benedict (day 22) + the kitchen's memory: regulars and streaks
THE BENEDICT — the first toast+poach composition: day 22 toasts the
bread in the kitchen, then onPopped routes to the poach pot instead of
the drawer (the bruschetta's oven trick, generalized); judgeBenedict
reads THE BREAD off the slice (browning/evenness/char) and THE POACH off
the egg (white/wobble/form), and The Plate sogs BOTH if you serve the
egg undrained — 'You put a wet egg on good toast. Now it is neither.'
Also in the endless rotation (electric pot past day 28).
THE KITCHEN REMEMBERS — recordVerdict on every serve path:
- Streaks: consecutive S grades tracked in the save; the judge notices
at 3 ('I am beginning to suspect competence.'), 5 ('I have started a
file.'), every 5 after, and eulogizes a broken one ('It was 7. We do
not speak of it.').
- The regulars: per-customer visits/last/best in the save. Return
tickets read 'visit №3 — last time: S' (or '...F. they came BACK.'),
and the judge compares: worse-than-last-time and redemption both get
a line. Delivered via JudgeView.extraLines, one-shot per verdict.
Verified live: day 22 starts at the kitchen, poaches after the pop,
combined card 9.1/10 with the architecture line; streak 1->2->3 fires
the line at three; day-20 rebooking shows the visit note.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
bffee40c6f
commit
9c4e366b65
31
src/dev.ts
31
src/dev.ts
@ -637,6 +637,37 @@ export function installDevHarness(app: App, game: Game): void {
|
|||||||
return { floats: s.eggs.map((e) => e.floatWord), rottenIn: s.rottenIn, dumped: s.bowlsDumped, rows };
|
return { floats: s.eggs.map((e) => e.floatWord), rottenIn: s.rottenIn, dumped: s.bowlsDumped, rows };
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** Play the day-22 benedict: toast the bread, then poach the egg for it. */
|
||||||
|
benedictDay() {
|
||||||
|
game.setDay(22);
|
||||||
|
run(6);
|
||||||
|
harness.toast(10, 6); // golden-ish; pops -> routes to the poach
|
||||||
|
run(10);
|
||||||
|
const pv = game.poachView as unknown as { session: import('./sim/poach').PoachSession };
|
||||||
|
const s = pv.session;
|
||||||
|
if (!s) return { error: 'not at poach after toast', view: app.currentView === game.poachView ? 'poach' : 'other' };
|
||||||
|
s.src.target = 7;
|
||||||
|
let g = 0;
|
||||||
|
while (s.src.output < 6 && g++ < 1200) run(1);
|
||||||
|
for (let k = 0; k < 3 * 24; k++) {
|
||||||
|
const a = (k / 24) * Math.PI * 2;
|
||||||
|
point(Math.cos(a) * 0.7, 0.7, Math.sin(a) * 0.7, true);
|
||||||
|
run(1);
|
||||||
|
}
|
||||||
|
point(1.6, 0.7, 1.6, false);
|
||||||
|
run(2);
|
||||||
|
tap('Space');
|
||||||
|
g = 0;
|
||||||
|
while ((s.whiteSet < 0.86 || s.yolkSet > 0.35) && s.yolkSet <= 0.3 && g++ < 60 * 40) run(1);
|
||||||
|
tap('KeyL');
|
||||||
|
for (let i = 0; i < 60 * 4; i++) run(1);
|
||||||
|
tap('Enter');
|
||||||
|
run(6);
|
||||||
|
const rows = [...document.querySelectorAll('.crit')].map((e) => e.textContent);
|
||||||
|
const lines = [...document.querySelectorAll('.judge-lines p')].map((e) => e.textContent);
|
||||||
|
return { rows, lines };
|
||||||
|
},
|
||||||
|
|
||||||
/** Play the day-21 poach with REAL input. good=true: shiver + vortex + drop
|
/** Play the day-21 poach with REAL input. good=true: shiver + vortex + drop
|
||||||
* + lift-at-set + drain. false: still water, rolling boil, overstay, no drain. */
|
* + lift-at-set + drain. false: still water, rolling boil, overstay, no drain. */
|
||||||
poachDay(good = true) {
|
poachDay(good = true) {
|
||||||
|
|||||||
@ -19,7 +19,7 @@ import { juiceCriteria, type JuiceResult } from '../sim/juicing';
|
|||||||
import { TempClock } from '../sim/tempclock';
|
import { TempClock } from '../sim/tempclock';
|
||||||
import type { RoastResult } from '../sim/roasting';
|
import type { RoastResult } from '../sim/roasting';
|
||||||
import type { AssemblyResult } from '../sim/assembly';
|
import type { AssemblyResult } from '../sim/assembly';
|
||||||
import { judgeBruschetta, judgeGrill, judgePan, judgeFridge, judgeSteak, judgeEggs, judgePoach, type BruschettaResult, type PrepResult, type Verdict } from './judging';
|
import { judgeBruschetta, judgeGrill, judgePan, judgeFridge, judgeSteak, judgeEggs, judgePoach, judgeBenedict, type BruschettaResult, type PrepResult, type Verdict } from './judging';
|
||||||
import { JudgeView } from '../scenes/judge';
|
import { JudgeView } from '../scenes/judge';
|
||||||
import { DrawerView } from '../scenes/drawer';
|
import { DrawerView } from '../scenes/drawer';
|
||||||
import { TOOLS, type ToolId } from '../sim/cutlery';
|
import { TOOLS, type ToolId } from '../sim/cutlery';
|
||||||
@ -46,6 +46,10 @@ interface Save {
|
|||||||
/** The cold store, persisted: what you stocked on a fridge day is still there
|
/** The cold store, persisted: what you stocked on a fridge day is still there
|
||||||
* (and older) when a later cook reaches in for it. */
|
* (and older) when a later cook reaches in for it. */
|
||||||
fridge?: StoredItemSave[];
|
fridge?: StoredItemSave[];
|
||||||
|
/** Consecutive S grades. The judge notices. Breaking it, he notices more. */
|
||||||
|
streak?: number;
|
||||||
|
/** The regulars remember: per-customer visit count and how last time went. */
|
||||||
|
customers?: Record<string, { visits: number; last: Grade; best: number }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -148,6 +152,7 @@ export class Game {
|
|||||||
// the drawer as it always did.
|
// the drawer as it always did.
|
||||||
this.kitchen.onPopped = () => {
|
this.kitchen.onPopped = () => {
|
||||||
if (this.order.bruschetta) this.enterOven();
|
if (this.order.bruschetta) this.enterOven();
|
||||||
|
else if (this.order.benedict) this.enterBenedictPoach();
|
||||||
else this.openDrawer();
|
else this.openDrawer();
|
||||||
};
|
};
|
||||||
this.kitchen.onServed = () => this.serve();
|
this.kitchen.onServed = () => this.serve();
|
||||||
@ -398,6 +403,7 @@ export class Game {
|
|||||||
};
|
};
|
||||||
const v = judgeBruschetta(slice, this.order, this.kitchen.toolId, seconds, result);
|
const v = judgeBruschetta(slice, this.order, this.kitchen.toolId, seconds, result);
|
||||||
this.lastBruschetta = { result, verdict: v };
|
this.lastBruschetta = { result, verdict: v };
|
||||||
|
this.recordVerdict(v);
|
||||||
this.save.total += v.total;
|
this.save.total += v.total;
|
||||||
const key = `day${this.order.day}`;
|
const key = `day${this.order.day}`;
|
||||||
this.save.best[key] = Math.max(this.save.best[key] ?? 0, v.total);
|
this.save.best[key] = Math.max(this.save.best[key] ?? 0, v.total);
|
||||||
@ -588,6 +594,37 @@ export class Game {
|
|||||||
this.enterToastFlow();
|
this.enterToastFlow();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** The benedict's second act: the toast is made — now poach the egg for it. */
|
||||||
|
private enterBenedictPoach(): void {
|
||||||
|
this.poach.reset(this.order.benedict!.fuel, 'now the egg — poach it and land it on the toast');
|
||||||
|
this.hideAllScenes();
|
||||||
|
this.poach.root.visible = true;
|
||||||
|
this.app.setView(this.poach);
|
||||||
|
this.ticket.show();
|
||||||
|
this.poach.onDone = (result) => {
|
||||||
|
this.poach.root.visible = false;
|
||||||
|
this.serveBenedict(result);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private serveBenedict(poach: ReturnType<PoachView['result']>): void {
|
||||||
|
this.timing = false;
|
||||||
|
const seconds = this.orderSeconds;
|
||||||
|
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());
|
||||||
|
audio.stamp();
|
||||||
|
this.app.setView(this.judgeView);
|
||||||
|
this.ticket.hide();
|
||||||
|
}
|
||||||
|
|
||||||
/** The poach pot: spin it, drop it, lift it, drain it, serve it. */
|
/** The poach pot: spin it, drop it, lift it, drain it, serve it. */
|
||||||
private enterPoach(): void {
|
private enterPoach(): void {
|
||||||
this.poach.reset(this.order.poach!.fuel);
|
this.poach.reset(this.order.poach!.fuel);
|
||||||
@ -606,6 +643,7 @@ export class Game {
|
|||||||
const seconds = this.orderSeconds;
|
const seconds = this.orderSeconds;
|
||||||
const slice = this.kitchen.currentSlice;
|
const slice = this.kitchen.currentSlice;
|
||||||
const v = judgePoach(result, this.order, seconds);
|
const v = judgePoach(result, this.order, seconds);
|
||||||
|
this.recordVerdict(v);
|
||||||
this.save.total += v.total;
|
this.save.total += v.total;
|
||||||
const key = `day${this.order.day}`;
|
const key = `day${this.order.day}`;
|
||||||
this.save.best[key] = Math.max(this.save.best[key] ?? 0, v.total);
|
this.save.best[key] = Math.max(this.save.best[key] ?? 0, v.total);
|
||||||
@ -637,6 +675,7 @@ export class Game {
|
|||||||
const seconds = this.orderSeconds;
|
const seconds = this.orderSeconds;
|
||||||
const slice = this.kitchen.currentSlice;
|
const slice = this.kitchen.currentSlice;
|
||||||
const v = judgeEggs(result, this.order, seconds);
|
const v = judgeEggs(result, this.order, seconds);
|
||||||
|
this.recordVerdict(v);
|
||||||
this.save.total += v.total;
|
this.save.total += v.total;
|
||||||
const key = `day${this.order.day}`;
|
const key = `day${this.order.day}`;
|
||||||
this.save.best[key] = Math.max(this.save.best[key] ?? 0, v.total);
|
this.save.best[key] = Math.max(this.save.best[key] ?? 0, v.total);
|
||||||
@ -679,6 +718,7 @@ export class Game {
|
|||||||
const seconds = this.orderSeconds;
|
const seconds = this.orderSeconds;
|
||||||
const slice = this.kitchen.currentSlice;
|
const slice = this.kitchen.currentSlice;
|
||||||
const v = judgeSteak(result, this.order, seconds);
|
const v = judgeSteak(result, this.order, seconds);
|
||||||
|
this.recordVerdict(v);
|
||||||
this.save.total += v.total;
|
this.save.total += v.total;
|
||||||
const key = `day${this.order.day}`;
|
const key = `day${this.order.day}`;
|
||||||
this.save.best[key] = Math.max(this.save.best[key] ?? 0, v.total);
|
this.save.best[key] = Math.max(this.save.best[key] ?? 0, v.total);
|
||||||
@ -712,6 +752,7 @@ export class Game {
|
|||||||
// fridge, at whatever freshness your storage (and the days) left each item.
|
// fridge, at whatever freshness your storage (and the days) left each item.
|
||||||
this.save.fridge = serializeStore(result.store);
|
this.save.fridge = serializeStore(result.store);
|
||||||
const v = judgeFridge(result, this.order, seconds);
|
const v = judgeFridge(result, this.order, seconds);
|
||||||
|
this.recordVerdict(v);
|
||||||
this.save.total += v.total;
|
this.save.total += v.total;
|
||||||
const key = `day${this.order.day}`;
|
const key = `day${this.order.day}`;
|
||||||
this.save.best[key] = Math.max(this.save.best[key] ?? 0, v.total);
|
this.save.best[key] = Math.max(this.save.best[key] ?? 0, v.total);
|
||||||
@ -763,6 +804,7 @@ export class Game {
|
|||||||
const seconds = this.orderSeconds;
|
const seconds = this.orderSeconds;
|
||||||
const slice = this.kitchen.currentSlice;
|
const slice = this.kitchen.currentSlice;
|
||||||
const v = judgePan(result, this.order, seconds, this.panIngredient ?? undefined);
|
const v = judgePan(result, this.order, seconds, this.panIngredient ?? undefined);
|
||||||
|
this.recordVerdict(v);
|
||||||
this.save.total += v.total;
|
this.save.total += v.total;
|
||||||
const key = `day${this.order.day}`;
|
const key = `day${this.order.day}`;
|
||||||
this.save.best[key] = Math.max(this.save.best[key] ?? 0, v.total);
|
this.save.best[key] = Math.max(this.save.best[key] ?? 0, v.total);
|
||||||
@ -793,6 +835,7 @@ export class Game {
|
|||||||
const seconds = this.orderSeconds;
|
const seconds = this.orderSeconds;
|
||||||
const slice = this.kitchen.currentSlice;
|
const slice = this.kitchen.currentSlice;
|
||||||
const v = judgeGrill(result, this.order, seconds);
|
const v = judgeGrill(result, this.order, seconds);
|
||||||
|
this.recordVerdict(v);
|
||||||
this.save.total += v.total;
|
this.save.total += v.total;
|
||||||
const key = `day${this.order.day}`;
|
const key = `day${this.order.day}`;
|
||||||
this.save.best[key] = Math.max(this.save.best[key] ?? 0, v.total);
|
this.save.best[key] = Math.max(this.save.best[key] ?? 0, v.total);
|
||||||
@ -830,11 +873,48 @@ export class Game {
|
|||||||
this.ticket.show();
|
this.ticket.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The kitchen's memory: streaks the judge acknowledges, and regulars who
|
||||||
|
* remember how last time went. Runs before the verdict shows, so its lines
|
||||||
|
* ride the judge screen via extraLines.
|
||||||
|
*/
|
||||||
|
private recordVerdict(v: Verdict): void {
|
||||||
|
const extras: string[] = [];
|
||||||
|
// The streak. He notices. He hates that he notices.
|
||||||
|
const prev = this.save.streak ?? 0;
|
||||||
|
if (v.grade === 'S') {
|
||||||
|
this.save.streak = prev + 1;
|
||||||
|
if (this.save.streak === 3) extras.push('Three in a row. I am beginning to suspect competence.');
|
||||||
|
else if (this.save.streak === 5) extras.push('Five. I have started a file.');
|
||||||
|
else if (this.save.streak > 5 && this.save.streak % 5 === 0) extras.push(`${this.save.streak} straight. This is no longer luck and we both know it.`);
|
||||||
|
} else {
|
||||||
|
if (prev >= 3) extras.push(`The streak is dead. It was ${prev}. We do not speak of it.`);
|
||||||
|
this.save.streak = 0;
|
||||||
|
}
|
||||||
|
// The regulars remember.
|
||||||
|
const who = this.order.who;
|
||||||
|
const book = (this.save.customers ??= {});
|
||||||
|
const c = book[who] ?? { visits: 0, last: v.grade, best: 0 };
|
||||||
|
if (c.visits > 0) {
|
||||||
|
if (c.last === 'S' && v.grade !== 'S') extras.push(`${who} remembers last time being better. So do I.`);
|
||||||
|
else if ((c.last === 'C' || c.last === 'F') && (v.grade === 'S' || v.grade === 'A')) extras.push(`An improvement on ${who}'s last visit. It was noted. It was needed.`);
|
||||||
|
}
|
||||||
|
c.visits++;
|
||||||
|
c.last = v.grade;
|
||||||
|
c.best = Math.max(c.best, v.total);
|
||||||
|
book[who] = c;
|
||||||
|
this.judgeView.extraLines = extras;
|
||||||
|
}
|
||||||
|
|
||||||
private renderTicket(): void {
|
private renderTicket(): void {
|
||||||
const o = this.order;
|
const o = this.order;
|
||||||
this.ticketEl.innerHTML = '';
|
this.ticketEl.innerHTML = '';
|
||||||
el('div', 'ticket-day', this.ticketEl, `DAY ${o.day}`);
|
el('div', 'ticket-day', this.ticketEl, `DAY ${o.day}`);
|
||||||
el('div', 'ticket-who', this.ticketEl, o.who);
|
el('div', 'ticket-who', this.ticketEl, o.who);
|
||||||
|
const knownAs = this.save.customers?.[o.who];
|
||||||
|
if (knownAs && knownAs.visits > 0) {
|
||||||
|
el('div', 'ticket-hint', this.ticketEl, `visit №${knownAs.visits + 1} — last time: ${knownAs.last}${knownAs.last === 'F' ? '. they came BACK.' : ''}`);
|
||||||
|
}
|
||||||
el('div', 'ticket-text', this.ticketEl, `“${o.text}”`);
|
el('div', 'ticket-text', this.ticketEl, `“${o.text}”`);
|
||||||
const spec = el('div', 'ticket-spec', this.ticketEl);
|
const spec = el('div', 'ticket-spec', this.ticketEl);
|
||||||
el('span', 'chip brand', spec, o.brand);
|
el('span', 'chip brand', spec, o.brand);
|
||||||
@ -900,6 +980,7 @@ export class Game {
|
|||||||
// one verdict covers the toast and everything the order asked beside it.
|
// one verdict covers the toast and everything the order asked beside it.
|
||||||
const extra = this.juiceResult ? juiceCriteria(this.juiceResult) : [];
|
const extra = this.juiceResult ? juiceCriteria(this.juiceResult) : [];
|
||||||
const v = judge(slice, this.order, this.kitchen.toolId, seconds, this.prepResult ?? undefined, extra);
|
const v = judge(slice, this.order, this.kitchen.toolId, seconds, this.prepResult ?? undefined, extra);
|
||||||
|
this.recordVerdict(v);
|
||||||
this.save.total += v.total;
|
this.save.total += v.total;
|
||||||
const key = `day${this.order.day}`;
|
const key = `day${this.order.day}`;
|
||||||
this.save.best[key] = Math.max(this.save.best[key] ?? 0, v.total);
|
this.save.best[key] = Math.max(this.save.best[key] ?? 0, v.total);
|
||||||
@ -956,13 +1037,13 @@ function loadSave(): Save {
|
|||||||
if (raw) {
|
if (raw) {
|
||||||
const s = JSON.parse(raw) as Partial<Save>;
|
const s = JSON.parse(raw) as Partial<Save>;
|
||||||
if (typeof s.day === 'number' && s.day >= 1) {
|
if (typeof s.day === 'number' && s.day >= 1) {
|
||||||
return { day: s.day, maxDay: Math.max(s.day, s.maxDay ?? s.day), total: s.total ?? 0, best: s.best ?? {}, fridge: s.fridge };
|
return { day: s.day, maxDay: Math.max(s.day, s.maxDay ?? s.day), total: s.total ?? 0, best: s.best ?? {}, fridge: s.fridge, streak: s.streak ?? 0, customers: s.customers ?? {} };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
/* a corrupt save is not worth a crash */
|
/* a corrupt save is not worth a crash */
|
||||||
}
|
}
|
||||||
return { day: 1, maxDay: 1, total: 0, best: {} };
|
return { day: 1, maxDay: 1, total: 0, best: {}, streak: 0, customers: {} };
|
||||||
}
|
}
|
||||||
|
|
||||||
function writeSave(s: Save): void {
|
function writeSave(s: Save): void {
|
||||||
|
|||||||
@ -616,6 +616,91 @@ export function judgePoach(r: PoachResult, order: Order, seconds: number): Verdi
|
|||||||
return { criteria, total, grade: gradeOf(total), best: sorted[sorted.length - 1], worst: sorted[0], lines: [] };
|
return { criteria, total, grade: gradeOf(total), best: sorted[sorted.length - 1], worst: sorted[0], lines: [] };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =====================================================================
|
||||||
|
// The benedict — two crafts, one plate. The Bread rows read the slice the
|
||||||
|
// toaster made; The Poach rows read the egg; the cling sogs the toast beneath.
|
||||||
|
|
||||||
|
export function judgeBenedict(slice: Slice, order: Order, r: PoachResult, seconds: number): Verdict {
|
||||||
|
const b = slice.browning.stats(slice.mask);
|
||||||
|
const charFrac = slice.browning.fraction(slice.mask, (v) => v > CHAR_THRESHOLD);
|
||||||
|
const criteria: Criterion[] = [
|
||||||
|
{
|
||||||
|
key: 'browning',
|
||||||
|
group: 'bread',
|
||||||
|
label: 'Browning',
|
||||||
|
score: clamp01(1 - Math.abs(b.mean - order.browning) / 0.3),
|
||||||
|
weight: 1.1,
|
||||||
|
detail: `${b.mean.toFixed(2)} against ${order.browning.toFixed(2)} asked`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'evenness',
|
||||||
|
group: 'bread',
|
||||||
|
label: 'Evenness',
|
||||||
|
score: clamp01(1 - b.stdev / 0.2),
|
||||||
|
weight: 0.8,
|
||||||
|
detail: `variation ${b.stdev.toFixed(3)}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'char',
|
||||||
|
group: 'bread',
|
||||||
|
label: 'Char',
|
||||||
|
score: order.noChar ? clamp01(1 - charFrac / 0.12) : clamp01(1 - charFrac / 0.45),
|
||||||
|
weight: 0.6,
|
||||||
|
detail: charFrac < 0.005 ? 'none' : `${Math.round(charFrac * 100)}% burnt`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'poachwhite',
|
||||||
|
group: 'poach',
|
||||||
|
label: 'The White',
|
||||||
|
score: clamp01(r.whiteScore),
|
||||||
|
weight: 1.1,
|
||||||
|
detail: r.whiteSet >= 0.85 ? 'set, not snotty' : r.whiteSet >= 0.55 ? 'soft in the middle' : 'raw — it never set',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'poachwobble',
|
||||||
|
group: 'poach',
|
||||||
|
label: 'The Wobble',
|
||||||
|
score: clamp01(r.yolkScore),
|
||||||
|
weight: 1.3,
|
||||||
|
detail: r.wobbleWord,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'poachform',
|
||||||
|
group: 'poach',
|
||||||
|
label: 'The Form',
|
||||||
|
score: clamp01(r.formScore),
|
||||||
|
weight: 0.9,
|
||||||
|
detail: r.rags > 0.5 ? 'a pot of rags' : r.rags > 0.15 ? 'trailing ribbons' : 'one neat comet',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'benedictsog',
|
||||||
|
group: 'bench',
|
||||||
|
label: 'The Plate',
|
||||||
|
// An undrained egg sogs the toast under it — both crafts pay.
|
||||||
|
score: clamp01(1 - r.cling * 1.1),
|
||||||
|
weight: 0.8,
|
||||||
|
detail: r.cling > 0.3 ? 'poach water soaking the toast' : 'dry underneath — drained properly',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'time',
|
||||||
|
label: 'Service',
|
||||||
|
score: clamp01(1 - (seconds - 75) / 100),
|
||||||
|
weight: 0.3,
|
||||||
|
detail: `${Math.round(seconds)}s`,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
let sum = 0;
|
||||||
|
let wsum = 0;
|
||||||
|
for (const c of criteria) {
|
||||||
|
sum += c.score * c.weight;
|
||||||
|
wsum += c.weight;
|
||||||
|
}
|
||||||
|
const total = (sum / wsum) * 10;
|
||||||
|
const rankable = criteria.filter((c) => c.key !== 'time');
|
||||||
|
const sorted = [...rankable].sort((a, b2) => a.score - b2.score);
|
||||||
|
return { criteria, total, grade: gradeOf(total), best: sorted[sorted.length - 1], worst: sorted[0], lines: [] };
|
||||||
|
}
|
||||||
|
|
||||||
// =====================================================================
|
// =====================================================================
|
||||||
// The rib eye — cook it, rest it, cut it. The Rest is the nerve test: it reads
|
// The rib eye — cook it, rest it, cut it. The Rest is the nerve test: it reads
|
||||||
// the flood, so cutting a hot steak bleeds points onto the board.
|
// the flood, so cutting a hot steak bleeds points onto the board.
|
||||||
|
|||||||
@ -436,6 +436,16 @@ Object.assign(BANK, {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Object.assign(BANK, {
|
||||||
|
benedictsog: {
|
||||||
|
bad: [
|
||||||
|
'You put a wet egg on good toast. Now it is neither.',
|
||||||
|
'The bread beneath is a sponge. The spoon has holes, use them.',
|
||||||
|
],
|
||||||
|
good: ['Dry toast under a trembling egg. That is the whole architecture.'],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
// The rib eye — the steakhouse grandfather who has watched a thousand cooks panic.
|
// The rib eye — the steakhouse grandfather who has watched a thousand cooks panic.
|
||||||
Object.assign(BANK, {
|
Object.assign(BANK, {
|
||||||
steakcook: {
|
steakcook: {
|
||||||
|
|||||||
@ -116,6 +116,9 @@ export interface Order {
|
|||||||
eggs?: { need: number; cartonAge: number };
|
eggs?: { need: number; cartonAge: number };
|
||||||
/** The poach: fuel under the pot. The vortex, the shiver, the drop, the wobble. */
|
/** The poach: fuel under the pot. The vortex, the shiver, the drop, the wobble. */
|
||||||
poach?: { fuel: string };
|
poach?: { fuel: string };
|
||||||
|
/** Eggs benedict: toast the bread, then poach the egg and land it ON the
|
||||||
|
* toast — one plate, two crafts, one verdict. */
|
||||||
|
benedict?: { fuel: string };
|
||||||
}
|
}
|
||||||
|
|
||||||
export const BROWNING_NAMES: [number, string][] = [
|
export const BROWNING_NAMES: [number, string][] = [
|
||||||
@ -472,6 +475,21 @@ export const DAYS: Order[] = [
|
|||||||
text: 'One poached egg, proper. Bring the water to a SHIVER — not a boil, it will tear. Spin a vortex, drop it in the eye, and lift it when the white sets and the yolk still trembles. I am going to press it.',
|
text: 'One poached egg, proper. Bring the water to a SHIVER — not a boil, it will tear. Spin a vortex, drop it in the eye, and lift it when the white sets and the yolk still trembles. I am going to press it.',
|
||||||
poach: { fuel: 'gas' },
|
poach: { fuel: 'gas' },
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
day: 22,
|
||||||
|
brand: 'WONDERSLICE',
|
||||||
|
bread: 'white',
|
||||||
|
spread: 'butter',
|
||||||
|
amount: 'normal',
|
||||||
|
browning: 0.55,
|
||||||
|
browningName: 'golden',
|
||||||
|
noChar: true,
|
||||||
|
tool: 'butter_knife',
|
||||||
|
butterSoftness: 0.5,
|
||||||
|
who: 'the benedict table, again',
|
||||||
|
text: 'The full benedict. Golden toast — THEN the egg: shiver, vortex, drop, lift it trembling, drain it, and land it on the bread. Two crafts, one plate. Ruin either and the other dies with it.',
|
||||||
|
benedict: { fuel: 'gas' },
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
/** Days beyond the script — keep going, with everything cranked. */
|
/** Days beyond the script — keep going, with everything cranked. */
|
||||||
@ -490,8 +508,9 @@ export function proceduralOrder(day: number, rand: () => number): Order {
|
|||||||
if (roll < 0.64) return proceduralGrill(day, rand);
|
if (roll < 0.64) return proceduralGrill(day, rand);
|
||||||
if (roll < 0.75) return proceduralPan(day, rand);
|
if (roll < 0.75) return proceduralPan(day, rand);
|
||||||
if (roll < 0.84) return proceduralSteak(day, rand);
|
if (roll < 0.84) return proceduralSteak(day, rand);
|
||||||
if (roll < 0.91) return proceduralEggs(day, rand);
|
if (roll < 0.90) return proceduralEggs(day, rand);
|
||||||
if (roll < 0.96) return proceduralPoach(day, rand);
|
if (roll < 0.94) return proceduralPoach(day, rand);
|
||||||
|
if (roll < 0.97) return proceduralBenedict(day, rand);
|
||||||
return proceduralFridge(day, rand);
|
return proceduralFridge(day, rand);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -598,6 +617,16 @@ function proceduralPoach(day: number, rand: () => number): Order {
|
|||||||
return o;
|
return o;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function proceduralBenedict(day: number, rand: () => number): Order {
|
||||||
|
const fuel = day > 28 && rand() < 0.4 ? 'electric' : 'gas';
|
||||||
|
const o = baseOrder(day, pickOf(rand, ['the benedict table, again', 'a hollandaise denialist', 'brunch, incarnate']), '');
|
||||||
|
o.browning = 0.45 + rand() * 0.2;
|
||||||
|
o.browningName = nameBrowning(o.browning);
|
||||||
|
o.benedict = { fuel };
|
||||||
|
o.text = `The full benedict — ${o.browningName} toast, then the poach on top. White set, yolk trembling, bread not soggy. Two crafts, one plate.`;
|
||||||
|
return o;
|
||||||
|
}
|
||||||
|
|
||||||
function proceduralFridge(day: number, rand: () => number): Order {
|
function proceduralFridge(day: number, rand: () => number): Order {
|
||||||
const o = baseOrder(day, 'the delivery, at 6am', '');
|
const o = baseOrder(day, 'the delivery, at 6am', '');
|
||||||
void rand();
|
void rand();
|
||||||
|
|||||||
@ -30,6 +30,8 @@ export class JudgeView implements View {
|
|||||||
private viewBtn!: HTMLButtonElement;
|
private viewBtn!: HTMLButtonElement;
|
||||||
|
|
||||||
onNext: (() => void) | null = null;
|
onNext: (() => void) | null = null;
|
||||||
|
/** One-shot lines the game adds to this verdict (streaks, regulars' memory). */
|
||||||
|
extraLines: string[] = [];
|
||||||
|
|
||||||
constructor(private app: App) {
|
constructor(private app: App) {
|
||||||
this.pedestal = new THREE.Group();
|
this.pedestal = new THREE.Group();
|
||||||
@ -107,6 +109,8 @@ export class JudgeView implements View {
|
|||||||
// asked and what for, not the unread toast defaults.
|
// asked and what for, not the unread toast defaults.
|
||||||
this.orderEl.textContent = order.grill
|
this.orderEl.textContent = order.grill
|
||||||
? `Day ${order.day} · ${order.who} asked for the barbie`
|
? `Day ${order.day} · ${order.who} asked for the barbie`
|
||||||
|
: order.benedict
|
||||||
|
? `Day ${order.day} · ${order.who} asked for the full benedict`
|
||||||
: order.poach
|
: order.poach
|
||||||
? `Day ${order.day} · ${order.who} asked for the poached egg`
|
? `Day ${order.day} · ${order.who} asked for the poached egg`
|
||||||
: order.eggs
|
: order.eggs
|
||||||
@ -170,6 +174,8 @@ export class JudgeView implements View {
|
|||||||
clear(this.linesEl);
|
clear(this.linesEl);
|
||||||
const lines = verdictLines(verdict, order, TOOLS[tool].name, rand);
|
const lines = verdictLines(verdict, order, TOOLS[tool].name, rand);
|
||||||
for (const l of lines) el('p', undefined, this.linesEl, l);
|
for (const l of lines) el('p', undefined, this.linesEl, l);
|
||||||
|
for (const l of this.extraLines) el('p', 'judge-extra', this.linesEl, l);
|
||||||
|
this.extraLines = [];
|
||||||
|
|
||||||
this.faceEl.src = assetUrl(`/assets/img/judge_${judgeFace(verdict.grade)}.png`);
|
this.faceEl.src = assetUrl(`/assets/img/judge_${judgeFace(verdict.grade)}.png`);
|
||||||
this.stampEl.textContent = verdict.grade;
|
this.stampEl.textContent = verdict.grade;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user