Add native Durak, poker, blackjack, roulette, Buckshot and billiards scenes with animation assets and regression tests. Improve arrival, city, hall, profiles, shops, audio and exit handling; remove the portable C game core and refresh shipping executables.
100 lines
4.9 KiB
Python
100 lines
4.9 KiB
Python
"""Check packed animation assets and snapshots from the production ASM renderer."""
|
|
import hashlib
|
|
import json
|
|
import struct
|
|
from pathlib import Path
|
|
|
|
from PIL import Image, ImageDraw, ImageStat
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
OUTPUT = ROOT / "build/actions"
|
|
|
|
|
|
def check_packs():
|
|
expected = {"street": (640, 480, 1), "guards": (192, 256, 32),
|
|
"merchant": (128, 128, 24), "props": (128, 128, 9),
|
|
"card-backs": (68, 96, 13), "table-hands": (144, 192, 8),
|
|
"materials": (128, 128, 4), "table-active": (640, 480, 1),
|
|
"city-tiles-v4": (128, 128, 16), "city-hands": (256, 192, 16),
|
|
"city-props": (256, 256, 6), "city-smoke": (96, 128, 4),
|
|
"city-cars": (256, 256, 16), "city-bin-materials": (128, 128, 4),
|
|
"city-clouds": (192, 192, 4)}
|
|
result = {}
|
|
for name, dimensions in expected.items():
|
|
data = (ROOT / "assets/animations/arrival" / f"{name}.bin").read_bytes()
|
|
magic, width, height, count, size = struct.unpack_from("<4s4I", data)
|
|
assert magic == b"ANM1" and (width, height, count) == dimensions, name
|
|
stride = width * height * 4
|
|
assert size == stride * count and len(data) == 20 + size, name
|
|
hashes = set()
|
|
for index in range(count):
|
|
payload = data[20 + index * stride:20 + (index + 1) * stride]
|
|
frame = Image.frombytes("RGBA", (width, height), payload, "raw", "BGRA")
|
|
if name == "city-hands" and 5 <= index <= 10:
|
|
frame.save(OUTPUT / f"city-hand-{index:02}.png")
|
|
if name == "city-tiles-v4" and 8 <= index <= 10:
|
|
frame.save(OUTPUT / f"city-window-{index:02}.png")
|
|
assert frame.getchannel("A").getbbox(), f"Empty sprite: {name}/{index}"
|
|
if name in ("city-hands", "city-props", "city-cars"):
|
|
assert set(frame.getchannel("A").tobytes()) <= {0, 255}, f"Translucent solid: {name}/{index}"
|
|
assert frame.getchannel("A").getextrema()[0] == 0, f"Opaque matte: {name}/{index}"
|
|
hashes.add(hashlib.sha256(payload).digest())
|
|
assert len(hashes) == count, f"Duplicate padding frames: {name}"
|
|
result[name] = {"frames": count, "distinct_frames": len(hashes)}
|
|
return result
|
|
|
|
|
|
def check_snapshot(name):
|
|
frame = Image.open(OUTPUT / f"{name}.bmp").convert("RGB")
|
|
assert frame.size == (640, 480), name
|
|
assert sum(ImageStat.Stat(frame).mean) > 30, f"Blank framebuffer: {name}"
|
|
assert len(frame.getcolors(640 * 480)) > 500, f"Missing artwork: {name}"
|
|
return frame
|
|
|
|
|
|
def check_sequence(prefix, count, crop, duration):
|
|
frames = [check_snapshot(f"{prefix}-{index:02}") for index in range(count)]
|
|
hashes = {hashlib.sha256(frame.crop(crop).tobytes()).digest() for frame in frames}
|
|
assert len(hashes) >= min(count, 5), f"Frozen animation: {prefix}"
|
|
palette = frames[0].quantize(colors=256)
|
|
encoded = [frame.quantize(palette=palette) for frame in frames]
|
|
encoded[0].save(OUTPUT / f"{prefix}.gif", save_all=True, append_images=encoded[1:],
|
|
duration=duration, loop=0, disposal=2)
|
|
return len(hashes)
|
|
|
|
|
|
def main():
|
|
packs = check_packs()
|
|
names = ["city-eyes", "city-upward", "city-smoke", "city-toss", "city-alley", "city-street", "city-skyline",
|
|
"arrival-street", "arrival-stop", "arrival-profile", "arrival-name",
|
|
"arrival-admit", "merchant-idle", "merchant-gesture", "arrival-hall"]
|
|
black = Image.open(OUTPUT / "city-black.bmp").convert("RGB")
|
|
assert black.size == (640, 480) and black.getbbox() is None, "Missing black transition"
|
|
sheet = Image.new("RGB", (1280, ((len(names) + 1) // 2) * 510), "#131c1e")
|
|
draw = ImageDraw.Draw(sheet)
|
|
hashes = set()
|
|
for index, name in enumerate(names):
|
|
frame = check_snapshot(name)
|
|
hashes.add(hashlib.sha256(frame.tobytes()).digest())
|
|
x, y = (index % 2) * 640, (index // 2) * 510
|
|
sheet.paste(frame, (x, y))
|
|
draw.text((x + 12, y + 488), f"Native MASM / {name}", fill="#eeeeee")
|
|
frame.save(OUTPUT / f"{name}.png")
|
|
assert len(hashes) == len(names), "Repeated scene snapshots"
|
|
sheet.save(OUTPUT / "arrival-contact-sheet.png")
|
|
sequences = {
|
|
"guard": check_sequence("guard-admit", 12, (0, 0, 640, 400), 100),
|
|
"merchant": check_sequence("merchant-talk", 12, (0, 56, 330, 390), 100),
|
|
"dealer_hand": check_sequence("poker-hand", 24, (0, 88, 640, 444), 60),
|
|
"cigarette_toss": check_sequence("city-toss", 20, (0, 180, 640, 480), 75),
|
|
"traffic": check_sequence("city-traffic", 16, (0, 120, 640, 480), 200),
|
|
}
|
|
report = {"result": "PASS", "packs": packs, "native_snapshots": len(names),
|
|
"distinct_sequence_states": sequences}
|
|
(OUTPUT / "arrival-visual-report.json").write_text(json.dumps(report, indent=2))
|
|
print(json.dumps(report))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|