- Billiards: add native Russian Pyramid & American Eight-Ball pool with physics prediction, controls, and regression tests - Scratch Tickets: add dedicated counter, mat presentation, elliptical coin scraping, 4 ticket types, and payout effects - First-Person Dice: add native dice scene, hand anchor frames, throwing animation, and table perspective - City Wake & Weather: add arrival cutscene, alley geometry, weather/rain effects, and lighter/rocker animation - World Dressing: add hall paintings, furniture packs, hall lighting, and scene regression tests - Build & Tooling: update build.bat for modern MSVC toolchain, update launcher hash, and refresh shipping binaries
77 lines
4 KiB
Python
77 lines
4 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
|
|
|
|
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(OUT / "actions-test.obj")
|
|
shipped = Coff(ROOT / "build/graphics/poker.obj")
|
|
for name, size in FUNCTIONS.items():
|
|
symbol = f"_{name}@{size}"
|
|
assert tested.signature(symbol) == shipped.signature(symbol), 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)
|