Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,11 @@ The winner is the LM agent who wins the most rounds.
## 🧩 Available Arenas

CodeClash includes competitive programming games and simulation-backed arenas, including BattleSnake,
CoreWar, CybORG, Halite, HuskyBench, RoboCode, RobotRumble, and SCML.
Bomberland, CoreWar, CybORG, Halite, HuskyBench, RoboCode, RobotRumble, and SCML.

Bomberland is a Bomberman-style grid arena based on Coder One's Bomberland competition. Agents edit
a Python `bomberland_agent.py` implementation and compete to maximize average score across seeded
simulations through survival, damage, kills, and destructible-block control.

SCML is a supply-chain negotiation arena based on the ANAC Supply Chain Management League OneShot
track. Agents edit a Python `scml_agent.py` implementation and compete to maximize average profit
Expand Down
2 changes: 2 additions & 0 deletions codeclash/arenas/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from codeclash.arenas.battlecode24.battlecode24 import BattleCode24Arena
from codeclash.arenas.battlecode25.battlecode25 import BattleCode25Arena
from codeclash.arenas.battlesnake.battlesnake import BattleSnakeArena
from codeclash.arenas.bomberland.bomberland import BomberlandArena
from codeclash.arenas.bridge.bridge import BridgeArena
from codeclash.arenas.chess.chess import ChessArena
from codeclash.arenas.corewar.corewar import CoreWarArena
Expand All @@ -23,6 +24,7 @@
BattleCode24Arena,
BattleCode25Arena,
BattleSnakeArena,
BomberlandArena,
BridgeArena,
ChessArena,
CoreWarArena,
Expand Down
23 changes: 23 additions & 0 deletions codeclash/arenas/bomberland/Bomberland.Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
FROM python:3.11-slim-bookworm

ARG BOMBERLAND_COMMIT=8b6b7a1c013d96feb0a5468a7a59a63a7c59dadc
ENV BOMBERLAND_UPSTREAM_COMMIT=${BOMBERLAND_COMMIT}

