From 8f6bc3cfdd38bcfc72aa1ba09a65f5f3e6202d59 Mon Sep 17 00:00:00 2001 From: Daniel Dolezal Date: Fri, 3 Apr 2026 21:17:08 +0200 Subject: [PATCH] add timeout budget when exeaded use quick save move before timeout --- server/GameBoard.py | 7 ++++++- snakes/BestBattleSnake.py | 39 +++++++++++++++++++++++++++++++++++++-- 2 files changed, 43 insertions(+), 3 deletions(-) diff --git a/server/GameBoard.py b/server/GameBoard.py index e7e0fa5..7d02d19 100644 --- a/server/GameBoard.py +++ b/server/GameBoard.py @@ -16,12 +16,13 @@ class GameBoard: self.ruleset = ruleset self.map = map self.url = self._get_game_url(True if ruleset["version"] == "cli" else False) + self.timeout = 500 # Setter Functions def _set_snakes(self, snakes:list[dict]): self.other_snakes = [ x for x in snakes if x["id"] != self.my_snake["id"] ] - def _set_my_snake(self, my_snake:str): + def _set_my_snake(self, my_snake:dict): self.my_snake = my_snake def _set_food(self, food:list[dict]): @@ -67,6 +68,9 @@ class GameBoard: def get_ruleset(self): return self.ruleset + def get_timeout(self): + return self.timeout + def get_my_snake_head(self): return self.my_snake["head"] @@ -97,6 +101,7 @@ class GameBoard: self._set_snakes(game_data['board']['snakes']) self._set_turn(game_data["turn"]) + self.timeout = int(game_data.get('game', {}).get('timeout', 500)) async def start_game(self, game_data:dict): self.init_snakes = len(game_data['board']['snakes']) diff --git a/snakes/BestBattleSnake.py b/snakes/BestBattleSnake.py index 359af61..1f615d8 100644 --- a/snakes/BestBattleSnake.py +++ b/snakes/BestBattleSnake.py @@ -2,6 +2,7 @@ from collections.abc import Iterator from collections import deque from typing import Any, cast import random, os +from time import perf_counter from snakes.TemplateSnake import TemplateSnake @@ -76,6 +77,8 @@ class BestBattleSnake(TemplateSnake): self.game_board = game_data self.calculations = [] self.duel_style = self._get_duel_style() + timeout_ms = (game_data.get_timeout() if hasattr(game_data, "get_timeout") else 500) + deadline = perf_counter() + (max(50, int(timeout_ms) - 120) / 1000.0) game_id = getattr(game_data, "id", None) turn = game_data.get_turn() @@ -161,6 +164,7 @@ class BestBattleSnake(TemplateSnake): enemy_can_grow_cache=enemy_can_grow_cache, width=width, height=height, + deadline=deadline, ) self.recent_heads.append(current_head_point) self.last_move = best_move @@ -183,6 +187,7 @@ class BestBattleSnake(TemplateSnake): hazard_damage=hazard_damage, width=width, height=height, + deadline=deadline, ) self.recent_heads.append(current_head_point) self.last_move = best_move @@ -193,6 +198,8 @@ class BestBattleSnake(TemplateSnake): scores:dict[str, float] = {} move_safety:dict[str, dict[str, Any]] = {} for move, pos in safe_moves.items(): + if self._time_exceeded(deadline): + break point = (pos["x"], pos["y"]) ate_food = point in food_set @@ -307,6 +314,18 @@ class BestBattleSnake(TemplateSnake): scores[move] = round(score, 5) + if not scores: + quick_move = ( + self.last_move + if self.last_move in safe_moves + else random.choice(list(safe_moves.keys())) + ) + self.recent_heads.append(current_head_point) + self.last_move = quick_move + self.add_to_history({"turn": turn, "move": quick_move, "reason": "timeout_budget"}) + self.previous_hazards = set(hazard_set) + return quick_move + survivable_moves = [ move for move, data in move_safety.items() if data["is_survivable"] ] @@ -341,7 +360,7 @@ class BestBattleSnake(TemplateSnake): self.previous_hazards = set(hazard_set) return best_move - def _choose_duel_move(self, safe_moves:MoveMap, my_body:list[Coord], my_len:int, my_health:int, food_set:set[Point], hazard_set:set[Point], other_snakes:list[SnakeState],enemy_attack_map:AttackMap, enemy_can_grow_cache:dict[Any, bool], previous_hazard_set:set[Point], hazard_damage:int, width:int, height:int) -> tuple[str, dict[str, float]]: + def _choose_duel_move(self, safe_moves:MoveMap, my_body:list[Coord], my_len:int, my_health:int, food_set:set[Point], hazard_set:set[Point], other_snakes:list[SnakeState], enemy_attack_map:AttackMap, enemy_can_grow_cache:dict[Any, bool], previous_hazard_set:set[Point], hazard_damage:int, width:int, height:int, deadline:float|None=None) -> tuple[str, dict[str, float]]: """Score and select a move for one-vs-one games.""" duel_weights = self._duel_weights(self.duel_style) enemy = other_snakes[0] @@ -354,6 +373,8 @@ class BestBattleSnake(TemplateSnake): move_safety:dict[str, dict[str, Any]] = {} for move, pos in safe_moves.items(): + if self._time_exceeded(deadline): + break point = (pos["x"], pos["y"]) ate_food = point in food_set @@ -509,18 +530,23 @@ class BestBattleSnake(TemplateSnake): else: considered_moves = list(scores.keys()) + if not scores: + return random.choice(list(safe_moves.keys())), {} + best_score = max(scores[move] for move in considered_moves) top_moves = [ move for move in considered_moves if best_score - scores[move] <= 1.5 ] return random.choice(top_moves), scores - def _choose_constrictor_move(self, safe_moves:MoveMap, my_body:list[Coord], my_len:int, other_snakes:list[SnakeState], food_set:set[Point], enemy_attack_map:AttackMap, enemy_heads:list[Point], enemy_can_grow_cache:dict[Any, bool], width:int, height:int) -> tuple[str, dict[str, float]]: + def _choose_constrictor_move(self, safe_moves:MoveMap, my_body:list[Coord], my_len:int, other_snakes:list[SnakeState], food_set:set[Point], enemy_attack_map:AttackMap, enemy_heads:list[Point], enemy_can_grow_cache:dict[Any, bool], width:int, height:int, deadline:float|None=None) -> tuple[str, dict[str, float]]: """Score and select a move for constrictor games.""" scores:dict[str, float] = {} move_safety:dict[str, dict[str, Any]] = {} for move, pos in safe_moves.items(): + if self._time_exceeded(deadline): + break point = (pos["x"], pos["y"]) future_body = self._future_body( my_body, pos, ate_food=False, is_constrictor=True @@ -604,6 +630,9 @@ class BestBattleSnake(TemplateSnake): else: considered_moves = list(scores.keys()) + if not scores: + return random.choice(list(safe_moves.keys())), {} + best_score = max(scores[move] for move in considered_moves) top_moves = [ move for move in considered_moves if best_score - scores[move] <= 2.0 @@ -779,6 +808,12 @@ class BestBattleSnake(TemplateSnake): return False return point in previous_hazard_set + def _time_exceeded(self, deadline:float|None) -> bool: + """Return True when the move-calculation time budget is exhausted.""" + if deadline is None: + return False + return perf_counter() >= deadline + def _nearest_food_distance(self, start:Point, food_set:set[Point], blocked:set[Point], width:int, height:int) -> int|None: """Compute shortest reachable distance to any food using BFS.""" if not food_set: