feat: add Prism snake and gameplay database lifecycle
Build and Push Docker Container / build-and-push (push) Successful in 8m3s

- Add bitboard-accelerated Prism and versioned Supreme snake implementations.
- Add database-backed move benchmarks and focused strategy tests.
- Normalize gameplay storage while preserving replay compatibility.
- Add deterministic game quality scoring and replay retention tiers.
- Add backup-first SQLite cleanup, verification, and replacement tooling.
- Add safe compact-plus-delta database merging with conflict detection.
- Extend SQLite and PostgreSQL schemas for replay and quality metadata.
- Add PostgreSQL development service and pytest import configuration.
- Update gameplay documentation and the quart_common submodule revision.
This commit is contained in:
2026-08-01 16:21:02 +02:00
parent 9a7f4de586
commit c704fbc742
21 changed files with 3156 additions and 88 deletions
+84
View File
@@ -0,0 +1,84 @@
"""Normalized gameplay turn storage and replay hydration.
A turn is split into one board row plus one snake row per participating snake.
Static snake identity belongs to ``game_snakes``. This avoids storing complete
snake payloads in the board, snakes, you, and snake-turn columns simultaneously.
"""
from typing import Callable
def compact_turn_json(board:dict) -> tuple[dict, list, dict, list, list]:
"""Return compatibility JSON plus canonical food and hazard values."""
return {}, [], {}, board.get("food", []), board.get("hazards", [])
def hydrate_replay_turns(game_row, turn_rows, snake_rows, decode_json:Callable) -> list[dict]:
rows_by_turn:dict[int, list] = {}
for row in snake_rows:
rows_by_turn.setdefault(int(row["turn"]), []).append(row)
turns = []
for row in turn_rows:
turn = int(row["turn"])
stored_board = decode_json(row["board_state_json"]) or {}
food = decode_json(row["food_json"])
hazards = decode_json(row["hazards_json"])
snakes = []
api_snakes = []
for snake_row in rows_by_turn.get(turn, []):
body = decode_json(snake_row["body_json"]) or []
api_snake = {
"id": snake_row["snake_id"],
"name": snake_row["snake_name"],
"health": snake_row["health"],
"length": snake_row["length"],
"head": {"x": snake_row["head_x"], "y": snake_row["head_y"]},
"body": body,
}
if snake_row["latency"] is not None:
api_snake["latency"] = snake_row["latency"]
api_snakes.append(api_snake)
snakes.append({
"snake_id": snake_row["snake_id"],
"snake_name": snake_row["snake_name"],
"health": snake_row["health"],
"length": snake_row["length"],
"head": api_snake["head"],
"body": body,
"is_you": bool(snake_row["is_you"]),
"inferred_move": snake_row["inferred_move"],
"latency": snake_row["latency"],
})
board = stored_board or {
"width": game_row["width"],
"height": game_row["height"],
"food": food or [],
"hazards": hazards or [],
"snakes": api_snakes,
}
if food is None:
food = board.get("food", [])
if hazards is None:
hazards = board.get("hazards", [])
you = decode_json(row["you_json"]) or {}
if not you:
you = next(
(snake for snake in api_snakes if snake["id"] == game_row["your_snake_id"]),
{},
)
turns.append({
"turn": turn,
"observed_at": row["observed_at"],
"my_move": row["my_move"],
"my_thinking": decode_json(row["my_thinking_json"]),
"board": board,
"food": food or [],
"hazards": hazards or [],
"you": you,
"snakes": snakes,
})
return turns