"""Verify real MASM Clover framebuffer captures and shipping-code parity.""" import argparse import hashlib import struct from pathlib import Path from PIL import Image, ImageChops from check_card_exit_build import Coff from check_coin_detail import font, text_matches ROOT = Path(__file__).resolve().parents[2] OUT = ROOT / "build/actions" NAMES = ["clover-ready", "clover-spin", "clover-stopped", "clover-win", "clover-next", "clover-paytable"] FUNCTIONS = {"InitNativeClover3D": 0, "FreeNativeClover3D": 0, "StartNativeClover3D": 0, "CloverEvaluate3D": 0, "SettleNativeClover3D": 0, "CloverEscape3D": 0, "CloverReelPosition3D": 4, "HandleNativeClover3D": 0, "DrawCloverSymbol3D": 12, "DrawCloverReel3D": 4, "RenderNativeClover3D": 0, "CloverComboText3D": 4, "DrawSlotTotals3D": 12, "EvaluateSlotCombos": 0, "SlotApplyWin": 12, "CardTableEscape3D": 0, "TableBusy3D": 0} def main(parity=False): packs = ROOT / "assets/animations/clover" raw = (packs / "cabinet-v1.bin").read_bytes() assert struct.unpack_from("<4s4I", raw) == (b"ANM1", 640, 480, 1, 640 * 480 * 4) assert set(raw[23::4]) == {255} cabinet = Image.frombytes("RGBA", (640, 480), raw[20:], "raw", "BGRA").convert("RGB") raw = (packs / "symbols-v1.bin").read_bytes() stride = 56 * 56 * 4 assert struct.unpack_from("<4s4I", raw) == (b"ANM1", 56, 56, 7, stride * 7) assert len(raw) == 20 + stride * 7 assert set(raw[23::4]) == {0, 255} assert len({hashlib.sha256(raw[20 + i * stride:20 + (i + 1) * stride]).digest() for i in range(7)}) == 7 frames = {} for name in NAMES: frame = Image.open(OUT / (name + ".bmp")).convert("RGB") assert frame.size == (640, 480) assert len(frame.getcolors(640 * 480)) > 2000, name frames[name] = frame frame.save(OUT / (name + ".png")) if name == "clover-paytable": continue difference = ImageChops.difference(frame, cabinet) for y in range(80, 318): for x in range(110, 530): in_reel = 126 <= y < 294 and any(left <= x < left + 56 for left in (144, 216, 288, 360, 432)) if not in_reel: assert difference.getpixel((x, y)) == (0, 0, 0), (name, x, y) assert len({hashlib.sha256(frame.tobytes()).digest() for frame in frames.values()}) == len(NAMES) # Every complete cell contains a symbol, not a truncated or empty partial row. for col in (144, 216, 288, 360, 432): for top in (126, 182, 238): cell = frames["clover-ready"].crop((col + 4, top + 4, col + 52, top + 52)) assert len(cell.getcolors(48 * 48)) > 20, (col, top) selected = ImageChops.difference(frames["clover-win"], frames["clover-next"]) assert selected.crop((144, 126, 488, 294)).getbbox(), "Changing the paid combo must change the highlighted cells" contact = Image.new("RGB", (1920, 960)) for i, name in enumerate(NAMES): contact.paste(frames[name], ((i % 3) * 640, (i // 3) * 480)) contact.save(OUT / "clover-contact.png") glyphs = font() text_matches(frames["clover-ready"], "КЛЕВЕР 5x3", 16, 12, 2, (232, 215, 173), glyphs) text_matches(frames["clover-win"], "27300", 384, 402, 2, (244, 243, 232), glyphs) text_matches(frames["clover-win"], "+27200", 384, 422, 2, (168, 221, 185), glyphs) text_matches(frames["clover-win"], "ДЖЕКПОТ", 144, 350, 2, (244, 243, 232), glyphs) text_matches(frames["clover-next"], "ГЛАЗ", 144, 350, 2, (244, 243, 232), glyphs) source = (ROOT / "src_graphics/native_clover.inc").read_text(encoding="ascii") assert "call EvaluateSlotCombos" in source assert "call GameSlot" not in source and "call PaySlotMultiplier" not in source if parity: tested = Coff(ROOT / "build/actions/actions-test.obj") shipped = Coff(ROOT / "build/graphics/poker.obj") for name, size in FUNCTIONS.items(): assert tested.signature(f"_{name}@{size}") == shipped.signature(f"_{name}@{size}"), name print("PASS: 5x3 complete cells, 7 real sprites, clipping, six distinct native frames, paid-mask selection" + (", tested/shipping ASM parity" if parity else "")) if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument("--parity", action="store_true") main(parser.parse_args().parity)