"""Validate native auxiliary-window frames and Russian CP1251 glyphs.""" import argparse import hashlib import json import re from pathlib import Path from PIL import Image, ImageDraw from check_card_exit_build import Coff ROOT = Path(__file__).resolve().parents[2] NAMES = ("idle", "air", "heads", "tails", "loss") def font(): values = [] for line in (ROOT / "font8x8.inc").read_text(encoding="cp1251").splitlines(): if line.startswith("db "): values.extend(int(v.strip()[:-1], 16) for v in line[3:].split(";")[0].split(",")) assert len(values) == 256 * 8 glyphs = [values[i * 8:(i + 1) * 8] for i in range(256)] labels = json.loads((ROOT / "assets/data/ui-ru.json").read_text(encoding="utf-8")) for text in labels.values(): for code in text.encode("cp1251"): if code >= 128: assert any(glyphs[code]), f"Empty Cyrillic glyph {code:02X}" assert glyphs[code] != glyphs[ord("?")], f"Question mark instead of {code:02X}" assert glyphs[0xA8] != glyphs[ord("E")], "Russian YO must have its own glyph" widths = { "coinFlipText3D": 6, "coinAgainText3D": 7, "coinExitText3D": 5, "wheelRed3D": 5, "wheelBlack3D": 5, "wheelEven3D": 5, "wheelOdd3D": 5, "wheelSpin3D": 7, "wheelClear3D": 5, "pkCheckText3D": 8, "pkCallText3D": 8, "pkRaiseText3D": 8, "pkFoldText3D": 8, "pkAllInText3D": 8, "cbHitText3D": 8, "cbStandText3D": 8, "cbDoubleText3D": 8, "merchantBuy3D": 12, "bs3d_textCuffKey": 8, "entryChoose3D": 16, "entryNew3D": 16, "poolOpenText3D": 16, "poolSolidsText3D": 16, "poolStripesText3D": 16, } for symbol, maximum in widths.items(): assert len(labels[symbol]) <= maximum, f"Russian button overflow: {symbol}" print(f"PASS: {len(labels)} translated labels have CP1251 glyphs including YO; narrow buttons fit") return glyphs def text_matches(frame, text, x, y, scale, ink, glyphs): for code in text.encode("cp1251"): for row, mask in enumerate(glyphs[code]): for column in range(8): if mask & (128 >> column): for dy in range(scale): for dx in range(scale): assert frame.getpixel((x + column * scale + dx, y + row * scale + dy)) == ink, (text, x, y) x += scale * 8 def captures(glyphs): frames = {} sheet = Image.new("RGB", (320 * 5, 424), (20, 25, 29)) draw = ImageDraw.Draw(sheet) for i, name in enumerate(NAMES): path = ROOT / "build/actions" / f"coin-detail-{name}.bmp" frame = Image.open(path).convert("RGB") assert frame.size == (320, 400), name assert len(frame.getcolors(128000)) > 100, name frame.save(path.with_suffix(".png")) frames[name] = frame sheet.paste(frame, (i * 320, 24)) draw.text((i * 320 + 8, 6), name, fill="white") assert frame.getpixel((319, 399)) == (20, 25, 29) assert len({hashlib.sha256(f.tobytes()).digest() for f in frames.values()}) == 5 for name, side, x in (("heads", "ОРЁЛ", 96), ("tails", "РЕШКА", 80), ("loss", "РЕШКА", 80)): text_matches(frames[name], side, x, 284, 4, (244, 231, 200), glyphs) text_matches(frames["heads"], "ВЫИГРЫШ", 104, 336, 2, (135, 212, 165), glyphs) text_matches(frames["loss"], "ПРОИГРЫШ", 96, 336, 2, (233, 147, 131), glyphs) text_matches(frames["air"], "МОНЕТА В ПОЛЁТЕ", 40, 310, 2, (229, 210, 158), glyphs) assert frames["heads"].crop((60, 66, 260, 266)).tobytes() != frames["tails"].crop((60, 66, 260, 266)).tobytes() assert frames["tails"].crop((60, 66, 260, 266)).tobytes() == frames["loss"].crop((60, 66, 260, 266)).tobytes() sheet.save(ROOT / "build/actions/coin-detail-contact.png") print("PASS: five distinct native detail frames; exact Russian result/win/loss glyphs and matching faces") def parity(): tested = Coff(ROOT / "build/actions/actions-test.obj") shipped = Coff(ROOT / "build/graphics/poker.obj") for name, count in (("OpenCoinDetail3D", 0), ("CloseCoinDetail3D", 0), ("LayoutCoinDetail3D", 0), ("UpdateCoinDetail3D", 0), ("RenderCoinDetail3D", 0), ("CoinDetailEvents3D", 16), ("CoinDetailBox3D", 20), ("CoinDetailText3D", 20), ("CoinDetailFace3D", 8), ("CoinDetailNumber3D", 16), ("ToggleFullscreen3D", 0)): symbol = f"_{name}@{count}" assert tested.signature(symbol) == shipped.signature(symbol), name print("PASS: shipping auxiliary-window lifecycle, renderer and fullscreen match native-tested MASM") if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument("--parity", action="store_true") parser.add_argument("--font-only", action="store_true") args = parser.parse_args() glyphs = font() if not args.font_only: captures(glyphs) if args.parity: parity()