toastsim/src/game/orders.ts
type-two 717ef56150 THE EGGS: day 20 — the crack, the separation, the float test, the beat
The egg dossier lands (atlas §1.5). sim/eggs.ts (pure): a carton of six,
each egg rolling freshness off the carton's age; the FLOAT TEST reads it
out (sinks flat / stands up / FLOATS) for free; the CRACK is a strength
window (soft = nothing, window = clean, hard = shell in the whites, fish
the bits out); SEPARATION passes the yolk shell-to-shell (fast breaks it,
old yolks are weaker — the float test also tells you how gently to pass);
a ROTTEN egg cracked blind ruins the bowl: green puff + a seed-varied
synth FART (audio.fart — pitch/length/wobble per egg, the tremolo loses
its nerve halfway). D dumps a ruined bowl; the dump clears the whites but
NOT the judge's memory (rottenCracked survives — no sin-laundering).
An old carton always has at least one liar in it (deterministic; the
glass finds it — floor, not lottery).

scenes/eggbench.ts: drag-to-glass float test, drag-to-rim + HOLD-SPACE
charge crack, alternate arrows to separate, click shell bits to fish.
judgeEggs: THE EGGS (Count/Shell/Whites) + The Nose; full line bank
('There is a GLASS OF WATER on the bench. It exists for exactly this.').
Day 20 authored (pavlova order) + eggs arm in the procedural rotation
(older cartons + need 4 past day 26).

Verified via REAL input: float-all + dodge + patient passes = 10.0/10;
blind rotten crack = the beat, dump recovers whites, The Nose remembers
= 6.3. Sim bars: crack window, float words, old-yolk weakness, fishing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 01:53:08 +10:00

630 lines
24 KiB
TypeScript

