Skip to content

Commit 837ca8d

Browse files
committed
Change cli to typer interface
1 parent db27002 commit 837ca8d

21 files changed

Lines changed: 368 additions & 186 deletions

File tree

CONTRIBUTING.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -101,13 +101,13 @@ CodeClash/
101101
│ ├── agents/ # AI agent implementations (MiniSWEAgent, etc.)
102102
│ ├── arenas/ # Game arena implementations
103103
│ ├── analysis/ # Post-tournament analysis tools
104+
| ├── cli/ # CLI entrypoint
104105
│ ├── tournaments/ # Tournament orchestration
105106
│ ├── viewer/ # Web-based results viewer
106107
│ └── utils/ # Shared utilities
107108
├── configs/ # Tournament configuration files
108109
├── docs/ # Documentation (MkDocs)
109-
├── tests/ # Test suite
110-
└── main.py # Main entry point
110+
└── tests/ # Test suite
111111
```
112112

113113
## Types of Contributions
@@ -170,7 +170,7 @@ Analysis tools live in `codeclash/analysis/`. We're particularly interested in:
170170
| Preview docs | `uv run mkdocs serve` |
171171
| Build wheel | `uv build --wheel` |
172172
| Build wheel + sdist | `uv build` |
173-
| Run a tournament | `uv run python main.py <config>` |
173+
| Run a tournament | `uv run codeclash run <config>` |
174174
| View results | `uv run python scripts/run_viewer.py` |
175175

176176
### Building Distributions

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ uv sync --extra dev
5454
cp .env.example .env # Then edit .env with your GITHUB_TOKEN
5555

5656
# Run a test battle
57-
uv run python main.py configs/test/battlesnake.yaml
57+
uv run codeclash run configs/test/battlesnake.yaml
5858
```
5959

6060
> [!TIP]
@@ -66,14 +66,14 @@ uv run python main.py configs/test/battlesnake.yaml
6666

6767
```bash
6868
pip install -e '.[dev]'
69-
python main.py configs/test/battlesnake.yaml
69+
codeclash run configs/test/battlesnake.yaml
7070
```
7171
</details>
7272

7373
Once this works, you should be set up to run a real tournament!
7474
To run *Claude Sonnet 4.5* against *o3* in a *BattleSnake* tournament with *5 rounds* and *1000 competition simulations* per round, run:
7575
```bash
76-
uv run python main.py configs/examples/BattleSnake__claude-sonnet-4-5-20250929__o3__r5__s1000.yaml
76+
uv run codeclash run configs/examples/BattleSnake__claude-sonnet-4-5-20250929__o3__r5__s1000.yaml
7777
```
7878

7979
## ⚔️ How It Works

codeclash/arenas/bomberland/runtime/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ Round simulation counts must be even so each player receives both starting sides
2727
Smoke command from the repository root:
2828

2929
```bash
30-
uv run python main.py configs/examples/Bomberland__dummy__r1__s2.yaml -o /tmp/codeclash-bomberland-smoke
30+
uv run codeclash run configs/examples/Bomberland__dummy__r1__s2.yaml -o /tmp/codeclash-bomberland-smoke
3131
```
3232

3333
Use a fresh `-o` directory when rerunning the smoke check. Expected output:

codeclash/cli/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
"""CodeClash command-line interface (`codeclash ...`)."""
2+
3+
from codeclash.cli.app import app
4+
5+
__all__ = ["app"]
Lines changed: 38 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,60 @@
1-
import argparse
1+
"""Root `codeclash` Typer app.
2+
3+
Single entrypoint for CodeClash. Subcommands:
4+
codeclash run <config> run a PvP tournament
5+
codeclash ladder make <config> build a ladder (round-robin ranking)
6+
codeclash ladder run <config> send a model up a ranked ladder
7+
codeclash rank {win-rate,elo,matrix} ... compute standings from logs
8+
"""
9+
210
import getpass
311
import random
412
import time
513
import uuid
614
from pathlib import Path
15+
from typing import Optional
716

17+
import typer
818
import yaml
919

1020
from codeclash import CONFIG_DIR
21+
from codeclash.cli.ladder import ladder_app
22+
from codeclash.cli.rank import rank_app
1123
from codeclash.constants import LOCAL_LOG_DIR
1224
from codeclash.tournaments.pvp import PvpTournament
1325
from codeclash.utils.aws import is_running_in_aws_batch
1426
from codeclash.utils.yaml_utils import resolve_includes
1527

