The cold chain closes the loop: stock day 17, cook it day 18
The fridge persists in the save (serializeStore/restoreStore/fetch in coldchain.ts). Day 18 'the lunch rush' pan-sears the very mushrooms you stored on day 17 — fetched FIFO at whatever freshness your storage left them. judgePan gains 'The Ingredient' row; a dangerous/off one HARD-CAPS the total (no sear saves rot). Judge line bank for the fetched ingredient. Verified live: identical cook (72%/57% sear), well-stored mushroom serves 7.4/10, badly-stored (warm door) serves the SAME sear but 3.0/10 (F) with 'The Ingredient: off the shelf — dangerous' capping it. Plain pan day 16 unaffected (8.0) — backward compatible. Day-17 delivery gains mushrooms; Order.pan.fromFridge flag; save carries the stock. Harness: t.coldChainArc(good?). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
2085f68655
commit
3f91bedc7f
@ -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-
|
`judgeFridge` scores THE FRIDGE (The Cold / Hygiene) + The Waste with the hygiene-
|
||||||
inspector line bank. Verified live: good storage (raw low, perishables cold, RTE
|
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
|
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
|
**C (3.5)** — 38% off, 38% dripped on, 3 wasted.
|
||||||
in the save between days) and the FIFO pull-for-a-cook are the remaining polish.
|
|
||||||
|
**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`
|
The cure is time you *want*; the cold chain is time you *fight*. `src/sim/coldchain.ts`
|
||||||
|
|||||||
29
src/dev.ts
29
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) };
|
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
|
/** 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. */
|
* (well or badly), shut the door (3 days), serve, read the scorecard. */
|
||||||
fridgeDay(good = true) {
|
fridgeDay(good = true) {
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import { OvenView } from '../scenes/oven';
|
|||||||
import { GrillView } from '../scenes/grill';
|
import { GrillView } from '../scenes/grill';
|
||||||
import { PanView } from '../scenes/pan';
|
import { PanView } from '../scenes/pan';
|
||||||
import { FridgeView } from '../scenes/fridge';
|
import { FridgeView } from '../scenes/fridge';
|
||||||
|
import { serializeStore, restoreStore, fetch as fetchStock, usableQuality, freshnessWord, type StoredItemSave } from '../sim/coldchain';
|
||||||
import { AssemblyView } from '../scenes/assembly';
|
import { AssemblyView } from '../scenes/assembly';
|
||||||
import type { CutPattern } from '../sim/cutting';
|
import type { CutPattern } from '../sim/cutting';
|
||||||
import type { IngredientId } from '../sim/ingredients';
|
import type { IngredientId } from '../sim/ingredients';
|
||||||
@ -36,6 +37,9 @@ interface Save {
|
|||||||
day: number;
|
day: number;
|
||||||
total: number;
|
total: number;
|
||||||
best: Record<string, number>;
|
best: Record<string, number>;
|
||||||
|
/** 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;
|
this.timing = false;
|
||||||
const seconds = this.orderSeconds;
|
const seconds = this.orderSeconds;
|
||||||
const slice = this.kitchen.currentSlice;
|
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);
|
const v = judgeFridge(result, this.order, seconds);
|
||||||
this.save.total += v.total;
|
this.save.total += v.total;
|
||||||
const key = `day${this.order.day}`;
|
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. */
|
/** 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 {
|
private enterPan(): void {
|
||||||
const p = this.order.pan!;
|
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.pan.reset(p.food, p.fuel);
|
||||||
this.hideAllScenes();
|
this.hideAllScenes();
|
||||||
this.pan.root.visible = true;
|
this.pan.root.visible = true;
|
||||||
@ -568,7 +591,7 @@ export class Game {
|
|||||||
this.timing = false;
|
this.timing = false;
|
||||||
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);
|
const v = judgePan(result, this.order, seconds, this.panIngredient ?? undefined);
|
||||||
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);
|
||||||
@ -762,7 +785,7 @@ 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, total: s.total ?? 0, best: s.best ?? {} };
|
return { day: s.day, total: s.total ?? 0, best: s.best ?? {}, fridge: s.fridge };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
@ -550,7 +550,7 @@ export function judgeFridge(h: ReturnType<typeof storeHealth>, order: Order, sec
|
|||||||
// M17 — The pan. Did both faces land a proper sear (the flip), did you cook in
|
// 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.
|
// 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 seared = clamp01(r.doneScore);
|
||||||
const criteria: Criterion[] = [
|
const criteria: Criterion[] = [
|
||||||
{
|
{
|
||||||
@ -585,13 +585,28 @@ export function judgePan(r: PanResult, order: Order, seconds: number): Verdict {
|
|||||||
detail: `${Math.round(seconds)}s`,
|
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 sum = 0;
|
||||||
let wsum = 0;
|
let wsum = 0;
|
||||||
for (const c of criteria) {
|
for (const c of criteria) {
|
||||||
sum += c.score * c.weight;
|
sum += c.score * c.weight;
|
||||||
wsum += 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 rankable = criteria.filter((c) => c.key !== 'time');
|
||||||
const sorted = [...rankable].sort((a, b) => a.score - b.score);
|
const sorted = [...rankable].sort((a, b) => a.score - b.score);
|
||||||
void order;
|
void order;
|
||||||
|
|||||||
@ -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.
|
// The pan — the butter snob.
|
||||||
Object.assign(BANK, {
|
Object.assign(BANK, {
|
||||||
pansear: {
|
pansear: {
|
||||||
|
|||||||
@ -100,8 +100,10 @@ export interface Order {
|
|||||||
* the whole day to the outdoor grill instead of the toaster. */
|
* the whole day to the outdoor grill instead of the toaster. */
|
||||||
grill?: string[];
|
grill?: string[];
|
||||||
/** The pan (M17): the food to sear, and the fuel the hob runs on. Presence
|
/** 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. */
|
* sends the day to the pan — butter it, foam it, flip it, don't burn it.
|
||||||
pan?: { food: string; fuel: string };
|
* `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
|
/** 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 — raw low, perishables cold, don't cram it, then three days pass. */
|
||||||
fridge?: boolean;
|
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.',
|
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,
|
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. */
|
/** Days beyond the script — keep going, with everything cranked. */
|
||||||
|
|||||||
@ -151,6 +151,7 @@ export class FridgeView implements View {
|
|||||||
{ id: 'salad', perish: 1, rte: true, color: 0x5aa84a },
|
{ id: 'salad', perish: 1, rte: true, color: 0x5aa84a },
|
||||||
{ id: 'cheese', perish: 0.7, rte: true, color: 0xe6c34e },
|
{ id: 'cheese', perish: 0.7, rte: true, color: 0xe6c34e },
|
||||||
{ id: 'berries', perish: 1.6, rte: true, age: 1, color: 0x8a3d78 },
|
{ 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 },
|
{ id: 'onion', perish: 0.3, color: 0xb99a5b },
|
||||||
];
|
];
|
||||||
spec.forEach((sp, i) => {
|
spec.forEach((sp, i) => {
|
||||||
|
|||||||
@ -211,3 +211,35 @@ export function storeHealth(s: ColdStore): { fresh: number; off: number; contami
|
|||||||
waste: s.wasteCount,
|
waste: s.wasteCount,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** A plain snapshot of the stock for the save file (no Rng, no functions). */
|
||||||
|
export type StoredItemSave = Omit<StoredItem, never>;
|
||||||
|
|
||||||
|
/** 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;
|
||||||
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user