The reference sheet was the bug: a clipped head, five scales, frozen legs

Oliver walked the sprite bench and reported the same two faults on every family:
the healthy walk loses its head, and the wounded walk's feet do not move. Both
were in the ONE FILE every zombie tennis character is drawn from — and the family
pipeline had copied it faithfully, which is the pipeline working.

WHERE IT CAME FROM. That sheet was not composed from the sprites. It was a
GENERATED RE-DRAW of the club pro, made in nine separate 4K calls, one per row
band, then stacked (gen4k.py, in a session scratchpad, untracked and now gone).
Nine calls returned nine scales, and call 1 came back with the shuffle frame
decapitated. Measured against the sprite it was drawn from, that one figure
scaled 2.341 wide and 1.996 tall where all thirty others were proportional to
three decimals: 33 px missing off the top, width untouched. The local input to
that call was exact at 2.000x. The sheet lost the head, not the sprite.

THE FIX IS TO STOP PUTTING THE REFERENCE THROUGH THE MODEL. _famref.py composes
it from the 31 z_r* frames on disk — one scale, applied once, NEAREST — and costs
no tokens. That kills the head and the five scales together, and it explains two
things in _zsheet.py that read like fudges: the fall1 re-anchor and the walk-frame
levelling exist only to undo per-band scale drift. The levelling is also what
turned a headless z_sh2 into a headless AND 13% oversized one, by reading the
missing head as a height error and scaling up to compensate.

THE GATE WAS MISSING ON THE INPUT. _zsheet.py checks what comes BACK from the
model — band count, figure count, stacked bodies, edges, palette, heights — and
nothing had ever checked the sheet handed TO it. _famref.check() re-reads the file
it just wrote with _zsheet's own detection and refuses one where any figure's
width and height scales disagree by 2%, or where a stride pair is frozen.

