fix: preserve gameplay data and correct duel evaluation
- Preserve snake customizations across database migrations and merges. - Lazily load optional storage backends for SQLite maintenance scripts. - Match Apex territory and nearest-food tie-breaking semantics. - Resolve duel occupancy after simultaneous movement and food growth. - Recompute simulated head-to-head danger after body growth. - Add regression coverage and declare the aiofiles dependency.
This commit is contained in:
@@ -21,20 +21,38 @@ class TestBitBoard(unittest.TestCase):
|
||||
|
||||
self.assertEqual(board.territory(board.idx(0, 0), [board.idx(4, 0)], 0), 0)
|
||||
|
||||
def test_territory_propagates_through_contested_cells(self):
|
||||
board = BitBoard(5, 3)
|
||||
blocked = board.set_to_bits({(0, 1), (1, 1), (3, 1), (4, 1)})
|
||||
|
||||
self.assertEqual(board.territory(board.idx(0, 0), [board.idx(4, 0)], blocked), 0)
|
||||
|
||||
def test_territory_ignores_enemy_only_disconnected_space_like_apex(self):
|
||||
board = BitBoard(5, 1)
|
||||
blocked = board.set_to_bits({(2, 0)})
|
||||
|
||||
self.assertEqual(board.territory(board.idx(0, 0), [board.idx(4, 0)], blocked), 2)
|
||||
|
||||
def test_nearest_food_returns_shortest_distance(self):
|
||||
board = BitBoard(5, 5)
|
||||
food = board.set_to_bits({(4, 4), (2, 1)})
|
||||
|
||||
self.assertEqual(board.nearest_food(board.idx(0, 0), food, 0), (3, board.idx(2, 1)))
|
||||
|
||||
def test_nearest_food_uses_apex_direction_order_for_ties(self):
|
||||
board = BitBoard(3, 3)
|
||||
food = board.set_to_bits({(1, 2), (0, 1), (2, 1), (1, 0)})
|
||||
|
||||
self.assertEqual(board.nearest_food(board.idx(1, 1), food, 0), (1, board.idx(1, 2)))
|
||||
|
||||
class TestPrismBattleSnake_GPT_5_6_Sol(unittest.TestCase):
|
||||
|
||||
def test_api_name_and_version_are_exposed(self):
|
||||
snake = PrismBattleSnake_GPT_5_6_Sol()
|
||||
|
||||
self.assertEqual(snake.name, "PrismBattleSnake")
|
||||
self.assertEqual(snake.version, "1.0.0")
|
||||
self.assertEqual(get_snake_version("PrismBattleSnake_GPT_5_6_Sol"), "1.0.0")
|
||||
self.assertEqual(snake.version, "1.0.1")
|
||||
self.assertEqual(get_snake_version("PrismBattleSnake_GPT_5_6_Sol"), "1.0.1")
|
||||
self.assertIsInstance(SnakeBuilder.build("PrismBattleSnake_GPT_5_6_Sol"), PrismBattleSnake_GPT_5_6_Sol)
|
||||
|
||||
def test_bitboard_primitives_match_apex(self):
|
||||
@@ -82,6 +100,18 @@ class TestPrismBattleSnake_GPT_5_6_Sol(unittest.TestCase):
|
||||
|
||||
self.assertGreater(value, 0)
|
||||
|
||||
def test_duel_search_keeps_tail_blocked_when_its_snake_eats(self):
|
||||
board = BitBoard(3, 3)
|
||||
search = BitboardDuelSearch(
|
||||
board=board, food={(0, 1)}, hazards=set(), hazard_count={},
|
||||
hazard_damage=15, deadline=None,
|
||||
)
|
||||
body = (board.idx(0, 0), board.idx(1, 0), board.idx(1, 1))
|
||||
|
||||
advanced = search._advance_body(body, board.idx(0, 1), ate=True)
|
||||
|
||||
self.assertIn(board.idx(1, 1), advanced)
|
||||
|
||||
def test_bitboard_duel_search_reuses_transpositions(self):
|
||||
board = BitBoard(5, 5)
|
||||
search = BitboardDuelSearch(
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import subprocess
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
class TestDatabasePackageImports(unittest.TestCase):
|
||||
def test_sqlite_backend_import_does_not_require_aiofiles(self):
|
||||
project_root = Path(__file__).resolve().parents[1]
|
||||
script = """
|
||||
import builtins
|
||||
|
||||
original_import = builtins.__import__
|
||||
def reject_aiofiles(name, *args, **kwargs):
|
||||
if name == 'aiofiles' or name.startswith('aiofiles.'):
|
||||
raise ModuleNotFoundError("aiofiles intentionally unavailable")
|
||||
return original_import(name, *args, **kwargs)
|
||||
|
||||
builtins.__import__ = reject_aiofiles
|
||||
from server.database.backend.SqliteGameplayBackend import SqliteGameplayBackend
|
||||
assert SqliteGameplayBackend.__name__ == 'SqliteGameplayBackend'
|
||||
"""
|
||||
result = subprocess.run(
|
||||
[sys.executable, "-c", script],
|
||||
cwd=project_root,
|
||||
text=True,
|
||||
capture_output=True,
|
||||
)
|
||||
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -27,10 +27,14 @@ class TestMergeGameplayDatabases(unittest.TestCase):
|
||||
90 if cleaned else None, "high" if cleaned else None,
|
||||
'["already_scored"]' if cleaned else None,
|
||||
))
|
||||
connection.execute(
|
||||
"INSERT INTO game_snakes (game_id,snake_id,snake_name,is_you) VALUES (?,?,?,?)",
|
||||
(game_id, "me", "PrismBattleSnake", 1),
|
||||
)
|
||||
connection.execute("""
|
||||
INSERT INTO game_snakes (
|
||||
game_id,snake_id,snake_name,is_you,customizations_json
|
||||
) VALUES (?,?,?,?,?)
|
||||
""", (
|
||||
game_id, "me", "PrismBattleSnake", 1,
|
||||
'{"color":"#663399","head":"ferret","tail":"swirl"}',
|
||||
))
|
||||
connection.execute("""
|
||||
INSERT INTO turns (
|
||||
game_id,turn,observed_at,my_move,my_thinking_json,
|
||||
@@ -69,6 +73,13 @@ class TestMergeGameplayDatabases(unittest.TestCase):
|
||||
])
|
||||
self.assertEqual(connection.execute("SELECT COUNT(*) FROM turns").fetchone()[0], 2)
|
||||
self.assertEqual(connection.execute("SELECT COUNT(*) FROM snake_turns").fetchone()[0], 2)
|
||||
customizations = connection.execute("""
|
||||
SELECT game_id, customizations_json FROM game_snakes ORDER BY game_id
|
||||
""").fetchall()
|
||||
self.assertEqual(customizations, [
|
||||
("base-game", '{"color":"#663399","head":"ferret","tail":"swirl"}'),
|
||||
("delta-game", '{"color":"#663399","head":"ferret","tail":"swirl"}'),
|
||||
])
|
||||
self.assertEqual(connection.execute("PRAGMA foreign_key_check").fetchall(), [])
|
||||
|
||||
def test_conflicting_duplicate_aborts_without_destination(self):
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
import sqlite3
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from scripts.migrate_gameplay_database import copy_game_snakes
|
||||
from server.database.backend.SqliteGameplayBackend import SqliteGameplayBackend
|
||||
|
||||
class TestMigrateGameplayDatabase(unittest.TestCase):
|
||||
def test_copy_game_snakes_preserves_customizations(self):
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
root = Path(temp_dir)
|
||||
source_path = root / "source.sqlite3"
|
||||
destination_path = root / "destination.sqlite3"
|
||||
SqliteGameplayBackend(str(source_path))
|
||||
SqliteGameplayBackend(str(destination_path))
|
||||
|
||||
with sqlite3.connect(source_path) as source:
|
||||
source.execute("PRAGMA foreign_keys = OFF")
|
||||
source.execute("""
|
||||
INSERT INTO game_snakes (
|
||||
game_id, snake_id, snake_name, is_you, customizations_json
|
||||
) VALUES (?, ?, ?, ?, ?)
|
||||
""", (
|
||||
"game-1", "snake-1", "PrismBattleSnake", 1,
|
||||
'{"color":"#663399","head":"ferret","tail":"swirl"}',
|
||||
))
|
||||
|
||||
source = sqlite3.connect(source_path)
|
||||
destination = sqlite3.connect(destination_path)
|
||||
try:
|
||||
copied = copy_game_snakes(
|
||||
source, destination, batch_size=10, retained_ids={"game-1"},
|
||||
)
|
||||
destination.commit()
|
||||
row = destination.execute("""
|
||||
SELECT snake_name, is_you, customizations_json
|
||||
FROM game_snakes WHERE game_id = ? AND snake_id = ?
|
||||
""", ("game-1", "snake-1")).fetchone()
|
||||
finally:
|
||||
source.close()
|
||||
destination.close()
|
||||
|
||||
self.assertEqual(copied, 1)
|
||||
self.assertEqual(row, (
|
||||
"PrismBattleSnake", 1,
|
||||
'{"color":"#663399","head":"ferret","tail":"swirl"}',
|
||||
))
|
||||
|
||||
def test_copy_game_snakes_defaults_legacy_schema_to_empty_customizations(self):
|
||||
source = sqlite3.connect(":memory:")
|
||||
destination = sqlite3.connect(":memory:")
|
||||
try:
|
||||
source.execute("""
|
||||
CREATE TABLE game_snakes (
|
||||
game_id TEXT, snake_id TEXT, snake_name TEXT, is_you INTEGER
|
||||
)
|
||||
""")
|
||||
source.execute(
|
||||
"INSERT INTO game_snakes VALUES (?, ?, ?, ?)",
|
||||
("game-1", "snake-1", "LegacySnake", 0),
|
||||
)
|
||||
destination.execute("""
|
||||
CREATE TABLE game_snakes (
|
||||
game_id TEXT, snake_id TEXT, snake_name TEXT, is_you INTEGER,
|
||||
customizations_json TEXT NOT NULL DEFAULT '{}'
|
||||
)
|
||||
""")
|
||||
|
||||
copied = copy_game_snakes(
|
||||
source, destination, batch_size=10, retained_ids={"game-1"},
|
||||
)
|
||||
row = destination.execute(
|
||||
"SELECT customizations_json FROM game_snakes"
|
||||
).fetchone()
|
||||
finally:
|
||||
source.close()
|
||||
destination.close()
|
||||
|
||||
self.assertEqual(copied, 1)
|
||||
self.assertEqual(row, ("{}",))
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user