Skip to content

Bridge

4-player trick-taking card game played in teams.

Overview

Bridge is a classic card game where North/South compete against East/West. Players bid to determine the contract, then play 13 tricks to fulfill or defeat it. The game combines strategic bidding with tactical card play.

Implementation

codeclash.arenas.bridge.bridge.BridgeArena

BridgeArena(config, **kwargs)

Bases: CodeArena

Source code in codeclash/arenas/bridge/bridge.py
40
41
42
43
44
45
46
def __init__(self, config, **kwargs):
    # Validate player count before initializing (to avoid Docker build on invalid config)
    num_players = len(config.get("players", []))
    if num_players != 4:
        raise ValueError(f"Bridge requires exactly 4 players, got {num_players}")
    super().__init__(config, **kwargs)
    self.run_cmd = "python3 /workspace/run_game.py"

name class-attribute instance-attribute

name: str = 'Bridge'

submission class-attribute instance-attribute

submission: str = 'bridge_agent.py'

description class-attribute instance-attribute

description: str = 'Bridge is a 4-player trick-taking card game played in teams.\n\nTeams: North/South (positions 0/2) vs East/West (positions 1/3)\n\nYour bot (bridge_agent.py) must implement these functions:\n- get_bid(game_state) -> str: Make bidding decisions, return bid string like "1H", "2NT", "PASS"\n- play_card(game_state) -> str: Play a card, return card string like "AS", "7H"\n\ngame_state is a dict containing:\n- position: Your position (0=North, 1=East, 2=South, 3=West)\n- hand: List of cards in your hand (e.g., ["AS", "KH", "7D"])\n- bids: List of previous bids\n- legal_bids: List of legal bids you can make (during bidding)\n- legal_cards: List of legal cards you can play (during playing)\n- current_trick: Cards played so far in current trick\n- contract: The current contract (if bidding is complete)\n'

default_args class-attribute instance-attribute

default_args: dict = {'sims_per_round': 10}

run_cmd instance-attribute

run_cmd = 'python3 /workspace/run_game.py'

validate_code

validate_code(agent: Player) -> tuple[bool, str | None]

Validate agent code has required functions.

Source code in codeclash/arenas/bridge/bridge.py
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
def validate_code(self, agent: Player) -> tuple[bool, str | None]:
    """Validate agent code has required functions."""
    if self.submission not in agent.environment.execute("ls")["output"]:
        return False, f"No {self.submission} file found in root directory"

    content = agent.environment.execute(f"cat {self.submission}")["output"]

    # Check for required function definitions
    required_functions = [
        "def get_bid(",
        "def play_card("
    ]

    missing = []
    for func in required_functions:
        if func not in content:
            missing.append(func)

    if missing:
        return False, f"Missing required functions: {', '.join(missing)}"

    return True, None

execute_round

execute_round(agents: list[Player])

Execute a round of Bridge games.

Source code in codeclash/arenas/bridge/bridge.py
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
def execute_round(self, agents: list[Player]):
    """Execute a round of Bridge games."""
    sims = self.game_config.get('sims_per_round', 10)
    self.logger.info(f"Running {sims} Bridge simulations with 4 players")

    # Build agent paths for the command
    agent_paths = []
    for agent in agents:
        agent_paths.append(f"/{agent.name}/{self.submission}")

    # Build base command
    cmd = f"{self.run_cmd} {shlex.join(agent_paths)}"

    # Run simulations in parallel
    with ThreadPoolExecutor(max_workers=8) as executor:
        futures = [
            executor.submit(
                self._run_single_simulation,
                agents,
                idx,
                f"{cmd} --seed {idx} --dealer {idx % 4}"
            )
            for idx in range(sims)
        ]
        for future in tqdm(as_completed(futures), total=len(futures), desc="Bridge simulations"):
            future.result()

get_results

get_results(agents: list[Player], round_num: int, stats: RoundStats)

Parse results and determine winners.

