implement royale game mode and tighten spaces in duel mode

This commit is contained in:
2026-04-03 19:26:56 +02:00
parent a3fe386198
commit 013ac98821
5 changed files with 461 additions and 81 deletions
+82
View File
@@ -31,6 +31,37 @@ default:
run: run:
"{{justfile_directory()}}/main.py" "{{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: build-battlesnake-cli:
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
@@ -76,6 +107,57 @@ test-seed: build-battlesnake-cli
BATTLESNAKE_CLI="{{justfile_directory()}}/{{BATTLESNAKE_CLI_BIN}}" 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 "$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 # Fataset helpers
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
+7 -1
View File
@@ -61,6 +61,12 @@ class GameBoard:
def get_type(self): def get_type(self):
return self.type return self.type
def get_map(self):
return self.map
def get_ruleset(self):
return self.ruleset
def get_my_snake_head(self): def get_my_snake_head(self):
return self.my_snake["head"] return self.my_snake["head"]
@@ -79,7 +85,7 @@ class GameBoard:
"width": self.width, "width": self.width,
"snakes": snakes, "snakes": snakes,
"food": self.food, "food": self.food,
"hazards": self.hazards "hazards": self.hazards,
} }
# Game Functions # Game Functions
+115 -79
View File
@@ -26,6 +26,7 @@ class BestBattleSnake(TemplateSnake):
self.recent_heads = deque(maxlen=14) self.recent_heads = deque(maxlen=14)
self.last_move = None self.last_move = None
self.last_game_id = None self.last_game_id = None
self.previous_hazards = set()
self.duel_style = self._get_duel_style() self.duel_style = self._get_duel_style()
def _get_duel_style(self): def _get_duel_style(self):
@@ -67,6 +68,7 @@ class BestBattleSnake(TemplateSnake):
if game_id != self.last_game_id or turn <= 1: if game_id != self.last_game_id or turn <= 1:
self.recent_heads.clear() self.recent_heads.clear()
self.last_move = None self.last_move = None
self.previous_hazards = set()
self.last_game_id = game_id self.last_game_id = game_id
my_snake = cast(dict[str, Any], game_data.get_my_snake()) 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} food_set = {(food["x"], food["y"]) for food in foods}
hazard_set = {(hazard["x"], hazard["y"]) for hazard in hazards} 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"]) current_head_point = (my_head["x"], my_head["y"])
safe_moves = self._legal_moves( safe_moves = self._legal_moves(
@@ -110,6 +122,7 @@ class BestBattleSnake(TemplateSnake):
"reason": "no_safe_moves", "reason": "no_safe_moves",
} }
) )
self.previous_hazards = set(hazard_set)
return fallback return fallback
enemy_attack_map = self._build_enemy_attack_map( enemy_attack_map = self._build_enemy_attack_map(
@@ -119,6 +132,7 @@ class BestBattleSnake(TemplateSnake):
is_constrictor=is_constrictor, is_constrictor=is_constrictor,
width=width, width=width,
height=height, height=height,
enemy_can_grow_cache=enemy_can_grow_cache,
) )
if is_constrictor: if is_constrictor:
@@ -129,12 +143,15 @@ class BestBattleSnake(TemplateSnake):
other_snakes=other_snakes, other_snakes=other_snakes,
food_set=food_set, food_set=food_set,
enemy_attack_map=enemy_attack_map, enemy_attack_map=enemy_attack_map,
enemy_heads=enemy_heads,
enemy_can_grow_cache=enemy_can_grow_cache,
width=width, width=width,
height=height, height=height,
) )
self.recent_heads.append(current_head_point) self.recent_heads.append(current_head_point)
self.last_move = best_move self.last_move = best_move
self.add_to_history({"turn": turn, "move": best_move, "scores": scores}) self.add_to_history({"turn": turn, "move": best_move, "scores": scores})
self.previous_hazards = set(hazard_set)
return best_move return best_move
if len(other_snakes) == 1: if len(other_snakes) == 1:
@@ -143,18 +160,20 @@ class BestBattleSnake(TemplateSnake):
my_body=my_body, my_body=my_body,
my_len=my_len, my_len=my_len,
my_health=my_health, my_health=my_health,
foods=foods,
food_set=food_set, food_set=food_set,
hazards=hazards,
hazard_set=hazard_set, hazard_set=hazard_set,
other_snakes=other_snakes, other_snakes=other_snakes,
enemy_attack_map=enemy_attack_map, 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, width=width,
height=height, height=height,
) )
self.recent_heads.append(current_head_point) self.recent_heads.append(current_head_point)
self.last_move = best_move self.last_move = best_move
self.add_to_history({"turn": turn, "move": best_move, "scores": scores}) self.add_to_history({"turn": turn, "move": best_move, "scores": scores})
self.previous_hazards = set(hazard_set)
return best_move return best_move
scores: dict[str, float] = {} scores: dict[str, float] = {}
@@ -169,6 +188,7 @@ class BestBattleSnake(TemplateSnake):
other_snakes=other_snakes, other_snakes=other_snakes,
food_set=food_set, food_set=food_set,
is_constrictor=is_constrictor, is_constrictor=is_constrictor,
enemy_can_grow_cache=enemy_can_grow_cache,
) )
blocked.discard(point) blocked.discard(point)
@@ -180,17 +200,13 @@ class BestBattleSnake(TemplateSnake):
) )
territory = self._territory_control_score( territory = self._territory_control_score(
my_start=point, my_start=point,
enemy_starts=[ enemy_starts=enemy_heads,
(snake["head"]["x"], snake["head"]["y"]) for snake in other_snakes
],
blocked=blocked, blocked=blocked,
width=width, width=width,
height=height, height=height,
) )
nearest_food_dist = self._nearest_food_distance( nearest_food_dist = self._nearest_food_distance(point, food_set, blocked, width, height)
point, foods, blocked, width, height
)
future_tail = future_body[-1] future_tail = future_body[-1]
tail_point = (future_tail["x"], future_tail["y"]) tail_point = (future_tail["x"], future_tail["y"])
tail_dist = self._path_distance( tail_dist = self._path_distance(
@@ -242,7 +258,9 @@ class BestBattleSnake(TemplateSnake):
score -= 40.0 score -= 40.0
if point in hazard_set: 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) score -= self._revisit_penalty(point)
@@ -256,8 +274,9 @@ class BestBattleSnake(TemplateSnake):
score -= 20.0 score -= 20.0
health_after_move = 100 if ate_food else my_health - 1 health_after_move = 100 if ate_food else my_health - 1
if point in hazard_set: hazard_active = self._hazard_is_active(point, ate_food, hazard_set, previous_hazard_set)
health_after_move -= 15 if hazard_active:
health_after_move -= hazard_damage
if health_after_move <= 0: if health_after_move <= 0:
score -= 10000.0 score -= 10000.0
@@ -305,27 +324,15 @@ class BestBattleSnake(TemplateSnake):
self.recent_heads.append(current_head_point) self.recent_heads.append(current_head_point)
self.last_move = best_move self.last_move = best_move
self.add_to_history({"turn": turn, "move": best_move, "scores": scores}) self.add_to_history({"turn": turn, "move": best_move, "scores": scores})
self.previous_hazards = set(hazard_set)
return best_move return best_move
def _choose_duel_move( 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):
self,
safe_moves,
my_body,
my_len,
my_health,
foods,
food_set,
hazards,
hazard_set,
other_snakes,
enemy_attack_map,
width,
height,
):
duel_weights = self._duel_weights(self.duel_style) duel_weights = self._duel_weights(self.duel_style)
enemy = other_snakes[0] enemy = other_snakes[0]
enemy_head = (enemy["head"]["x"], enemy["head"]["y"]) enemy_head = (enemy["head"]["x"], enemy["head"]["y"])
enemy_len = enemy.get("length", len(enemy["body"])) enemy_len = enemy.get("length", len(enemy["body"]))
encase_target_space = max(8, enemy_len * 2)
scores: dict[str, float] = {} scores: dict[str, float] = {}
move_safety: dict[str, dict[str, Any]] = {} move_safety: dict[str, dict[str, Any]] = {}
@@ -342,6 +349,7 @@ class BestBattleSnake(TemplateSnake):
other_snakes=other_snakes, other_snakes=other_snakes,
food_set=food_set, food_set=food_set,
is_constrictor=False, is_constrictor=False,
enemy_can_grow_cache=enemy_can_grow_cache,
) )
blocked.discard(point) blocked.discard(point)
@@ -352,9 +360,7 @@ class BestBattleSnake(TemplateSnake):
future_body, blocked, width, height future_body, blocked, width, height
) )
nearest_food_dist = self._nearest_food_distance( nearest_food_dist = self._nearest_food_distance(point, food_set, blocked, width, height)
point, foods, blocked, width, height
)
future_tail = future_body[-1] future_tail = future_body[-1]
tail_point = (future_tail["x"], future_tail["y"]) tail_point = (future_tail["x"], future_tail["y"])
tail_dist = self._path_distance( tail_dist = self._path_distance(
@@ -380,6 +386,7 @@ class BestBattleSnake(TemplateSnake):
enemy_attack_len is not None and enemy_attack_len >= my_len enemy_attack_len is not None and enemy_attack_len >= my_len
) )
direct_head_distance = self._manhattan(point, enemy_head) 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 = 0.0
score += reachable_space * 2.8 score += reachable_space * 2.8
@@ -393,6 +400,15 @@ class BestBattleSnake(TemplateSnake):
if losing_head_to_head: if losing_head_to_head:
score -= 1500.0 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 my_len > enemy_len:
if direct_head_distance == 1: if direct_head_distance == 1:
score += 220.0 * duel_weights["head_pressure"] score += 220.0 * duel_weights["head_pressure"]
@@ -420,7 +436,9 @@ class BestBattleSnake(TemplateSnake):
score -= 50.0 score -= 50.0
if point in hazard_set: 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) score -= self._revisit_penalty(point)
@@ -434,8 +452,9 @@ class BestBattleSnake(TemplateSnake):
score -= 20.0 score -= 20.0
health_after_move = 100 if ate_food else my_health - 1 health_after_move = 100 if ate_food else my_health - 1
if point in hazard_set: hazard_active = self._hazard_is_active(point, ate_food, hazard_set, previous_hazard_set)
health_after_move -= 15 if hazard_active:
health_after_move -= hazard_damage
if health_after_move <= 0: if health_after_move <= 0:
score -= 10000.0 score -= 10000.0
@@ -478,17 +497,7 @@ class BestBattleSnake(TemplateSnake):
] ]
return random.choice(top_moves), scores return random.choice(top_moves), scores
def _choose_constrictor_move( 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):
self,
safe_moves,
my_body,
my_len,
other_snakes,
food_set,
enemy_attack_map,
width,
height,
):
scores: dict[str, float] = {} scores: dict[str, float] = {}
move_safety: dict[str, dict[str, Any]] = {} move_safety: dict[str, dict[str, Any]] = {}
@@ -502,6 +511,7 @@ class BestBattleSnake(TemplateSnake):
other_snakes=other_snakes, other_snakes=other_snakes,
food_set=food_set, food_set=food_set,
is_constrictor=True, is_constrictor=True,
enemy_can_grow_cache=enemy_can_grow_cache,
) )
blocked.discard(point) blocked.discard(point)
@@ -513,9 +523,7 @@ class BestBattleSnake(TemplateSnake):
) )
territory = self._territory_control_score( territory = self._territory_control_score(
my_start=point, my_start=point,
enemy_starts=[ enemy_starts=enemy_heads,
(snake["head"]["x"], snake["head"]["y"]) for snake in other_snakes
],
blocked=blocked, blocked=blocked,
width=width, width=width,
height=height, height=height,
@@ -583,16 +591,14 @@ class BestBattleSnake(TemplateSnake):
] ]
return random.choice(top_moves), scores return random.choice(top_moves), scores
def _legal_moves( def _legal_moves(self, my_head, my_body, other_snakes, food_set, is_constrictor, width, height):
self, my_head, my_body, other_snakes, food_set, is_constrictor, width, height
):
occupied = self._occupied_cells(my_body, other_snakes) occupied = self._occupied_cells(my_body, other_snakes)
own_tail = (my_body[-1]["x"], my_body[-1]["y"]) own_tail = (my_body[-1]["x"], my_body[-1]["y"])
own_tail_stacked = self._is_tail_stacked(my_body) own_tail_stacked = self._is_tail_stacked(my_body)
safe_moves = {} safe_moves = {}
for move, pos in self.get_possible_moves(my_head).items(): for move, (dx, dy) in self.DIRECTIONS.items():
point = (pos["x"], pos["y"]) point = (my_head["x"] + dx, my_head["y"] + dy)
if not self._in_bounds(point, width, height): if not self._in_bounds(point, width, height):
continue continue
@@ -608,7 +614,7 @@ class BestBattleSnake(TemplateSnake):
if point in occupied and not can_step_on_tail: if point in occupied and not can_step_on_tail:
continue continue
safe_moves[move] = pos safe_moves[move] = {"x": point[0], "y": point[1]}
return safe_moves return safe_moves
@@ -618,7 +624,7 @@ class BestBattleSnake(TemplateSnake):
occupied |= {(segment["x"], segment["y"]) for segment in snake["body"]} occupied |= {(segment["x"], segment["y"]) for segment in snake["body"]}
return occupied 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} blocked = {(segment["x"], segment["y"]) for segment in future_body}
if not is_constrictor and not self._is_tail_stacked(future_body): if not is_constrictor and not self._is_tail_stacked(future_body):
@@ -632,7 +638,13 @@ class BestBattleSnake(TemplateSnake):
if is_constrictor: if is_constrictor:
continue 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 continue
if self._is_tail_stacked(snake["body"]): if self._is_tail_stacked(snake["body"]):
@@ -643,20 +655,25 @@ class BestBattleSnake(TemplateSnake):
return blocked return blocked
def _build_enemy_attack_map( def _build_enemy_attack_map(self, my_snake, other_snakes, food_set, is_constrictor, width, height, enemy_can_grow_cache=None):
self, my_snake, other_snakes, food_set, is_constrictor, width, height
):
occupied = self._occupied_cells(my_snake["body"], other_snakes) 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 = {} attack_map = {}
for enemy in other_snakes: for enemy in other_snakes:
enemy_len = enemy.get("length", len(enemy["body"])) enemy_len = enemy.get("length", len(enemy["body"]))
enemy_tail = (enemy["body"][-1]["x"], enemy["body"][-1]["y"]) enemy_tail = (enemy["body"][-1]["x"], enemy["body"][-1]["y"])
enemy_tail_stacked = self._is_tail_stacked(enemy["body"]) 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(): enemy_head = enemy["head"]
point = (pos["x"], pos["y"]) for dx, dy in self.DIRECTIONS.values():
point = (enemy_head["x"] + dx, enemy_head["y"] + dy)
if not self._in_bounds(point, width, height): if not self._in_bounds(point, width, height):
continue continue
@@ -664,18 +681,14 @@ class BestBattleSnake(TemplateSnake):
not is_constrictor not is_constrictor
and point == enemy_tail and point == enemy_tail
and not enemy_tail_stacked 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: if point in occupied and not can_step_on_enemy_tail:
continue continue
# Do not consider impossible overlap directly into my own occupied body except head swap possibilities. # Do not consider impossible overlap directly into my own occupied body except head swap possibilities.
if point in { if point in my_body_points:
(segment["x"], segment["y"])
for segment in my_snake["body"]
if my_snake["id"] == my_id
}:
continue continue
previous = attack_map.get(point) previous = attack_map.get(point)
@@ -694,9 +707,7 @@ class BestBattleSnake(TemplateSnake):
next_body.pop() next_body.pop()
return next_body return next_body
def _can_step_on_own_tail( def _can_step_on_own_tail(self, point, own_tail, own_tail_is_stacked, ate_food, is_constrictor):
self, point, own_tail, own_tail_is_stacked, ate_food, is_constrictor
):
if is_constrictor: if is_constrictor:
return False return False
if ate_food: if ate_food:
@@ -717,17 +728,31 @@ class BestBattleSnake(TemplateSnake):
return True return True
return False return False
def _nearest_food_distance(self, start, foods, blocked, width, height): def _hazard_damage_per_turn(self, game_data):
if not foods: ruleset = {}
return None 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)]) queue = deque([(start, 0)])
seen = {start} seen = {start}
while queue: while queue:
point, distance = queue.popleft() point, distance = queue.popleft()
if point in targets: if point in food_set:
return distance return distance
for neighbor in self._neighbors(point): for neighbor in self._neighbors(point):
@@ -735,7 +760,7 @@ class BestBattleSnake(TemplateSnake):
continue continue
if not self._in_bounds(neighbor, width, height): if not self._in_bounds(neighbor, width, height):
continue continue
if neighbor in blocked and neighbor not in targets: if neighbor in blocked and neighbor not in food_set:
continue continue
seen.add(neighbor) seen.add(neighbor)
queue.append((neighbor, distance + 1)) queue.append((neighbor, distance + 1))
@@ -797,8 +822,8 @@ class BestBattleSnake(TemplateSnake):
next_head = future_body[0] next_head = future_body[0]
count = 0 count = 0
for pos in self.get_possible_moves(next_head).values(): for dx, dy in self.DIRECTIONS.values():
point = (pos["x"], pos["y"]) point = (next_head["x"] + dx, next_head["y"] + dy)
if not self._in_bounds(point, width, height): if not self._in_bounds(point, width, height):
continue continue
if point in blocked: if point in blocked:
@@ -807,6 +832,8 @@ class BestBattleSnake(TemplateSnake):
return count return count
def _revisit_penalty(self, point): def _revisit_penalty(self, point):
if not self.recent_heads:
return 0.0
penalty = 0.0 penalty = 0.0
for index, old_point in enumerate(reversed(self.recent_heads), start=1): for index, old_point in enumerate(reversed(self.recent_heads), start=1):
if old_point != point: if old_point != point:
@@ -867,6 +894,15 @@ class BestBattleSnake(TemplateSnake):
return distances 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): def _neighbors(self, point):
for dx, dy in self.DIRECTIONS.values(): for dx, dy in self.DIRECTIONS.values():
yield (point[0] + dx, point[1] + dy) yield (point[0] + dx, point[1] + dy)
@@ -878,8 +914,8 @@ class BestBattleSnake(TemplateSnake):
return 0 <= point[0] < width and 0 <= point[1] < height return 0 <= point[0] < width and 0 <= point[1] < height
def _fallback_move(self, head, width, height): def _fallback_move(self, head, width, height):
for move, pos in self.get_possible_moves(head).items(): for move, (dx, dy) in self.DIRECTIONS.items():
point = (pos["x"], pos["y"]) point = (head["x"] + dx, head["y"] + dy)
if self._in_bounds(point, width, height): if self._in_bounds(point, width, height):
return move return move
return "up" return "up"
+109
View File
@@ -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()
+147
View File
@@ -328,6 +328,84 @@ class TestBestBattleSnake(unittest.TestCase):
move = make_board(game_state).snake_neat_make_a_move() move = make_board(game_state).snake_neat_make_a_move()
self.assertNotEqual(move, "up") 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): def test_constrictor_avoids_growth_dead_end(self):
game_state = { game_state = {
"game": { "game": {
@@ -393,6 +471,75 @@ class TestBestBattleSnake(unittest.TestCase):
move = make_board(game_state).snake_neat_make_a_move() move = make_board(game_state).snake_neat_make_a_move()
self.assertEqual(move, "up") 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__": if __name__ == "__main__":
unittest.main() unittest.main()