The opponent is complete: 3 wound stages x 11 states, and a gate for wounds that never happened

The serve and the fall now exist at every wound stage, so a one-armed opponent
still serves and a headless one still goes down, instead of reverting to a
whole body to do it. 33 frames, no gaps.

THE BALL IS OUT OF THE SERVE SPRITES. Oliver: it was painted into three frames
and the game draws the ball itself, so every one of them would have ridden
along at the wrong place, the wrong time and in the wrong number. Hands open
and empty now, poses unchanged as if a ball were there.

THE FOURTH GATE, and the one nothing else could see: "copy this exactly, change
ONE thing" loses when the change is a REMOVAL. Taking a head off worked every
time — it is dramatic. Taking an ARM off mostly did not: five of seven wound
frames came back as the reference faithfully redrawn, 0.02-0.05 of their own
silhouette different, and the ladder quietly HEALED the body the score had just
maimed. Frame count, background, clipping, detail, kit and stride all pass that
sheet. A batch now declares how far it must move from the sheet it was drawn
from — a copy scores 0.02-0.05, a real amputation 0.12-0.38, a decapitation
0.27-0.75 — and the whole set now measures 0.18-0.65.

AND THE BACKGROUND RULE MOVED TO THE FRONT OF EVERY PROMPT. Six rejections in
eleven rolls were "background is not magenta", and the generator was not
glitching: it was painting the zombies ON A TENNIS COURT. The chained restyle
subjects are long, and the magenta instruction sat behind three hundred words
of "keep everything exactly as the reference", where it stopped being honoured.
Same sentence, said before anything can bury it.

One design consequence kept rather than papered over: the missing arm IS the
tossing arm, so a wounded opponent tosses off the racket face the way one-armed
players actually do. The wound changes how he plays, which is what slice 3 has
been asking for.

Known and NOT fixed: the shorts hue drifts from -9 to -19 down the ladder. Each
stage is measured against the one before it, so a legal 4-point hop three times
over is a 14-point total nobody authorised. The fix is to anchor every stage to
the healthy frame with a total budget instead of a per-hop one.

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

No files matched your search

+43 -5
View File
@@ -55,6 +55,9 @@ CANVAS = (
"The canvas is a WIDE LANDSCAPE STRIP and every figure stands side by side in ONE SINGLE HORIZONTAL "
"ROW on the same ground line — never a grid, never two rows, never stacked."
)
BG_FIRST = ("THE BACKGROUND OF THE OUTPUT IMAGE MUST BE ONE FLAT SOLID MAGENTA #FF00FF. No court, "
"no grass, no lines, no scenery, no sky, no gradient — nothing behind the figures but flat "
"magenta, edge to edge. ")
STYLE = RES + FIDELITY + CANVAS
NEW_STYLE = RES + (
"Match the reference's DRAWING STYLE, its overall figure height and its facing — but the "
@@ -177,16 +180,40 @@ def kit_off(outs, ref_name, tol=(12, 10, 1.5, 4)):
# opt-in per batch (`stride: [[i, j]]`) rather than applied to everything with two frames in it.
STRIDE_H, STRIDE_TOP, STRIDE_LEG, STRIDE_HIP = 0.04, 0.20, 0.30, 0.62
def stride_stats(pa, pb):
def _masks(pa, pb):
a, b = Image.open(pa).convert('RGBA'), Image.open(pb).convert('RGBA')
w, h = max(a.width, b.width), max(a.height, b.height)
def m(im):
c = Image.new('RGBA', (w, h)); c.paste(im, ((w - im.width) // 2, h - im.height))
return np.array(c)[..., 3] > 110
ma, mb = m(a), m(b)
return a, b, m(a), m(b), h
def _d(x, y):
return round(float((x ^ y).sum() / max(1, (x | y).sum())), 3)
def stride_stats(pa, pb):
a, b, ma, mb, h = _masks(pa, pb)
hip = int(h * STRIDE_HIP)
d = lambda x, y: round(float((x ^ y).sum() / max(1, (x | y).sum())), 3)
return round(a.height / max(1, b.height), 3), d(ma[:hip], mb[:hip]), d(ma[hip:], mb[hip:])
return round(a.height / max(1, b.height), 3), _d(ma[:hip], mb[:hip]), _d(ma[hip:], mb[hip:])
# ---- DID THE CHANGE ACTUALLY HAPPEN? A chained restyle asks for one sheet redrawn with ONE THING
# different, and "copy this exactly" reliably beats "…but change that": removing a HEAD works every
# time (it is dramatic), removing an ARM mostly did not — five of seven wound frames came back as
# the reference redrawn, differing by 0.02-0.05 of their own silhouette, and the ladder silently
# healed the body it had just maimed. So a batch can declare how much it must differ from the sheet
# it was drawn FROM. Measured against the known numbers: a copy scores 0.02-0.05, a real amputation
# 0.12-0.38, a decapitation 0.27-0.75. Nothing else in the pipeline can see this — the art is
# beautiful, correctly coloured, correctly posed and completely wrong.
def changed_off(outs, refs, want):
for o, r in zip(outs, refs):
rp = os.path.join(HERE, r + '.png')
if not os.path.exists(rp): continue
_, _, ma, mb, _ = _masks(o, rp)
d = _d(ma, mb)
if d < want:
return (f'{os.path.basename(o)} is {r} redrawn — only {d:.3f} of the silhouette differs '
f'(want >= {want}). The thing that was supposed to change did not change.')
return None
def stride_off(outs, pairs):
for i, j in pairs:
@@ -245,7 +272,14 @@ def run(name, spec, dry=False):
ref = ref_sheet(shown, f'{SCRATCH}/{name}_ref.png', spec.get('scale', 2.0),
frames=len(sprites) + (1 if lead else 0))
sheet = f'{SCRATCH}/{name}_hi.png'
prompt = f"{spec['subject']} {STATE_STYLE if spec.get('state') else POSE_STYLE if spec.get('pose') else NEW_STYLE if spec.get('newart') else STYLE}"
# THE BACKGROUND RULE GOES FIRST, not only at the end of the style block. The chained restyle
# subjects are long, and with the magenta instruction sitting behind three hundred words of
# "keep everything exactly as the reference" the generator started painting the figures on a
# COURT — grey-blue, grey-green, dark green backgrounds, six rejections in eleven rolls. Same
# sentence, said before anything else has had a chance to bury it.
prompt = (BG_FIRST + spec['subject'] + ' '
+ (STATE_STYLE if spec.get('state') else POSE_STYLE if spec.get('pose')
else NEW_STYLE if spec.get('newart') else STYLE))
if dry:
print(f'[{name}] would generate {len(sprites)} frames: {", ".join(sprites)}'); return True
print(f'[{name}] generating {len(sprites)} frames...')
@@ -287,6 +321,10 @@ def run(name, spec, dry=False):
if spec.get('stride'):
bad = stride_off(outs, spec['stride'])
if bad: print(f'[{name}] REJECTED: {bad}'); return False
# a chained restyle declares how far it must move from the sheet it was drawn from
if spec.get('changed') and len(spec.get('ref_only', [])) == len(sprites):
bad = changed_off(outs, spec['ref_only'], spec['changed'])
if bad: print(f'[{name}] REJECTED: {bad}'); return False
for o, s in zip(outs, sprites):
if spec.get('check', 'finer') == 'finer': colour_match(o, s)
Image.open(o).save(os.path.join(HERE, s + '.png'))