From 013ac98821e63e8baf5dfef197cbba4f8193c1e5 Mon Sep 17 00:00:00 2001 From: Daniel Dolezal Date: Fri, 3 Apr 2026 19:26:56 +0200 Subject: [PATCH] implement royale game mode and tighten spaces in duel mode --- justfile | 82 +++++++++++++ server/GameBoard.py | 10 +- snakes/BestBattleSnake.py | 194 ++++++++++++++++++------------- tests/bench_best_battle_snake.py | 109 +++++++++++++++++ tests/test_BestBattleSnake.py | 147 +++++++++++++++++++++++ 5 files changed, 461 insertions(+), 81 deletions(-) create mode 100644 tests/bench_best_battle_snake.py diff --git a/justfile b/justfile index fb2a9cd..765d7c3 100644 --- a/justfile +++ b/justfile @@ -31,6 +31,37 @@ default: run: "{{justfile_directory()}}/main.py" +run-snake port="8000" snake="BestBattleSnake": + HOST="127.0.0.1" PORT="{{port}}" SNAKE="{{snake}}" DEBUG="false" DEBUG_SERVER="false" "{{justfile_directory()}}/main.py" + +run-4-snakes base_port="9101" snake="BestBattleSnake": + #!/usr/bin/env bash + set -euo pipefail + + pids=() + for i in 0 1 2 3; do + port="$(({{base_port}} + i))" + echo "Starting snake on :$port" + HOST="127.0.0.1" PORT="$port" SNAKE="{{snake}}" DEBUG="false" DEBUG_SERVER="false" "{{justfile_directory()}}/main.py" & + pids[$i]="$!" + done + + cleanup() { + for pid in "${pids[@]}"; do + kill "$pid" 2>/dev/null || true + done + wait || true + } + trap cleanup EXIT INT TERM + + wait + +bench-best-snake iterations="1000": + #!/usr/bin/env bash + set -euo pipefail + + PYTHONPATH="{{justfile_directory()}}" python "{{justfile_directory()}}/tests/bench_best_battle_snake.py" --iterations "{{iterations}}" + build-battlesnake-cli: #!/usr/bin/env bash set -euo pipefail @@ -76,6 +107,57 @@ test-seed: build-battlesnake-cli BATTLESNAKE_CLI="{{justfile_directory()}}/{{BATTLESNAKE_CLI_BIN}}" "$BATTLESNAKE_CLI" play -W 11 -H 11 --name 'Python Starter Project' --url http://localhost:8000 -g solo --browser --seed 1713099635738952360 +test-local-4 mode="standard" map="standard" base_port="9101" snake="BestBattleSnake" seed="1713099635738952360" browser="true": build-battlesnake-cli + #!/usr/bin/env bash + set -euo pipefail + + BATTLESNAKE_CLI="{{justfile_directory()}}/{{BATTLESNAKE_CLI_BIN}}" + LOG_DIR="{{justfile_directory()}}/.tools/snake-logs" + mkdir -p "$LOG_DIR" + + pids=() + for i in 0 1 2 3; do + port="$(({{base_port}} + i))" + log_file="$LOG_DIR/snake-$((i+1)).log" + echo "Starting snake-$((i+1)) on :$port (log: $log_file)" + HOST="127.0.0.1" PORT="$port" SNAKE="{{snake}}" DEBUG="false" DEBUG_SERVER="false" "{{justfile_directory()}}/main.py" > >(tee "$log_file") 2>&1 & + pids[$i]="$!" + done + + cleanup() { + for pid in "${pids[@]}"; do + kill "$pid" 2>/dev/null || true + done + wait || true + } + trap cleanup EXIT INT TERM + + for i in 0 1 2 3; do + port="$(({{base_port}} + i))" + for _ in $(seq 1 30); do + if curl -fsS "http://127.0.0.1:$port" >/dev/null 2>&1; then + break + fi + sleep 0.2 + done + if ! curl -fsS "http://127.0.0.1:$port" >/dev/null 2>&1; then + echo "Snake on :$port did not start correctly. Check logs in $LOG_DIR" + exit 1 + fi + done + + BROWSER_FLAG="" + if [ "{{browser}}" = "true" ]; then + BROWSER_FLAG="--browser" + fi + + "$BATTLESNAKE_CLI" play -W 11 -H 11 \ + --name "Snake 1" --url "http://127.0.0.1:{{base_port}}" \ + --name "Snake 2" --url "http://127.0.0.1:$(({{base_port}} + 1))" \ + --name "Snake 3" --url "http://127.0.0.1:$(({{base_port}} + 2))" \ + --name "Snake 4" --url "http://127.0.0.1:$(({{base_port}} + 3))" \ + -g "{{mode}}" --map "{{map}}" --seed "{{seed}}" $BROWSER_FLAG + # ------------------------------------------------------------------------------ # Fataset helpers # ------------------------------------------------------------------------------ diff --git a/server/GameBoard.py b/server/GameBoard.py index 48fed3d..e7e0fa5 100644 --- a/server/GameBoard.py +++ b/server/GameBoard.py @@ -61,6 +61,12 @@ class GameBoard: def get_type(self): return self.type + def get_map(self): + return self.map + + def get_ruleset(self): + return self.ruleset + def get_my_snake_head(self): return self.my_snake["head"] @@ -79,8 +85,8 @@ class GameBoard: "width": self.width, "snakes": snakes, "food": self.food, - "hazards": self.hazards - } + "hazards": self.hazards, + } # Game Functions def read_game_data(self, game_data:dict): diff --git a/snakes/BestBattleSnake.py b/snakes/BestBattleSnake.py index f9a0257..b14856c 100644 --- a/snakes/BestBattleSnake.py +++ b/snakes/BestBattleSnake.py @@ -26,6 +26,7 @@ class BestBattleSnake(TemplateSnake): self.recent_heads = deque(maxlen=14) self.last_move = None self.last_game_id = None + self.previous_hazards = set() self.duel_style = self._get_duel_style() def _get_duel_style(self): @@ -67,6 +68,7 @@ class BestBattleSnake(TemplateSnake): if game_id != self.last_game_id or turn <= 1: self.recent_heads.clear() self.last_move = None + self.previous_hazards = set() self.last_game_id = game_id my_snake = cast(dict[str, Any], game_data.get_my_snake()) @@ -87,6 +89,16 @@ class BestBattleSnake(TemplateSnake): food_set = {(food["x"], food["y"]) for food in foods} hazard_set = {(hazard["x"], hazard["y"]) for hazard in hazards} + previous_hazard_set = set(self.previous_hazards) + hazard_damage = self._hazard_damage_per_turn(game_data) + enemy_heads = [ + (snake["head"]["x"], snake["head"]["y"]) for snake in other_snakes + ] + enemy_can_grow_cache = { + snake["id"]: self._enemy_can_grow_this_turn(snake, food_set) + for snake in other_snakes + if "id" in snake + } current_head_point = (my_head["x"], my_head["y"]) safe_moves = self._legal_moves( @@ -110,6 +122,7 @@ class BestBattleSnake(TemplateSnake): "reason": "no_safe_moves", } ) + self.previous_hazards = set(hazard_set) return fallback enemy_attack_map = self._build_enemy_attack_map( @@ -119,6 +132,7 @@ class BestBattleSnake(TemplateSnake): is_constrictor=is_constrictor, width=width, height=height, + enemy_can_grow_cache=enemy_can_grow_cache, ) if is_constrictor: @@ -129,12 +143,15 @@ class BestBattleSnake(TemplateSnake): other_snakes=other_snakes, food_set=food_set, enemy_attack_map=enemy_attack_map, + enemy_heads=enemy_heads, + enemy_can_grow_cache=enemy_can_grow_cache, width=width, height=height, ) self.recent_heads.append(current_head_point) self.last_move = best_move self.add_to_history({"turn": turn, "move": best_move, "scores": scores}) + self.previous_hazards = set(hazard_set) return best_move if len(other_snakes) == 1: @@ -143,18 +160,20 @@ class BestBattleSnake(TemplateSnake): my_body=my_body, my_len=my_len, my_health=my_health, - foods=foods, food_set=food_set, - hazards=hazards, hazard_set=hazard_set, other_snakes=other_snakes, enemy_attack_map=enemy_attack_map, + enemy_can_grow_cache=enemy_can_grow_cache, + previous_hazard_set=previous_hazard_set, + hazard_damage=hazard_damage, width=width, height=height, ) self.recent_heads.append(current_head_point) self.last_move = best_move self.add_to_history({"turn": turn, "move": best_move, "scores": scores}) + self.previous_hazards = set(hazard_set) return best_move scores: dict[str, float] = {} @@ -169,6 +188,7 @@ class BestBattleSnake(TemplateSnake): other_snakes=other_snakes, food_set=food_set, is_constrictor=is_constrictor, + enemy_can_grow_cache=enemy_can_grow_cache, ) blocked.discard(point) @@ -180,17 +200,13 @@ class BestBattleSnake(TemplateSnake): ) territory = self._territory_control_score( my_start=point, - enemy_starts=[ - (snake["head"]["x"], snake["head"]["y"]) for snake in other_snakes - ], + enemy_starts=enemy_heads, blocked=blocked, width=width, height=height, ) - nearest_food_dist = self._nearest_food_distance( - point, foods, blocked, width, height - ) + nearest_food_dist = self._nearest_food_distance(point, food_set, blocked, width, height) future_tail = future_body[-1] tail_point = (future_tail["x"], future_tail["y"]) tail_dist = self._path_distance( @@ -242,7 +258,9 @@ class BestBattleSnake(TemplateSnake): score -= 40.0 if point in hazard_set: - score -= 70.0 if my_health > 35 else 250.0 + hazard_scale = max(0.5, hazard_damage / 14.0) + if not ate_food: + score -= (70.0 if my_health > 35 else 250.0) * hazard_scale score -= self._revisit_penalty(point) @@ -256,8 +274,9 @@ class BestBattleSnake(TemplateSnake): score -= 20.0 health_after_move = 100 if ate_food else my_health - 1 - if point in hazard_set: - health_after_move -= 15 + hazard_active = self._hazard_is_active(point, ate_food, hazard_set, previous_hazard_set) + if hazard_active: + health_after_move -= hazard_damage if health_after_move <= 0: score -= 10000.0 @@ -305,27 +324,15 @@ class BestBattleSnake(TemplateSnake): self.recent_heads.append(current_head_point) self.last_move = best_move self.add_to_history({"turn": turn, "move": best_move, "scores": scores}) + self.previous_hazards = set(hazard_set) return best_move - def _choose_duel_move( - self, - safe_moves, - my_body, - my_len, - my_health, - foods, - food_set, - hazards, - hazard_set, - other_snakes, - enemy_attack_map, - width, - height, - ): + def _choose_duel_move(self, safe_moves, my_body, my_len, my_health, food_set, hazard_set, other_snakes, enemy_attack_map, enemy_can_grow_cache, previous_hazard_set, hazard_damage, width, height): duel_weights = self._duel_weights(self.duel_style) enemy = other_snakes[0] enemy_head = (enemy["head"]["x"], enemy["head"]["y"]) enemy_len = enemy.get("length", len(enemy["body"])) + encase_target_space = max(8, enemy_len * 2) scores: dict[str, float] = {} move_safety: dict[str, dict[str, Any]] = {} @@ -342,6 +349,7 @@ class BestBattleSnake(TemplateSnake): other_snakes=other_snakes, food_set=food_set, is_constrictor=False, + enemy_can_grow_cache=enemy_can_grow_cache, ) blocked.discard(point) @@ -352,9 +360,7 @@ class BestBattleSnake(TemplateSnake): future_body, blocked, width, height ) - nearest_food_dist = self._nearest_food_distance( - point, foods, blocked, width, height - ) + nearest_food_dist = self._nearest_food_distance(point, food_set, blocked, width, height) future_tail = future_body[-1] tail_point = (future_tail["x"], future_tail["y"]) tail_dist = self._path_distance( @@ -380,6 +386,7 @@ class BestBattleSnake(TemplateSnake): enemy_attack_len is not None and enemy_attack_len >= my_len ) direct_head_distance = self._manhattan(point, enemy_head) + enemy_space, enemy_options = self._enemy_confinement_metrics(enemy_head, blocked, width, height) score = 0.0 score += reachable_space * 2.8 @@ -393,6 +400,15 @@ class BestBattleSnake(TemplateSnake): if losing_head_to_head: score -= 1500.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: + score += (encase_target_space - enemy_space) * 42.0 + score += max(0, 3 - enemy_options) * 95.0 + if reachable_space > enemy_space: + score += 120.0 + if direct_head_distance <= 2: + score += 40.0 + if my_len > enemy_len: if direct_head_distance == 1: score += 220.0 * duel_weights["head_pressure"] @@ -420,7 +436,9 @@ class BestBattleSnake(TemplateSnake): score -= 50.0 if point in hazard_set: - score -= 70.0 if my_health > 35 else 250.0 + hazard_scale = max(0.5, hazard_damage / 14.0) + if not ate_food: + score -= (70.0 if my_health > 35 else 250.0) * hazard_scale score -= self._revisit_penalty(point) @@ -434,8 +452,9 @@ class BestBattleSnake(TemplateSnake): score -= 20.0 health_after_move = 100 if ate_food else my_health - 1 - if point in hazard_set: - health_after_move -= 15 + hazard_active = self._hazard_is_active(point, ate_food, hazard_set, previous_hazard_set) + if hazard_active: + health_after_move -= hazard_damage if health_after_move <= 0: score -= 10000.0 @@ -478,17 +497,7 @@ class BestBattleSnake(TemplateSnake): ] return random.choice(top_moves), scores - def _choose_constrictor_move( - self, - safe_moves, - my_body, - my_len, - other_snakes, - food_set, - enemy_attack_map, - width, - height, - ): + def _choose_constrictor_move(self, safe_moves, my_body, my_len, other_snakes, food_set, enemy_attack_map, enemy_heads, enemy_can_grow_cache, width, height): scores: dict[str, float] = {} move_safety: dict[str, dict[str, Any]] = {} @@ -502,6 +511,7 @@ class BestBattleSnake(TemplateSnake): other_snakes=other_snakes, food_set=food_set, is_constrictor=True, + enemy_can_grow_cache=enemy_can_grow_cache, ) blocked.discard(point) @@ -513,9 +523,7 @@ class BestBattleSnake(TemplateSnake): ) territory = self._territory_control_score( my_start=point, - enemy_starts=[ - (snake["head"]["x"], snake["head"]["y"]) for snake in other_snakes - ], + enemy_starts=enemy_heads, blocked=blocked, width=width, height=height, @@ -583,16 +591,14 @@ class BestBattleSnake(TemplateSnake): ] return random.choice(top_moves), scores - def _legal_moves( - self, my_head, my_body, other_snakes, food_set, is_constrictor, width, height - ): + def _legal_moves(self, my_head, my_body, other_snakes, food_set, is_constrictor, width, height): occupied = self._occupied_cells(my_body, other_snakes) own_tail = (my_body[-1]["x"], my_body[-1]["y"]) own_tail_stacked = self._is_tail_stacked(my_body) safe_moves = {} - for move, pos in self.get_possible_moves(my_head).items(): - point = (pos["x"], pos["y"]) + for move, (dx, dy) in self.DIRECTIONS.items(): + point = (my_head["x"] + dx, my_head["y"] + dy) if not self._in_bounds(point, width, height): continue @@ -608,7 +614,7 @@ class BestBattleSnake(TemplateSnake): if point in occupied and not can_step_on_tail: continue - safe_moves[move] = pos + safe_moves[move] = {"x": point[0], "y": point[1]} return safe_moves @@ -618,7 +624,7 @@ class BestBattleSnake(TemplateSnake): occupied |= {(segment["x"], segment["y"]) for segment in snake["body"]} return occupied - def _simulation_blocked(self, future_body, other_snakes, food_set, is_constrictor): + def _simulation_blocked(self, future_body, other_snakes, food_set, is_constrictor, enemy_can_grow_cache=None): blocked = {(segment["x"], segment["y"]) for segment in future_body} if not is_constrictor and not self._is_tail_stacked(future_body): @@ -632,7 +638,13 @@ class BestBattleSnake(TemplateSnake): if is_constrictor: continue - if self._enemy_can_grow_this_turn(snake, food_set): + snake_id = snake.get("id") + enemy_can_grow = ( + enemy_can_grow_cache.get(snake_id) + if enemy_can_grow_cache and snake_id is not None + else self._enemy_can_grow_this_turn(snake, food_set) + ) + if enemy_can_grow: continue if self._is_tail_stacked(snake["body"]): @@ -643,20 +655,25 @@ class BestBattleSnake(TemplateSnake): return blocked - def _build_enemy_attack_map( - self, my_snake, other_snakes, food_set, is_constrictor, width, height - ): + def _build_enemy_attack_map(self, my_snake, other_snakes, food_set, is_constrictor, width, height, enemy_can_grow_cache=None): occupied = self._occupied_cells(my_snake["body"], other_snakes) - my_id = my_snake["id"] + my_body_points = {(segment["x"], segment["y"]) for segment in my_snake["body"]} attack_map = {} for enemy in other_snakes: enemy_len = enemy.get("length", len(enemy["body"])) enemy_tail = (enemy["body"][-1]["x"], enemy["body"][-1]["y"]) enemy_tail_stacked = self._is_tail_stacked(enemy["body"]) + snake_id = enemy.get("id") + enemy_can_grow = ( + enemy_can_grow_cache.get(snake_id) + if enemy_can_grow_cache and snake_id is not None + else self._enemy_can_grow_this_turn(enemy, food_set) + ) - for pos in self.get_possible_moves(enemy["head"]).values(): - point = (pos["x"], pos["y"]) + enemy_head = enemy["head"] + for dx, dy in self.DIRECTIONS.values(): + point = (enemy_head["x"] + dx, enemy_head["y"] + dy) if not self._in_bounds(point, width, height): continue @@ -664,18 +681,14 @@ class BestBattleSnake(TemplateSnake): not is_constrictor and point == enemy_tail and not enemy_tail_stacked - and not self._enemy_can_grow_this_turn(enemy, food_set) + and not enemy_can_grow ) if point in occupied and not can_step_on_enemy_tail: continue # Do not consider impossible overlap directly into my own occupied body except head swap possibilities. - if point in { - (segment["x"], segment["y"]) - for segment in my_snake["body"] - if my_snake["id"] == my_id - }: + if point in my_body_points: continue previous = attack_map.get(point) @@ -694,9 +707,7 @@ class BestBattleSnake(TemplateSnake): next_body.pop() return next_body - def _can_step_on_own_tail( - self, point, own_tail, own_tail_is_stacked, ate_food, is_constrictor - ): + def _can_step_on_own_tail(self, point, own_tail, own_tail_is_stacked, ate_food, is_constrictor): if is_constrictor: return False if ate_food: @@ -717,17 +728,31 @@ class BestBattleSnake(TemplateSnake): return True return False - def _nearest_food_distance(self, start, foods, blocked, width, height): - if not foods: - return None + def _hazard_damage_per_turn(self, game_data): + ruleset = {} + if hasattr(game_data, "get_ruleset"): + ruleset = game_data.get_ruleset() or {} + elif hasattr(game_data, "ruleset"): + ruleset = game_data.ruleset or {} + settings = ruleset.get("settings", {}) if isinstance(ruleset, dict) else {} + return int(settings.get("hazardDamagePerTurn", 15)) - targets = {(food["x"], food["y"]) for food in foods} + def _hazard_is_active(self, point, ate_food, hazard_set, previous_hazard_set): + if point not in hazard_set: + return False + if ate_food: + return False + return point in previous_hazard_set + + def _nearest_food_distance(self, start, food_set, blocked, width, height): + if not food_set: + return None queue = deque([(start, 0)]) seen = {start} while queue: point, distance = queue.popleft() - if point in targets: + if point in food_set: return distance for neighbor in self._neighbors(point): @@ -735,7 +760,7 @@ class BestBattleSnake(TemplateSnake): continue if not self._in_bounds(neighbor, width, height): continue - if neighbor in blocked and neighbor not in targets: + if neighbor in blocked and neighbor not in food_set: continue seen.add(neighbor) queue.append((neighbor, distance + 1)) @@ -797,8 +822,8 @@ class BestBattleSnake(TemplateSnake): next_head = future_body[0] count = 0 - for pos in self.get_possible_moves(next_head).values(): - point = (pos["x"], pos["y"]) + 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 if point in blocked: @@ -807,6 +832,8 @@ class BestBattleSnake(TemplateSnake): return count def _revisit_penalty(self, point): + if not self.recent_heads: + return 0.0 penalty = 0.0 for index, old_point in enumerate(reversed(self.recent_heads), start=1): if old_point != point: @@ -867,6 +894,15 @@ class BestBattleSnake(TemplateSnake): return distances + def _enemy_confinement_metrics(self, enemy_head, blocked, width, height): + enemy_blocked = set(blocked) + enemy_blocked.discard(enemy_head) + enemy_space = self._flood_fill_count(enemy_head, enemy_blocked, width, height) + enemy_options = self._open_neighbor_count( + enemy_head, enemy_blocked, width, height + ) + return enemy_space, enemy_options + def _neighbors(self, point): for dx, dy in self.DIRECTIONS.values(): yield (point[0] + dx, point[1] + dy) @@ -878,8 +914,8 @@ class BestBattleSnake(TemplateSnake): return 0 <= point[0] < width and 0 <= point[1] < height def _fallback_move(self, head, width, height): - for move, pos in self.get_possible_moves(head).items(): - point = (pos["x"], pos["y"]) + for move, (dx, dy) in self.DIRECTIONS.items(): + point = (head["x"] + dx, head["y"] + dy) if self._in_bounds(point, width, height): return move return "up" diff --git a/tests/bench_best_battle_snake.py b/tests/bench_best_battle_snake.py new file mode 100644 index 0000000..116a330 --- /dev/null +++ b/tests/bench_best_battle_snake.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python3 +import argparse +import time + +from server.GameBoard import GameBoard +from snakes.BestBattleSnake import BestBattleSnake + +def build_game_state() -> dict: + return { + "game": { + "id": "bench-best-snake", + "ruleset": { + "name": "standard", + "version": "v1.0.0", + "settings": {"hazardDamagePerTurn": 14}, + }, + "source": "custom", + "map": "standard", + }, + "turn": 42, + "board": { + "height": 11, + "width": 11, + "food": [{"x": 1, "y": 9}, {"x": 9, "y": 1}], + "hazards": [], + "snakes": [ + { + "id": "me", + "name": "me", + "health": 74, + "length": 8, + "head": {"x": 5, "y": 5}, + "body": [ + {"x": 5, "y": 5}, + {"x": 5, "y": 4}, + {"x": 5, "y": 3}, + {"x": 4, "y": 3}, + {"x": 3, "y": 3}, + {"x": 3, "y": 4}, + {"x": 3, "y": 5}, + {"x": 4, "y": 5}, + ], + }, + { + "id": "enemy", + "name": "enemy", + "health": 70, + "length": 8, + "head": {"x": 7, "y": 7}, + "body": [ + {"x": 7, "y": 7}, + {"x": 7, "y": 6}, + {"x": 7, "y": 5}, + {"x": 8, "y": 5}, + {"x": 9, "y": 5}, + {"x": 9, "y": 6}, + {"x": 9, "y": 7}, + {"x": 8, "y": 7}, + ], + }, + ], + }, + "you": { + "id": "me", + "name": "me", + "health": 74, + "length": 8, + "head": {"x": 5, "y": 5}, + "body": [ + {"x": 5, "y": 5}, + {"x": 5, "y": 4}, + {"x": 5, "y": 3}, + {"x": 4, "y": 3}, + {"x": 3, "y": 3}, + {"x": 3, "y": 4}, + {"x": 3, "y": 5}, + {"x": 4, "y": 5}, + ], + }, + } + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--iterations", type=int, default=1000) + args = parser.parse_args() + + game_state = build_game_state() + 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"]["source"], + map=game_state["game"]["map"], + snake_class=BestBattleSnake(), + ) + + start = time.perf_counter() + for i in range(args.iterations): + game_state["turn"] = i + 1 + board.read_game_data(game_state) + board.snake_neat_make_a_move() + elapsed = time.perf_counter() - start + + avg_ms = (elapsed / max(1, args.iterations)) * 1000.0 + print(f"BestBattleSnake benchmark: {args.iterations} moves in {elapsed:.4f}s ({avg_ms:.3f} ms/move)") + +if __name__ == "__main__": + main() diff --git a/tests/test_BestBattleSnake.py b/tests/test_BestBattleSnake.py index 00bad70..baa7d1e 100644 --- a/tests/test_BestBattleSnake.py +++ b/tests/test_BestBattleSnake.py @@ -328,6 +328,84 @@ class TestBestBattleSnake(unittest.TestCase): move = make_board(game_state).snake_neat_make_a_move() self.assertNotEqual(move, "up") + def test_royale_uses_ruleset_hazard_damage_setting(self): + game_state = { + "game": { + "id": "test-royale-hazard-setting", + "ruleset": { + "name": "standard", + "version": "v1.0.0", + "settings": {"hazardDamagePerTurn": 22}, + }, + "source": "custom", + "map": "royale", + }, + "turn": 5, + "board": { + "height": 11, + "width": 11, + "food": [], + "hazards": [], + "snakes": [ + { + "id": "me", + "name": "me", + "health": 80, + "length": 3, + "head": {"x": 5, "y": 5}, + "body": [ + {"x": 5, "y": 5}, + {"x": 5, "y": 4}, + {"x": 5, "y": 3}, + ], + } + ], + }, + "you": { + "id": "me", + "name": "me", + "health": 80, + "length": 3, + "head": {"x": 5, "y": 5}, + "body": [ + {"x": 5, "y": 5}, + {"x": 5, "y": 4}, + {"x": 5, "y": 3}, + ], + }, + } + + board = make_board(game_state) + snake = board.snake_class + self.assertEqual(snake._hazard_damage_per_turn(board), 22) + + def test_royale_new_hazard_has_spawn_grace(self): + snake = BestBattleSnake() + point = (4, 4) + hazard_set = {point} + + self.assertFalse( + snake._hazard_is_active( + point, ate_food=False, hazard_set=hazard_set, previous_hazard_set=set() + ) + ) + self.assertTrue( + snake._hazard_is_active( + point, + ate_food=False, + hazard_set=hazard_set, + previous_hazard_set=hazard_set, + ) + ) + self.assertFalse( + snake._hazard_is_active( + point, + ate_food=True, + hazard_set=hazard_set, + previous_hazard_set=hazard_set, + ) + ) + def test_constrictor_avoids_growth_dead_end(self): game_state = { "game": { @@ -393,6 +471,75 @@ class TestBestBattleSnake(unittest.TestCase): move = make_board(game_state).snake_neat_make_a_move() self.assertEqual(move, "up") + def test_duel_tightens_space_when_enemy_is_encased(self): + game_state = { + "game": { + "id": "test-encase-tighten", + "ruleset": {"name": "standard", "version": "v1.0.0"}, + "source": "custom", + "map": "standard", + }, + "turn": 40, + "board": { + "height": 7, + "width": 7, + "food": [{"x": 0, "y": 0}], + "hazards": [], + "snakes": [ + { + "id": "me", + "name": "me", + "health": 92, + "length": 8, + "head": {"x": 2, "y": 3}, + "body": [ + {"x": 2, "y": 3}, + {"x": 2, "y": 2}, + {"x": 1, "y": 2}, + {"x": 1, "y": 3}, + {"x": 1, "y": 4}, + {"x": 2, "y": 4}, + {"x": 3, "y": 4}, + {"x": 3, "y": 5}, + ], + }, + { + "id": "enemy", + "name": "enemy", + "health": 88, + "length": 5, + "head": {"x": 4, "y": 3}, + "body": [ + {"x": 4, "y": 3}, + {"x": 5, "y": 3}, + {"x": 5, "y": 2}, + {"x": 4, "y": 2}, + {"x": 4, "y": 1}, + ], + }, + ], + }, + "you": { + "id": "me", + "name": "me", + "health": 92, + "length": 8, + "head": {"x": 2, "y": 3}, + "body": [ + {"x": 2, "y": 3}, + {"x": 2, "y": 2}, + {"x": 1, "y": 2}, + {"x": 1, "y": 3}, + {"x": 1, "y": 4}, + {"x": 2, "y": 4}, + {"x": 3, "y": 4}, + {"x": 3, "y": 5}, + ], + }, + } + + move = make_board(game_state).snake_neat_make_a_move() + self.assertEqual(move, "right") if __name__ == "__main__": unittest.main()