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 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) { out.push(fill(pick(BANK[v.best.key]?.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; }