Three new doctrines: IRREVERSIBILITY (seasoning is a one-way vector
walk — taste to see, dilute to compromise, never subtract), INFORMATION
TOOLS (thermometer/taste-spoon reveal instead of change; knowing costs
time, the judge respects a confident blind serve), SPEC CUTS (brunoise/
julienne/chiffonade/tourne as data on the existing cutting sim).
P6 water & oil (the fryer's menace), P7 the spice rack (curry capstone,
chili's 8s lag trap, bloom/toast/grind), P8 the knife roll (knife SET
as data, mandoline, melon baller, PREP DRILLS score-attack mode), P9
directed heat (the blowtorch paints a browning Field; creme brulee ends
in the judge's spoon-tap CRACK test), P10 cold & time (cures that span
real days, the churn, granita interrupts), P11 the banquet (fork-anchor
carving, three plates landing hot at once, credits).
Seeds 400-629 reserved per phase, same-seed families for prop sets.
Kill list live: microwave is a cameo, wellington waits, sous vide is a
joke prop. Everything rides the bones — new poetry on proven physics.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
P2 cut from MASTERPLAN for an Opus 4.8 session to execute cold:
M16 salt (the dose-curve bone, two salts, the timing matrix and its
named sin), M17 the pan (butter state machine, sear Field, flip, baste
— mushrooms and bell pepper as first customers), M18 rib eye (season/
cook/rest/cut/pair — the early-cut juice flood is the lesson, no UI
timer, the steak's slump is the clock), M19 the steak day. Working
agreements + every trap this session paid for (assetUrl subpath, born-
hidden panels, stale-harness guard, m4pro retries, worktree hygiene).
Asset batch seeds 180-209. Ends with: deploy, then cut Brief 4 from P3.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Absolute '/assets/...' paths 404 under the /toastsim/ subpath (and the
root /assets/ URL is another game's — returns 200 with wrong content,
the classic partly.party trap). assetUrl() prefixes BASE_URL inside
loadProp + the four direct loads (bench/backdrop textures, title art,
judge portraits). Verified live: JS application/javascript, toaster.glb
4.5MB binary, title_art image/png, title screen renders.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The multi-station order, on the proven bones. Cut the sourdough (M10) →
toast (M1) → roast the tomatoes (toaster generalized) → assemble (garlic rub,
topping point-set, sog clock) → serve. Judge groups The Bread / The Topping /
The Bench. Planning is the mechanic: the temperature clock drifts the two
cheeses on real service time.
What I built on top of the partial work (sims + judge criteria + lines were
already in place and good — kept them):
- Temperature clock WIRED (sim/tempclock.ts): TempClock armed on bruschetta
days, steps every tick while timing; a per-perishable fridge/bench toggle UI
(game.showTempToggle) that reads the judge's own readiness word live; a
toggle costs 8s service (reopening the fridge has teeth). Ticket hints:
"get the brie out now", "leave the parmesan in".
- Roast tomatoes WIRED: OvenView routed after the toast lands on bruschetta
orders (kitchen.onPopped branch → enterOven). Browning Field reused, blister
past 0.6, collapse to sauce past 0.9. oven_tray.glb is absent → loadProp
catch-fallback to the procedural tray (graceful, as designed).
- Assembly scene BUILT (scenes/assembly.ts, new): own toasted garlic-rubbed
slice; G rubs the clove (one-swipe coverage), 1/2/3 drop tomato/cheese/basil
as a point set; ENTER serves. Distribution scored by Clark-Evans (the rind
code, fed the topping list); balance by mass-per-area band.
- Sog clock: rate x wet-mass x moisture / thickness — the doorstop resists,
the thin slice surrenders; the judge presses a finger ("It bends. Toast
should not bend.").
- Day 14 order added; enterOven/enterAssembly/serveBruschetta follow the exact
runNextPrep/enterPrep/enterJuice machinery. Scorecard grouped
bread/topping/bench via judge()'s existing group path.
- Harness (src/dev.ts): t.roast(s,power), t.rub(passes), t.assemble({...}),
t.sogWait(s), t.tempToggle(id), and t.bruschetta() happy path returning the
full grouped stats.
Fixes to the partial work: judge.ts shadowed the `order` param with a local
`const order` (rename → groupRank; would not typecheck); oven.ts material
opacity access typed as Material|Material[] (cast once).
Tuning (measured headless — the pure sims run without three.js):
- assembly SOG_RATE 0.006 → 0.004: doorstop (0.31) under a normal wet load
(5 tomato ~2.1) served promptly (8s) stays CRISP (sog 0.21); dawdled 30s goes
soft (0.77); a thin slice (0.12) bends inside ~15s. The bakery loaf matters.
- assembly MASS_BAND [1.1,2.1] → [2.5,4.5]: the old band scored a normal build
(5t+4c+3b = 3.82 total mass) as an off-band 0; the band now sits around a
proper generous topping. Sparse ~1.3 reads bare, a heap ~6.6 slides off.
- roasting RATE 0.042 kept; comment corrected to the measured window (power 6:
stalls wet to ~26s, blisters 28-36s, collapses past ~40s).
- brie softensInSec 80 (from partial work) kept: out at order start, spreadable
by assembly.
VERIFY (dev build; run in the console):
t.start(); t.bruschetta()
Expected (headless sim + live path):
roast: {mean ~0.70, blister ~1.0, collapse 0, score ~1.0}
distribution: ~1.0 (Clark-Evans R ~1.25, evenly strewn)
balance: {mass ~3.82 in band [2.5,4.5], score ~1.0}
sog: {value ~0.21, word "crisp", score ~0.99}
rub: {coverage ~0.35-0.5, score ~1.0}
temp: {score 1.0 (brie taken out, parmesan left in)}
bench: ~1.0 (clean roast, no slump)
grade A/S, total ~9
Sub-mechanic checks (after t.bruschetta up to the oven, or standalone):
t.roast(26,6) → mean ~0.56, blister ~0 ("under-roasted")
t.roast(42,6) → collapse ~0.69 ("collapsing to sauce")
skip t.tempToggle('brie') → Temperature 0, worst "brie fridge-hard"
thin slice + t.sogWait(15) → sog ~1.0, "it bends"
typecheck clean (tsc --noEmit), vite build clean. node_modules is a symlink to
the main checkout and is deliberately NOT committed (dir-only .gitignore rule
misses the symlink; staged the 12 source files explicitly).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
It was born visible — a day starting at a station (prep/juice/grate)
never passes through kitchen.enter()/exit(), so the BROWNING panel sat
over every station HUD. Hidden at construction like every other scene;
verified hidden at bench, shown after handoff, hidden at juice.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Dice show the criss-cross (drawn 1.6x true size — honest proportions
vanish against the board at game camera distance); wedges explode 0.28
from center so gaps and uneven angles read. Slices keep the fan. Plus
scripts/deploy.sh (partly.party/toastsim, cratewars pattern) — pending
an ssh permission grant to run the final docker cp step.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Day 12: juice order routed end-to-end (72.8%, 4 pips, THE JUICE beside
THE TOAST on one scorecard). Day 13: the stop-line — proper dice stops
at 0.846 and scores 1.0; through-the-root scores 0.0 at identical CV.
Sting 0.098 quick → 1.0 after 10s idle; the screen blurs to genuinely
unreadable (screenshot-verified). Juicer GLBs landed mid-review.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Stage 2 Lane B. Two jobs, both riding the existing prep-bench machinery
(prepQueue drained through enterPrep/runNextPrep before enterToastFlow, results
folded into the judged scorecard) — extended, not rebuilt.
JOB 1 — the juice station is a real day now.
- orders.ts: PrepStep is a discriminated union; new 'juice' kind carries the
juicer tier + optional halve + fixed seed count. Day 12 (Deidre): "fresh
orange juice with the toast" — juicer comes from the ORDER (Pyramid Classic,
the starter, no seed moat), the halve is left clean (dead-centre) so the twist
is the whole test; the halve->juice chain stays M12's own later order. seeds
fixed at 5 so Pips is a skill row, not a lottery.
- game.ts: the Game owns a JuiceView (like PrepView). runNextPrep branches on
step kind; enterJuice mirrors enterPrep. The glass is scored on its own
criteria (juiceCriteria) folded into judge() via a new `extra` param; its
spray joins The Bench through the same prepResult path a cut order uses.
- lines.ts: JUICE_LINES merged into the verdict BANK; juicerRecognitionLine
wired off order.prep (the Juicernaut announces itself before the verdict).
- dev.ts: t.juice(revs, press) drives the REAL routed game.juiceView (not the
__tj sandbox); follow with t.handoff() to serve the glass.
JOB 2 — M14 onions, the technique test (brief section 6).
- The stop-line rides the existing cut sim with one additive hook: on the
onion dice's root-ward first pass, RELEASING the blade mid-saw commits a cut
that stops short of the root (liftKnife now commits + returns its frame).
finishCut is factored so a stopped cut and a through cut share the path.
Saw all the way (progress hits 1) and the root severs -> the layers scatter.
Gated behind pattern { kind:'dice', root:true } so day 10's plain dice
(M11-verified) is untouched. That is the "ending a cut deliberately short"
feel — restraint one cut at a time, not a menu choice.
- Sting clock: stingStep raises sting with open cut-surface x time x how much
you crushed rather than sliced (blunt/squashy = tear gas). Rendered as a
cheap full-screen DOM overlay (radial vignette + backdrop blur) that grows
with sting, plus an "eyes fine/watering/streaming" readout and a green
stop-line marker on the onion.
- Scored as two new rows (judging.ts): The Dice (stem discipline, w1.3) and
The Eyes (sting endured, w0.7). The Pieces (CV) stays the criss-cross
spacing, so the two skills are told apart. Lines for 'dice' and 'sting'.
- Day 13 (the inspector): dice the onion PROPERLY with The Best Thing.
- dev.ts: t.chop gains stopAt; t.diceOnion({stopAt|through}) drives both.
Constants and why:
- STOP_MIN 0.15 — a release shallower than this is a fumble, not a stop.
- STING_RATE 0.02 — a sharp, quick clean dice finishes ~0.05 (clear-eyed);
the same onion left open 10s floods to 1.0. Sharp+fast wins, dawdling blinds.
- stemDiscipline band: full marks 0.55..0.9 depth, graze 0.9..0.97, through
>=0.97 = confetti; undercut <0.55 = joined chunks.
- t.diceOnion default stopAt 0.8 -> commits ~0.85, level with the 0.82 marker.
Verified headless (deterministic; sim is pure):
Day 12 juice, t.day(12); t.juice(3, 0.7):
extracted 0.728, theoretical 1.0, yieldOfReachable 0.728, pips 4,
seedsCaught 0 (Classic has no moat -> serve the pips or fish them).
Criteria: The Juice score 0.631 (w1.3), Pips 0.200 (w1.1). t.handoff()
serves the glass; the toast half then runs as any non-loaf day, and the
scorecard shows THE PREP (The Juice, Pips, The Bench) beside THE TOAST/SPREAD.
Day 13 onion, t.day(13); t.diceOnion(): // proper dice, stop short
16 pieces, CV 0, rootThrough 0, stopDepths [0.85,0.85,0.85],
The Dice score 1.000, sting 0.052 -> The Eyes 0.939 ("clear-eyed").
t.day(13); t.diceOnion({ through: true }): // cut through the root
rootThrough 3, stopDepths [1,1,1], The Dice score 0.000 (confetti line
fires), CV still 0 -> the disaster reads on The Dice, not a lying CV.
Sting accumulation: after a proper dice, t.run(600) (10s idle) ->
sting 0.052 -> 1.000 ("blind"), The Eyes 0.939 -> 0.000. The cut onion
stings while it sits, exactly as it should.
Plain dice unaffected: onion { kind:'dice', n:4 } (no root) -> 16 pieces,
CV 0, rootThrough 0, phase done — M11's day 10 is byte-for-byte the same test.
typecheck clean, npm run build clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The prep bench's third station, built on the M12 juice lane's exact shape:
pure sim (sim/grating.ts, no three.js), a mirroring scene (scenes/grate.ts),
and a separate harness on window.__tg (dev-grate.ts). One additive DEV import
line in main.ts. Judge integration is exported and documented in-file
(grateResult / grateCriteria / GRATE_LINES / knuckleLine) — NOT wired into
game/* (that lane is owned elsewhere this stage).
The verb is a STROKE down the box-grater face: length × pressure → shred mass.
Long confident strokes land on the pile; short scrubby ones shear off the
board (mess). Two customers on one prop — cheese (parmesan) and zest (orange):
- Cheese shrinks the block as you take mass off it. As it gets SMALL your
knuckles near the teeth: knuckle-graze risk = pressure × stroke-length ×
block-proximity, so it only bites in the tense last third. Parmesan grates
clean IF cold; it softens on the bench (temp clock, warmOnBench) and soft
cheese SMEARS — output drops, the teeth CLOG (clearClog costs service time).
The butter inversion: butter wants warm, cheese wants cold.
- Zest: each of 8 orange regions has its own depth; grate one past the pith
line (ingredient gratable.pithDepth 0.35) and it sheds white pith into the
pile instead of zest. Rotate (scroll) to a fresh region — the skill is
rotation discipline. The pith idea is re-derived from the ingredient's own
pithDepth; grating deliberately does NOT import juicing (independent lanes).
Judge: The Gratings/The Zest (amount band, w1.0, docked hard per knuckle),
The Pith (zest, w1.1), The Grate (cheese clog+smear, w0.9). Bench scored
separately via result.bench, same path prep/juice use. 8 bad + 3 good lines
per criterion, plus the knuckle opener.
Tuning (first principles, then measured headless on the pure sim):
- RASP 0.12, gratable.yield 0.9 → a long stroke (len 0.9 × press 0.7) sheds
~0.068/stroke; ~7-8 strokes make an in-band pile.
- KNUCKLE_SAFE_BLOCK 0.35: proximity is zero above it, ramps to 1 at empty.
KNUCKLE_GAIN 0.6. Oranges never shrink, so zest never grazes (its danger is
pith, not knuckles) — by design.
- ZEST_DEPTH_GAIN 0.18 → a region hits the 0.35 pith line in ~3 hard strokes,
so rotate by then. ZEST_MASS_FRAC 0.4 (zest is a whisper vs a cheese pile).
- SPRAY_GAIN 0.9 on (1-length)^2 — spray climbs steeply as strokes shorten.
- SMEAR_LOSS 0.8, CLOG_GAIN 0.35 × grater.clogProne (box 0.7 / microplane 1.3).
Verified: tsc --noEmit clean, npm run build clean. Sim driven headless with
the scene's own seed (0x6ac0f). __tg calls and expected numbers:
__tg.enter({ingredient:'parmesan'}); __tg.stroke(8,0.7,0.9); __tg.stats()
→ gratings≈0.61 "a good pile", blockLeft≈0.38, knuckleHits 0, clog 0;
The Gratings 1.0, The Grate 1.0.
__tg.enter({ingredient:'parmesan'}); __tg.stroke(12,0.6,0.25); __tg.stats()
→ scrub: gratings≈0.10 "a dusting", stray≈0.10 off the board, block barely
touched (0.81); The Gratings 0.24.
__tg.enter({ingredient:'parmesan',softness:1}); __tg.stroke(8,0.8,0.9); __tg.stats()
→ soft: clog 1.0, smear≈0.61, gratings≈0.05; The Grate 0.0 "clogged solid".
__tg.enter({ingredient:'parmesan',block:0.25}); __tg.stroke(8,0.9,0.95); __tg.stats()
→ danger: knuckleHits 4, blockLeft≈0.02, gratings≈0.24; The Gratings 0.008.
__tg.enter({ingredient:'parmesan',block:0.25}); __tg.stroke(8,0.9,0.35); __tg.stats()
→ ease off: knuckleHits 0, gratings≈0.22; The Gratings 0.54. The lesson pays.
__tg.enter({ingredient:'orange'}); __tg.stroke(5,0.6,0.8) with rotate every 2
→ gratings≈0.12 in band, pithFrac 0; The Zest 1.0, The Pith 1.0.
__tg.enter({ingredient:'orange'}); __tg.stroke(9,0.9,0.9); __tg.stats() // no rotation
→ pithFrac≈0.74 "74% pith"; The Pith 0.0.
(Scene HUD, pile mound, red knuckle flash, and scroll-rotate are visual — the
reviewer drives __tg and screenshots; the numbers above come from the pure sim
the scene feeds.)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Juicer verified through __tj: Pyramid 72.8%/4 pips, Juicernaut 100%/all
caught, damaged halve caps at theoretical 0.6, mash 8.5% + pulp,
reversals 77.1% vs 72.8% equal-sweep. Station renders (cone, moat,
filling glass, live HUD). Not yet routed into a day — integration point
documented in sim/juicing.ts.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The juice station, on the same bones. Press-and-twist reuses the jar's swirl
detector: signed angular sweep of the pointer around the reamer becomes juice
one frame at a time, so twist rate IS juice rate (the feel that had to be right,
brief §9). Dwell is the downforce — holding the half down ramps press, and the
twist while you hold is what juices.
NEW src/sim/juicing.ts (pure, no three — mirrors cutting.ts):
- theoreticalYield(halve) = base·(1−2|offset|)·(1−0.5·wedge): the halve sets the
reachable ceiling, so a butchered 60/40 caps you at ~70% and the bar can't lie.
- juiceTwist(s, dTheta, press, dt, rand): pulse = sweep·press·ream, capped by
what's reachable; twist REVERSALS bite a fresh face (a small bonus, real reamer
physics); pressing WITHOUT twisting mashes → pulp + bench spray + no juice;
pressing too HARD shoves seeds past the moat; grinding a DRY half reams the pith
→ bitterness that no yield buys back.
- Three tiers as tools (like knives differ): Pyramid Classic (no moat, ream 0.92),
Ribbed Deluxe (catches ~80%, +4%), The Juicernaut (catches all, +10%, ream 1.32).
- Single game integration point, documented in-file: juiceResult(session) → the
number-bag; juiceCriteria(result) → The Juice (yield band − bitterness − murk,
w1.3) + Pips (seed count, w1.1); JUICE_LINES (5 bad / 3 good each) +
juicerRecognitionLine(). Bench rides the existing benchCriterion via result.bench.
NEW src/scenes/juice.ts: JuiceView mirroring prep.ts — procedural ribbed cone in
a moat dish, glass with a rising juice column, orange half, mess field for spray,
loadProp GLB fallbacks (juicer_pyramid/juice_glass). Live HUD reads the judge's
own numbers: "62% of what this orange had", a ceiling marker the fill can't pass,
pips (click to fish out), bench word, pith warning.
NEW src/dev-juice.ts: SEPARATE installer (no collision with dev.ts) — owns its own
JuiceView, drives the REAL input path (pointer circles for the twist, press
injected like dev.ts injects kitchen.power). Exposes window.__tj. main.ts: one
additive DEV-only import line.
VERIFY (reviewer, ~3 console calls after the dev server loads):
__tj.enter({ seeds: 5 }); __tj.twist(3, 0.7); __tj.stats()
→ yieldOfOrange ~0.73, pips 4 (Pyramid lets seeds through), bench dirtied
__tj.enter({ juicer: 'juicernaut', seeds: 5 }); __tj.twist(4, 0.8); __tj.stats()
→ yieldOfReachable ~1.0, pips 0 (catches all), bitterness rises if you grind on
__tj.enter({ offset: 0.2 }); __tj.twist(6, 0.8); __tj.stats()
→ theoretical ~0.60, yieldOfOrange caps at ~0.60 (the butchered halve)
__tj.enter(); __tj.mash(1.0, 0.9); __tj.stats()
→ extracted 0, pulp/spray > 0, pips forced out (lean-without-twist = a mash)
__tj.enter(); __tj.wristle(6, 0.7); __tj.stats() vs __tj.twist(3, 0.7)
→ wristle ~0.77 vs twist ~0.73 at equal sweep: reversals pay off
Measured (standalone port of the sim, same constants):
pyramid 3rev@0.7 → 0.728 | @0.9 → 0.936 | 5rev@0.85 → 1.0 (bitter 0.38, over-reamed)
deluxe 4rev@0.8 → caught 3/5, 2 pips | juicernaut → caught 5/5, 0 pips
butcher off .2 → capped 0.60 | mash 1s@0.9 → ext 0, pulp 0.135, 3 pips
tsc --noEmit clean; npm run build clean. Not routed into game.ts yet (game lane's
job) — the harness owns the view so the station is fully drivable now.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Order.prep is no longer decorative. beginDay() now drains an order's prep
queue through the bench BEFORE the toaster, folding each PrepResult into the
judged day, then falls through to the existing toast flow (loaf knife-trip or
straight to the kitchen). The bench→kitchen ENTER handoff is the same verified
door; both the standalone harness path (t.prep) and the day flow go through one
enterPrep().
What changed:
- orders.ts: PrepStep gains an optional `knife` (prep knives are supplied by
the order, not fished from the drawer — that trip is for the spread tool).
prepAsk() words a step for the ticket. Two new scripted days:
day 10 — dice an onion (dinner knife), then golden white + butter.
day 11 — six wedges of tomato (dinner knife), then a scrape on golden white.
The onion showcases dice (layered flesh punishes a bad cut); the wet tomato
showcases wedges (press-without-saw skates and hoses the board).
- game.ts: prepQueue drained by runNextPrep(); enterToastFlow() extracted from
beginDay(); startPrep() now delegates to enterPrep(); mergePrep() folds a
second step honestly (worst CV, summed mess, all pieces) though every shipped
order is single-step. Ticket renders a warn-chip per prep step.
- dev.ts: t.wedges({n,dy,wobble}) and t.dice({n,dy,wobble}) drive a whole
pattern deterministically off the existing t.chop() — the dice's two rotated
passes are just 2·(n-1) chops (the sim banks pass A and rotates the board
itself). t.handoff() taps the ENTER the bench waits on. Both return prepStats.
Measured (pure-sim, formulas from cutting.ts, positions the harness uses):
- wedges n=6, cuts at [-0.5,-0.167,0.167] → 6 wedges, CV 0.0000 (even).
- dice n=4, cuts [-0.25,0,0.25] both passes → 16 cells, CV 0.0000 (even).
- wedges with wobbled spacing [-0.5,-0.35,0.28] → CV 0.6351 ("a lottery").
CV 0 for even placement confirms the pieces criterion is a real skill test on
both new patterns; wobble/dy in the helpers exercises the failure end.
Verify (dev build, window.__t):
t.start(); t.day(10); t.dice({n:4}); t.prepStats() // 16 pieces, low CV, bench
t.handoff(); // lands in the kitchen
t.toast(9); t.pick('butter_knife'); t.spread(); t.kitchen.serve()
// scorecard groups THE PREP (The Pieces / The Bench) + THE TOAST / SPREAD
t.start(); t.day(11); t.wedges({n:6}); t.prepStats() // 6 pieces, low CV
Lottery/mess check: t.day(10); t.dice({n:4, dy:0.35, wobble:0.06}) → high CV,
messy bench, "a crime scene".
typecheck clean, vite build clean. Untouched: sim/juicing.ts, scenes/prep.ts,
gen-assets.sh, MASTERPLAN.md.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Salt & butter as cross-cutting foundation systems (salt-type × timing
matrix, butter state machine closing the whip→butter loop), P2 FIRE
(steak: season/cook/rest/cut/pair), P3 eggs & air (whisk shear, poach
vortex, two-layer set field), P4 the pass (composition, swoosh, dosing
danger), P5 service. Build model: parallel opus agents in worktree
lanes, reviewer holds the browser, harness numbers gate every merge.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- bell_pepper: waxy slick skin (2 slips pressed vs tomato's 2 at higher
displacement), 60-seed core — cutting it scatters seeds, bench reads
'a crime scene' with zero extra code. Hollowness ponytail-deferred.
- Brief §7.5: capers+wasabi as the shared dosing-danger class, the
mustard family on the jar/drawer system, rib eye as its own milestone
(cook/rest/cut-across-grain/mustard pairing), baked stuffed potatoes
(lid cut, scoop-as-inverse-spread, restuff). Each rides existing bones.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- button_mushroom / portobello / king_oyster in the ingredient cast:
slippery skins (0.55-0.8, the tomato treatment), soft flesh, portobello
gills layered and weeping; new 'mushroom' cap-and-stem silhouette.
- Verified via harness: dinner knife pressed on a button skates (1 slip,
cut lands 0.157 off-aim; tomato baseline 2 slips / 0.218), The Best
Thing bites clean through under the same press, sawing properly cuts
center with zero slips.
- gen-assets.sh: find MODELBEAST at ~/MODELBEAST, token from
backnforth/.env, MB_HOST to the m3ultra queue; every mb stage retries
10x with 60s pause because m4pro's venvs are gone (ponytail-marked,
remove when m4pro is healed).
- launch.json: port 5273 (5173 taken by another session's server).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Code-complete and verified headless; NOT wired into a day yet (see brief §0.5).
The core, all reusing the M0-M10 bones:
- sim/ingredients.ts: the cast (tomato/orange/onion/garlic/parmesan/brie/
cucumber/avocado/roast_tomato) with shape, skin slip, flesh moisture+layers,
seeds, sting, temperature needs, gratability.
- sim/cutting.ts: CutSession = ingredient + knife + pattern (halve/slices/
wedges/dice), generalizing the M10 slicing sim. New physics: slip (press a
flat blade on slippery skin and it skates, the aim jumps, juice sprays) and
juice (moisture x speed, so deliberate leaks and frantic hoses).
- sim/mess.ts: the bench is judged. Field over the board UV + a solids list;
the wipe is the butter brush in reverse and mass-conserving, so one pass
smears and the second cleans.
- scenes/prep.ts: aim/bite/saw, deepening seams, pieces fan out, juice stain
texture, instanced solids, live 'bench: clean/smeared/a crime scene'.
- judging.ts: The Pieces (CV, w1.3) + The Bench (w0.9); clarkEvans extracted
for M15's toppings; criteria grouped by station and the card grouped with it.
- lines.ts: 8 lines each, in his voice.
Measured, not asserted:
- Even deliberate slices -> CV ~0, bench mass 0.51.
- Sloppy + frantic (0.3 u/frame) -> CV 0.72, mass 18.8, 16 solids, 'a crime scene'.
- Dinner knife pressed on a tomato without sawing -> skates, aim jumps 0 -> 0.12.
The Best Thing under the same press bites in 7 frames, 0 slips.
- Wipe two passes -> mass 0.51 -> 0.09.
Tuning found by playing: slice CV runs over piece WIDTHS not volumes (round
ingredients make end slices thin by geometry — a perfect cut scored 0.40, which
is a geometry lesson, not a skill test); juice coefficient 0.5 -> 0.1 (the first
tomato flooded the board before the second cut).
Assets: gen_3d retries once per stage, bakery seeds bumped, prep cast queued.
2D landed (bruschetta_hero, wonderslice_bag). 3D is BLOCKED: every bg_remove
fails because m4pro's disk is 100% full. Procedural stand-ins carry the bench.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Played end-to-end via the extended harness (t.start/day/pick/slice):
- Day 9 knife trip: bread_knife dragged above the rim, 0.45s dwell commits.
- Clean doorstop (bread knife, 0.05 u/frame): 5 strokes, wedge 0, squash 0.
STROKE_GAIN 0.16 -> 0.10 to hit the 4-6 deliberate-stroke feel.
- The Best Thing: 3 strokes, and the same wobble input that wedges the bread
knife to 0.299 only reaches 0.044 — nearly wobble-proof, as specced.
- Coupling: wobbled cut toasts at browning stdev 0.088 vs 0.071 clean, same
mean — 'a bit wedged' band (>0.18) confirmed in the Cut criterion.
- Slam cut (0.4 u/frame): squash 1.0, arrives pre-damaged (damage 0.02 pre-toast).
- Soup spoon siege: 17 strokes / 166 frames, miserable but possible.
- Brands: quality now drives heat-noise amp (0.12+0.42*(1-q)) AND blotch
frequency (3+3*(1-q)); WONDERSLICE visibly patchier than Crustworthy at 10s.
- Slicer visuals: cut seam deepens with progress; freed slice tips off the
loaf and lands crumb-up (doneTimer 0.7 -> 1.05 for the fall).
- gen-assets.sh: retry-once per stage (rsync flakes), bakery seeds bumped,
prep-bench cast queued ($1 prep).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
Sat down with it like someone who'd never seen it. Five findings, all
reproduced before fixing, all verified after:
1. The kitchen panel was a cheat console. BREAD/SPREAD/TOOL/BUTTER are the
order's facts, but the widgets stayed live — you could fetch a soup spoon
and then quietly re-select the butter knife. Locked during play; the
browning dial stays yours because it's a real control.
2. The drawer card printed the ANSWER under the silhouette. The entire
"similar but not identical" puzzle, solved by a caption. The name is gone;
after ten seconds of fumbling it fades in as a pity: "fine. it's the
butter knife."
3. Lifting anything above the rim committed it instantly — but fishing means
lifting blockers, so you could accidentally "choose" whatever you were
moving aside. Commit now needs 0.45s of continuous hold above the rim,
which doubles as a beat to compare what you're holding against the
silhouette. Verified: a wobbly lift takes ~0.95s because dipping below
the rim resets the clock, and that's correct.
4. Coverage scored 20% on a slice a player had visibly covered corner to
corner. Root cause: the "covered" floor scaled with the ASKED amount, so
Coverage and Amount both punished thinness — double jeopardy. Coverage now
asks its own question (are there bare patches? floor = 0.06 absolute) and
the same conscientious play scores 84%.
5. Nothing told you how much was on the toast until the verdict. New live
readout, computed by the judge's own functions: "on the toast: thin —
normal asked · 84% covered". Watching it, a played-well day 1 went
8.8 -> 9.0, and the missing 0.2 is named on the card (stripes), not hidden.
Also: the drawer holds 11 pieces dropped down a tighter column (a heap you
rummage in, not an exploded diagram — padded with extra teaspoons, like every
real drawer), it sits on a bench instead of floating in a void, Service is
sim-time rather than wall-clock, and the title mentions the drawer exists.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Natural peanut butter stratifies while it sits. The jar carries the state:
strat 0..1, set per order (smooth 0.75, crunchy 0.9 — chunks sink). The first
unstirred dip takes the oil slick off the top; the ones after it dig into what
the oil left behind. Verified dip trace, no stirring: 0.875, 0.669, 0.462,
0.256. Swirl the knife in the jar (~2.5 circles of angular sweep; holding still
stirs nothing) and every dip is 0.5. Stirring costs time, and time costs toast
warmth — the mechanic pays the same tax as everything else.
Consistency pushes back through the physics that already exist: an oily load
multiplies the yield down (glides at any angle, goes on thin), a dry one
multiplies it up (fights like cold butter, tears). The shader shows it — slick
is dark and shiny, grout is pale and matte.
The judge reads what LANDED, not what the jar was: every deposit remembers the
oil it landed at, and the criterion scores the mass-weighted mean AND stdev.
The stdev is the design catch, found by verification: a slick strip and a grout
strip average to 0.54 — "just right" — and the first cut scored the lazy play
as if the jar had been stirred. Nobody eats the average:
never stirred, strips oil 0.54 ±0.23 "slick here, grout there" C
one slick dip, whole oil 0.87 ±0.00 "a slick"
stirred first oil 0.50 ±0.00 "just right"
Crunchy reuses the particle system wholesale — chunks ride the knife, shed per
stroke-distance, get judged with the same Clark–Evans index ("Peanut chunks —
21 bits, evenly strewn (R 1.20)"), and the rind line bank is genericised via a
{bits} placeholder so he complains correctly about either.
Day 6 is crunchy now ("the PROPER kind"), the ticket warns when a jar has been
sitting, and the JAR bar shows how mixed it is. Five new judge lines, including
"You did not stir the jar. The jar knows. I know."
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A particulate spread. The jelly is ordinary rheology; the rind is discrete
solids that ride the knife and drop off per unit of stroke DISTANCE, not per
second — a dabbing knife makes a pile, a moving knife lays a trail, and since
the shed rate scales with what's left on the blade, one dip cannot cover the
slice. Even rind takes several dips placed with intent. Scraping picks bits
back up (60% survive the blade), so a clump can be argued with.
The judge scores the scatter with the Clark–Evans nearest-neighbour index over
the bread's own area, and quotes it: R near 0 is one clump, 1 is random, above
1 is deliberate. Verified end to end on day 8:
dab in a corner 8 bits "one clump (R 0.24)" drags grade to B
three placed dips 24 bits "evenly strewn (R 1.17)" A, 8.9/10
First cut shed per-second; even careful strokes dumped the whole load in the
first half-second and scored "clumped (R 0.50)". Distance-based shedding is
what makes evenness the reward for technique rather than luck.
Rind renders as instanced chunks parented to the slice mesh, so it rides the
pop, the landing, and the pedestal turn. Points, not a field: the judge wants
nearest-neighbour statistics, and those want points.
Same sRGB trap as M3, new spot: the instanced material's colour was passed as
raw floats, which the renderer reads as linear — candied orange rendered as
pale butter. setRGB(..., SRGBColorSpace).
New content: day 8 handwritten (Deidre wants a piece in every bite), marmalade
in the procedural pool, five rind-district lines for the judge, and a marmalade
jar GLB generated on-device like everything else, with a plain-glass fallback
while it loads.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Content (verified — same dial, same 18s, four breads that disagree):
bread 4s 9s 18s char
white 0.22 0.54 1.08 93%
multigrain 0.20 0.52 1.06 92%
raisin 0.30 0.76 1.14 99.8%
sourdough 0.11 0.33 0.74 14%
Sourdough stalls at half the rate then accelerates hardest (x3.0 vs white's
x2.45) as its water boils off; raisin browns 40% faster and is essentially
carbon by 18s. Seven handwritten days then procedural, day persisted.
Assets, all generated on-device and free, all reproducible from fixed seeds:
- the judge's five expressions (one seed, one description, only the expression
phrase varying — he stays the same man while his face falls)
- title art, wooden bench, blurred kitchen backdrop
- butter dish / peanut butter / MITEY jars as GLBs, normalised on load
(recentre, scale to fit, sit on y=0) rather than hand-fixed, so regenerating
them doesn't mean re-fixing them
The toaster stays procedural: the generated one is prettier but this one has a
lever we can drive and a dial that turns. Judgment call, per the brief.
Sound is synthesised, no samples: clatter is driven by real Rapier contact-force
events and scaled by force, which is the only way a drawer of cutlery can sound
like one. Plus lever clunk, knife scrape pitched by pressure, and the stamp.
Perf, measured (16.7ms is the 60fps budget):
kitchen 0.99ms/step · drawer 0.12ms · drawer while dragging 0.21ms
Production build: 2.87MB / 1.0MB gzipped, mostly Rapier's inlined wasm + three.
Textures carry colorSpace = SRGBColorSpace explicitly — same class of bug as the
albedo gamma fix in M3, and it washes them out silently if you forget.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
toast -> drawer -> spread. Eight pieces settle into a real tangle; find the one
on the card, drag it up and over the rim, and whatever you're holding is what
you're spreading with.
The link, verified end to end (day 4, butter softness 0.3):
4s in the drawer warmth 0.78 yield 0.343 needs angle 0.36 comfortable
50s in the drawer warmth 0.17 yield 0.565 needs angle 0.60 cliff is 0.62
Dawdling walks you to within two hundredths of the scrape cliff. On day 7
(fridge-hard butter) it tips over into genuinely unspreadable. The drawer isn't
a timer bolted on the side — it's the thing that makes the butter hard.
Physics choices that earn their keep:
- Compound boxes, never trimeshes. A trimesh fork tangled in a trimesh fork is
a solver nightmare and slow with it; the tine envelope is what matters anyway.
- Grab is a capped PD spring, not a fixed joint, so a snagged piece fights the
ones lying on it and loses instead of tunnelling through them.
- The solver is fixed-stepped so a frame hitch can't detonate the pile, and the
drawer settles offline so you open it on an existing mess.
- Clatter is synthesised from real contact-force events, velocity-scaled — a
drawer of cutlery is a noise, and the noise has to come from the collisions.
Three fixes:
- Quaternion.toArray() returns [x,y,z,w]; Rapier wants {x,y,z,w}. Every axis
read back undefined -> NaN in the solver -> wasm panic on the first step.
- The drag plane faced the camera, but the camera looks down into the drawer, so
dragging to the top of the screen couldn't physically lift anything over the
rim. It's a vertical plane now: up-screen means up.
- Cooling lived in kitchen.update(), which stops running the moment the drawer
opens — so the toast stayed hot no matter how long you rummaged, quietly
cancelling the entire mechanic. It's an app-level tick now.
The drawer was 4.6x3.4 and the pieces politely spread out flat. Tangling needs
crowding: 3.1x2.2, dropped down a narrow column so they land on each other.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The loop closes: ticket -> toast -> spread -> ENTER -> verdict -> next day.
- judging.ts: 9 weighted criteria, each reading the same fields the knife was
pushing around, so every line of the scorecard points at something real
("0.78 against 0.68 asked", "thick — thin asked", "28% burnt").
- lines.ts: ~45 lines keyed to whichever criterion actually decided the score,
so the verdict and the scorecard always agree — the verdict just has feelings
about it. MITEY has its own vocabulary.
- judge.ts: the toast turns on a pedestal under a spotlight, grade stamps in,
heatmap toggle for browning/spread. The generated inspector reacts by grade.
- orders.ts: seven handwritten days, then procedural. Day 1 is soft butter on
white; day 7 is a translucent film of MITEY on wet sourdough with fridge-hard
butter and a steak knife.
The find: every art colour was authored as sRGB and fed straight into a linear
lighting pipeline. Linear 0.14 encodes back out to sRGB ~0.4, so "near-black"
MITEY rendered as TAN and saturated butter washed to pale cream. This was the
root cause of the legibility fights in M0 and M2 — I'd been treating the symptom
by pushing the specular around. One line (albedo = pow(albedo, 2.2), mixing
stays in sRGB because that's the space the palette was picked in) and the whole
art direction landed: MITEY is genuinely black, butter is butter, the crust went
from cream to a rich golden brown.
Also: the specular lobe was far too broad. The slice is flat with the light and
camera both above it, so dot(N,H) ~0.98 everywhere and a wide lobe blankets the
whole slice in white. Tightened, so only ridges tilted into the light catch —
which is what a spread actually looks like.
The slice rolls its own lighting and ignores scene lights, so the judge's
spotlight did nothing to it; it now swaps its own rig for presentation.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Knife angle (wheel) is the only input, and spread/scrape/gouge all fall out of it.
Verified by driving real input through the real code paths:
cold toast + fridge butter -> damage 0.015, spread 0.029 (tears, as designed)
fresh warm toast -> damage 0, spread 0.117 (flows cleanly)
burnt toast, knife on edge -> char 99.9% -> 82.7% -> 54.5%
evenness 0.029 -> 0.123 -> 0.226
pale toast, knife on edge -> gouges; steak knife gouges 3.2x harder
That evenness column is the mechanic: scraping rescues you from char and wrecks
uniformity doing it. The judge will have opinions.
The trap is calibrated, not hoped for. Pressure comes from steepness, but past
SCRAPE_ANGLE the knife stops spreading:
fridge butter / cold toast yield 0.72 needs angle 0.75 IMPOSSIBLE
bench butter / cold toast yield 0.54 needs angle 0.57 (cliff at 0.62)
fridge butter / fresh toast yield 0.36 needs angle 0.38 fine
soft butter / fresh toast yield 0.16 needs angle 0.12 dream mode
So cold toast + hard butter cannot be spread at any angle, and the way out isn't
technique — it's not dawdling. The pressure gauge draws both marks so you can
see the gold sitting past the red and understand why you're losing.
Emergent and kept: a steak knife's narrow blade concentrates pressure enough to
beat cold butter's yield. It's the right tool for cold butter and a menace
everywhere else.
- cutlery.ts: 9 hand-authored archetypes (silhouettes are gameplay — the drawer
has to be fair) + compound box colliders for M4.
- dev.ts: harness that drives real gestures deterministically. Earns its keep.
Two bugs: cutlery meshes used mesh.rotation.x = -PI/2, which sends a profile
drawn toward +y to -z and one drawn toward -y to +z — the handle and blade were
laid out in opposite directions, overlapping, nowhere near the cursor. Now
rotated at the geometry level. And resize() computed aspect = 0/0 = NaN when the
container reports zero, which poisons the projection matrix so every raycast
silently misses — i.e. the entire mechanic stops with no error.
Metals need something to reflect: added a RoomEnvironment IBL and rebalanced the
direct rig, which was tuned before it existed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Playable: pick a bread, set the dial, drop the lever, watch it brown, pop it
onto the plate. No timer readout — you go by smell.
- toasting.ts: heat map from real toaster behaviour (element stripes, cool top
edge where the slice stands proud of the slot, edge falloff, per-run noise
bias). Moisture must boil off before the crust browns at full rate, so
sourdough stalls then catches up; sugar browns and burns early.
Rate is measured against the heat map's actual ~0.77 mean, not guessed:
power 6 -> golden 0.511 at 10s, verified in-browser.
- Props on a real scale (1 unit ~ 11cm, one slice wide) — the toaster is
meant to dwarf the bread.
- The pop solves a genuine ballistic arc from slot to plate.
Three bugs found by driving it rather than trusting the compile:
- valueNoise2D read one past its grid at x=w-1, so heatBias was NaN along an
edge, which poisoned mean browning to NaN — i.e. every judge score would
have been NaN. Clamped.
- erode() clamped at the field border, so border texels could never erode out
of the mask and kept those NaNs in scope. Off-grid now counts as outside.
- Timer.connect(document) zeroes dt whenever document.hidden is true, which
froze the sim solid in an embedded browser. Dropped; rAF already pauses for
hidden tabs. App.step(dt) is now exposed so the game can be driven
deterministically for verification.
Also: screen shake was mutating the camera permanently instead of offsetting
it per-render, and the plate's lathe profile touched the axis (degenerate
triangles -> starburst normals) — rebuilt from primitives.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The bread slice is the whole game's canvas, so it's built first and properly:
- Field: a 128x128 scalar field with bilinear sample, a mass-conserving brush,
self-levelling relax, and the masked statistics the judge will read.
- Slice: extruded loaf silhouette (domed top), planar-projected UVs so the sim
grid lines up with the bread, per-texel silhouette mask, and four fields
(browning/dryness/spread/damage) packed into one RGBA8 texture per frame.
- Custom shader: browning ramp with blotchy char, crumb/crust/inclusions,
spread wetting + opacity + thickness relief, gloss, gouge exposure, and
flat data views for the judge screen.
- bread.ts / spreads.ts: the archetypes and the rheology the difficulty
curve will live in.
- scripts/gen-assets.sh: reproducible fixed-seed asset generation through
MODELBEAST (flux_local -> bg_remove_local -> hunyuan3d_mlx). All on-device.
Legibility work that mattered: butter rendered invisible at first. Fixes were
a gamma-style wetting darkening (a multiply *brightens* pale crumb), a normal
perturbation taken in UV units rather than raw texel deltas (~30x too small),
and pulling the lighting back under 1.0 so ACES stops desaturating yellow to
cream.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>