Skip to content

Commit 76ae557

Browse files
committed
Fixes for battlesnake error hardening
1 parent a4a409b commit 76ae557

1 file changed

Lines changed: 47 additions & 19 deletions

File tree

codeclash/games/battlesnake/battlesnake.py

Lines changed: 47 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -23,21 +23,30 @@ def __init__(self, config, *, tournament_id: str, local_output_dir: Path):
2323
self.run_cmd_round += f" --{arg}"
2424
else:
2525
self.run_cmd_round += f" --{arg} {val}"
26+
self._failed_to_start_player = []
2627

27-
def _wait_for_ports(self, ports: list[int], timeout: float = 3.0) -> None:
28-
"""Wait for all ports to be available, up to timeout seconds."""
28+
def _wait_for_ports(self, requested_ports: list[int], timeout: float = 3.0) -> list[int]:
29+
"""Wait for ports to be served, up to timeout seconds.
30+
31+
Returns:
32+
List of ports that are actually served after timeout.
33+
"""
2934
start_time = time.time()
35+
available_ports = set()
36+
3037
while time.time() - start_time < timeout:
31-
for port in ports:
32-
result = self.environment.execute(f"nc -z 0.0.0.0 {port}")
33-
if result["returncode"] != 0:
34-
break
35-
else:
36-
# All ports are ready (loop completed without break)
37-
return
38+
for port in set(requested_ports) - available_ports:
39+
result = self.environment.execute(f"wget -S --spider --timeout=1 http://localhost:{port}/ 2>&1")
40+
if result["returncode"] == 0 or "200 OK" in result["output"] or "HTTP/" in result["output"]:
41+
available_ports.add(port)
42+
43+
if len(available_ports) == len(requested_ports):
44+
return list(available_ports)
3845

3946
time.sleep(0.1)
4047

48+
return list(available_ports)
49+
4150
def _run_single_simulation(self, cmd: str, idx: int) -> tuple[str, str]:
4251
"""Run a single battlesnake simulation and return log and result outputs."""
4352
assert_zero_exit_code(
@@ -50,17 +59,28 @@ def _run_single_simulation(self, cmd: str, idx: int) -> tuple[str, str]:
5059
def execute_round(self, agents: list[Player]):
5160
self.logger.debug("Starting game servers")
5261
cmd = []
53-
ports = []
62+
player2port = {}
5463
for idx, agent in enumerate(agents):
5564
port = 8001 + idx
56-
ports.append(port)
65+
player2port[agent.name] = port
5766
# Surprisingly slow despite using &
5867
# Start server in background - just add & to run in background!
5968
self.environment.execute(f"PORT={port} python main.py &", cwd=f"/{agent.name}")
6069
cmd.append(f"--url http://0.0.0.0:{port} -n {agent.name}")
6170

62-
self.logger.debug(f"Waiting for ports: {ports}")
63-
self._wait_for_ports(ports)
71+
self.logger.debug(f"Waiting for ports: {player2port}")
72+
available_ports = self._wait_for_ports(list(player2port.values()))
73+
74+
if not available_ports:
75+
raise RuntimeError("All games failed to start")
76+
77+
if len(available_ports) == 1:
78+
missing_ports = set(player2port.values()) - set(available_ports)
79+
player = next(player for player, port in player2port.items() if port in missing_ports)
80+
self.logger.warning(f"Player {player} failed to start")
81+
self._failed_to_start_player.append(player)
82+
return
83+
6484
self.logger.debug("All ports are ready")
6585

6686
try:
@@ -84,12 +104,20 @@ def execute_round(self, agents: list[Player]):
84104

85105
def get_results(self, agents: list[Player], round_num: int, stats: RoundStats):
86106
scores = {}
87-
for idx in range(self.game_config["sims_per_round"]):
88-
with open(self.log_round(round_num) / f"sim_{idx}.jsonl") as f:
89-
lines = f.read().strip().split("\n")
90-
results = json.loads(lines[-1]) # Get the last line which contains the game result
91-
winner = RESULT_TIE if results["isDraw"] else results["winnerName"]
92-
scores[winner] = scores.get(winner, 0) + 1
107+
available_players = [player.name for player in agents if player.name not in self._failed_to_start_player]
108+
if len(available_players) > 1:
109+
# We ran the game
110+
for idx in range(self.game_config["sims_per_round"]):
111+
with open(self.log_round(round_num) / f"sim_{idx}.jsonl") as f:
112+
lines = f.read().strip().split("\n")
113+
results = json.loads(lines[-1]) # Get the last line which contains the game result
114+
winner = RESULT_TIE if results["isDraw"] else results["winnerName"]
115+
scores[winner] = scores.get(winner, 0) + 1
116+
else:
117+
self.logger.warning(f"Only one player ({available_players[0]}) started, giving them the win")
118+
# We didn't run a game, so we just give the one player the win
119+
available_player = available_players[0]
120+
scores = {available_player: self.game_config["sims_per_round"]}
93121

94122
winner = max(scores, key=scores.get)
95123
winner = RESULT_TIE if list(scores.values()).count(scores[winner]) > 1 else winner

0 commit comments

Comments
 (0)