c704fbc742
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.
372 lines
14 KiB
Python
372 lines
14 KiB
Python
"""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()
|