THE CARD: the recipe stops being narrative and becomes law
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 <noreply@anthropic.com>
This commit is contained in:
parent
29f6b25c5b
commit
782971ee83
@ -710,7 +710,7 @@ export function installDevHarness(app: App, game: Game): void {
|
|||||||
await F(33); // ~0.5 charge: firm, nothing jumps
|
await F(33); // ~0.5 charge: firm, nothing jumps
|
||||||
window.dispatchEvent(new KeyboardEvent('keyup', { code: 'KeyF' }));
|
window.dispatchEvent(new KeyboardEvent('keyup', { code: 'KeyF' }));
|
||||||
await F(4);
|
await F(4);
|
||||||
s.knob = 8; // 190 to golden
|
s.knob = 7; // 190 to golden — the card is the LAW
|
||||||
tap('Space'); // back in
|
tap('Space'); // back in
|
||||||
g = 0;
|
g = 0;
|
||||||
while (mean() < 0.66 && g++ < 60 * 90) await F(1);
|
while (mean() < 0.66 && g++ < 60 * 90) await F(1);
|
||||||
|
|||||||
@ -647,6 +647,32 @@ export function judgeAirfryer(r: import('../sim/airfryer').FryerResult, order: O
|
|||||||
weight: 1.1,
|
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',
|
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<number, number>, k: number) => (m[k] ?? 0) + ((m[k - 1] ?? 0) + (m[k + 1] ?? 0)) * 0.5;
|
||||||
|
const tsum = (m: Record<number, number>) => 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',
|
key: 'frycount',
|
||||||
group: 'bench',
|
group: 'bench',
|
||||||
|
|||||||
@ -119,8 +119,9 @@ export interface Order {
|
|||||||
/** Eggs benedict: toast the bread, then poach the egg and land it ON the
|
/** Eggs benedict: toast the bread, then poach the egg and land it ON the
|
||||||
* toast — one plate, two crafts, one verdict. */
|
* toast — one plate, two crafts, one verdict. */
|
||||||
benedict?: { fuel: string };
|
benedict?: { fuel: string };
|
||||||
/** THE AIR FRYER: arrangement is the skill. The card is the law. */
|
/** THE AIR FRYER: arrangement is the skill. The card is the LAW — two
|
||||||
airfryer?: { count: number; card: string };
|
* 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][] = [
|
export const BROWNING_NAMES: [number, string][] = [
|
||||||
@ -505,7 +506,7 @@ export const DAYS: Order[] = [
|
|||||||
butterSoftness: 0.5,
|
butterSoftness: 0.5,
|
||||||
who: 'the wing man',
|
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.',
|
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']),
|
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.`,
|
`${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;
|
return o;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -47,6 +47,12 @@ export interface FryerSession {
|
|||||||
steamed: number;
|
steamed: number;
|
||||||
/** Shakes that went badly (pieces out). */
|
/** Shakes that went badly (pieces out). */
|
||||||
lostCount: number;
|
lostCount: number;
|
||||||
|
/** Heat-weighted seconds spent at each knob BEFORE the first shake... */
|
||||||
|
preTime: Record<number, number>;
|
||||||
|
/** ...and after it — the card judges the two phases separately. */
|
||||||
|
postTime: Record<number, number>;
|
||||||
|
/** Mean browning at each shake — 'half-time' is a real place. */
|
||||||
|
shakes: number[];
|
||||||
rng: Rng;
|
rng: Rng;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -63,7 +69,7 @@ export function newFryerSession(count = 6, seed = 20260721): FryerSession {
|
|||||||
lost: false,
|
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 {
|
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 over = Math.max(0, smother - SMOTHER_AT) / (1 - SMOTHER_AT);
|
||||||
const steamFactor = 1 - over * 0.85; // crowded = wet heat, no crisp
|
const steamFactor = 1 - over * 0.85; // crowded = wet heat, no crisp
|
||||||
if (over > 0) s.steamed += over * dt * heatN * 2;
|
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++) {
|
for (let i = 0; i < s.pieces.length; i++) {
|
||||||
const p = s.pieces[i];
|
const p = s.pieces[i];
|
||||||
if (p.lost) continue;
|
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.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
|
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;
|
s.seconds += 2;
|
||||||
return { redistributed: true, jumped };
|
return { redistributed: true, jumped };
|
||||||
}
|
}
|
||||||
@ -159,6 +171,9 @@ export interface FryerResult {
|
|||||||
lost: number;
|
lost: number;
|
||||||
served: number;
|
served: number;
|
||||||
seconds: number;
|
seconds: number;
|
||||||
|
preTime: Record<number, number>;
|
||||||
|
postTime: Record<number, number>;
|
||||||
|
shakes: number[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fryerResult(s: FryerSession): FryerResult {
|
export function fryerResult(s: FryerSession): FryerResult {
|
||||||
@ -175,6 +190,9 @@ export function fryerResult(s: FryerSession): FryerResult {
|
|||||||
lost: s.lostCount,
|
lost: s.lostCount,
|
||||||
served: live.length,
|
served: live.length,
|
||||||
seconds: s.seconds,
|
seconds: s.seconds,
|
||||||
|
preTime: s.preTime,
|
||||||
|
postTime: s.postTime,
|
||||||
|
shakes: s.shakes,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user