Files
Erik W. a62da8926d release: v2.3.0 refactor and UI/audio fixes
- bump project/runtime version markers to v2.3.0 (pyproject + TetriNode UI version)

- complete behavior-preserving refactor from monoliths into modular Python and JS architecture (tetrinode/ + js/live/)

- add parity + standard regression harnesses under qa/

- fix queue panel layout so Queue bottom stays aligned to matrix bottom regardless of Controls visibility

- fix music playback fallback when selected track is none, including modal Play behavior and runtime playback path
2026-02-08 17:14:25 -05:00

88 lines
3.0 KiB
Python

import numpy as np
import torch
from PIL import Image, ImageDraw
from ..constants import COLORS, PREVIEW_GRID, SHAPES
from ..game.rng import _new_bag
def _render_next_piece(shape, block_size, colors=None):
palette = colors or COLORS
grid = PREVIEW_GRID
size = grid * block_size
img = Image.new("RGB", (size, size), palette["X"])
draw = ImageDraw.Draw(img)
cells = SHAPES[shape][0]
min_x = min(x for x, _ in cells)
min_y = min(y for _, y in cells)
max_x = max(x for x, _ in cells)
max_y = max(y for _, y in cells)
shape_w = max_x - min_x + 1
shape_h = max_y - min_y + 1
offset_x = (grid - shape_w) // 2 - min_x
offset_y = (grid - shape_h) // 2 - min_y
for x, y in cells:
gx = x + offset_x
gy = y + offset_y
if 0 <= gx < grid and 0 <= gy < grid:
x0 = gx * block_size
y0 = gy * block_size
draw.rectangle([x0, y0, x0 + block_size - 2, y0 + block_size - 2], fill=palette[shape])
arr = np.array(img).astype(np.float32) / 255.0
return torch.from_numpy(arr)[None, ...]
def _get_upcoming_shapes(state, count):
if count <= 0:
return []
upcoming = [state.get("next_piece_shape")] + list(state.get("bag", []))
bag_count = state.get("bag_count", 0)
seed = state.get("seed", 0)
while len(upcoming) < count:
bag = _new_bag(seed, bag_count)
bag_count += 1
upcoming.extend(bag)
return upcoming[:count]
def _render_queue(shapes, block_size, colors=None):
palette = colors or COLORS
if not shapes:
size = PREVIEW_GRID * block_size
img = Image.new("RGB", (size, size), palette["X"])
arr = np.array(img).astype(np.float32) / 255.0
return torch.from_numpy(arr)[None, ...]
gap = block_size
width = PREVIEW_GRID * block_size
height = len(shapes) * PREVIEW_GRID * block_size + max(0, len(shapes) - 1) * gap
img = Image.new("RGB", (width, height), palette["X"])
draw = ImageDraw.Draw(img)
for idx, shape in enumerate(shapes):
if shape not in SHAPES:
continue
offset_y = idx * (PREVIEW_GRID * block_size + gap)
cells = SHAPES[shape][0]
min_x = min(x for x, _ in cells)
min_y = min(y for _, y in cells)
max_x = max(x for x, _ in cells)
max_y = max(y for _, y in cells)
shape_w = max_x - min_x + 1
shape_h = max_y - min_y + 1
offset_x = (PREVIEW_GRID - shape_w) // 2 - min_x
offset_cell_y = (PREVIEW_GRID - shape_h) // 2 - min_y
for x, y in cells:
gx = x + offset_x
gy = y + offset_cell_y
if 0 <= gx < PREVIEW_GRID and 0 <= gy < PREVIEW_GRID:
x0 = gx * block_size
y0 = offset_y + gy * block_size
draw.rectangle(
[x0, y0, x0 + block_size - 2, y0 + block_size - 2],
fill=palette[shape],
)
arr = np.array(img).astype(np.float32) / 255.0
return torch.from_numpy(arr)[None, ...]