Skip to content

Commit 3d669e1

Browse files
MuhtashamPlayer
andauthored
add bridge (#86)
* add draft * fix imports * Move Bridge game server to separate CodeClash-ai/Bridge repo - Update Dockerfile to clone from https://github.com/CodeClash-ai/Bridge - Remove game_server/ and examples/ (now in separate repo) - Update bridge.py path references * 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 --------- Co-authored-by: Player <[email protected]>
1 parent 0e6ef7c commit 3d669e1

File tree

9 files changed

+549
-0
lines changed

9 files changed

+549
-0
lines changed

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.halite.halite import HaliteArena
@@ -13,6 +14,7 @@
1314
ARENAS = [
1415
BattleCodeArena,
1516
BattleSnakeArena,
17+
BridgeArena,
1618
CoreWarArena,
1719
DummyArena,
1820
HaliteArena,
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+
)
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.

configs/test/bridge.yaml

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
tournament:
2+
rounds: 3
3+
game:
4+
name: Bridge
5+
sims_per_round: 10
6+
players:
7+
- agent: dummy
8+
name: north
9+
- agent: dummy
10+
name: east
11+
- agent: dummy
12+
name: south
13+
- agent: dummy
14+
name: west
15+
prompts:
16+
game_description: |
17+
You are a software developer ({{player_id}}) competing in a Bridge coding game.
18+
19+
Bridge is a 4-player trick-taking card game played in teams:
20+
- North/South (positions 0/2) vs East/West (positions 1/3)
21+
22+
The game is played in {{rounds}} rounds. For every round, you edit your bot code (bridge_agent.py).
23+
After all players finish editing, games are run automatically. This is round {{round}}.
24+
25+
Your bot must implement two functions:
26+
- get_bid(game_state) -> str: Return bid like "1H", "2NT", "PASS"
27+
- play_card(game_state) -> str: Return card like "AS", "7H"
28+
29+
The game_state dict contains:
30+
- position: Your position (0=North, 1=East, 2=South, 3=West)
31+
- hand: List of cards in your hand
32+
- bids: List of previous bids
33+
- legal_bids: Legal bids you can make (during bidding)
34+
- legal_cards: Legal cards you can play (during playing)
35+
- current_trick: Cards played so far in current trick
36+
- contract: The current contract (if bidding is complete)

0 commit comments

Comments
 (0)