Skip to content

Commit 2a24a53

Browse files
committed
Add CybORG arena
1 parent a66d63e commit 2a24a53

19 files changed

Lines changed: 644 additions & 4 deletions

File tree

.github/mlc_config.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,15 @@
1818
{
1919
"pattern": "https://docs\\.codeclash\\.io"
2020
},
21+
{
22+
"pattern": "https://robotrumble\\.org/boards/2"
23+
},
24+
{
25+
"pattern": "https://robocode\\.sourceforge\\.io.*"
26+
},
27+
{
28+
"pattern": "https://huskybench\\.com/.*"
29+
},
2130
{
2231
"pattern": "https?://(.*\\.)?twitter\\.com/.*"
2332
},
@@ -26,6 +35,9 @@
2635
},
2736
{
2837
"pattern": "https://www\\.contributor-covenant\\.org/version/2/1/code_of_conduct\\.html"
38+
},
39+
{
40+
"pattern": "https://join\\.slack\\.com/t/swe-bench/shared_invite/.*"
2941
}
3042
]
3143
}

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,15 @@ Critically, *LMs don't play the game directly*.
9595
Their code serves as their competitive proxy.
9696
The winner is the LM agent who wins the most rounds.
9797

98+
## 🧩 Available Arenas
99+
100+
CodeClash includes competitive programming games and simulation-backed arenas, including BattleSnake,
101+
CoreWar, CybORG, Halite, HuskyBench, RoboCode, and RobotRumble.
102+
103+
CybORG is a simulated cyber-defense arena based on the CAGE Challenge 3 DroneSwarm scenario. Agents
104+
edit a Python `cyborg_agent.py` implementation and compete to maximize blue-team reward across
105+
simulated episodes.
106+
98107
## 🚀 Get Involved
99108

100109
- Check out our [docs](https://docs.codeclash.ai/) for more details on running different arenas, configuring tournaments, etc.

codeclash/arenas/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from codeclash.arenas.bridge.bridge import BridgeArena
77
from codeclash.arenas.chess.chess import ChessArena
88
from codeclash.arenas.corewar.corewar import CoreWarArena
9+
from codeclash.arenas.cyborg.cyborg import CybORGArena
910
from codeclash.arenas.dummy.dummy import DummyArena
1011
from codeclash.arenas.figgie.figgie import FiggieArena
1112
from codeclash.arenas.gomoku.gomoku import GomokuArena
@@ -24,6 +25,7 @@
2425
BridgeArena,
2526
ChessArena,
2627
CoreWarArena,
28+
CybORGArena,
2729
DummyArena,
2830
FiggieArena,
2931
GomokuArena,
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
FROM python:3.11-slim-bookworm
2+
3+
ENV DEBIAN_FRONTEND=noninteractive \
4+
PYTHONDONTWRITEBYTECODE=1 \
5+
PIP_NO_CACHE_DIR=1
6+
7+
RUN apt-get update \
8+
&& apt-get install -y --no-install-recommends \
9+
ca-certificates git build-essential jq \
10+
&& rm -rf /var/lib/apt/lists/*
11+
12+
RUN python -m pip install --upgrade pip \
13+
&& git clone https://github.com/cage-challenge/CybORG.git /opt/CybORG \
14+
&& cd /opt/CybORG \
15+
&& git checkout a2d03f99e587af153ae0ac50fb94ba6272e4fff2 \
16+
&& python -m pip install "numpy<1.24" -e /opt/CybORG
17+
18+
WORKDIR /workspace
19+
20+
COPY codeclash/arenas/cyborg/runtime/ /workspace/
21+
22+
RUN git init \
23+
&& git config user.email "player@codeclash.com" \
24+
&& git config user.name "Player" \
25+
&& git add . \
26+
&& git commit -m "Initial CybORG workspace"
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from codeclash.arenas.cyborg.cyborg import CybORGArena
2+
3+
__all__ = ["CybORGArena"]

codeclash/arenas/cyborg/cyborg.py

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import json
2+
import shlex
3+
import subprocess
4+
5+
from codeclash.agents.player import Player
6+
from codeclash.arenas.arena import CodeArena, RoundStats
7+
from codeclash.constants import RESULT_TIE
8+
from codeclash.utils.environment import assert_zero_exit_code
9+
10+
RESULTS_JSON = "cyborg_results.json"
11+
12+
13+
class CybORGArena(CodeArena):
14+
name: str = "CybORG"
15+
submission: str = "cyborg_agent.py"
16+
description: str = """CybORG is a simulated cyber-defense arena based on the CAGE Challenge 3 DroneSwarm scenario.
17+
18+
Your bot is a Python file named `cyborg_agent.py` that defines a class named `MyAgent`.
19+
`MyAgent` should inherit from a CybORG BaseAgent-compatible class, for example:
20+
21+
from CybORG.Agents import RandomAgent
22+
23+
class MyAgent(RandomAgent):
24+
...
25+
26+
Each round evaluates every submitted agent independently on the same seeded DroneSwarm episodes.
27+
Your agent controls the blue-team drone agents through CybORG's simulated PettingZoo interface.
28+
The objective is to maximize average episode reward. This arena uses CybORG simulation only and does
29+
not run real exploit tools or interact with external networks.
30+
"""
31+
default_args: dict = {
32+
"steps_per_episode": 30,
33+
"num_drones": 18,
34+
"timeout": 240,
35+
}
36+
37+
def _game_arg(self, key: str):
38+
return self.game_config.get("args", {}).get(key, self.default_args[key])
39+
40+
def _episodes_per_round(self) -> int:
41+
return int(self.game_config.get("args", {}).get("episodes_per_round", self.game_config["sims_per_round"]))
42+
43+
def validate_code(self, agent: Player) -> tuple[bool, str | None]:
44+
quoted_submission = shlex.quote(self.submission)
45+
file_check = agent.environment.execute(f"test -f {quoted_submission} && echo exists")
46+
if "exists" not in file_check["output"]:
47+
return False, f"Submission file `{self.submission}` not found in the workspace root"
48+
49+
content = agent.environment.execute(f"cat {quoted_submission}")["output"]
50+
if not content.strip():
51+
return False, f"`{self.submission}` is empty"
52+
53+
syntax_check = agent.environment.execute(f"python -m py_compile {quoted_submission}")
54+
if syntax_check["returncode"] != 0:
55+
return False, f"Python syntax error in `{self.submission}`:\n{syntax_check['output']}"
56+
57+
import_check = agent.environment.execute(
58+
"python - <<'PY'\n"
59+
"import importlib.util\n"
60+
f"spec = importlib.util.spec_from_file_location('submission_agent', {self.submission!r})\n"
61+
"module = importlib.util.module_from_spec(spec)\n"
62+
"spec.loader.exec_module(module)\n"
63+
"assert hasattr(module, 'MyAgent'), 'MyAgent class not found'\n"
64+
"from CybORG.Agents import BaseAgent\n"
65+
"assert issubclass(module.MyAgent, BaseAgent), 'MyAgent must inherit from a CybORG BaseAgent class'\n"
66+
"PY"
67+
)
68+
if import_check["returncode"] != 0:
69+
return False, f"Could not import `MyAgent` from `{self.submission}`:\n{import_check['output']}"
70+
71+
return True, None
72+
73+
def execute_round(self, agents: list[Player]) -> None:
74+
agent_args = []
75+
for agent in agents:
76+
agent_args.extend(["--agent", f"{agent.name}=/{agent.name}/{self.submission}"])
77+
78+
cmd = [
79+
"python",
80+
"run_cyborg.py",
81+
"--episodes",
82+
str(self._episodes_per_round()),
83+
"--steps",
84+
str(self._game_arg("steps_per_episode")),
85+
"--drones",
86+
str(self._game_arg("num_drones")),
87+
"--output",
88+
str(self.log_env / RESULTS_JSON),
89+
*agent_args,
90+
]
91+
full_cmd = " ".join(shlex.quote(part) for part in cmd)
92+
self.logger.info(f"Running game: {full_cmd}")
93+
try:
94+
response = self.environment.execute(full_cmd, timeout=int(self._game_arg("timeout")))
95+
except subprocess.TimeoutExpired as exc:
96+
raise RuntimeError("CybORG round timed out") from exc
97+
assert_zero_exit_code(response, logger=self.logger)
98+
99+
def get_results(self, agents: list[Player], round_num: int, stats: RoundStats):
100+
result_file = self.log_round(round_num) / RESULTS_JSON
101+
if not result_file.exists():
102+
self.logger.error(f"Missing result file: {result_file}")
103+
stats.winner = RESULT_TIE
104+
for agent in agents:
105+
stats.scores[agent.name] = 0.0
106+
stats.player_stats[agent.name].score = 0.0
107+
return
108+
109+
with open(result_file) as f:
110+
result = json.load(f)
111+
112+
scores = {agent.name: 0.0 for agent in agents}
113+
for player, score in result.get("average_scores", {}).items():
114+
if player in scores:
115+
scores[player] = float(score)
116+
117+
stats.scores = scores
118+
stats.details = result.get("details", [])
119+
for player, score in scores.items():
120+
stats.player_stats[player].score = score
121+
122+
if not scores:
123+
stats.winner = RESULT_TIE
124+
return
125+
126+
top_score = max(scores.values())
127+
winners = [player for player, score in scores.items() if score == top_score]
128+
stats.winner = winners[0] if len(winners) == 1 else RESULT_TIE
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
__pycache__/
2+
*.py[cod]
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# CybORG CodeClash Workspace
2+
3+
Edit `cyborg_agent.py`.
4+
5+
Your file must define `MyAgent`, a CybORG `BaseAgent` subclass. A safe starting point is:
6+
7+
```python
8+
from CybORG.Agents import RandomAgent
9+
10+
11+
class MyAgent(RandomAgent):
12+
pass
13+
```
14+
15+
The arena runs simulated CAGE Challenge 3 DroneSwarm episodes and scores agents by average reward.
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
from CybORG.Agents import RandomAgent
2+
3+
4+
class MyAgent(RandomAgent):
5+
"""Baseline CybORG blue-team agent.
6+
7+
Improve this class to choose better defensive actions in the simulated DroneSwarm scenario.
8+
"""
9+
10+
pass

0 commit comments

Comments
 (0)