Compare commits

..

No commits in common. "afe2bf174ba09cd787c3d341c1791ee948c8ea86" and "2085f68655e99de6cea8c90aaef0a25137b2d3cb" have entirely different histories.

8 changed files with 11 additions and 180 deletions

View File

@ -249,18 +249,8 @@ 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 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.
**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.
The cure is time you *want*; the cold chain is time you *fight*. `src/sim/coldchain.ts`

View File

@ -517,35 +517,6 @@ 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) {

View File

@ -8,7 +8,6 @@ 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, type StoredItemSave } from '../sim/coldchain';
import { AssemblyView } from '../scenes/assembly';
import type { CutPattern } from '../sim/cutting';
import type { IngredientId } from '../sim/ingredients';
@ -37,9 +36,6 @@ interface Save {
day: number;
total: 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[];
}
/**
@ -143,12 +139,6 @@ export class Game {
this.kitchen.flash(cut.knife === 'the_best_thing' ? 'cut with The Best Thing' : 'now toast it');
};
this.judgeView.onNext = () => {
// Commit any fridge consumption atomically with the day advance: a reload
// before this point re-derives the same fetch, so the cap can't be dodged.
if (this.pendingFridge) {
this.save.fridge = this.pendingFridge;
this.pendingFridge = null;
}
this.save.day++;
writeSave(this.save);
this.beginDay();
@ -547,9 +537,6 @@ 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}`;
@ -564,28 +551,8 @@ 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;
/** The fridge stock AFTER this day's fetch, held until the day actually advances
* (committed in onNext) so a mid-service reload re-fetches the same item and
* can't dodge the rot-cap or silently lose the ingredient. */
private pendingFridge: StoredItemSave[] | null = null;
private enterPan(): void {
const p = this.order.pan!;
this.panIngredient = null;
this.pendingFridge = null;
if (p.fromFridge && this.save.fridge?.length) {
// Reach into the fridge you stocked: FIFO, at whatever freshness your storage
// left it. The consumed stock is HELD (pendingFridge), not written yet.
const store = restoreStore(this.save.fridge);
const got = fetchStock(store, p.food);
if (got) {
const q = usableQuality(got);
this.panIngredient = { quality: q.quality, word: q.word };
this.pendingFridge = serializeStore(store);
}
}
this.pan.reset(p.food, p.fuel);
this.hideAllScenes();
this.pan.root.visible = true;
@ -601,7 +568,7 @@ export class Game {
this.timing = false;
const seconds = this.orderSeconds;
const slice = this.kitchen.currentSlice;
const v = judgePan(result, this.order, seconds, this.panIngredient ?? undefined);
const v = judgePan(result, this.order, seconds);
this.save.total += v.total;
const key = `day${this.order.day}`;
this.save.best[key] = Math.max(this.save.best[key] ?? 0, v.total);
@ -795,7 +762,7 @@ function loadSave(): Save {
if (raw) {
const s = JSON.parse(raw) as Partial<Save>;
if (typeof s.day === 'number' && s.day >= 1) {
return { day: s.day, total: s.total ?? 0, best: s.best ?? {}, fridge: s.fridge };
return { day: s.day, total: s.total ?? 0, best: s.best ?? {} };
}
}
} catch {

View File

@ -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
// good butter not burnt (the bitterness), and is the finished piece even.
export function judgePan(r: PanResult, order: Order, seconds: number, ingredient?: { quality: number; word: string }): Verdict {
export function judgePan(r: PanResult, order: Order, seconds: number): Verdict {
const seared = clamp01(r.doneScore);
const criteria: Criterion[] = [
{
@ -585,32 +585,13 @@ export function judgePan(r: PanResult, order: Order, seconds: number, ingredient
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) {
// Scored on the freshness BAND, not raw quality: a genuinely fresh ingredient
// is full marks (so perfect storage still leaves S on the table — the floors
// law), use-soon is a real dent, off/dangerous is near-zero and hard-caps below.
const bandScore = ingredient.word === 'fresh' ? 1 : ingredient.word === 'use soon' ? 0.55 : ingredient.word === 'off' ? 0.2 : 0;
criteria.push({
key: 'ingredient',
group: 'pan',
label: 'The Ingredient',
score: bandScore,
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;
}
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 total = (sum / wsum) * 10;
const rankable = criteria.filter((c) => c.key !== 'time');
const sorted = [...rankable].sort((a, b) => a.score - b.score);
void order;

View File

@ -367,22 +367,6 @@ 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: {

View File

@ -5,7 +5,6 @@ import { CUT_BANDS, type CutClass } from '../sim/slicing';
import type { CutPattern } from '../sim/cutting';
import type { IngredientId } from '../sim/ingredients';
import type { JuicerId } from '../sim/juicing';
import { FRIDGE_MUSHROOM_ID } from '../sim/coldchain';
/**
* A stop at the prep bench before the toaster. M11 shipped the 'cut' kind;
@ -101,10 +100,8 @@ 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.
* `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 };
* sends the day to the pan butter it, foam it, flip it, don't burn it. */
pan?: { food: string; fuel: string };
/** 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;
@ -404,21 +401,6 @@ 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: FRIDGE_MUSHROOM_ID, fuel: 'gas', fromFridge: true },
},
];
/** Days beyond the script — keep going, with everything cranked. */

View File

@ -10,7 +10,6 @@ import {
coldStep,
storeHealth,
ZONE_COLD,
FRIDGE_MUSHROOM_ID,
} from '../sim/coldchain';
import { el, Panel } from '../ui/hud';
@ -152,7 +151,6 @@ 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: FRIDGE_MUSHROOM_ID, perish: 1.3, color: 0xd8cbb0 },
{ id: 'onion', perish: 0.3, color: 0xb99a5b },
];
spec.forEach((sp, i) => {

View File

@ -50,10 +50,6 @@ export const FRESH = 0.7;
export const USE_SOON = 0.4;
export const OFF = 0.15;
/** The mushroom that ties the fridge day (delivery) to the pan day (fetch). One
* id, so a rename can't silently uncouple the two days. */
export const FRIDGE_MUSHROOM_ID = 'button_mushroom';
/** Base spoilage per DAY at bench temp, before the item's own perishability.
* Spoilage lives on the day clock, not the second clock food goes off across
* days (the cure system's twin), and a service leaves things out for a fraction
@ -187,15 +183,11 @@ export function freshnessWord(it: StoredItem): FreshWord {
* a serve you do not walk back.
*/
export function usableQuality(it: StoredItem): { quality: number; safe: boolean; word: FreshWord } {
const word = freshnessWord(it);
let q = it.freshness;
if (it.frozen) q *= 0.78; // thawed texture — steep enough that a 3-day freeze on a
// mid-perishable no longer beats the coldest fridge shelf.
if (it.frozen) q *= 0.82; // thawed texture
if (it.contaminated) q *= 0.4;
q = Math.max(0, q);
// The word describes what you're ACTUALLY handed (post-thaw, post-drip), not the
// raw freshness — a frozen item can read "fresh" on the shelf and "use soon" on the plate.
const word: FreshWord = q >= FRESH ? 'fresh' : q >= USE_SOON ? 'use soon' : q >= OFF ? 'off' : 'dangerous';
return { quality: q, safe: it.freshness > OFF && !it.contaminated, word };
return { quality: Math.max(0, q), safe: it.freshness > OFF && !it.contaminated, word };
}
/** The inspector's storage read: hygiene, rotation, and what's gone off on your watch. */
@ -219,37 +211,3 @@ 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<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).
* Corruption-tolerant: a garbled save (unknown zone, non-array, junk item) yields
* an empty/partial store rather than an uncaught crash the save intent of loadSave. */
export function restoreStore(items: StoredItemSave[], seed = 20260720): ColdStore {
const s = newColdStore(seed);
if (Array.isArray(items)) for (const it of items) if (it && s.zones[it.zone]) 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;
}