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>
589 lines
23 KiB
TypeScript
589 lines
23 KiB
TypeScript
import type { Criterion, Grade, Verdict } from './judging';
|
|
import type { Order } from './orders';
|
|
import { SPREADS } from '../sim/spreads';
|
|
import { JUICE_LINES, juicerRecognitionLine } from '../sim/juicing';
|
|
|
|
/**
|
|
* The judge. Deadpan, specific, never cruel about anything except MITEY, about
|
|
* which he is unwell.
|
|
*
|
|
* Lines are keyed to the criterion that actually decided the score, so the
|
|
* verdict always tells you the same thing the scorecard does — just meaner.
|
|
*/
|
|
|
|
type Bank = Record<string, { bad: string[]; good: string[] }>;
|
|
|
|
const BANK: Bank = {
|
|
browning: {
|
|
bad: [
|
|
'I asked for {asked}. This is {got}. These are different words.',
|
|
'You have made something adjacent to toast.',
|
|
'The colour is a decision. You appear not to have made one.',
|
|
'This is bread that has been near a toaster. Not the same thing.',
|
|
'Somewhere in this kitchen is a dial. It has numbers on it.',
|
|
],
|
|
good: [
|
|
'The colour is correct. I want that on the record.',
|
|
'That is the shade I asked for. Precisely the shade.',
|
|
'Colour: no notes.',
|
|
],
|
|
},
|
|
evenness: {
|
|
bad: [
|
|
'One half of this has had a completely different morning to the other.',
|
|
'It is striped. Toast should not be striped.',
|
|
'I can see where you rescued it. So can everyone.',
|
|
'This slice has weather.',
|
|
'There are three separate climates on this toast.',
|
|
],
|
|
good: [
|
|
'Even, corner to corner. That is harder than it looks.',
|
|
'No hot spots, no pale spots. Good.',
|
|
'Uniform. Genuinely uniform.',
|
|
],
|
|
},
|
|
coverage: {
|
|
bad: [
|
|
'You have buttered an island and left a continent.',
|
|
'The corners are not decorative. They are toast.',
|
|
'There is a border of bare toast. Why is there a border.',
|
|
'You got most of it. Most.',
|
|
'A generous interpretation of the word "spread".',
|
|
],
|
|
good: [
|
|
'Edge to edge. Somebody was raised properly.',
|
|
'Full coverage. Not a bare corner on it.',
|
|
'You went to the crusts. I noticed.',
|
|
],
|
|
},
|
|
uniformity: {
|
|
bad: [
|
|
'It has a thick end and a thin end, like a bad argument.',
|
|
'Lumpy. I can feel it from here.',
|
|
'The {spread} is doing whatever it likes.',
|
|
'This is not a film. It is terrain.',
|
|
],
|
|
good: [
|
|
'Flat, even, no ridges. Very tidy.',
|
|
'Consistent all the way across.',
|
|
'A properly level spread. Rare.',
|
|
],
|
|
},
|
|
char: {
|
|
bad: [
|
|
'I said no burnt bits. This is a burnt bit convention.',
|
|
'There is carbon on this. Actual carbon.',
|
|
'You burnt it. Then you served it. The second decision is the interesting one.',
|
|
'I asked for toast, not evidence.',
|
|
],
|
|
good: [
|
|
'Not a scorch on it.',
|
|
'No char. Well caught.',
|
|
'Clean. Nothing burnt.',
|
|
],
|
|
},
|
|
integrity: {
|
|
bad: [
|
|
'You have not spread this. You have assaulted it.',
|
|
'The surface is gone. You dragged it off and served the wound.',
|
|
'Cold butter. Warm toast. One of these was your job.',
|
|
'There is a crater. The coverage is admirable. The crater is not.',
|
|
'This bread has been through something and would rather not discuss it.',
|
|
],
|
|
good: [
|
|
'Not a tear on it. That is the whole trick, and you did it.',
|
|
'The surface is perfect. No drag, no gouges.',
|
|
'Intact. You let something be warm first, didn\'t you.',
|
|
],
|
|
},
|
|
amount: {
|
|
bad: [
|
|
'I said {asked}. You have applied {got}.',
|
|
'That is not {asked}. That is a statement.',
|
|
'Somebody has never been told no.',
|
|
],
|
|
good: [
|
|
'Exactly the right amount. Exactly.',
|
|
'The quantity is correct. Thank you.',
|
|
],
|
|
},
|
|
cut: {
|
|
bad: [
|
|
'You have cut a ramp. This toast has a gradient.',
|
|
'Thin at one end, a plank at the other. Pick a thickness.',
|
|
'You leaned on it. The loaf remembers. So do I.',
|
|
'The bakery wept when this happened. I heard them from here.',
|
|
'That is not a slice, that is a decision you kept changing.',
|
|
],
|
|
good: [
|
|
'A clean cut. The bakery would nod.',
|
|
'Thick, even, parallel faces. That is a SLICE.',
|
|
'Someone respected the loaf. It shows.',
|
|
],
|
|
},
|
|
rind: {
|
|
bad: [
|
|
'All the {bits}, in one postcode.',
|
|
'You have made a toast with a {bits} district.',
|
|
'Every piece of {bits}, huddled together for warmth. Spread them OUT.',
|
|
'One bite of this is all {bits}. The others are an apology.',
|
|
'There is no {bits} on this. Did you eat it? You ate it.',
|
|
],
|
|
good: [
|
|
'Evenly strewn {bits}. That takes patience, and you had it.',
|
|
'Every bite gets its {bits}. That is the entire craft.',
|
|
'The {bits}: textbook distribution. I did not expect that today.',
|
|
],
|
|
},
|
|
consistency: {
|
|
bad: [
|
|
'It is weeping oil. I can hear it.',
|
|
'You did not stir the jar. The jar knows. I know.',
|
|
'This has the texture of grout. Grout with ambitions.',
|
|
'Half slick, half plaster. Pick a toast.',
|
|
'The oil went on first and the cement went on after. Stir. The. Jar.',
|
|
],
|
|
good: [
|
|
'Properly stirred. You can taste the difference, and I intend to.',
|
|
'The consistency is exactly right. Someone respected the jar.',
|
|
'Smooth, even, no slick. The jar was stirred by an adult.',
|
|
],
|
|
},
|
|
tool: {
|
|
bad: [
|
|
'You did this with a {tool}. I can tell. Everyone can tell.',
|
|
'The {tool} was a choice.',
|
|
'There was a whole drawer. You picked the {tool}.',
|
|
],
|
|
good: [
|
|
'Right tool. It shows.',
|
|
'Correct utensil. Small thing. Not nothing.',
|
|
],
|
|
},
|
|
cutcv: {
|
|
bad: [
|
|
'These pieces are related, at best. Distant cousins.',
|
|
'One of these is a slice. The rest are opinions.',
|
|
'I lined them up by size. That took a while. That is the problem.',
|
|
'You cut this the way weather cuts a coastline.',
|
|
'Every piece is a surprise. Nobody ordered surprises.',
|
|
],
|
|
good: [
|
|
'Even pieces. Actually even. I measured.',
|
|
'You could deal these like cards.',
|
|
'Uniform. The knife was under instruction, and it listened.',
|
|
],
|
|
},
|
|
bench: {
|
|
bad: [
|
|
'There is juice on everything. Everything.',
|
|
"Somebody's seeds are on my floor.",
|
|
'The board looks like it lost an argument.',
|
|
'You cooked here the way storms cook.',
|
|
'I am not inspecting the food yet. I am inspecting the aftermath.',
|
|
],
|
|
good: [
|
|
"A clean board. You'd be surprised how rare.",
|
|
'The bench is spotless. I checked the corners.',
|
|
'Somebody wiped as they went. Somebody was raised right.',
|
|
],
|
|
},
|
|
dice: {
|
|
bad: [
|
|
'You went through the stem. The onion became confetti. You served confetti.',
|
|
'The root was the one thing holding it together. You cut the root.',
|
|
'It fell apart into slivers. A dice has corners. This has regrets.',
|
|
'You did not stop. The onion did — all over my board.',
|
|
'That is not diced. That is shredded, by someone in a hurry.',
|
|
],
|
|
good: [
|
|
'Square. Actually square. I am keeping one.',
|
|
'You stopped at the root every time. That is the whole skill, and you have it.',
|
|
'Even little cubes, and the root held. Textbook.',
|
|
],
|
|
},
|
|
sting: {
|
|
bad: [
|
|
'You were crying before the second cut. It shows in the pieces.',
|
|
'Blunt and slow. The onion made you pay in tears.',
|
|
'You crushed it more than you cut it. I could smell it from the pass.',
|
|
'Take your time and the onion takes your eyes. You took your time.',
|
|
],
|
|
good: [
|
|
'Sharp, quick, dry-eyed. That is how you beat an onion.',
|
|
'Not a tear. You were faster than it was.',
|
|
'Clean and fast. The onion never got a word in.',
|
|
],
|
|
},
|
|
// --- M15 bruschetta ---
|
|
roast: {
|
|
bad: [
|
|
'These tomatoes are raw. You showed them the oven. That is not the same as using it.',
|
|
'You roasted them into sauce. This is not a topping, it is a spill with ambitions.',
|
|
'Half blistered, half collapsed. You opened the oven at exactly the wrong two moments.',
|
|
'A tomato should blister, not surrender. These surrendered.',
|
|
'You cooked the water out and then kept going. Now there is nothing but skin and regret.',
|
|
],
|
|
good: [
|
|
'Blistered, not burst. That is the whole window and you were in it.',
|
|
'The tomatoes caught just enough. Sweet, not sludge.',
|
|
'Properly roasted. They hold their shape and give up their sugar. Good.',
|
|
],
|
|
},
|
|
topdist: {
|
|
bad: [
|
|
'All the topping, one corner. The rest of the toast is a rumour.',
|
|
'You built a little hill. Bruschetta is not a hill.',
|
|
'One bite has everything. The next has toast and disappointment.',
|
|
'It is clumped. Spread it OUT — every bite earns its share.',
|
|
'This is a garnish that gave up and sat down together.',
|
|
],
|
|
good: [
|
|
'Evenly strewn, corner to corner. Every bite is the same bite. Good.',
|
|
'You placed each piece like it mattered. It did.',
|
|
'Textbook scatter. I did not have to hunt for the tomato.',
|
|
],
|
|
},
|
|
balance: {
|
|
bad: [
|
|
'There is barely anything on this. The toast is doing all the work.',
|
|
'You heaped it. It is sliding off as I look at it.',
|
|
'Thin in the middle of a thick idea. Commit to the topping.',
|
|
'A tomato, a thought, and a lot of bare bread.',
|
|
'This is either a very sad pizza or a very confused piece of toast.',
|
|
],
|
|
good: [
|
|
'Thick, even, and it stays put. That is the balance, exactly.',
|
|
'Enough of everything, too much of nothing. Well judged.',
|
|
'A generous, level topping. Nobody gets a bare bite.',
|
|
],
|
|
},
|
|
sog: {
|
|
bad: [
|
|
'It bends. Toast should not bend.',
|
|
'I pressed it and it folded like a wet letter. You were too slow.',
|
|
'The juice won. It always wins if you dawdle, and you dawdled.',
|
|
'Soggy. The doorstop was your one defence and you let it soak anyway.',
|
|
'This has the structural integrity of an apology.',
|
|
],
|
|
good: [
|
|
'It snapped. That is the sound a bruschetta is meant to make.',
|
|
'Crisp under the finger. You served it in time. Good.',
|
|
'Still crunching. The thick cut earned its keep.',
|
|
],
|
|
},
|
|
rub: {
|
|
bad: [
|
|
'No garlic. It is the name of the dish, more or less.',
|
|
'You forgot the clove. I can taste the absence of it.',
|
|
'You have not rubbed this, you have varnished it. My eyes are watering.',
|
|
'A whole clove, in one streak. The rest is plain toast.',
|
|
],
|
|
good: [
|
|
'A light, even rub. You can smell it before you taste it. Correct.',
|
|
'The garlic is there and it knows its place. Good.',
|
|
'One pass, all over. That is all it ever needed.',
|
|
],
|
|
},
|
|
temp: {
|
|
bad: [
|
|
'The brie is fridge-hard. You had all service to take it out. You took none of it.',
|
|
'The parmesan is sweating. You left it on the bench like it owed you money.',
|
|
'Cold cheese does not spread and warm cheese does not shave. You managed both, wrongly.',
|
|
'You did not plan. The clock planned for you, and the clock is cruel.',
|
|
],
|
|
good: [
|
|
'Brie soft, parmesan cold. You worked the fridge like someone who has done this.',
|
|
'Both cheeses exactly as they should be. That is planning, and I noticed.',
|
|
'You took the brie out early and left the parmesan in. Textbook.',
|
|
],
|
|
},
|
|
};
|
|
|
|
// The charcoal grill's rows — the deadpan char snob.
|
|
Object.assign(BANK, {
|
|
sear: {
|
|
bad: [
|
|
'There is no char on this. There is a memory of char. A rumour.',
|
|
'This never met the coals. It was in the same postcode as the coals.',
|
|
'One side is Sunday. The other side is a house fire. Commit to a Tuesday.',
|
|
'You had a screaming bed and a cool lee and you used neither. Bold. Wrong.',
|
|
'Raw. Not rare. Raw. There is a word for it and it is that one.',
|
|
],
|
|
good: [
|
|
'A proper sear. You arranged the coals and you moved with intent. I saw it.',
|
|
'That is a sear. Crust where crust belongs. I am almost moved.',
|
|
'You seared this over charcoal you built with genuine care. Almost.',
|
|
],
|
|
},
|
|
grillchar: {
|
|
bad: [
|
|
'Flare-up. You let the fat conduct the orchestra. It chose inferno.',
|
|
'This is carbon. Delicious, structural carbon. Inedible.',
|
|
'You watched it flare and you did nothing. We both watched. Only one of us learned.',
|
|
],
|
|
good: [
|
|
'No carbon. You dodged the flares. That is the whole discipline, right there.',
|
|
'Charred where it should be, nowhere it should not. Correct.',
|
|
],
|
|
},
|
|
grillbench: {
|
|
bad: [
|
|
'The flare-ups. I counted them. So did the smoke detector, spiritually.',
|
|
'Ash in the sear. The wind had opinions and you had no windbreak.',
|
|
],
|
|
good: ['A clean bed, no flares. You cooked, you did not fight a fire.', 'Tidy coals. Tidy cook.'],
|
|
},
|
|
});
|
|
|
|
// The fridge — the hygiene inspector, who is happiest saying nothing.
|
|
Object.assign(BANK, {
|
|
coldkept: {
|
|
bad: [
|
|
'Half this delivery is a science experiment now. Cold things go COLD, love.',
|
|
'You put the fish in the door. The door. The warmest shelf in the box.',
|
|
'This has gone off. Not adventurous. Off. There is a difference and you crossed it.',
|
|
'Perishables belong in the back where it is coldest. You knew that. The bin knows it now.',
|
|
],
|
|
good: [
|
|
'Everything still fresh in three days. You know where the cold lives. Good.',
|
|
'Stored like someone who has been paged about a bad prawn. Correct.',
|
|
],
|
|
},
|
|
hygiene: {
|
|
bad: [
|
|
'Raw chicken dripping onto the salad. That is not a fridge, that is a crime scene with a light in it.',
|
|
'Something raw sat above something ready-to-eat. I can hear the health board from here.',
|
|
'Cross-contamination. The one rule. Raw goes on the BOTTOM. Always.',
|
|
],
|
|
good: [
|
|
'Raw on the bottom, nothing dripped. That is the whole discipline and you have it.',
|
|
'No cross-contamination. Boring. Boring is the goal. Well done.',
|
|
],
|
|
},
|
|
waste: {
|
|
bad: ['You threw good money in the bin because it rotted in the back. Rotate your stock.', 'Waste. That was profit, once.'],
|
|
good: ['Nothing wasted. You used the old before the new.'],
|
|
},
|
|
});
|
|
|
|
// The eggs — he has a nose, and he has heard things he cannot unhear.
|
|
Object.assign(BANK, {
|
|
eggcount: {
|
|
bad: [
|
|
'I asked for {asked}. Count what is in that bowl. We are not close.',
|
|
'Short. An egg count is not a suggestion.',
|
|
],
|
|
good: ['The full count, all clean. Correct.', 'Every egg accounted for. Good.'],
|
|
},
|
|
eggshell: {
|
|
bad: [
|
|
'There is shell in the whites. Someone is going to bite that. It will be me.',
|
|
'Shrapnel. You cracked it like it owed you money. Tap. TAP.',
|
|
'I can hear the shell from here. Fish it out next time. All of it.',
|
|
],
|
|
good: ['Not a fragment of shell. A civilized crack.', 'Clean cracks, clean bowl. That is the tap.'],
|
|
},
|
|
eggwhites: {
|
|
bad: [
|
|
'Yolk in the whites. They will never whip now. They know it. You know it.',
|
|
'You rushed the pass and the yolk went. Slow hands, love. The yolk is not in a hurry.',
|
|
'These whites are ruined and we both watched it happen.',
|
|
],
|
|
good: ['Whites clean enough to whip to glass. The pass was patient. I noticed.', 'Yolks whole, whites pure. That is separation.'],
|
|
},
|
|
eggnose: {
|
|
bad: [
|
|
'You cracked a rotten one into the bowl. I heard it from here. The whole street heard it.',
|
|
'There is a GLASS OF WATER on the bench. It exists for exactly this. Use it.',
|
|
'That smell is now part of the kitchen. Part of me. Float them FIRST.',
|
|
],
|
|
good: ['You floated the suspects and none slipped through. That is a nose you can trust.', 'Nothing rotten reached the bowl. The glass did its work.'],
|
|
},
|
|
});
|
|
|
|
// The rib eye — the steakhouse grandfather who has watched a thousand cooks panic.
|
|
Object.assign(BANK, {
|
|
steakcook: {
|
|
bad: [
|
|
'You asked it a question and pulled it out before it answered. Grey in the middle, or raw — pick a lane.',
|
|
'The interior is a guess. Cooking a steak is not a guess.',
|
|
'Over. You cooked the life out of it and then kept going.',
|
|
],
|
|
good: ['Edge to edge, the doneness they asked for. That is a cook.', 'The interior is exactly right. You read it.'],
|
|
},
|
|
steakrest: {
|
|
bad: [
|
|
'You cut it the second it left the pan. It bled out on the board. I watched the whole thing.',
|
|
'No rest. All that juice is on the wood, not in the meat. That was the dinner, weeping.',
|
|
'Patience is the last ingredient and you left it out. It bled.',
|
|
],
|
|
good: [
|
|
'You let it rest. You held your nerve while it did nothing, and it kept every drop. THAT is the job.',
|
|
'Rested. Cut it and it held. A cook who can wait is a cook.',
|
|
],
|
|
},
|
|
steakcut: {
|
|
bad: [
|
|
'You cut along the grain. Every slice is a rope. Turn the knife ninety degrees, always.',
|
|
'Torn, not sliced. Steady the hand — a steak deserves one clean pass.',
|
|
'Chewy. You cut WITH the fibres. Across. Across the grain.',
|
|
],
|
|
good: [
|
|
'Across the grain, one clean pass each. Short fibres, tender. You know where the muscle runs.',
|
|
'Every slice across the grain, none torn. That is knife work.',
|
|
],
|
|
},
|
|
});
|
|
|
|
// The fetched ingredient — the cook who reads a use-by date.
|
|
Object.assign(BANK, {
|
|
ingredient: {
|
|
bad: [
|
|
'This was off before it hit the pan. You stored it in the door, didn\'t you. Three days ago.',
|
|
'No sear saves a bad mushroom. The fridge did this, not the flame.',
|
|
'You cooked something that had already given up. I can taste the fridge door.',
|
|
'Fresh is a choice you make days early. You made the other one.',
|
|
],
|
|
good: [
|
|
'The mushroom was still perfect. Cold, back of the fridge, used in time. That is the whole craft before the pan even lights.',
|
|
'Fresh off the shelf. You store like someone who respects the ingredient.',
|
|
],
|
|
},
|
|
});
|
|
|
|
// The pan — the butter snob.
|
|
Object.assign(BANK, {
|
|
pansear: {
|
|
bad: [
|
|
'This is pale. You had a screaming pan and a piece of food and you introduced them politely.',
|
|
'No crust. Searing is a commitment. You hedged.',
|
|
'One face is dinner. The other face has never met heat. Turn it over. That is the entire trick.',
|
|
'You steamed a mushroom in a frying pan. That takes a special lack of nerve.',
|
|
],
|
|
good: [
|
|
'A crust on both faces. You landed it in the foam and you turned it in time. Correct.',
|
|
'That is a sear. Even, deep, both sides. I have notes and they are all compliments.',
|
|
],
|
|
},
|
|
paneven: {
|
|
bad: [
|
|
'One side Sunday, one side a rumour. You forgot it had a back.',
|
|
'These two faces were cooked in different postcodes.',
|
|
],
|
|
good: ['Both faces the same shade. You flipped it when it asked. Good hands.'],
|
|
},
|
|
panbutter: {
|
|
bad: [
|
|
'You cooked in burnt butter. I can taste the moment you stopped paying attention.',
|
|
'The butter went past noisette into grief. It took the food with it.',
|
|
'Bitter. That is the butter. That is a smell you chose and did not act on.',
|
|
],
|
|
good: [
|
|
'Noisette. You browned the butter and stopped. That restraint is the whole dish.',
|
|
'Good butter, foamed not burnt. You were watching. I noticed.',
|
|
],
|
|
},
|
|
});
|
|
|
|
// The juicer's two rows (The Juice + Pips) ship their lines with the sim, so the
|
|
// station owns its whole voice. Merge them in rather than duplicate them here.
|
|
Object.assign(BANK, JUICE_LINES);
|
|
|
|
/** MITEY gets its own vocabulary. */
|
|
const MITEY_CRIMES = [
|
|
'This is not a scrape of MITEY. This is a hate crime.',
|
|
'You have applied MITEY the way one applies paint.',
|
|
'I asked to see the toast THROUGH it. I can see nothing. There is only night.',
|
|
'That is a decade of MITEY on one slice. Some of us have to live here.',
|
|
];
|
|
|
|
const GRADE_OPENERS: Record<Grade, string[]> = {
|
|
S: ['Well.', 'I have been doing this for thirty-one years.', 'Hm.'],
|
|
A: ['Close.', 'Nearly.', 'Good.'],
|
|
B: ['Adequate.', 'It is toast.', 'Fine.'],
|
|
C: ['Hm.', 'Right.', 'Well, it exists.'],
|
|
F: ['No.', 'Absolutely not.', 'Take it away.'],
|
|
};
|
|
|
|
const GRADE_CLOSERS: Record<Grade, string[]> = {
|
|
S: [
|
|
'That is the toast. Do it again tomorrow and I will start to worry.',
|
|
'I have no notes. I dislike having no notes.',
|
|
'Perfect. Irritatingly perfect.',
|
|
],
|
|
A: ['Very nearly the toast.', 'One thing away from excellent.', 'Good work. Not finished work.'],
|
|
B: ['It will be eaten. That is the bar it cleared.', 'Serviceable.', 'Nobody will complain. Nobody will remember.'],
|
|
C: ['I have eaten worse. Not recently.', 'Try again.', 'This is why we have a scorecard.'],
|
|
F: ['I am writing this down.', 'Start again. Properly.', 'The bread deserved better.'],
|
|
};
|
|
|
|
function fill(s: string, order: Order, v: Verdict, tool: string): string {
|
|
const spread = SPREADS[order.spread];
|
|
return s
|
|
.replace('{asked}', order.amount === 'thin' ? 'a scrape' : order.amount)
|
|
.replace('{got}', v.criteria.find((c) => c.key === 'amount')?.detail.split(' — ')[0] ?? 'that')
|
|
// Mid-sentence, so lowercase — except MITEY, which is shouted on principle.
|
|
.replace('{spread}', spread.id === 'mitey' ? 'MITEY' : spread.name.toLowerCase())
|
|
.replaceAll('{bits}', spread.particles?.name ?? 'rind')
|
|
.replace('{tool}', tool.toLowerCase());
|
|
}
|
|
|
|
/**
|
|
* Two lines: what went worst, and what went best. Which is what the scorecard
|
|
* says too — this just says it with feeling.
|
|
*/
|
|
export function verdictLines(v: Verdict, order: Order, tool: string, rand: () => number): string[] {
|
|
const pick = <T>(a: T[]): T => a[Math.floor(rand() * a.length)];
|
|
const out: string[] = [];
|
|
out.push(pick(GRADE_OPENERS[v.grade]));
|
|
|
|
// The Juicernaut announces itself before the verdict proper — if the order
|
|
// was juiced on one, and only then.
|
|
for (const step of order.prep ?? []) {
|
|
if (step.kind === 'juice') {
|
|
const rec = juicerRecognitionLine(step.juicer);
|
|
if (rec) out.push(rec);
|
|
break;
|
|
}
|
|
}
|
|
|
|
const mitey = order.spread === 'mitey';
|
|
const amountBad = (v.criteria.find((c) => c.key === 'amount')?.score ?? 1) < 0.4;
|
|
if (mitey && amountBad) {
|
|
out.push(pick(MITEY_CRIMES));
|
|
} else if (v.worst.score < 0.72) {
|
|
out.push(fill(pick(BANK[v.worst.key]?.bad ?? ['Hm.']), order, v, tool));
|
|
}
|
|
|
|
if (v.best.score > 0.85 && v.best.key !== v.worst.key) {
|
|
// Guard: a criterion with no BANK entry (or an empty good[]) must not push an
|
|
// undefined line into fill() — some rows (The Board, Service) carry no praise.
|
|
const good = BANK[v.best.key]?.good;
|
|
if (good && good.length) out.push(fill(pick(good), order, v, tool));
|
|
}
|
|
|
|
out.push(pick(GRADE_CLOSERS[v.grade]));
|
|
return out.filter(Boolean);
|
|
}
|
|
|
|
/** Which portrait to show. */
|
|
export function judgeFace(grade: Grade): string {
|
|
switch (grade) {
|
|
case 'S':
|
|
return 'impressed';
|
|
case 'A':
|
|
return 'intrigued';
|
|
case 'B':
|
|
return 'neutral';
|
|
case 'C':
|
|
return 'disappointed';
|
|
default:
|
|
return 'horrified';
|
|
}
|
|
}
|
|
|
|
export function describeCriterion(c: Criterion): string {
|
|
return c.detail;
|
|
}
|