| 11 | COREWAR_LOG = "sim_{idx}.log" |
| 12 | TRACE_RAW = "trace_raw.txt" |
| 13 | |
| 14 | |
| 15 | class CoreWarArena(CodeArena): |
| 16 | name: str = "CoreWar" |
| 17 | description: str = """CoreWar is a programming battle where you write "warriors" in an assembly-like language called Redcode to compete within a virtual machine (MARS), aiming to eliminate your rivals by making their code self-terminate. |
| 18 | Victory comes from crafting clever tactics—replicators, scanners, bombers—that exploit memory layout and instruction timing to control the core. |
| 19 | |
| 20 | Reading the logs: each round's score is computed over many simulated battles, but only a sample of them are saved as replay traces (`sim_*.jsonl`) in `/logs/` due to storage limits. The score reflects every battle played, not just the replayed subset, so treat the replays as representative examples rather than the full basis of the result.""" |
| 21 | submission: str = "warrior.red" |
| 22 | |
| 23 | def __init__(self, config, **kwargs): |
| 24 | super().__init__(config, **kwargs) |
| 25 | # Always run in brief mode (`-b`). Without it, pmars prints disassembled listing of |
| 26 | # every warrior, which leaks opponent |
| 27 | self.run_cmd_round: str = "./src/pmars -b" |
| 28 | for arg, val in self.game_config.get("args", self.default_args).items(): |
| 29 | if isinstance(val, bool): |
| 30 | if val: |
| 31 | self.run_cmd_round += f" -{arg}" |
| 32 | else: |
| 33 | self.run_cmd_round += f" -{arg} {val}" |
| 34 | |
| 35 | def _record_count(self) -> int: |
| 36 | # Bounded subset of scored battles to record for replay (recording is the costly part). |
| 37 | sims = self.game_config["sims_per_round"] |
| 38 | return max(1, min(sims, self.game_config.get("record_battles", 100))) |
| 39 | |
| 40 | def _run_single_simulation(self, agents: list[Player], idx: int): |
| 41 | # Shift agents by idx to vary starting positions |
| 42 | agents = agents[idx:] + agents[:idx] |
| 43 | args = [f"/{agent.name}/{self.submission}" for agent in agents] |
| 44 | n = self.game_config["sims_per_round"] |
| 45 | log = self.log_env / COREWAR_LOG.format(idx=idx) |
| 46 | if idx == 0: |
| 47 | # Record R scored battles with -T + the rest plain; both score blocks append to one |
| 48 | # log (get_results sums them), so replays are genuine scored battles. i == agents[i]. |
| 49 | r = self._record_count() |
| 50 | self._trace_agent_names = [agent.name for agent in agents] |
| 51 | parts = [f"{self.run_cmd_round} {shlex.join(args)} -r {r} -T {self.log_env / TRACE_RAW} >> {log}"] |
| 52 | if n - r > 0: |
| 53 | parts.append(f"{self.run_cmd_round} {shlex.join(args)} -r {n - r} >> {log}") |
| 54 | cmd = f"rm -f {log}; " + "; ".join(parts) + ";" |
| 55 | else: |
| 56 | cmd = f"{self.run_cmd_round} {shlex.join(args)} -r {n} > {log};" |
| 57 | self.logger.info(f"Running game: {cmd}") |
| 58 | response = self.environment.execute(cmd) |
| 59 | assert response["returncode"] == 0, response |
| 60 | |
| 61 | def execute_round(self, agents: list[Player]): |
| 62 | with ThreadPoolExecutor(self.game_config.get("sim_concurrency", 4)) as executor: |
| 63 | futures = [executor.submit(self._run_single_simulation, agents, idx) for idx in range(len(agents))] |
| 64 | for future in as_completed(futures): |
| 65 | future.result() |
| 66 | |
| 67 | def copy_logs_from_env(self, round_num: int) -> None: |
| 68 | # Distill the -T battles into sim_{i}.jsonl + trace.md on the host, then drop the raw stream. |
| 69 | super().copy_logs_from_env(round_num) |
| 70 | raw = self.log_round(round_num) / TRACE_RAW |
nothing calls this directly
no outgoing calls
no test coverage detected