THE CRITIC'S FACE: he brings his own portraits now

Five moods generated on the Cloudflare Workers AI FLUX lane (10k free
neurons a day — scripts/cf-flux.py is the tap): a gaunt man in a black
three-piece suit, burgundy scarf, small round glasses, tiny notebook and
fountain pen, same claymation family as the judge and unmistakably not
him. Fixed seed, expression-only prompt deltas, so he is the same figure
across all five — the horrified one recoils mid-note, the impressed one
wears the small smile of a man betrayed by a good meal.

The grayscale filter is gone: the scorecard and the corner eye both
route by critic day (critic_* / judge_* prefix), demos and dailies stay
the judge's own. Also fixed 'the the count betrayed you' — his rows
already start with THE and he would never stammer in print.

Verified live: day 20 corner eye critic_neutral, scorecard
critic_impressed at 10.0 and critic_disappointed at 4.9 (C stamp, his
lines), day 21 back to the judge everywhere.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
type-two 2026-07-21 02:25:40 +10:00
parent 9ca713b5c9
commit 720e4dd5df
9 changed files with 40 additions and 6 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 494 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 548 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 500 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 515 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 504 KiB

32
scripts/cf-flux.py Executable file
View File

@ -0,0 +1,32 @@
#!/usr/bin/env python3
"""One FLUX-1-schnell image via Cloudflare Workers AI (10k free neurons/day).
Usage: cf-flux.py <outfile.png> <prompt> [steps] [seed]
Creds come from backnforth/.env never printed, never committed."""
import base64
import json
import os
import sys
import urllib.request
out, prompt = sys.argv[1], sys.argv[2]
steps = int(sys.argv[3]) if len(sys.argv) > 3 else 8
seed = int(sys.argv[4]) if len(sys.argv) > 4 else 0
env = {}
with open(os.path.expanduser('~/Documents/backnforth/.env')) as f:
for line in f:
if '=' in line and not line.startswith('#'):
k, _, v = line.strip().partition('=')
env[k] = v
req = urllib.request.Request(
f"https://api.cloudflare.com/client/v4/accounts/{env['CLOUDFLARE_ACCOUNT_ID']}/ai/run/@cf/black-forest-labs/flux-1-schnell",
data=json.dumps({'prompt': prompt, 'steps': steps, 'seed': seed}).encode(),
headers={'Authorization': f"Bearer {env['CLOUDFLARE_API_TOKEN']}", 'Content-Type': 'application/json'},
)
r = json.load(urllib.request.urlopen(req, timeout=120))
if not r.get('success'):
sys.exit('CF error: ' + json.dumps(r.get('errors')))
with open(out, 'wb') as f:
f.write(base64.b64decode(r['result']['image']))
print(out)

View File

@ -641,7 +641,8 @@ export class Game {
}
beginDay(): void {
eye.show(); // he watches you cook
eye.critic = !this.dailyDate && !this.demoMode && this.save.day % 10 === 0;
eye.show(); // he watches you cook — or on the tenth day, HE does
document.body.classList.toggle('shadow', this.shadowMode);
const day = this.save.day;
// Procedural days are seeded BY the day, not by a running stream: a reload
@ -999,7 +1000,7 @@ export class Game {
if (v.grade === 'S') extras.push('The Critic: I came to write a takedown. I cannot. This is the worse outcome — for me.');
else if (v.grade === 'A') extras.push('The Critic: competent. The word has never once appeared in a good review.');
else if (v.grade === 'F') extras.push('The Critic: I will be gentle in print. This kitchen has suffered enough today.');
else extras.push(`The Critic: the ${v.worst.label.toLowerCase()} betrayed you. It will be the headline.`);
else extras.push(`The Critic: the ${v.worst.label.toLowerCase().replace(/^the /, '')} betrayed you. It will be the headline.`);
}
// THE MOTHER: a lively starter firms the crumb on toaster days. A dead
// one is on the shelf where he can see it, and he can smell a coward.

View File

@ -33,7 +33,7 @@ export class JudgeView implements View {
onNext: (() => void) | null = null;
/** One-shot lines the game adds to this verdict (streaks, regulars' memory). */
extraLines: string[] = [];
/** Every 10th day THE CRITIC seats himself — same face, drained of warmth. */
/** Every 10th day THE CRITIC seats himself — his own face, his own notebook. */
critic = false;
/** The one-line result SHARE copies to the clipboard. */
shareText = '';
@ -265,8 +265,7 @@ export class JudgeView implements View {
this.extraLines = [];
this.lastCaption = { day: order.day, grade: verdict.grade, total: verdict.total, line: lines[1] ?? lines[0] ?? '' };
this.faceEl.src = assetUrl(`/assets/img/judge_${judgeFace(verdict.grade)}.png`);
this.faceEl.style.filter = this.critic ? 'grayscale(1) contrast(1.25)' : '';
this.faceEl.src = assetUrl(`/assets/img/${this.critic ? 'critic' : 'judge'}_${judgeFace(verdict.grade)}.png`);
this.stampEl.textContent = verdict.grade;
this.stampEl.className = `judge-stamp grade-${verdict.grade}`;
// re-trigger the stamp animation

View File

@ -9,6 +9,8 @@ export type Mood = 'neutral' | 'intrigued' | 'impressed' | 'disappointed' | 'hor
*/
class Eye {
private el: HTMLImageElement | null = null;
/** Critic days: the corner watcher is the man with the notebook. */
critic = false;
private timer: number | null = null;
private current: Mood = 'neutral';
@ -32,7 +34,7 @@ class Eye {
private set(m: Mood): void {
this.current = m;
this.ensure().src = assetUrl(`/assets/img/judge_${m}.png`);
this.ensure().src = assetUrl(`/assets/img/${this.critic ? 'critic' : 'judge'}_${m}.png`);
}
show(): void {