feat(database): store gameplay replays in compact form
Build and Push Docker Container / build-and-push (push) Successful in 3m52s
Build and Push Docker Container / build-and-push (push) Successful in 3m52s
- Add compact storage package: codec, records, hydrate, replay blobs, cache. - Running games write game_turns and snake_states keyed by integer ids. - Encode moves as 1-byte codes and bodies as head plus step codes. - Store food and hazards only when they changed since the previous turn. - Strip reasoning fields that duplicate stored values and restore on read. - Pack finished games into one zstd blob per game in game_replays. - Merge late turns into packed blobs during the stale-game sweep. - Extract SQLite and PostgreSQL schemas into dedicated modules. - Add read/import game bundle and list_game_ids to the backend template. - Add legacy readers and converter for the old turns/snake_turns layout. - Add migrate_gameplay_to_compact.py with in-place or cross-DB migration. - Verify every migrated game and keep legacy rows on failure. - Support --since, --minimum-quality and guarded --drop-legacy. - Add just recipe migrate-gameplay-postgres. - Rewrite merge script to copy game by game via backend bundles. - Move benchmark state loading into benchmark_states for both backends. - Update PostgreSQL cleanup script to the new replay tables. - Drop sqlite-zstd build stages from the Dockerfile. - Remove old migration scripts, normalized_turn and their tests. - Add tests for compact storage, schema, legacy migration and PostgreSQL. - Document the compact layout and migration workflow in the README.
This commit is contained in:
@@ -1,6 +1,21 @@
|
||||
import asyncio
|
||||
import unittest
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from server.database.benchmark_states import _build_state, is_postgresql_source
|
||||
from server.database.benchmark_states import is_postgresql_source, replay_turn_to_state, sample_states
|
||||
|
||||
GAME = {
|
||||
"game_id": "game-id", "source": "league", "map": "standard",
|
||||
"ruleset_name": "standard", "ruleset_version": "v1",
|
||||
}
|
||||
|
||||
def _turn(number:int) -> dict:
|
||||
you = {"id": "you-id", "head": {"x": 1, "y": 2}, "body": [{"x": 1, "y": 2}]}
|
||||
return {
|
||||
"turn": number,
|
||||
"board": {"width": 11, "height": 11, "food": [{"x": 5, "y": 5}], "hazards": [], "snakes": [you]},
|
||||
"you": you,
|
||||
}
|
||||
|
||||
class TestBenchmarkStates(unittest.TestCase):
|
||||
def test_detects_postgresql_dsn(self):
|
||||
@@ -8,23 +23,27 @@ class TestBenchmarkStates(unittest.TestCase):
|
||||
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
|
||||
def test_builds_state_from_replay_turn(self):
|
||||
board, metadata = replay_turn_to_state(GAME, _turn(7))
|
||||
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)
|
||||
self.assertEqual(metadata["ruleset"]["version"], "v1")
|
||||
|
||||
def test_turn_without_snakes_is_skipped(self):
|
||||
self.assertIsNone(replay_turn_to_state(GAME, {"turn": 0, "board": {"snakes": []}, "you": {}}))
|
||||
|
||||
def test_samples_every_stride_turn_from_newest_games(self):
|
||||
backend = AsyncMock()
|
||||
backend.list_game_ids.return_value = ["old", "new"]
|
||||
backend.get_game_replay.side_effect = lambda game_id: {
|
||||
"game": {**GAME, "game_id": game_id},
|
||||
"turns": [_turn(number) for number in range(4)],
|
||||
}
|
||||
|
||||
states = asyncio.run(sample_states(backend, samples=3, stride=3))
|
||||
|
||||
self.assertEqual([(m["game_id"], m["turn"]) for _, m in states], [("new", 3), ("new", 0), ("old", 1)])
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user