RUN apt-get update \
&& apt-get install -y --no-install-recommends git ca-certificates \
&& rm -rf /var/lib/apt/lists/*

# Keep a pinned copy of the upstream competition source for provenance and
# agent authors who want to inspect the original starter-kit shape.
RUN git clone https://github.com/CoderOneHQ/bomberland.git /opt/bomberland \
&& cd /opt/bomberland \
&& git checkout ${BOMBERLAND_COMMIT}

WORKDIR /workspace
COPY codeclash/arenas/bomberland/runtime/ /workspace/

RUN git init \
&& git config user.email "arena@codeclash.com" \
&& git config user.name "CodeClash Arena" \
&& git add . \
&& git commit -m "Initialize Bomberland runtime"
3 changes: 3 additions & 0 deletions codeclash/arenas/bomberland/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from codeclash.arenas.bomberland.bomberland import BomberlandArena

__all__ = ["BomberlandArena"]
161 changes: 161 additions & 0 deletions codeclash/arenas/bomberland/bomberland.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
import json
import shlex
import subprocess

from codeclash.agents.player import Player
from codeclash.arenas.arena import CodeArena, RoundStats
from codeclash.constants import RESULT_TIE
from codeclash.utils.environment import assert_zero_exit_code

RESULTS_JSON = "bomberland_results.json"
CRASH_SCORE = -1_000_000.0


class BomberlandArena(CodeArena):
name: str = "Bomberland"
submission: str = "bomberland_agent.py"
description: str = """Bomberland is a Bomberman-style multi-agent arena based on Coder One's Bomberland competition.

Your bot is a Python file named `bomberland_agent.py` that defines a callable named `next_actions`.
The callable receives a game-state dictionary and should return a dictionary mapping unit ids to actions:

def next_actions(game_state):
return {"unit_0": "up"}

Valid actions are `up`, `down`, `left`, `right`, `bomb`, and `stay`. Each round runs several
deterministic seeded games. Your units move on a destructible grid, place bombs, destroy blocks,
damage opposing units, and score by survival, damage, kills, and block destruction.
"""
default_args: dict = {
"sims_per_round": 4,
"ticks": 80,
"width": 11,
"height": 11,
"unit_count": 3,
"agent_timeout": 0.25,
"validation_timeout": 5,
"timeout": 180,
}

def __init__(self, config: dict, **kwargs):
player_count = len(config.get("players", []))
if player_count != 2:
raise ValueError("Bomberland requires exactly two players")
game_config = config.get("game", {})
game_args = game_config.get("args", {})
sims_per_round = int(
game_args.get("sims_per_round", game_config.get("sims_per_round", self.default_args["sims_per_round"]))
)
if sims_per_round % 2 != 0:
raise ValueError("Bomberland requires an even sims_per_round so both players get paired starting sides")
super().__init__(config, **kwargs)

def _game_arg(self, key: str):
nested_args = self.game_config.get("args", {})
return nested_args.get(key, self.game_config.get(key, self.default_args[key]))

def _sims_per_round(self) -> int:
return int(self._game_arg("sims_per_round"))

def validate_code(self, agent: Player) -> tuple[bool, str | None]:
quoted_submission = shlex.quote(self.submission)
file_check = agent.environment.execute(f"test -f {quoted_submission} && echo exists")
if "exists" not in file_check["output"]:
return False, f"Submission file `{self.submission}` not found in the workspace root"

content = agent.environment.execute(f"cat {quoted_submission}")["output"]
if not content.strip():
return False, f"`{self.submission}` is empty"

syntax_check = agent.environment.execute(f"python -m py_compile {quoted_submission}")
if syntax_check["returncode"] != 0:
return False, f"Python syntax error in `{self.submission}`:\n{syntax_check['output']}"

import_check = agent.environment.execute(
"python - <<'PY'\n"
"import importlib.util\n"
f"spec = importlib.util.spec_from_file_location('submission_agent', {self.submission!r})\n"
"module = importlib.util.module_from_spec(spec)\n"
"spec.loader.exec_module(module)\n"
"assert hasattr(module, 'next_actions'), 'next_actions callable not found'\n"
"assert callable(module.next_actions), 'next_actions must be callable'\n"
"state = {\n"
" 'connection': {'agent_id': 'Alice'},\n"
" 'agents': {'Alice': {'unit_ids': ['u0']}},\n"
" 'unit_state': {'u0': {'agent_id': 'Alice', 'hp': 3, 'coordinates': [1, 1]}},\n"
" 'entities': [],\n"
" 'world': {'width': 5, 'height': 5},\n"
" 'tick': 0,\n"
"}\n"
"result = module.next_actions(state)\n"
"assert result is None or isinstance(result, dict), 'next_actions must return a dict or None'\n"
"PY",
timeout=int(self._game_arg("validation_timeout")),
)
if import_check["returncode"] != 0:
return False, f"Could not import or call `next_actions` from `{self.submission}`:\n{import_check['output']}"

return True, None

def execute_round(self, agents: list[Player]) -> None:
agent_args = []
for agent in agents:
agent_args.extend(["--agent", f"{agent.name}=/{agent.name}/{self.submission}"])

cmd = [
"python",
"run_bomberland.py",
"--sims",
str(self._sims_per_round()),
"--ticks",
str(self._game_arg("ticks")),
"--width",
str(self._game_arg("width")),
"--height",
str(self._game_arg("height")),
"--unit-count",
str(self._game_arg("unit_count")),
"--agent-timeout",
str(self._game_arg("agent_timeout")),
"--output",
str(self.log_env / RESULTS_JSON),
*agent_args,
]
full_cmd = " ".join(shlex.quote(part) for part in cmd)
self.logger.info(f"Running game: {full_cmd}")
try:
response = self.environment.execute(full_cmd, timeout=int(self._game_arg("timeout")))
except subprocess.TimeoutExpired as exc:
raise RuntimeError("Bomberland round timed out") from exc
assert_zero_exit_code(response, logger=self.logger)

def get_results(self, agents: list[Player], round_num: int, stats: RoundStats):
result_file = self.log_round(round_num) / RESULTS_JSON
if not result_file.exists():
self.logger.error(f"Missing result file: {result_file}")
stats.winner = RESULT_TIE
for agent in agents:
stats.scores[agent.name] = CRASH_SCORE
stats.player_stats[agent.name].score = CRASH_SCORE
return

with open(result_file) as f:
result = json.load(f)

scores = {agent.name: CRASH_SCORE for agent in agents}
for player, score in result.get("average_scores", {}).items():
if player in scores:
scores[player] = float(score)

stats.scores = scores
stats.details = result.get("details", [])
for player, score in scores.items():
stats.player_stats[player].score = score

if not scores:
stats.winner = RESULT_TIE
return

top_score = max(scores.values())
winners = [player for player, score in scores.items() if score == top_score]
stats.winner = winners[0] if len(winners) == 1 else RESULT_TIE
3 changes: 3 additions & 0 deletions codeclash/arenas/bomberland/runtime/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
__pycache__/
*.pyc
bomberland_results.json
38 changes: 38 additions & 0 deletions codeclash/arenas/bomberland/runtime/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Bomberland CodeClash Runtime

This runtime adapts the Coder One Bomberland competition format into a compact,
deterministic CodeClash arena. The Docker image keeps a pinned checkout of
`CoderOneHQ/bomberland` at `/opt/bomberland` for provenance and starter-kit
reference, while `run_bomberland.py` provides the runtime used by CodeClash.

Submissions must provide `bomberland_agent.py` with:

```python
def next_actions(game_state):
return {"unit_0": "up"}
```

Valid string actions are `up`, `down`, `left`, `right`, `bomb`, and `stay`.
The game-state dictionary follows the upstream starter-kit shape where possible:
`connection.agent_id` identifies the player, `agents[player].unit_ids` lists the
controlled units, `unit_state` contains unit coordinates and health, and
`entities` contains walls, destructible blocks, bombs, and blast tiles.

Round simulation counts must be even so each player receives both starting sides.

Smoke command from the repository root:

```bash
uv run python main.py configs/examples/Bomberland__dummy__r1__s2.yaml -o /tmp/codeclash-bomberland-smoke
```

Expected result shape:

```json
{
"average_scores": {"player_a": 330.0, "player_b": 330.0},
"total_scores": {"player_a": 660.0, "player_b": 660.0},
"sims": 2,
"details": ["... per-simulation JSON strings ..."]
}
```
5 changes: 5 additions & 0 deletions codeclash/arenas/bomberland/runtime/bomberland_agent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
def next_actions(game_state):
agent_id = game_state["connection"]["agent_id"]
unit_ids = game_state["agents"].get(agent_id, {}).get("unit_ids", [])
unit_state = game_state.get("unit_state", {})
return {unit_id: "stay" for unit_id in unit_ids if unit_state.get(unit_id, {}).get("hp", 0) > 0}
Loading
Loading