KAZIK/tools/tests/check_billiards.py
Artvasil09 41ba8f46ed feat: add rooftop city and optimize native terrace rendering
Add rooftop elevator, lounge, volumetric city, river and animated traffic. Prune invisible mesh faces, clip CPU surfaces, streamline compositing and add automatic/F8 roof quality profiles with renderer diagnostics.

Include dartboard/reticle improvements, tracked fortune orb numerals and fate-session visuals, native regression tests, compatible jukebox IDs and updated shipping executables.
2026-10-26 23:20:35 +03:00

78 lines
4.1 KiB
Python

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