import { BRANDS, type BreadId } from '../sim/bread';
import type { AmountClass, SpreadId } from '../sim/spreads';
import type { ToolId } from '../sim/cutlery';
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;
* M12 adds 'juice' (the ribbed reamer). Zesting, grating and roasting arrive
* with their stations and extend this union.
*/
export type PrepStep =
| {
kind: 'cut';
ingredient: IngredientId;
pattern: CutPattern;
/** The knife the bench hands you. Prep knives don't come from the drawer
* (that trip is for the spread tool); the order supplies them. */
knife?: ToolId;
}
| {
kind: 'juice';
ingredient: IngredientId;
/** Which reamer sits on the bench for this order (progression, like bread
* brands). Day 12 hands you the Pyramid Classic — no seed moat. */
juicer: JuicerId;
/** The cut the halving left, capping the reachable juice. Absent = a clean
* cook's halve (dead-centre), so the whole test is the twist itself. */
halve?: { offset: number; wedge: number };
/** Seeds in this orange (fixed so the pip count is a skill test, not a
* dice roll). Defaults to the orange's own count via the station. */
seeds?: number;
};
/**
* The bruschetta capstone (day 14). Its presence on an order flips the day flow
* from cut→toast→spread to cut→toast→roast→assemble→serve, and arms the
* temperature clock for the perishables it names.
*/
export interface BruschettaSpec {
/** Tomato halves on the oven tray. */
roastHalves: number;
/** Perishables the temp clock tracks: brie wants out early (soft), parmesan
* wants to stay in (cold). */
perishables: IngredientId[];
/** The oven's fuel character (heatsource SKU): 'oven_gas' roasts hot-at-centre
* and uneven, 'oven_fan' laboratory-flat. Omit for the legacy dial-is-heat oven. */
oven?: string;
}
/** The ticket's short line for a prep step: "dice the onion", "juice the orange". */
export function prepAsk(step: PrepStep): string {
const name = step.ingredient.replace(/_/g, ' ');
if (step.kind === 'juice') return `juice the ${name}`;
const p = step.pattern;
switch (p.kind) {
case 'halve':
return `halve the ${name}`;
case 'slices':
return `${p.n} slices of ${name}`;
case 'wedges':
return `${p.n} wedges of ${name}`;
case 'dice':
return `dice the ${name}`;
}
}
export interface Order {
day: number;
bread: BreadId;
spread: SpreadId;
amount: AmountClass;
/** Target mean browning, 0..1. */
browning: number;
browningName: string;
/** The customer specifically does not want char. Some don't care. */
noChar: boolean;
/** What the drawer will make you find. */
tool: ToolId;
/** Which brand of the bread — quality is mechanical, not cosmetic. */
brand: string;
/** Comes as a whole loaf: find a bread knife, slice it yourself. */
fromLoaf?: boolean;
/** How thick they want the slice, when it's a loaf. */
cutClass?: CutClass;
cutBand?: [number, number];
/** 0 = fridge-hard, 1 = soft. The single cruellest dial in the game. */
butterSoftness: number;
/** How the customer says it. */
text: string;
/** Who's asking. */
who: string;
/** Work at the prep bench, when the order has any. */
prep?: PrepStep[];
/** The bruschetta capstone: roast + assemble replace the spread flow. */
bruschetta?: BruschettaSpec;
/** The charcoal grill (M-H6): the food to cook over the coals. Presence sends
* 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 };
/** 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;
/** The rib eye: sear it on the pan, then REST it and cut across the grain.
* `target` is the doneness asked; `grain` is the fibre direction to cut across. */
steak?: { fuel: string; target: 'rare' | 'medium' | 'well'; grain: number };
/** The egg bench: `need` separated eggs from a carton whose `age` sets the
* rotten odds. The float test is on the bench; using it is the skill. */
eggs?: { need: number; cartonAge: number };
}
export const BROWNING_NAMES: [number, string][] = [
[0.18, 'barely warmed through'],
[0.34, 'light'],
[0.5, 'golden'],
[0.66, 'well done'],
[0.8, 'dark'],
];
export function nameBrowning(v: number): string {
let best = BROWNING_NAMES[0];
for (const b of BROWNING_NAMES) if (Math.abs(b[0] - v) < Math.abs(best[0] - v)) best = b;
return best[1];
}
/**
* The handwritten first week. The curve is deliberate: day 1 is butter on white
* with a butter knife and butter that's been out all morning — every dial set to
* "forgiving". By day 7 you're doing a translucent film of MITEY on thick wet
* sourdough with fridge-hard butter... and a steak knife, because the drawer is
* the drawer.
*/
export const DAYS: Order[] = [
{
day: 1,
brand: 'WONDERSLICE',
bread: 'white',
spread: 'butter',
amount: 'normal',
browning: 0.5,
browningName: 'golden',
noChar: true,
tool: 'butter_knife',
butterSoftness: 0.85,
who: 'Deidre',
text: 'Just golden, love. Butter. Normal amount of butter.',
},
{
day: 2,
brand: 'WONDERSLICE',
bread: 'white',
spread: 'butter',
amount: 'thin',
browning: 0.66,
browningName: 'well done',
noChar: true,
tool: 'dinner_knife',
butterSoftness: 0.55,
who: 'Ray',
text: "Well done. Scrape of butter — I said a scrape, not a shovel.",
},
{
day: 3,
brand: 'Seedy Business',
bread: 'multigrain',
spread: 'peanut',
amount: 'thick',
browning: 0.42,
browningName: 'light',
noChar: true,
tool: 'dessert_spoon',
butterSoftness: 0.5,
who: 'a child',
text: 'PEANUT BUTTER. LOTS. Bread barely toasted please.',
},
{
day: 4,
brand: 'Sun Chariot',
bread: 'raisin',
spread: 'butter',
amount: 'normal',
browning: 0.44,
browningName: 'light',
noChar: true,
tool: 'butter_knife',
butterSoftness: 0.3,
who: 'Deidre',
text: "Raisin loaf. Careful — it catches. I'll know if it catches.",
},
{
day: 5,
brand: 'WONDERSLICE',
bread: 'white',
spread: 'mitey',
amount: 'normal',
browning: 0.62,
browningName: 'well done',
noChar: true,
tool: 'teaspoon',
butterSoftness: 0.4,
who: 'Ray',
text: 'MITEY. Proper amount. You know what proper means.',
},
{
day: 6,
brand: 'Longlife Sour-ish',
bread: 'sourdough',
spread: 'crunchy',
amount: 'normal',
browning: 0.72,
browningName: 'dark',
noChar: false,
tool: 'steak_knife',
butterSoftness: 0.25,
who: 'a man in a hurry',
text: "Dark. Sourdough. Crunchy peanut butter — the PROPER kind. Don't care if it catches a bit.",
},
{
day: 7,
brand: 'Longlife Sour-ish',
bread: 'sourdough',
spread: 'mitey',
amount: 'thin',
browning: 0.68,
browningName: 'well done',
noChar: true,
tool: 'steak_knife',
butterSoftness: 0.0,
who: 'the inspector himself',
text: "Thick sourdough, well done, and a *scrape* of MITEY. I want to see the toast through it.",
},
{
day: 8,
brand: 'Crustworthy',
bread: 'white',
spread: 'marmalade',
amount: 'normal',
browning: 0.55,
browningName: 'golden',
noChar: true,
tool: 'spreader',
butterSoftness: 0.5,
who: 'Deidre',
text: 'Marmalade! And mind the rind, love — a piece in every bite, not a pile in one corner.',
},
{
day: 9,
brand: 'the bakery loaf',
bread: 'sourdough',
spread: 'butter',
amount: 'normal',
browning: 0.58,
browningName: 'golden',
noChar: true,
tool: 'butter_knife',
butterSoftness: 0.45,
fromLoaf: true,
cutClass: 'doorstop',
cutBand: CUT_BANDS.doorstop,
who: 'the inspector himself',
text: 'The bakery loaf. Slice it yourself — a DOORSTOP, and I want the faces parallel. Then golden, butter, normal.',
},
{
day: 10,
brand: 'WONDERSLICE',
bread: 'white',
spread: 'butter',
amount: 'normal',
browning: 0.5,
browningName: 'golden',
noChar: true,
tool: 'butter_knife',
butterSoftness: 0.6,
who: 'Deidre',
text: 'Dice me an onion first — small and square, mind the stem — then golden white, butter, normal.',
// The onion is the dice's showcase: layered flesh means a bad cut explodes
// the piece CV. The dinner knife bites clean if you saw instead of crush.
prep: [{ kind: 'cut', ingredient: 'onion', pattern: { kind: 'dice', n: 4 }, knife: 'dinner_knife' }],
},
{
day: 11,
brand: 'Crustworthy',
bread: 'white',
spread: 'butter',
amount: 'thin',
browning: 0.55,
browningName: 'golden',
noChar: true,
tool: 'butter_knife',
butterSoftness: 0.55,
who: 'a regular',
text: 'Six wedges of tomato — even ones — and a scrape of butter on golden white.',
// Tomato = the wet, slippery showcase for wedges: press without sawing and
// it skates and hoses the board; saw and it gives clean radial cuts.
prep: [{ kind: 'cut', ingredient: 'tomato', pattern: { kind: 'wedges', n: 6 }, knife: 'dinner_knife' }],
},
{
day: 12,
brand: 'WONDERSLICE',
bread: 'white',
spread: 'butter',
amount: 'normal',
browning: 0.5,
browningName: 'golden',
noChar: true,
tool: 'butter_knife',
butterSoftness: 0.55,
who: 'Deidre',
text: 'Fresh orange juice with the toast, love — squeeze it properly, mind the pips. Then golden white, butter, normal.',
// The juice detour, first outing. The Pyramid Classic has no seed moat, so
// every pip it frees goes to the glass — the twist is the whole test. Five
// seeds fixed so Pips is a skill row, not a lottery. The halve is left clean
// (dead-centre) — the halve→juice chain is M12's own later order.
prep: [{ kind: 'juice', ingredient: 'orange', juicer: 'pyramid_classic', seeds: 5 }],
},
{
day: 13,
brand: 'WONDERSLICE',
bread: 'white',
spread: 'butter',
amount: 'normal',
browning: 0.5,
browningName: 'golden',
noChar: true,
tool: 'butter_knife',
butterSoftness: 0.5,
who: 'the inspector himself',
text: 'Dice this onion PROPERLY — stop the cut short of the root or it falls to confetti. Sharp and quick, mind your eyes. Then golden white, butter.',
// The technique test. root: true arms the stop-line — the first pass of cuts
// must be RELEASED short of the root; saw through and the layers scatter and
// the Dice row collapses. The Best Thing is the sharpest edge, so squash (and
// therefore sting) stays low and the whole story is restraint at the root.
prep: [{ kind: 'cut', ingredient: 'onion', pattern: { kind: 'dice', n: 4, root: true }, knife: 'the_best_thing' }],
},
{
day: 14,
brand: 'the bakery loaf',
bread: 'sourdough',
// The spread flow is replaced by roast + assemble on this order — the garlic
// rub stands in for the spread — but the Order shape still wants these, and
// they stay harmless defaults the bruschetta path never reads.
spread: 'butter',
amount: 'normal',
browning: 0.55,
browningName: 'golden',
noChar: true,
tool: 'butter_knife',
butterSoftness: 0.5,
fromLoaf: true,
cutClass: 'doorstop',
cutBand: CUT_BANDS.doorstop,
who: 'the inspector himself',
text: 'Bruschetta. Cut the sourdough thick and toast it, then rub it with garlic. Roast the tomatoes till they BLISTER — not sauce. Brie soft, parmesan cold. Top it even and balanced, and serve it before it bends.',
// The capstone: cut the loaf → toast → roast the tomatoes → assemble (rub,
// top, watch the sog) → serve. The temp clock tracks the two cheeses from
// order start; take the brie out early, leave the parmesan in.
bruschetta: { roastHalves: 4, perishables: ['brie', 'parmesan'], oven: 'oven_gas' },
},
{
day: 15,
brand: 'the bakery loaf',
bread: 'white',
// The grill day routes entirely to the outdoor coals — the toast defaults are
// never read, same as the bruschetta order.
spread: 'butter',
amount: 'normal',
browning: 0.55,
browningName: 'golden',
noChar: true,
tool: 'butter_knife',
butterSoftness: 0.5,
who: 'the bloke from two doors down',
text: "Chuck these on the barbie, love. Bank the coals hot one side, cool the other — sear 'em then move 'em off before they flare. A proper char, not a cremation.",
grill: ['steak', 'snag', 'corn'],
},
{
day: 16,
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 chef, on her day off',
text: 'Pan-sear the mushrooms on the gas. Butter in first — wait for the FOAM, not the smoke. Land them in the foam, brown both faces, baste them in the noisette. Burn the butter and I will know.',
pan: { food: 'button_mushroom', fuel: 'gas' },
},
{
day: 17,
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 delivery, at 6am',
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 },
},
{
day: 19,
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 man at table nine',
text: 'Rib eye, medium. Sear it in the pan, both faces, foam the butter. Then REST it — do not touch it, do not cut it, let it settle. And when you slice, go ACROSS the grain, clean. Cut it early and I will see it bleed.',
steak: { fuel: 'gas', target: 'medium', grain: 0.35 },
},
{
day: 20,
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 pavlova order',
text: "Three eggs, separated, for the pav. No shell in my whites, no yolk in my whites — and the carton's been sitting a bit, so FLOAT them first. Crack one rotten egg into that bowl and I will hear it from the front room.",
eggs: { need: 3, cartonAge: 0.55 },
},
];
/** Days beyond the script — keep going, with everything cranked. */
/**
* Days beyond the script use the WHOLE kitchen. Toast stays the bread and
* butter (a third of days); the other two thirds rotate through every station
* the campaign taught, each with its own knobs turned by the day number.
* Deterministic per day (the caller seeds by day) — a reload re-deals the SAME
* order, so there's no reroll-scumming your way to an easy one.
*/
export function proceduralOrder(day: number, rand: () => number): Order {
const roll = rand();
if (roll < 0.32) return proceduralToast(day, rand);
if (roll < 0.44) return proceduralPrep(day, rand);
if (roll < 0.53) return proceduralJuice(day, rand);
if (roll < 0.64) return proceduralGrill(day, rand);
if (roll < 0.75) return proceduralPan(day, rand);
if (roll < 0.85) return proceduralSteak(day, rand);
if (roll < 0.93) return proceduralEggs(day, rand);
return proceduralFridge(day, rand);
}
const pickOf = <T,>(rand: () => number, a: T[]): T => a[Math.floor(rand() * a.length)];
/** The toast fields every Order carries; station days never read most of them. */
function baseOrder(day: number, who: string, text: string): Order {
return {
day,
bread: 'white',
brand: 'WONDERSLICE',
spread: 'butter',
amount: 'normal',
browning: 0.55,
browningName: 'golden',
noChar: true,
tool: 'butter_knife',
butterSoftness: 0.5,
who,
text,
};
}
function proceduralPrep(day: number, rand: () => number): Order {
const cuttables: IngredientId[] = ['tomato', 'onion', 'cucumber', 'bell_pepper', 'button_mushroom', 'portobello', 'king_oyster'];
const ingredient = pickOf(rand, cuttables);
const kinds = ['slices', 'wedges', 'dice'] as const;
const kind = pickOf(rand, [...kinds]);
// The knife pool loosens with the days — sometimes you get the good one.
const knife: ToolId = rand() < 0.2 ? 'the_best_thing' : 'dinner_knife';
const n = kind === 'dice' ? 3 + Math.floor(rand() * 2) : 4 + Math.floor(rand() * (kind === 'wedges' ? 5 : 3));
// Past day 21, an onion dice is the TECHNIQUE dice — stop at the root.
const root = kind === 'dice' && ingredient === 'onion' && day > 21;
const pattern: CutPattern = kind === 'dice' ? { kind, n, ...(root ? { root: true } : {}) } : { kind, n };
const o = baseOrder(
day,
pickOf(rand, ['a regular', 'the lunch rush', 'Deidre', 'the caterer next door']),
'',
);
o.prep = [{ kind: 'cut', ingredient, pattern, knife }];
o.text = `${prepAsk(o.prep[0]).replace(/^./, (c) => c.toUpperCase())} — even ones, tidy bench. Then ${o.browningName} white, butter.`;
return o;
}
function proceduralJuice(day: number, rand: () => number): Order {
// Better reamers show up as the days go on — and more pips with them.
const juicer: JuicerId = day > 25 && rand() < 0.4 ? 'juicernaut' : day > 21 && rand() < 0.5 ? 'ribbed_deluxe' : 'pyramid_classic';
const seeds = 3 + Math.floor(rand() * 4);
const o = baseOrder(day, pickOf(rand, ['Deidre', 'a thirsty tradie', 'the morning walker']), '');
o.prep = [{ kind: 'juice', ingredient: 'orange', juicer, seeds }];
o.text = `Fresh orange juice with the toast — squeeze it properly, mind the pips. Then ${o.browningName} white, butter.`;
return o;
}
function proceduralGrill(day: number, rand: () => number): Order {
const pool = ['steak', 'snag', 'corn', 'mushroom'];
const count = day > 24 ? 4 : 3;
const foods: string[] = [];
for (let i = 0; i < count; i++) foods.push(pool[Math.floor(rand() * pool.length)]);
const o = baseOrder(day, pickOf(rand, ['the bloke from two doors down', 'half the street', 'the cricket club']), '');
o.grill = foods;
o.text = `Barbie's on — ${count} bits. Bank the coals hot one side, sear 'em, then off to the lee before they flare. A proper char, not a cremation.`;
return o;
}
function proceduralPan(day: number, rand: () => number): Order {
const foods = ['button_mushroom', 'portobello', 'bell_pepper'];
// Electric arrives day 22 (the coil that won't let go); induction day 26.
const fuel = day > 25 && rand() < 0.35 ? 'induction' : day > 21 && rand() < 0.45 ? 'electric' : 'gas';
const food = pickOf(rand, foods);
const o = baseOrder(day, pickOf(rand, ['the chef, on her day off', 'a food blogger', 'Ray']), '');
o.pan = { food, fuel };
const fuelLine = fuel === 'electric' ? 'On the electric — mind the coil, it remembers.' : fuel === 'induction' ? 'On the induction — silent, exact, no excuses.' : 'On the gas.';
o.text = `Pan-sear the ${food.replace(/_/g, ' ')}s. ${fuelLine} Butter in first — wait for the FOAM. Both faces. Burn the butter and I will know.`;
return o;
}
function proceduralSteak(day: number, rand: () => number): Order {
const target = pickOf(rand, ['rare', 'medium', 'medium', 'well'] as const);
const grain = rand() * Math.PI;
const fuel = day > 23 && rand() < 0.4 ? 'electric' : 'gas';
const o = baseOrder(day, pickOf(rand, ['the man at table nine', 'a birthday dinner', 'the butcher, testing you']), '');
o.steak = { fuel, target, grain };
o.text = `Rib eye, ${target}. Sear it, then REST it — do not cut it early, I will see it bleed. And slice ACROSS the grain, clean.`;
return o;
}
function proceduralEggs(day: number, rand: () => number): Order {
// The carton gets older as the days climb — more rotten odds, weaker yolks.
const cartonAge = Math.min(0.85, 0.35 + (day - 19) * 0.02 + rand() * 0.25);
const need = day > 26 ? 4 : 3;
const o = baseOrder(day, pickOf(rand, ['the pavlova order', 'a meringue emergency', 'the CWA stall']), '');
o.eggs = { need, cartonAge };
o.text = `${need} eggs, separated — no shell, no yolk in the whites. The carton's ${cartonAge > 0.6 ? 'been sitting a WHILE' : 'not the freshest'}, so float them first.`;
return o;
}
function proceduralFridge(day: number, rand: () => number): Order {
const o = baseOrder(day, 'the delivery, at 6am', '');
void rand();
o.fridge = true;
o.text = 'The order came in. Raw meat down low, perishables in the cold at the back, and don\'t cram one shelf. Three days till the inspector looks.';
return o;
}
function proceduralToast(day: number, rand: () => number): Order {
const breads: BreadId[] = ['white', 'multigrain', 'raisin', 'sourdough'];
const spreads: SpreadId[] = ['butter', 'peanut', 'crunchy', 'mitey', 'marmalade'];
const amounts: AmountClass[] = ['thin', 'normal', 'thick'];
const tools: ToolId[] = [
'butter_knife',
'dinner_knife',
'steak_knife',
'spreader',
'dinner_fork',
'dessert_fork',
'teaspoon',
'dessert_spoon',
'soup_spoon',
];
const pick = <T>(a: T[]): T => a[Math.floor(rand() * a.length)];
const spread = pick(spreads);
const bread = pick(breads);
// Brand: the good stuff shows up more as the days go on — and bakery
// sourdough is loaf-only, which means the bread knife, which means the drawer.
const pool = BRANDS[bread];
const wantGood = rand() < Math.min(0.85, 0.25 + (day - 9) * 0.06);
const sorted = [...pool].sort((a, b) => a.quality - b.quality);
const brand = wantGood ? sorted[sorted.length - 1] : sorted[0];
const cuts: CutClass[] = ['thin cut', 'regular cut', 'doorstop'];
const cutClass = pick(cuts);
const amount: AmountClass =
spread === 'mitey' ? (rand() < 0.75 ? 'thin' : 'normal') : pick(amounts);
const browning = 0.3 + rand() * 0.5;
return {
day,
bread,
brand: brand.name,
fromLoaf: !!brand.loaf,
cutClass: brand.loaf ? cutClass : undefined,
cutBand: brand.loaf ? CUT_BANDS[cutClass] : undefined,
spread,
amount,
browning,
browningName: nameBrowning(browning),
noChar: rand() < 0.8,
tool: pick(tools),
// It only gets colder from here.
butterSoftness: Math.max(0, 0.4 - (day - 9) * 0.08) * rand(),
who: pick(['Deidre', 'Ray', 'a regular', 'someone new', 'the inspector himself']),
text: '',
};
}