128 lines
6.7 KiB
Python
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()
|