Skip to content

Commit fe5f9bd

Browse files
committed
Fix Bridge arena to run simulations inside Docker
- Refactor bridge.py to use run_game.py runner script (like RobotRumble) - Add example config Bridge__claude-3-5-haiku__r2__s10.yaml - Games now execute properly with correct scoring
1 parent c94b859 commit fe5f9bd

2 files changed

Lines changed: 112 additions & 107 deletions

File tree

codeclash/arenas/bridge/bridge.py

Lines changed: 38 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
"""Bridge Arena for CodeClash."""
22

33
import json
4-
import sys
4+
import shlex
5+
import subprocess
6+
from collections import Counter
57
from concurrent.futures import ThreadPoolExecutor, as_completed
6-
from pathlib import Path
78

89
from tqdm.auto import tqdm
910

@@ -42,6 +43,7 @@ def __init__(self, config, **kwargs):
4243
if num_players != 4:
4344
raise ValueError(f"Bridge requires exactly 4 players, got {num_players}")
4445
super().__init__(config, **kwargs)
46+
self.run_cmd = "python3 /workspace/run_game.py"
4547

4648
def validate_code(self, agent: Player) -> tuple[bool, str | None]:
4749
"""Validate agent code has required functions."""
@@ -66,120 +68,44 @@ def validate_code(self, agent: Player) -> tuple[bool, str | None]:
6668

6769
return True, None
6870

69-
def _run_single_simulation(self, agents: list[Player], idx: int):
71+
def _run_single_simulation(self, agents: list[Player], idx: int, cmd: str):
7072
"""Run a single Bridge game simulation."""
71-
game_server_path = str(Path(self.environment.config.cwd) / "game_server")
72-
try:
73-
# Import BridgeGame from game_server (cloned from CodeClash-ai/Bridge repo)
74-
sys.path.insert(0, game_server_path)
75-
from game import BridgeGame
76-
77-
# Create game with random seed for reproducibility
78-
game = BridgeGame(seed=idx, dealer=idx % 4)
73+
full_cmd = f"{cmd} -o {self.log_env / f'sim_{idx}.json'}"
7974

80-
# Add players at positions 0-3 (North, East, South, West)
81-
for position, agent in enumerate(agents):
82-
game.add_player(position, agent.name)
83-
84-
# Start game (deals cards)
85-
if not game.start_game():
86-
self.logger.error(f"Simulation {idx}: Failed to start game")
87-
return
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 ""
8880

89-
# Import agent modules dynamically
90-
agent_modules = []
91-
for agent in agents:
92-
agent_path = Path(agent.environment.config.cwd) / self.submission
93-
spec = __import__(
94-
'importlib.util'
95-
).util.spec_from_file_location(f"agent_{agent.name}", agent_path)
96-
module = __import__('importlib.util').util.module_from_spec(spec)
97-
spec.loader.exec_module(module)
98-
agent_modules.append(module)
99-
100-
# Bidding phase
101-
while game.phase == 'bidding':
102-
current_pos = game.current_player
103-
state = game.get_state(current_pos)
104-
105-
# Prepare game_state for agent
106-
agent_state = {
107-
'position': current_pos,
108-
'hand': state.get('hand', game.hands.get(current_pos, [])),
109-
'bids': state['bids'],
110-
'legal_bids': game.get_legal_bids(current_pos),
111-
'dealer': state['dealer'],
112-
'vulnerability': state['vulnerability'],
113-
}
114-
115-
# Get bid from agent
116-
try:
117-
bid = agent_modules[current_pos].get_bid(agent_state)
118-
except Exception as e:
119-
self.logger.error(f"Simulation {idx}: Agent {agents[current_pos].name} error in get_bid: {e}")
120-
bid = "PASS"
121-
122-
# Make bid
123-
if not game.make_bid(current_pos, bid):
124-
self.logger.warning(
125-
f"Simulation {idx}: Invalid bid '{bid}' from {agents[current_pos].name}, defaulting to PASS"
126-
)
127-
game.make_bid(current_pos, "PASS")
128-
129-
# Playing phase
130-
while game.phase == 'playing':
131-
current_pos = game.current_player
132-
state = game.get_state(current_pos)
133-
134-
# Prepare game_state for agent
135-
agent_state = {
136-
'position': current_pos,
137-
'hand': state.get('hand', game.hands.get(current_pos, [])),
138-
'current_trick': state['current_trick'],
139-
'legal_cards': game.get_legal_cards(current_pos),
140-
'contract': state['contract'],
141-
'tricks_won': state['tricks_won'],
142-
}
143-
144-
# Get card from agent
145-
try:
146-
card = agent_modules[current_pos].play_card(agent_state)
147-
except Exception as e:
148-
self.logger.error(f"Simulation {idx}: Agent {agents[current_pos].name} error in play_card: {e}")
149-
legal = game.get_legal_cards(current_pos)
150-
card = legal[0] if legal else "AS"
151-
152-
# Play card
153-
if not game.play_card(current_pos, card):
154-
self.logger.warning(
155-
f"Simulation {idx}: Invalid card '{card}' from {agents[current_pos].name}, using first legal card"
156-
)
157-
legal = game.get_legal_cards(current_pos)
158-
if legal:
159-
game.play_card(current_pos, legal[0])
160-
161-
# Save result to JSON log
162-
result = game.get_result()
163-
log_file = self.log_env / f"sim_{idx}.json"
164-
with open(log_file, 'w') as f:
165-
json.dump(result, f, indent=2)
166-
167-
except Exception as e:
168-
self.logger.error(f"Simulation {idx} failed with error: {e}")
169-
finally:
170-
# Clean up sys.path
171-
if game_server_path in sys.path:
172-
sys.path.remove(game_server_path)
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"]
17386

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

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+
179100
# Run simulations in parallel
180-
with ThreadPoolExecutor(max_workers=20) as executor:
101+
with ThreadPoolExecutor(max_workers=8) as executor:
181102
futures = [
182-
executor.submit(self._run_single_simulation, agents, idx)
103+
executor.submit(
104+
self._run_single_simulation,
105+
agents,
106+
idx,
107+
f"{cmd} --seed {idx} --dealer {idx % 4}"
108+
)
183109
for idx in range(sims)
184110
]
185111
for future in tqdm(as_completed(futures), total=len(futures), desc="Bridge simulations"):
@@ -192,7 +118,7 @@ def get_results(self, agents: list[Player], round_num: int, stats: RoundStats):
192118
games_played = 0
193119

