# -*- 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!")