Skip to content

Commit 013fda0

Browse files
committed
Merge branch 'main' into feat/add-gomoku
2 parents 30a3f74 + 5d83b63 commit 013fda0

36 files changed

Lines changed: 1488 additions & 77 deletions

codeclash/agents/minisweagent.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@
1616
from codeclash.utils.environment import copy_to_container
1717

1818
os.environ["MSWEA_MODEL_RETRY_STOP_AFTER_ATTEMPT"] = "90"
19-
os.environ["LITELLM_MODEL_REGISTRY_PATH"] = str((REPO_DIR / "configs" / "litellm_custom_model_config.yaml").resolve())
19+
os.environ["LITELLM_MODEL_REGISTRY_PATH"] = str(
20+
(REPO_DIR / "configs" / "mini" / "litellm_custom_model_config.yaml").resolve()
21+
)
2022

2123

2224
class ClashAgent(DefaultAgent):

codeclash/arenas/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from codeclash.arenas.arena import CodeArena
22
from codeclash.arenas.battlecode.battlecode import BattleCodeArena
33
from codeclash.arenas.battlesnake.battlesnake import BattleSnakeArena
4+
from codeclash.arenas.bridge.bridge import BridgeArena
45
from codeclash.arenas.corewar.corewar import CoreWarArena
56
from codeclash.arenas.dummy.dummy import DummyArena
67
from codeclash.arenas.gomoku.gomoku import GomokuArena
@@ -14,6 +15,7 @@
1415
ARENAS = [
1516
BattleCodeArena,
1617
BattleSnakeArena,
18+
BridgeArena,
1719
CoreWarArena,
1820
DummyArena,
1921
GomokuArena,

codeclash/arenas/battlecode/battlecode.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ def _run_single_simulation(self, agents: list[Player], idx: int, cmd: str) -> st
4949
def execute_round(self, agents: list[Player]):
5050
for agent in agents:
5151
src, dest = f"/{agent.name}/src/{BC_FOLDER}/", str(DIR_WORK / "src" / agent.name)
52-
self.environment.execute(f"cp -r {src} {dest}")
52+
self.environment.execute(f"rm -rf {dest}; cp -r {src} {dest}")
5353
args = [f"--p{idx + 1}-dir src --p{idx + 1} {agent.name}" for idx, agent in enumerate(agents)]
5454
cmd = f"{self.run_cmd_round} {' '.join(args)}"
5555
self.logger.info(f"Running game: {cmd}")
@@ -69,8 +69,10 @@ def get_results(self, agents: list[Player], round_num: int, stats: RoundStats):
6969
for idx in range(self.game_config["sims_per_round"]):
7070
with open(self.log_round(round_num) / BC_LOG.format(idx=idx)) as f:
7171
lines = f.read().strip().split("\n")
72+
if len(lines) < 3:
73+
# Game likely crashed, skip this simulation
74+
continue
7275
# Get the third-to-last line which contains the winner info
73-
assert len(lines) >= 3, "Log file does not contain enough lines to determine winner"
7476
winner_line = lines[-3]
7577
reason_line = lines[-2]
7678
match = re.search(r"\s\((.*)\)\swins\s\(", winner_line)
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
FROM ubuntu:22.04
2+
3+
ENV DEBIAN_FRONTEND=noninteractive
4+
5+
# Install Python 3.10 and basic tools
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+
RUN git clone https://github.com/CodeClash-ai/Bridge.git /workspace \
13+
&& cd /workspace \
14+
&& git remote set-url origin https://github.com/CodeClash-ai/Bridge.git
15+
16+
WORKDIR /workspace
17+
18+
# No additional dependencies needed - game logic is pure Python
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Bridge arena for CodeClash."""

codeclash/arenas/bridge/bridge.py

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
"""Bridge Arena for CodeClash."""
2+
3+
import json
4+
import shlex
5+
import subprocess
6+
from collections import Counter
7+
from concurrent.futures import ThreadPoolExecutor, as_completed
8+
9+
from tqdm.auto import tqdm
10+
11+
from codeclash.agents.player import Player
12+
from codeclash.arenas.arena import CodeArena, RoundStats
13+
from codeclash.constants import RESULT_TIE
14+
15+
16+
class BridgeArena(CodeArena):
17+
name: str = "Bridge"
18+
submission: str = "bridge_agent.py"
19+
description: str = """Bridge is a 4-player trick-taking card game played in teams.
20+
21+
Teams: North/South (positions 0/2) vs East/West (positions 1/3)
22+
23+
Your bot (bridge_agent.py) must implement these functions:
24+
- get_bid(game_state) -> str: Make bidding decisions, return bid string like "1H", "2NT", "PASS"
25+
- play_card(game_state) -> str: Play a card, return card string like "AS", "7H"
26+
27+
game_state is a dict containing:
28+
- position: Your position (0=North, 1=East, 2=South, 3=West)
29+
- hand: List of cards in your hand (e.g., ["AS", "KH", "7D"])
30+
- bids: List of previous bids
31+
- legal_bids: List of legal bids you can make (during bidding)
32+
- legal_cards: List of legal cards you can play (during playing)
33+
- current_trick: Cards played so far in current trick
34+
- contract: The current contract (if bidding is complete)
35+
"""
36+
default_args: dict = {
37+
"sims_per_round": 10,
38+
}
39+
40+
def __init__(self, config, **kwargs):
41+
# Validate player count before initializing (to avoid Docker build on invalid config)
42+
num_players = len(config.get("players", []))
43+
if num_players != 4:
44+
raise ValueError(f"Bridge requires exactly 4 players, got {num_players}")
45+
super().__init__(config, **kwargs)
46+
self.run_cmd = "python3 /workspace/run_game.py"
47+
48+
def validate_code(self, agent: Player) -> tuple[bool, str | None]:
49+
"""Validate agent code has required functions."""
50+
if self.submission not in agent.environment.execute("ls")["output"]:
51+
return False, f"No {self.submission} file found in root directory"
52+
53+
content = agent.environment.execute(f"cat {self.submission}")["output"]
54+
55+
# Check for required function definitions
56+
required_functions = [
57+
"def get_bid(",
58+
"def play_card("
59+
]
60+
61+
missing = []
62+
for func in required_functions:
63+
if func not in content:
64+
missing.append(func)
65+
66+
if missing:
67+
return False, f"Missing required functions: {', '.join(missing)}"
68+
69+
return True, None
70+
71+
def _run_single_simulation(self, agents: list[Player], idx: int, cmd: str):
72+
"""Run a single Bridge game simulation."""
73+
full_cmd = f"{cmd} -o {self.log_env / f'sim_{idx}.json'}"
74+
75+
try:
76+
response = self.environment.execute(full_cmd, timeout=60)
77+
except subprocess.TimeoutExpired:
78+
self.logger.warning(f"Bridge simulation {idx} timed out")
79+
return ""
80+
81+
if response["returncode"] != 0:
82+
self.logger.warning(
83+
f"Bridge simulation {idx} failed with exit code {response['returncode']}:\n{response['output']}"
84+
)
85+
return response["output"]
86+
87+
def execute_round(self, agents: list[Player]):
88+
"""Execute a round of Bridge games."""
89+
sims = self.game_config.get('sims_per_round', 10)
90+
self.logger.info(f"Running {sims} Bridge simulations with 4 players")
91+
92+
# Build agent paths for the command
93+
agent_paths = []
94+
for agent in agents:
95+
agent_paths.append(f"/{agent.name}/{self.submission}")
96+
97+
# Build base command
98+
cmd = f"{self.run_cmd} {shlex.join(agent_paths)}"
99+
100+
# Run simulations in parallel
101+
with ThreadPoolExecutor(max_workers=8) as executor:
102+
futures = [
103+
executor.submit(
104+
self._run_single_simulation,
105+
agents,
106+
idx,
107+
f"{cmd} --seed {idx} --dealer {idx % 4}"
108+
)
109+
for idx in range(sims)
110+
]
111+
for future in tqdm(as_completed(futures), total=len(futures), desc="Bridge simulations"):
112+
future.result()
113+
114+
def get_results(self, agents: list[Player], round_num: int, stats: RoundStats):
115+
"""Parse results and determine winners."""
116+
# Initialize team scores
117+
team_scores = {'NS': 0.0, 'EW': 0.0}
118+
games_played = 0
119+
120+
# Parse all simulation logs
121+
for idx in range(self.game_config.get('sims_per_round', 10)):
122+
log_file = self.log_round(round_num) / f"sim_{idx}.json"
123+
124+
if not log_file.exists():
125+
self.logger.warning(f"Log file {log_file} not found, skipping")
126+
continue
127+
128+
try:
129+
with open(log_file) as f:
130+
result = json.load(f)
131+
132+
# Check for error
133+
if 'error' in result:
134+
self.logger.warning(f"Simulation {idx} had error: {result['error']}")
135+
continue
136+
137+
# Extract VP scores for each team
138+
vp_scores = result.get('normalized_score', {})
139+
if vp_scores:
140+
team_scores['NS'] += vp_scores.get('NS', 0.0)
141+
team_scores['EW'] += vp_scores.get('EW', 0.0)
142+
games_played += 1
143+
except (json.JSONDecodeError, KeyError) as e:
144+
self.logger.warning(f"Error parsing {log_file}: {e}")
145+
continue
146+
147+
if games_played == 0:
148+
self.logger.error("No valid game results found")
149+
stats.winner = RESULT_TIE
150+
for agent in agents:
151+
stats.scores[agent.name] = 0.0
152+
stats.player_stats[agent.name].score = 0.0
153+
return
154+
155+
# Average the scores
156+
team_scores['NS'] /= games_played
157+
team_scores['EW'] /= games_played
158+
159+
# Determine winning team
160+
if abs(team_scores['NS'] - team_scores['EW']) < 0.01: # Tie threshold
161+
stats.winner = RESULT_TIE
162+
elif team_scores['NS'] > team_scores['EW']:
163+
stats.winner = f"{agents[0].name}/{agents[2].name}"
164+
else:
165+
stats.winner = f"{agents[1].name}/{agents[3].name}"
166+
167+
# Assign scores to individual players based on their team
168+
for position, agent in enumerate(agents):
169+
team = 'NS' if position % 2 == 0 else 'EW'
170+
score = team_scores[team]
171+
stats.scores[agent.name] = score
172+
stats.player_stats[agent.name].score = score
173+
174+
self.logger.info(
175+
f"Round {round_num} results - NS: {team_scores['NS']:.3f}, "
176+
f"EW: {team_scores['EW']:.3f}, Winner: {stats.winner}"
177+
)

codeclash/arenas/corewar/corewar.py

Lines changed: 57 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
import re
22
import shlex
3+
from collections import defaultdict
4+
from concurrent.futures import ThreadPoolExecutor, as_completed
35

46
from codeclash.agents.player import Player
57
from codeclash.arenas.arena import CodeArena, RoundStats
8+
from codeclash.constants import RESULT_TIE
69

7-
COREWAR_LOG = "sim.log"
10+
COREWAR_LOG = "sim_{idx}.log"
811

912

1013
class CoreWarArena(CodeArena):
@@ -23,49 +26,73 @@ def __init__(self, config, **kwargs):
2326
else:
2427
self.run_cmd_round += f" -{arg} {val}"
2528

26-
def execute_round(self, agents: list[Player]):
29+
def _run_single_simulation(self, agents: list[Player], idx: int):
30+
# Shift agents by idx to vary starting positions
31+
agents = agents[idx:] + agents[:idx]
2732
args = [f"/{agent.name}/{self.submission}" for agent in agents]
2833
cmd = (
2934
f"{self.run_cmd_round} {shlex.join(args)} "
3035
f"-r {self.game_config['sims_per_round']} "
31-
f"> {self.log_env / COREWAR_LOG};"
36+
f"> {self.log_env / COREWAR_LOG.format(idx=idx)};"
3237
)
3338
self.logger.info(f"Running game: {cmd}")
3439
response = self.environment.execute(cmd)
3540
assert response["returncode"] == 0, response
3641

42+
def execute_round(self, agents: list[Player]):
43+
with ThreadPoolExecutor(4) as executor:
44+
futures = [executor.submit(self._run_single_simulation, agents, idx) for idx in range(len(agents))]
45+
for future in as_completed(futures):
46+
future.result()
47+
3748
def get_results(self, agents: list[Player], round_num: int, stats: RoundStats):
38-
with open(self.log_round(round_num) / COREWAR_LOG) as f:
39-
result_output = f.read()
40-
self.logger.debug(f"Determining winner from result output: {result_output}")
41-
scores = []
42-
n = len(agents) * 2
43-
lines = result_output.strip().split("\n")
49+
scores, wins = defaultdict(int), defaultdict(int)
50+
for idx in range(len(agents)):
51+
shift = agents[idx:] + agents[:idx] # Shift agents by idx to match simulation order
52+
with open(self.log_round(round_num) / COREWAR_LOG.format(idx=idx)) as f:
53+
result_output = f.read()
4454

45-
# Get the last n lines which contain the scores (closer to original)
46-
relevant_lines = lines[-n:] if len(lines) >= n else lines
47-
relevant_lines = [l for l in relevant_lines if len(l.strip()) > 0]
48-
self.logger.debug(f"Relevant lines for scoring: {relevant_lines}")
55+
# Get the last n lines which contain the scores (closer to original)
56+
lines = result_output.strip().split("\n")
57+
relevant_lines = lines[-len(shift) * 2 :] if len(lines) >= len(shift) * 2 else lines
58+
relevant_lines = [l for l in relevant_lines if len(l.strip()) > 0]
4959

50-
# Go through each line; we assume score position is correlated with agent index
51-
for line in relevant_lines:
52-
match = re.search(r".*\sby\s.*\sscores\s(\d+)", line)
53-
if match:
54-
score = int(match.group(1))
55-
scores.append(score)
60+
# Go through each line; score position is correlated with agent index
61+
for i, line in enumerate(relevant_lines):
62+
match = re.search(r".*\sby\s.*\sscores\s(\d+)", line)
63+
if match:
64+
scores[shift[i].name] += int(match.group(1))
5665

57-
if scores:
58-
if len(scores) != len(agents):
59-
self.logger.error(f"Have {len(scores)} scores but {len(agents)} agents")
60-
stats.winner = agents[scores.index(max(scores))].name
61-
stats.scores = {agent.name: score for agent, score in zip(agents, scores)}
62-
else:
63-
self.logger.debug("No scores found, returning unknown")
64-
stats.winner = "unknown"
65-
stats.scores = {agent.name: 0 for agent in agents}
66+
# Last line corresponds to absolute number of wins
67+
last = relevant_lines[-1][len("Results:") :].strip()
68+
for i, w in enumerate(last.split()[:-1]): # NOTE: Omitting ties (last entry)
69+
wins[shift[i].name] += int(w)
6670

67-
for player, score in stats.scores.items():
68-
stats.player_stats[player].score = score
71+
if len(wins) != len(agents):
72+
# Should not happen
73+
self.logger.error(f"Have {len(wins)} wins but {len(agents)} agents")
74+
75+
# Bookkeeping
76+
stats.scores = {a.name: wins[a.name] for a in agents}
77+
for a in agents:
78+
stats.player_stats[a.name].score = wins[a.name]
79+
80+
# Determine overall winner by highest wins, then highest score
81+
max_wins = max(wins.values(), default=0)
82+
potential_winners = [name for name, w in wins.items() if w == max_wins]
83+
if len(potential_winners) == 1:
84+
stats.winner = potential_winners[0]
85+
else:
86+
# Tie-break by score
87+
max_score = -1
88+
winner = RESULT_TIE
89+
for name in potential_winners:
90+
if scores[name] > max_score:
91+
max_score = scores[name]
92+
winner = name
93+
elif scores[name] == max_score:
94+
winner = RESULT_TIE
95+
stats.winner = winner
6996

7097
def validate_code(self, agent: Player) -> tuple[bool, str | None]:
7198
if self.submission not in agent.environment.execute("ls")["output"]:

0 commit comments

Comments
 (0)