From 782971ee83b42868e9ca91c29db8f8d35bb961d9 Mon Sep 17 00:00:00 2001 From: type-two Date: Tue, 21 Jul 2026 03:38:05 +1000 Subject: [PATCH] THE CARD: the recipe stops being narrative and becomes law MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The multi-cycle timer doctrine from the atlas, first station: the air fryer's cursed card ('180° until colour · SHAKE · 190° to golden') is now DATA — deg1, deg2, and a half-time shake — and the sim keeps the ledger to judge it by: heat-weighted seconds at every knob position, split into pre-shake and post-shake phases, plus the mean browning at each shake so 'half-time' is a real place (0.15–0.5) and not a vibe. A new row on the scorecard, The Card: 40% ran phase one at its degrees, 40% phase two at its, 20% shook in the window. Ignore it entirely and the detail reads 'no shake. the card wept'; right temps but a mistimed shake is its own sentence. Procedural wing days now deal varied cards (170/180 into 190/200). The best bug of the batch: the verifier caught MY demo driver setting knob 8 for the finish — that is 200°, and the card says 190°. 'roughly the card, squinting.' The judge was right and the demonstrator now obeys: 'as written', 9.5/10. Co-Authored-By: Claude Fable 5 --- src/dev.ts | 2 +- src/game/judging.ts | 26 ++++++++++++++++++++++++++ src/game/orders.ts | 11 +++++++---- src/sim/airfryer.ts | 20 +++++++++++++++++++- 4 files changed, 53 insertions(+), 6 deletions(-) diff --git a/src/dev.ts b/src/dev.ts index 32c6be9..8a8d919 100644 --- a/src/dev.ts +++ b/src/dev.ts @@ -710,7 +710,7 @@ export function installDevHarness(app: App, game: Game): void { await F(33); // ~0.5 charge: firm, nothing jumps window.dispatchEvent(new KeyboardEvent('keyup', { code: 'KeyF' })); await F(4); - s.knob = 8; // 190 to golden + s.knob = 7; // 190 to golden — the card is the LAW tap('Space'); // back in g = 0; while (mean() < 0.66 && g++ < 60 * 90) await F(1); diff --git a/src/game/judging.ts b/src/game/judging.ts index 4c8af79..086a01b 100644 --- a/src/game/judging.ts +++ b/src/game/judging.ts @@ -647,6 +647,32 @@ export function judgeAirfryer(r: import('../sim/airfryer').FryerResult, order: O weight: 1.1, detail: r.steamed > 2.5 ? 'that is a steamer wearing a fryer badge' : r.steamed > 0.6 ? 'crowded for a while — it shows' : 'every wing breathed', }, + { + key: 'frycard', + group: 'fryer', + label: 'The Card', + ...(() => { + const deg1 = order.airfryer?.deg1 ?? 180; + const deg2 = order.airfryer?.deg2 ?? 190; + const k1 = Math.round((deg1 - 120) / 10); + const k2 = Math.round((deg2 - 120) / 10); + const near = (m: Record, k: number) => (m[k] ?? 0) + ((m[k - 1] ?? 0) + (m[k + 1] ?? 0)) * 0.5; + const tsum = (m: Record) => Object.values(m).reduce((a, b) => a + b, 0); + const p1 = tsum(r.preTime) > 1 ? near(r.preTime, k1) / tsum(r.preTime) : 0; + const p2 = tsum(r.postTime) > 1 ? near(r.postTime, k2) / tsum(r.postTime) : 0; + const halfTime = r.shakes.some((b) => b >= 0.15 && b <= 0.5); + const score = clamp01(p1 * 0.4 + p2 * 0.4 + (halfTime ? 0.2 : 0)); + const detail = r.shakes.length === 0 + ? 'no shake. the card wept' + : !halfTime + ? p1 > 0.7 && p2 > 0.7 ? 'right temps, wrong moment to shake' : 'shaken, but the temps ignored the card' + : p1 > 0.7 && p2 > 0.7 + ? `${deg1}° then ${deg2}°, shaken at half-time — as written` + : p2 < 0.3 ? `never made the ${deg2}° finish` : 'roughly the card, squinting'; + return { score, detail }; + })(), + weight: 0.9, + }, { key: 'frycount', group: 'bench', diff --git a/src/game/orders.ts b/src/game/orders.ts index 5b96888..f328351 100644 --- a/src/game/orders.ts +++ b/src/game/orders.ts @@ -119,8 +119,9 @@ export interface Order { /** Eggs benedict: toast the bread, then poach the egg and land it ON the * toast — one plate, two crafts, one verdict. */ benedict?: { fuel: string }; - /** THE AIR FRYER: arrangement is the skill. The card is the law. */ - airfryer?: { count: number; card: string }; + /** THE AIR FRYER: arrangement is the skill. The card is the LAW — two + * temperature phases with a shake between, and the judge checks the ledger. */ + airfryer?: { count: number; card: string; deg1: number; deg2: number }; } export const BROWNING_NAMES: [number, string][] = [ @@ -505,7 +506,7 @@ export const DAYS: Order[] = [ butterSoftness: 0.5, who: 'the wing man', text: 'Six wings in the air fryer. If they touch, they steam — and warm is not fried. Give every wing its air, SHAKE at half-time for the pale faces, and count them out: what jumps the rack is mine to look at.', - airfryer: { count: 6, card: 'THE CARD: 180\u00b0 until they colour \u00b7 drawer OUT, SHAKE \u00b7 190\u00b0 to golden \u00b7 all six present' }, + airfryer: { count: 6, deg1: 180, deg2: 190, card: 'THE CARD: 180\u00b0 until they colour \u00b7 drawer OUT, SHAKE \u00b7 190\u00b0 to golden \u00b7 all six present' }, }, ]; @@ -539,7 +540,9 @@ function proceduralAirfryer(day: number, rand: () => number): Order { pickOf(rand, ['the wing man', 'the lunch rush', 'a regular', 'someone new']), `${count} wings, air-fried. Air between every piece, a proper SHAKE at half-time, nothing on my floor.`, ); - o.airfryer = { count, card: `THE CARD: 180\u00b0 until colour \u00b7 SHAKE \u00b7 190\u00b0 to golden \u00b7 all ${count} present` }; + const deg1 = 170 + Math.floor(rand() * 2) * 10; // 170 or 180 + const deg2 = 190 + Math.floor(rand() * 2) * 10; // 190 or 200 + o.airfryer = { count, deg1, deg2, card: `THE CARD: ${deg1}\u00b0 until colour \u00b7 SHAKE \u00b7 ${deg2}\u00b0 to golden \u00b7 all ${count} present` }; return o; } diff --git a/src/sim/airfryer.ts b/src/sim/airfryer.ts index c27aaac..c81e932 100644 --- a/src/sim/airfryer.ts +++ b/src/sim/airfryer.ts @@ -47,6 +47,12 @@ export interface FryerSession { steamed: number; /** Shakes that went badly (pieces out). */ lostCount: number; + /** Heat-weighted seconds spent at each knob BEFORE the first shake... */ + preTime: Record; + /** ...and after it — the card judges the two phases separately. */ + postTime: Record; + /** Mean browning at each shake — 'half-time' is a real place. */ + shakes: number[]; rng: Rng; } @@ -63,7 +69,7 @@ export function newFryerSession(count = 6, seed = 20260721): FryerSession { lost: false, }); } - return { pieces, knob: 0, heat: 0, drawerOpen: true, seconds: 0, steamed: 0, lostCount: 0, rng }; + return { pieces, knob: 0, heat: 0, drawerOpen: true, seconds: 0, steamed: 0, lostCount: 0, preTime: {}, postTime: {}, shakes: [], rng }; } export function setFryerKnob(s: FryerSession, k: number): void { @@ -113,6 +119,10 @@ export function fryStep(s: FryerSession, dt: number): void { const over = Math.max(0, smother - SMOTHER_AT) / (1 - SMOTHER_AT); const steamFactor = 1 - over * 0.85; // crowded = wet heat, no crisp if (over > 0) s.steamed += over * dt * heatN * 2; + // The card's ledger: where the heat actually went, phase by phase. + const k = Math.round(s.knob); + const bucket = s.shakes.length ? s.postTime : s.preTime; + bucket[k] = (bucket[k] ?? 0) + dt * heatN; for (let i = 0; i < s.pieces.length; i++) { const p = s.pieces[i]; if (p.lost) continue; @@ -147,6 +157,8 @@ export function shake(s: FryerSession, strength: number): { redistributed: boole p.y = Math.max(PIECE_R, Math.min(TRAY - PIECE_R, p.y + (s.rng.next() - 0.5) * 0.3 * st)); p.pale *= 1 - st * 0.8; // the pale face turns to the wind } + const liveNow = s.pieces.filter((p) => !p.lost); + s.shakes.push(liveNow.length ? liveNow.reduce((a, p) => a + p.browning, 0) / liveNow.length : 0); s.seconds += 2; return { redistributed: true, jumped }; } @@ -159,6 +171,9 @@ export interface FryerResult { lost: number; served: number; seconds: number; + preTime: Record; + postTime: Record; + shakes: number[]; } export function fryerResult(s: FryerSession): FryerResult { @@ -175,6 +190,9 @@ export function fryerResult(s: FryerSession): FryerResult { lost: s.lostCount, served: live.length, seconds: s.seconds, + preTime: s.preTime, + postTime: s.postTime, + shakes: s.shakes, }; }