Skip to content

Commit 9c85151

Browse files
committed
Add Figgie arena (Jane Street trading card game)
1 parent f4ea06b commit 9c85151

6 files changed

Lines changed: 305 additions & 0 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,
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: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
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+
Your bot (main.py) must implement:
33+
34+
def get_action(state: dict) -> dict
35+
36+
state contains:
37+
- position: your player index (0-3 or 0-4)
38+
- hand: dict of suit -> count of cards you hold
39+
- money: your current money
40+
- bids: dict of suit -> {price, player} or None
41+
- offers: dict of suit -> {price, player} or None
42+
- trades: list of completed trades
43+
- num_players: number of players (4 or 5)
44+
- turn: current turn number
45+
46+
Return one of:
47+
- {"type": "pass"}
48+
- {"type": "bid", "suit": "spades", "price": 5}
49+
- {"type": "offer", "suit": "spades", "price": 10}
50+
- {"type": "buy", "suit": "spades"}
51+
- {"type": "sell", "suit": "spades"}
52+
53+
Suits: "spades", "clubs", "hearts", "diamonds"
54+
"""
55+
56+
def __init__(self, config, **kwargs):
57+
super().__init__(config, **kwargs)
58+
num_players = len(config.get("players", []))
59+
if num_players not in [4, 5]:
60+
raise ValueError(f"Figgie requires 4 or 5 players, got {num_players}")
61+
62+
def execute_round(self, agents: list[Player]) -> None:
63+
args = [f"/{agent.name}/{self.submission}" for agent in agents]
64+
cmd = f"python engine.py {' '.join(args)} -r {self.game_config['sims_per_round']} > {self.log_env / FIGGIE_LOG};"
65+
self.logger.info(f"Running game: {cmd}")
66+
assert_zero_exit_code(self.environment.execute(cmd))
67+
68+
def get_results(self, agents: list[Player], round_num: int, stats: RoundStats):
69+
with open(self.log_round(round_num) / FIGGIE_LOG) as f:
70+
round_log = f.read()
71+
lines = round_log.split("FINAL_RESULTS")[-1].splitlines()
72+
73+
scores = {}
74+
for line in lines:
75+
match = re.search(r"Bot\_(\d)\_main:\s(\d+)\srounds\swon", line)
76+
if match:
77+
bot_id = match.group(1)
78+
rounds_won = int(match.group(2))
79+
scores[agents[int(bot_id) - 1].name] = rounds_won
80+
81+
# Handle draws
82+
draw_match = re.search(r"Draws:\s(\d+)", round_log)
83+
if draw_match:
84+
draws = int(draw_match.group(1))
85+
if draws > 0:
86+
scores[RESULT_TIE] = draws
87+
88+
stats.winner = max(scores, key=scores.get) if scores else "unknown"
89+
# Check for tie (equal scores)
90+
if scores:
91+
max_score = max(scores.values())
92+
winners_with_max = [k for k, v in scores.items() if v == max_score and k != RESULT_TIE]
93+
if len(winners_with_max) > 1:
94+
stats.winner = RESULT_TIE
95+
96+
stats.scores = scores
97+
for player, score in scores.items():
98+
if player != RESULT_TIE:
99+
stats.player_stats[player].score = score
100+
101+
def validate_code(self, agent: Player) -> tuple[bool, str | None]:
102+
if self.submission not in agent.environment.execute("ls")["output"]:
103+
return False, f"No {self.submission} file found in the root directory"
104+
105+
bot_content = agent.environment.execute(f"cat {self.submission}")["output"]
106+
107+
if "def get_action(" not in bot_content:
108+
return (
109+
False,
110+
f"{self.submission} must define a get_action(state) function. "
111+
"See the game description for the required signature.",
112+
)
113+
114+
return True, None

configs/test/figgie.yaml

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
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 players, each starting with $350 and 10 cards (40 cards total)
22+
- Deck: one 12-card suit, two 10-card suits, one 8-card suit
23+
- Goal suit: same color as 12-card suit, contains 8 or 10 cards (secret until end)
24+
- Players ante $50 each to form a $200 pot
25+
- At end: $10 per goal suit card, remainder to player(s) with most goal suit cards
26+
27+
Your bot (main.py) must implement:
28+
def get_action(state: dict) -> dict
29+
30+
Return one of:
31+
- {"type": "pass"}
32+
- {"type": "bid", "suit": "spades", "price": 5}
33+
- {"type": "offer", "suit": "spades", "price": 10}
34+
- {"type": "buy", "suit": "spades"}
35+
- {"type": "sell", "suit": "spades"}
36+
37+
The game is played in {{rounds}} rounds. For every round, you edit program code that controls your bot. This is round {{round}}.
38+
After you and your competitors finish editing your codebases, the game is run automatically.
39+
40+
Your task: improve the bot in `main.py`, located in {{working_dir}}.
41+
{{working_dir}} is your codebase, which contains both your bot and supporting assets.

tests/arenas/test_figgie.py

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
"""Unit tests for FiggieArena."""
2+
3+
import pytest
4+
5+
from codeclash.arenas.figgie.figgie import FiggieArena
6+
7+
VALID_FIGGIE_BOT = """
8+
def get_action(state):
9+
'''Make a trading decision based on game state.'''
10+
hand = state.get('hand', {})
11+
offers = state.get('offers', {})
12+
bids = state.get('bids', {})
13+
position = state.get('position', 0)
14+
15+
# Simple strategy: try to sell non-goal suits
16+
for suit in ['spades', 'clubs', 'hearts', 'diamonds']:
17+
bid = bids.get(suit)
18+
if bid and bid.get('player') != position and hand.get(suit, 0) > 0:
19+
return {'type': 'sell', 'suit': suit}
20+
21+
return {'type': 'pass'}
22+
"""
23+
24+
25+
class TestFiggieValidation:
26+
"""Tests for FiggieArena.validate_code()"""
27+
28+
@pytest.fixture
29+
def arena(self, tmp_log_dir, minimal_config):
30+
"""Create FiggieArena instance with mocked environment."""
31+
config = minimal_config.copy()
32+
config["game"]["name"] = "Figgie"
33+
config["players"] = [
34+
{"name": "p1", "agent": "dummy"},
35+
{"name": "p2", "agent": "dummy"},
36+
{"name": "p3", "agent": "dummy"},
37+
{"name": "p4", "agent": "dummy"},
38+
]
39+
arena = FiggieArena.__new__(FiggieArena)
40+
arena.submission = "main.py"
41+
arena.log_local = tmp_log_dir
42+
return arena
43+
44+
def test_valid_submission(self, arena, mock_player_factory):
45+
"""Test that a valid Figgie bot passes validation."""
46+
player = mock_player_factory(
47+
name="test_player",
48+
files={"main.py": VALID_FIGGIE_BOT},
49+
command_outputs={
50+
"ls": {"output": "main.py\n", "returncode": 0},
51+
"cat main.py": {"output": VALID_FIGGIE_BOT, "returncode": 0},
52+
},
53+
)
54+
is_valid, error = arena.validate_code(player)
55+
assert is_valid is True
56+
assert error is None
57+
58+
def test_missing_file(self, arena, mock_player_factory):
59+
"""Test that missing main.py fails validation."""
60+
player = mock_player_factory(
61+
name="test_player",
62+
files={},
63+
command_outputs={
64+
"ls": {"output": "other.py\n", "returncode": 0},
65+
},
66+
)
67+
is_valid, error = arena.validate_code(player)
68+
assert is_valid is False
69+
assert "main.py" in error
70+
71+
def test_missing_get_action_function(self, arena, mock_player_factory):
72+
"""Test that missing get_action function fails validation."""
73+
bot_code = """
74+
def make_move(game_state):
75+
'''Wrong function name.'''
76+
return {'type': 'pass'}
77+
"""
78+
player = mock_player_factory(
79+
name="test_player",
80+
files={"main.py": bot_code},
81+
command_outputs={
82+
"ls": {"output": "main.py\n", "returncode": 0},
83+
"cat main.py": {"output": bot_code, "returncode": 0},
84+
},
85+
)
86+
is_valid, error = arena.validate_code(player)
87+
assert is_valid is False
88+
assert "get_action" in error
89+
90+
91+
class TestFiggieRequirements:
92+
"""Test Figgie-specific requirements."""
93+
94+
def test_rejects_invalid_player_count(self, minimal_config, tmp_log_dir):
95+
"""Test that Figgie rejects invalid player counts (not 4 or 5)."""
96+
config = minimal_config.copy()
97+
config["game"]["name"] = "Figgie"
98+
config["players"] = [
99+
{"name": "p1", "agent": "dummy"},
100+
{"name": "p2", "agent": "dummy"},
101+
]
102+
103+
with pytest.raises(ValueError, match="Figgie requires 4 or 5 players"):
104+
FiggieArena(
105+
config,
106+
tournament_id="test_tournament",
107+
local_output_dir=tmp_log_dir
108+
)
109+
110+
def test_rejects_6_players(self, minimal_config, tmp_log_dir):
111+
"""Test that Figgie rejects 6 players."""
112+
config = minimal_config.copy()
113+
config["game"]["name"] = "Figgie"
114+
config["players"] = [
115+
{"name": f"p{i}", "agent": "dummy"} for i in range(6)
116+
]
117+
118+
with pytest.raises(ValueError, match="Figgie requires 4 or 5 players"):
119+
FiggieArena(
120+
config,
121+
tournament_id="test_tournament",
122+
local_output_dir=tmp_log_dir
123+
)
124+
125+
def test_accepts_4_or_5_players(self):
126+
"""Test that Figgie accepts 4 or 5 players by checking class properties."""
127+
assert FiggieArena.name == "Figgie"
128+
assert FiggieArena.submission == "main.py"
129+
# Description should mention both 4 and 5 players
130+
assert "4 or 5 players" in FiggieArena.description

0 commit comments

Comments
 (0)