THE MASTER SHEETS: a new zombie is now three calls and no pose design

Oliver asked what the reference sheet is called. It did not have a name — it
was assembled per batch into /tmp and thrown away, so the pose set that took a
day to get right existed only as 33 loose PNGs.

  assets/_sheet_master_play.png    stance, shuffle A, shuffle B, forehand
  assets/_sheet_master_serve.png   bounce, toss, strike
  assets/_sheet_master_fall.png    buckle, knees, down, rise

Magenta-backed, spaced, feet on one ground line — loadable straight into a
generator as-is. `python3 assets/_master.py` rebuilds them from the installed
frames, so the master can never drift from what the game actually draws.

THREE SHEETS AND NOT ONE, for a measured reason: the generator answers at a
fixed width around 1400-1800 px. Four figures across gives each 350-440 px,
which carries the 90-110 px of figure height the style block asks for. All
eleven on one strip would be ~130 px each and the detail gate would reject
every roll. Four, four and three is what fits.

And the thing the master is FOR:

  python3 assets/_master.py --variant br "an ENORMOUS FAT ZOMBIE BRUTE: …"
  python3 assets/_regen.py br_play br_serve br_fall --tries=4

which writes three restyle batches, seeds their placeholders and declares the
stride pair so the gate covers the new body from its first roll. A variant is
three generator calls and NO POSE DESIGN AT ALL — the reference specifies
eleven poses that already work and the prompt only says who to draw. Measured
on the brute test: the restyled stride pair came back at upper 0.148 / legs
0.618 against the master's own 0.157 / 0.565, very slightly better than what it
copied.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Opus 5 committed 2026-08-12 12:52:24 +02:00
1 parent 5c0db0a22e
commit 7056945e62
4 files changed
+96

No files matched your search

