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; 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 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 = { 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 = { 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 = (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; }