feat(snake): optimize duels and persist customizations
Build and Push Docker Container / build-and-push (push) Successful in 7m29s
Build and Push Docker Container / build-and-push (push) Successful in 7m29s
- Add deadline-aware iterative duel search with bitboards and tuple bodies. - Reuse transposition bounds and move-order hints across search depths. - Persist snake colors, heads, and tails in SQLite and PostgreSQL. - Restore customization metadata when hydrating dashboard replays. - Cover duel deadlines, cache reuse, schema storage, and replay output.
This commit is contained in:
@@ -1,9 +1,11 @@
|
||||
import unittest
|
||||
from time import perf_counter
|
||||
|
||||
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
|
||||
from snakes.bitboard_duel_search import BitboardDuelSearch
|
||||
|
||||
class TestBitBoard(unittest.TestCase):
|
||||
|
||||
@@ -64,5 +66,51 @@ class TestPrismBattleSnake_GPT_5_6_Sol(unittest.TestCase):
|
||||
self.assertEqual(open_count, 9)
|
||||
self.assertEqual(trapped_count, 1)
|
||||
|
||||
def test_bitboard_duel_search_values_length_advantage(self):
|
||||
snake = PrismBattleSnake_GPT_5_6_Sol()
|
||||
my_body = [{"x": 1, "y": 0}, {"x": 0, "y": 0}, {"x": 0, "y": 1}]
|
||||
enemy_body = [{"x": 1, "y": 2}, {"x": 0, "y": 2}]
|
||||
|
||||
value = snake._minimax_sim(
|
||||
my_body=my_body, enemy_body=enemy_body,
|
||||
food_set=set(), hazard_set=set(),
|
||||
my_health=100, enemy_health=100,
|
||||
hazard_damage=15, hazard_count={},
|
||||
width=3, height=3, depth=2,
|
||||
alpha=-1e9, beta=1e9, deadline=None,
|
||||
)
|
||||
|
||||
self.assertGreater(value, 0)
|
||||
|
||||
def test_bitboard_duel_search_reuses_transpositions(self):
|
||||
board = BitBoard(5, 5)
|
||||
search = BitboardDuelSearch(
|
||||
board=board, food=set(), hazards=set(), hazard_count={},
|
||||
hazard_damage=15, deadline=perf_counter() + 1.0,
|
||||
)
|
||||
my_body = [{"x": 1, "y": 1}, {"x": 1, "y": 0}, {"x": 0, "y": 0}]
|
||||
enemy_body = [{"x": 3, "y": 3}, {"x": 3, "y": 4}, {"x": 4, "y": 4}]
|
||||
|
||||
search.search_depth(my_body, enemy_body, 100, 100, 3, set())
|
||||
hits_before = search.cache_hits
|
||||
search.search_depth(my_body, enemy_body, 100, 100, 3, set())
|
||||
|
||||
self.assertGreater(search.cache_hits, hits_before)
|
||||
|
||||
def test_bitboard_duel_search_respects_deadline(self):
|
||||
board = BitBoard(11, 11)
|
||||
search = BitboardDuelSearch(
|
||||
board=board, food=set(), hazards=set(), hazard_count={},
|
||||
hazard_damage=15, deadline=perf_counter() - 0.001,
|
||||
)
|
||||
my_body = [{"x": 2, "y": 2}, {"x": 2, "y": 1}, {"x": 2, "y": 0}]
|
||||
enemy_body = [{"x": 8, "y": 8}, {"x": 8, "y": 9}, {"x": 8, "y": 10}]
|
||||
|
||||
started = perf_counter()
|
||||
_, depth = search.search(my_body, enemy_body, 100, 100, 6, set())
|
||||
|
||||
self.assertEqual(depth, 0)
|
||||
self.assertLess(perf_counter() - started, 0.05)
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -13,6 +13,11 @@ class TestGameplayDatabase(unittest.IsolatedAsyncioTestCase):
|
||||
"name": "Me",
|
||||
"health": 90,
|
||||
"length": 3,
|
||||
"customizations": {
|
||||
"color": "#00ff00",
|
||||
"head": "ferret",
|
||||
"tail": "swirl",
|
||||
},
|
||||
"head": {"x": me_head[0], "y": me_head[1]},
|
||||
"body": [
|
||||
{"x": me_head[0], "y": me_head[1]},
|
||||
@@ -99,10 +104,13 @@ class TestGameplayDatabase(unittest.IsolatedAsyncioTestCase):
|
||||
""", ("game-abc", 2, "me")).fetchone()[0]
|
||||
self.assertNotEqual(stored_body, "[]")
|
||||
identities = connection.execute("""
|
||||
SELECT snake_id, snake_name, is_you FROM game_snakes
|
||||
SELECT snake_id, snake_name, is_you, customizations_json FROM game_snakes
|
||||
WHERE game_id = ? ORDER BY snake_id
|
||||
""", ("game-abc",)).fetchall()
|
||||
self.assertEqual(identities, [("enemy", "Enemy", 0), ("me", "Me", 1)])
|
||||
self.assertEqual(identities, [
|
||||
("enemy", "Enemy", 0, "{}"),
|
||||
("me", "Me", 1, '{"color":"#00ff00","head":"ferret","tail":"swirl"}'),
|
||||
])
|
||||
repeated_identity = connection.execute("""
|
||||
SELECT snake_name, is_you FROM snake_turns
|
||||
WHERE game_id = ? AND turn = ? AND snake_id = ?
|
||||
@@ -127,7 +135,15 @@ class TestGameplayDatabase(unittest.IsolatedAsyncioTestCase):
|
||||
self.assertEqual(replay["turns"][1]["my_thinking"]["reason"], "food")
|
||||
self.assertEqual(replay["turns"][1]["food"], [{"x": 2, "y": 2}])
|
||||
self.assertEqual(replay["turns"][1]["you"]["id"], "me")
|
||||
self.assertEqual(len(replay["turns"][1]["snakes"][0]["body"]), 3)
|
||||
me = next(snake for snake in replay["turns"][1]["snakes"] if snake["snake_id"] == "me")
|
||||
self.assertEqual(me["customizations"], {
|
||||
"color": "#00ff00",
|
||||
"head": "ferret",
|
||||
"tail": "swirl",
|
||||
})
|
||||
self.assertEqual(len(me["body"]), 3)
|
||||
api_me = next(snake for snake in replay["turns"][1]["board"]["snakes"] if snake["id"] == "me")
|
||||
self.assertEqual(api_me["customizations"], me["customizations"])
|
||||
|
||||
connection.close()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user