Skip to content

Commit 090caf0

Browse files
committed
Improve CC:Ladder: rung framing, richer commit labels, branch carry-over fix
1 parent e3d90a7 commit 090caf0

5 files changed

Lines changed: 56 additions & 18 deletions

File tree

codeclash/agents/player.py

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -78,12 +78,23 @@ def __init__(
7878
logger=self.logger,
7979
)
8080
else:
81-
# Resume the branch if a previous round pushed it to the remote, else create it.
81+
# Resume the branch if a previous rung/round pushed it to the remote, else create it.
82+
# Reset the local branch to the fetched remote tip with -B.
8283
assert_zero_exit_code(self.environment.execute("git fetch origin"), logger=self.logger)
83-
if self.environment.execute(f"git checkout {self._branch_name}").get("returncode", 0) != 0:
84-
self.logger.info(f"Branch {self._branch_name} doesn't exist, creating it")
84+
if (
85+
self.environment.execute(f"git rev-parse --verify --quiet origin/{self._branch_name}").get(
86+
"returncode", 1
87+
)
88+
== 0
89+
):
8590
assert_zero_exit_code(
86-
self.environment.execute(f"git checkout -b {self._branch_name}"),
91+
self.environment.execute(f"git checkout -B {self._branch_name} origin/{self._branch_name}"),
92+
logger=self.logger,
93+
)
94+
else:
95+
self.logger.info(f"Branch {self._branch_name} doesn't exist on remote, creating it")
96+
assert_zero_exit_code(
97+
self.environment.execute(f"git checkout -B {self._branch_name}"),
8798
logger=self.logger,
8899
)
89100

@@ -172,11 +183,17 @@ def reset_and_apply_patch(self, patch: str, *, base_commit: str = "", filter_pat
172183

173184
# --- Helper methods ---
174185

186+
def _round_message(self, round: int) -> str:
187+
"""Commit/tag message for a round. The tournament sets ``commit_label`` on the player config —
188+
a prefix that carries its own separator (the ladder sets ``"Rung 2/50 (opponent, elo #49) — "``;
189+
PvP sets ``""``). Pure concatenation, no per-mode branching here."""
190+
return f"{self.config['commit_label']}Round {round} Update"
191+
175192
def _tag_round(self, round: int) -> None:
176193
"""Git tag the codebase at the given round."""
177194
tag = self._get_round_tag_name(round)
178195
assert_zero_exit_code(
179-
self.environment.execute(f"git tag -a {tag} -m 'Round {round} Update'"),
196+
self.environment.execute(f"git tag -a {tag} -m '{self._round_message(round)}'"),
180197
logger=self.logger,
181198
)
182199
self._metadata["round_tags"][round] = tag
@@ -203,7 +220,7 @@ def _commit(self) -> None:
203220
for cmd in [
204221
"git add -A",
205222
unstage_binaries,
206-
f"git commit --allow-empty -m 'Round {r} Update'",
223+
f"git commit --allow-empty -m '{self._round_message(r)}'",
207224
]:
208225
assert_zero_exit_code(self.environment.execute(cmd), logger=self.logger)
209226
self._tag_round(r)

codeclash/replay/base.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -367,8 +367,7 @@ def build_index(tour: TournamentInfo) -> str:
367367
group_td = f"<td>{g.group}</td>" if laddered else ""
368368
sim_td = f"<td>{sim_winner}</td>" if have_sim_winner else ""
369369
rows.append(
370-
f"<tr>{group_td}<td>{g.round}</td><td>{g.sim}</td>{sim_td}"
371-
f"<td>{round_winner or ''}</td><td>{cell}</td></tr>"
370+
f"<tr>{group_td}<td>{g.round}</td><td>{g.sim}</td>{sim_td}<td>{round_winner or ''}</td><td>{cell}</td></tr>"
372371
)
373372

374373
style = (

codeclash/tournaments/ladder.py

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -201,13 +201,20 @@ def run(self) -> dict:
201201
print(advancement_rule)
202202
logger.info(advancement_rule)
203203

204-
rungs_cleared = 0
205204
advanced = False
206205
opponent: dict = {}
207206
opponent_rank = 0
207+
rung = 0
208+
total = len(self.ladder)
208209
for idx, opponent in enumerate(self.ladder):
209-
opponent_rank = len(self.ladder) - idx
210+
# `rung` counts the climb from the bottom (1 = weakest opponent faced first, `total` =
211+
# strongest); `opponent_rank` is the opponent's Elo standing (1 = strongest overall).
212+
rung = idx + 1
213+
opponent_rank = total - idx
210214
opponent["name"] = _player_slug(opponent["branch_init"])
215+
# Prefix the climber's commit/tag messages with rung context (a prefix carrying its own
216+
# trailing separator; see Player._round_message).
217+
self.player["commit_label"] = f"Rung {rung}/{total} ({opponent['name']}, elo #{opponent_rank}) — "
211218
if "branch_init" in self.player and idx > 0:
212219
# After first opponent, remove branch_init so the player continues from the
213220
# previous tournament's codebase.
@@ -256,23 +263,23 @@ def run(self) -> dict:
256263
print("=" * 10)
257264
print(
258265
f"{self.player['name']} did not clear {opponent['name']} "
259-
f"(rank {opponent_rank}/{len(self.ladder)}): won {player_wins}/{len(round_winners)} agent rounds "
266+
f"(rung {rung}/{total}, elo #{opponent_rank}): won {player_wins}/{len(round_winners)} agent rounds "
260267
f"(needed >= {self.min_round_wins}), last {self.win_last_k} round(s) won: {won_last_k}.\n"
261268
"Ladder challenge ends."
262269
)
263270
print("=" * 10)
264271
break
265272

266-
rungs_cleared += 1
267273
print("=" * 10)
268274
print(
269-
f"{self.player['name']} successfully beat {opponent['name']} (rank {opponent_rank}/{len(self.ladder)}) "
275+
f"{self.player['name']} successfully beat {opponent['name']} (rung {rung}/{total}, elo #{opponent_rank}) "
270276
f"in {player_wins}/{len(round_winners)} rounds.\n"
271277
"Ladder challenge continuing"
272278
)
273279
print("=" * 10)
274280

275281
# Persist the overall climb result to a ladder-level metadata.json in the run's parent dir.
282+
rungs_cleared = rung if advanced else max(rung - 1, 0)
276283
ladder_summary = {
277284
"player": self.player["name"],
278285
"game": self.config["game"]["name"],
@@ -290,5 +297,5 @@ def run(self) -> dict:
290297
json.dump(ladder_summary, f, indent=2)
291298

292299
print(f"Ladder tournament complete. Logs saved to {self.parent_dir}")
293-
print(f"Final opponent faced: {opponent['name']} (rank {opponent_rank}/{len(self.ladder)} in ladder)")
300+
print(f"Final opponent faced: {opponent['name']} (rung {rung}/{total}, elo #{opponent_rank})")
294301
return ladder_summary

codeclash/tournaments/pvp.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,9 @@ def get_metadata(self) -> dict:
6767

6868
def get_agent(self, agent_config: dict, prompts: dict) -> Player:
6969
"""Create an agent with environment and game context."""
70+
# Commit/tag messages are prefixed with `commit_label` (see Player._round_message). PvP uses
71+
# no prefix by default, but it can be overridden in the agent config.
72+
agent_config.setdefault("commit_label", "")
7073
environment = self.game.get_environment(f"{self.game.game_id}.{agent_config['name']}")
7174

7275
game_context = GameContext(

docs/usage/tournaments.md

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ uv run codeclash run <config_path> [options]
1919
| Flag | Short | Description |
2020
|------|-------|-------------|
2121
| `--cleanup` | `-c` | Clean up game environment after running |
22-
| `--push` | `-p` | Push each agent's final codebase to a new GitHub repository |
2322
| `--output-dir PATH` | `-o PATH` | Custom output directory (default: `logs/<username>/`) |
2423
| `--suffix TEXT` | `-s TEXT` | Suffix to append to the output folder name |
2524
| `--keep-containers` | `-k` | Keep Docker containers after games/agents finish (useful for debugging) |
@@ -37,9 +36,6 @@ uv run codeclash run configs/test/battlesnake.yaml -k
3736
uv run codeclash run configs/pvp/BattleSnake__claude-sonnet-4-5-20250929__o3__r15__s1000.yaml \
3837
-o ./my_experiments \
3938
-s experiment1
40-
41-
# Push final codebases to GitHub
42-
uv run codeclash run configs/pvp/BattleSnake__claude-sonnet-4-5-20250929__o3__r15__s1000.yaml -p
4339
```
4440

4541
## Configuration Anatomy
@@ -151,6 +147,22 @@ Each player entry defines an AI agent:
151147
| `config` | dict | Agent-specific configuration |
152148
| `config.model` | dict | LLM model settings |
153149
| `config.agent` | dict | Agent behavior settings |
150+
| `branch_init` | string | Optional. Git branch to seed the agent's codebase from (e.g. `human/<author>/<bot>`). Defaults to the arena's starter branch. |
151+
| `push` | bool | Optional (default `false`). Push the agent's evolving codebase to a remote branch after each round. Requires `GITHUB_TOKEN` with write access to the arena repo. Set per-player in the config (there is no CLI flag for it). |
152+
| `branch` | string | Optional. Remote branch name to push to. Usually set automatically by the orchestrator (the ladder derives it from the run); defaults to `<game_id>.<name>`. |
153+
| `commit_label` | string | Optional. Prefix prepended to each round's git commit/tag message (otherwise just `Round N Update`). See note below. |
154+
155+
#### Commit labels
156+
157+
Every round, an agent's codebase is committed (and, with `push: true`, pushed) with the message `Round N Update`. The optional `commit_label` field prepends context to that message — it's a **prefix string that carries its own separator**, so the final message is simply `f"{commit_label}Round N Update"`.
158+
159+
It's normally set **programmatically by the tournament**, not hand-written: a plain PvP run leaves it `""` (→ `Round 1 Update`), while a **ladder** run sets it per rung so history reads like:
160+
161+
```
162+
Rung 2/50 (nessegrev-julia, elo #49) — Round 1 Update
163+
```
164+
165+
You *can* set it per-player in the config (e.g. to tag an experiment), but most users won't need to. Leave it unset for the default `Round N Update`.
154166

155167
#### Model Configuration
156168

0 commit comments

Comments
 (0)