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:
@@ -0,0 +1,68 @@
|
||||
import unittest
|
||||
|
||||
from snakes import SnakeBuilder, get_snake_version
|
||||
from snakes.ApexBattleSnake import ApexBattleSnake
|
||||
from snakes.PrismBattleSnake_GPT_5_6_Sol import PrismBattleSnake_GPT_5_6_Sol
|
||||
from snakes.bitboard import BitBoard
|
||||
|
||||
class TestBitBoard(unittest.TestCase):
|
||||
|
||||
def test_flood_fill_respects_walls(self):
|
||||
board = BitBoard(3, 3)
|
||||
blocked = board.set_to_bits({(1, 0), (1, 1), (1, 2)})
|
||||
|
||||
self.assertEqual(board.flood_count(board.idx(0, 1), blocked), 3)
|
||||
self.assertEqual(board.path_distance(board.idx(0, 1), board.idx(2, 1), blocked), None)
|
||||
|
||||
def test_territory_counts_ties_for_neither_side(self):
|
||||
board = BitBoard(5, 1)
|
||||
|
||||
self.assertEqual(board.territory(board.idx(0, 0), [board.idx(4, 0)], 0), 0)
|
||||
|
||||
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)))
|
||||
|
||||
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.assertIsInstance(SnakeBuilder.build("PrismBattleSnake_GPT_5_6_Sol"), PrismBattleSnake_GPT_5_6_Sol)
|
||||
|
||||
def test_bitboard_primitives_match_apex(self):
|
||||
apex = ApexBattleSnake()
|
||||
prism = PrismBattleSnake_GPT_5_6_Sol()
|
||||
blocked = {(1, 0), (1, 1), (3, 2), (3, 3)}
|
||||
|
||||
self.assertEqual(
|
||||
prism._flood_fill_count((0, 0), blocked, 5, 5),
|
||||
apex._flood_fill_count((0, 0), blocked, 5, 5),
|
||||
)
|
||||
self.assertEqual(
|
||||
prism._distance_map((0, 0), blocked, 5, 5),
|
||||
apex._distance_map((0, 0), blocked, 5, 5),
|
||||
)
|
||||
self.assertEqual(
|
||||
prism._path_distance((0, 0), (4, 4), blocked, 5, 5),
|
||||
apex._path_distance((0, 0), (4, 4), blocked, 5, 5),
|
||||
)
|
||||
|
||||
def test_mutated_blocked_set_does_not_return_stale_result(self):
|
||||
snake = PrismBattleSnake_GPT_5_6_Sol()
|
||||
blocked: set[tuple[int, int]] = set()
|
||||
|
||||
open_count = snake._flood_fill_count((1, 1), blocked, 3, 3)
|
||||
blocked.update({(0, 1), (1, 0), (2, 1), (1, 2)})
|
||||
trapped_count = snake._flood_fill_count((1, 1), blocked, 3, 3)
|
||||
|
||||
self.assertEqual(open_count, 9)
|
||||
self.assertEqual(trapped_count, 1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,371 @@
|
||||
"""Tests for SupremeBattleSnake.
|
||||
|
||||
Validates that the bitboard-accelerated snake produces correct results
|
||||
and that the bitboard engine itself is sound.
|
||||
"""
|
||||
import unittest
|
||||
|
||||
from snakes.SupremeBattleSnake_ClaudeOpus4_6 import SupremeBattleSnake_ClaudeOpus4_6 as SupremeBattleSnake
|
||||
from snakes.bitboard import BitBoard
|
||||
from server.GameBoard import GameBoard
|
||||
|
||||
# ── Helpers ───────────────────────────────────────────────────────────────────
|
||||
|
||||
def make_board(game_state: dict) -> GameBoard:
|
||||
snake = SupremeBattleSnake()
|
||||
board = GameBoard(
|
||||
game_id=game_state["game"]["id"],
|
||||
width=game_state["board"]["width"],
|
||||
height=game_state["board"]["height"],
|
||||
ruleset=game_state["game"]["ruleset"],
|
||||
source=game_state["game"].get("source", "custom"),
|
||||
map=game_state["game"].get("map", "standard"),
|
||||
snake_class=snake,
|
||||
)
|
||||
board.read_game_data(game_state)
|
||||
return board
|
||||
|
||||
def move(game_state: dict) -> str:
|
||||
return make_board(game_state).snake_neat_make_a_move()
|
||||
|
||||
def gs(
|
||||
my_body: list[tuple],
|
||||
other_bodies: list[list[tuple]] | None = None,
|
||||
foods: list[tuple] | None = None,
|
||||
hazards: list[tuple] | None = None,
|
||||
my_health: int = 90,
|
||||
my_id: str = "me",
|
||||
enemy_health: int = 90,
|
||||
game_type: str = "standard",
|
||||
game_map: str = "standard",
|
||||
hazard_damage: int = 14,
|
||||
width: int = 11,
|
||||
height: int = 11,
|
||||
turn: int = 20,
|
||||
game_id: str = "test-game",
|
||||
) -> dict:
|
||||
other_bodies = other_bodies or []
|
||||
foods = foods or []
|
||||
hazards = hazards or []
|
||||
|
||||
def body_dicts(coords):
|
||||
return [{"x": x, "y": y} for x, y in coords]
|
||||
|
||||
my_snake = {
|
||||
"id": my_id, "name": "SupremeBattleSnake", "health": my_health,
|
||||
"body": body_dicts(my_body),
|
||||
"head": {"x": my_body[0][0], "y": my_body[0][1]},
|
||||
"length": len(my_body),
|
||||
"latency": "50", "shout": "",
|
||||
}
|
||||
|
||||
snakes = [my_snake]
|
||||
for i, body in enumerate(other_bodies):
|
||||
snakes.append({
|
||||
"id": f"enemy-{i}", "name": f"Enemy{i}", "health": enemy_health,
|
||||
"body": body_dicts(body),
|
||||
"head": {"x": body[0][0], "y": body[0][1]},
|
||||
"length": len(body),
|
||||
"latency": "60", "shout": "",
|
||||
})
|
||||
|
||||
ruleset = {
|
||||
"name": game_type, "version": "v1.0.0",
|
||||
"settings": {"hazardDamagePerTurn": hazard_damage},
|
||||
}
|
||||
|
||||
return {
|
||||
"game": {"id": game_id, "ruleset": ruleset, "source": "custom", "map": game_map},
|
||||
"turn": turn,
|
||||
"board": {
|
||||
"height": height, "width": width,
|
||||
"food": body_dicts(foods),
|
||||
"hazards": body_dicts(hazards),
|
||||
"snakes": snakes,
|
||||
},
|
||||
"you": my_snake,
|
||||
}
|
||||
|
||||
# ── BitBoard unit tests ──────────────────────────────────────────────────────
|
||||
|
||||
class TestBitBoard(unittest.TestCase):
|
||||
|
||||
def test_flood_fill_open_board(self):
|
||||
bb = BitBoard(5, 5)
|
||||
count = bb.flood_count(bb.idx(2, 2), 0)
|
||||
self.assertEqual(count, 25)
|
||||
|
||||
def test_flood_fill_blocked_center(self):
|
||||
bb = BitBoard(5, 5)
|
||||
# Block all 4 neighbours of (2,2)
|
||||
blocked = (
|
||||
bb.pt_bit(1, 2) | bb.pt_bit(3, 2)
|
||||
| bb.pt_bit(2, 1) | bb.pt_bit(2, 3)
|
||||
)
|
||||
count = bb.flood_count(bb.idx(2, 2), blocked)
|
||||
self.assertEqual(count, 1) # only the start cell
|
||||
|
||||
def test_flood_fill_row_wall(self):
|
||||
bb = BitBoard(5, 5)
|
||||
# Block entire row y=2, except (2,2) itself
|
||||
blocked = 0
|
||||
for x in range(5):
|
||||
if x != 2:
|
||||
blocked |= bb.pt_bit(x, 2)
|
||||
# Start at (2,3) — should reach everything above the wall
|
||||
count_above = bb.flood_count(bb.idx(2, 3), blocked)
|
||||
self.assertGreater(count_above, 1)
|
||||
self.assertLess(count_above, 25)
|
||||
|
||||
def test_territory_center_vs_corner(self):
|
||||
bb = BitBoard(11, 11)
|
||||
score = bb.territory(bb.idx(5, 5), [bb.idx(0, 0)], 0)
|
||||
self.assertGreater(score, 0)
|
||||
|
||||
def test_territory_symmetric(self):
|
||||
bb = BitBoard(11, 11)
|
||||
score = bb.territory(bb.idx(0, 0), [bb.idx(10, 10)], 0)
|
||||
self.assertEqual(score, 0) # symmetric → tied
|
||||
|
||||
def test_partition_sizes_no_cut(self):
|
||||
bb = BitBoard(5, 5)
|
||||
sizes = bb.partition_sizes(bb.idx(2, 2), 0)
|
||||
# Open board — removing center doesn't split it (all neighbours connected)
|
||||
self.assertEqual(sizes, [])
|
||||
|
||||
def test_partition_sizes_bridge(self):
|
||||
bb = BitBoard(3, 3)
|
||||
# Block corners so (1,1) becomes a bridge:
|
||||
# . X .
|
||||
# X . X
|
||||
# . X .
|
||||
blocked = (
|
||||
bb.pt_bit(0, 0) | bb.pt_bit(2, 0)
|
||||
| bb.pt_bit(0, 2) | bb.pt_bit(2, 2)
|
||||
)
|
||||
sizes = bb.partition_sizes(bb.idx(1, 1), blocked)
|
||||
# Removing (1,1) from the cross → 4 isolated cells
|
||||
self.assertEqual(len(sizes), 4)
|
||||
self.assertTrue(all(s == 1 for s in sizes))
|
||||
|
||||
def test_distance_map_correctness(self):
|
||||
bb = BitBoard(5, 5)
|
||||
dmap = bb.distance_map(bb.idx(0, 0), 0)
|
||||
self.assertEqual(dmap[bb.idx(0, 0)], 0)
|
||||
self.assertEqual(dmap[bb.idx(1, 0)], 1)
|
||||
self.assertEqual(dmap[bb.idx(4, 4)], 8)
|
||||
|
||||
def test_path_distance_blocked(self):
|
||||
bb = BitBoard(5, 5)
|
||||
# Block a wall separating left from right
|
||||
blocked = 0
|
||||
for y in range(5):
|
||||
blocked |= bb.pt_bit(2, y)
|
||||
result = bb.path_distance(bb.idx(0, 0), bb.idx(4, 4), blocked)
|
||||
self.assertIsNone(result)
|
||||
|
||||
def test_path_distance_unblocked(self):
|
||||
bb = BitBoard(5, 5)
|
||||
result = bb.path_distance(bb.idx(0, 0), bb.idx(4, 4), 0)
|
||||
self.assertEqual(result, 8)
|
||||
|
||||
def test_nearest_food_finds_closest(self):
|
||||
bb = BitBoard(11, 11)
|
||||
food = bb.pt_bit(5, 6) | bb.pt_bit(0, 0)
|
||||
dist, idx = bb.nearest_food(bb.idx(5, 5), food, 0)
|
||||
self.assertEqual(dist, 1)
|
||||
self.assertEqual(idx, bb.idx(5, 6))
|
||||
|
||||
def test_nearest_food_none(self):
|
||||
bb = BitBoard(5, 5)
|
||||
dist, idx = bb.nearest_food(bb.idx(2, 2), 0, 0)
|
||||
self.assertIsNone(dist)
|
||||
|
||||
def test_open_neighbor_count_center(self):
|
||||
bb = BitBoard(5, 5)
|
||||
self.assertEqual(bb.open_neighbor_count(bb.idx(2, 2), 0), 4)
|
||||
|
||||
def test_open_neighbor_count_corner(self):
|
||||
bb = BitBoard(5, 5)
|
||||
self.assertEqual(bb.open_neighbor_count(bb.idx(0, 0), 0), 2)
|
||||
|
||||
def test_set_to_bits_roundtrip(self):
|
||||
bb = BitBoard(11, 11)
|
||||
pts = {(3, 7), (0, 0), (10, 10), (5, 5)}
|
||||
bits = bb.set_to_bits(pts)
|
||||
for x, y in pts:
|
||||
self.assertTrue(bits & bb.pt_bit(x, y))
|
||||
self.assertEqual(bits.bit_count(), len(pts))
|
||||
|
||||
def test_no_row_wraparound(self):
|
||||
"""Right-column expansion must not wrap to the next row's left column."""
|
||||
bb = BitBoard(5, 5)
|
||||
start = bb.idx(4, 0) # rightmost column, bottom row
|
||||
# Block everything except start and (0,1) — if wrapping happened, (0,1) would be adjacent
|
||||
blocked = bb.board_mask & ~(1 << start) & ~bb.pt_bit(0, 1)
|
||||
reachable = bb.flood_fill(start, blocked)
|
||||
self.assertEqual(reachable.bit_count(), 1) # only start itself
|
||||
|
||||
# ── Snake safety tests ────────────────────────────────────────────────────────
|
||||
|
||||
class TestSupremeWallAndBodyAvoidance(unittest.TestCase):
|
||||
|
||||
def test_avoids_left_wall(self):
|
||||
result = move(gs(my_body=[(0, 5), (1, 5), (2, 5)],
|
||||
other_bodies=[[(9, 9), (9, 8), (9, 7)]]))
|
||||
self.assertNotEqual(result, "left")
|
||||
|
||||
def test_avoids_bottom_wall(self):
|
||||
result = move(gs(my_body=[(5, 0), (5, 1), (5, 2)],
|
||||
other_bodies=[[(9, 9), (9, 8), (9, 7)]]))
|
||||
self.assertNotEqual(result, "down")
|
||||
|
||||
def test_avoids_own_body(self):
|
||||
result = move(gs(my_body=[(5, 5), (6, 5), (7, 5), (8, 5)],
|
||||
other_bodies=[[(1, 1), (1, 2), (1, 3)]],
|
||||
foods=[(5, 9)]))
|
||||
self.assertNotEqual(result, "right")
|
||||
|
||||
def test_avoids_enemy_body(self):
|
||||
result = move(gs(my_body=[(5, 5), (5, 4), (5, 3)],
|
||||
other_bodies=[[(6, 5), (7, 5), (8, 5), (9, 5), (9, 6), (9, 7)]]))
|
||||
self.assertNotEqual(result, "right")
|
||||
|
||||
def test_only_one_safe_move_taken(self):
|
||||
result = move(gs(my_body=[(1, 1), (1, 2), (2, 2), (2, 1)],
|
||||
other_bodies=[], foods=[(5, 5)], width=7, height=7))
|
||||
self.assertEqual(result, "right")
|
||||
|
||||
def test_no_safe_moves_returns_valid_direction(self):
|
||||
result = move(gs(my_body=[(0, 0), (0, 1), (1, 1), (1, 0)], other_bodies=[]))
|
||||
self.assertIn(result, ("up", "down", "left", "right"))
|
||||
|
||||
# ── Duel mode ─────────────────────────────────────────────────────────────────
|
||||
|
||||
class TestSupremeDuelMode(unittest.TestCase):
|
||||
|
||||
def test_avoids_h2h_with_equal_length(self):
|
||||
result = move(gs(my_body=[(5, 5), (5, 4), (5, 3)],
|
||||
other_bodies=[[(7, 5), (7, 4), (7, 3)]],
|
||||
foods=[(0, 0)]))
|
||||
self.assertNotEqual(result, "right")
|
||||
|
||||
def test_head_hunts_smaller_enemy(self):
|
||||
result = move(gs(
|
||||
my_body=[(5, 5), (5, 4), (5, 3), (5, 2), (5, 1), (4, 1), (4, 2)],
|
||||
other_bodies=[[(7, 5), (7, 4), (7, 3)]],
|
||||
foods=[(0, 0)]))
|
||||
self.assertEqual(result, "right")
|
||||
|
||||
def test_chases_food_when_low_health(self):
|
||||
result = move(gs(my_body=[(5, 5), (5, 4), (5, 3)],
|
||||
other_bodies=[[(9, 9), (9, 8), (9, 7)]],
|
||||
foods=[(5, 6)], my_health=10))
|
||||
self.assertEqual(result, "up")
|
||||
|
||||
# ── Constrictor mode ──────────────────────────────────────────────────────────
|
||||
|
||||
class TestSupremeConstrictorMode(unittest.TestCase):
|
||||
|
||||
def test_returns_valid_move(self):
|
||||
result = move(gs(my_body=[(5, 5), (5, 4), (5, 3)],
|
||||
other_bodies=[[(3, 3), (3, 4), (3, 5)]],
|
||||
game_type="constrictor"))
|
||||
self.assertIn(result, ("up", "down", "left", "right"))
|
||||
|
||||
# ── Multi-snake mode ─────────────────────────────────────────────────────────
|
||||
|
||||
class TestSupremeMultiSnakeMode(unittest.TestCase):
|
||||
|
||||
def test_returns_valid_move(self):
|
||||
result = move(gs(my_body=[(5, 5), (5, 4), (5, 3)],
|
||||
other_bodies=[[(2, 2), (2, 3), (2, 4)],
|
||||
[(8, 8), (8, 7), (8, 6)]],
|
||||
foods=[(3, 3), (7, 7)]))
|
||||
self.assertIn(result, ("up", "down", "left", "right"))
|
||||
|
||||
# ── Hazard tests ──────────────────────────────────────────────────────────────
|
||||
|
||||
class TestSupremeHazard(unittest.TestCase):
|
||||
|
||||
def test_hazard_penalizes_score(self):
|
||||
snake = SupremeBattleSnake()
|
||||
snake._bb = BitBoard(11, 11)
|
||||
snake._bb_w = 11
|
||||
snake._bb_h = 11
|
||||
snake._bits_cache = {}
|
||||
snake._bits_cache_turn = 0
|
||||
snake.game_board = make_board(gs(
|
||||
my_body=[(5, 5), (5, 4), (5, 3)],
|
||||
hazards=[(6, 5)], hazard_damage=14))
|
||||
snake.previous_hazards = {(6, 5)}
|
||||
snake._enemy_dmaps = []
|
||||
snake._enemy_heads = []
|
||||
snake._base_blocked = set()
|
||||
|
||||
score_right, _ = snake._score_move(
|
||||
move="right", pos={"x": 6, "y": 5},
|
||||
my_body=[{"x": 5, "y": 5}, {"x": 5, "y": 4}, {"x": 5, "y": 3}],
|
||||
my_len=3, my_health=90,
|
||||
other_snakes=[], food_set=set(),
|
||||
hazard_set={(6, 5)}, hazard_damage=14, hazard_count={(6, 5): 1},
|
||||
previous_hazard_set={(6, 5)},
|
||||
is_constrictor=False, enemy_attack_map={},
|
||||
enemy_can_grow={}, total_occupancy=0.05,
|
||||
width=11, height=11, deadline=None)
|
||||
score_up, _ = snake._score_move(
|
||||
move="up", pos={"x": 5, "y": 6},
|
||||
my_body=[{"x": 5, "y": 5}, {"x": 5, "y": 4}, {"x": 5, "y": 3}],
|
||||
my_len=3, my_health=90,
|
||||
other_snakes=[], food_set=set(),
|
||||
hazard_set={(6, 5)}, hazard_damage=14, hazard_count={(6, 5): 1},
|
||||
previous_hazard_set={(6, 5)},
|
||||
is_constrictor=False, enemy_attack_map={},
|
||||
enemy_can_grow={}, total_occupancy=0.05,
|
||||
width=11, height=11, deadline=None)
|
||||
self.assertGreater(score_up, score_right)
|
||||
|
||||
# ── Version ───────────────────────────────────────────────────────────────────
|
||||
|
||||
class TestSupremeVersion(unittest.TestCase):
|
||||
|
||||
def test_version(self):
|
||||
self.assertEqual(SupremeBattleSnake.VERSION, "1.0.0")
|
||||
|
||||
def test_class_name_contains_claude(self):
|
||||
snake = SupremeBattleSnake()
|
||||
self.assertIn("Claude", snake.__class__.__name__)
|
||||
|
||||
def test_builder(self):
|
||||
from snakes import SnakeBuilder
|
||||
snake = SnakeBuilder.build("SupremeBattleSnake_ClaudeOpus4_6")
|
||||
self.assertIsInstance(snake, SupremeBattleSnake)
|
||||
|
||||
# ── Parity: Supreme makes same decisions as Apex on key scenarios ────────────
|
||||
|
||||
class TestParityWithApex(unittest.TestCase):
|
||||
"""Ensure the bitboard optimisations don't change strategic behaviour."""
|
||||
|
||||
def test_trapped_corner(self):
|
||||
"""Both snakes should survive a forced single-exit scenario."""
|
||||
from snakes.ApexBattleSnake import ApexBattleSnake
|
||||
|
||||
state = gs(my_body=[(1, 1), (1, 2), (2, 2), (2, 1)],
|
||||
other_bodies=[], foods=[(5, 5)], width=7, height=7)
|
||||
|
||||
apex_snake = ApexBattleSnake()
|
||||
apex_board = GameBoard(game_id="parity", width=7, height=7,
|
||||
ruleset=state["game"]["ruleset"],
|
||||
source="custom", map="standard",
|
||||
snake_class=apex_snake)
|
||||
apex_board.read_game_data(state)
|
||||
apex_move = apex_board.snake_neat_make_a_move()
|
||||
|
||||
supreme_move = move(state)
|
||||
|
||||
# Both must find the only safe exit
|
||||
self.assertEqual(apex_move, "right")
|
||||
self.assertEqual(supreme_move, "right")
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user