Skip to content

Commit 47708a4

Browse files
committed
Add Bomberland arena
1 parent 7fcc206 commit 47708a4

13 files changed

Lines changed: 999 additions & 1 deletion

File tree

README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,11 @@ The winner is the LM agent who wins the most rounds.
9898
## 🧩 Available Arenas
9999

100100
CodeClash includes competitive programming games and simulation-backed arenas, including BattleSnake,
101-
CoreWar, CybORG, Halite, HuskyBench, RoboCode, RobotRumble, and SCML.
101+
Bomberland, CoreWar, CybORG, Halite, HuskyBench, RoboCode, RobotRumble, and SCML.
102+
103+
Bomberland is a Bomberman-style grid arena based on Coder One's Bomberland competition. Agents edit
104+
a Python `bomberland_agent.py` implementation and compete to maximize average score across seeded
105+
simulations through survival, damage, kills, and destructible-block control.
102106

103107
SCML is a supply-chain negotiation arena based on the ANAC Supply Chain Management League OneShot
104108
track. Agents edit a Python `scml_agent.py` implementation and compete to maximize average profit

codeclash/arenas/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from codeclash.arenas.battlecode24.battlecode24 import BattleCode24Arena
44
from codeclash.arenas.battlecode25.battlecode25 import BattleCode25Arena
55
from codeclash.arenas.battlesnake.battlesnake import BattleSnakeArena
6+
from codeclash.arenas.bomberland.bomberland import BomberlandArena
67
from codeclash.arenas.bridge.bridge import BridgeArena
78
from codeclash.arenas.chess.chess import ChessArena
89
from codeclash.arenas.corewar.corewar import CoreWarArena
@@ -23,6 +24,7 @@
2324
BattleCode24Arena,
2425
BattleCode25Arena,
2526
BattleSnakeArena,
27+
BomberlandArena,
2628
BridgeArena,
2729
ChessArena,
2830
CoreWarArena,
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
FROM python:3.11-slim-bookworm
2+
3+
ARG BOMBERLAND_COMMIT=8b6b7a1c013d96feb0a5468a7a59a63a7c59dadc
4+
ENV BOMBERLAND_UPSTREAM_COMMIT=${BOMBERLAND_COMMIT}
5+
6+
RUN apt-get update \
7+
&& apt-get install -y --no-install-recommends git ca-certificates \
8+
&& rm -rf /var/lib/apt/lists/*
9+
10+
# Keep a pinned copy of the upstream competition source for provenance and
11+
# agent authors who want to inspect the original starter-kit shape.
12+
RUN git clone https://github.com/CoderOneHQ/bomberland.git /opt/bomberland \
13+
&& cd /opt/bomberland \
14+
&& git checkout ${BOMBERLAND_COMMIT}
15+
16+
WORKDIR /workspace
17+
COPY codeclash/arenas/bomberland/runtime/ /workspace/
18+
19+
RUN git init \
20+
&& git config user.email "arena@codeclash.com" \
21+
&& git config user.name "CodeClash Arena" \
22+
&& git add . \
23+
&& git commit -m "Initialize Bomberland runtime"
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from codeclash.arenas.bomberland.bomberland import BomberlandArena
2+
3+
__all__ = ["BomberlandArena"]
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
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 = "bomberland_results.json"
11+
CRASH_SCORE = -1_000_000.0
12+
13+
14+
class BomberlandArena(CodeArena):
15+
name: str = "Bomberland"
16+
submission: str = "bomberland_agent.py"
17+
description: str = """Bomberland is a Bomberman-style multi-agent arena based on Coder One's Bomberland competition.
18+
19+
Your bot is a Python file named `bomberland_agent.py` that defines a callable named `next_actions`.
20+
The callable receives a game-state dictionary and should return a dictionary mapping unit ids to actions:
21+
22+
def next_actions(game_state):
23+
return {"unit_0": "up"}
24+
25+
Valid actions are `up`, `down`, `left`, `right`, `bomb`, and `stay`. Each round runs several
26+
deterministic seeded games. Your units move on a destructible grid, place bombs, destroy blocks,
27+
damage opposing units, and score by survival, damage, kills, and block destruction.
28+
"""
29+
default_args: dict = {
30+
"sims_per_round": 3,
31+
"ticks": 80,
32+
"width": 11,
33+
"height": 11,
34+
"unit_count": 3,
35+
"agent_timeout": 0.25,
36+
"timeout": 180,
37+
}
38+
39+
def _game_arg(self, key: str):
40+
nested_args = self.game_config.get("args", {})
41+
return nested_args.get(key, self.game_config.get(key, self.default_args[key]))
42+
43+
def _sims_per_round(self) -> int:
44+
return int(self._game_arg("sims_per_round"))
45+
46+
def validate_code(self, agent: Player) -> tuple[bool, str | None]:
47+
quoted_submission = shlex.quote(self.submission)
48+
file_check = agent.environment.execute(f"test -f {quoted_submission} && echo exists")
49+
if "exists" not in file_check["output"]:
50+
return False, f"Submission file `{self.submission}` not found in the workspace root"
51+
52+
content = agent.environment.execute(f"cat {quoted_submission}")["output"]
53+
if not content.strip():
54+
return False, f"`{self.submission}` is empty"
55+
56+
syntax_check = agent.environment.execute(f"python -m py_compile {quoted_submission}")
57+
if syntax_check["returncode"] != 0:
58+
return False, f"Python syntax error in `{self.submission}`:\n{syntax_check['output']}"
59+
60+
import_check = agent.environment.execute(
61+
"python - <<'PY'\n"
62+
"import importlib.util\n"
63+
f"spec = importlib.util.spec_from_file_location('submission_agent', {self.submission!r})\n"
64+
"module = importlib.util.module_from_spec(spec)\n"
65+
"spec.loader.exec_module(module)\n"
66+
"assert hasattr(module, 'next_actions'), 'next_actions callable not found'\n"
67+
"assert callable(module.next_actions), 'next_actions must be callable'\n"
68+
"state = {\n"
69+
" 'connection': {'agent_id': 'Alice'},\n"
70+
" 'agents': {'Alice': {'unit_ids': ['u0']}},\n"
71+
" 'unit_state': {'u0': {'agent_id': 'Alice', 'hp': 3, 'coordinates': [1, 1]}},\n"
72+
" 'entities': [],\n"
73+
" 'world': {'width': 5, 'height': 5},\n"
74+
" 'tick': 0,\n"
75+
"}\n"
76+
"result = module.next_actions(state)\n"
77+
"assert result is None or isinstance(result, dict), 'next_actions must return a dict or None'\n"
78+
"PY"
79+
)
80+
if import_check["returncode"] != 0:
81+
return False, f"Could not import or call `next_actions` from `{self.submission}`:\n{import_check['output']}"
82+
83+
return True, None
84+
85+
def execute_round(self, agents: list[Player]) -> None:
86+
agent_args = []
87+
for agent in agents:
88+
agent_args.extend(["--agent", f"{agent.name}=/{agent.name}/{self.submission}"])
89+
90+
cmd = [
91+
"python",
92+
"run_bomberland.py",
93+
"--sims",
94+
str(self._sims_per_round()),
95+
"--ticks",
96+
str(self._game_arg("ticks")),
97+
"--width",
98+
str(self._game_arg("width")),
99+
"--height",
100+
str(self._game_arg("height")),
101+
"--unit-count",
102+
str(self._game_arg("unit_count")),
103+
"--agent-timeout",
104+
str(self._game_arg("agent_timeout")),
105+
"--output",
106+
str(self.log_env / RESULTS_JSON),
107+
*agent_args,
108+
]
109+
full_cmd = " ".join(shlex.quote(part) for part in cmd)
110+
self.logger.info(f"Running game: {full_cmd}")
111+
try:
112+
response = self.environment.execute(full_cmd, timeout=int(self._game_arg("timeout")))
113+
except subprocess.TimeoutExpired as exc:
114+
raise RuntimeError("Bomberland round timed out") from exc
115+
assert_zero_exit_code(response, logger=self.logger)
116+
117+
def get_results(self, agents: list[Player], round_num: int, stats: RoundStats):
118+
result_file = self.log_round(round_num) / RESULTS_JSON
119+
if not result_file.exists():
120+
self.logger.error(f"Missing result file: {result_file}")
121+
stats.winner = RESULT_TIE
122+
for agent in agents:
123+
stats.scores[agent.name] = CRASH_SCORE
124+
stats.player_stats[agent.name].score = CRASH_SCORE
125+
return
126+
127+
with open(result_file) as f:
128+
result = json.load(f)
129+
130+
scores = {agent.name: CRASH_SCORE for agent in agents}
131+
for player, score in result.get("average_scores", {}).items():
132+
if player in scores:
133+
scores[player] = float(score)
134+
135+
stats.scores = scores
136+
stats.details = result.get("details", [])
137+
for player, score in scores.items():
138+
stats.player_stats[player].score = score
139+
140+
if not scores:
141+
stats.winner = RESULT_TIE
142+
return
143+
144+
top_score = max(scores.values())
145+
winners = [player for player, score in scores.items() if score == top_score]
146+
stats.winner = winners[0] if len(winners) == 1 else RESULT_TIE
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
__pycache__/
2+
*.pyc
3+
bomberland_results.json
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# Bomberland CodeClash Runtime
2+
3+
This runtime adapts the Coder One Bomberland competition format into a compact,
4+
deterministic CodeClash arena. The Docker image keeps a pinned checkout of
5+
`CoderOneHQ/bomberland` at `/opt/bomberland` for provenance and starter-kit
6+
reference, while `run_bomberland.py` provides the runtime used by CodeClash.
7+
8+
Submissions must provide `bomberland_agent.py` with:
9+
10+
```python
11+
def next_actions(game_state):
12+
return {"unit_0": "up"}
13+
```
14+
15+
Valid string actions are `up`, `down`, `left`, `right`, `bomb`, and `stay`.
16+
The game-state dictionary follows the upstream starter-kit shape where possible:
17+
`connection.agent_id` identifies the player, `agents[player].unit_ids` lists the
18+
controlled units, `unit_state` contains unit coordinates and health, and
19+
`entities` contains walls, destructible blocks, bombs, and blast tiles.
20+
21+
Smoke command from the repository root:
22+
23+
```bash
24+
uv run python main.py configs/examples/Bomberland__dummy__r1__s2.yaml -o /tmp/codeclash-bomberland-smoke
25+
```
26+
27+
Expected result shape:
28+
29+
```json
30+
{
31+
"average_scores": {"player_a": 330.0, "player_b": 330.0},
32+
"total_scores": {"player_a": 660.0, "player_b": 660.0},
33+
"sims": 2,
34+
"details": ["... per-simulation JSON strings ..."]
35+
}
36+
```
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
def next_actions(game_state):
2+
agent_id = game_state["connection"]["agent_id"]
3+
unit_ids = game_state["agents"].get(agent_id, {}).get("unit_ids", [])
4+
unit_state = game_state.get("unit_state", {})
5+
return {
6+
unit_id: "stay"
7+
for unit_id in unit_ids
8+
if unit_state.get(unit_id, {}).get("hp", 0) > 0
9+
}

0 commit comments

Comments
 (0)