"""Check coin pack layout, native framebuffer captures and shipping MASM parity.""" import argparse import hashlib import json import struct from pathlib import Path from PIL import Image, ImageDraw from check_card_exit_build import Coff ROOT = Path(__file__).resolve().parents[2] NAMES = ("idle", "hold", "release", "air", "apex", "descent", "land", "bounce", "result", "tails") def assets(): manifest = json.loads((ROOT / "assets/animations/coin/manifest-v1.json").read_text()) for name, shape in (("table", (640, 480, 1)), ("faces", (128, 128, 2)), ("hands", (224, 224, 16))): version = 2 if name == "hands" else 1 data = (ROOT / "assets/animations/coin" / f"{name}-v{version}.bin").read_bytes() width, height, count = shape size = width * height * 4 assert manifest[name] == list(shape), name assert struct.unpack_from("<4s4I", data) == (b"ANM1", *shape, size * count), name assert len(data) == 20 + size * count, name assert set(data[23::4]) == ({255} if name == "table" else {0, 255}), name hashes = [] for i in range(count): pixels = data[20 + i * size:20 + (i + 1) * size] hashes.append(hashlib.sha256(pixels).hexdigest()) opaque = sum(alpha == 255 for alpha in pixels[3::4]) assert opaque > width * height // 10, (name, i) if name == "hands" and i < 5: frame = Image.frombytes("RGBA", (width, height), pixels, "raw", "BGRA") anchor = frame.crop((64, 12, 112, 32)) gold = sum(a == 255 and r > 130 and g * 100 > r * 70 and b * 100 < r * 40 for r, g, b, a in anchor.get_flattened_data()) assert gold > 10, ("Held coin lost its contact anchor", i, gold) assert len(set(hashes)) == count, name print("PASS: opaque table, two coin faces, sixteen hand frames; held coin/fingers share one sprite") def captures(): sheet = Image.new("RGB", (1280, 5 * 504), (20, 25, 29)) labels = ImageDraw.Draw(sheet) hashes = [] for i, name in enumerate(NAMES): path = ROOT / "build/actions" / f"coin-{name}.bmp" frame = Image.open(path).convert("RGB") assert frame.size == (640, 480), name assert len(frame.getcolors(640 * 480)) > 1000, name hashes.append(hashlib.sha256(frame.tobytes()).hexdigest()) frame.save(path.with_suffix(".png")) x, y = i % 2 * 640, i // 2 * 504 sheet.paste(frame, (x, y + 24)) labels.text((x + 8, y + 6), name, fill="white") assert len(set(hashes)) == len(NAMES), "Toss captures must be distinct" sheet.save(ROOT / "build/actions/coin-contact-v1.png") print("PASS: 10 distinct, nonblank native frames including both result faces") def parity(): tested = Coff(ROOT / "build/actions/actions-test.obj") shipped = Coff(ROOT / "build/graphics/poker.obj") for name, arguments in (("InitCoinGame", 0), ("HandleCoinInput", 0), ("RenderCoinGame", 0), ("InitNativeCoin3D", 0), ("LoadNativeCoinArt3D", 0), ("FreeNativeCoinArt3D", 0), ("HandleNativeCoin3D", 0), ("RenderNativeCoin3D", 0), ("CoinEscape3D", 0), ("CoinSettle3D", 0), ("CoinPose3D", 4), ("DrawCoinShadow3D", 20), ("CardTableEscape3D", 0)): symbol = f"_{name}@{arguments}" assert tested.signature(symbol) == shipped.signature(symbol), name print("PASS: published coin renderer, controls, payout and exit match native-tested MASM") if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument("--assets-only", action="store_true") parser.add_argument("--parity", action="store_true") options = parser.parse_args() assets() if not options.assets_only: captures() if options.parity: parity()