Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions codeclash/arenas/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from codeclash.arenas.bridge.bridge import BridgeArena
from codeclash.arenas.corewar.corewar import CoreWarArena
from codeclash.arenas.dummy.dummy import DummyArena
from codeclash.arenas.figgie.figgie import FiggieArena
from codeclash.arenas.gomoku.gomoku import GomokuArena
from codeclash.arenas.halite.halite import HaliteArena
from codeclash.arenas.halite2.halite2 import Halite2Arena
Expand All @@ -18,6 +19,7 @@
BridgeArena,
CoreWarArena,
DummyArena,
FiggieArena,
GomokuArena,
HaliteArena,
Halite2Arena,
Expand Down
37 changes: 14 additions & 23 deletions codeclash/arenas/bridge/bridge.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import json
import shlex
import subprocess
from collections import Counter
from concurrent.futures import ThreadPoolExecutor, as_completed

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

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

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

def execute_round(self, agents: list[Player]):
"""Execute a round of Bridge games."""
sims = self.game_config.get('sims_per_round', 10)
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
Expand All @@ -100,12 +96,7 @@ def execute_round(self, agents: list[Player]):
# 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}"
)
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"):
Expand All @@ -114,11 +105,11 @@ def execute_round(self, agents: list[Player]):
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}
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)):
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():
Expand All @@ -130,15 +121,15 @@ def get_results(self, agents: list[Player], round_num: int, stats: RoundStats):
result = json.load(f)

# Check for error
if 'error' in result:
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', {})
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)
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}")
Expand All @@ -153,20 +144,20 @@ def get_results(self, agents: list[Player], round_num: int, stats: RoundStats):
return

# Average the scores
team_scores['NS'] /= games_played
team_scores['EW'] /= games_played
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
if abs(team_scores["NS"] - team_scores["EW"]) < 0.01: # Tie threshold
stats.winner = RESULT_TIE
elif team_scores['NS'] > team_scores['EW']:
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'
team = "NS" if position % 2 == 0 else "EW"
score = team_scores[team]
stats.scores[agent.name] = score
stats.player_stats[agent.name].score = score
Expand Down
18 changes: 18 additions & 0 deletions codeclash/arenas/figgie/Figgie.Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
FROM ubuntu:22.04

ENV DEBIAN_FRONTEND=noninteractive

