|
| 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 |
0 commit comments