Skip to content

Commit b393675

Browse files
committed
Init Halite arena
1 parent 299dad9 commit b393675

6 files changed

Lines changed: 144 additions & 16 deletions

File tree

codeclash/games/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from codeclash.games.corewar.corewar import CoreWarGame
44
from codeclash.games.dummy.dummy_game import DummyGame
55
from codeclash.games.game import CodeGame
6+
from codeclash.games.halite.halite import HaliteGame
67
from codeclash.games.huskybench.huskybench import HuskyBenchGame
78
from codeclash.games.robocode.robocode import RoboCodeGame
89
from codeclash.games.robotrumble.robotrumble import RobotRumbleGame
@@ -12,6 +13,7 @@
1213
BattleSnakeGame,
1314
CoreWarGame,
1415
DummyGame,
16+
HaliteGame,
1517
HuskyBenchGame,
1618
RoboCodeGame,
1719
RobotRumbleGame,

codeclash/games/halite/__init__.py

Whitespace-only changes.

codeclash/games/halite/halite.py

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import re
2+
import shlex
3+
import subprocess
4+
from collections import Counter
5+
from concurrent.futures import ThreadPoolExecutor, as_completed
6+
7+
from tqdm.auto import tqdm
8+
9+
from codeclash.agents.player import Player
10+
from codeclash.constants import RESULT_TIE
11+
from codeclash.games.game import CodeGame, RoundStats
12+
13+
HALITE_LOG = "sim_{idx}.log"
14+
15+
16+
class HaliteGame(CodeGame):
17+
name: str = "Halite"
18+
description: str = """Halite is a strategic programming game where players write bots to control ships that gather resources, build structures, and compete for dominance on a grid-based map.
19+
Victory is achieved by outmaneuvering opponents, optimizing resource collection, and strategically expanding your territory"""
20+
default_args: dict = {}
21+
22+
def __init__(self, config, **kwargs):
23+
super().__init__(config, **kwargs)
24+
self.run_cmd_round: str = f"./environment/halite --replaydirectory {self.log_env}"
25+
for arg, val in self.game_config.get("args", self.default_args).items():
26+
if isinstance(val, bool):
27+
if val:
28+
self.run_cmd_round += f" --{arg}"
29+
else:
30+
self.run_cmd_round += f" --{arg} {val}"
31+
32+
def _run_single_simulation(self, agents: list[Player], idx: int, cmd: str):
33+
"""Run a single halite simulation and return the output."""
34+
cmd = f"{cmd} > {self.log_env / HALITE_LOG.format(idx=idx)}"
35+
36+
# Run the simulation and return the output
37+
try:
38+
response = self.environment.execute(cmd, timeout=120)
39+
except subprocess.TimeoutExpired:
40+
self.logger.warning(f"Halite simulation {idx} timed out: {cmd}")
41+
return
42+
if response["returncode"] != 0:
43+
self.logger.warning(
44+
f"Halite simulation {idx} failed with exit code {response['returncode']}:\n{response['output']}"
45+
)
46+
47+
def execute_round(self, agents: list[Player]):
48+
entries = []
49+
for agent in agents:
50+
entries.append(f"python /{agent.name}/airesources/Python/RandomBot.py")
51+
cmd = f"{self.run_cmd_round} {shlex.join(entries)}"
52+
self.logger.info(f"Running game: {cmd}")
53+
with ThreadPoolExecutor(5) as executor:
54+
futures = [
55+
executor.submit(self._run_single_simulation, agents, idx, cmd)
56+
for idx in range(self.game_config["sims_per_round"])
57+
]
58+
for future in tqdm(as_completed(futures), total=len(futures)):
59+
future.result()
60+
61+
def get_results(self, agents: list[Player], round_num: int, stats: RoundStats):
62+
winners = []
63+
pattern = r"Player\s#(\d+),\s(.*),\scame\sin\srank\s#(\d+)"
64+
for idx in range(self.game_config["sims_per_round"]):
65+
log_file = self.log_round(round_num) / HALITE_LOG.format(idx=idx)
66+
with open(log_file) as f:
67+
lines = f.readlines()[-len(agents) - 1 :]
68+
for line in lines:
69+
match = re.search(pattern, line)
70+
if match:
71+
player_idx = int(match.group(1)) - 1
72+
rank = int(match.group(3))
73+
if rank == 1:
74+
winners.append(agents[player_idx].name)
75+
76+
# Count wins
77+
win_counts = Counter(winners)
78+
79+
# Find all winners with the maximum count
80+
max_wins = max(win_counts.values(), default=0)
81+
overall_winners = [name for name, count in win_counts.items() if count == max_wins]
82+
83+
# Update stats
84+
stats.winner = RESULT_TIE if len(overall_winners) > 1 else overall_winners[0]
85+
stats.scores = dict(win_counts)
86+
for player, score in win_counts.items():
87+
if player != RESULT_TIE:
88+
stats.player_stats[player].score = score
89+
90+
def validate_code(self, agent: Player) -> tuple[bool, str | None]:
91+
return True, None

codeclash/games/robotrumble/robotrumble.py