# Install Python 3.10 and basic tools
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
curl ca-certificates python3.10 python3.10-venv \
python3-pip python-is-python3 wget git build-essential jq curl locales \
&& rm -rf /var/lib/apt/lists/*

# Clone Figgie game repository
RUN git clone https://github.com/CodeClash-ai/Figgie.git /workspace \
&& cd /workspace \
&& git remote set-url origin https://github.com/CodeClash-ai/Figgie.git
WORKDIR /workspace

# No additional dependencies needed - engine uses only standard library
Empty file.
120 changes: 120 additions & 0 deletions codeclash/arenas/figgie/figgie.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
"""Figgie Arena for CodeClash.

Figgie is a card trading game invented at Jane Street in 2013.
It simulates open-outcry commodities trading.
"""

import re

from codeclash.agents.player import Player
from codeclash.arenas.arena import CodeArena, RoundStats
from codeclash.constants import RESULT_TIE
from codeclash.utils.environment import assert_zero_exit_code

FIGGIE_LOG = "result.log"


class FiggieArena(CodeArena):
name: str = "Figgie"
submission: str = "main.py"
description: str = """Figgie is a card trading game invented at Jane Street in 2013.
It simulates open-outcry commodities trading where players buy and sell cards to accumulate the goal suit.

Game Rules:
- 4 or 5 players, each starting with $350
- 4 players: $50 ante, 10 cards each
- 5 players: $40 ante, 8 cards each
- Pot is always $200
- Deck: one 12-card suit, two 10-card suits, one 8-card suit
- Goal suit: same color as 12-card suit, contains 8 or 10 cards
- At end: $10 per goal suit card, remainder to player(s) with most goal suit cards

Trading Model (Simultaneous Tick):
- Each tick, ALL players are polled for their action
- Actions are executed in random order (simulates racing to the order book)
- Order books cleared after each trade (per official Figgie rules)

Your bot (main.py) must implement:

def get_action(state: dict) -> dict

state contains:
- position: your player index (0-3 or 0-4)
- hand: dict of suit -> count of cards you hold
- money: your current money
- books: dict of suit -> {bid: {price, player} or None, ask: {price, player} or None, last_trade}
- trades: list of completed trades
- num_players: number of players (4 or 5)
- tick: current tick number

Return one of:
- {"type": "pass"}
- {"type": "bid", "suit": "spades", "price": 5}
- {"type": "ask", "suit": "spades", "price": 10}
- {"type": "buy", "suit": "spades"}
- {"type": "sell", "suit": "spades"}

Suits: "spades", "clubs", "hearts", "diamonds"
"""

def __init__(self, config, **kwargs):
super().__init__(config, **kwargs)
num_players = len(config.get("players", []))
if num_players not in [4, 5]:
raise ValueError(f"Figgie requires 4 or 5 players, got {num_players}")

def execute_round(self, agents: list[Player]) -> None:
args = [f"/{agent.name}/{self.submission}" for agent in agents]
cmd = (
f"python engine.py {' '.join(args)} -r {self.game_config['sims_per_round']} > {self.log_env / FIGGIE_LOG};"
)
self.logger.info(f"Running game: {cmd}")
assert_zero_exit_code(self.environment.execute(cmd))

def get_results(self, agents: list[Player], round_num: int, stats: RoundStats):
with open(self.log_round(round_num) / FIGGIE_LOG) as f:
round_log = f.read()
lines = round_log.split("FINAL_RESULTS")[-1].splitlines()

scores = {}
for line in lines:
match = re.search(r"Bot\_(\d)\_main:\s(\d+)\srounds\swon", line)
if match:
bot_id = match.group(1)
rounds_won = int(match.group(2))
scores[agents[int(bot_id) - 1].name] = rounds_won

# Handle draws
draw_match = re.search(r"Draws:\s(\d+)", round_log)
if draw_match:
draws = int(draw_match.group(1))
if draws > 0:
scores[RESULT_TIE] = draws

stats.winner = max(scores, key=scores.get) if scores else "unknown"
# Check for tie (equal scores)
if scores:
max_score = max(scores.values())
winners_with_max = [k for k, v in scores.items() if v == max_score and k != RESULT_TIE]
if len(winners_with_max) > 1:
stats.winner = RESULT_TIE

stats.scores = scores
for player, score in scores.items():
if player != RESULT_TIE:
stats.player_stats[player].score = score

def validate_code(self, agent: Player) -> tuple[bool, str | None]:
if self.submission not in agent.environment.execute("ls")["output"]:
return False, f"No {self.submission} file found in the root directory"

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

if "def get_action(" not in bot_content:
return (
False,
f"{self.submission} must define a get_action(state) function. "
"See the game description for the required signature.",
)

return True, None
6 changes: 4 additions & 2 deletions codeclash/arenas/gomoku/gomoku.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ def __init__(self, config, **kwargs):

def execute_round(self, agents: list[Player]) -> None:
args = [f"/{agent.name}/{self.submission}" for agent in agents]
cmd = f"python engine.py {' '.join(args)} -r {self.game_config['sims_per_round']} > {self.log_env / GOMOKU_LOG};"
cmd = (
f"python engine.py {' '.join(args)} -r {self.game_config['sims_per_round']} > {self.log_env / GOMOKU_LOG};"
)
self.logger.info(f"Running game: {cmd}")
assert_zero_exit_code(self.environment.execute(cmd))

Expand Down Expand Up @@ -75,7 +77,7 @@ def validate_code(self, agent: Player) -> tuple[bool, str | None]:
return (
False,
f"{self.submission} must define a get_move(board, color) function. "
"See the game description for the required signature."
"See the game description for the required signature.",
)

return True, None
57 changes: 57 additions & 0 deletions configs/test/figgie.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
tournament:
rounds: 3
game:
name: Figgie
sims_per_round: 10
players:
- agent: dummy
name: p1
- agent: dummy
name: p2
- agent: dummy
name: p3
- agent: dummy
name: p4
prompts:
game_description: |
You are a software developer ({{player_id}}) competing in a coding game called Figgie.
Figgie is a card trading game invented at Jane Street that simulates open-outcry commodities trading.

Game Rules:
- 4 or 5 players, each starting with $350
- 4 players: $50 ante, 10 cards each
- 5 players: $40 ante, 8 cards each
- Pot is always $200
- Deck: one 12-card suit, two 10-card suits, one 8-card suit
- Goal suit: same color as 12-card suit, contains 8 or 10 cards (secret until end)
- At end: $10 per goal suit card, remainder to player(s) with most goal suit cards

Trading Model (Simultaneous Tick):
- Each tick, ALL players are polled for their action
- Actions are executed in random order (simulates racing to the order book)
- Order books cleared after each trade (per official Figgie rules)

Your bot (main.py) must implement:
def get_action(state: dict) -> dict

state contains:
- position: your player index
- hand: dict of suit -> count of cards you hold
- money: your current money
- books: dict of suit -> {bid: {price, player} or None, ask: {price, player} or None, last_trade}
- trades: list of completed trades
- num_players: number of players (4 or 5)
- tick: current tick number

Return one of:
- {"type": "pass"}
- {"type": "bid", "suit": "spades", "price": 5}
- {"type": "ask", "suit": "spades", "price": 10}
- {"type": "buy", "suit": "spades"}
- {"type": "sell", "suit": "spades"}

The game is played in {{rounds}} rounds. For every round, you edit program code that controls your bot. This is round {{round}}.
After you and your competitors finish editing your codebases, the game is run automatically.

Your task: improve the bot in `main.py`, located in {{working_dir}}.
{{working_dir}} is your codebase, which contains both your bot and supporting assets.
7 changes: 1 addition & 6 deletions tests/arenas/test_bridge.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,6 @@ def get_bid(game_state):
assert "def play_card(" in error



class TestBridgeRequirements:
"""Test Bridge-specific requirements."""

Expand All @@ -119,11 +118,7 @@ def test_requires_4_players(self, minimal_config, tmp_log_dir):
]

with pytest.raises(ValueError, match="Bridge requires exactly 4 players"):
BridgeArena(
config,
tournament_id="test_tournament",
local_output_dir=tmp_log_dir
)
BridgeArena(config, tournament_id="test_tournament", local_output_dir=tmp_log_dir)

def test_accepts_4_players(self):
"""Test that Bridge accepts exactly 4 players by checking class properties."""
Expand Down
Loading