Add native Durak, poker, blackjack, roulette, Buckshot and billiards scenes with animation assets and regression tests. Improve arrival, city, hall, profiles, shops, audio and exit handling; remove the portable C game core and refresh shipping executables.
128 lines
5 KiB
Python
128 lines
5 KiB
Python
"""Compile the original ANSI animation into compact native glyph/color frames."""
|
|
import json
|
|
import re
|
|
import struct
|
|
from pathlib import Path
|
|
|
|
from PIL import Image, ImageDraw, ImageFont
|
|
|
|
COLS, ROWS = 72, 21
|
|
CELL_W, CELL_H = 8, 16
|
|
ESCAPE = re.compile(r"\x1b\[([0-9;?]*)([A-Za-z])")
|
|
|
|
|
|
def palette():
|
|
colors = [0x000000, 0x800000, 0x008000, 0x808000, 0x000080, 0x800080, 0x008080, 0xC0C0C0,
|
|
0x808080, 0xFF0000, 0x00FF00, 0xFFFF00, 0x0000FF, 0xFF00FF, 0x00FFFF, 0xFFFFFF]
|
|
levels = [0, 95, 135, 175, 215, 255]
|
|
colors += [(r << 16) | (g << 8) | b for r in levels for g in levels for b in levels]
|
|
colors += [((8 + i * 10) * 0x010101) for i in range(24)]
|
|
return colors
|
|
|
|
|
|
def parse_frame(text):
|
|
cells = [[(" ", 15) for _ in range(COLS)] for _ in range(ROWS)]
|
|
x = y = 0
|
|
color = 15
|
|
offset = 0
|
|
while offset < len(text):
|
|
match = ESCAPE.match(text, offset)
|
|
if match:
|
|
parameters, command = match.groups()
|
|
if command == "m":
|
|
values = [int(v) for v in parameters.split(";") if v]
|
|
if len(values) == 3 and values[:2] == [38, 5]:
|
|
color = values[2]
|
|
elif values == [0]:
|
|
color = 15
|
|
else:
|
|
raise ValueError(f"Unsupported foreground escape {match.group()!r}")
|
|
elif command == "H":
|
|
x = y = 0
|
|
elif command == "J" and parameters == "2":
|
|
cells = [[(" ", 15) for _ in range(COLS)] for _ in range(ROWS)]
|
|
elif parameters == "?25" and command in ("h", "l"):
|
|
pass
|
|
else:
|
|
raise ValueError(f"Unsupported escape {match.group()!r}")
|
|
offset = match.end()
|
|
continue
|
|
char = text[offset]
|
|
offset += 1
|
|
if char == "\r":
|
|
x = 0
|
|
elif char == "\n":
|
|
x = 0
|
|
y += 1
|
|
else:
|
|
if char != " " and (x >= COLS or y >= ROWS):
|
|
raise ValueError(f"Visible cell outside frame: ({x}, {y})")
|
|
if x < COLS and y < ROWS:
|
|
cells[y][x] = (char, color)
|
|
x += 1
|
|
return [cell for row in cells for cell in row]
|
|
|
|
|
|
def glyph(char, font):
|
|
image = Image.new("L", (CELL_W, CELL_H))
|
|
draw = ImageDraw.Draw(image)
|
|
# Box/block glyphs fill their cells, so the old logo remains continuous.
|
|
if char == "\u2588":
|
|
draw.rectangle((0, 0, 7, 15), fill=255)
|
|
elif char in "\u2550\u2551\u2554\u2557\u255a\u255d":
|
|
if char == "\u2550":
|
|
draw.line((0, 6, 7, 6), fill=255)
|
|
draw.line((0, 9, 7, 9), fill=255)
|
|
elif char == "\u2551":
|
|
draw.line((2, 0, 2, 15), fill=255)
|
|
draw.line((5, 0, 5, 15), fill=255)
|
|
else:
|
|
left = char in "\u2554\u255a"
|
|
top = char in "\u2554\u2557"
|
|
edge_x, end_y = (7 if left else 0), (15 if top else 0)
|
|
for x, y in [(2, 6), (5, 9)]:
|
|
draw.line((x, y, x, end_y), fill=255)
|
|
draw.line((x, y, edge_x, y), fill=255)
|
|
elif char != " ":
|
|
draw.text((0, 0), char, font=font, fill=255)
|
|
return bytes(sum((1 << (7 - x)) for x in range(CELL_W) if image.getpixel((x, y)) >= 96)
|
|
for y in range(CELL_H))
|
|
|
|
|
|
def main():
|
|
root = Path(__file__).resolve().parents[2]
|
|
source = root / "assets/animations/casino_intro.ans"
|
|
raw = source.read_bytes()
|
|
text = raw.decode("utf-8")
|
|
parts = text.replace("\r\n", "\n").split("\f")
|
|
frames, loop_start = [], None
|
|
for part in parts:
|
|
if part.strip() == "@@LOOP@@":
|
|
loop_start = len(frames)
|
|
elif part:
|
|
frames.append(parse_frame(part))
|
|
if loop_start is None or not 0 <= loop_start < len(frames):
|
|
raise ValueError("Missing loop boundary")
|
|
characters = [" "] + sorted({char for frame in frames for char, _ in frame} - {" "})
|
|
if len(characters) > 256:
|
|
raise ValueError("Too many glyphs")
|
|
indexes = {char: i for i, char in enumerate(characters)}
|
|
font = ImageFont.truetype("C:/Windows/Fonts/consola.ttf", 13)
|
|
masks = b"".join(glyph(char, font) for char in characters).ljust(4096, b"\0")
|
|
colors = palette()
|
|
payload = struct.pack("<256I", *colors) + masks
|
|
payload += b"".join(struct.pack("<H", indexes[char] | color << 8)
|
|
for frame in frames for char, color in frame)
|
|
output = root / "assets/animations/intro/eblan-sequence.bin"
|
|
output.parent.mkdir(parents=True, exist_ok=True)
|
|
output.write_bytes(struct.pack("<4s8I", b"EBGA", COLS, ROWS, len(frames), loop_start,
|
|
45, len(characters), len(payload), 1) + payload)
|
|
manifest = {"source": str(source.relative_to(root)), "frames": len(frames),
|
|
"loop_start": loop_start, "frame_ms": 45, "glyphs": len(characters),
|
|
"bytes": output.stat().st_size, "encoding": "UTF-8 original / native binary runtime"}
|
|
(output.with_suffix(".json")).write_text(json.dumps(manifest, indent=2), encoding="utf-8")
|
|
print(json.dumps(manifest))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|