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
@@ -18,6 +18,7 @@ from pathlib import Path
from urllib.parse import urlparse, urlunparse
from .Template import GameplayBackendTemplate
from server.database.normalized_turn import compact_turn_json
logger = logging.getLogger(__name__)
if not logger.handlers:
@@ -47,7 +48,12 @@ CREATE TABLE IF NOT EXISTS games (
winner_name TEXT,
winner_you BOOLEAN NOT NULL DEFAULT FALSE,
final_turn INTEGER NOT NULL DEFAULT 0,
status TEXT NOT NULL DEFAULT 'running'
status TEXT NOT NULL DEFAULT 'running',
has_replay BOOLEAN NOT NULL DEFAULT TRUE,
quality_status TEXT NOT NULL DEFAULT 'retained',
quality_score INTEGER,
quality_tier TEXT,
quality_reasons JSONB
);
CREATE TABLE IF NOT EXISTS turns (
@@ -65,6 +71,14 @@ CREATE TABLE IF NOT EXISTS turns (
UNIQUE (game_id, turn)
);
CREATE TABLE IF NOT EXISTS game_snakes (
game_id TEXT NOT NULL REFERENCES games(game_id) ON DELETE CASCADE,
snake_id TEXT NOT NULL,
snake_name TEXT,
is_you BOOLEAN NOT NULL DEFAULT FALSE,
PRIMARY KEY (game_id, snake_id)
);
CREATE TABLE IF NOT EXISTS snake_turns (
id BIGSERIAL PRIMARY KEY,
game_id TEXT NOT NULL REFERENCES games(game_id) ON DELETE CASCADE,
@@ -93,6 +107,11 @@ ALTER TABLE games ADD COLUMN IF NOT EXISTS game_type TEXT;
ALTER TABLE games ADD COLUMN IF NOT EXISTS your_snake_type TEXT;
ALTER TABLE games ADD COLUMN IF NOT EXISTS your_snake_version TEXT;
ALTER TABLE games ADD COLUMN IF NOT EXISTS winner_name TEXT;
ALTER TABLE games ADD COLUMN IF NOT EXISTS has_replay BOOLEAN NOT NULL DEFAULT TRUE;
ALTER TABLE games ADD COLUMN IF NOT EXISTS quality_status TEXT NOT NULL DEFAULT 'retained';
ALTER TABLE games ADD COLUMN IF NOT EXISTS quality_score INTEGER;
ALTER TABLE games ADD COLUMN IF NOT EXISTS quality_tier TEXT;
ALTER TABLE games ADD COLUMN IF NOT EXISTS quality_reasons JSONB;
ALTER TABLE turns ADD COLUMN IF NOT EXISTS my_thinking JSONB;
ALTER TABLE snake_turns ADD COLUMN IF NOT EXISTS latency TEXT;
"""
@@ -433,9 +452,23 @@ class PostgresqlGameplayBackend(GameplayBackendTemplate):
game_id = game.get("id")
turn = int(game_state.get("turn", 0))
board_json, snakes_json, you_json, food_json, hazards_json = compact_turn_json(board)
pool = await self._get_pool()
async with pool.acquire() as conn:
async with conn.transaction():
await conn.executemany("""
INSERT INTO game_snakes (game_id, snake_id, snake_name, is_you)
VALUES ($1,$2,$3,$4)
ON CONFLICT (game_id, snake_id) DO UPDATE SET
snake_name = EXCLUDED.snake_name,
is_you = EXCLUDED.is_you
""",
[
(game_id, snake.get("id"), snake.get("name"), snake.get("id") == you.get("id"))
for snake in snakes if snake.get("id") is not None
],
)
await conn.execute("""
INSERT INTO turns (
game_id, turn, observed_at, my_move, my_thinking,
@@ -456,11 +489,11 @@ class PostgresqlGameplayBackend(GameplayBackendTemplate):
self._utc_now_ts(),
my_move,
my_thinking,
board,
snakes,
you,
board.get("food", []),
board.get("hazards", []),
board_json,
snakes_json,
you_json,
food_json,
hazards_json,
)
previous_positions:dict[str, tuple[int, int]] = {}
@@ -498,8 +531,8 @@ class PostgresqlGameplayBackend(GameplayBackendTemplate):
inferred_move = EXCLUDED.inferred_move,
latency = EXCLUDED.latency
""",
p_game_id, p_turn, p_snake_id, p_name, p_health, p_length,
p_head_x, p_head_y, p_body, p_is_you, p_inferred, p_latency,
p_game_id, p_turn, p_snake_id, None, p_health, p_length,
p_head_x, p_head_y, p_body, False, p_inferred, p_latency,
)
await conn.execute("""
@@ -561,10 +594,12 @@ class PostgresqlGameplayBackend(GameplayBackendTemplate):
final_turn = int(row["final_turn"] or 0)
snake_rows = await conn.fetch("""
SELECT snake_id, snake_name
FROM snake_turns
WHERE game_id = $1 AND turn = $2
ORDER BY is_you DESC, snake_name ASC
SELECT st.snake_id, COALESCE(gs.snake_name, st.snake_name) AS snake_name
FROM snake_turns AS st
LEFT JOIN game_snakes AS gs
ON gs.game_id = st.game_id AND gs.snake_id = st.snake_id
WHERE st.game_id = $1 AND st.turn = $2
ORDER BY COALESCE(gs.is_you, st.is_you) DESC, snake_name ASC
""",
game_id, final_turn,
)
@@ -578,10 +613,12 @@ class PostgresqlGameplayBackend(GameplayBackendTemplate):
if latest_row is not None and latest_row["latest_turn"] is not None:
final_turn = int(latest_row["latest_turn"])
snake_rows = await conn.fetch("""
SELECT snake_id, snake_name
FROM snake_turns
WHERE game_id = $1 AND turn = $2
ORDER BY is_you DESC, snake_name ASC
SELECT st.snake_id, COALESCE(gs.snake_name, st.snake_name) AS snake_name
FROM snake_turns AS st
LEFT JOIN game_snakes AS gs
ON gs.game_id = st.game_id AND gs.snake_id = st.snake_id
WHERE st.game_id = $1 AND st.turn = $2
ORDER BY COALESCE(gs.is_you, st.is_you) DESC, snake_name ASC
""",
game_id, final_turn,
)
@@ -640,6 +677,7 @@ class PostgresqlGameplayBackend(GameplayBackendTemplate):
SELECT game_id, started_at, ended_at, map_name, ruleset_name, game_type,
your_snake_name, your_snake_type, your_snake_version, winner_you, final_turn, status
FROM games
WHERE has_replay
ORDER BY started_at DESC
LIMIT $1
""",
@@ -656,6 +694,7 @@ class PostgresqlGameplayBackend(GameplayBackendTemplate):
your_snake_name, your_snake_type, your_snake_version,
winner_you, winner_name, final_turn, status
FROM games
WHERE has_replay
ORDER BY started_at DESC
LIMIT $1
""",
@@ -673,7 +712,7 @@ class PostgresqlGameplayBackend(GameplayBackendTemplate):
your_snake_type, your_snake_version,
winner_name, winner_you, final_turn, status
FROM games
WHERE game_id = $1
WHERE game_id = $1 AND has_replay
""",
game_id,
)
@@ -696,11 +735,16 @@ class PostgresqlGameplayBackend(GameplayBackendTemplate):
)
snake_rows = await conn.fetch("""
SELECT turn, snake_id, snake_name, health, length, head_x, head_y,
body AS body_json, is_you, inferred_move, latency
FROM snake_turns
WHERE game_id = $1
ORDER BY turn ASC, is_you DESC, snake_name ASC
SELECT st.turn, st.snake_id,
COALESCE(gs.snake_name, st.snake_name) AS snake_name,
st.health, st.length, st.head_x, st.head_y, st.body AS body_json,
COALESCE(gs.is_you, st.is_you) AS is_you,
st.inferred_move, st.latency
FROM snake_turns AS st
LEFT JOIN game_snakes AS gs
ON gs.game_id = st.game_id AND gs.snake_id = st.snake_id
WHERE st.game_id = $1
ORDER BY st.turn ASC, is_you DESC, snake_name ASC
""",
game_id,
)