KAZIK/tools/tests/test_minigames_render.py

379 lines
15 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 -*-
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):
# 0x00BBGGRR format
r = c & 0xFF
g = (c >> 8) & 0xFF
b = (c >> 16) & 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:
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 = 0x0000FFFF
fill_c = 0x001B4B22
txt_c = 0x0000FFFF
else:
border_c = 0x00606870
fill_c = 0x001A1C22
txt_c = 0x00D0D8E0
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):
# Draw stack of 3D casino clay chips
for i in range(count):
cy = y - i * 4
self.draw_box(x + 2, cy + 5, 20, 2, 0x00041006) # shadow
self.draw_box_border(x, cy, 24, 6, 0x001515B8, 0x00FFFFFF, 1)
# white stripe inlays
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 = 0x00007A9A
# Imperial Eagle
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, 0x0000D7FF)
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 = 0x00007A9A
# Roman numeral 'I'
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, 0x0000D7FF)
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, 0x00031408)
self.draw_box(x + size, y + 3, 3, size, 0x00031408)
body_color = 0x00F0F4FA
bevel_color = 0x00C0C6D0
border_color = 0x00353540
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 = 0x001515C8
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, 0x000A0410)
else:
self.draw_box(x + 10, y + 42, 28, 2, 0x0012061A)
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, 0x00221A30, 0x0000B0D0, 1)
self.draw_box(x + 18, lift_y + 28, 12, 8, 0x00332545)
self.draw_box(x + 22, lift_y + 28, 4, 8, 0x0055406E)
self.draw_box_border(x + 8, lift_y + 8, 32, 20, 0x002D1E40, 0x0000D7FF, 1)
self.draw_box(x + 14, lift_y + 10, 6, 16, 0x00503570)
self.draw_box(x + 17, lift_y + 10, 3, 16, 0x007850A0)
self.draw_box_border(x + 4, lift_y + 2, 40, 7, 0x00150A20, 0x0000FFFF, 1)
self.draw_box(x + 12, lift_y + 4, 16, 2, 0x0000FFFF)
self.draw_box(x + 21, lift_y + 12, 6, 5, 0x0000D7FF)
self.draw_box(x + 22, lift_y + 17, 4, 3, 0x0000D7FF)
self.draw_pixel(x + 22, lift_y + 14, 0x00150A20)
self.draw_pixel(x + 24, lift_y + 14, 0x00150A20)
self.draw_string(x + 21, lift_y + 24, str(cup_id), 0x0000FFFF)
def draw_glowing_orb(self, cx, cy):
self.draw_circle(cx, cy, 14, 0x00081E2E)
self.draw_circle(cx, cy, 11, 0x00103A58)
self.draw_circle(cx, cy, 8, 0x001A588A)
self.draw_circle(cx, cy, 6, 0x0000B0E0)
self.draw_circle(cx, cy, 4, 0x0000E0FF)
self.draw_circle(cx - 1, cy - 1, 2, 0x00A0FFFF)
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, 0x00060914) # Mahogany cabinet
b.draw_box_border(6, 6, 308, 228, 0x000F1522, 0x001B2E48, 3)
for cx in [12, 306]:
for cy in [12, 226]:
b.draw_box(cx, cy, 3, 3, 0x0000D7FF)
b.draw_pixel(cx + 1, cy + 1, 0x00FFFFFF)
b.draw_box_border(20, 12, 280, 26, 0x000A101C, 0x0000D7FF, 2)
b.draw_string(44, 21, '*** З О Л О Т А Я М О Н Е Т А ***', 0x0000FFFF)
for bx in range(24, 296, 12):
b.draw_pixel(bx, 13, 0x0000FFFF)
b.draw_pixel(bx, 36, 0x0000D7FF)
# Status meters
b.draw_box_border(20, 44, 110, 18, 0x000A1A0E, 0x001A4B22, 1)
b.draw_string(24, 49, 'БАНК: $12500', 0x0050FF50)
b.draw_box_border(190, 44, 110, 18, 0x001A100A, 0x003A2814, 1)
b.draw_string(194, 49, 'СТАВКА: $50 [W/S]', 0x0000D7FF)
# Center Dish
b.draw_circle(160, 110, 46, 0x000090C0)
b.draw_circle(160, 110, 44, 0x0000D7FF)
b.draw_circle(160, 110, 41, 0x00180824)
# Coin at rest (Heads)
b.draw_coin_ellipse(160, 110, 26, 26, 0x0000A0D0, 0x0000D7FF, relief=1)
# Left: [1] ОРЕЛ (Active)
b.draw_box_border(20, 72, 76, 52, 0x001C3822, 0x0000FFFF, 2)
b.draw_string(28, 78, '[1] ОРЁЛ', 0x0000FFFF)
b.draw_coin_ellipse(58, 106, 12, 12, 0x0000A0D0, 0x0000D7FF, relief=1)
# Right: [2] РЕШКА
b.draw_box_border(224, 72, 76, 52, 0x00121620, 0x00404855, 1)
b.draw_string(232, 78, '[2] РЕШКА', 0x0090A0B0)
b.draw_coin_ellipse(262, 106, 12, 12, 0x000080A0, 0x0000B0D0, relief=2)
# Streak
b.draw_box_border(44, 154, 232, 20, 0x000E141E, 0x0000D7FF, 1)
b.draw_string(52, 160, 'СЕРИЯ: [X] [X] [X] [ ] [ ] x4!', 0x0000FFFF)
# Keycaps
b.draw_box(16, 194, 288, 1, 0x001E2E40)
b.draw_keycap(22, 202, '1:ОРЁЛ', is_active=True)
b.draw_keycap(88, 202, '2:РЕШКА')
b.draw_keycap(160, 202, 'ПРОБЕЛ:БРОСОК', is_active=True)
b.draw_keycap(274, 202, 'ESC')
b.save(filename)
# ==============================================================================
# 2. RENDER DICE GAME
# ==============================================================================
def render_dice(filename='dice_preview.png'):
b = RasterBuffer()
# Emerald felt
b.draw_box(0, 0, 320, 240, 0x00082E12)
# Oak wood border & leather bumper
b.draw_box_border(4, 4, 312, 232, 0x00093816, 0x00122244, 3)
b.draw_box_border(8, 8, 304, 224, 0x000A3F19, 0x0005140A, 2)
# Pyramidal backboard rubber diamond texture along top
for bx in range(12, 308, 12):
b.draw_box(bx + 4, 11, 4, 4, 0x00041006)
b.draw_pixel(bx + 5, 12, 0x000F4820)
# Header
b.draw_string(48, 20, '==== G R A N D C R A P S T A B L E ====', 0x0000D7FF)
b.draw_string(72, 32, '[ PASS LINE ] [ ANY CRAPS ]', 0x0000B0A0)
# Status
b.draw_box_border(16, 46, 100, 16, 0x0006240E, 0x000E5022, 1)
b.draw_string(20, 50, 'БАНК: $12500', 0x0050FF50)
b.draw_box_border(204, 46, 100, 16, 0x0006240E, 0x000E5022, 1)
b.draw_string(208, 50, 'СТАВКА: $50 [W/S]', 0x0000D7FF)
# Betting Zones
# Left: [1] ЧЕТ (Active with chip stack!)
b.draw_box_border(16, 68, 88, 54, 0x000B441D, 0x0000FFFF, 2)
b.draw_string(24, 74, '[1] ЧЁТ', 0x0000FFFF)
b.draw_string(20, 86, '2 4 6 8 10 12', 0x0000D7FF)
b.draw_chip_stack(48, 110, count=4)
# Right: [2] НЕЧЕТ
b.draw_box_border(216, 68, 88, 54, 0x00073014, 0x001A602A, 1)
b.draw_string(224, 74, '[2] НЕЧЕТ', 0x0080B090)
b.draw_string(222, 86, '3 5 7 9 11', 0x00508060)
# Center Dice Field
b.draw_box_border(112, 68, 96, 54, 0x00072E13, 0x00145826, 1)
# Draw Two Precision Dice
b.draw_craps_die(120, 76, 36, 4)
b.draw_craps_die(164, 76, 36, 6)
# Score Plaque
b.draw_box_border(36, 130, 248, 30, 0x00051D0C, 0x0000D7FF, 1)
b.draw_string(44, 135, 'СУММА: 4 + 6 = 10 (ЧЁТ)', 0x0000FFFF)
b.draw_string(68, 147, 'ВЫИГРЫШ: +$100! (x2)', 0x0050FF50)
# Keycaps
b.draw_box(16, 188, 288, 1, 0x00104820)
b.draw_keycap(20, 198, '1:ЧЁТ', is_active=True)
b.draw_keycap(82, 198, '2:НЕЧЕТ')
b.draw_keycap(160, 198, 'ПРОБЕЛ:БРОСОК', is_active=True)
b.draw_keycap(274, 198, 'ESC')
b.save(filename)
# ==============================================================================
# 3. RENDER SKULLS GAME
# ==============================================================================
def render_skulls(filename='skulls_preview.png'):
b = RasterBuffer()
# Dark gothic table with purple velvet
b.draw_box(0, 0, 320, 240, 0x000D0612)
b.draw_box_border(4, 4, 312, 232, 0x00160B20, 0x00321845, 3)
b.draw_box_border(8, 8, 304, 224, 0x00220F30, 0x0000D7FF, 1)
# Header
b.draw_box_border(24, 12, 272, 24, 0x0012081A, 0x0000D7FF, 2)
b.draw_string(48, 20, '+ + + Н А П Ё Р С Т К И С К В Е Р Н Ы + + +', 0x0000FFFF)
# Status
b.draw_box_border(20, 42, 110, 16, 0x000D0616, 0x00401858, 1)
b.draw_string(24, 46, 'БАНК: $12500', 0x0050FF50)
b.draw_box_border(190, 42, 110, 16, 0x000D0616, 0x00401858, 1)
b.draw_string(194, 46, 'СТАВКА: $50 [W/S]', 0x0000D7FF)
# Table Runner
b.draw_box_border(16, 66, 288, 114, 0x001A0B25, 0x00452060, 1)
# Narrative banner
b.draw_string(40, 74, 'СЛЕДИ ЗА ЗОЛОТЫМ ОКОМ СУДЬБЫ...', 0x0000FFFF)
# Draw 3 Gothic Cups (Cup 2 is lifted revealing glowing orb!)
b.draw_gothic_cup(48, 100, 1, is_lifted=False)
b.draw_gothic_cup(136, 100, 2, is_lifted=True, has_ball=True)
b.draw_gothic_cup(224, 100, 3, is_lifted=False)
# Keycaps
b.draw_box(16, 190, 288, 1, 0x003A1850)
b.draw_keycap(18, 200, '1:КУБОК')
b.draw_keycap(82, 200, '2:КУБОК', is_active=True)
b.draw_keycap(146, 200, '3:КУБОК')
b.draw_keycap(212, 200, 'ПРОБЕЛ', is_active=True)
b.draw_keycap(274, 200, 'ESC')
b.save(filename)
if __name__ == '__main__':
render_coin()
render_dice()
render_skulls()
print("All 3 minigames previews rendered successfully!")