"""Validate real MASM billiards captures, packed art and tested/shipping parity.""" import argparse import hashlib import struct from pathlib import Path from PIL import Image from check_card_exit_build import Coff from check_coin_detail import font, text_matches from check_terminals import signature ROOT = Path(__file__).resolve().parents[2] OUT = ROOT / "build/actions" NAMES = ("billiards-select", "billiards-eight", "billiards-trace", "pyramid-rack", "pyramid-aim", "pyramid-strike", "pyramid-penalty", "pyramid-choice", "pyramid-win") FUNCTIONS = {"InitPool3D": 0, "ResetPool3D": 0, "StartBilliards3D": 4, "PoolStep3D": 0, "PoolCollide3D": 8, "PoolPocketBall3D": 4, "PoolJawContact3D": 4, "PoolFinishShot3D": 0, "PoolShoot3D": 0, "PoolPredict3D": 0, "PoolDrawPrediction3D": 0, "PoolTraceLine3D": 20, "PoolAimPoint3D": 8, "PoolChooseAI3D": 0, "EightLegal3D": 4, "EightCount3D": 0, "EightAutoCall3D": 0, "EightSyncGroups3D": 0, "EightFinish3D": 0, "EightHand3D": 0, "PyramidSelect3D": 4, "PyramidRail3D": 8, "PyramidCross3D": 4, "PyramidContact3D": 8, "PyramidPenalty3D": 4, "PyramidChoice3D": 4, "PyramidRespot3D": 4, "HandlePool3D": 0, "RenderPool3D": 0, "PyramidBall3D": 16} def main(parity=False): for name, count in (("balls-v1.bin", 8), ("pool-balls-v1.bin", 16)): raw = (ROOT / "assets/animations/pyramid" / name).read_bytes() stride = 12 * 12 * 4 assert struct.unpack_from("<4s4I", raw) == (b"ANM1", 12, 12, count, stride * count) assert len(raw) == 20 + stride * count for index in range(count): frame = Image.frombytes("RGBA", (12, 12), raw[20 + index * stride:20 + (index + 1) * stride], "raw", "BGRA") assert frame.getbbox() is not None assert frame.getbbox()[0] > 0 and frame.getbbox()[1] > 0 frames = {} for name in NAMES: frame = Image.open(OUT / (name + ".bmp")).convert("RGB") assert frame.size == (640, 480) assert len(frame.getcolors(640 * 480)) > 300, name frames[name] = frame frame.save(OUT / (name + ".png")) assert len({hashlib.sha256(im.tobytes()).digest() for im in frames.values()}) == len(NAMES) glyphs = font() text_matches(frames["billiards-select"], "БИЛЬЯРД", 32, 24, 2, (232, 215, 173), glyphs) text_matches(frames["billiards-eight"], "ПУЛ / 8", 16, 12, 2, (232, 215, 173), glyphs) text_matches(frames["pyramid-rack"], "ПИРАМИДА", 16, 12, 2, (232, 215, 173), glyphs) for name, total in (("eight-human-shot", "6"), ("eight-group-credit", "1")): frame = Image.open(OUT / (name + ".bmp")).convert("RGB") text_matches(frame, "ВЫ", 176, 12, 2, (232, 215, 173), glyphs) text_matches(frame, "ИИ", 320, 12, 2, (232, 215, 173), glyphs) text_matches(frame, total, 224, 12, 2, (244, 243, 232), glyphs) text_matches(frame, total, 288, 12, 2, (244, 243, 232), glyphs) frame.save(OUT / (name + ".png")) trace = frames["billiards-trace"] # The actual two-ball preview must show an object path toward and back from # the right cushion, not merely the cue's first-contact ray. assert sum(trace.getpixel((x, 200)) == (141, 181, 169) for x in range(350, 570)) > 15 contact = Image.new("RGB", (1920, 1440)) for i, name in enumerate(NAMES): contact.paste(frames[name], ((i % 3) * 640, (i // 3) * 480)) contact.save(OUT / "billiards-contact.png") if parity: tested = Coff(ROOT / "build/billiards/module.obj") shipped = Coff(ROOT / "build/graphics/poker.obj") for name, size in FUNCTIONS.items(): symbol = f"_{name}@{size}" assert signature(tested,symbol,tail_branches=True) == signature(shipped,symbol,tail_branches=True), name print("PASS: Russian selection, both ball sets, native captures, rebound prediction" + (" and shipping/tested billiards parity" if parity else "")) if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument("--parity", action="store_true") main(parser.parse_args().parity)