+96
View File
@@ -0,0 +1,96 @@
# _master.py — build THE MASTER SHEETS: the canonical pose set a new zombie is restyled FROM.
#
# python3 assets/_master.py # rebuild assets/_sheet_master_*.png from the installed frames
#
# WHY THIS EXISTS. A new zombie variant used to mean inventing a character AND a pose set, and the
# generator answers that by inventing a fresh figure per frame — which is how the walk cycles ended
# up as eleven unrelated illustrations. Handing it a FINISHED SHEET and asking for a restyle turns
# the job into a transform: the reference does the specifying and the prompt only says who to draw.
# Measured, the poses survive it intact (the brute restyle came back at upper 0.148 / legs 0.618
# against the master's own 0.157 / 0.565 — very slightly BETTER than what it copied).
#
# WHY THREE SHEETS AND NOT ONE. The generator answers at a fixed width, around 1400-1800 px. At four
# figures across each gets 350-440 px, which carries the 90-110 px of figure height the style block
# asks for. All eleven on one strip would be ~130 px each and the detail gate would reject every
# roll. Four, four and three is the shape that fits — and it is already the shape of the three
# batches that made them.
import os, sys
from PIL import Image
HERE = os.path.dirname(os.path.abspath(__file__))
MAGENTA = (255, 0, 255, 255)
# The master pose set, split the way it is generated. Keep these in step with config.BENCH — that is
# the screen these frames are judged on.
SHEETS = {
'play': ['z_r1', 'z_r2', 'z_r3', 'z_r4'], # stance, shuffle A, shuffle B, forehand
'serve': ['z_rbounce', 'z_rtoss', 'z_rserve'], # bounce, toss, strike
'fall': ['z_rfall1', 'z_rfall2', 'z_rfall3', 'z_rrise'], # buckle, knees, down, rise
}
def build(name, frames, scale=2, gap=90, pad=70):
ims = [Image.open(os.path.join(HERE, f + '.png')).convert('RGBA') for f in frames]
ims = [i.resize((i.width * scale, i.height * scale), Image.NEAREST) for i in ims]
w = sum(i.width for i in ims) + gap * (len(ims) - 1) + pad * 2
h = max(i.height for i in ims) + pad * 2
s = Image.new('RGBA', (w, h), MAGENTA)
x = pad
for i in ims:
s.paste(i, (x, h - pad - i.height), i) # feet on one ground line, as the game draws them
x += i.width + gap
out = os.path.join(HERE, f'_sheet_master_{name}.png')
s.save(out)
print(f'{os.path.basename(out):26s} {w}x{h} {len(frames)} frames ({", ".join(frames)})')
# ---- A NEW ZOMBIE OFF THE MASTER ------------------------------------------------------------
# python3 assets/_master.py --variant br "an ENORMOUS FAT ZOMBIE BRUTE: a huge sagging belly, a
# filthy stretched shirt, heavy brown work trousers…"
# python3 assets/_regen.py br_play br_serve br_fall --tries=4
#
# Writes three batches that RESTYLE the master sheets into a new character, seeds their placeholder
# PNGs, and declares the stride pair so the gate covers the new body from its first roll. The whole
# variant is then three generator calls and no pose design at all — which is the point of having a
# master: the reference specifies eleven poses that already work, and the prompt only says who.
import json, shutil, collections
RESTYLE = (
"The reference image is a {n}-FRAME SPRITE SHEET of one zombie tennis player. Redraw THE WHOLE "
"SHEET as a COMPLETELY DIFFERENT ZOMBIE: {who} "
"KEEP ALL {n} POSES EXACTLY AS THEY ARE IN THE REFERENCE — the same stance, the same steps with "
"the same leg forward in each, the same swing, the same facing, the same arm and racket "
"positions, the same order left to right, the same total height and the same distance from the "
"camera. Only the CHARACTER changes; the POSES do not. "
"There is NO TENNIS BALL anywhere in any frame — the game draws the ball itself, so the hands and "
"the air are empty and the poses are the same as if a ball were there. "
"Output exactly {n} figures side by side in ONE horizontal row, all on the same ground line. ")
STRIDE_NOTE = ("In particular frames 2 and 3 must stay what they are in the reference: ONE DRAWING "
"SHOWN TWICE with only the legs different — everything above the hips identical "
"between them, the legs clearly swapped, and the two frames NOT identical copies. ")
def variant(key, who):
bpath = os.path.join(HERE, '_regen_batches.json')
B = json.load(open(bpath, encoding='utf-8'), object_pairs_hook=collections.OrderedDict)
for part, frames in SHEETS.items():
names = ['z_' + key + f[3:] for f in frames] # z_r1 -> z_br1, z_rtoss -> z_brtoss
for src, dst in zip(frames, names): # placeholders: the density bar needs a baseline
shutil.copy(os.path.join(HERE, src + '.png'), os.path.join(HERE, dst + '.png'))
spec = dict(sprites=names, ref_only=frames, newart=True, check='loose', anchor=0,
subject=RESTYLE.format(n=len(frames), who=who)
+ (STRIDE_NOTE if part == 'play' else ''))
if part == 'play': spec['stride'] = [[1, 2]] # the gate covers it from the first roll
B[f'{key}_{part}'] = spec
print(f'{key}_{part:6s} -> {", ".join(names)}')
json.dump(B, open(bpath, 'w', encoding='utf-8'), indent=1, ensure_ascii=False)
print(f'\nnow run: python3 assets/_regen.py {key}_play {key}_serve {key}_fall --tries=4')
if __name__ == '__main__':
if '--variant' in sys.argv:
i = sys.argv.index('--variant')
variant(sys.argv[i + 1], sys.argv[i + 2])
sys.exit()
for name, frames in SHEETS.items():
missing = [f for f in frames if not os.path.exists(os.path.join(HERE, f + '.png'))]
if missing:
print(f'[{name}] SKIPPED, missing: {", ".join(missing)}'); continue
build(name, frames)
Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB