@@ -34,7 +34,7 @@ def __init__(self, config, **kwargs):
3434 self .run_cmd_round += f" --{ arg } { val } "
3535 self ._failed_to_start_player = []
3636
37- def _wait_for_ports (self , requested_ports : list [int ], timeout : float = 60 .0 ) -> list [int ]:
37+ def _wait_for_ports (self , requested_ports : list [int ], timeout : float = 180 .0 ) -> list [int ]:
3838 """Wait for ports to be served, up to timeout seconds.
3939
4040 Returns:
@@ -84,6 +84,16 @@ def _run_single_simulation(self, player2port: dict[str, int], idx: int) -> str:
8484 )
8585 return response ["output" ]
8686
87+ def _start_cmd (self , agent : Player ) -> str :
88+ """Command to start the submission's server on $PORT. Submissions are either the
89+ Python starter (main.py) or any other language via a run.sh launch script, which
90+ compiles from source and starts the server (we commit source, never binaries).
91+ run.sh runs from the copied codebase, so slow compiles are covered by the
92+ (generous) port-wait timeout rather than a separate build step."""
93+ if "run.sh" in self .environment .execute (f"ls /{ agent .name } " )["output" ]:
94+ return "bash run.sh"
95+ return f"python { self .submission } "
96+
8797 def execute_round (self , agents : list [Player ]):
8898 self ._failed_to_start_player = []
8999 assert len (agents ) > 1 , "Battlesnake requires at least two players"
@@ -92,9 +102,9 @@ def execute_round(self, agents: list[Player]):
92102 for idx , agent in enumerate (agents ):
93103 port = 8001 + idx
94104 player2port [agent .name ] = port
95- # Surprisingly slow despite using &
96- # Start server in background - just add & to run in background!
97- self .environment .execute (f"PORT={ port } python { self .submission } &" , cwd = f"/{ agent .name } " )
105+ # Start server in background (& ). Submission may be Python (main.py) or any
106+ # other language via a run.sh launch script.
107+ self .environment .execute (f"PORT={ port } { self ._start_cmd ( agent ) } &" , cwd = f"/{ agent .name } " )
98108
99109 self .logger .debug (f"Waiting for ports: { player2port } " )
100110 available_ports = self ._wait_for_ports (list (player2port .values ()))
@@ -129,8 +139,12 @@ def execute_round(self, agents: list[Player]):
129139 for future in tqdm (as_completed (futures ), total = len (futures )):
130140 future .result ()
131141 finally :
132- # Kill all python servers when done
142+ # Kill all servers started this round (any language) so ports free up for the
143+ # next round. pkill covers the Python starter; fuser frees each game port for
144+ # compiled/interpreted servers launched via run.sh.
133145 self .environment .execute (f"pkill -f 'python { self .submission } ' || true" )
146+ for port in player2port .values ():
147+ self .environment .execute (f"fuser -k { port } /tcp 2>/dev/null || true" )
134148
135149 def get_results (self , agents : list [Player ], round_num : int , stats : RoundStats ):
136150 scores = defaultdict (int )
@@ -163,7 +177,13 @@ def get_results(self, agents: list[Player], round_num: int, stats: RoundStats):
163177 stats .player_stats [player ].score = score
164178
165179 def validate_code (self , agent : Player ) -> tuple [bool , str | None ]:
166- if self .submission not in agent .environment .execute ("ls" )["output" ]:
180+ listing = agent .environment .execute ("ls" )["output" ]
181+ # Non-Python submissions declare how to launch their server via run.sh (any
182+ # language). We trust the launch script here; a broken one is caught at runtime
183+ # by _wait_for_ports (failed-to-start -> forfeit).
184+ if "run.sh" in listing :
185+ return True , None
186+ if self .submission not in listing :
167187 return False , f"No { self .submission } file found in the root directory"
168188 # note: no longer calling splitlines
169189 bot_content = agent .environment .execute (f"cat { self .submission } " )["output" ]
0 commit comments