"""Compile the actual MASM card renderers and check every visible rank.""" import argparse from pathlib import Path import re import struct import subprocess from PIL import Image from check_card_exit_build import Coff ROOT = Path(__file__).resolve().parents[2] OUT = ROOT / "build/card-ranks" RANKS = ("2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K", "A") FUNCTIONS = { "casino3d.inc": ("DrawPixel", "DrawBox", "DrawBoxBorder", "DrawChar8x8", "DrawString8x8"), "src_graphics/card_table_assets.inc": ("DrawNativeSuit3D", "DrawNativeCard3D"), "src_graphics/native_blackjack_party_render.inc": ("DrawBJMiniCard3D",), } def read(path): return (ROOT / path).read_text(encoding="cp1251") def procedure(source, name): return re.search(rf"^{name} PROC\b.*?^{name} ENDP", source, re.M | re.S).group() def prepare(): OUT.mkdir(exist_ok=True) ranks = re.search(r"^cardRankNames3D db .*", read("src_graphics/card_tables_data.inc"), re.M).group() suits = re.search(r"^cardSuitMasks3D db .*\n(?: db .*\n){3}", read("src_graphics/card_tables_data.inc"), re.M).group() backs = re.search(r"^cardBackColors3D dd .*\n dd .*", read("src_graphics/platform_data.inc"), re.M).group() parts = [""".386 .model flat,stdcall option casemap:none SCREEN_W equ 640 SCREEN_H equ 480 ExitProcess PROTO :DWORD CreateFileA PROTO :DWORD,:DWORD,:DWORD,:DWORD,:DWORD,:DWORD,:DWORD WriteFile PROTO :DWORD,:DWORD,:DWORD,:DWORD,:DWORD CloseHandle PROTO :DWORD DrawBox PROTO :DWORD,:DWORD,:DWORD,:DWORD,:DWORD DrawAnimSprite3D PROTO :DWORD,:DWORD,:DWORD,:DWORD,:DWORD,:DWORD AnimPack3D STRUCT magic dd ? imageWidth dd ? height dd ? frames dd ? bytes dd ? pixels dd ? AnimPack3D ENDS .data include font8x8.inc selectedBack3D dd 0 backsPack3D AnimPack3D <> actionBmpHeader db 42h,4Dh dd 1228854 dw 0,0 dd 54,40,640,-480 dw 1,32 dd 0,1228800,0,0,0,0 """, ranks, suits, backs] for suit in range(4): for page in range(2): parts.append(f'sheet{suit}_{page} db "build/card-ranks/suit-{suit}-{page}.bmp",0') parts.append(".data?\nscreenBuffer dd 640*480 dup(?)\n.code") for path, names in FUNCTIONS.items(): source = read(path) parts.extend(procedure(source, name) for name in names) parts.append(procedure(read("tools/tests/casino_actions_test.inc"), "WriteActionSnapshot3D")) parts.append("""DrawAnimSprite3D PROC pack:DWORD,frame:DWORD,x:DWORD,y:DWORD,w:DWORD,h:DWORD invoke ExitProcess,99 ret DrawAnimSprite3D ENDP CardRanksStart PROC """) for suit in range(4): for page in range(2): parts.append("invoke DrawBox,0,0,320,240,00213629h") for rank in range(page * 7, min(page * 7 + 7, 13)): x = 6 + (rank % 7) * 44 for mode, width in enumerate((34, 20, 18, 20)): y = 4 + mode * 60 if mode == 3: parts.append(f"invoke DrawBJMiniCard3D,{x},{y},{suit*13+rank},0") else: parts.append(f"invoke DrawNativeCard3D,{x},{y},{suit*13+rank},0,{width}") parts.append(f"invoke WriteActionSnapshot3D,OFFSET sheet{suit}_{page}\ntest eax,eax\njz failed") parts.append("invoke ExitProcess,0\nfailed:\ninvoke ExitProcess,1\nCardRanksStart ENDP\nend CardRanksStart") (OUT / "ranks.asm").write_text("\n".join(parts), encoding="ascii") def verify(): font = bytes(int(x, 16) for x in re.findall(r"\b([0-9A-Fa-f]{3})h\b", read("font8x8.inc"))) assert len(font) == 256 * 8 for suit in range(4): ink = (191, 38, 60) if suit in (1, 2) else (25, 31, 36) for page in range(2): image = Image.open(OUT / f"suit-{suit}-{page}.bmp").convert("RGB") for rank in range(page * 7, min(page * 7 + 7, 13)): x = 6 + (rank % 7) * 44 for mode, width in enumerate((34, 20, 18, 20)): y = 4 + mode * 60 offset = 2 if mode == 3 else (1 if width == 18 else 3) expected = {(x + offset + letter * 8 + col, y + 3 + row) for letter, char in enumerate(RANKS[rank]) for row in range(8) for col in range(8) if font[ord(char) * 8 + row] & (128 >> col)} actual = {(px, py) for px in range(x, x + width) for py in range(y + 3, y + 11) if image.getpixel((px * 2, py * 2)) == ink} assert actual == expected, (suit, rank, mode, actual ^ expected) assert all(x < px < x + width - 1 for px, _ in expected) image.save(OUT / f"suit-{suit}-{page}.png") print("PASS: 208 native card faces; 10/A and all ranks, four suits, narrow/normal/guest cards") def main(): parser = argparse.ArgumentParser() parser.add_argument("--parity", action="store_true") args = parser.parse_args() if args.parity: tested, shipped = Coff(OUT / "ranks.obj"), Coff(ROOT / "build/graphics/poker.obj") for name, count in (("DrawNativeCard3D", 20), ("DrawBJMiniCard3D", 16)): symbol = f"_{name}@{count}" assert tested.signature(symbol) == shipped.signature(symbol), name table = b"".join(rank.encode().ljust(3, b"\0") for rank in RANKS) assert table in tested.data and table in shipped.data verify() print("PASS: shipping card renderers and rank strings match the tested MASM") return prepare() masm = Path("F:/MASM32/masm32") subprocess.run([str(masm / "bin/ml.exe"), "/c", "/coff", "/Cp", "/nologo", "/I.", "/Fobuild/card-ranks/ranks.obj", "build/card-ranks/ranks.asm"], cwd=ROOT, check=True) subprocess.run([str(masm / "bin/link.exe"), "/nologo", "/SUBSYSTEM:CONSOLE", f"/LIBPATH:{masm / 'lib'}", "/OUT:build/card-ranks/ranks.exe", "build/card-ranks/ranks.obj", "kernel32.lib"], cwd=ROOT, check=True) subprocess.run([str(OUT / "ranks.exe")], cwd=ROOT, check=True) verify() if __name__ == "__main__": main()