Skip to content

Commit 5440a2d

Browse files
committed
Update win conditions
1 parent ef0cd30 commit 5440a2d

21 files changed

Lines changed: 127 additions & 77 deletions

codeclash/cli/ladder.py

Lines changed: 82 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import copy
44
import getpass
5+
import json
56
import time
67
from concurrent.futures import ThreadPoolExecutor, as_completed
78
from pathlib import Path
@@ -18,43 +19,55 @@
1819
logger = get_logger("ladder")
1920

2021

21-
def _resolve_ladder_rules(ladder_rules: dict, rounds: int) -> tuple[float, int]:
22-
"""Validate the optional ``ladder_rules`` block and return ``(min_round_win_fraction, win_last_k)``.
22+
def _resolve_ladder_rules(ladder_rules: dict, rounds: int) -> tuple[int, int]:
23+
"""Validate the required ``ladder_rules`` block and return ``(min_round_wins, win_last_k)``.
2324
24-
Defaults: win at least ``min_round_win_fraction`` (0.4) of the *agent* rounds AND win the last
25-
``win_last_k`` (1) round(s). The baseline round 0 (identical, un-edited codebases) is excluded
26-
from this count — it reflects game variance, not the agent — so the fraction is taken over the
27-
``rounds`` rounds the agent actually edits.
25+
Both keys must be specified explicitly in the config (no defaults):
26+
- ``min_round_wins``: the whole number of *agent* rounds the player must win to advance
27+
(a ``>=`` threshold). Must be ``1 <= min_round_wins <= rounds``.
28+
- ``win_last_k``: the player must win the last ``win_last_k`` round(s). ``1`` means just the final
29+
round; ``0`` disables the trailing-rounds requirement entirely. Must be ``<= min_round_wins``.
30+
31+
The baseline round 0 (identical, un-edited codebases) is excluded from the count — it reflects
32+
game variance, not the agent — so wins are counted over the ``rounds`` rounds the agent actually
33+
edits (rounds 1..``rounds``).
2834
"""
29-
min_round_win_fraction = ladder_rules.get("min_round_win_fraction", 0.4)
30-
win_last_k = ladder_rules.get("win_last_k", 1)
35+
if "min_round_wins" not in ladder_rules:
36+
typer.echo("ladder_rules.min_round_wins is required; specify it explicitly in the config.")
37+
raise typer.Exit(1)
38+
if "win_last_k" not in ladder_rules:
39+
typer.echo("ladder_rules.win_last_k is required; specify it explicitly in the config.")
40+
raise typer.Exit(1)
41+
min_round_wins = ladder_rules["min_round_wins"]
42+
win_last_k = ladder_rules["win_last_k"]
3143

32-
# win_last_k: number of trailing rounds the player must win (1 == just the final round).
33-
if isinstance(win_last_k, bool) or not isinstance(win_last_k, int):
34-
typer.echo(f"ladder_rules.win_last_k must be an integer, got {win_last_k!r}.")
44+
# min_round_wins: whole number of agent rounds the player must win (round 0 excluded).
45+
if isinstance(min_round_wins, bool) or not isinstance(min_round_wins, int):
46+
typer.echo(f"ladder_rules.min_round_wins must be an integer, got {min_round_wins!r}.")
3547
raise typer.Exit(1)
36-
if win_last_k < 1:
48+
if not 1 <= min_round_wins <= rounds:
3749
typer.echo(
38-
f"ladder_rules.win_last_k must be >= 1, got {win_last_k}. Use 1 to require winning only the final round."
50+
f"ladder_rules.min_round_wins must be in [1, {rounds}] (tournament.rounds), got {min_round_wins}."
3951
)
4052
raise typer.Exit(1)
41-
if win_last_k > rounds:
42-
typer.echo(f"ladder_rules.win_last_k ({win_last_k}) cannot exceed tournament.rounds ({rounds}).")
43-
raise typer.Exit(1)
4453

45-
# min_round_win_fraction: player must win >= this fraction of the agent rounds (round 0 excluded).
46-
if isinstance(min_round_win_fraction, bool) or not isinstance(min_round_win_fraction, (int, float)):
47-
typer.echo(f"ladder_rules.min_round_win_fraction must be a number, got {min_round_win_fraction!r}.")
54+
# win_last_k: number of trailing rounds the player must win (1 == just the final round, 0 == disabled).
55+
if isinstance(win_last_k, bool) or not isinstance(win_last_k, int):
56+
typer.echo(f"ladder_rules.win_last_k must be an integer, got {win_last_k!r}.")
57+
raise typer.Exit(1)
58+
if win_last_k < 0:
59+
typer.echo(
60+
f"ladder_rules.win_last_k must be >= 0, got {win_last_k}. "
61+
"Use 0 to disable the trailing-rounds requirement, or 1 to require winning only the final round."
62+
)
4863
raise typer.Exit(1)
49-
if not 0 <= min_round_win_fraction <= 1:
64+
if win_last_k > min_round_wins:
5065
typer.echo(
51-
f"ladder_rules.min_round_win_fraction must be in [0, 1], got {min_round_win_fraction}. "
52-
"The player must win >= this fraction of the agent rounds; 1 requires winning all of them, "
53-
"0 drops the fraction requirement."
66+
f"ladder_rules.win_last_k ({win_last_k}) cannot exceed ladder_rules.min_round_wins ({min_round_wins})."
5467
)
5568
raise typer.Exit(1)
5669

57-
return float(min_round_win_fraction), win_last_k
70+
return min_round_wins, win_last_k
5871

5972

6073
ladder_app = typer.Typer(
@@ -141,20 +154,25 @@ def run(
141154
config["tournament"]["rounds"],
142155
config["game"]["sims_per_round"],
143156
)
144-
min_round_win_fraction, win_last_k = _resolve_ladder_rules(config.get("ladder_rules", {}), rounds)
157+
min_round_wins, win_last_k = _resolve_ladder_rules(config.get("ladder_rules", {}), rounds)
145158
timestamp = time.strftime("%y%m%d%H%M%S")
146159
del config["player"]
147160
del config["ladder"]
148161
config.pop("ladder_rules", None)
149162

150-
print(
151-
f"Ladder advancement rule: win >= {min_round_win_fraction:.0%} of {rounds} agent rounds "
152-
f"(baseline round 0 excluded) and win the last {win_last_k} round(s)."
163+
last_k_rule = "disabled" if win_last_k == 0 else f"win the last {win_last_k} round(s)"
164+
advancement_rule = (
165+
f"Ladder advancement rule: win >= {min_round_wins} of {rounds} agent rounds "
166+
f"(baseline round 0 excluded) and {last_k_rule}."
153167
)
168+
print(advancement_rule)
169+
logger.info(advancement_rule)
154170
ladder_folder = f"LadderTournament.{config['game']['name']}.r{rounds}.s{sims}.{timestamp}"
155171
player["branch"] = ladder_folder
156172
parent_dir = LOCAL_LOG_DIR / getpass.getuser() / ladder_folder
157173

174+
rungs_cleared = 0
175+
advanced = False
158176
for idx, opponent in enumerate(ladder):
159177
opponent_rank = len(ladder) - idx
160178
opponent["name"] = opponent["branch_init"].replace("human/", "").replace("/", "_")
@@ -190,24 +208,37 @@ def run(
190208
metadata = yaml.safe_load(f)
191209
round_winners = [r["winner"] for k, r in metadata["round_stats"].items() if int(k) != 0]
192210

193-
# Advancement rule (configurable via `ladder_rules`): win at least
194-
# `min_round_win_fraction` of the agent rounds AND win the last `win_last_k` rounds.
211+
# Advancement rule (required via `ladder_rules`): win at least `min_round_wins` of the
212+
# agent rounds AND win the last `win_last_k` rounds. win_last_k == 0 disables the
213+
# trailing-rounds requirement.
195214
player_wins = sum(1 for w in round_winners if w == player["name"])
196-
won_majority = player_wins >= len(round_winners) * min_round_win_fraction
197-
won_last_k = all(w == player["name"] for w in round_winners[-win_last_k:])
215+
won_majority = player_wins >= min_round_wins
216+
won_last_k = win_last_k == 0 or all(w == player["name"] for w in round_winners[-win_last_k:])
217+
advanced = won_majority and won_last_k
198218

199-
if not won_majority or not won_last_k:
219+
# Record this rung's outcome in its metadata.json (durable gameplay log). The rule itself
220+
# (min_round_wins, win_last_k) is constant across the run and lives in the ladder summary.
221+
metadata["ladder_advancement"] = {
222+
"player_wins": player_wins,
223+
"won_last_k": won_last_k,
224+
"cleared": advanced,
225+
}
226+
with open(metadata_path, "w") as f:
227+
json.dump(metadata, f, indent=2)
228+
229+
if not advanced:
200230
# Player failed the advancement rule; the ladder challenge ends here.
201231
print("=" * 10)
202232
print(
203233
f"{player['name']} did not clear {opponent['name']} "
204234
f"(rank {opponent_rank}/{len(ladder)}): won {player_wins}/{len(round_winners)} agent rounds "
205-
f"(needed >= {min_round_win_fraction:.0%}), last {win_last_k} round(s) won: {won_last_k}.\n"
235+
f"(needed >= {min_round_wins}), last {win_last_k} round(s) won: {won_last_k}.\n"
206236
"Ladder challenge ends."
207237
)
208238
print("=" * 10)
209239
break
210240

241+
rungs_cleared += 1
211242
print("=" * 10)
212243
print(
213244
f"{player['name']} successfully beat {opponent['name']} (rank {opponent_rank}/{len(ladder)}) "
@@ -216,5 +247,22 @@ def run(
216247
)
217248
print("=" * 10)
218249

250+
# Persist the overall climb result to a ladder-level metadata.json in the run's parent dir.
251+
ladder_summary = {
252+
"player": player["name"],
253+
"game": config["game"]["name"],
254+
"rounds": rounds,
255+
"min_round_wins": min_round_wins,
256+
"win_last_k": win_last_k,
257+
"ladder_size": len(ladder),
258+
"rungs_cleared": rungs_cleared,
259+
"final_opponent": opponent["name"],
260+
"final_opponent_rank": opponent_rank,
261+
"cleared_ladder": rungs_cleared == len(ladder),
262+
}
263+
parent_dir.mkdir(parents=True, exist_ok=True)
264+
with open(parent_dir / "metadata.json", "w") as f:
265+
json.dump(ladder_summary, f, indent=2)
266+
219267
print(f"Ladder tournament complete. Logs saved to {parent_dir}")
220268
print(f"Final opponent faced: {opponent['name']} (rank {opponent_rank}/{len(ladder)} in ladder)")

configs/ablations/ladder/README.md

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,13 @@ Each run config carries a `ladder_rules` block controlling what it takes to clea
4343

4444
```yaml
4545
ladder_rules:
46-
min_round_win_fraction: 0.5 # must win strictly more than this fraction of rounds
47-
win_last_k: 1 # ...and must win the last K rounds (1 == just the final round)
46+
min_round_wins: 2 # must win >= this many of the agent rounds to advance (round 0 excluded)
47+
win_last_k: 0 # ...and must win the last K rounds (1 == just the final round, 0 == disabled)
4848
```
4949
50-
The defaults shown above reproduce the historical behavior (strict majority of rounds **and** win the final round); the block is optional and falls back to these values if omitted. Validation:
50+
Both keys are **required** — there are no defaults; a config that omits either one errors out. `min_round_wins` is a whole number: the player advances when `player_wins >= min_round_wins`. The baseline round 0 (identical, un-edited codebases) is excluded, so with `tournament.rounds: 5` there are 5 scored agent rounds (rounds 1–5). Validation:
5151

52-
- `win_last_k` must be an integer with `1 <= win_last_k <= tournament.rounds`.
53-
- `min_round_win_fraction` must be a number in `[0, 1)`; `0` drops the majority requirement (e.g. `min_round_win_fraction: 0` + `win_last_k: 1` means "just win the final round").
52+
- `min_round_wins` must be an integer with `1 <= min_round_wins <= tournament.rounds`.
53+
- `win_last_k` must be an integer with `0 <= win_last_k <= min_round_wins`. `0` **disables** the trailing-rounds requirement; `1` means "just win the final round".
54+
55+
The per-rung outcome (`player_wins`, `won_last_k`, `cleared`) is persisted under `ladder_advancement` in each rung's `metadata.json`. The overall climb result (`rungs_cleared`, `final_opponent`, `cleared_ladder`, …) is written to a ladder-level `metadata.json` in the run's parent dir.

configs/ablations/ladder/battlesnake.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
tournament:
66
rounds: 5
77
ladder_rules:
8-
min_round_win_fraction: 0.5 # advance only on a strict majority of rounds
9-
win_last_k: 1 # ...and win the last K rounds (K=1 == just the final round)
8+
min_round_wins: 2 # rounds to win to advance (1..total rounds)
9+
win_last_k: 0 # must win last K (0 = off, <= min_round_wins)
1010
game:
1111
name: BattleSnake
1212
sims_per_round: 250

configs/ablations/ladder/battlesnake__gemini_3_5_flash.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
tournament:
44
rounds: 5
55
ladder_rules:
6-
min_round_win_fraction: 0.5 # advance only on a strict majority of rounds
7-
win_last_k: 1 # ...and win the last K rounds (K=1 == just the final round)
6+
min_round_wins: 2 # rounds to win to advance (1..total rounds)
7+
win_last_k: 0 # must win last K (0 = off, <= min_round_wins)
88
game:
99
name: BattleSnake
1010
sims_per_round: 250

configs/ablations/ladder/battlesnake__gpt_5_5.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
tournament:
44
rounds: 5
55
ladder_rules:
6-
min_round_win_fraction: 0.5 # advance only on a strict majority of rounds
7-
win_last_k: 1 # ...and win the last K rounds (K=1 == just the final round)
6+
min_round_wins: 2 # rounds to win to advance (1..total rounds)
7+
win_last_k: 0 # must win last K (0 = off, <= min_round_wins)
88
game:
99
name: BattleSnake
1010
sims_per_round: 250

configs/ablations/ladder/battlesnake__opus_4_7.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
tournament:
44
rounds: 5
55
ladder_rules:
6-
min_round_win_fraction: 0.5 # advance only on a strict majority of rounds
7-
win_last_k: 1 # ...and win the last K rounds (K=1 == just the final round)
6+
min_round_wins: 2 # rounds to win to advance (1..total rounds)
7+
win_last_k: 0 # must win last K (0 = off, <= min_round_wins)
88
game:
99
name: BattleSnake
1010
sims_per_round: 250

configs/ablations/ladder/battlesnake__opus_4_8.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
tournament:
44
rounds: 5
55
ladder_rules:
6-
min_round_win_fraction: 0.5 # advance only on a strict majority of rounds
7-
win_last_k: 1 # ...and win the last K rounds (K=1 == just the final round)
6+
min_round_wins: 2 # rounds to win to advance (1..total rounds)
7+
win_last_k: 0 # must win last K (0 = off, <= min_round_wins)
88
game:
99
name: BattleSnake
1010
sims_per_round: 250

configs/ablations/ladder/battlesnake__sonnet_5.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
tournament:
44
rounds: 5
55
ladder_rules:
6-
min_round_win_fraction: 0.5 # advance only on a strict majority of rounds
7-
win_last_k: 1 # ...and win the last K rounds (K=1 == just the final round)
6+
min_round_wins: 2 # rounds to win to advance (1..total rounds)
7+
win_last_k: 0 # must win last K (0 = off, <= min_round_wins)
88
game:
99
name: BattleSnake
1010
sims_per_round: 250

configs/ablations/ladder/battlesnake_llama_smoke.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66
tournament:
77
rounds: 2
88
ladder_rules:
9-
min_round_win_fraction: 0.5 # win >= half the agent rounds (round 0 excluded)
10-
win_last_k: 1 # ...and win the last K rounds (K=1 == the final round)
9+
min_round_wins: 1 # rounds to win to advance (1..total rounds)
10+
win_last_k: 0 # must win last K (0 = off, <= min_round_wins)
1111
game:
1212
name: BattleSnake
1313
sims_per_round: 10

configs/ablations/ladder/corewar.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
tournament:
22
rounds: 5
33
ladder_rules:
4-
min_round_win_fraction: 0.5 # advance only on a strict majority of rounds
5-
win_last_k: 1 # ...and win the last K rounds (K=1 == just the final round)
4+
min_round_wins: 2 # rounds to win to advance (1..total rounds)
5+
win_last_k: 0 # must win last K (0 = off, <= min_round_wins)
66
game:
77
name: CoreWar
88
sims_per_round: 2000

0 commit comments

Comments
 (0)