KAZIK/tools/tests/check_arrival_frames.py
Artvasil09 3ad5bf4c9d Develop assembly-only casino scenes and native games
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.
2026-10-08 22:39:19 +03:00

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()