Skip to content

Commit c318e75

Browse files
committed
Merge branch 'main' into chess-arena
2 parents 28b88e7 + 71feae3 commit c318e75

46 files changed

Lines changed: 362 additions & 1095 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

codeclash/analysis/viz/round_score_distribution.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from tqdm import tqdm
1010

1111
from codeclash.analysis.viz.utils import ASSETS_DIR, FONT_BOLD, MODEL_TO_DISPLAY_NAME
12-
from codeclash.arenas import BattleCodeArena, DummyGame
12+
from codeclash.arenas import BattleCode25Arena, DummyArena
1313
from codeclash.constants import LOCAL_LOG_DIR
1414
from codeclash.utils.log import get_logger
1515

@@ -32,7 +32,7 @@ def get_normalized_scores(metadata_path: Path) -> tuple[str | None, dict[str, li
3232
if len(players) != 2:
3333
return None, {}, {}
3434

35-
if game_name in {DummyGame.name, BattleCodeArena.name}:
35+
if game_name in {DummyArena.name, BattleCode25Arena.name}:
3636
return None, {}, {}
3737

3838
# Map player names to models

codeclash/analysis/viz/win_rate_distribution.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from tqdm import tqdm
1010

1111
from codeclash.analysis.viz.utils import ASSETS_DIR, FONT_BOLD, MODEL_TO_DISPLAY_NAME
12-
from codeclash.arenas import BattleCodeArena, DummyArena
12+
from codeclash.arenas import BattleCode25Arena, DummyArena
1313
from codeclash.constants import LOCAL_LOG_DIR, RESULT_TIE
1414
from codeclash.utils.log import get_logger
1515

@@ -33,7 +33,7 @@ def get_player_win_counts(metadata_path: Path) -> tuple[str | None, dict[str, li
3333
if len(players) != 2:
3434
return None, {}, {}
3535

36-
if game_name in {DummyArena.name, BattleCodeArena.name}:
36+
if game_name in {DummyArena.name, BattleCode25Arena.name}:
3737
return None, {}, {}
3838

3939
# Map player names to models
@@ -176,7 +176,7 @@ def get_tournament_wins_by_model(metadata_path: Path) -> dict[str, tuple[bool, i
176176
if len(players) != 2:
177177
return {}
178178

179-
if game_name in {DummyArena.name, BattleCodeArena.name}:
179+
if game_name in {DummyArena.name, BattleCode25Arena.name}:
180180
return {}
181181

182182
# Map player names to models

codeclash/arenas/__init__.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
from codeclash.arenas.arena import CodeArena
2-
from codeclash.arenas.battlecode.battlecode import BattleCodeArena
2+
from codeclash.arenas.battlecode25.battlecode25 import BattleCode25Arena
33
from codeclash.arenas.battlesnake.battlesnake import BattleSnakeArena
44
from codeclash.arenas.bridge.bridge import BridgeArena
55
from codeclash.arenas.corewar.corewar import CoreWarArena
66
from codeclash.arenas.dummy.dummy import DummyArena
7+
from codeclash.arenas.figgie.figgie import FiggieArena
78
from codeclash.arenas.gomoku.gomoku import GomokuArena
89
from codeclash.arenas.halite.halite import HaliteArena
910
from codeclash.arenas.halite2.halite2 import Halite2Arena
@@ -13,11 +14,12 @@
1314
from codeclash.arenas.robotrumble.robotrumble import RobotRumbleArena
1415

1516
ARENAS = [
16-
BattleCodeArena,
17+
BattleCode25Arena,
1718
BattleSnakeArena,
1819
BridgeArena,
1920
CoreWarArena,
2021
DummyArena,
22+
FiggieArena,
2123
GomokuArena,
2224
HaliteArena,
2325
Halite2Arena,

codeclash/arenas/battlecode/BattleCode.Dockerfile renamed to codeclash/arenas/battlecode25/BattleCode25.Dockerfile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
55
build-essential git curl && \
66
rm -rf /var/lib/apt/lists/*
77

8-
RUN git clone https://github.com/CodeClash-ai/BattleCode.git /workspace \
8+
RUN git clone https://github.com/CodeClash-ai/BattleCode2025.git /workspace \
99
&& cd /workspace \
10-
&& git remote set-url origin https://github.com/CodeClash-ai/BattleCode.git
10+
&& git remote set-url origin https://github.com/CodeClash-ai/BattleCode2025.git
1111
WORKDIR /workspace
1212

1313
RUN python run.py update

codeclash/arenas/battlecode/battlecode.py renamed to codeclash/arenas/battlecode25/battlecode25.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@
1414
BC_TIE = "Reason: The winning team won arbitrarily (coin flip)."
1515

1616

17-
class BattleCodeArena(CodeArena):
18-
name: str = "BattleCode"
19-
description: str = """Battlecode 25 throws you into a real-time strategy showdown where your Python bot pilots a team of specialized robots—Soldiers, Moppers, Splashers—alongside towers that spawn units or generate resources.
17+
class BattleCode25Arena(CodeArena):
18+
name: str = "BattleCode25"
19+
description: str = """BattleCode 2025 throws you into a real-time strategy showdown where your Python bot pilots a team of specialized robots—Soldiers, Moppers, Splashers—alongside towers that spawn units or generate resources.
2020
Your mission: paint over 70% of the map (or eliminate the enemy) by coordinating cleanups, area cover, and tower-building through tight bytecode budgets and clever unit synergy."""
2121
default_args: dict = {
2222
"maps": "quack",
@@ -25,7 +25,7 @@ class BattleCodeArena(CodeArena):
2525

2626
def __init__(self, config, **kwargs):
2727
super().__init__(config, **kwargs)
28-
assert len(config["players"]) == 2, "BattleCode is a two-player game"
28+
assert len(config["players"]) == 2, "BattleCode25 is a two-player game"
2929
self.run_cmd_round: str = "python run.py run"
3030
for arg, val in self.game_config.get("args", self.default_args).items():
3131
if isinstance(val, bool):

codeclash/arenas/bridge/bridge.py

Lines changed: 14 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import json
44
import shlex
55
import subprocess
6-
from collections import Counter
76
from concurrent.futures import ThreadPoolExecutor, as_completed
87

98
from tqdm.auto import tqdm
@@ -53,10 +52,7 @@ def validate_code(self, agent: Player) -> tuple[bool, str | None]:
5352
content = agent.environment.execute(f"cat {self.submission}")["output"]
5453

5554
# Check for required function definitions
56-
required_functions = [
57-
"def get_bid(",
58-
"def play_card("
59-
]
55+
required_functions = ["def get_bid(", "def play_card("]
6056

6157
missing = []
6258
for func in required_functions:
@@ -86,7 +82,7 @@ def _run_single_simulation(self, agents: list[Player], idx: int, cmd: str):
8682

8783
def execute_round(self, agents: list[Player]):
8884
"""Execute a round of Bridge games."""
89-
sims = self.game_config.get('sims_per_round', 10)
85+
sims = self.game_config.get("sims_per_round", 10)
9086
self.logger.info(f"Running {sims} Bridge simulations with 4 players")
9187

9288
# Build agent paths for the command
@@ -100,12 +96,7 @@ def execute_round(self, agents: list[Player]):
10096
# Run simulations in parallel
10197
with ThreadPoolExecutor(max_workers=8) as executor:
10298
futures = [
103-
executor.submit(
104-
self._run_single_simulation,
105-
agents,
106-
idx,
107-
f"{cmd} --seed {idx} --dealer {idx % 4}"
108-
)
99+
executor.submit(self._run_single_simulation, agents, idx, f"{cmd} --seed {idx} --dealer {idx % 4}")
109100
for idx in range(sims)
110101
]
111102
for future in tqdm(as_completed(futures), total=len(futures), desc="Bridge simulations"):
@@ -114,11 +105,11 @@ def execute_round(self, agents: list[Player]):
114105
def get_results(self, agents: list[Player], round_num: int, stats: RoundStats):
115106
"""Parse results and determine winners."""
116107
# Initialize team scores
117-
team_scores = {'NS': 0.0, 'EW': 0.0}
108+
team_scores = {"NS": 0.0, "EW": 0.0}
118109
games_played = 0
119110

120111
# Parse all simulation logs
121-
for idx in range(self.game_config.get('sims_per_round', 10)):
112+
for idx in range(self.game_config.get("sims_per_round", 10)):
122113
log_file = self.log_round(round_num) / f"sim_{idx}.json"
123114

124115
if not log_file.exists():
@@ -130,15 +121,15 @@ def get_results(self, agents: list[Player], round_num: int, stats: RoundStats):
130121
result = json.load(f)
131122

132123
# Check for error
133-
if 'error' in result:
124+
if "error" in result:
134125
self.logger.warning(f"Simulation {idx} had error: {result['error']}")
135126
continue
136127

137128
# Extract VP scores for each team
138-
vp_scores = result.get('normalized_score', {})
129+
vp_scores = result.get("normalized_score", {})
139130
if vp_scores:
140-
team_scores['NS'] += vp_scores.get('NS', 0.0)
141-
team_scores['EW'] += vp_scores.get('EW', 0.0)
131+
team_scores["NS"] += vp_scores.get("NS", 0.0)
132+
team_scores["EW"] += vp_scores.get("EW", 0.0)
142133
games_played += 1
143134
except (json.JSONDecodeError, KeyError) as e:
144135
self.logger.warning(f"Error parsing {log_file}: {e}")
@@ -153,20 +144,20 @@ def get_results(self, agents: list[Player], round_num: int, stats: RoundStats):
153144
return
154145

155146
# Average the scores
156-
team_scores['NS'] /= games_played
157-
team_scores['EW'] /= games_played
147+
team_scores["NS"] /= games_played
148+
team_scores["EW"] /= games_played
158149

159150
# Determine winning team
160-
if abs(team_scores['NS'] - team_scores['EW']) < 0.01: # Tie threshold
151+
if abs(team_scores["NS"] - team_scores["EW"]) < 0.01: # Tie threshold
161152
stats.winner = RESULT_TIE
162-
elif team_scores['NS'] > team_scores['EW']:
153+
elif team_scores["NS"] > team_scores["EW"]:
163154
stats.winner = f"{agents[0].name}/{agents[2].name}"
164155
else:
165156
stats.winner = f"{agents[1].name}/{agents[3].name}"
166157

167158
# Assign scores to individual players based on their team
168159
for position, agent in enumerate(agents):
169-
team = 'NS' if position % 2 == 0 else 'EW'
160+
team = "NS" if position % 2 == 0 else "EW"
170161
score = team_scores[team]
171162
stats.scores[agent.name] = score
172163
stats.player_stats[agent.name].score = score
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+
# Clone Figgie game repository
13+
RUN git clone https://github.com/CodeClash-ai/Figgie.git /workspace \
14+
&& cd /workspace \
15+
&& git remote set-url origin https://github.com/CodeClash-ai/Figgie.git
16+
WORKDIR /workspace
17+
18+
# No additional dependencies needed - engine uses only standard library

codeclash/arenas/figgie/__init__.py

Whitespace-only changes.

codeclash/arenas/figgie/figgie.py

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
"""Figgie Arena for CodeClash.
2+
3+
Figgie is a card trading game invented at Jane Street in 2013.
4+
It simulates open-outcry commodities trading.
5+
"""
6+
7+
import re
8+
9+
from codeclash.agents.player import Player
10+
from codeclash.arenas.arena import CodeArena, RoundStats
11+
from codeclash.constants import RESULT_TIE
12+
from codeclash.utils.environment import assert_zero_exit_code
13+
14+
FIGGIE_LOG = "result.log"
15+
16+
17+
class FiggieArena(CodeArena):
18+
name: str = "Figgie"
19+
submission: str = "main.py"
20+
description: str = """Figgie is a card trading game invented at Jane Street in 2013.
21+
It simulates open-outcry commodities trading where players buy and sell cards to accumulate the goal suit.
22+
23+
Game Rules:
24+
- 4 or 5 players, each starting with $350
25+
- 4 players: $50 ante, 10 cards each
26+
- 5 players: $40 ante, 8 cards each
27+
- Pot is always $200
28+
- Deck: one 12-card suit, two 10-card suits, one 8-card suit
29+
- Goal suit: same color as 12-card suit, contains 8 or 10 cards
30+
- At end: $10 per goal suit card, remainder to player(s) with most goal suit cards
31+
32+
Trading Model (Simultaneous Tick):
33+
- Each tick, ALL players are polled for their action
34+
- Actions are executed in random order (simulates racing to the order book)
35+
- Order books cleared after each trade (per official Figgie rules)
36+
37+
Your bot (main.py) must implement:
38+
39+
def get_action(state: dict) -> dict
40+
41+
state contains:
42+
- position: your player index (0-3 or 0-4)
43+
- hand: dict of suit -> count of cards you hold
44+
- money: your current money
45+
- books: dict of suit -> {bid: {price, player} or None, ask: {price, player} or None, last_trade}
46+
- trades: list of completed trades
47+
- num_players: number of players (4 or 5)
48+
- tick: current tick number
49+
50+
Return one of:
51+
- {"type": "pass"}
52+
- {"type": "bid", "suit": "spades", "price": 5}
53+
- {"type": "ask", "suit": "spades", "price": 10}
54+
- {"type": "buy", "suit": "spades"}
55+
- {"type": "sell", "suit": "spades"}
56+
57+
Suits: "spades", "clubs", "hearts", "diamonds"
58+
"""
59+
60+
def __init__(self, config, **kwargs):
61+
super().__init__(config, **kwargs)
62+
num_players = len(config.get("players", []))
63+
if num_players not in [4, 5]:
64+
raise ValueError(f"Figgie requires 4 or 5 players, got {num_players}")
65+
66+
def execute_round(self, agents: list[Player]) -> None:
67+
args = [f"/{agent.name}/{self.submission}" for agent in agents]
68+
cmd = f"python engine.py {' '.join(args)} -r {self.game_config['sims_per_round']} -o {self.log_env} > {self.log_env / FIGGIE_LOG};"
69+
self.logger.info(f"Running game: {cmd}")
70+
assert_zero_exit_code(self.environment.execute(cmd))
71+
72+
def get_results(self, agents: list[Player], round_num: int, stats: RoundStats):
73+
with open(self.log_round(round_num) / FIGGIE_LOG) as f:
74+
round_log = f.read()
75+
lines = round_log.split("FINAL_RESULTS")[-1].splitlines()
76+
77+
scores = {}
78+
for line in lines:
79+
match = re.search(r"Bot\_(\d)\_main:\s(\d+)\srounds\swon", line)
80+
if match:
81+
bot_id = match.group(1)
82+
rounds_won = int(match.group(2))
83+
scores[agents[int(bot_id) - 1].name] = rounds_won
84+
85+
# Handle draws
86+
draw_match = re.search(r"Draws:\s(\d+)", round_log)
87+
if draw_match:
88+
draws = int(draw_match.group(1))
89+
if draws > 0:
90+
scores[RESULT_TIE] = draws
91+
92+
stats.winner = max(scores, key=scores.get) if scores else "unknown"
93+
# Check for tie (equal scores)
94+
if scores:
95+
max_score = max(scores.values())
96+
winners_with_max = [k for k, v in scores.items() if v == max_score and k != RESULT_TIE]
97+
if len(winners_with_max) > 1:
98+
stats.winner = RESULT_TIE
99+
100+
stats.scores = scores
101+
for player, score in scores.items():
102+
if player != RESULT_TIE:
103+
stats.player_stats[player].score = score
104+
105+
def validate_code(self, agent: Player) -> tuple[bool, str | None]:
106+
if self.submission not in agent.environment.execute("ls")["output"]:
107+
return False, f"No {self.submission} file found in the root directory"
108+
109+
bot_content = agent.environment.execute(f"cat {self.submission}")["output"]
110+
111+
if "def get_action(" not in bot_content:
112+
return (
113+
False,
114+
f"{self.submission} must define a get_action(state) function. "
115+
"See the game description for the required signature.",
116+
)
117+
118+
return True, None

0 commit comments

Comments
 (0)