KAZIK/tools/tests/check_buckshot_frames.py
Artvasil09 3ad5bf4c9d Develop assembly-only casino scenes and native games
Add native Durak, poker, blackjack, roulette, Buckshot and billiards scenes with animation assets and regression tests. Improve arrival, city, hall, profiles, shops, audio and exit handling; remove the portable C game core and refresh shipping executables.
2026-10-08 22:39:19 +03:00

89 lines
4.5 KiB
Python

"""Validate generated packs and snapshots from the actual MASM renderer."""
import hashlib
import argparse
import json
import struct
from pathlib import Path
from PIL import Image, ImageDraw
ROOT = Path(__file__).resolve().parents[2]
OUTPUT = ROOT / 'build/actions'
def main(packs_only=False):
manifest = json.loads((ROOT / 'assets/animations/buckshot/manifest-v2.json').read_text())
for name, expected in manifest.items():
data = (ROOT / 'assets/animations/buckshot' / name).read_bytes()
magic, width, height, count, length = struct.unpack_from('<4s4I', data)
assert magic == b'ANM1'
assert [width, height] == expected['size'] and count == expected['frames']
assert len(data) == 20 + length == 20 + width * height * 4 * count
hashes = set()
for i in range(count):
payload = data[20 + i * width * height * 4:20 + (i + 1) * width * height * 4]
frame = Image.frombytes('RGBA', (width, height), payload, 'raw', 'BGRA')
hashes.add(hashlib.sha256(payload).hexdigest())
if name == 'self-flash-v4.bin' and i < 4:
hot = [(x, y, a) for y in range(height) for x in range(width)
for r, g, b, a in [frame.getpixel((x, y))]
if r > 240 and g > 220 and b > 150 and a > 64]
weight = sum(a for x, y, a in hot)
assert weight > 0, (name, i)
cx = sum(x * a for x, y, a in hot) / weight
cy = sum(y * a for x, y, a in hot) / weight
assert abs(cx - 64) < 2 and abs(cy - 64) < 2, (name, i, cx, cy)
if name not in ('room-v2.bin', 'blood-v3.bin', 'self-flash-v4.bin'):
assert set(frame.getchannel('A').tobytes()) == {0, 255}, (name, i)
bounds = frame.getchannel('A').getbbox()
assert bounds and 0 < bounds[0] < bounds[2] < width, (name, i, bounds)
assert len(hashes) == count, name
if packs_only:
print(f'PASS: {len(manifest)} packs, frontal ignition anchors centered within 2 pixels')
return
snapshots = sorted(OUTPUT.glob('buckshot-*.bmp'))
assert len(snapshots) == 30, len(snapshots)
report = {}
for path in snapshots:
frame = Image.open(path).convert('RGB')
assert frame.size == (640, 480)
colors = len(frame.getcolors(640 * 480))
assert colors > 1000, (path.name, colors)
cinematic = any(part in path.stem for part in ('revive-', 'death-')) or path.stem in (
'buckshot-self-fire', 'buckshot-self-peak', 'buckshot-self-smoke', 'buckshot-dealer-fire')
if not cinematic:
assert frame.getpixel((0, 0)) == (17, 17, 22), path.name
assert frame.getpixel((0, 479)) == (17, 22, 23), path.name
else:
assert frame.getpixel((0, 479)) != (17, 22, 23), ('HUD hides cinematic', path.name)
report[path.stem] = {'colors': colors, 'sha256': hashlib.sha256(frame.tobytes()).hexdigest()}
frame.save(path.with_suffix('.png'))
assert len({item['sha256'] for item in report.values()}) == len(snapshots)
peak = Image.open(OUTPUT / 'buckshot-self-peak.bmp').convert('RGB')
r, g, b = peak.getpixel((315, 228))
assert r > 240 and g > 220 and b > 150, ('Muzzle ignition missed the bore', r, g, b)
names = ['ready', 'self-peak', 'death-dealer', 'revive-dealer-shock']
contact = Image.new('RGB', (1280, 1010), '#191c19')
draw = ImageDraw.Draw(contact)
for i, name in enumerate(names):
x, y = i % 2 * 640, i // 2 * 505
draw.text((x + 8, y + 5), name, fill='white')
contact.paste(Image.open(OUTPUT / f'buckshot-{name}.bmp'), (x, y + 25))
contact.save(OUTPUT / 'buckshot-contact-sheet.png')
sequence = ['self-rise', 'self-aim', 'self-fire', 'self-peak', 'self-smoke', 'death-player',
'revive-player-arms', 'revive-player-shock', 'lose']
frames = [Image.open(OUTPUT / f'buckshot-{name}.bmp').convert('RGB') for name in sequence]
frames[0].save(OUTPUT / 'buckshot-keyframes.gif', save_all=True, append_images=frames[1:],
duration=[300, 300, 30, 50, 300, 800, 700, 800, 1000], loop=0)
(OUTPUT / 'buckshot-qa.json').write_text(json.dumps(report, indent=2), encoding='utf-8')
print(f'PASS: {len(manifest)} packs, 148 distinct sprite/VFX poses, 30 MASM framebuffer snapshots')
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('--packs-only', action='store_true')
main(parser.parse_args().packs_only)