KAZIK/tools/tests/test_minigames_render.py
Игорь Федосеев b1cd66b322 fix(ui,audio): eliminate cyan color bug, restore authentic IBM VGA Cyrillic font, implement native 2.5D Skill Check
- Fix Little-Endian DIB color inversion (0000FFFFh/0000D7FFh -> 00FFFF00h/00FFD700h) restoring luxurious Gold, Emerald Felt, and Crimson Red palettes across the entire game
- Extract authentic IBM VGA 8x8 font from app866.fon fixing Cyrillic glyph distortion ('М', 'Л', 'П', 'Д')
- Implement native in-engine 2.5D Dead by Daylight Skill Check (MODE_SKILL) eliminating external console popups
- Fix audio mappings to dedicated minigame sounds with zero gunshots outside Russian roulette
- Update visual rasterization test suite and verified 640x480 preview renders
2026-09-30 21:17:07 +03:00

451 lines
18 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# -*- coding: utf-8 -*-
"""
Visual simulation & rasterization test for 2.5D Casino Minigames.
Renders pixel-perfect 640x480 (320x240 upscaled 2x) PNG previews of:
- Coin Flip (Монетка)
- Craps / Dice (Кости)
- Skulls & Cups (Напёрстки)
- Dead by Daylight Skill Check (Проверка реакции)
Uses authentic IBM VGA 8x8 font and correct 0x00RRGGBB Little-Endian DIB color space.
"""
import math
from pathlib import Path
from PIL import Image
REPO_ROOT = Path(__file__).resolve().parents[2]
OUTPUT_DIR = REPO_ROOT / 'docs' / 'previews_minigames'
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
# Parse font8x8.inc
font_path = REPO_ROOT / 'font8x8.inc'
font_bytes = bytearray(256 * 8)
with open(font_path, 'r', encoding='latin1') as f:
text = f.read()
import re
matches = re.findall(r'0([0-9A-Fa-f]{2})h', text)
for i, m in enumerate(matches[:256*8]):
font_bytes[i] = int(m, 16)
SCREEN_W = 640
SCREEN_H = 480
class RasterBuffer:
def __init__(self):
self.pixels = [(0, 0, 0)] * (SCREEN_W * SCREEN_H)
def to_rgb(self, c):
# 0x00RRGGBB format:
# In x86 Little-Endian StretchDIBits (BI_RGB 32-bit):
# byte 0 = Blue (c & 0xFF)
# byte 1 = Green ((c >> 8) & 0xFF)
# byte 2 = Red ((c >> 16) & 0xFF)
r = (c >> 16) & 0xFF
g = (c >> 8) & 0xFF
b = c & 0xFF
return (r, g, b)
def draw_box(self, x, y, w, h, color):
px = x * 2
py = y * 2
pw = w * 2
ph = h * 2
rgb = self.to_rgb(color)
for row in range(py, min(py + ph, SCREEN_H)):
if row < 0:
continue
base = row * SCREEN_W
for col in range(px, min(px + pw, SCREEN_W)):
if col < 0:
continue
self.pixels[base + col] = rgb
def draw_box_border(self, x, y, w, h, fill_color, border_color, b_width):
self.draw_box(x, y, w, h, border_color)
self.draw_box(x + b_width, y + b_width, w - b_width * 2, h - b_width * 2, fill_color)
def draw_pixel(self, x, y, color):
self.draw_box(x, y, 1, 1, color)
def draw_circle(self, cx, cy, radius, color):
r_sq = radius * radius
for dy in range(-radius, radius + 1):
for dx in range(-radius, radius + 1):
if dx * dx + dy * dy <= r_sq:
self.draw_pixel(cx + dx, cy + dy, color)
def draw_char(self, x, y, ch, color):
if isinstance(ch, str):
try:
code = ch.encode('cp1251')[0]
except Exception:
code = ord(ch) & 0xFF
else:
code = ch & 0xFF
glyph_offset = code * 8
for row in range(8):
row_bits = font_bytes[glyph_offset + row]
for col in range(8):
if row_bits & (0x80 >> col):
self.draw_pixel(x + col, y + row, color)
def draw_string(self, x, y, s, color):
cur_x = x
for ch in s:
self.draw_char(cur_x, y, ch, color)
cur_x += 8
def draw_keycap(self, x, y, text, is_active=False):
w = len(text) * 8 + 8
h = 13
if is_active:
border_c = 0x00FFFF00 # Bright Yellow border
fill_c = 0x001B4B22 # Casino Green fill
txt_c = 0x00FFFF00 # Bright Yellow text
else:
border_c = 0x00606870 # Slate border
fill_c = 0x001A1C22 # Charcoal fill
txt_c = 0x00D0D8E0 # Silver text
self.draw_box(x, y + 2, w, h, 0x0008080C) # shadow
self.draw_box_border(x, y, w, h, fill_c, border_c, 1)
self.draw_string(x + 4, y + 3, text, txt_c)
return w + 4
def draw_chip_stack(self, x, y, count=3):
for i in range(count):
cy = y - i * 4
self.draw_box(x + 2, cy + 5, 20, 2, 0x00061004) # shadow
self.draw_box_border(x, cy, 24, 6, 0x00B81515, 0x00FFFFFF, 1) # Red chip
self.draw_box(x + 4, cy + 1, 3, 4, 0x00FFFFFF)
self.draw_box(x + 17, cy + 1, 3, 4, 0x00FFFFFF)
def draw_coin_ellipse(self, cx, cy, rx, ry, outer_color, inner_color, relief=0):
if rx <= 0 or ry <= 0:
return
rx_sq = rx * rx
ry_sq = ry * ry
rx_ry_sq = rx_sq * ry_sq
thresh_sq = (rx_ry_sq * 3) // 4
for dy in range(-ry, ry + 1):
cur_y = cy + dy
for dx in range(-rx, rx + 1):
cur_x = cx + dx
val = dx * dx * ry_sq + dy * dy * rx_sq
if val <= rx_ry_sq:
if val >= thresh_sq:
self.draw_pixel(cur_x, cur_y, outer_color)
else:
self.draw_pixel(cur_x, cur_y, inner_color)
if rx >= 14 and relief == 1:
gold_dark = 0x009A7A00 # Dark Gold relief
gold_bright = 0x00FFD700
# Double-headed imperial eagle relief
self.draw_box(cx - 3, cy - 14, 7, 2, gold_dark)
self.draw_box(cx - 4, cy - 12, 9, 2, gold_dark)
self.draw_box(cx - 2, cy - 9, 5, 12, gold_dark)
self.draw_box(cx - 1, cy - 8, 3, 10, gold_bright)
self.draw_box(cx - 10, cy - 8, 7, 3, gold_dark)
self.draw_box(cx - 13, cy - 5, 10, 3, gold_dark)
self.draw_box(cx - 11, cy - 2, 7, 3, gold_dark)
self.draw_box(cx + 4, cy - 8, 7, 3, gold_dark)
self.draw_box(cx + 4, cy - 5, 10, 3, gold_dark)
self.draw_box(cx + 5, cy - 2, 7, 3, gold_dark)
self.draw_box(cx - 4, cy + 4, 9, 3, gold_dark)
self.draw_box(cx - 3, cy + 7, 7, 3, gold_dark)
elif rx >= 14 and relief == 2:
gold_dark = 0x009A7A00
gold_bright = 0x00FFD700
# Roman numeral 'I' & laurel crown
self.draw_box(cx - 5, cy - 10, 11, 3, gold_dark)
self.draw_box(cx - 2, cy - 7, 5, 14, gold_dark)
self.draw_box(cx - 1, cy - 6, 3, 12, gold_bright)
self.draw_box(cx - 6, cy + 7, 13, 3, gold_dark)
for w_dy in [-10, -5, 0, 5, 9]:
self.draw_box(cx - 12, cy + w_dy, 2, 2, gold_dark)
self.draw_box(cx + 11, cy + w_dy, 2, 2, gold_dark)
def draw_craps_die(self, x, y, size, val):
self.draw_box(x + 3, y + size, size, 3, 0x00081403)
self.draw_box(x + size, y + 3, 3, size, 0x00081403)
body_color = 0x00FAF4F0
bevel_color = 0x00D0C6C0
border_color = 0x00403535
self.draw_box_border(x, y, size, size, body_color, border_color, 1)
self.draw_box(x + 1, y + 1, size - 2, 1, 0x00FFFFFF)
self.draw_box(x + 1, y + 1, 1, size - 2, 0x00FFFFFF)
self.draw_box(x + 1, y + size - 2, size - 2, 1, bevel_color)
self.draw_box(x + size - 2, y + 1, 1, size - 2, bevel_color)
pip_c = 0x00C81515 # Red ruby pips
pip_r = 3
if val in [1, 3, 5]:
self.draw_circle(x + size // 2, y + size // 2, pip_r, pip_c)
self.draw_pixel(x + size // 2 - 1, y + size // 2 - 1, 0x00FFFFFF)
if val >= 2:
self.draw_circle(x + 9, y + 9, pip_r, pip_c)
self.draw_circle(x + size - 10, y + size - 10, pip_r, pip_c)
if val >= 4:
self.draw_circle(x + size - 10, y + 9, pip_r, pip_c)
self.draw_circle(x + 9, y + size - 10, pip_r, pip_c)
if val == 6:
self.draw_circle(x + 9, y + size // 2, pip_r, pip_c)
self.draw_circle(x + size - 10, y + size // 2, pip_r, pip_c)
def draw_gothic_cup(self, x, y, cup_id, is_lifted=False, has_ball=False):
lift_y = y - 36 if is_lifted else y
if not is_lifted:
self.draw_box(x + 6, y + 42, 36, 4, 0x0010040A)
else:
self.draw_box(x + 10, y + 42, 28, 2, 0x001A0612)
if is_lifted and has_ball:
self.draw_glowing_orb(x + 24, y + 26)
self.draw_box_border(x + 6, lift_y + 36, 36, 6, 0x00301A22, 0x00D0B000, 1)
self.draw_box(x + 18, lift_y + 28, 12, 8, 0x00452533)
self.draw_box(x + 22, lift_y + 28, 4, 8, 0x006E4055)
self.draw_box_border(x + 8, lift_y + 8, 32, 20, 0x00401E2D, 0x00FFD700, 1)
self.draw_box(x + 14, lift_y + 10, 6, 16, 0x00703550)
self.draw_box(x + 17, lift_y + 10, 3, 16, 0x00A05078)
self.draw_box_border(x + 4, lift_y + 2, 40, 7, 0x00200A15, 0x00FFFF00, 1)
self.draw_box(x + 12, lift_y + 4, 16, 2, 0x00FFFF00)
# Skull face relief on goblet
self.draw_box(x + 21, lift_y + 12, 6, 5, 0x00FFD700)
self.draw_box(x + 22, lift_y + 17, 4, 3, 0x00FFD700)
self.draw_pixel(x + 22, lift_y + 14, 0x00200A15)
self.draw_pixel(x + 24, lift_y + 14, 0x00200A15)
self.draw_string(x + 21, lift_y + 24, str(cup_id), 0x00FFFF00)
def draw_glowing_orb(self, cx, cy):
self.draw_circle(cx, cy, 14, 0x002E1E08)
self.draw_circle(cx, cy, 11, 0x00583A10)
self.draw_circle(cx, cy, 8, 0x008A581A)
self.draw_circle(cx, cy, 6, 0x00D0B000)
self.draw_circle(cx, cy, 4, 0x00FFE000)
self.draw_circle(cx - 1, cy - 1, 2, 0x00FFFF80)
self.draw_pixel(cx - 2, cy - 2, 0x00FFFFFF)
def save(self, filename):
img = Image.new('RGB', (SCREEN_W, SCREEN_H))
img.putdata(self.pixels)
out_path = OUTPUT_DIR / filename
img.save(out_path)
print(f"Saved {out_path}")
# ==============================================================================
# 1. RENDER COIN GAME (Монетка)
# ==============================================================================
def render_coin(filename='coin_preview.png'):
b = RasterBuffer()
b.draw_box(0, 0, 320, 240, 0x00140906) # Mahogany cabinet
b.draw_box_border(6, 6, 308, 228, 0x0022150F, 0x00482E1B, 3)
for cx in [12, 306]:
for cy in [12, 226]:
b.draw_box(cx, cy, 3, 3, 0x00FFD700)
b.draw_pixel(cx + 1, cy + 1, 0x00FFFFFF)
b.draw_box_border(20, 12, 280, 26, 0x001C100A, 0x00FFD700, 2)
b.draw_string(44, 21, '* * * З О Л О Т А Я М О Н Е Т А * * *', 0x00FFFF00)
for bx in range(24, 296, 12):
b.draw_pixel(bx, 13, 0x00FFFF00)
b.draw_pixel(bx, 36, 0x00FFD700)
# Status meters
b.draw_box_border(20, 44, 110, 18, 0x000E1A0A, 0x00224B1A, 1)
b.draw_string(24, 49, 'БАНК: $12500', 0x003CFF3C)
b.draw_box_border(190, 44, 110, 18, 0x001A100A, 0x003A2814, 1)
b.draw_string(194, 49, 'СТАВКА: $250', 0x00FFD700)
# Side indicators
# Left: Heads (Selected)
b.draw_box(20, 72, 76, 68, 0x000A101C)
b.draw_box_border(20, 72, 76, 68, 0x00101828, 0x00FFFF00, 2)
b.draw_string(30, 78, '[1] ОРЁЛ', 0x00FFFF00)
b.draw_coin_ellipse(58, 106, 14, 14, 0x00D0A000, 0x00FFD700, 1)
b.draw_string(24, 126, 'ВЫБРАНО', 0x003CFF3C)
# Right: Tails
b.draw_box(224, 72, 76, 68, 0x000A101C)
b.draw_box_border(224, 72, 76, 68, 0x00101828, 0x00404855, 1)
b.draw_string(234, 78, '[2] РЕШКА', 0x00D0D8E0)
b.draw_coin_ellipse(262, 106, 14, 14, 0x00A08000, 0x00D0B000, 2)
# Center coin spotlight
b.draw_circle(160, 110, 48, 0x00121620)
b.draw_circle(160, 110, 44, 0x00FFD700)
b.draw_circle(160, 110, 42, 0x000E141E)
b.draw_coin_ellipse(160, 110, 26, 26, 0x00D0A000, 0x00FFD700, 1)
# Result Banner
b.draw_box_border(44, 154, 232, 22, 0x000E141E, 0x003CFF3C, 1)
b.draw_string(60, 161, 'ПОБЕДА: +$500! СЕРИЯ x3', 0x003CFF3C)
# Bottom Keycaps
kx = 16
kx += b.draw_keycap(kx, 192, '[1] ОРЁЛ', is_active=True)
kx += b.draw_keycap(kx, 192, '[2] РЕШКА', is_active=False)
kx += b.draw_keycap(kx, 192, '[SPACE]', is_active=True)
b.draw_keycap(kx, 192, '[ESC]', is_active=False)
b.save(filename)
# ==============================================================================
# 2. RENDER DICE GAME (Кости)
# ==============================================================================
def render_dice(filename='dice_preview.png'):
b = RasterBuffer()
b.draw_box(0, 0, 320, 240, 0x00140906) # Mahogany frame
b.draw_box_border(6, 6, 308, 228, 0x000A3816, 0x00FFD700, 2) # Deep Green Felt
# Header
b.draw_box_border(20, 12, 280, 26, 0x00072E13, 0x00FFD700, 2)
b.draw_string(48, 21, '=== G R A N D C R A P S ===', 0x00FFFF00)
# Status
b.draw_box_border(20, 44, 110, 18, 0x00072E13, 0x001B4B22, 1)
b.draw_string(24, 49, 'БАНК: $12500', 0x003CFF3C)
b.draw_box_border(190, 44, 110, 18, 0x00072E13, 0x001B4B22, 1)
b.draw_string(194, 49, 'СТАВКА: $100', 0x00FFD700)
# Betting Pass Lines
# [1] ЧЁТ
b.draw_box_border(16, 70, 88, 48, 0x00072E13, 0x00FFFF00, 2)
b.draw_string(30, 76, '[1] ЧЁТ', 0x00FFFF00)
b.draw_string(22, 90, ' 2 4 6 8 10 ', 0x00FFD700)
b.draw_string(26, 104, 'ВЫБРАНО', 0x003CFF3C)
# [2] НЕЧЕТ
b.draw_box_border(216, 70, 88, 48, 0x00072E13, 0x001B4B22, 1)
b.draw_string(226, 76, '[2] НЕЧЕТ', 0x00D0D8E0)
b.draw_string(224, 90, ' 3 5 7 9 11 ', 0x0080B090)
# Center Craps Table: 2 Dice
b.draw_craps_die(116, 80, 36, 4)
b.draw_craps_die(168, 76, 36, 6)
# Sum & Result
b.draw_box_border(36, 132, 248, 36, 0x00072E13, 0x00FFD700, 1)
b.draw_string(48, 138, 'СУММА: 4 + 6 = 10 (ЧЁТ)', 0x00FFFF00)
b.draw_string(68, 152, 'ВЫИГРЫШ: +$200! (x2)', 0x003CFF3C)
# Keycaps
kx = 16
kx += b.draw_keycap(kx, 192, '[1] ЧЁТ', is_active=True)
kx += b.draw_keycap(kx, 192, '[2] НЕЧЕТ', is_active=False)
kx += b.draw_keycap(kx, 192, '[SPACE]', is_active=True)
b.draw_keycap(kx, 192, '[ESC]', is_active=False)
b.save(filename)
# ==============================================================================
# 3. RENDER SKULLS GAME (Напёрстки)
# ==============================================================================
def render_skulls(filename='skulls_preview.png'):
b = RasterBuffer()
b.draw_box(0, 0, 320, 240, 0x00140906)
b.draw_box_border(6, 6, 308, 228, 0x00300F22, 0x00FFD700, 2) # Gothic Purple
# Header
b.draw_box_border(20, 12, 280, 26, 0x001A0812, 0x00FFD700, 2)
b.draw_string(44, 21, '* * * Н А П Ё Р С Т К И * * *', 0x00FFFF00)
# Status
b.draw_box_border(20, 44, 110, 18, 0x001A0812, 0x00452033, 1)
b.draw_string(24, 49, 'БАНК: $12500', 0x003CFF3C)
b.draw_box_border(190, 44, 110, 18, 0x001A0812, 0x00452033, 1)
b.draw_string(194, 49, 'СТАВКА: $300', 0x00FFD700)
# Subtitle
b.draw_string(40, 62, 'ВЫИГРЫШ: +$900! (x3) ВЫ УГАДАЛИ!', 0x003CFF3C)
# 3 Cups: Cup 2 lifted revealing gold ball
b.draw_gothic_cup(36, 114, 1, is_lifted=False, has_ball=False)
b.draw_gothic_cup(136, 114, 2, is_lifted=True, has_ball=True)
b.draw_gothic_cup(236, 114, 3, is_lifted=False, has_ball=False)
# Selection indicators
b.draw_string(54, 168, '[ 1 ]', 0x00D0D8E0)
b.draw_string(150, 168, '[ 2 ]', 0x00FFFF00)
b.draw_string(250, 168, '[ 3 ]', 0x00D0D8E0)
# Keycaps
kx = 16
kx += b.draw_keycap(kx, 192, '[1]', is_active=False)
kx += b.draw_keycap(kx, 192, '[2]', is_active=True)
kx += b.draw_keycap(kx, 192, '[3]', is_active=False)
kx += b.draw_keycap(kx, 192, '[SPACE] СТАРТ', is_active=True)
b.draw_keycap(kx, 192, '[ESC]', is_active=False)
b.save(filename)
# ==============================================================================
# 4. RENDER SKILL CHECK GAME (Dead by Daylight Survivor Check)
# ==============================================================================
def render_skill(filename='skill_preview.png'):
b = RasterBuffer()
# Industrial survivor steel bezel
b.draw_box(0, 0, 320, 240, 0x00121416)
b.draw_box_border(6, 6, 308, 228, 0x001A1D20, 0x00FFD700, 2)
# Header
b.draw_box_border(20, 12, 280, 26, 0x00121418, 0x00FFD700, 2)
b.draw_string(44, 21, '* * * Н А П Ё Р С Т К И * * *', 0x00FFFF00)
# Status
b.draw_box_border(20, 44, 110, 18, 0x00121418, 0x002A3540, 1)
b.draw_string(24, 49, 'БАНК: $12500', 0x003CFF3C)
b.draw_box_border(190, 44, 110, 18, 0x00121418, 0x002A3540, 1)
b.draw_string(194, 49, 'СТАВКА: $200', 0x00FFD700)
# Survivor progress
b.draw_string(24, 70, 'ХИТ: 3 / 5', 0x003CFF3C)
b.draw_string(180, 70, 'ПРОМАХ: 1 / 3', 0x00FF6464)
# Tension Dial / Track: X=50 to 270, Y=100 to 134
track_x = 50
track_y = 102
track_w = 220
track_h = 32
# Outer frame
b.draw_box_border(track_x, track_y, track_w, track_h, 0x000F1114, 0x00404855, 2)
# Calibrated ticks
for tx in range(track_x + 10, track_x + track_w - 10, 12):
b.draw_box(tx, track_y + 2, 1, 6, 0x00303845)
b.draw_box(tx, track_y + track_h - 8, 1, 6, 0x00303845)
# Normal Success Zone (Green): X=150 to 198 (48px)
b.draw_box_border(150, track_y + 4, 48, track_h - 8, 0x00155020, 0x003CFF3C, 1)
# Great / Perfect Sweet Spot (Gold): X=186 to 198 (12px)
b.draw_box_border(186, track_y + 4, 12, track_h - 8, 0x00FFD700, 0x00FFFF00, 1)
b.draw_string(156, track_y + 10, 'HIT!', 0x003CFF3C)
# Needle at X=190 (Inside Great Zone!)
needle_x = 190
b.draw_box(needle_x - 1, track_y - 4, 3, track_h + 8, 0x00FFFFFF)
b.draw_box(needle_x - 2, track_y - 6, 5, 2, 0x00FF3030)
b.draw_box(needle_x - 2, track_y + track_h + 4, 5, 2, 0x00FF3030)
# Prompt text
b.draw_string(52, 150, 'ОТЛИЧНО! GREAT SKILL CHECK!', 0x00FFFF00)
b.draw_string(76, 166, 'НАЖМИТЕ [ПРОБЕЛ] В ЗОНЕ', 0x00D0D8E0)
# Bottom Keycaps
kx = 16
kx += b.draw_keycap(kx, 192, '[SPACE] РЕАКЦИЯ', is_active=True)
kx += b.draw_keycap(kx, 192, '[ВВЕРХ/ВНИЗ]', is_active=False)
b.draw_keycap(kx, 192, '[ESC]', is_active=False)
b.save(filename)
if __name__ == '__main__':
render_coin()
render_dice()
render_skulls()
render_skill()
print("[ALL DONE] All 4 previews successfully generated!")