KAZIK/tools/tests/check_card_ranks.py
Artvasil09 797ee366dc feat: implement upper lounge, manual stairs, cabaret, and casino terminals
- Native player-controlled stairs, upper lounge bar, and Hell Tower infernal portal
- Cabaret stage with musical acts, audience seating, and interaction
- Dedicated cash/trophy wall terminals with native balance and achievement screens
- Projecting 3D wall lanterns with ray/plane intersection
- Six roaming guests with seated poses and card rank rendering updates
- Synchronized launcher hash and updated binaries
2026-10-18 21:36:12 +03:00

146 lines
6 KiB
Python

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