28+
app = typer.Typer(
29+
no_args_is_help=True,
30+
add_completion=False,
31+
context_settings={"help_option_names": ["-h", "--help"]},
32+
help="CodeClash: run coding-game tournaments, build ladders, and rank players.",
33+
)
34+
app.add_typer(ladder_app, name="ladder", help="Build and run CC:Ladder tournaments.")
35+
app.add_typer(rank_app, name="rank", help="Compute player standings from game logs.")
1636

17-
def main(
18-
config_path: Path,
19-
*,
20-
cleanup: bool = False,
21-
output_dir: Path | None = None,
22-
suffix: str = "",
23-
keep_containers: bool = False,
37+
38+
@app.command()
39+
def run(
40+
config_path: Path = typer.Argument(..., help="Path to the tournament config file."),
41+
cleanup: bool = typer.Option(False, "--cleanup", "-c", help="Clean up the game environment after running."),
42+
output_dir: Optional[Path] = typer.Option(
43+
None, "--output-dir", "-o", help="Output directory (default: logs/<user>)."
44+
),
45+
suffix: str = typer.Option("", "--suffix", "-s", help="Suffix for the output folder name (no leading dot)."),
46+
keep_containers: bool = typer.Option(
47+
False, "--keep-containers", "-k", help="Do not remove containers after games/agent finish."
48+
),
2449
):
50+
"""Run a PvP tournament from a config file."""
2551
yaml_content = config_path.read_text()
2652
preprocessed_yaml = resolve_includes(yaml_content, base_dir=CONFIG_DIR)
2753
config = yaml.safe_load(preprocessed_yaml)
2854

2955
def get_output_path() -> Path:
3056
if is_running_in_aws_batch():
3157
# Offset timestamp by random seconds to avoid collisions
32-
# Hopefully that means we can just remove the uuid part later on
3358
offset = random.randint(0, 600)
3459
timestamp = time.strftime("%y%m%d%H%M%S", time.localtime(time.time() + offset))
3560
else:
@@ -48,7 +73,6 @@ def get_output_path() -> Path:
4873
if transparent:
4974
folder_name += ".transparent"
5075
if is_running_in_aws_batch():
51-
# Also add a UUID just to be safe
5276
_uuid = str(uuid.uuid4())
5377
folder_name += f".{_uuid}-uuid"
5478
if output_dir is None:
@@ -60,46 +84,14 @@ def get_output_path() -> Path:
6084
return output_dir / folder_name
6185

6286
full_output_dir = get_output_path()
63-
6487
tournament = PvpTournament(config, output_dir=full_output_dir, cleanup=cleanup, keep_containers=keep_containers)
6588
tournament.run()
6689

6790

68-
def main_cli(argv: list[str] | None = None):
69-
parser = argparse.ArgumentParser(description="CodeClash")
70-
parser.add_argument(
71-
"config_path",
72-
type=Path,
73-
help="Path to the config file.",
74-
)
75-
parser.add_argument(
76-
"-c",
77-
"--cleanup",
78-
action="store_true",
79-
help="If set, do not clean up the game environment after running.",
80-
)
81-
parser.add_argument(
82-
"-o",
83-
"--output-dir",
84-
type=Path,
85-
help="Sets the output directory (default is 'logs' with current user subdirectory).",
86-
)
87-
parser.add_argument(
88-
"-s",
89-
"--suffix",
90-
type=str,
91-
help="Suffix to attach to the folder name. Does not include leading dot or underscore.",
92-
default="",
93-
)
94-
parser.add_argument(
95-
"-k",
96-
"--keep-containers",
97-
action="store_true",
98-
help="Do not remove containers after games/agent finish",
99-
)
100-
args = parser.parse_args(argv)
101-
main(**vars(args))
91+
def main() -> None:
92+
"""Console-script entrypoint (`codeclash`)."""
93+
app()
10294

10395

10496
if __name__ == "__main__":
105-
main_cli()
97+
app()
Lines changed: 51 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,67 @@
1-
import argparse
1+
"""`codeclash ladder` subcommands: build a ladder (make) and climb it (run)."""
2+
23
import getpass
34
import time
45
from pathlib import Path
6+
from typing import Optional
57

8+
import typer
69
import yaml
710

811
from codeclash import CONFIG_DIR
912
from codeclash.constants import LOCAL_LOG_DIR
1013
from codeclash.tournaments.pvp import PvpTournament
1114
from codeclash.utils.yaml_utils import resolve_includes
1215

16+
ladder_app = typer.Typer(
17+
no_args_is_help=True, add_completion=False, context_settings={"help_option_names": ["-h", "--help"]}
18+
)
19+
20+
21+
@ladder_app.command("make")
22+
def make(
23+
config_path: Path = typer.Argument(..., help="Path to the ladder (round-robin) config file."),
24+
):
25+
"""Build a ladder: run PvP tournaments across all pairs of players (for ranking)."""
26+
yaml_content = config_path.read_text()
27+
preprocessed_yaml = resolve_includes(yaml_content, base_dir=CONFIG_DIR)
28+
config = yaml.safe_load(preprocessed_yaml)
1329

14-
def main(
15-
config_path: Path,
16-
*,
17-
cleanup: bool = False,
18-
output_dir: Path | None = None,
19-
suffix: str = "",
20-
keep_containers: bool = False,
30+
players = config["players"]
31+
num_players = len(players)
32+
for i in range(num_players):
33+
for j in range(i + 1, num_players):
34+
player1 = players[i]
35+
player1["name"] = player1["branch_init"]
36+
player2 = players[j]
37+
player2["name"] = player2["branch_init"]
38+
pvp_config = {
39+
**config,
40+
"players": [player1, player2],
41+
}
42+
43+
vs = f"PvpTournament.{player1['name']}_vs_{player2['name']}".replace("/", "_")
44+
output_dir = LOCAL_LOG_DIR / "ladder" / config["game"]["name"] / vs
45+
try:
46+
tournament = PvpTournament(pvp_config, output_dir=output_dir)
47+
except FileExistsError:
48+
continue
49+
tournament.run()
50+
51+
52+
@ladder_app.command("run")
53+
def run(
54+
config_path: Path = typer.Argument(..., help="Path to the ladder config (with `player` + `ladder`)."),
55+
cleanup: bool = typer.Option(False, "--cleanup", "-c", help="Clean up the game environment after running."),
56+
output_dir: Optional[Path] = typer.Option(
57+
None, "--output-dir", "-o", help="Output directory (default: logs/<user>)."
58+
),
59+
suffix: str = typer.Option("", "--suffix", "-s", help="Suffix for the output folder name (no leading dot)."),
60+
keep_containers: bool = typer.Option(
61+
False, "--keep-containers", "-k", help="Do not remove containers after games/agent finish."
62+
),
2163
):
64+
"""Send a model up a ranked ladder, rung by rung, until it loses."""
2265
yaml_content = config_path.read_text()
2366
preprocessed_yaml = resolve_includes(yaml_content, base_dir=CONFIG_DIR)
2467
config = yaml.safe_load(preprocessed_yaml)
@@ -88,43 +131,3 @@ def main(
88131

89132
print(f"Ladder tournament complete. Logs saved to {parent_dir}")
90133
print(f"Final opponent faced: {opponent['name']} (rank {opponent_rank}/{len(ladder)} in ladder)")
91-
92-
93-
def main_cli(argv: list[str] | None = None):
94-
parser = argparse.ArgumentParser(description="CodeClash")
95-
parser.add_argument(
96-
"config_path",
97-
type=Path,
98-
help="Path to the config file.",
99-
)
100-
parser.add_argument(
101-
"-c",
102-
"--cleanup",
103-
action="store_true",
104-
help="If set, do not clean up the game environment after running.",
105-
)
106-
parser.add_argument(
107-
"-o",
108-
"--output-dir",
109-
type=Path,
110-
help="Sets the output directory (default is 'logs' with current user subdirectory).",
111-
)
112-
parser.add_argument(
113-
"-s",
114-
"--suffix",
115-
type=str,
116-
help="Suffix to attach to the folder name. Does not include leading dot or underscore.",
117-
default="",
118-
)
119-
parser.add_argument(
120-
"-k",
121-
"--keep-containers",
122-
action="store_true",
123-
help="Do not remove containers after games/agent finish",
124-
)
125-
args = parser.parse_args(argv)
126-
main(**vars(args))
127-
128-
129-
if __name__ == "__main__":
130-
main_cli()

codeclash/cli/rank.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
"""`codeclash rank` subcommands: compute player standings from game logs."""
2+
3+
import subprocess
4+
import sys
5+
from pathlib import Path
6+
7+
import typer
8+
9+
from codeclash.analysis import matrix as matrix_mod
10+
from codeclash.analysis.metrics import win_rate as win_rate_mod
11+
from codeclash.constants import LOCAL_LOG_DIR
12+
13+
rank_app = typer.Typer(
14+
no_args_is_help=True, add_completion=False, context_settings={"help_option_names": ["-h", "--help"]}
15+
)
16+
17+
18+
@rank_app.command("win-rate")
19+
def win_rate(
20+
logs: Path = typer.Argument(LOCAL_LOG_DIR, help="Path to game logs (default: logs/)."),
21+
):
22+
"""Print per-game and game-agnostic win rates for each model."""
23+
win_rate_mod.main(logs)
24+
25+
26+
@rank_app.command("matrix")
27+
def matrix(
28+
pvp_output_dir: Path = typer.Argument(..., help="Path to a PvP tournament output directory."),
29+
repetitions: int = typer.Option(3, "--repetitions", "-n", help="Repetitions per matchup."),
30+
max_workers: int = typer.Option(4, "--max-workers", "-w", help="Number of parallel game workers."),
31+
):
32+
"""Evaluate PvP tournament matrices (head-to-head win matrix)."""
33+
matrix_mod.main(pvp_output_dir, n_repetitions=repetitions, max_workers=max_workers)
34+
35+
36+
@rank_app.command(
37+
"elo",
38+
context_settings={"allow_extra_args": True, "ignore_unknown_options": True},
39+
help=(
40+
"Fit a Bradley-Terry/Elo model and generate plots. Forwards all arguments to the elo "
41+
"analysis module, e.g.: codeclash rank elo -d logs/ --print-matrix --output-dir assets/elo_plots"
42+
),
43+
)
44+
def elo(ctx: typer.Context):
45+
"""Passthrough to `python -m codeclash.analysis.metrics.elo` (rich analysis + plots)."""
46+
raise SystemExit(subprocess.run([sys.executable, "-m", "codeclash.analysis.metrics.elo", *ctx.args]).returncode)

codeclash/viewer/static/js/picker.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -503,7 +503,7 @@ async function fillTextareaWithAWSSubmitCommands() {
503503
// Format output as AWS submit commands
504504
const commands = successfulResults.map(
505505
(result) =>
506-
`aws/run_job.py -- aws/docker_and_sync.sh python main.py configs/main/${result.config_name}`,
506+
`aws/run_job.py -- aws/docker_and_sync.sh codeclash run configs/main/${result.config_name}`,
507507
);
508508
textarea.value = commands.join("\n");
509509

configs/ablations/ladder/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ For a more static and hill-climb-able version of CodeClash, we introduce CC:Ladd
55
For instance, for RobotRumble, we created a ladder by doing the following steps:
66
1. From the online [leaderboard](https://robotrumble.org/boards/2), we manually crawled all open source, published bots and pushed them as branches to the [CC:RobotRumble](https://github.com/CodeClash-ai/RobotRumble) repository.
77
2. We then created the `robotrumble.yaml` file in this folder.
8-
3. Next, from the repository root, we run `uv run python scripts/run_ladder.py configs/ablations/ladder/robotrumble.yaml`, which runs PvP Tournaments against all pairs of branches.
8+
3. Next, from the repository root, we run `uv run codeclash ladder run configs/ablations/ladder/robotrumble.yaml`, which runs PvP Tournaments against all pairs of branches.
99
4. From these logs, we then calculate win rate to rank all models.
1010

1111
You can follow these steps to create your own "CC:<arena>" ladder.

configs/ablations/ladder/make_battlesnake.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# Round-robin over ALL imported human BattleSnakes, used to RANK them (skill Phase 3).
2-
# Run: uv run python scripts/make_ladder.py configs/ablations/ladder/make_battlesnake.yaml
2+
# Run: uv run codeclash ladder make configs/ablations/ladder/make_battlesnake.yaml
33
# Then rank from the logs with codeclash/analysis/metrics/win_rate.py (or elo.py).
44
#
55
# Params: args match the main BattleSnake configs (11x11, browser off). Following the

0 commit comments

Comments
 (0)