Skip to content

BattleSnake

Multiplayer snake game where snakes compete to be the last survivor.

Overview

BattleSnake is a multi-player version of the classic snake game. Players implement an HTTP server that responds to game state with movement decisions.

Game Rules

  • Snakes move on a grid
  • Eat food to grow longer
  • Avoid walls and other snakes
  • Last snake standing wins

Submission Format

Players must implement an HTTP server with specific endpoints:

  • GET /: Return snake metadata
  • POST /start: Handle game start
  • POST /move: Return movement decision
  • POST /end: Handle game end

Configuration Example

game:
  name: BattleSnake
  rounds: 10
  sims_per_round: 5
  timeout: 300

players:
  - name: Snake1
    model: gpt-4
  - name: Snake2
    model: claude-3

Resources

Implementation

codeclash.arenas.battlesnake.battlesnake.BattleSnakeArena

BattleSnakeArena(config, **kwargs)

Bases: CodeArena

Source code in codeclash/arenas/battlesnake/battlesnake.py
26
27
28
29
30
31
32
33
34
35
def __init__(self, config, **kwargs):
    super().__init__(config, **kwargs)
    self.run_cmd_round: str = "./battlesnake play"
    for arg, val in self.game_config.get("args", self.default_args).items():
        if isinstance(val, bool):
            if val:
                self.run_cmd_round += f" --{arg}"
        else:
            self.run_cmd_round += f" --{arg} {val}"
    self._failed_to_start_player = []

name class-attribute instance-attribute

name: str = 'BattleSnake'

submission class-attribute instance-attribute

submission: str = 'main.py'

description class-attribute instance-attribute

description: str = 'Your bot (`main.py`) controls a snake on a grid-based board.\nSnakes collect food, avoid collisions, and try to outlast their opponents.'

default_args class-attribute instance-attribute

default_args: dict = {'width': 11, 'height': 11, 'browser': False}

run_cmd_round instance-attribute

run_cmd_round: str = './battlesnake play'

execute_round

execute_round(agents: list[Player])
Source code in codeclash/arenas/battlesnake/battlesnake.py
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
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
def execute_round(self, agents: list[Player]):
    self._failed_to_start_player = []
    assert len(agents) > 1, "Battlesnake requires at least two players"
    self.logger.debug("Starting game servers")
    player2port = {}
    for idx, agent in enumerate(agents):
        port = 8001 + idx
        player2port[agent.name] = port
        # Surprisingly slow despite using &
        # Start server in background - just add & to run in background!
        self.environment.execute(f"PORT={port} python {self.submission} &", cwd=f"/{agent.name}")

    self.logger.debug(f"Waiting for ports: {player2port}")
    available_ports = self._wait_for_ports(list(player2port.values()))

    if not available_ports:
        raise RuntimeError("All games failed to start")

    if len(available_ports) == 1:
        missing_ports = set(player2port.values()) - set(available_ports)
        missing_player = next(player for player, port in player2port.items() if port in missing_ports)
        self.logger.warning(f"Player {missing_player} failed to start")
        self._failed_to_start_player.append(missing_player)
        return

    if len(available_ports) < len(agents):
        raise RuntimeError(f"Only {len(available_ports)} players started: {available_ports}")

    self.logger.debug("All ports are ready")

    try:
        self.logger.info(f"Running game with players: {list(player2port.keys())}")

        # Use ThreadPoolExecutor for parallel execution
        with ThreadPoolExecutor(20) as executor:
            # Submit all simulations to the thread pool
            futures = [
                executor.submit(self._run_single_simulation, player2port, idx)
                for idx in range(self.game_config["sims_per_round"])
            ]

            # Collect results as they complete
            for future in tqdm(as_completed(futures), total=len(futures)):
                future.result()
    finally:
        # Kill all python servers when done
        self.environment.execute(f"pkill -f 'python {self.submission}' || true")

get_results

get_results(agents: list[Player], round_num: int, stats: RoundStats)
Source code in codeclash/arenas/battlesnake/battlesnake.py
135
136
137
138
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
def get_results(self, agents: list[Player], round_num: int, stats: RoundStats):
    scores = defaultdict(int)
    available_players = [player.name for player in agents if player.name not in self._failed_to_start_player]
    if len(available_players) > 1:
        # We ran the game
        for idx in range(self.game_config["sims_per_round"]):
            try:
                with open(self.log_round(round_num) / f"sim_{idx}.jsonl") as f:
                    lines = f.read().strip().split("\n")
                    results = json.loads(lines[-1])  # Get the last line which contains the game result
                    winner = RESULT_TIE if results["isDraw"] else results["winnerName"]
                    scores[winner] += 1
            except FileNotFoundError:
                self.logger.warning(f"Simulation {idx} not found, skipping")
            except json.JSONDecodeError:
                self.logger.warning(f"Simulation {idx} is not a valid JSON, skipping")
    else:
        self.logger.warning(f"Only one player ({available_players[0]}) started, giving them the win")
        # We didn't run a game, so we just give the one player the win
        available_player = available_players[0]
        scores = {available_player: self.game_config["sims_per_round"]}

    winner = max(scores, key=scores.get)
    winner = RESULT_TIE if list(scores.values()).count(scores[winner]) > 1 else winner
    stats.winner = winner
    stats.scores = scores
    for player, score in scores.items():
        if player != RESULT_TIE:
            stats.player_stats[player].score = score

validate_code

validate_code(agent: Player) -> tuple[bool, str | None]
Source code in codeclash/arenas/battlesnake/battlesnake.py
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
def validate_code(self, agent: Player) -> tuple[bool, str | None]:
    if self.submission not in agent.environment.execute("ls")["output"]:
        return False, f"No {self.submission} file found in the root directory"
    # note: no longer calling splitlines
    bot_content = agent.environment.execute(f"cat {self.submission}")["output"]
    error_msg = []
    for func in [
        "def info(",
        "def start(",
        "def end(",
        "def move(",
    ]:
        if func not in bot_content:
            error_msg.append(f"There should be a `{func}` function implemented in `{self.submission}`")
    if len(error_msg) > 0:
        return False, "\n".join(error_msg + ["Don't change the function signatures!"])
    return True, None