# _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). # # WHAT THE FIRST VARIANT SHOWED, and what the next attempt should change. The brute's PLAY sheet is # a good brute. Its serve and fall sheets are each a slightly DIFFERENT brute, because all three # restyle from the master independently — so each one invents the character afresh from the words, # and "pale grey-green work shirt" lands somewhere new every time. The palette gate now measures the # gap (15 to 48 against z_br1, where a good frame is under 8.5) and refuses them. # # The fix is to give the later sheets BOTH things: the master frame for the POSE and the variant's # own first sheet for the CHARACTER, instead of the words for the character. That is the same lesson # as the wound stages — a reference beats a description — applied one level up. Until then a variant # is reliable for its play sheet and a gamble for the other two. # # 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 # THE FIRST SHEET describes the character in WORDS, because at that point no picture of it exists. 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. ") # ...AND EVERY SHEET AFTER IT DESCRIBES THE CHARACTER WITH A PICTURE OF IT. This is the whole fix. # The brute's three sheets each restyled from the master independently and each re-derived "pale # grey-green work shirt" from the same paragraph, so the game got three different brutes and the # palette gate — correctly — refused two of them. A paragraph is a lossy specification of a # character; the character is not. # # It is also the pattern that already worked: the racket zombie's own play sheet was generated from # [z_c1, o_idle1, o_run1, o_swing1] — the character as an IMAGE, the poses as IMAGES — in one call, # first time. I had it, and then did not use it for variants. RESTYLE_KNOWN = ( "The FIRST reference image is THE CHARACTER — the exact zombie to draw, and the ONLY source for " "who it is: its build, its bulk, its height, its clothing and every colour of it. Copy that " "character exactly. " "The REMAINING reference images are {n} POSES, and are there ONLY to show what the body is doing " "— never who it is. " "Draw THE CHARACTER FROM THE FIRST IMAGE in each of those {n} poses, in the same order, at the " "same size and the same distance from the camera, all facing the same way. The character never " "changes; only the pose does. " "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. ") # ---- THE WOUND LADDER. A match opponent is scored ON ITS BODY: every point won takes a stage off # it, so a variant that only exists whole would jump from untouched to dead with nothing between. # Each stage is a restyle of THE STAGE ABOVE IT — that chaining is what finally made damage # accumulate, because the missing arm then rides in the PICTURE instead of having to win an argument # with it. The wound rows have no separate stance frame: the shuffle doubles as one, which is how # drawZombie already reads them. WOUND = ( "The reference image is a {n}-FRAME SPRITE SHEET of one zombie tennis player. REDRAW THE WHOLE " "SHEET with ONE THING CHANGED: {change} " "EVERYTHING ELSE STAYS EXACTLY AS IT IS IN THE REFERENCE — the same character, the same clothing " "and its exact shades, the same skin, the same racket, the same {n} poses in the same order, the " "same leg positions, the same facing, the same build. There is NO TENNIS BALL in any frame. " "Output exactly {n} figures side by side in ONE horizontal row, all on the same ground line. ") ARM_OFF = ( "the arm WITHOUT the racket is COMPLETELY GONE — torn off at the shoulder, leaving a ragged " "stump gushing blood down that side and the clothing slashed open across the ribs. COUNT THE " "ARMS: there must be EXACTLY ONE ARM in every single frame, the one holding the racket. Nothing " "hangs from the other shoulder. A frame with two arms in it is wrong — this is the whole change " "being asked for, so do not copy the reference's second arm across. Where the reference uses " "that arm, the stump is thrown out in its place instead. ") HEADLESS = ( "the HEAD IS COMPLETELY GONE — no head, no face, no hair anywhere above the shoulders, only a " "ragged gushing neck stump, so every figure is A CLEAR HEAD SHORTER than in the reference. It is " "ALREADY missing one arm in the reference and it STAYS missing — it still has only one arm. ") # ...and because the missing arm IS the tossing arm, a wounded server has to toss the way one-armed # players really do. The wound changing how it plays is the point, not a detail. ONE_ARM_SERVE = ( "Because it has only one arm it can no longer throw the ball up with a free hand: in the TOSS " "frame the stump is flung upward instead and the racket hand flicks up in front of the face, as " "a one-armed player tosses off the racket face. ") 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) seed = lambda src, dst: shutil.copy(os.path.join(HERE, src + '.png'), os.path.join(HERE, dst + '.png')) made = [] def batch(name, sprites, refs, subject, stride=None, changed=None, newart=False, lead=False): # `lead` means the first reference is THE CHARACTER and not a pose — it is shown to the # generator and is not one of the frames asked for, so it is skipped when pairing references # to outputs (both for seeding and for the run-time baseline in _regen). pose_refs = refs[1:] if lead else refs # Only the sheets drawn off the MASTER are seeded here. A chained wound stage is seeded by # _regen at run time, from whatever its reference has become by then — seeding it now would # score it against a copy of the master, two steps better than the stage it follows. if newart: for src, dst in zip(pose_refs, sprites): # NEVER OVERWRITE ART THAT EXISTS. Re-running --variant to rewrite a spec used to # re-seed every placeholder from the master, which quietly replaced a finished brute's # serve and fall sheets with copies of the lean man they were drawn from. A # placeholder is only ever a stand-in for a frame that is not there yet. if not os.path.exists(os.path.join(HERE, dst + '.png')): seed(src, dst) spec = dict(sprites=sprites, ref_only=list(refs), check='loose', anchor=0, subject=subject) spec['newart' if newart else 'state'] = True if stride: spec['stride'] = stride if changed: spec['changed'] = changed # EVERY SHEET OF A VARIANT IS PAINTED IN THE FIRST SHEET'S COLOURS. Anchoring each stage to # the one before it let a legal hop compound into a drift nobody authorised; anchoring all of # them to the healthy body means the whole variant, wounds and all, stays one character. # The first sheet is what DEFINES the palette, so it is the only one not measured against it. if sprites[0] != K + '1': spec['palette_anchor'] = K + '1' B[name] = spec made.append(name) print(f'{name:16s} <- {refs[0]}… {", ".join(sprites)}') K = 'z_' + key # --- the whole body. The PLAY sheet invents the character from the words; serve and fall are then # drawn from the character it produced (K+'1' leads their reference list) plus the master's poses. # That ordering is why the three must be run in the order printed at the end. for part, frames in SHEETS.items(): first = part == 'play' refs = frames if first else [K + '1'] + frames batch(f'{key}_{part}', [K + f[3:] for f in frames], refs, (RESTYLE.format(n=len(frames), who=who) + STRIDE_NOTE) if first else RESTYLE_KNOWN.format(n=len(frames)), stride=[[1, 2]] if first else None, newart=True, lead=not first) # --- the two wound stages, each chained off the one above it WALK = ['2', '3', '4'] # shuffle A, shuffle B, forehand — the wound rows' three frames SERVE = ['bounce', 'toss', 'serve'] FALL = ['fall1', 'fall2', 'fall3', 'rise'] for st, change, extra, moved in ((1, ARM_OFF, ONE_ARM_SERVE, 0.10), (2, HEADLESS, '', 0.15)): up = K + (f'w{st - 1}' if st > 1 else '') # the stage this one is drawn from walk_src = [up + s for s in (['', 'b', 's'] if st > 1 else WALK)] batch(f'{key}_w{st}', [f'{K}w{st}', f'{K}w{st}b', f'{K}w{st}s'], walk_src, WOUND.format(n=3, change=change) + 'In particular frames 1 and 2 are ONE DRAWING SHOWN TWICE with only the legs ' 'different — identical above the hips, legs clearly swapped, and NOT identical copies. ', stride=[[0, 1]], changed=moved) batch(f'{key}_serve_w{st}', [f'{K}w{st}' + s for s in SERVE], [up + s for s in SERVE], WOUND.format(n=3, change=change) + extra, changed=moved) batch(f'{key}_fall_w{st}', [f'{K}w{st}' + s for s in FALL], [up + s for s in FALL], WOUND.format(n=4, change=change), changed=moved) json.dump(B, open(bpath, 'w', encoding='utf-8'), indent=1, ensure_ascii=False) print(f'\n{len(made)} batches, 31 frames. Run them IN THIS ORDER — each wound stage is drawn ' f'from the stage above it, so an earlier one must be installed first:\n\n' f' python3 assets/_regen.py {" ".join(made)} --tries=4\n') 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)