feat: add Prism snake and gameplay database lifecycle
Build and Push Docker Container / build-and-push (push) Successful in 8m3s
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:
@@ -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,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user