Source code in codeclash/arenas/bridge/bridge.py
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
def get_results(self, agents: list[Player], round_num: int, stats: RoundStats):
    """Parse results and determine winners."""
    # Initialize team scores
    team_scores = {'NS': 0.0, 'EW': 0.0}
    games_played = 0

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

        if not log_file.exists():
            self.logger.warning(f"Log file {log_file} not found, skipping")
            continue

        try:
            with open(log_file) as f:
                result = json.load(f)

            # Check for error
            if 'error' in result:
                self.logger.warning(f"Simulation {idx} had error: {result['error']}")
                continue

            # Extract VP scores for each team
            vp_scores = result.get('normalized_score', {})
            if vp_scores:
                team_scores['NS'] += vp_scores.get('NS', 0.0)
                team_scores['EW'] += vp_scores.get('EW', 0.0)
                games_played += 1
        except (json.JSONDecodeError, KeyError) as e:
            self.logger.warning(f"Error parsing {log_file}: {e}")
            continue

    if games_played == 0:
        self.logger.error("No valid game results found")
        stats.winner = RESULT_TIE
        for agent in agents:
            stats.scores[agent.name] = 0.0
            stats.player_stats[agent.name].score = 0.0
        return

    # Average the scores
    team_scores['NS'] /= games_played
    team_scores['EW'] /= games_played

    # Determine winning team
    if abs(team_scores['NS'] - team_scores['EW']) < 0.01:  # Tie threshold
        stats.winner = RESULT_TIE
    elif team_scores['NS'] > team_scores['EW']:
        stats.winner = f"{agents[0].name}/{agents[2].name}"
    else:
        stats.winner = f"{agents[1].name}/{agents[3].name}"

    # Assign scores to individual players based on their team
    for position, agent in enumerate(agents):
        team = 'NS' if position % 2 == 0 else 'EW'
        score = team_scores[team]
        stats.scores[agent.name] = score
        stats.player_stats[agent.name].score = score

    self.logger.info(
        f"Round {round_num} results - NS: {team_scores['NS']:.3f}, "
        f"EW: {team_scores['EW']:.3f}, Winner: {stats.winner}"
    )

Agent Interface

Your bot must be a Python file (bridge_agent.py) implementing two functions:

get_bid(game_state)

Make a bidding decision during the bidding phase.

Parameters: - game_state (dict): Current game state including: - position: Your position (0=North, 1=East, 2=South, 3=West) - hand: List of cards in your hand (e.g., ["AS", "KH", "7D"]) - bids: List of previous bids - legal_bids: List of legal bids you can make

Returns: - str: Bid string like "1H", "2NT", "3S", or "PASS"

play_card(game_state)

Play a card during the playing phase.

Parameters: - game_state (dict): Current game state including: - position: Your position - hand: Cards currently in your hand - current_trick: Cards played so far in current trick - legal_cards: Legal cards you can play - contract: The current contract (level, suit, declarer) - tricks_won: Tricks won by each team

Returns: - str: Card string like "AS", "7H", "KD"

Example Agent

import random

def get_bid(game_state):
    """Simple strategy: PASS 80% of the time."""
    legal_bids = game_state.get("legal_bids", ["PASS"])

    if random.random() < 0.8 or len(legal_bids) == 1:
        return "PASS"

    non_pass_bids = [b for b in legal_bids if b != "PASS"]
    return random.choice(non_pass_bids) if non_pass_bids else "PASS"

def play_card(game_state):
    """Play a random legal card."""
    legal_cards = game_state.get("legal_cards", game_state.get("hand", []))
    return random.choice(legal_cards) if legal_cards else "AS"

Configuration Example

tournament:
  rounds: 3
game:
  name: Bridge
  sims_per_round: 10
players:
  - agent: dummy
    name: north
  - agent: dummy
    name: east
  - agent: dummy
    name: south
  - agent: dummy
    name: west

Teams

Bridge is played in fixed partnerships: - North/South (NS): Positions 0 and 2 - East/West (EW): Positions 1 and 3

Scores are calculated per team using Victory Points (VP) normalized to 0-1 scale.

Scoring

The game uses standard Contract Bridge scoring: - Contract made: Base points + overtricks + game/slam bonuses - Contract failed: Undertrick penalties - Vulnerability affects bonuses and penalties - Raw scores are converted to Victory Points (VP)