toastsim/src/sim/poach.ts
type-two bffee40c6f THE POACH: day 21 — the shiver, the vortex, the drop, the wobble test
The near-game, treated with respect (masterplan P3). sim/poach.ts (pure,
rides the heat-source bone): the pot chases the knob on the fuel's lag
and the poach wants the SHIVER (5.8-7.4) — cooler rags the white on the
way in, a rolling boil (>8.2) tears it the whole time. The VORTEX is the
reamer-twist at pot scale: swirl builds it, it DECAYS (swirl then drop,
not swirl then admire), and the strength at the drop is the judged form.
Two-layer set: white first, the yolk only firms once the white is mostly
there — lift in the window (white >=0.85, yolk <=0.35) or you've boiled
an egg the long way. Then DRAIN on the slotted spoon. Stirring a dropped
egg is vandalism (rags it directly).

scenes/poach.ts: WHEEL flame, circular-drag swirl (real angular sweep),
SPACE drop, L lift, hold-to-drain, ENTER serves; vortex streaks that spin
and fade, shiver-vs-boil bubbles, the white gathering tighter as it sets,
a hard yolk going pale. judgePoach: THE POACH (White/Wobble/Form) + The
Drain; the wobble line is the dossier's: 'It should tremble. Like you,
right now.' Day 21 authored + poach in the endless rotation (electric
pot past day 27).

Verified with REAL input: shiver + 91% vortex + lift-at-tremble + drain
= 9.9/10 'it TREMBLES — perfect', 'one neat comet (vortex 91%)'; still
water into a rolling boil, overstayed, undrained = 3.6/10, 'a pot of
rags', 'it does not move at all'.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 02:34:50 +10:00

185 lines
6.4 KiB
TypeScript