194120
# Parse all simulation logs
195-
for idx in range(self.game_config['sims_per_round']):
121+
for idx in range(self.game_config.get('sims_per_round', 10)):
196122
log_file = self.log_round(round_num) / f"sim_{idx}.json"
197123

198124
if not log_file.exists():
@@ -203,6 +129,11 @@ def get_results(self, agents: list[Player], round_num: int, stats: RoundStats):
203129
with open(log_file) as f:
204130
result = json.load(f)
205131

132+
# Check for error
133+
if 'error' in result:
134+
self.logger.warning(f"Simulation {idx} had error: {result['error']}")
135+
continue
136+
206137
# Extract VP scores for each team
207138
vp_scores = result.get('normalized_score', {})
208139
if vp_scores:
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
tournament:
2+
rounds: 2
3+
game:
4+
name: Bridge
5+
sims_per_round: 10
6+
players:
7+
- agent: mini
8+
name: north
9+
config:
10+
agent: !include mini/default.yaml
11+
model:
12+
model_name: 'anthropic/claude-3-5-haiku-20241022'
13+
model_kwargs:
14+
temperature: 0.2
15+
max_tokens: 4096
16+
- agent: mini
17+
name: east
18+
config:
19+
agent: !include mini/default.yaml
20+
model:
21+
model_name: 'anthropic/claude-3-5-haiku-20241022'
22+
model_kwargs:
23+
temperature: 0.2
24+
max_tokens: 4096
25+
- agent: mini
26+
name: south
27+
config:
28+
agent: !include mini/default.yaml
29+
model:
30+
model_name: 'anthropic/claude-3-5-haiku-20241022'
31+
model_kwargs:
32+
temperature: 0.2
33+
max_tokens: 4096
34+
- agent: mini
35+
name: west
36+
config:
37+
agent: !include mini/default.yaml
38+
model:
39+
model_name: 'anthropic/claude-3-5-haiku-20241022'
40+
model_kwargs:
41+
temperature: 0.2
42+
max_tokens: 4096
43+
prompts:
44+
game_description: |-
45+
You are a software developer ({{player_id}}) competing in a coding game called Bridge.
46+
Bridge is a 4-player trick-taking card game played in partnerships: North/South vs East/West.
47+
48+
Your position: {{player_id}} (North=0, East=1, South=2, West=3)
49+
Teams: North/South (positions 0/2) vs East/West (positions 1/3)
50+
51+
The game is played in {{total_rounds}} rounds. For every round, you (and your competitors) edit program code that controls your bot. This is round {{round}}.
52+
After everyone finishes editing their codebases, the game is run automatically.
53+
54+
Your task: improve the bot in `bridge_agent.py`, located in {{working_dir}}.
55+
{{working_dir}} is your codebase, which contains both your bot and supporting assets.
56+
All of your commands will be executed in the {{working_dir}} directory.
57+
58+
Your bot must implement two functions:
59+
- get_bid(game_state) -> str: Make bidding decisions during the auction
60+
- play_card(game_state) -> str: Play a card during the play phase
61+
62+
game_state contains:
63+
- position: Your seat (0-3)
64+
- hand: Your cards (e.g., ["AS", "KH", "7D", "TC"])
65+
- legal_bids/legal_cards: Valid moves you can make
66+
- bids: Previous bids in the auction
67+
- current_trick: Cards played in current trick
68+
- contract: The final contract (after bidding)
69+
- tricks_won: Tricks won by each team
70+
71+
Card notation: <rank><suit> where rank is A,K,Q,J,T,9,8,7,6,5,4,3,2 and suit is S,H,D,C
72+
Bid notation: "PASS" or level(1-7) + strain(C,D,H,S,NT) like "1H", "3NT", "7S"
73+
74+
Check examples/random_agent.py in the workspace for a starting template.

0 commit comments

Comments
 (0)