Lines changed: 10 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ def __init__(self, config, **kwargs):
3333
else:
3434
self.run_cmd_round += f" --{arg} {val}"
3535

36-
def _run_single_simulation(self, agents: list[Player], idx: int, cmd: str) -> str:
36+
def _run_single_simulation(self, agents: list[Player], idx: int, cmd: str):
3737
"""Run a single robotrumble simulation and return the output."""
3838
cmd = f"{cmd} > {self.log_env / f'sim_{idx}.{self.sim_ext}'}"
3939

@@ -42,17 +42,17 @@ def _run_single_simulation(self, agents: list[Player], idx: int, cmd: str) -> st
4242
response = self.environment.execute(cmd, timeout=120)
4343
except subprocess.TimeoutExpired:
4444
self.logger.warning(f"RobotRumble simulation {idx} timed out: {cmd}")
45-
return ""
45+
return
4646
if response["returncode"] != 0:
4747
self.logger.warning(
4848
f"RobotRumble simulation {idx} failed with exit code {response['returncode']}:\n{response['output']}"
4949
)
50-
return response["output"]
5150

5251
def execute_round(self, agents: list[Player]):
5352
self.logger.info(f"Running game with players: {[agent.name for agent in agents]}")
5453
args = [f"/{agent.name}/{self.submission}" for agent in agents]
5554
cmd = f"{self.run_cmd_round} {shlex.join(args)}"
55+
self.logger.info(f"Running game: {cmd}")
5656

5757
with ThreadPoolExecutor(5) as executor:
5858
# Submit all simulations to the thread pool
@@ -61,9 +61,6 @@ def execute_round(self, agents: list[Player]):
6161
for idx in range(self.game_config.get("sims_per_round", 100))
6262
]
6363

64-
# Log command being run
65-
self.logger.info(f"Running game: {cmd}")
66-
6764
# Collect results as they complete
6865
for future in tqdm(as_completed(futures), total=len(futures)):
6966
future.result()
@@ -117,21 +114,18 @@ def get_results(self, agents: list[Player], round_num: int, stats: RoundStats):
117114
else self._get_winner_json(output_file, agents)
118115
)
119116

120-
# Count occurrences of each winner
121-
counts = Counter(winners)
117+
# Count wins
118+
win_counts = Counter(winners)
122119

123120
# Find all winners with the maximum count
124-
max_count = max(counts.values())
125-
top_winners = [w for w, c in counts.items() if c == max_count]
126-
127-
# If multiple winners have the same count, return RESULT_TIE
128-
final_winner = RESULT_TIE if len(top_winners) > 1 else top_winners[0]
121+
max_wins = max(win_counts.values())
122+
overall_winners = [name for name, count in win_counts.items() if count == max_wins]
129123

130124
# Update stats
131-
stats.winner = final_winner
125+
stats.winner = RESULT_TIE if len(overall_winners) > 1 else overall_winners[0]
132126
stats.details.append(f"In this round, {agents[0].name} was Blue and {agents[1].name} was Red.")
133-
stats.scores = dict(counts)
134-
for player, score in counts.items():
127+
stats.scores = dict(win_counts)
128+
for player, score in win_counts.items():
135129
if player != RESULT_TIE:
136130
stats.player_stats[player].score = score
137131

configs/test/players/2/halite.yaml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
tournament:
2+
rounds: 1
3+
game:
4+
name: Halite
5+
sims_per_round: 10
6+
players:
7+
- agent: dummy
8+
name: p1
9+
- agent: dummy
10+
name: p2
11+
prompts:
12+
game_description: |
13+
You are a software developer ({{player_id}}) competing in an arena called Halite.
14+
In this game, you will write code to control ships that gather resources, build structures, and compete for dominance on a grid-based map.
15+
Victory is achieved by outmaneuvering opponents, optimizing resource collection, and strategically expanding your territory.
16+
17+
The game is played in {{rounds}} rounds. For every round, you (and your competitor) edit program code that controls your bot. This is round {{round}}.
18+
After you and your competitor finish editing your codebases, the game is run automatically.
19+
20+
Your task: improve the bot, located in {{working_dir}}.
21+
{{working_dir}} is your codebase, which contains both your bot and supporting assets.

docker/Halite.Dockerfile

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
FROM ubuntu:22.04
2+
3+
ENV DEBIAN_FRONTEND=noninteractive
4+
5+
# Install Python 3.10 (and alias python→python3.10), pip, and prerequisites
6+
RUN apt-get update \
7+
&& apt-get install -y --no-install-recommends \
8+
curl ca-certificates python3.10 python3.10-venv \
9+
python3-pip python-is-python3 wget git build-essential jq curl locales \
10+
&& rm -rf /var/lib/apt/lists/*
11+
12+
ARG GITHUB_TOKEN
13+
RUN git clone https://${GITHUB_TOKEN}@github.com/emagedoc/Halite.git /workspace \
14+
&& cd /workspace \
15+
&& git remote set-url origin https://github.com/emagedoc/Halite.git \
16+
&& unset GITHUB_TOKEN
17+
18+
WORKDIR /workspace
19+
20+
RUN cd environment && make

0 commit comments

Comments
 (0)