diff --git a/snakes/BestBattleSnake.py b/snakes/BestBattleSnake.py index bc71add..3652ebb 100644 --- a/snakes/BestBattleSnake.py +++ b/snakes/BestBattleSnake.py @@ -295,6 +295,15 @@ class BestBattleSnake(TemplateSnake): next_options = self._next_turn_option_count( future_body, blocked, width, height ) + enemy_safe_options = self._safe_next_turn_option_count( + future_body=future_body, + other_snakes=other_snakes, + food_set=food_set, + is_constrictor=is_constrictor, + enemy_can_grow_cache=enemy_can_grow_cache, + width=width, + height=height, + ) territory = self._territory_control_score( my_start=point, enemy_starts=enemy_heads, @@ -320,12 +329,17 @@ class BestBattleSnake(TemplateSnake): score += reachable_space * 2.6 score += liberties * 18.0 score += next_options * 10.0 + score += enemy_safe_options * 24.0 score += territory * 0.35 if reachable_space < required_space: score -= 1200.0 if liberties == 0: score -= 900.0 + if enemy_safe_options == 0: + score -= 1700.0 + elif enemy_safe_options == 1: + score -= 420.0 enemy_len = enemy_attack_map.get(point) if enemy_len is not None: @@ -382,9 +396,11 @@ class BestBattleSnake(TemplateSnake): move_safety[move] = { "is_survivable": (not is_dead_end) and (not is_losing_head_to_head) + and enemy_safe_options > 0 and health_after_move > 0, "reachable_space": reachable_space, "next_options": next_options, + "enemy_safe_options": enemy_safe_options, "tail_escape": has_tail_escape, } @@ -474,6 +490,15 @@ class BestBattleSnake(TemplateSnake): next_options = self._next_turn_option_count( future_body, blocked, width, height ) + enemy_safe_options = self._safe_next_turn_option_count( + future_body=future_body, + other_snakes=other_snakes, + food_set=food_set, + is_constrictor=False, + enemy_can_grow_cache=enemy_can_grow_cache, + width=width, + height=height, + ) nearest_food_dist = self._nearest_food_distance(point, food_set, blocked, width, height) future_tail = future_body[-1] @@ -507,6 +532,7 @@ class BestBattleSnake(TemplateSnake): score += reachable_space * 2.8 score += liberties * 18.0 score += next_options * 10.0 + score += enemy_safe_options * 24.0 score += territory * 0.50 if likely_dead_end: @@ -514,6 +540,10 @@ class BestBattleSnake(TemplateSnake): if losing_head_to_head: score -= 1500.0 + if enemy_safe_options == 0: + score -= 1800.0 + elif enemy_safe_options == 1: + score -= 450.0 is_safe_tightening_move = not likely_dead_end and not losing_head_to_head if is_safe_tightening_move and enemy_space <= encase_target_space: @@ -578,8 +608,10 @@ class BestBattleSnake(TemplateSnake): move_safety[move] = { "is_survivable": (not likely_dead_end) and (not losing_head_to_head) + and enemy_safe_options > 0 and health_after_move > 0, "reachable_space": reachable_space, + "enemy_safe_options": enemy_safe_options, "tail_escape": has_tail_escape, } scores[move] = round(score, 5) @@ -644,6 +676,15 @@ class BestBattleSnake(TemplateSnake): next_options = self._next_turn_option_count( future_body, blocked, width, height ) + enemy_safe_options = self._safe_next_turn_option_count( + future_body=future_body, + other_snakes=other_snakes, + food_set=food_set, + is_constrictor=True, + enemy_can_grow_cache=enemy_can_grow_cache, + width=width, + height=height, + ) territory = self._territory_control_score( my_start=point, enemy_starts=enemy_heads, @@ -669,6 +710,7 @@ class BestBattleSnake(TemplateSnake): score += reachable_space * 3.8 score += liberties * 24.0 score += next_options * 16.0 + score += enemy_safe_options * 26.0 score += territory * 0.65 score += (reachable_space - enemy_best_space) * 3.2 score += (max(0, 8 - enemy_total_options)) * 18.0 @@ -685,6 +727,10 @@ class BestBattleSnake(TemplateSnake): score -= 2400.0 elif enemy_len is not None: score += 90.0 + if enemy_safe_options == 0: + score -= 2200.0 + elif enemy_safe_options == 1: + score -= 520.0 score -= self._revisit_penalty(point) @@ -698,8 +744,11 @@ class BestBattleSnake(TemplateSnake): score -= 35.0 move_safety[move] = { - "is_survivable": (not is_dead_end) and (not is_losing_head_to_head), + "is_survivable": (not is_dead_end) + and (not is_losing_head_to_head) + and enemy_safe_options > 0, "reachable_space": reachable_space, + "enemy_safe_options": enemy_safe_options, } scores[move] = round(score, 5) @@ -997,6 +1046,64 @@ class BestBattleSnake(TemplateSnake): count += 1 return count + + def _safe_next_turn_option_count(self, future_body:list[Coord], other_snakes:list[SnakeState], food_set:set[Point], is_constrictor:bool, enemy_can_grow_cache:dict[Any, bool]|None, width:int, height:int) -> int: + """Count next-turn moves that stay safe from enemy head contests.""" + if not future_body: + return 0 + + my_len = len(future_body) + future_snake = { + "head": future_body[0], + "body": future_body, + "length": my_len, + } + enemy_attack_map = self._build_enemy_attack_map( + my_snake=future_snake, + other_snakes=other_snakes, + food_set=food_set, + is_constrictor=is_constrictor, + width=width, + height=height, + enemy_can_grow_cache=enemy_can_grow_cache, + ) + blocked = self._simulation_blocked( + future_body=future_body, + other_snakes=other_snakes, + food_set=food_set, + is_constrictor=is_constrictor, + enemy_can_grow_cache=enemy_can_grow_cache, + ) + + next_head = future_body[0] + own_tail = (future_body[-1]["x"], future_body[-1]["y"]) + own_tail_stacked = self._is_tail_stacked(future_body) + safe_count = 0 + + for dx, dy in self.DIRECTIONS.values(): + point = (next_head["x"] + dx, next_head["y"] + dy) + if not self._in_bounds(point, width, height): + continue + + ate_food = point in food_set + can_step_on_tail = self._can_step_on_own_tail( + point=point, + own_tail=own_tail, + own_tail_is_stacked=own_tail_stacked, + ate_food=ate_food, + is_constrictor=is_constrictor, + ) + if point in blocked and not can_step_on_tail: + continue + + enemy_len = enemy_attack_map.get(point) + if enemy_len is not None and enemy_len >= my_len: + continue + + safe_count += 1 + + return safe_count + def _revisit_penalty(self, point:Point) -> float: """Return penalty for revisiting recent head positions.""" if not self.recent_heads: diff --git a/tests/test_BestBattleSnake.py b/tests/test_BestBattleSnake.py index 478afd0..08733be 100644 --- a/tests/test_BestBattleSnake.py +++ b/tests/test_BestBattleSnake.py @@ -468,6 +468,84 @@ class TestBestBattleSnake(unittest.TestCase): move = make_board(game_state).snake_neat_make_a_move() self.assertNotEqual(move, "up") + def test_avoids_enemy_block_in_trap(self): + game_state = { + "game": { + "id": "test-enemy-block-in-trap", + "ruleset": {"name": "standard", "version": "v1.0.0"}, + "source": "custom", + "map": "standard", + }, + "turn": 24, + "board": { + "height": 7, + "width": 7, + "food": [{"x": 3, "y": 3}], + "hazards": [], + "snakes": [ + { + "id": "me", + "name": "me", + "health": 72, + "length": 4, + "head": {"x": 3, "y": 2}, + "body": [ + {"x": 3, "y": 2}, + {"x": 3, "y": 1}, + {"x": 2, "y": 1}, + {"x": 2, "y": 2}, + ], + }, + { + "id": "enemy-a", + "name": "enemy-a", + "health": 90, + "length": 6, + "head": {"x": 4, "y": 4}, + "body": [ + {"x": 4, "y": 4}, + {"x": 4, "y": 3}, + {"x": 4, "y": 2}, + {"x": 5, "y": 2}, + {"x": 5, "y": 3}, + {"x": 5, "y": 4}, + ], + }, + { + "id": "enemy-b", + "name": "enemy-b", + "health": 90, + "length": 6, + "head": {"x": 1, "y": 3}, + "body": [ + {"x": 1, "y": 3}, + {"x": 1, "y": 4}, + {"x": 0, "y": 4}, + {"x": 0, "y": 3}, + {"x": 0, "y": 2}, + {"x": 1, "y": 2}, + ], + }, + ], + }, + "you": { + "id": "me", + "name": "me", + "health": 72, + "length": 4, + "head": {"x": 3, "y": 2}, + "body": [ + {"x": 3, "y": 2}, + {"x": 3, "y": 1}, + {"x": 2, "y": 1}, + {"x": 2, "y": 2}, + ], + }, + } + + move = make_board(game_state).snake_neat_make_a_move() + self.assertEqual(move, "left") + def test_royale_uses_ruleset_hazard_damage_setting(self): game_state = { "game": {