| 15 | |
| 16 | |
| 17 | class BattleCode25Arena(CodeArena): |
| 18 | name: str = "BattleCode25" |
| 19 | description: str = """BattleCode 2025 throws you into a real-time strategy showdown where your Python bot pilots a team of specialized robots—Soldiers, Moppers, Splashers—alongside towers that spawn units or generate resources. |
| 20 | Your mission: paint over 70% of the map (or eliminate the enemy) by coordinating cleanups, area cover, and tower-building through tight bytecode budgets and clever unit synergy.""" |
| 21 | default_args: dict = { |
| 22 | "maps": "quack", |
| 23 | } |
| 24 | submission: str = "src/mysubmission" |
| 25 | |
| 26 | def __init__(self, config, **kwargs): |
| 27 | super().__init__(config, **kwargs) |
| 28 | assert len(config["players"]) == 2, "BattleCode25 is a two-player game" |
| 29 | self.run_cmd_round: str = "python run.py run" |
| 30 | for arg, val in self.game_config.get("args", self.default_args).items(): |
| 31 | if isinstance(val, bool): |
| 32 | if val: |
| 33 | self.run_cmd_round += f" --{arg}" |
| 34 | else: |
| 35 | self.run_cmd_round += f" --{arg} {val}" |
| 36 | |
| 37 | def _run_single_simulation(self, agents: list[Player], idx: int, cmd: str) -> str: |
| 38 | try: |
| 39 | response = self.environment.execute(cmd + f" > {self.log_env / BC_LOG.format(idx=idx)}") |
| 40 | except subprocess.TimeoutExpired: |
| 41 | self.logger.warning(f"BattleCode simulation {idx} timed out: {cmd}") |
| 42 | return "" |
| 43 | if response["returncode"] != 0: |
| 44 | self.logger.warning( |
| 45 | f"BattleCode simulation {idx} failed with exit code {response['returncode']}:\n{response['output']}" |
| 46 | ) |
| 47 | return response["output"] |
| 48 | |
| 49 | def execute_round(self, agents: list[Player]): |
| 50 | for agent in agents: |
| 51 | src, dest = f"/{agent.name}/src/{BC_FOLDER}/", str(DIR_WORK / "src" / agent.name) |
| 52 | self.environment.execute(f"rm -rf {dest}; cp -r {src} {dest}") |
| 53 | args = [f"--p{idx + 1}-dir src --p{idx + 1} {agent.name}" for idx, agent in enumerate(agents)] |
| 54 | cmd = f"{self.run_cmd_round} {' '.join(args)}" |
| 55 | self.logger.info(f"Running game: {cmd}") |
| 56 | |
| 57 | with ThreadPoolExecutor(self.game_config.get("sim_concurrency", 5)) as executor: |
| 58 | # Submit all simulations to the thread pool |
| 59 | futures = [ |
| 60 | executor.submit(self._run_single_simulation, agents, idx, cmd) |
| 61 | for idx in range(self.game_config["sims_per_round"]) |
| 62 | ] |
| 63 | # Collect results as they complete |
| 64 | for future in tqdm(as_completed(futures), total=len(futures), desc="Simulations"): |
| 65 | future.result() |
| 66 | |
| 67 | def get_results(self, agents: list[Player], round_num: int, stats: RoundStats): |
| 68 | scores = defaultdict(int) |
| 69 | for idx in range(self.game_config["sims_per_round"]): |
| 70 | with open(self.log_round(round_num) / BC_LOG.format(idx=idx)) as f: |
| 71 | lines = f.read().strip().split("\n") |
| 72 | if len(lines) < 3: |
| 73 | # Game likely crashed, skip this simulation |
| 74 | continue |
nothing calls this directly
no outgoing calls
no test coverage detected