Skip to content

Bomberland

Bomberman-style multi-agent arena based on Coder One's Bomberland competition.

Overview

Bomberland is a grid-world arena where agents control several units, move around indestructible and destructible blocks, place timed bombs, and try to outscore the opponent through survival, damage, kills, and block destruction.

The upstream Bomberland project uses a TypeScript websocket engine and starter-kit agents. The CodeClash adapter keeps a pinned upstream checkout in the Docker image for provenance and starter-kit reference, while using a compact deterministic Python runtime for CodeClash tournament execution. This avoids requiring Docker Compose inside the arena container while preserving the same core agent shape: submitted code receives a game-state dictionary and returns one action per controlled unit.

Resources

Implementation

codeclash.arenas.bomberland.bomberland.BomberlandArena

BomberlandArena(config: dict, **kwargs)

Bases: CodeArena

Source code in codeclash/arenas/bomberland/bomberland.py
43
44
45
46
47
48
49
50
51
52
53
54
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)

name class-attribute instance-attribute

name: str = 'Bomberland'

submission class-attribute instance-attribute

submission: str = 'bomberland_agent.py'

description class-attribute instance-attribute

description: str = 'Bomberland is a Bomberman-style multi-agent arena based on Coder One\'s Bomberland competition.\n\nYour bot is a Python file named `bomberland_agent.py` that defines a callable named `next_actions`.\nThe callable receives a game-state dictionary and should return a dictionary mapping unit ids to actions:\n\n    def next_actions(game_state):\n        return {"unit_0": "up"}\n\nValid actions are `up`, `down`, `left`, `right`, `bomb`, `stay`, and `detonate` (to blow up one of\nyour own bombs early, e.g. the string `"detonate:x,y"` or `{"type": "detonate", "coordinates": [x, y]}`;\nbombs also explode automatically after their timer). Each round runs several deterministic seeded\ngames. Your units move on a destructible grid, place bombs, destroy blocks, damage opposing units,\nand score by survival, damage, kills, and block destruction. Bomb blasts (`x` entities) stay active\nbriefly and damage any unit standing on or moving into them.\n'

default_args class-attribute instance-attribute

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}

validate_code

validate_code(agent: Player) -> tuple[bool, str | None]
Source code in codeclash/arenas/bomberland/bomberland.py
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
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']}"

    validation_timeout = int(self._game_arg("validation_timeout"))
    try:
        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=validation_timeout,
        )
    except subprocess.TimeoutExpired:
        return False, f"`next_actions` validation exceeded {validation_timeout}s 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

execute_round

execute_round(agents: list[Player]) -> None
Source code in codeclash/arenas/bomberland/bomberland.py
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
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)

get_results

get_results(agents: list[Player], round_num: int, stats: RoundStats)
Source code in codeclash/arenas/bomberland/bomberland.py
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
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
            stats.details.append(
                json.dumps(
                    {
                        "player": agent.name,
                        "score": CRASH_SCORE,
                        "status": "error",
                        "error": f"missing Bomberland result file: {result_file}",
                    },
                    sort_keys=True,
                )
            )
        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

Agent Interface

Your bot must be a Python file named bomberland_agent.py that defines next_actions.

def next_actions(game_state):
    agent_id = game_state["connection"]["agent_id"]
    unit_ids = game_state["agents"][agent_id]["unit_ids"]
    return {unit_id: "stay" for unit_id in unit_ids}

Valid string actions are up, down, left, right, bomb, and stay. The runtime also accepts dictionary move actions such as {"type": "move", "move": "up"} for compatibility with common starter-kit styles.

Configuration Example

tournament:
  rounds: 1
game:
  name: Bomberland
  sims_per_round: 2
  args:
    ticks: 40
    width: 11
    height: 11
    unit_count: 3
players:
  - agent: dummy
    name: alpha
  - agent: dummy
    name: beta

Scoring

The arena runs sims_per_round deterministic seeded games. sims_per_round must be even so each player receives both starting sides for paired seeds. Each player receives an average score computed from surviving health, surviving units, enemy damage, kills, destroyed blocks, invalid actions, and agent runtime errors.

Smoke Test

From the repository root, run the dummy-player example:

uv run codeclash run configs/examples/Bomberland__dummy__r1__s2.yaml -o /tmp/codeclash-bomberland-smoke

Use a fresh -o directory when rerunning the smoke check.

Expected shape:

  • the command exits with status 0;
  • both players pass submission validation;
  • stdout includes In round 0, the winner is ... and In round 1, the winner is ...;
  • each round summary contains floating-point average scores for alpha and beta;
  • per-simulation details include scores, stats, alive_units, alive_hp, ticks, and winner fields;
  • per-player stats include agent_errors and invalid_actions;
  • the output directory contains metadata.json, game.log, tournament.log, and rounds/round_0.tar.gz / rounds/round_1.tar.gz.

The arena writes bomberland_results.json inside each round log with this shape:

{
  "average_scores": {"alpha": 330.0, "beta": 330.0},
  "total_scores": {"alpha": 660.0, "beta": 660.0},
  "sims": 2,
  "details": ["... per-simulation JSON strings ..."]
}

A representative metadata.json round contains a scores object with one floating-point average score per player:

"scores": {
  "alpha": 330.0,
  "beta": 330.0
}

Exact values can change with arena configuration; the smoke check is meant to verify the Docker/runtime adapter path, player-name mapping, paired starting sides, and score/log artifact shape.

The exact tournament directory name includes a timestamp, so inspect the metadata with:

find /tmp/codeclash-bomberland-smoke -maxdepth 3 -name metadata.json -print