KAZIK/tools/tests/check_clover.py
Artvasil09 881a0061dc Add native NPC visitors with contracts, Clover slots, Pyramid, and party table scenes
Implement 32x32 BFS visitor navigation and daily contracts system with companion .npc journals; add native assembly scenes for Clover slots, Pyramid cards, Blackjack Party and Russian Party; package animation and sprite packs; add comprehensive offline test suites and rebuild shipping binaries.
2026-10-13 09:47:33 +03:00

82 lines
4.3 KiB
Python

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