A second plate, drawn rather than tinted (assets/_board.py — _regen.py cannot do this job: it keys magenta, crops to ink and rescales figures to a common height, and a scoreboard is a full-bleed rectangle with no figure in it). The club's bottle-green board is the club's; the match is played on one nobody has cleaned in a long time, and the ink changes with it — a green 'dim' over dried blood reads as a printing fault. Two column changes go with it: the name slot says KEEPER / JUDGE / CHAMPION instead of THEM (eight characters, `short` on the ladder rows), and the SETS column is gone in the match, where it was a permanent 0 — the bar under the board already carries that mode's progress. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
77 lines
4.3 KiB
Python
77 lines
4.3 KiB
Python
# _board.py — draw a SCOREBOARD PLATE. One frame, full-bleed, no figures.
|
|
#
|
|
# python3 assets/_board.py match # the zombie-tennis board -> assets/scoreboard_z.png
|
|
#
|
|
# _regen.py cannot do this job: it keys a magenta background, crops to ink and rescales every figure
|
|
# to a common 224 height, and a scoreboard is a full-bleed RECTANGLE with no figure in it. So this
|
|
# is the same generate-and-install loop with the sprite machinery taken out — generate, resize to
|
|
# the exact plate size the HUD draws (render.js draws it at 150x42 from a 300x84 source), quantise
|
|
# to flat pixel-art colours, install.
|
|
#
|
|
# It is deliberately a plate and NOT a tint of the club's board: the two boards say different things
|
|
# and one of them is the club's, so the second is drawn rather than filtered (see the settled rule —
|
|
# procedural is for blood, fire and smoke, not for art).
|
|
import os, subprocess, sys
|
|
import numpy as np
|
|
from PIL import Image
|
|
|
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
SCRATCH = os.environ.get('SPLAT_SCRATCH', '/tmp/splatter-regen')
|
|
GEN = os.path.expanduser('~/.claude/skills/generate-image/scripts/generate_image.py')
|
|
PY = os.environ.get('SPLAT_PY', sys.executable)
|
|
MODEL = os.environ.get('SPLAT_MODEL', 'google/gemini-3-pro-image')
|
|
|
|
W, H = 300, 84 # the source size render.js halves to 150x42
|
|
|
|
STYLE = (
|
|
"Draw it as 16-BIT PIXEL ART with hard flat colours and hard edges — no gradients, no blur, no "
|
|
"anti-aliasing, no photographic texture, no lighting effects. The image is ONE SOLID PLATE that "
|
|
"FILLS THE ENTIRE CANVAS EDGE TO EDGE: no background around it, no drop shadow, no border of "
|
|
"empty space, no perspective, no rounded corners. Straight on, flat, like a UI panel in a "
|
|
"Super Nintendo sports game. ABSOLUTELY NO TEXT, NO LETTERS, NO NUMBERS AND NO LOGOS anywhere. "
|
|
)
|
|
|
|
BOARDS = {
|
|
'match': dict(
|
|
out='scoreboard_z.png', ref='scoreboard.png',
|
|
subject=(
|
|
"The reference is a tennis club's manual scoreboard panel drawn as a 16-bit UI plate: a "
|
|
"bevelled outer frame, a flat field, a coloured vertical spine down the left edge, and a "
|
|
"recessed sunken slot in the right third. Draw a DIFFERENT board with EXACTLY the same "
|
|
"layout, the same proportions and the same parts in the same places — but it is the "
|
|
"board of a court that has gone wrong: the field is a dark dried BLOOD RED-BROWN instead "
|
|
"of bottle green, the frame is tarnished pitted BRASS gone dark and green at the edges, "
|
|
"the left spine is a deep BONE-WHITE strip, and the recessed right slot is near-black "
|
|
"with a faint red glow inside it. Add fine cracks and old dark stains across the field, "
|
|
"and a few chipped patches on the frame — a board nobody has cleaned in a long time. "
|
|
)),
|
|
}
|
|
|
|
def install(name):
|
|
spec = BOARDS[name]
|
|
os.makedirs(SCRATCH, exist_ok=True)
|
|
raw = f'{SCRATCH}/board_{name}.png'
|
|
ref = os.path.join(HERE, spec['ref'])
|
|
# the reference is upscaled: a 300x84 source is small enough that the generator reads it as a
|
|
# thumbnail and answers with a thumbnail's worth of detail
|
|
Image.open(ref).convert('RGB').resize((W * 4, H * 4), Image.NEAREST).save(f'{SCRATCH}/board_{name}_ref.png')
|
|
r = subprocess.run([PY, GEN, spec['subject'] + STYLE, '--model', MODEL,
|
|
'--input', f'{SCRATCH}/board_{name}_ref.png', '--output', raw],
|
|
capture_output=True, text=True, cwd=os.path.dirname(HERE))
|
|
if not os.path.exists(raw):
|
|
print(f'[{name}] FAILED: generator returned no image\n{r.stdout[-500:]}\n{r.stderr[-500:]}')
|
|
return False
|
|
im = Image.open(raw).convert('RGB').resize((W, H), Image.BOX) # box filter: an honest downscale
|
|
# quantise to a small flat palette, the same idea _fine.py ends on — a generated plate arrives
|
|
# with hundreds of near-identical shades and reads as a photo of pixel art rather than as pixel art
|
|
a = np.array(im)
|
|
q = Image.fromarray(a).quantize(colors=24, method=Image.MEDIANCUT, dither=Image.NONE).convert('RGB')
|
|
q.convert('RGBA').save(os.path.join(HERE, spec['out']))
|
|
print(f'[{name}] installed {spec["out"]} {W}x{H}')
|
|
return True
|
|
|
|
if __name__ == '__main__':
|
|
names = sys.argv[1:] or list(BOARDS)
|
|
ok = all(install(n) for n in names)
|
|
sys.exit(0 if ok else 1)
|