379 lines
15 KiB
Python
379 lines
15 KiB
Python
# -*- 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!")
|