Pixellab based system

This commit is contained in:
=
2026-10-01 22:46:04 -05:00
parent 97a27f9a42
commit 0e905a2824
650 changed files with 10879 additions and 113 deletions

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env python3
"""
Validate the part library in parts/.
Validate a part library (default: the pixellab one in parts_pl/; --library classic for parts/).
Fails on:
- a missing file for any part listed in sprite_parts.py
@@ -8,9 +8,10 @@ Fails on:
- any partial alpha
- a missing (or duplicated) required marker for the slot / body type
- any legal composite that triggers "canvas expanded": checked exhaustively per body
via bounding boxes, plus a random sample of real composites
via bounding boxes (only for libraries without a fits() rule), plus a random sample of
real composites
python validate_parts.py [--sample N]
python validate_parts.py [--library pixellab|classic] [--sample N] [--verbose]
"""
import random
import sys
@@ -21,10 +22,11 @@ import numpy as np
from PIL import Image
import generate_sprite as gs
import sprite_parts as cfg
from sprite_parts import TYPES
PARTS = Path("parts")
ALLOWED = {tuple(v) for v in gs.KEYS.values()} | {gs.PUPIL, gs.EYE_WHITE} | {tuple(v) for v in gs.MARKERS.values()}
lib = None # registry module, set in main
PARTS = None
ALLOWED = {tuple(v) for v in gs.KEYS.values()} | {gs.PUPIL, gs.EYE_WHITE} | {tuple(v) for v in gs.MARKERS.values()} | gs.FIXED_COLORS
MARKER_BY_COLOR = {v: k for k, v in gs.MARKERS.items()}
@@ -87,7 +89,7 @@ def exhaustive_extents():
found by taking each side's extreme from a different slot (or one part covering both)."""
problems = []
worst = {}
for body in cfg.BODY:
for body in lib.BODY:
plan = body["plans"][0]
b = bbox_of("body", body["name"])
if b is None:
@@ -100,7 +102,7 @@ def exhaustive_extents():
opt = {}
for slot in gs.PICK_ORDER[1:]:
lst = []
for p in cfg.SLOTS[slot]:
for p in lib.SLOTS[slot]:
if p["name"] == "none" or plan not in p["plans"] or p["name"] in body["deny"]:
continue
if p["requires"] and not set(p["requires"]) & tags:
@@ -138,10 +140,10 @@ def exhaustive_extents():
w, wwhy = worst_span(0, 2, fx0, fx1)
h, hwhy = worst_span(1, 3, fy0, fy1)
worst[body["name"]] = (w, h, wwhy, hwhy)
if w + 2 > gs.CANVAS:
problems.append(f"{body['name']}: worst width {w} > {gs.CANVAS - 2} ({wwhy})")
if h + gs.GROUND_MARGIN + 1 > gs.CANVAS:
problems.append(f"{body['name']}: worst height {h} > {gs.CANVAS - gs.GROUND_MARGIN - 1} ({hwhy})")
if w + 2 > lib.CANVAS:
problems.append(f"{body['name']}: worst width {w} > {lib.CANVAS - 2} ({wwhy})")
if h + lib.GROUND_MARGIN + 1 > lib.CANVAS:
problems.append(f"{body['name']}: worst height {h} > {lib.CANVAS - lib.GROUND_MARGIN - 1} ({hwhy})")
return problems, worst
@@ -149,37 +151,52 @@ def sample_composites(n):
problems = []
rng = random.Random(12345)
for i in range(n):
ptype = rng.choice(cfg.TYPES)
ptype = rng.choice(TYPES)
prng = random.Random(i)
plan, chosen, markings, tags = gs.pick_parts(ptype, prng)
plan, chosen, markings, tags = gs.pick_parts(lib, ptype, prng)
if any(not (PARTS / s / f"{p['name']}.png").exists() for s, p in chosen.items() if p["name"] != "none"):
continue
spr = gs.compose(str(PARTS), chosen, markings, prng)
if max(spr.width + 2, spr.height + gs.GROUND_MARGIN + 1) > gs.CANVAS:
if max(spr.width + 2, spr.height + lib.GROUND_MARGIN + 1) > lib.CANVAS:
problems.append(f"sample {ptype} seed {i}: {spr.width}x{spr.height} "
f"({', '.join(f'{s}={p['name']}' for s, p in chosen.items())})")
return problems
def main(argv):
global lib, PARTS
n = 2000
if "--sample" in argv:
n = int(argv[argv.index("--sample") + 1])
name = argv[argv.index("--library") + 1] if "--library" in argv else gs.DEFAULT_LIBRARY
if name not in gs.LIBRARIES:
print(f"unknown library {name!r}, choose from {', '.join(gs.LIBRARIES)}")
return 2
lib = gs.load_library(name)
PARTS = Path(lib.PARTS_DIR)
print(f"library {name} ({PARTS}/, canvas {lib.CANVAS})")
if not lib.BODY:
print("library is empty (no bodies), nothing to validate")
return 0
errs = []
for slot, parts in cfg.SLOTS.items():
for slot, parts in lib.SLOTS.items():
for p in parts:
if p["name"] != "none":
errs += check_file(slot, p)
size_probs, worst = exhaustive_extents()
errs += size_probs
if hasattr(lib, "fits"):
worst = {}
print("exhaustive size check skipped: library has a fits() rule, relying on the random sample")
else:
size_probs, worst = exhaustive_extents()
errs += size_probs
errs += sample_composites(n)
for e in errs:
print("FAIL", e)
if "--verbose" in argv:
for b, (w, hh, ww, hw) in sorted(worst.items()):
print(f"{b:16s} worst {w}x{hh} w:{ww} h:{hw}")
total = sum(1 for parts in cfg.SLOTS.values() for p in parts if p["name"] != "none")
present = sum(1 for slot, parts in cfg.SLOTS.items() for p in parts
total = sum(1 for parts in lib.SLOTS.values() for p in parts if p["name"] != "none")
present = sum(1 for slot, parts in lib.SLOTS.items() for p in parts
if p["name"] != "none" and (PARTS / slot / f"{p['name']}.png").exists())
print(f"{present}/{total} parts present, {len(errs)} problem(s)")
return 1 if errs else 0