diff --git a/KITCHEN-ATLAS.md b/KITCHEN-ATLAS.md index 4847e87..9e8f88b 100644 --- a/KITCHEN-ATLAS.md +++ b/KITCHEN-ATLAS.md @@ -249,8 +249,18 @@ door), shut the door, three days compress-pass (items recolour as they spoil), a `judgeFridge` scores THE FRIDGE (The Cold / Hygiene) + The Waste with the hygiene- inspector line bank. Verified live: good storage (raw low, perishables cold, RTE high) grades **S (10.0)**; bad storage (raw high in the warm door, crammed) grades -**C (3.5)** — 38% off, 38% dripped on, 3 wasted. Cross-day carryover (fridge ages -in the save between days) and the FIFO pull-for-a-cook are the remaining polish. +**C (3.5)** — 38% off, 38% dripped on, 3 wasted. + +**CROSS-DAY LOOP SHIPPED**: the fridge persists in the save (`serializeStore`/ +`restoreStore`) — what you stock on day 17 is still there on day 18. Day 18 "the +lunch rush" pan-sears **the very mushrooms from your own fridge** (`fetch` FIFO); +the fetched `usableQuality` is a judged row ("The Ingredient") and a genuinely off +one HARD-CAPS the dish — no sear saves rot. Verified: identical cook (72%/57% sear), +well-stored mushroom → **7.4/10**, badly-stored (warm door) → same sear, off +ingredient → **3.0/10 (F)**; plain pan day unaffected (8.0). Judge: "No sear saves a +bad mushroom. The fridge did this, not the flame." Remaining polish: per-day-transition +ageing (currently the fridge day's own 3-day compress does the ageing) and the fetch +wired into the grill/other cooks too. The cure is time you *want*; the cold chain is time you *fight*. `src/sim/coldchain.ts` diff --git a/src/dev.ts b/src/dev.ts index 0a3b0be..8e243ac 100644 --- a/src/dev.ts +++ b/src/dev.ts @@ -517,6 +517,35 @@ export function installDevHarness(app: App, game: Game): void { return { fuel, wind, sheltered, mean: +mean.toFixed(3), stdev: +stdev.toFixed(4) }; }, + /** The cross-day arc: stock the fridge on day 17 (well or badly), then cook + * those very mushrooms on day 18. Bad storage → an off mushroom caps the sear. */ + coldChainArc(goodStorage = true) { + harness.fridgeDay(goodStorage); // day 17: stock + 3 days + serve + run(5); + // Advance to day 18 (the judge's NEXT button bumps the day + beginDay). + const g = game as unknown as { save: { day: number; fridge?: unknown[] } }; + const storedCount = g.save.fridge?.length ?? 0; + game.setDay(18); + run(6); + const atPan = app.currentView === game.panView ? 'pan' : 'other'; + const fetched = (game as unknown as { panIngredient: { quality: number; word: string } | null }).panIngredient; + // Cook it well and serve — only the fetched ingredient's quality varies. + const pv = game.panView as unknown as { session: import('./sim/pan').PanSession }; + const s = pv.session; + if (s) { + setPanKnob(s, 7); addButter(s); + let guard = 0; while (butterState(s) !== 'foaming' && guard++ < 60 * 30) run(1); + addFood(s, 'button_mushroom', 'A'); + for (let i = 0; i < 60 * 8; i++) run(1); + flip(s); + for (let i = 0; i < 60 * 9; i++) run(1); + tap('Enter'); run(5); + } + const rows = [...document.querySelectorAll('.crit')].map((e) => e.textContent); + const gradeEl = document.querySelector('.judge-grade, .grade'); + return { storedCount, atPan, fetched, grade: gradeEl ? gradeEl.textContent : null, rows }; + }, + /** Play the whole day-17 fridge order: route in, put the delivery away * (well or badly), shut the door (3 days), serve, read the scorecard. */ fridgeDay(good = true) { diff --git a/src/game/game.ts b/src/game/game.ts index d9c6392..e6b0856 100644 --- a/src/game/game.ts +++ b/src/game/game.ts @@ -8,6 +8,7 @@ import { OvenView } from '../scenes/oven'; import { GrillView } from '../scenes/grill'; import { PanView } from '../scenes/pan'; import { FridgeView } from '../scenes/fridge'; +import { serializeStore, restoreStore, fetch as fetchStock, usableQuality, freshnessWord, type StoredItemSave } from '../sim/coldchain'; import { AssemblyView } from '../scenes/assembly'; import type { CutPattern } from '../sim/cutting'; import type { IngredientId } from '../sim/ingredients'; @@ -36,6 +37,9 @@ interface Save { day: number; total: number; best: Record; + /** The cold store, persisted: what you stocked on a fridge day is still there + * (and older) when a later cook reaches in for it. */ + fridge?: StoredItemSave[]; } /** @@ -537,6 +541,9 @@ export class Game { this.timing = false; const seconds = this.orderSeconds; const slice = this.kitchen.currentSlice; + // Persist the stock exactly as you left it — a later cook reaches into THIS + // fridge, at whatever freshness your storage (and the days) left each item. + this.save.fridge = serializeStore(result.store); const v = judgeFridge(result, this.order, seconds); this.save.total += v.total; const key = `day${this.order.day}`; @@ -551,8 +558,24 @@ export class Game { } /** The pan day (M17): a fuel-aware hob — butter it, foam it, flip it, serve. */ + /** When a pan day fetches its food from the fridge, what came out. */ + private panIngredient: { quality: number; word: string } | null = null; + private enterPan(): void { const p = this.order.pan!; + this.panIngredient = null; + if (p.fromFridge && this.save.fridge?.length) { + // Reach into the fridge you stocked: FIFO, at whatever freshness your + // storage left it. Then write the (now smaller) stock back to the save. + const store = restoreStore(this.save.fridge); + const got = fetchStock(store, p.food); + if (got) { + const q = usableQuality(got); + this.panIngredient = { quality: q.quality, word: freshnessWord(got) }; + this.save.fridge = serializeStore(store); + writeSave(this.save); + } + } this.pan.reset(p.food, p.fuel); this.hideAllScenes(); this.pan.root.visible = true; @@ -568,7 +591,7 @@ export class Game { this.timing = false; const seconds = this.orderSeconds; const slice = this.kitchen.currentSlice; - const v = judgePan(result, this.order, seconds); + const v = judgePan(result, this.order, seconds, this.panIngredient ?? undefined); this.save.total += v.total; const key = `day${this.order.day}`; this.save.best[key] = Math.max(this.save.best[key] ?? 0, v.total); @@ -762,7 +785,7 @@ function loadSave(): Save { if (raw) { const s = JSON.parse(raw) as Partial; if (typeof s.day === 'number' && s.day >= 1) { - return { day: s.day, total: s.total ?? 0, best: s.best ?? {} }; + return { day: s.day, total: s.total ?? 0, best: s.best ?? {}, fridge: s.fridge }; } } } catch { diff --git a/src/game/judging.ts b/src/game/judging.ts index 6a378bf..22bdfa3 100644 --- a/src/game/judging.ts +++ b/src/game/judging.ts @@ -550,7 +550,7 @@ export function judgeFridge(h: ReturnType, order: Order, sec // M17 — The pan. Did both faces land a proper sear (the flip), did you cook in // good butter not burnt (the bitterness), and is the finished piece even. -export function judgePan(r: PanResult, order: Order, seconds: number): Verdict { +export function judgePan(r: PanResult, order: Order, seconds: number, ingredient?: { quality: number; word: string }): Verdict { const seared = clamp01(r.doneScore); const criteria: Criterion[] = [ { @@ -585,13 +585,28 @@ export function judgePan(r: PanResult, order: Order, seconds: number): Verdict { detail: `${Math.round(seconds)}s`, }, ]; + // If the mushroom came out of the fridge, its freshness is judged on its own + // row — and a genuinely off ingredient CAPS the whole dish, because no sear + // saves rot. You can't cook your way out of bad storage. + if (ingredient) { + criteria.push({ + key: 'ingredient', + group: 'pan', + label: 'The Ingredient', + score: clamp01(ingredient.quality), + weight: 1.2, + detail: `off the shelf — ${ingredient.word}`, + }); + } let sum = 0; let wsum = 0; for (const c of criteria) { sum += c.score * c.weight; wsum += c.weight; } - const total = (sum / wsum) * 10; + let total = (sum / wsum) * 10; + // A dangerous/off ingredient hard-caps the total — you served it, after all. + if (ingredient && ingredient.quality < 0.4) total = Math.min(total, 3 + ingredient.quality * 5); const rankable = criteria.filter((c) => c.key !== 'time'); const sorted = [...rankable].sort((a, b) => a.score - b.score); void order; diff --git a/src/game/lines.ts b/src/game/lines.ts index dc4dd02..0847143 100644 --- a/src/game/lines.ts +++ b/src/game/lines.ts @@ -367,6 +367,22 @@ Object.assign(BANK, { }, }); +// The fetched ingredient — the cook who reads a use-by date. +Object.assign(BANK, { + ingredient: { + bad: [ + 'This was off before it hit the pan. You stored it in the door, didn\'t you. Three days ago.', + 'No sear saves a bad mushroom. The fridge did this, not the flame.', + 'You cooked something that had already given up. I can taste the fridge door.', + 'Fresh is a choice you make days early. You made the other one.', + ], + good: [ + 'The mushroom was still perfect. Cold, back of the fridge, used in time. That is the whole craft before the pan even lights.', + 'Fresh off the shelf. You store like someone who respects the ingredient.', + ], + }, +}); + // The pan — the butter snob. Object.assign(BANK, { pansear: { diff --git a/src/game/orders.ts b/src/game/orders.ts index 2302573..689cdc0 100644 --- a/src/game/orders.ts +++ b/src/game/orders.ts @@ -100,8 +100,10 @@ export interface Order { * the whole day to the outdoor grill instead of the toaster. */ grill?: string[]; /** The pan (M17): the food to sear, and the fuel the hob runs on. Presence - * sends the day to the pan — butter it, foam it, flip it, don't burn it. */ - pan?: { food: string; fuel: string }; + * sends the day to the pan — butter it, foam it, flip it, don't burn it. + * `fromFridge` pulls the food out of your stored stock — how you stocked it + * days ago decides how fresh it is now. */ + pan?: { food: string; fuel: string; fromFridge?: boolean }; /** The cold chain: a delivery to put away. Presence sends the day to the * fridge — raw low, perishables cold, don't cram it, then three days pass. */ fridge?: boolean; @@ -401,6 +403,21 @@ export const DAYS: Order[] = [ text: 'The order came in. Put it away before the inspector does his rounds — raw meat down low where it can\'t drip, the perishables in the cold at the back, and don\'t cram one shelf or nothing breathes. Three days till he looks.', fridge: true, }, + { + day: 18, + brand: 'the bakery loaf', + bread: 'white', + spread: 'butter', + amount: 'normal', + browning: 0.55, + browningName: 'golden', + noChar: true, + tool: 'butter_knife', + butterSoftness: 0.5, + who: 'the lunch rush', + text: 'Pan-sear the mushrooms — the ones from your own fridge. Whatever shape they\'re in now is the shape you stored them in. Butter, foam, both faces. No excuses about the delivery; that was your fridge.', + pan: { food: 'button_mushroom', fuel: 'gas', fromFridge: true }, + }, ]; /** Days beyond the script — keep going, with everything cranked. */ diff --git a/src/scenes/fridge.ts b/src/scenes/fridge.ts index ce9ed3a..a0a3fe2 100644 --- a/src/scenes/fridge.ts +++ b/src/scenes/fridge.ts @@ -151,6 +151,7 @@ export class FridgeView implements View { { id: 'salad', perish: 1, rte: true, color: 0x5aa84a }, { id: 'cheese', perish: 0.7, rte: true, color: 0xe6c34e }, { id: 'berries', perish: 1.6, rte: true, age: 1, color: 0x8a3d78 }, + { id: 'button_mushroom', perish: 1.3, color: 0xd8cbb0 }, { id: 'onion', perish: 0.3, color: 0xb99a5b }, ]; spec.forEach((sp, i) => { diff --git a/src/sim/coldchain.ts b/src/sim/coldchain.ts index 907fe01..f706789 100644 --- a/src/sim/coldchain.ts +++ b/src/sim/coldchain.ts @@ -211,3 +211,35 @@ export function storeHealth(s: ColdStore): { fresh: number; off: number; contami waste: s.wasteCount, }; } + +/** A plain snapshot of the stock for the save file (no Rng, no functions). */ +export type StoredItemSave = Omit; + +/** Flatten every zone's items to a plain array — each item already knows its zone. */ +export function serializeStore(s: ColdStore): StoredItemSave[] { + const out: StoredItemSave[] = []; + for (const zone of Object.keys(s.zones) as ZoneId[]) for (const it of s.zones[zone]) out.push({ ...it }); + return out; +} + +/** Rebuild a store from a save snapshot (a fresh Rng — spoilage is deterministic). */ +export function restoreStore(items: StoredItemSave[], seed = 20260720): ColdStore { + const s = newColdStore(seed); + for (const it of items) s.zones[it.zone].push({ ...it }); + return s; +} + +/** Take one item of `id` out of the store, oldest-first (FIFO) across the whole + * fridge — the cook reaches in for stock. Returns null if you've none. */ +export function fetch(s: ColdStore, id: string): StoredItem | null { + let found: { zone: ZoneId; idx: number; it: StoredItem } | null = null; + for (const zone of Object.keys(s.zones) as ZoneId[]) { + const arr = s.zones[zone]; + for (let i = 0; i < arr.length; i++) { + if (arr[i].id === id && (!found || arr[i].age > found.it.age)) found = { zone, idx: i, it: arr[i] }; + } + } + if (!found) return null; + s.zones[found.zone].splice(found.idx, 1); + return found.it; +}