THE STRIDE GATE: a walk cycle is one drawing with the legs moved, and now something checks
Oliver, on the sprites, again: "when people on a tennis court walk or run their legs move... this pattern is almost always failing." It has failed repeatedly because NOTHING IN THE PIPELINE MEASURED IT. Frame count, magenta background, clipped edges and detail density all pass cleanly on two unrelated illustrations of the same character, which is exactly what the generator produces when asked for "the same frame with the other leg forward". Three numbers per declared pair, silhouette only, on the feet-aligned centred overlay drawStatic actually draws: HGT height ratio — off 1.00 means the body grows and shrinks once per step TOP difference ABOVE the hip — near zero for a real pair: same man, same camera LEG difference BELOW it — this is the step Calibrated on the pairs that read (brute wound lurch 1.00/0.000/0.638, shambler 1.00/0.070/0.520) against the ones that do not (TOP 0.47-0.85, or LEG 0.09). Opt-in per batch via `stride: [[i, j]]`, because the player's four-frame RUN sits at TOP 0.23 and is right — a run swings the arms too. `--audit` runs the same gate over the sprites ALREADY INSTALLED and exits non-zero on a failure: a gate that only fires on new rolls says nothing about the art already in the game, which is where this was noticed. Five of the six racket pairs fail it today. And the kit gate, which already existed and was never switched on here: Oliver spotted the headless men's trousers had changed colour. Measured — the headless set's near-white fraction DOUBLES (9.5 -> 19.5) where losing a head only accounts for ~1.3 of that, and the wounded swing drifted the other way (shorts 60.5 against its own stage's 85). palette_ref + a per-batch tolerance now catch all three drifted sets and pass the one that looked right. Fixed while in there, mine from the last commit: the wounded stride stepped off walkT, a wall clock, while the healthy one steps off ground covered — the exact foot-slide this project banned for the heavy bodies. No art regenerated yet: the gate and the audit come first, so a re-roll can be told from a lucky one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
19ee62067a
commit
1c405f045f
5 files changed
+138
-9
No files matched your search
+67
-1
@@ -149,6 +149,48 @@ def kit_off(outs, ref_name, tol=(12, 10, 1.5)):
|
||||
f'(tolerance {tol})')
|
||||
return None
|
||||
|
||||
# ---- THE STRIDE GATE. A walk cycle is ONE DRAWING WITH THE LEGS MOVED. The generator does not know
|
||||
# that: asked for "frame 2, then the same with the other leg forward", it draws two independent
|
||||
# illustrations of the character — different torso twist, different arm positions, different camera
|
||||
# distance — and every other check in this file passes on that happily (the frame count is right, the
|
||||
# background is magenta, nothing touches an edge, the detail is fine). So the failure has been
|
||||
# INVISIBLE and it has shipped repeatedly: the turned CPU's z_c1/z_c2 differ by 0.85 above the hip.
|
||||
#
|
||||
# Three numbers, silhouette only, on a feet-aligned centred overlay — which is how drawStatic puts
|
||||
# them on screen, so this measures what the eye actually sees:
|
||||
# HGT height ratio. drawStatic sizes every frame by its own PNG height, so anything off 1.00 is a
|
||||
# body that GROWS AND SHRINKS once per step (the shambler's walk does, by 8%).
|
||||
# TOP how much differs ABOVE the hip. Near zero for a real stride pair — same man, same camera.
|
||||
# LEG how much differs BELOW it. This is the step. Near zero means the legs never moved.
|
||||
# Calibrated on the pairs that read: the brute's wound lurch is (1.00, 0.000, 0.638) and the
|
||||
# shambler's is (1.00, 0.070, 0.520); the failures sit at TOP 0.47-0.85 or LEG 0.09. The player's
|
||||
# four-frame RUN sits at TOP 0.23 and is fine — a run swings the arms too — which is why this gate is
|
||||
# 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):
|
||||
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)
|
||||
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:])
|
||||
|
||||
def stride_off(outs, pairs):
|
||||
for i, j in pairs:
|
||||
hgt, top, leg = stride_stats(outs[i], outs[j])
|
||||
why = ('body pulsates' if abs(hgt - 1) > STRIDE_H else
|
||||
'whole body redrawn, not a leg swap' if top > STRIDE_TOP else
|
||||
'the legs do not move' if leg < STRIDE_LEG else None)
|
||||
if why:
|
||||
return (f'frames {i}/{j} are not one body with the legs moved — {why} '
|
||||
f'(height x{hgt:.2f}, upper {top:.3f}, legs {leg:.3f}; '
|
||||
f'want ~1.00 / <={STRIDE_TOP} / >={STRIDE_LEG})')
|
||||
return None
|
||||
|
||||
def not_magenta(sheet):
|
||||
"""The generator sometimes ignores the magenta and paints its own background. That was written
|
||||
off as harmless — the key uses the corner colour — right up until it returned a GREEN sheet and
|
||||
@@ -231,7 +273,10 @@ def run(name, spec, dry=False):
|
||||
if sum(after) / len(after) < sum(before) / len(before) * bar:
|
||||
print(f'[{name}] REJECTED: not finer than what it replaces {before} -> {after}'); return False
|
||||
if spec.get('palette_ref'):
|
||||
bad = kit_off(outs, spec['palette_ref'])
|
||||
bad = kit_off(outs, spec['palette_ref'], tuple(spec.get('palette_tol', (12, 10, 1.5))))
|
||||
if bad: print(f'[{name}] REJECTED: {bad}'); return False
|
||||
if spec.get('stride'):
|
||||
bad = stride_off(outs, spec['stride'])
|
||||
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)
|
||||
@@ -239,9 +284,30 @@ def run(name, spec, dry=False):
|
||||
print(f'[{name}] installed. detail {before} -> {after}')
|
||||
return True
|
||||
|
||||
# --audit runs the stride gate over the sprites ALREADY INSTALLED, which is the other half of it
|
||||
# being useful: a gate that only fires on new rolls says nothing about the art already in the game,
|
||||
# and the art already in the game is where this was first noticed.
|
||||
def audit(B, names):
|
||||
print(f"{'':34s} {'HGT':>6s} {'TOP':>6s} {'LEG':>6s} verdict")
|
||||
bad = []
|
||||
for n in names:
|
||||
for i, j in B[n].get('stride', []):
|
||||
a, b = (os.path.join(HERE, B[n]['sprites'][k] + '.png') for k in (i, j))
|
||||
if not (os.path.exists(a) and os.path.exists(b)): continue
|
||||
hgt, top, leg = stride_stats(a, b)
|
||||
why = stride_off([a, b], [[0, 1]])
|
||||
label = f"{n} {B[n]['sprites'][i]}/{B[n]['sprites'][j]}"
|
||||
print(f'{label:34s} {hgt:6.2f} {top:6.3f} {leg:6.3f} {"FAIL" if why else "ok"}')
|
||||
if why: bad.append(label)
|
||||
print('\nFAILS THE GATE:', ', '.join(bad) if bad else 'none')
|
||||
return not bad
|
||||
|
||||
if __name__ == '__main__':
|
||||
args = sys.argv[1:]
|
||||
B = batches()
|
||||
if '--audit' in args:
|
||||
names = [a for a in args if not a.startswith('--')] or [k for k, v in B.items() if v.get('stride')]
|
||||
sys.exit(0 if audit(B, names) else 1)
|
||||
if not args or '--list' in args:
|
||||
for k, v in B.items(): print(f'{k:16s} {len(v["sprites"]):2d} frames {", ".join(v["sprites"])}')
|
||||
sys.exit()
|
||||
|
||||
Reference in new issue
Block a user