import { type HeatSource, newHeatSource, setKnob, heatStep } from './heatsource';
/**
* THE POACH — almost a whole game, and treated like one.
*
* Four acts, none skippable:
*
* THE WATER — the pot chases the knob on the fuel's lag (the heat-source
* bone). The poach wants the SHIVER — that band where the surface
* trembles but nothing rolls. Too cool and the white disperses into
* rags; a rolling boil and it tears the egg apart while it cooks.
* THE VORTEX — swirl the water first. A good vortex wraps the white around
* the yolk as it lands; drop into still water and the white walks off
* on its own. The vortex DECAYS — swirl, then drop, not swirl and
* admire.
* THE DROP — from low, into the eye of the spin. Height splats it.
* THE SET — white sets first, then the yolk starts to firm. Lift when the
* white is set and the yolk still trembles; every second past that is
* a second toward a boiled egg in disguise. Then DRAIN it on the
* spoon — nobody wants poach water on the plate.
*
* The wobble test at the pass reads yolkSet: the judge presses it, on camera.
* "It should tremble. Like you, right now."
*
* Pure — no three.js — the harness poaches headless.
*/
/** The shiver: where a poach wants the water. Above BOIL it tears. */
export const SHIVER_LO = 5.8;
export const SHIVER_HI = 7.4;
export const BOIL_AT = 8.2;
/** White is SET past here (snotty below ~0.6). */
export const WHITE_SET = 0.85;
/** Yolk still trembles below here; past it you've boiled an egg in disguise. */
export const YOLK_TREMBLE = 0.35;
/** How fast the vortex dies once you stop stirring. */
const VORTEX_DECAY = 0.09;
export interface PoachSession {
src: HeatSource;
/** Vortex strength 0..1 — built by swirling, decays on its own. */
vortex: number;
dropped: boolean;
lifted: boolean;
/** 0 raw .. 1 fully set. */
whiteSet: number;
/** 0 liquid .. 1 hard. The wobble test reads this. */
yolkSet: number;
/** White lost to rags — still water, a high drop, or a rolling boil. */
rags: number;
/** Poach water still clinging — drain on the spoon before the pass. */
cling: number;
/** Vortex strength at the moment of the drop (the judged number). */
vortexAtDrop: number;
tempAtDrop: number;
seconds: number;
}
export function newPoachSession(fuel = 'gas'): PoachSession {
return {
src: newHeatSource(fuel),
vortex: 0,
dropped: false,
lifted: false,
whiteSet: 0,
yolkSet: 0,
rags: 0,
cling: 1,
vortexAtDrop: 0,
tempAtDrop: 0,
seconds: 0,
};
}
export function setPotKnob(s: PoachSession, k: number): void {
setKnob(s.src, k);
}
/** Stir: add angular sweep (radians this frame) into the vortex. */
export function swirl(s: PoachSession, sweep: number): void {
if (s.dropped) {
// Stirring a dropped egg is vandalism — it rags the white directly.
s.rags = Math.min(1, s.rags + Math.abs(sweep) * 0.06);
return;
}
s.vortex = Math.min(1, s.vortex + Math.abs(sweep) * 0.055);
}
/**
* Drop the egg from `height` (0 rim-low .. 1 show-off). The vortex catches the
* white; still water and altitude both cost you form, immediately.
*/
export function dropEgg(s: PoachSession, height = 0): void {
if (s.dropped) return;
s.dropped = true;
s.vortexAtDrop = s.vortex;
s.tempAtDrop = s.src.output;
s.rags = Math.min(1, s.rags + (1 - s.vortex) * 0.45 + height * 0.4);
// Cold water at the drop lets the white walk before any heat can catch it.
if (s.src.output < SHIVER_LO) s.rags = Math.min(1, s.rags + (SHIVER_LO - s.src.output) * 0.12);
}
/** Lift it out on the spoon. The set stops; the drain begins. */
export function liftEgg(s: PoachSession): void {
if (s.dropped) s.lifted = true;
}
/** Hold it on the spoon: cling drains away. Patience, again. */
export function drainStep(s: PoachSession, dt: number): void {
if (s.lifted) s.cling = Math.max(0, s.cling - dt / 4);
}
export function poachStep(s: PoachSession, dt: number): void {
heatStep(s.src, dt);
s.vortex = Math.max(0, s.vortex - VORTEX_DECAY * dt);
const t = s.src.output;
if (s.dropped && !s.lifted) {
// The white sets fastest in the shiver; cooler is slower (and it rags on
// the way in); a boil sets it fast but TEARS it the whole time.
const inBand = t >= SHIVER_LO && t <= SHIVER_HI;
const rate = inBand ? 0.055 : t > SHIVER_HI ? 0.07 : t > 4 ? 0.03 : 0.012;
s.whiteSet = Math.min(1, s.whiteSet + rate * dt);
if (t > BOIL_AT) s.rags = Math.min(1, s.rags + (t - BOIL_AT) * 0.045 * dt);
// The yolk only starts once the white is mostly there.
if (s.whiteSet > 0.75) s.yolkSet = Math.min(1, s.yolkSet + 0.035 * (inBand ? 1 : 1.4) * dt);
}
s.seconds += dt;
}
export interface PoachResult {
/** Set, not snotty. */
whiteScore: number;
/** The wobble: liquid yolk full marks, hard yolk none. */
yolkScore: number;
/** One neat comet, not a pot of rags. */
formScore: number;
/** Drained on the spoon, or served with its own puddle. */
drainScore: number;
whiteSet: number;
yolkSet: number;
rags: number;
cling: number;
vortexAtDrop: number;
wobbleWord: string;
}
const clamp01 = (v: number) => (v < 0 ? 0 : v > 1 ? 1 : v);
export function poachResult(s: PoachSession): PoachResult {
const whiteScore = !s.dropped ? 0 : clamp01((s.whiteSet - 0.55) / (WHITE_SET - 0.55));
const yolkScore = !s.dropped ? 0 : clamp01(1 - Math.max(0, s.yolkSet - YOLK_TREMBLE) / 0.4);
const formScore = clamp01(1 - s.rags * 1.3);
const drainScore = clamp01(1 - s.cling * 0.9);
const wobbleWord =
!s.dropped ? 'there is no egg'
: s.yolkSet < 0.15 && s.whiteSet < WHITE_SET ? 'it collapses — raw inside'
: s.yolkSet <= YOLK_TREMBLE ? 'it TREMBLES — perfect'
: s.yolkSet < 0.6 ? 'a stiff little nod'
: 'it does not move at all';
return {
whiteScore,
yolkScore,
formScore,
drainScore,
whiteSet: s.whiteSet,
yolkSet: s.yolkSet,
rags: s.rags,
cling: s.cling,
vortexAtDrop: s.vortexAtDrop,
wobbleWord,
};
}
/** The water's diegetic tell — never a thermometer. */
export function waterWord(s: PoachSession): string {
const t = s.src.output;
if (t < 3) return 'still water';
if (t < SHIVER_LO) return 'warming — not yet';
if (t <= SHIVER_HI) return 'a gentle SHIVER — now';
if (t <= BOIL_AT) return 'simmering hard';
return 'ROLLING — it will tear';
}