Skip to content

Commit 9b181cd

Browse files
committed
add draft
1 parent beb66dd commit 9b181cd

13 files changed

Lines changed: 1027 additions & 0 deletions

File tree

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: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
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+
WORKDIR /workspace
13+
14+
# Copy Bridge game server code (game logic, deck management, scoring)
15+
COPY codeclash/arenas/bridge/game_server /workspace/game_server
16+
17+
# 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: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
"""Bridge Arena for CodeClash."""
2+
3+
import json
4+
import sys
5+
from concurrent.futures import ThreadPoolExecutor, as_completed
6+
from pathlib import Path
7+
8+
from tqdm.auto import tqdm
9+
10+
from codeclash.agents.player import Player
11+
from codeclash.arenas.arena import CodeArena, RoundStats
12+
from codeclash.constants import RESULT_TIE
13+
14+
15+
class BridgeArena(CodeArena):
16+
name: str = "Bridge"
17+
submission: str = "bridge_agent.py"
18+
description: str = """Bridge is a 4-player trick-taking card game played in teams.
19+
20+
Teams: North/South (positions 0/2) vs East/West (positions 1/3)
21+
22+
Your bot (bridge_agent.py) must implement these functions:
23+
- get_bid(game_state) -> str: Make bidding decisions, return bid string like "1H", "2NT", "PASS"
24+
- play_card(game_state) -> str: Play a card, return card string like "AS", "7H"
25+
26+
game_state is a dict containing:
27+
- position: Your position (0=North, 1=East, 2=South, 3=West)
28+
- hand: List of cards in your hand (e.g., ["AS", "KH", "7D"])
29+
- bids: List of previous bids
30+
- legal_bids: List of legal bids you can make (during bidding)
31+
- legal_cards: List of legal cards you can play (during playing)
32+
- current_trick: Cards played so far in current trick
33+
- contract: The current contract (if bidding is complete)
34+
"""
35+
default_args: dict = {
36+
"sims_per_round": 10,
37+
}
38+
39+
def __init__(self, config, **kwargs):
40+
# Validate player count before initializing (to avoid Docker build on invalid config)
41+
num_players = len(config.get("players", []))
42+
if num_players != 4:
43+
raise ValueError(f"Bridge requires exactly 4 players, got {num_players}")
44+
super().__init__(config, **kwargs)
45+
46+
def validate_code(self, agent: Player) -> tuple[bool, str | None]:
47+
"""Validate agent code has required functions."""
48+
if self.submission not in agent.environment.execute("ls")["output"]:
49+
return False, f"No {self.submission} file found in root directory"
50+
51+
content = agent.environment.execute(f"cat {self.submission}")["output"]
52+
53+
# Check for required function definitions
54+
required_functions = [
55+
"def get_bid(",
56+
"def play_card("
57+
]
58+
59+
missing = []
60+
for func in required_functions:
61+
if func not in content:
62+
missing.append(func)
63+
64+
if missing:
65+
return False, f"Missing required functions: {', '.join(missing)}"
66+
67+
return True, None
68+
69+
def _run_single_simulation(self, agents: list[Player], idx: int):
70+
"""Run a single Bridge game simulation."""
71+
try:
72+
# Import BridgeGame from game_server
73+
sys.path.insert(0, str(Path(self.environment.config.cwd) / "game_server"))
74+
from game import BridgeGame
75+
76+
# Create game with random seed for reproducibility
77+
game = BridgeGame(seed=idx, dealer=idx % 4)
78+
79+
# Add players at positions 0-3 (North, East, South, West)
80+
for position, agent in enumerate(agents):
81+
game.add_player(position, agent.name)
82+
83+
# Start game (deals cards)
84+
if not game.start_game():
85+
self.logger.error(f"Simulation {idx}: Failed to start game")
86+
return
87+
88+
# Import agent modules dynamically
89+
agent_modules = []
90+
for agent in agents:
91+
agent_path = Path(agent.environment.config.cwd) / self.submission
92+
spec = __import__(
93+
'importlib.util'
94+
).util.spec_from_file_location(f"agent_{agent.name}", agent_path)
95+
module = __import__('importlib.util').util.module_from_spec(spec)
96+
spec.loader.exec_module(module)
97+
agent_modules.append(module)
98+
99+
# Bidding phase
100+
while game.phase == 'bidding':
101+
current_pos = game.current_player
102+
state = game.get_state(current_pos)
103+
104+
# Prepare game_state for agent
105+
agent_state = {
106+
'position': current_pos,
107+
'hand': state.get('hand', game.hands.get(current_pos, [])),
108+
'bids': state['bids'],
109+
'legal_bids': game.get_legal_bids(current_pos),
110+
'dealer': state['dealer'],
111+
'vulnerability': state['vulnerability'],
112+
}
113+
114+
# Get bid from agent
115+
try:
116+
bid = agent_modules[current_pos].get_bid(agent_state)
117+
except Exception as e:
118+
self.logger.error(f"Simulation {idx}: Agent {agents[current_pos].name} error in get_bid: {e}")
119+
bid = "PASS"
120+
121+
# Make bid
122+
if not game.make_bid(current_pos, bid):
123+
self.logger.warning(
124+
f"Simulation {idx}: Invalid bid '{bid}' from {agents[current_pos].name}, defaulting to PASS"
125+
)
126+
game.make_bid(current_pos, "PASS")
127+
128+
# Playing phase
129+
while game.phase == 'playing':
130+
current_pos = game.current_player
131+
state = game.get_state(current_pos)
132+
133+
# Prepare game_state for agent
134+
agent_state = {
135+
'position': current_pos,
136+
'hand': state.get('hand', game.hands.get(current_pos, [])),
137+
'current_trick': state['current_trick'],
138+
'legal_cards': game.get_legal_cards(current_pos),
139+
'contract': state['contract'],
140+
'tricks_won': state['tricks_won'],
141+
}
142+
143+
# Get card from agent
144+
try:
145+
card = agent_modules[current_pos].play_card(agent_state)
146+
except Exception as e:
147+
self.logger.error(f"Simulation {idx}: Agent {agents[current_pos].name} error in play_card: {e}")
148+
legal = game.get_legal_cards(current_pos)
149+
card = legal[0] if legal else "AS"
150+
151+
# Play card
152+
if not game.play_card(current_pos, card):
153+
self.logger.warning(
154+
f"Simulation {idx}: Invalid card '{card}' from {agents[current_pos].name}, using first legal card"
155+
)
156+
legal = game.get_legal_cards(current_pos)
157+
if legal:
158+
game.play_card(current_pos, legal[0])
159+
160+
# Save result to JSON log
161+
result = game.get_result()
162+
log_file = self.log_env / f"sim_{idx}.json"
163+
with open(log_file, 'w') as f:
164+
json.dump(result, f, indent=2)
165+
166+
except Exception as e:
167+
self.logger.error(f"Simulation {idx} failed with error: {e}")
168+
finally:
169+
# Clean up sys.path
170+
if str(Path(self.environment.config.cwd) / "game_server") in sys.path:
171+
sys.path.remove(str(Path(self.environment.config.cwd) / "game_server"))
172+
173+
def execute_round(self, agents: list[Player]):
174+
"""Execute a round of Bridge games."""
175+
sims = self.game_config['sims_per_round']
176+
self.logger.info(f"Running {sims} Bridge simulations with 4 players")
177+
178+
# Run simulations in parallel
179+
with ThreadPoolExecutor(max_workers=20) as executor:
180+
futures = [
181+
executor.submit(self._run_single_simulation, agents, idx)
182+
for idx in range(sims)
183+
]
184+
for future in tqdm(as_completed(futures), total=len(futures), desc="Bridge simulations"):
185+
future.result()
186+
187+
def get_results(self, agents: list[Player], round_num: int, stats: RoundStats):
188+
"""Parse results and determine winners."""
189+
# Initialize team scores
190+
team_scores = {'NS': 0.0, 'EW': 0.0}
191+
games_played = 0
192+
193+
# Parse all simulation logs
194+
for idx in range(self.game_config['sims_per_round']):
195+
log_file = self.log_round(round_num) / f"sim_{idx}.json"
196+
197+
if not log_file.exists():
198+
self.logger.warning(f"Log file {log_file} not found, skipping")
199+
continue
200+
201+
try:
202+
with open(log_file) as f:
203+
result = json.load(f)
204+
205+
# Extract VP scores for each team
206+
vp_scores = result.get('normalized_score', {})
207+
if vp_scores:
208+
team_scores['NS'] += vp_scores.get('NS', 0.0)
209+
team_scores['EW'] += vp_scores.get('EW', 0.0)
210+
games_played += 1
211+
except (json.JSONDecodeError, KeyError) as e:
212+
self.logger.warning(f"Error parsing {log_file}: {e}")
213+
continue
214+
215+
if games_played == 0:
216+
self.logger.error("No valid game results found")
217+
stats.winner = RESULT_TIE
218+
for agent in agents:
219+
stats.scores[agent.name] = 0.0
220+
stats.player_stats[agent.name].score = 0.0
221+
return
222+
223+
# Average the scores
224+
team_scores['NS'] /= games_played
225+
team_scores['EW'] /= games_played
226+
227+
# Determine winning team
228+
if abs(team_scores['NS'] - team_scores['EW']) < 0.01: # Tie threshold
229+
stats.winner = RESULT_TIE
230+
elif team_scores['NS'] > team_scores['EW']:
231+
stats.winner = f"{agents[0].name}/{agents[2].name}"
232+
else:
233+
stats.winner = f"{agents[1].name}/{agents[3].name}"
234+
235+
# Assign scores to individual players based on their team
236+
for position, agent in enumerate(agents):
237+
team = 'NS' if position % 2 == 0 else 'EW'
238+
score = team_scores[team]
239+
stats.scores[agent.name] = score
240+
stats.player_stats[agent.name].score = score
241+
242+
self.logger.info(
243+
f"Round {round_num} results - NS: {team_scores['NS']:.3f}, "
244+
f"EW: {team_scores['EW']:.3f}, Winner: {stats.winner}"
245+
)
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
"""
2+
Example random Bridge agent.
3+
4+
This bot makes random legal moves for both bidding and playing.
5+
Use this as a template for your own Bridge bot.
6+
"""
7+
8+
import random
9+
10+
11+
def get_bid(game_state):
12+
"""
13+
Make a bidding decision.
14+
15+
Args:
16+
game_state: Dictionary containing:
17+
- position: Your position (0=North, 1=East, 2=South, 3=West)
18+
- hand: List of cards in your hand (e.g., ["AS", "KH", "7D"])
19+
- bids: List of previous bids
20+
- legal_bids: List of legal bids you can make
21+
22+
Returns:
23+
A bid string (e.g., "PASS", "1H", "2NT", "3S")
24+
"""
25+
legal_bids = game_state.get("legal_bids", ["PASS"])
26+
27+
# Simple strategy: PASS 80% of the time, random bid 20%
28+
if random.random() < 0.8 or len(legal_bids) == 1:
29+
return "PASS"
30+
31+
# Filter out PASS and choose a random bid
32+
non_pass_bids = [b for b in legal_bids if b != "PASS"]
33+
if non_pass_bids:
34+
return random.choice(non_pass_bids)
35+
return "PASS"
36+
37+
38+
def play_card(game_state):
39+
"""
40+
Play a card.
41+
42+
Args:
43+
game_state: Dictionary containing:
44+
- position: Your position (0=North, 1=East, 2=South, 3=West)
45+
- hand: List of cards currently in your hand
46+
- current_trick: List of cards played so far in current trick
47+
- legal_cards: List of legal cards you can play
48+
- contract: The current contract (level, suit, declarer)
49+
- tricks_won: Tricks won by each team so far
50+
51+
Returns:
52+
A card string (e.g., "AS", "7H", "KD")
53+
"""
54+
legal_cards = game_state.get("legal_cards", game_state.get("hand", []))
55+
56+
if not legal_cards:
57+
# Should never happen, but fallback to first card in hand
58+
hand = game_state.get("hand", [])
59+
return hand[0] if hand else "AS"
60+
61+
# Play a random legal card
62+
return random.choice(legal_cards)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Bridge game server logic."""

0 commit comments

Comments
 (0)