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.
82 lines
4.3 KiB
Python
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)
|