KAZIK/tools/tests/check_backrooms.py

128 lines
6.7 KiB
Python

"""Execute shipping MASM maze/entry/collision/render, inspect native output."""
from collections import deque
from pathlib import Path
import struct
import subprocess
from PIL import Image
from check_artifacts import toolchain
ROOT = Path(__file__).resolve().parents[2]
OUT = ROOT / 'build/scratch'
def check_exit_faces():
raw = (OUT/'backrooms-exit-faces.bin').read_bytes()
assert len(raw) == 8*640*480*4
frames = [Image.frombytes('RGBA', (640, 480), raw[i:i+640*480*4],
'raw', 'BGRA').convert('RGB')
for i in range(0, len(raw), 640*480*4)]
# Source glyph at runtime's exact nearest-neighbor UV scale. Compare both
# orientations rather than merely counting green pixels (mirrors pass that).
pack = (ROOT/'assets/sprites/backrooms/materials.bin').read_bytes()
art = Image.frombytes('RGBA', (128, 128), pack[20+4*65536:], 'raw', 'BGRA')
glyph = [(x, y) for y in range(48, 70) for x in range(27, 101)
if min(art.getpixel((x, y))[:3]) > 240]
for face, name in enumerate(('north', 'south', 'west', 'east')):
clean, vhs = frames[2*face:2*face+2]
clean.save(OUT/f'backrooms-exit-{name}.png')
vhs.save(OUT/f'backrooms-exit-{name}-vhs.png')
white = [(x, y) for y in range(0, 300) for x in range(60, 580)
if min(clean.getpixel((x, y))) > 240]
assert white, f'{name}: EXIT missing'
x0, x1 = min(x for x, y in white), max(x for x, y in white)
y0, y1 = min(y for x, y in white), max(y for x, y in white)
gx0, gx1 = min(x for x, y in glyph), max(x for x, y in glyph)
gy0, gy1 = min(y for x, y in glyph), max(y for x, y in glyph)
def score(mirror):
hits = 0
for x, y in glyph:
u = (x-gx0)/(gx1-gx0)
if mirror:
u = 1-u
px = round(x0+u*(x1-x0))
py = round(y0+(y-gy0)/(gy1-gy0)*(y1-y0))
hits += min(clean.getpixel((px, py))) > 240
return hits/len(glyph)
normal, mirrored = score(False), score(True)
assert normal > .85 and normal > mirrored+.12, (name, normal, mirrored)
# White glyphs may darken on scanlines, never wrap into black/color noise.
corrupt = sum(min(vhs.getpixel(p)) < 180 or
max(vhs.getpixel(p))-min(vhs.getpixel(p)) > 12 for p in white)
assert corrupt == 0, (name, 'VHS corrupt white pixels', corrupt, len(white))
def main():
OUT.mkdir(parents=True, exist_ok=True)
ml, link = toolchain()
sdk = Path('C:/Program Files (x86)/Windows Kits/10/Lib')
lib = next(p/'um/x86' for p in sorted(sdk.iterdir(), reverse=True)
if (p/'um/x86/kernel32.lib').exists())
for args in ([ml, '/c', '/coff', '/Cp', '/nologo', '/I.', '/DBACKROOMS_TEST=1',
'/Fobuild/scratch/backrooms.obj', 'poker.asm'],
[link, '/nologo', '/SUBSYSTEM:CONSOLE', '/LIBPATH:'+str(lib),
'/OUT:build/scratch/backrooms-test.exe', 'build/scratch/backrooms.obj',
'kernel32.lib', 'winmm.lib', 'user32.lib', 'ws2_32.lib', 'gdi32.lib']):
subprocess.run(list(map(str, args)), cwd=ROOT, check=True, timeout=120)
subprocess.run([str(OUT/'backrooms-test.exe')], cwd=ROOT, check=True, timeout=90)
raw = (OUT/'backrooms-native.bin').read_bytes()
assert len(raw) == 256 * 1052
fingerprints = set()
minimum = 10000
for offset in range(0, len(raw), 1052):
grid = raw[offset:offset+1024]
exit_cell, *games = struct.unpack_from('<7I', raw, offset+1024)
assert all(grid[x] for x in range(32))
assert all(grid[31*32+x] for x in range(32))
assert all(grid[y*32] and grid[y*32+31] for y in range(32))
distance, queue = {33: 0}, deque([33])
while queue:
cell = queue.popleft()
for neighbor in (cell-32, cell+1, cell+32, cell-1):
if grid[neighbor] == 0 and neighbor not in distance:
distance[neighbor] = distance[cell]+1
queue.append(neighbor)
assert len(distance) == grid.count(0), 'Disconnected open component'
assert distance[exit_cell] == max(distance.values())
assert len(set(games)) == 6 and exit_cell not in games and 33 not in games
assert all(cell in distance for cell in games)
minimum = min(minimum, distance[exit_cell])
fingerprints.add(grid)
assert len(fingerprints) == 256
assert minimum >= 40, minimum
luck = (OUT/'backrooms-luck.bin').read_bytes()
assert len(luck) == 1000*452
wins = dict.fromkeys(('coin', 'dice', '777', 'clover', 'blackjack', 'poker'), 0)
for row in struct.iter_unpack('<113I', luck):
coin, d1, d2, s1, s2, s3, clover, bj = row[:8]
assert coin in (1, 2) and 1 <= d1 <= 6 and 1 <= d2 <= 6
wins['coin'] += coin == 1
wins['dice'] += (d1+d2) % 2 == 0
assert (s1, s2, s3) in ((1, 1, 1), (2, 5, 3))
wins['777'] += s1 == s2 == s3
assert clover in (0, 78)
wins['clover'] += clover > 0
deck, poker, button = row[8:60], row[60:112], row[112]
assert sorted(deck) == sorted(poker) == list(range(52)), 'Duplicate/missing card'
assert bj in (19, 21)
assert bj == (21 if deck[0] == 12 else 19)
wins['blackjack'] += bj == 21
# Actual non-button-first deal order, plus actual burn/flop/turn/river offsets.
seat = button ^ 1
hand = [poker[seat], poker[seat+2], *(poker[i] for i in (5, 6, 7, 9, 11))]
wins['poker'] += set(range(8, 13)).issubset(hand)
assert all(860 <= n <= 940 for n in wins.values()), wins
image = Image.frombytes('RGBA', (640, 480), (OUT/'backrooms-frame.bin').read_bytes(), 'raw', 'BGRA').convert('RGB')
image.save(OUT/'backrooms-frame.png')
assert len(image.getcolors(640*480)) > 100
image = Image.frombytes('RGBA', (640, 480), (OUT/'backrooms-exit.bin').read_bytes(), 'raw', 'BGRA').convert('RGB')
image.save(OUT/'backrooms-exit.png')
assert sum(g > 1.4*r and g > 1.2*b for r, g, b in image.get_flattened_data()) > 100, 'Exit not visibly green'
check_exit_faces()
print('PASS: readable non-mirrored EXIT on all four wall faces; VHS preserves white glyphs')
print(f'PASS: 256 native seeds; sealed connected mazes; farthest exit >= {minimum} steps; unique reachable games; entry hold/reset; collision; isolation; profile roundtrip; render')
print(f'PASS: 1000 production rounds per game, valid unique decks, wins: {wins}')
print('PASS: all blackjack party sizes; 7 scratch designs; roulette; Durak conservation/trump; 9 Russian party/bullet setups; Buckshot counts; preview saves; repeat reward')
if __name__ == '__main__':
main()