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

128 lines
4.0 KiB
Python

import json
from ..constants import BOARD_HEIGHT, BOARD_WIDTH, SHAPES, STATE_VERSION
from ..game.pieces import _collides
from ..game.rng import _empty_board, _pop_shape, _spawn_piece
def _default_state(seed):
state = {
"version": STATE_VERSION,
"board": _empty_board(),
"bag": [],
"bag_count": 0,
"seed": seed,
"start_level": 1,
"level_progression": "fixed",
"level": 1,
"piece": None,
"next_piece_shape": None,
"hold_piece_shape": None,
"hold_used": False,
"score": 0,
"lines_cleared_total": 0,
"tetrises": 0,
"tspins": 0,
"combo_streak": 0,
"combo_total": 0,
"goal_lines_total": 0.0,
"b2b_active": False,
"game_over": False,
"last_action": None,
"last_rotate_kick": None,
"tspin": "none",
"options": {},
}
state["piece"] = _spawn_piece(_pop_shape(state))
state["next_piece_shape"] = _pop_shape(state)
if _collides(state["board"], state["piece"]):
state["game_over"] = True
return state
def _deserialize_state(state_json, seed, enforce_seed=True):
if not state_json:
return _default_state(seed)
try:
state = json.loads(state_json)
except json.JSONDecodeError:
return _default_state(seed)
if not isinstance(state, dict):
return _default_state(seed)
if state.get("version") != STATE_VERSION:
return _default_state(seed)
board = state.get("board")
piece = state.get("piece")
if not _valid_board(board) or not _valid_piece(piece):
return _default_state(seed)
if enforce_seed and state.get("seed") != seed:
return _default_state(seed)
if "bag" not in state or "bag_count" not in state:
state["bag"] = []
state["bag_count"] = 0
next_shape = state.get("next_piece_shape")
if next_shape not in SHAPES:
state["next_piece_shape"] = _pop_shape(state)
if "score" not in state:
state["score"] = 0
if "lines_cleared_total" not in state:
state["lines_cleared_total"] = 0
if "tetrises" not in state:
state["tetrises"] = 0
if "tspins" not in state:
state["tspins"] = 0
if "combo_streak" not in state:
state["combo_streak"] = 0
if "combo_total" not in state:
state["combo_total"] = 0
if "goal_lines_total" not in state:
state["goal_lines_total"] = float(state.get("lines_cleared_total", 0))
if "b2b_active" not in state:
state["b2b_active"] = False
if "game_over" not in state:
state["game_over"] = False
if "start_level" not in state:
state["start_level"] = 1
if "level_progression" not in state:
state["level_progression"] = "fixed"
if "level" not in state:
state["level"] = state.get("start_level", 1)
if "last_action" not in state:
state["last_action"] = None
if "last_rotate_kick" not in state:
state["last_rotate_kick"] = None
if "tspin" not in state:
state["tspin"] = "none"
if "options" not in state:
state["options"] = {}
if "hold_piece_shape" not in state:
state["hold_piece_shape"] = None
if state.get("hold_piece_shape") not in SHAPES:
state["hold_piece_shape"] = None
if "hold_used" not in state:
state["hold_used"] = False
return state
def _valid_board(board):
if not isinstance(board, list) or len(board) != BOARD_HEIGHT:
return False
for row in board:
if not isinstance(row, list) or len(row) != BOARD_WIDTH:
return False
for cell in row:
if cell != 0 and cell not in SHAPES:
return False
return True
def _valid_piece(piece):
if not isinstance(piece, dict):
return False
shape = piece.get("shape")
if shape not in SHAPES:
return False
for key in ("rot", "x", "y"):
if not isinstance(piece.get(key), int):
return False
return True