THE THIRD DEFECT NEEDED NEW ART. z_rw1b had the same legs as z_rw1 (15.1% against
the healthy pair's 51.1%), so the club pro has slid through both wound stages
since the day he shipped, and every family inherited it. _stride.py draws the
missing twin, and what made it work after words had failed for as long as they
were tried is SHOWING the edit: image 2 is the healthy pair side by side, the same
character performing exactly the change being asked for. The headless twin then
took the ARM-OFF pair as its example — a wounded body is a closer picture of the
edit, the same chaining the wound ladder already uses. Its gate measures three
things and prints all three: legs moved, body above the hips did not, racket kept
its bed. The thresholds are CALIBRATED on pairs that already ship, not chosen —
my first guesses rejected z_e1/z_e2 and z_g1/z_g2, both accepted art. And a twin
FILLS the racket bed instead of keying it out: the first rolls came back with 959
px of enclosed transparency against their base's 0, which would have flickered the
racket face empty/filled once per step.

AFTER, on the installed sprites — leg difference within each stride pair, where
the wounded pairs used to sit between 4.8% and 19.7%:

  r  51.1 / 68.1 / 36.7     ru 55.9 / 59.1 / 38.7
  br 40.7 / 42.0 / 26.8     gk 41.1 / 44.6 / 30.6
  sh 59.4 / 71.4 / 43.9     lj 38.4 / 45.5 / 27.4
  bb 56.5 / 67.1 / 43.2     st 51.6 / 66.1 / 35.7

All seven families re-rolled off the rebuilt reference, every play row with its
head, and all 24 family/wound-stage states alternating a real pair — checked by
instrumenting drawImage on the bench, not by eye. br and lj sit lowest on the
headless pair and are worth a re-roll if they ever read as stiff; lj needed one,
its first roll came back frozen on all three pairs with the headless row 31 px
short.

AND THE REFERENCE IS TRACKED NOW. It lived in gitignored _refcheck/, so the thing
the entire pipeline rests on was unreproducible for as long as it existed. It is
assets/_pose_ref_family.png, next to _pose_ref_serve.png, and the scripts that
build and consume it are in the repo instead of a scratchpad.

Also in here: the SHAMBLE BOB, which was a 1 px lift on a wall clock applied to
every body every frame whether it was moving or not — a held pose bobbed forever
and a corpse bobbed while it lay on the ground. It only happens while walking now,
and it steps off ground covered, which is the same rule the strides follow.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Opus 5 committed 2026-08-12 22:45:05 +02:00
1 parent eb0e8775a4
commit e121df569c
226 files changed
+772 -10

No files matched your search

+138 -7
View File
@@ -1768,16 +1768,50 @@ stops being something you police with gates and becomes a property of the reques
the model's own guidance says: frames generated in bulk as a sheet hold their character; frames
generated one at a time do not.
### THE POSE REFERENCE IS `assets/_pose_ref_family.png`, AND IT IS TRACKED
**One file is the reference for every zombie tennis character there will ever be.** 31 frames of the
club pro in 9 row bands, exactly `_zsheet.layout()`'s order, on magenta. Every family in `ZFAMS`
except `r` itself was drawn off it. It lived in `assets/_refcheck/` — which is **gitignored** — so the
thing the entire pipeline rests on was untracked and unreproducible for as long as it existed. It is
`assets/_pose_ref_family.png` now, next to `_pose_ref_serve.png`, and it is committed. It ships in the
image via `COPY assets`, like every other source sheet; that is not worth a Dockerfile exception.
**Nothing in this repo composed it.** `_regen.ref_sheet()` builds ONE row at ONE scale — it is not
what made a 9-row sheet. Whatever did was ad-hoc and is gone. Rebuilding the sheet therefore means
writing the composer INTO the repo, once, with the gate below.
#### NEVER PUT THE REFERENCE THROUGH THE MODEL
The version of this sheet that produced the six broken families was a GENERATED RE-DRAW of `z_r*` —
nine separate 4K calls, one per row band, stacked locally. That round trip is where both of Oliver's
sprite-bench faults came from, and `_famref.py` exists to make it impossible: the sheet is composed
from the sprites on disk, one scale, NEAREST, no API. See "The three defects" below for the numbers.
### The four steps
```bash
# 1. the pose sheet — 31 existing frames, one PNG (any family that is already good; today z_r*)
# Composed locally, no API. See assets/_refcheck/brute_poses_in.png for the shape.
# 2. the character — ONE frame of the new body, upscaled NEAREST, on magenta
# 3. ONE call: poses from image 1, character from image 2
# 4. cut it back down
python3 assets/_zsheet.py <sheet.png> <famkey> --out assets/_refcheck/sprites # review first
python3 assets/_zsheet.py <sheet.png> <famkey> --install # then install
# 1. THE POSE SHEET — composed from the 31 z_r* frames, one scale, NEAREST, gated. No API call.
uv run --with pillow --with numpy python assets/_famref.py
# 2+3. THE CHARACTER (one frame at 4x NEAREST on magenta) and ONE call: poses from image 1,
# character from image 2. Both live in _famroll.py; add a family with one row in its FAMS table.
uv run --with pillow --with requests python assets/_famroll.py <famkey>
# 4. CUT IT BACK DOWN — review first, install second.
uv run --with pillow --with numpy python assets/_zsheet.py assets/_refcheck/fam_<k>.png <k> --out assets/_refcheck/rev_<k>
uv run --with pillow --with numpy python assets/_zsheet.py assets/_refcheck/fam_<k>.png <k> --install
```
`uv run --with …` is how these run: this machine's python has no PIL or numpy of its own, and the
previous session's interpreter was a uv ephemeral env. `SPLAT_PY` still overrides for `_regen.py`.
**A frozen stride pair in the master blocks the whole pipeline, by design.** `_famref.py` refuses to
write the sheet if any of the three stride pairs has legs that barely move — because the model copies
what it is shown, so a frozen pair in the reference is a frozen pair in every family drawn off it.
Draw the missing twin first:
```bash
uv run --with pillow --with numpy --with requests python assets/_stride.py z_rw1 # then z_rw2
uv run --with pillow --with numpy --with requests python assets/_stride.py z_rw2 --example z_rw1
```
Then: add the key to `config.ZFAMS`, put `fam: '<key>'` on an `OPPONENTS` row, bump `SPRV` and
@@ -1860,6 +1894,103 @@ magenta — check a new sheet.
`z_brw1fall` was ONE DRAWING PASTED FOUR TIMES (0.011–0.055 apart, where a real fall sequence is
0.53–0.89). Run it before trusting any chained batch.
### The three defects the sprite bench found, and the fix (2026-08-12, ALL FIXED)
Oliver walked the sprite bench and reported the same two faults on every family: the healthy walk
loses its head, and the wounded walk's feet do not move. All of it traced to `_pose_ref_family.png`,
and the pipeline had copied it faithfully — **which is the pipeline working.** Measured per figure as
the implied scale of the sheet against the `z_r*` PNG it was pasted from, width and height separately:
```
band 0 play 1 w2.339 h2.362 | 2 w2.341 h1.996 <-- the ONLY non-proportional figure in all 31
3 w2.345 h2.360 | 4 w2.333 h2.361
band 1 serve 2.335 | band 2 fall 1.902 | band 3 w1 walk 2.34 | band 4 w1 serve 2.30
band 5 w1 fall 1.922 | band 6 w2 walk 2.38 | band 7 w2 serve 2.047 | band 8 w2 fall 1.945
```
**1. `z_r2` IS PASTED WITH ITS HEAD CLIPPED OFF.** Width scales at 2.341, height at 1.996 — 33 sprite
px missing off the top, width untouched. Every other figure is proportional to three decimals, so
this is a clip and it is the only one in the sheet. `z_r2.png` on disk is fine (123x225, head
present): the damage is in the composition, not the sprite. One bad paste, six families — and this is
the whole of "healthy walk loses its head", because drawZombie picks frames 2/3 as the walk pair.
The last commit's walk-frame levelling makes it WORSE, and that is not the levelling being wrong: it
reads the missing head as a height error and scales the body up to compensate, so `z_sh2` is 147 px
wide against `z_sh3`'s 116. The walk flickers between a normal body and an oversized headless one.
**2. THE SHEET IS NOT ONE SCALE, IT IS FIVE.** The fall bands sit at 81% of the standing bands and the
w2 serve band at 87%. That is the exact bug `_zsheet.py` was written to prevent on the way DOWN,
present on the way UP — the sheet tells the model a falling body is a fifth smaller than a standing
one. **`_zsheet.py`'s fall1 re-anchor and the walk levelling exist only to undo this.** Rebuild the
sheet at one global scale and both should become no-ops; that is the check that they were
compensations and not fudges.
**3. THE WOUNDED STRIDE PAIR WAS NEVER DRAWN, and a rebuild will not fix it.** Leg-region silhouette
difference within the stride pair (feet-anchored, mass-centred, bottom third):
| | healthy 2 <-> 3 | w1 <-> w1b | w2 <-> w2b |
|---|---|---|---|
| the reference sheet | **51.1%** | 14.3% | 14.5% |
| installed `r` | 51.0% | 15.2% | 14.8% |
| the six new families | 41–70% | 4.8–19.7% | 6.2–19.9% |
`z_rw1b` genuinely has the same legs as `z_rw1` on disk, so the club pro has been sliding through both
wound stages since the day he shipped — `r` IS the reference, which is why its numbers match it
exactly. drawZombie is animating correctly (verified by instrumenting `drawImage` on the bench: it
alternates `z_rw1` <-> `z_rw1b` every 0.42 m of ground covered, the right frames at the right
cadence). This one needs the master's `w1b`/`w2b` REDRAWN with the legs swapped before a rebuild is
worth spending a roll on.
**THE GATE THAT WAS MISSING IS ON THE INPUT.** `_zsheet.py` checks the roll that comes back — band
count, figure count, stacked bodies, edges, palette, heights — and nothing had ever checked the sheet
handed TO the model. `_famref.check()` is that gate: it re-reads the sheet it just wrote with
`_zsheet`'s own band and figure detection, and refuses one where any figure's width and height scales
disagree by more than 2% (the head clip was a 0.35 discrepancy) or where a stride pair is frozen.
#### How each one was fixed
| defect | fix | cost |
|---|---|---|
| 1 · head | `_famref.py` composes the sheet from the sprites on disk, one scale, NEAREST | no API call |
| 2 · five scales | same — one `k` for all 31 figures, derived once | no API call |
| 3 · frozen wounded stride | `_stride.py` draws the missing twin, shown the healthy pair as the example | 2 rolls |
Defect 3 is the one that needed new art, and the thing that made it work after every previous attempt
failed was **showing the edit instead of describing it**: IMAGE 2 is the healthy stride pair side by
side, the same character performing exactly the change being asked for. Words alone ("identical above
the hips, legs clearly swapped") had returned near-identical drawings for as long as they were tried.
The headless twin then took the ARM-OFF pair as its example rather than the healthy one — a wounded
body is a closer picture of the edit, the same chaining the wound ladder itself uses.
`_stride.py`'s gate measures three things and prints all three: legs moved (>=30%), body above the
hips did NOT (<=25%), and the racket bed survived (>=0.6x the base's area). **Its thresholds are
calibrated on stride pairs that already ship, not chosen** — the table is in the file. My first
guesses, 35 and 14, rejected `z_e1/z_e2` and `z_g1/z_g2`, both of which are accepted art.
**And a stride twin FILLS the racket bed instead of keying it out** — the opposite of `_zsheet.py`,
for the opposite reason. `_zsheet` cuts a whole family, so every bed keys out together and the racket
is an open hoop throughout. A twin is half of a pair whose other half is already on disk with a
painted bed: the first rolls came back with 959 px of enclosed transparency against their base's 0,
which would have flickered the racket face empty/filled once per step.
#### After the fix, measured on the installed sprites
Every family re-rolled off the rebuilt reference. Leg difference within each stride pair:
| fam | walk 2/3 | w1/w1b | w2/w2b | | fam | walk 2/3 | w1/w1b | w2/w2b |
|---|---|---|---|---|---|---|---|---|
| r | 51.1% | 68.1% | 36.7% | | ru | 55.9% | 59.1% | 38.7% |
| br | 40.7% | 42.0% | 26.8% | | gk | 41.1% | 44.6% | 30.6% |
| sh | 59.4% | 71.4% | 43.9% | | lj | 38.4% | 45.5% | 27.4% |
| bb | 56.5% | 67.1% | 43.2% | | st | 51.6% | 66.1% | 35.7% |
| | | | | | | *was 4.8–19.7%* | | |
Every play row has its head, verified frame by frame in `#bench`, and all 24 family/wound-stage states
alternate a real pair (checked by instrumenting `drawImage` on the bench, not by eye). `br` and `lj`
sit at 26.8 / 27.4 on the headless pair — the hardest one, subtler than the rest but visibly moving;
worth a re-roll if they ever read as stiff. `lj` needed one re-roll: its first came back frozen on all
three pairs with the headless row 31 px short.
### Still open
- The style is close but not identical to the hand-made sprites: softer outline, blended shading