feat: add live replays and PostgreSQL maintenance tools
Build and Push Docker Container / build-and-push (push) Successful in 7m53s

- Stream compact live replay updates across local and clustered dashboards.
- Render responsive snake bodies as SVG paths with aligned custom icons.
- Add cache-busted assets, replay fallback routes, and live-follow playback.
- Support PostgreSQL benchmark sampling and idempotent SQLite migration.
- Add dry-run cleanup for old low-quality PostgreSQL replay payloads.
- Reward safe perimeter lanes and bump Prism to version 1.5.0.
- Add backend, migration, dashboard, and perimeter regression coverage.
This commit is contained in:
2026-08-02 00:50:46 +02:00
parent 9b99b526e4
commit f14d780f29
29 changed files with 1574 additions and 310 deletions
+51
View File
@@ -0,0 +1,51 @@
import unittest
from snakes.engine.perimeter import perimeter_geometry_score
class TestPerimeterGeometryScore(unittest.TestCase):
def score(self, point, **overrides):
values = {
"point": point,
"current_head": (0, 5),
"width": 11,
"height": 11,
"occupancy": 0.35,
"snake_length": 12,
"reachable_space": 80,
"required_space": 12,
"liberties": 2,
"next_options": 2,
"safe_next_options": 2,
"tail_escape": True,
"dead_end": False,
"losing_head_to_head": False,
}
values.update(overrides)
return perimeter_geometry_score(**values)
def test_safe_wall_lane_can_outscore_adjacent_inner_lane(self):
wall = self.score((0, 6))
inner = self.score((1, 5))
self.assertGreater(wall, inner)
def test_unsafe_wall_does_not_receive_perimeter_reward(self):
safe_wall = self.score((0, 6))
unsafe_wall = self.score((0, 6), safe_next_options=1)
self.assertGreater(safe_wall, unsafe_wall + 30.0)
def test_corner_is_less_attractive_than_straight_edge(self):
corner = self.score((0, 0), current_head=(0, 1))
straight_edge = self.score((0, 6))
self.assertGreater(straight_edge, corner)
def test_losing_head_to_head_never_gets_perimeter_reward(self):
safe_wall = self.score((0, 6))
contested_wall = self.score((0, 6), losing_head_to_head=True)
self.assertGreater(safe_wall, contested_wall + 30.0)
if __name__ == "__main__":
unittest.main()
@@ -52,8 +52,8 @@ class TestPrismBattleSnake_GPT_5_6_Sol(unittest.TestCase):
snake = PrismBattleSnake_GPT_5_6_Sol()
self.assertEqual(snake.name, "PrismBattleSnake")
self.assertEqual(snake.version, "1.4.0")
self.assertEqual(get_snake_version("PrismBattleSnake_GPT_5_6_Sol"), "1.4.0")
self.assertEqual(snake.version, "1.5.0")
self.assertEqual(get_snake_version("PrismBattleSnake_GPT_5_6_Sol"), "1.5.0")
self.assertGreaterEqual(snake._planning_depth, 4)
self.assertIsInstance(SnakeBuilder.build("PrismBattleSnake_GPT_5_6_Sol"), PrismBattleSnake_GPT_5_6_Sol)
+30
View File
@@ -0,0 +1,30 @@
import unittest
from server.database.benchmark_states import _build_state, is_postgresql_source
class TestBenchmarkStates(unittest.TestCase):
def test_detects_postgresql_dsn(self):
self.assertTrue(is_postgresql_source("postgresql://user:pass@example.com/db"))
self.assertTrue(is_postgresql_source("postgres://user:pass@example.com/db"))
self.assertFalse(is_postgresql_source("/tmp/gameplay.sqlite3"))
def test_builds_normalized_state_from_postgresql_json_values(self):
row = (
1, {}, {}, [{"x": 5, "y": 5}], [], "you-id", "You", 11, 11,
"game-id", "league", "standard", "standard", "v1", 7,
)
snake_rows = [
("you-id", "You", 90, 3, 1, 2, [{"x": 1, "y": 2}], {"head": "default"}),
]
state = _build_state(row, snake_rows)
self.assertIsNotNone(state)
board, metadata = state
self.assertEqual(board["food"], [{"x": 5, "y": 5}])
self.assertEqual(board["snakes"][0]["id"], "you-id")
self.assertEqual(metadata["you"]["id"], "you-id")
self.assertEqual(metadata["turn"], 7)
if __name__ == "__main__":
unittest.main()
+59
View File
@@ -0,0 +1,59 @@
import unittest
from unittest.mock import AsyncMock
from server.services.dashboard_query import DashboardQueryService
class _Logger:
def warning(self, message):
return message
class TestDashboardQueryService(unittest.IsolatedAsyncioTestCase):
def setUp(self):
self.database = AsyncMock()
self.hub = AsyncMock()
self.service = DashboardQueryService(
gameplay_database=self.database,
ws_hub=self.hub,
logger=_Logger(),
dashboard_running_game_stale_sec=600,
)
async def test_pushes_compact_latest_turn_event(self):
self.database.get_game_replay.return_value = {
"game": {"game_id": "game-1", "status": "running"},
"turns": [{"turn": 1}, {"turn": 2, "my_move": "up"}],
}
await self.service.push_dashboard_game_replay_update("game-1")
payload = self.hub.broadcast_payload.await_args.args[0]
self.assertEqual(payload["type"], "dashboard_game_replay_update")
self.assertEqual(payload["game_id"], "game-1")
self.assertEqual(payload["turn"], {"turn": 2, "my_move": "up"})
self.assertNotIn("turns", payload)
async def test_publishes_game_id_for_cluster_live_updates(self):
self.database.get_game_replay.return_value = {
"game": {"game_id": "game-1", "status": "running"},
"turns": [{"turn": 3}],
}
publish = AsyncMock()
self.service.set_publish_notice(publish)
await self.service.push_dashboard_game_replay_update("game-1")
publish.assert_awaited_once_with("game_turn", "game-1")
async def test_remote_turn_notice_loads_and_broadcasts_turn(self):
self.database.get_game_replay.return_value = {
"game": {"game_id": "game-1", "status": "running"},
"turns": [{"turn": 4}],
}
await self.service.on_dashboard_games_update_notice("game_turn", "game-1")
payload = self.hub.broadcast_payload.await_args.args[0]
self.assertEqual(payload["turn"]["turn"], 4)
if __name__ == "__main__":
unittest.main()
+49
View File
@@ -0,0 +1,49 @@
import os
import unittest
from unittest.mock import AsyncMock
from server.Server import Server
REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
REPLAY = {
'game': {'game_id': 'game-1', 'width': 11, 'height': 11},
'turns': [{'turn': 0, 'snakes': []}],
}
class TestDashboardReplayRoute(unittest.IsolatedAsyncioTestCase):
"""The dashboard falls back to this route when the replay websocket is down."""
def setUp(self):
self.server = Server(
data_path=REPO_ROOT,
snake_type='PrismBattleSnake',
storage_type='memory',
metrics_backend='memory',
gameplay_db_enabled=False,
)
self.database = AsyncMock()
self.server.dashboard_query.gameplay_database = self.database
async def test_serves_replay_payload(self):
self.database.get_game_replay.return_value = dict(REPLAY)
response = await self.server.app.test_client().get('/dashboard/game/game-1')
self.assertEqual(response.status_code, 200)
payload = await response.get_json()
self.assertEqual(payload['game']['game_id'], 'game-1')
self.assertEqual(len(payload['turns']), 1)
self.database.get_game_replay.assert_awaited_once_with('game-1')
async def test_unknown_game_returns_404(self):
self.database.get_game_replay.return_value = None
response = await self.server.app.test_client().get('/dashboard/game/nope')
self.assertEqual(response.status_code, 404)
payload = await response.get_json()
self.assertEqual(payload['error'], 'game_not_found')
if __name__ == '__main__':
unittest.main()
+51
View File
@@ -0,0 +1,51 @@
import os
import re
import unittest
from server.Server import Server
REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
class TestDashboardStaticAssets(unittest.IsolatedAsyncioTestCase):
def setUp(self):
self.server = Server(
data_path=REPO_ROOT,
snake_type='PrismBattleSnake',
storage_type='memory',
metrics_backend='memory',
gameplay_db_enabled=False,
)
def _static_url(self):
return self.server.app.jinja_env.globals['static_url']
async def test_static_url_carries_file_mtime(self):
asset = 'js/GameBoard.js'
expected_version = int(os.path.getmtime(
os.path.join(self.server.app.static_folder, asset)
))
async with self.server.app.test_request_context('/dashboard'):
url = self._static_url()(asset)
self.assertEqual(url, f'/files/{asset}?v={expected_version}')
async def test_static_url_tolerates_missing_asset(self):
async with self.server.app.test_request_context('/dashboard'):
url = self._static_url()('js/DoesNotExist.js')
self.assertEqual(url, '/files/js/DoesNotExist.js?v=0')
async def test_dashboard_page_versions_every_static_asset(self):
client = self.server.app.test_client()
response = await client.get('/dashboard')
self.assertEqual(response.status_code, 200)
body = await response.get_data(as_text=True)
references = re.findall(r'(?:href|src)="(/files/[^"]+)"', body)
self.assertTrue(references, 'dashboard page referenced no static assets')
for reference in references:
self.assertRegex(reference, r'\?v=\d+$', f'unversioned static asset: {reference}')
if __name__ == '__main__':
unittest.main()
+34
View File
@@ -0,0 +1,34 @@
import unittest
from scripts.migrate_sqlite_to_postgresql import parse_json, parse_timestamp, transform_row
class TestMigrateSqliteToPostgresql(unittest.TestCase):
def test_transform_row_converts_timestamp_boolean_and_json(self):
columns = ("started_at", "winner_you", "quality_reasons", "game_id")
row = ("2026-08-01T10:20:30+00:00", 1, '["complete"]', "game-1")
transformed = transform_row(columns, row)
self.assertEqual(transformed[0].isoformat(), "2026-08-01T10:20:30+00:00")
self.assertIs(transformed[1], True)
self.assertEqual(transformed[2], '["complete"]')
self.assertEqual(transformed[3], "game-1")
def test_json_defaults_match_not_null_postgresql_columns(self):
columns = ("customizations", "board_state", "snakes", "you", "food", "hazards", "body")
self.assertEqual(
transform_row(columns, (None,) * len(columns)),
("{}", "{}", "[]", "{}", "[]", "[]", "[]"),
)
def test_invalid_json_aborts_instead_of_silently_losing_data(self):
with self.assertRaisesRegex(ValueError, "Invalid JSON"):
parse_json("{invalid")
def test_z_suffix_timestamp_is_timezone_aware(self):
parsed = parse_timestamp("2026-08-01T10:20:30Z")
self.assertIsNotNone(parsed.tzinfo)
self.assertEqual(parsed.utcoffset().total_seconds(), 0)
if __name__ == "__main__":
unittest.main()
+65
View File
@@ -0,0 +1,65 @@
import sqlite3
import tempfile
import unittest
from pathlib import Path
from server.database.backend.PostgresqlGameplayBackend import PostgresqlGameplayBackend
class TestPostgresqlGameplayMigration(unittest.TestCase):
def _create_source(self, path:Path, winner_column:str, winner_value:str|None) -> None:
with sqlite3.connect(path) as connection:
connection.executescript(f"""
CREATE TABLE games (
game_id TEXT PRIMARY KEY, started_at TEXT NOT NULL, ended_at TEXT,
width INTEGER, height INTEGER, source TEXT, map_name TEXT,
ruleset_name TEXT, ruleset_version TEXT, your_snake_id TEXT,
your_snake_name TEXT, your_snake_type TEXT, your_snake_version TEXT,
game_type TEXT, {winner_column} TEXT, winner_you INTEGER,
final_turn INTEGER, status TEXT
);
CREATE TABLE turns (
game_id TEXT, turn INTEGER, observed_at TEXT, my_move TEXT,
my_thinking_json TEXT, board_state_json TEXT, snakes_json TEXT,
you_json TEXT, food_json TEXT, hazards_json TEXT
);
CREATE TABLE snake_turns (
game_id TEXT, turn INTEGER, snake_id TEXT, snake_name TEXT,
health INTEGER, length INTEGER, head_x INTEGER, head_y INTEGER,
body_json TEXT, is_you INTEGER, inferred_move TEXT, latency TEXT
);
""")
connection.execute(
f"""INSERT INTO games (
game_id, started_at, {winner_column}, winner_you, final_turn, status
) VALUES (?, ?, ?, ?, ?, ?)""",
("game-1", "2026-08-01T10:00:00+00:00", winner_value, 1, 4, "finished"),
)
def test_reads_current_winner_name_schema(self):
with tempfile.TemporaryDirectory() as temp_dir:
path = Path(temp_dir) / "gameplay.sqlite3"
self._create_source(path, "winner_name", "Prism")
backend = PostgresqlGameplayBackend("postgresql://example", sqlite_migration_path=str(path))
games, turns, snake_turns = backend._read_sqlite_data_sync(str(path))
self.assertEqual(games[0]["winner_name"], "Prism")
self.assertEqual(turns, [])
self.assertEqual(snake_turns, [])
self.assertEqual(backend._migrated_winner_name(games[0]["winner_name"]), "Prism")
def test_reads_legacy_winner_names_json_schema(self):
with tempfile.TemporaryDirectory() as temp_dir:
path = Path(temp_dir) / "gameplay.sqlite3"
self._create_source(path, "winner_names_json", '["Prism"]')
backend = PostgresqlGameplayBackend("postgresql://example", sqlite_migration_path=str(path))
games, _, _ = backend._read_sqlite_data_sync(str(path))
self.assertEqual(
backend._migrated_winner_name(games[0]["winner_name"]),
"Prism",
)
if __name__ == "__main__":
unittest.main()