Skip to content

Commit 34a936e

Browse files
Add Figgie arena (#90)
* Add Figgie arena (Jane Street trading card game) * Fix ruff issues * Update Figgie arena for simultaneous tick model * Update figgie test config with new interface - Use 'ask' instead of 'offer' action type - Add state description with order book interface - Document simultaneous tick trading model - Support both 4 and 5 player games * Write per-sim logs --------- Co-authored-by: John Yang <byjohnyang@gmail.com>
1 parent 7fb9f18 commit 34a936e

9 files changed

Lines changed: 334 additions & 31 deletions

File tree

codeclash/arenas/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from codeclash.arenas.bridge.bridge import BridgeArena
55
from codeclash.arenas.corewar.corewar import CoreWarArena
66
from codeclash.arenas.dummy.dummy import DummyArena
7+
from codeclash.arenas.figgie.figgie import FiggieArena
78
from codeclash.arenas.gomoku.gomoku import GomokuArena
89
from codeclash.arenas.halite.halite import HaliteArena
910
from codeclash.arenas.halite2.halite2 import Halite2Arena
@@ -18,6 +19,7 @@
1819
BridgeArena,
1920
CoreWarArena,
2021
DummyArena,
22+
FiggieArena,
2123
GomokuArena,
2224
HaliteArena,
2325
Halite2Arena,

codeclash/arenas/bridge/bridge.py

Lines changed: 14 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import json
44
import shlex
55
import subprocess
6-
from collections import Counter
76
from concurrent.futures import ThreadPoolExecutor, as_completed
87

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

5554
# Check for required function definitions
56-
required_functions = [
57-
"def get_bid(",
58-
"def play_card("
59-
]
55+
required_functions = ["def get_bid(", "def play_card("]
6056

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

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

9288
# Build agent paths for the command
@@ -100,12 +96,7 @@ def execute_round(self, agents: list[Player]):
10096
# Run simulations in parallel
10197
with ThreadPoolExecutor(max_workers=8) as executor:
10298
futures = [
103-
executor.submit(
104-
self._run_single_simulation,
105-
agents,
106-
idx,
107-
f"{cmd} --seed {idx} --dealer {idx % 4}"
108-
)
99+
executor.submit(self._run_single_simulation, agents, idx, f"{cmd} --seed {idx} --dealer {idx % 4}")
109100
for idx in range(sims)
110101
]
111102
for future in tqdm(as_completed(futures), total=len(futures), desc="Bridge simulations"):
@@ -114,11 +105,11 @@ def execute_round(self, agents: list[Player]):
114105
def get_results(self, agents: list[Player], round_num: int, stats: RoundStats):
115106
"""Parse results and determine winners."""
116107
# Initialize team scores
117-
team_scores = {'NS': 0.0, 'EW': 0.0}
108+
team_scores = {"NS": 0.0, "EW": 0.0}
118109
games_played = 0
119110

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

124115
if not log_file.exists():
@@ -130,15 +121,15 @@ def get_results(self, agents: list[Player], round_num: int, stats: RoundStats):
130121
result = json.load(f)
131122

132123
# Check for error
133-
if 'error' in result:
124+
if "error" in result:
134125
self.logger.warning(f"Simulation {idx} had error: {result['error']}")
135126
continue
136127

137128
# Extract VP scores for each team
138-
vp_scores = result.get('normalized_score', {})
129+
vp_scores = result.get("normalized_score", {})
139130
if vp_scores:
140-
team_scores['NS'] += vp_scores.get('NS', 0.0)
141-
team_scores['EW'] += vp_scores.get('EW', 0.0)
131+
team_scores["NS"] += vp_scores.get("NS", 0.0)
132+
team_scores["EW"] += vp_scores.get("EW", 0.0)
142133
games_played += 1
143134
except (json.JSONDecodeError, KeyError) as e:
144135
self.logger.warning(f"Error parsing {log_file}: {e}")
@@ -153,20 +144,20 @@ def get_results(self, agents: list[Player], round_num: int, stats: RoundStats):
153144
return
154145

155146
# Average the scores
156-
team_scores['NS'] /= games_played
157-
team_scores['EW'] /= games_played
147+
team_scores["NS"] /= games_played
148+
team_scores["EW"] /= games_played
158149

159150
# Determine winning team
160-
if abs(team_scores['NS'] - team_scores['EW']) < 0.01: # Tie threshold
151+
if abs(team_scores["NS"] - team_scores["EW"]) < 0.01: # Tie threshold
161152
stats.winner = RESULT_TIE
162-
elif team_scores['NS'] > team_scores['EW']:
153+
elif team_scores["NS"] > team_scores["EW"]:
163154
stats.winner = f"{agents[0].name}/{agents[2].name}"
164155
else:
165156
stats.winner = f"{agents[1].name}/{agents[3].name}"
166157

167158
# Assign scores to individual players based on their team
168159
for position, agent in enumerate(agents):
169-
team = 'NS' if position % 2 == 0 else 'EW'
160+
team = "NS" if position % 2 == 0 else "EW"
170161
score = team_scores[team]
171162
stats.scores[agent.name] = score
172163
stats.player_stats[agent.name].score = score
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+
# Clone Figgie game repository
13+
RUN git clone https://github.com/CodeClash-ai/Figgie.git /workspace \
14+
&& cd /workspace \
15+
&& git remote set-url origin https://github.com/CodeClash-ai/Figgie.git
16+
WORKDIR /workspace
17+
18+
# No additional dependencies needed - engine uses only standard library

codeclash/arenas/figgie/__init__.py

Whitespace-only changes.

codeclash/arenas/figgie/figgie.py

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
"""Figgie Arena for CodeClash.
2+
3+
Figgie is a card trading game invented at Jane Street in 2013.
4+
It simulates open-outcry commodities trading.
5+
"""
6+
7+
import re
8+
9+
from codeclash.agents.player import Player
10+
from codeclash.arenas.arena import CodeArena, RoundStats
11+
from codeclash.constants import RESULT_TIE
12+
from codeclash.utils.environment import assert_zero_exit_code
13+
14+
FIGGIE_LOG = "result.log"
15+
16+
17+
class FiggieArena(CodeArena):
18+
name: str = "Figgie"
19+
submission: str = "main.py"
20+
description: str = """Figgie is a card trading game invented at Jane Street in 2013.
21+
It simulates open-outcry commodities trading where players buy and sell cards to accumulate the goal suit.
22+
23+
Game Rules:
24+
- 4 or 5 players, each starting with $350
25+
- 4 players: $50 ante, 10 cards each
26+
- 5 players: $40 ante, 8 cards each
27+
- Pot is always $200
28+
- Deck: one 12-card suit, two 10-card suits, one 8-card suit
29+
- Goal suit: same color as 12-card suit, contains 8 or 10 cards
30+
- At end: $10 per goal suit card, remainder to player(s) with most goal suit cards
31+
32+
Trading Model (Simultaneous Tick):
33+
- Each tick, ALL players are polled for their action
34+
- Actions are executed in random order (simulates racing to the order book)
35+
- Order books cleared after each trade (per official Figgie rules)
36+
37+
Your bot (main.py) must implement:
38+
39+
def get_action(state: dict) -> dict
40+
41+
state contains:
42+
- position: your player index (0-3 or 0-4)
43+
- hand: dict of suit -> count of cards you hold
44+
- money: your current money
45+
- books: dict of suit -> {bid: {price, player} or None, ask: {price, player} or None, last_trade}
46+
- trades: list of completed trades
47+
- num_players: number of players (4 or 5)
48+
- tick: current tick number
49+
50+
Return one of:
51+
- {"type": "pass"}
52+
- {"type": "bid", "suit": "spades", "price": 5}
53+
- {"type": "ask", "suit": "spades", "price": 10}
54+
- {"type": "buy", "suit": "spades"}
55+
- {"type": "sell", "suit": "spades"}
56+
57+
Suits: "spades", "clubs", "hearts", "diamonds"
58+
"""
59+
60+
def __init__(self, config, **kwargs):
61+
super().__init__(config, **kwargs)
62+
num_players = len(config.get("players", []))
63+
if num_players not in [4, 5]:
64+
raise ValueError(f"Figgie requires 4 or 5 players, got {num_players}")
65+
66+
def execute_round(self, agents: list[Player]) -> None:
67+
args = [f"/{agent.name}/{self.submission}" for agent in agents]
68+
cmd = f"python engine.py {' '.join(args)} -r {self.game_config['sims_per_round']} -o {self.log_env} > {self.log_env / FIGGIE_LOG};"
69+
self.logger.info(f"Running game: {cmd}")
70+
assert_zero_exit_code(self.environment.execute(cmd))
71+
72+
def get_results(self, agents: list[Player], round_num: int, stats: RoundStats):
73+
with open(self.log_round(round_num) / FIGGIE_LOG) as f:
74+
round_log = f.read()
75+
lines = round_log.split("FINAL_RESULTS")[-1].splitlines()
76+
77+
scores = {}
78+
for line in lines:
79+
match = re.search(r"Bot\_(\d)\_main:\s(\d+)\srounds\swon", line)
80+
if match:
81+
bot_id = match.group(1)
82+
rounds_won = int(match.group(2))
83+
scores[agents[int(bot_id) - 1].name] = rounds_won
84+
85+
# Handle draws
86+
draw_match = re.search(r"Draws:\s(\d+)", round_log)
87+
if draw_match:
88+
draws = int(draw_match.group(1))
89+
if draws > 0:
90+
scores[RESULT_TIE] = draws
91+
92+
stats.winner = max(scores, key=scores.get) if scores else "unknown"
93+
# Check for tie (equal scores)
94+
if scores:
95+
max_score = max(scores.values())
96+
winners_with_max = [k for k, v in scores.items() if v == max_score and k != RESULT_TIE]
97+
if len(winners_with_max) > 1:
98+
stats.winner = RESULT_TIE
99+
100+
stats.scores = scores
101+
for player, score in scores.items():
102+
if player != RESULT_TIE:
103+
stats.player_stats[player].score = score
104+
105+
def validate_code(self, agent: Player) -> tuple[bool, str | None]:
106+
if self.submission not in agent.environment.execute("ls")["output"]:
107+
return False, f"No {self.submission} file found in the root directory"
108+
109+
bot_content = agent.environment.execute(f"cat {self.submission}")["output"]
110+
111+
if "def get_action(" not in bot_content:
112+
return (
113+
False,
114+
f"{self.submission} must define a get_action(state) function. "
115+
"See the game description for the required signature.",
116+
)
117+
118+
return True, None

codeclash/arenas/gomoku/gomoku.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,9 @@ def __init__(self, config, **kwargs):
2828

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

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

8183
return True, None

configs/test/figgie.yaml

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
tournament:
2+
rounds: 3
3+
game:
4+
name: Figgie
5+
sims_per_round: 10
6+
players:
7+
- agent: dummy
8+
name: p1
9+
- agent: dummy
10+
name: p2
11+
- agent: dummy
12+
name: p3
13+
- agent: dummy
14+
name: p4
15+
prompts:
16+
game_description: |
17+
You are a software developer ({{player_id}}) competing in a coding game called Figgie.
18+
Figgie is a card trading game invented at Jane Street that simulates open-outcry commodities trading.
19+
20+
Game Rules:
21+
- 4 or 5 players, each starting with $350
22+
- 4 players: $50 ante, 10 cards each
23+
- 5 players: $40 ante, 8 cards each
24+
- Pot is always $200
25+
- Deck: one 12-card suit, two 10-card suits, one 8-card suit
26+
- Goal suit: same color as 12-card suit, contains 8 or 10 cards (secret until end)
27+
- At end: $10 per goal suit card, remainder to player(s) with most goal suit cards
28+
29+
Trading Model (Simultaneous Tick):
30+
- Each tick, ALL players are polled for their action
31+
- Actions are executed in random order (simulates racing to the order book)
32+
- Order books cleared after each trade (per official Figgie rules)
33+
34+
Your bot (main.py) must implement:
35+
def get_action(state: dict) -> dict
36+
37+
state contains:
38+
- position: your player index
39+
- hand: dict of suit -> count of cards you hold
40+
- money: your current money
41+
- books: dict of suit -> {bid: {price, player} or None, ask: {price, player} or None, last_trade}
42+
- trades: list of completed trades
43+
- num_players: number of players (4 or 5)
44+
- tick: current tick number
45+
46+
Return one of:
47+
- {"type": "pass"}
48+
- {"type": "bid", "suit": "spades", "price": 5}
49+
- {"type": "ask", "suit": "spades", "price": 10}
50+
- {"type": "buy", "suit": "spades"}
51+
- {"type": "sell", "suit": "spades"}
52+
53+
The game is played in {{rounds}} rounds. For every round, you edit program code that controls your bot. This is round {{round}}.
54+
After you and your competitors finish editing your codebases, the game is run automatically.
55+
56+
Your task: improve the bot in `main.py`, located in {{working_dir}}.
57+
{{working_dir}} is your codebase, which contains both your bot and supporting assets.

tests/arenas/test_bridge.py

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,6 @@ def get_bid(game_state):
105105
assert "def play_card(" in error
106106

107107

108-
109108
class TestBridgeRequirements:
110109
"""Test Bridge-specific requirements."""
111110

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

121120
with pytest.raises(ValueError, match="Bridge requires exactly 4 players"):
122-
BridgeArena(
123-
config,
124-
tournament_id="test_tournament",
125-
local_output_dir=tmp_log_dir
126-
)
121+
BridgeArena(config, tournament_id="test_tournament", local_output_dir=tmp_log_dir)
127122

128123
def test_accepts_4_players(self):
129124
"""Test that Bridge accepts exactly 4 players by checking class properties."""

0 commit comments

Comments
 (0)