toastsim/src/sim/bread.ts
monster 31f2eff28e M10 WIP + Build Brief 2: the bakery is code-complete, the prep bench is specced
M10 (code-complete, verification pending — see OPUS-BUILD-BRIEF-2.md §2):
- Brands with mechanical quality (WONDERSLICE 0.25 vs Crustworthy 0.8 etc.):
  quality scales heat-map patchiness and crumb fragility. Bakery sourdough is
  loaf-only.
- Slicing minigame: aim thickness, saw with vertical strokes; progress/wobble/
  squash; wedge slices brown unevenly in the toaster (the bad cut follows you);
  squash arrives as pre-existing damage.
- Bread Knife + THE BEST THING (heavy, soft-scalloped, saw eff 1.65) join the
  drawer; on loaf days the drawer trip happens BEFORE toasting ("they are
  waiting") and whatever you fish out is what you slice with.
- Day 9 handwritten (the inspector wants a doorstop with parallel faces),
  procedural brands/loaf days from day 10, The Cut criterion (band + wedge +
  squash, TBT signature bonus), cut line bank.
- Verified so far: day-9 ticket chips, knife-trip drawer with bread_knife AND
  the_best_thing present, timer label, typecheck + build. Slicer end-to-end
  play, tuning, and couplings assertions are itemised in the brief.

OPUS-BUILD-BRIEF-2.md — the prep bench: generalized ingredients + cutting
(slip, juice, piece-evenness CV), the judged mess field, oranges + ribbed
pyramid juicer (halve evenness caps yield, seeds by juicer tier, the
Juicernaut), the grater (zest/pith line, cheese needs cold, knuckles), onions
(sting that blurs REAL vision, the stop-short-of-the-stem criss-cross dice),
and bruschetta as the capstone (temperature planning, roast tomatoes on the
browning field, topping Clark-Evans, the sog clock vs doorstop slices).

Bakery asset batch partially failed (flux errors + one lost GLB) — regen
instructions with retries are in the brief §8.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 16:24:05 +10:00

140 lines
4.3 KiB
TypeScript

/**
* Bread archetypes. The three numbers that matter for the toasting sim:
* thickness — thermal lag (slow to brown) but holds warmth longer, which
* matters later because warm toast is what softens hard butter.
* moisture — water must boil off before the crust can brown, so a wet crumb
* stalls, then catches up fast once it dries. Sourdough's trap.
* sugar — browns early and burns early. Raisin bread is a countdown.
*/
export interface Bread {
id: BreadId;
name: string;
blurb: string;
thickness: number; // world units
width: number;
height: number;
domeH: number; // how much of the top is the loaf's dome
moisture: number; // 0..1
sugar: number; // 0..1
crumb: [number, number, number];
crust: [number, number, number];
crumbScale: number; // crumb noise frequency
crumbContrast: number;
/** Speckles baked into the crumb: raisins, grain kibble. */
inclusions: number;
inclusionColor: [number, number, number];
}
export type BreadId = 'white' | 'sourdough' | 'multigrain' | 'raisin';
export const BREADS: Record<BreadId, Bread> = {
white: {
id: 'white',
name: 'Supermarket White',
blurb: 'Forgiving. Browns like it wants to be liked.',
thickness: 0.13,
width: 1.0,
height: 1.0,
domeH: 0.26,
moisture: 0.18,
sugar: 0.22,
crumb: [0.95, 0.9, 0.78],
crust: [0.78, 0.6, 0.36],
crumbScale: 34,
crumbContrast: 0.07,
inclusions: 0,
inclusionColor: [0, 0, 0],
},
sourdough: {
id: 'sourdough',
name: 'Thick-Cut Sourdough',
blurb: 'Wet, dense, and stubborn. Stalls, then catches you out.',
thickness: 0.23,
width: 1.06,
height: 1.02,
domeH: 0.3,
moisture: 0.72,
sugar: 0.1,
crumb: [0.93, 0.87, 0.72],
crust: [0.62, 0.42, 0.22],
crumbScale: 16,
crumbContrast: 0.16,
inclusions: 0,
inclusionColor: [0, 0, 0],
},
multigrain: {
id: 'multigrain',
name: 'Multigrain',
blurb: 'The seeds catch first. They always catch first.',
thickness: 0.15,
width: 1.0,
height: 0.98,
domeH: 0.24,
moisture: 0.34,
sugar: 0.3,
crumb: [0.82, 0.72, 0.55],
crust: [0.55, 0.4, 0.24],
crumbScale: 30,
crumbContrast: 0.13,
inclusions: 0.55,
inclusionColor: [0.36, 0.26, 0.15],
},
raisin: {
id: 'raisin',
name: 'Raisin Loaf',
blurb: 'Sugar. Everywhere. You have less time than you think.',
thickness: 0.14,
width: 0.98,
height: 0.98,
domeH: 0.26,
moisture: 0.3,
sugar: 0.92,
crumb: [0.93, 0.84, 0.66],
crust: [0.7, 0.48, 0.26],
crumbScale: 26,
crumbContrast: 0.09,
inclusions: 0.85,
inclusionColor: [0.22, 0.11, 0.12],
},
};
export const BREAD_ORDER: BreadId[] = ['white', 'multigrain', 'raisin', 'sourdough'];
/**
* Brands. The same style of bread exists at very different altitudes, and the
* altitude is mechanical, not cosmetic: `quality` scales how patchily the slice
* takes heat and how easily the crumb tears. Cheap bread is a lottery you can
* partially outplay; good bread does what you tell it.
*
* `loaf: true` means it only exists unsliced — the bakery does not pre-slice,
* and that's where the bread knife (and The Best Thing) earn their keep.
*/
export interface BrandDef {
name: string;
/** 0..1 — evenness of crumb, structural integrity, general dignity. */
quality: number;
/** Comes as a whole loaf: you slice it yourself. */
loaf?: boolean;
blurb: string;
}
export const BRANDS: Record<BreadId, BrandDef[]> = {
white: [
{ name: 'WONDERSLICE', quality: 0.25, blurb: 'Every slice identical. None of them good.' },
{ name: 'Crustworthy', quality: 0.8, blurb: 'White bread with references.' },
],
multigrain: [
{ name: 'Seedy Business', quality: 0.55, blurb: 'The seeds are load-bearing.' },
{ name: 'Grain Bill', quality: 0.85, blurb: 'Serious grains, alphabetised.' },
],
raisin: [{ name: 'Sun Chariot', quality: 0.6, blurb: 'The raisins travel in packs.' }],
sourdough: [
{ name: 'Longlife Sour-ish', quality: 0.35, blurb: 'Sourdough the way a photocopy is a painting.' },
{ name: 'the bakery loaf', quality: 1.0, loaf: true, blurb: 'Loaf only. The bakery does not pre-slice. The bakery is right.' },
],
};
export function brandOf(bread: BreadId, name: string): BrandDef {
return BRANDS[bread].find((b) => b.name === name) ?? BRANDS[bread][0];
}