|
17 | 17 |
|
18 | 18 | logger = get_logger("ladder") |
19 | 19 |
|
| 20 | + |
| 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)``. |
| 23 | +
|
| 24 | + Defaults reproduce the historical behavior: win a strict majority of rounds |
| 25 | + (``min_round_win_fraction=0.5``) AND win the final round (``win_last_k=1``). |
| 26 | + """ |
| 27 | + min_round_win_fraction = ladder_rules.get("min_round_win_fraction", 0.5) |
| 28 | + win_last_k = ladder_rules.get("win_last_k", 1) |
| 29 | + |
| 30 | + # win_last_k: number of trailing rounds the player must win (1 == just the final round). |
| 31 | + if isinstance(win_last_k, bool) or not isinstance(win_last_k, int): |
| 32 | + typer.echo(f"ladder_rules.win_last_k must be an integer, got {win_last_k!r}.") |
| 33 | + raise typer.Exit(1) |
| 34 | + if win_last_k < 1: |
| 35 | + typer.echo( |
| 36 | + f"ladder_rules.win_last_k must be >= 1, got {win_last_k}. " |
| 37 | + "Use 1 to require winning only the final round." |
| 38 | + ) |
| 39 | + raise typer.Exit(1) |
| 40 | + if win_last_k > rounds: |
| 41 | + typer.echo( |
| 42 | + f"ladder_rules.win_last_k ({win_last_k}) cannot exceed tournament.rounds ({rounds})." |
| 43 | + ) |
| 44 | + raise typer.Exit(1) |
| 45 | + |
| 46 | + # min_round_win_fraction: player must win strictly more than this fraction of rounds. |
| 47 | + if isinstance(min_round_win_fraction, bool) or not isinstance(min_round_win_fraction, (int, float)): |
| 48 | + typer.echo(f"ladder_rules.min_round_win_fraction must be a number, got {min_round_win_fraction!r}.") |
| 49 | + raise typer.Exit(1) |
| 50 | + if not 0 <= min_round_win_fraction < 1: |
| 51 | + typer.echo( |
| 52 | + f"ladder_rules.min_round_win_fraction must be in [0, 1), got {min_round_win_fraction}. " |
| 53 | + "0.5 requires a strict majority; 0 drops the majority requirement." |
| 54 | + ) |
| 55 | + raise typer.Exit(1) |
| 56 | + |
| 57 | + return float(min_round_win_fraction), win_last_k |
| 58 | + |
20 | 59 | ladder_app = typer.Typer( |
21 | 60 | no_args_is_help=True, |
22 | 61 | add_completion=False, |
@@ -101,9 +140,16 @@ def run( |
101 | 140 | config["tournament"]["rounds"], |
102 | 141 | config["game"]["sims_per_round"], |
103 | 142 | ) |
| 143 | + min_round_win_fraction, win_last_k = _resolve_ladder_rules(config.get("ladder_rules", {}), rounds) |
104 | 144 | timestamp = time.strftime("%y%m%d%H%M%S") |
105 | 145 | del config["player"] |
106 | 146 | del config["ladder"] |
| 147 | + config.pop("ladder_rules", None) |
| 148 | + |
| 149 | + print( |
| 150 | + f"Ladder advancement rule: win > {min_round_win_fraction:.0%} of {rounds} rounds " |
| 151 | + f"and win the last {win_last_k} round(s)." |
| 152 | + ) |
107 | 153 | ladder_folder = f"LadderTournament.{config['game']['name']}.r{rounds}.s{sims}.{timestamp}" |
108 | 154 | player["branch"] = ladder_folder |
109 | 155 | parent_dir = LOCAL_LOG_DIR / getpass.getuser() / ladder_folder |
@@ -143,12 +189,22 @@ def run( |
143 | 189 | metadata = yaml.safe_load(f) |
144 | 190 | round_winners = [r["winner"] for r in metadata["round_stats"].values()] |
145 | 191 |
|
146 | | - # Player must have won majority of rounds and the last round to continue ladder |
| 192 | + # Advancement rule (configurable via `ladder_rules`): win strictly more than |
| 193 | + # `min_round_win_fraction` of rounds AND win the last `win_last_k` rounds. |
147 | 194 | player_wins = sum(1 for w in round_winners if w == player["name"]) |
148 | | - player_won_last = round_winners[-1] == player["name"] |
149 | | - |
150 | | - if not player_wins > len(round_winners) // 2 or not player_won_last: |
151 | | - # If player lost tournament, ladder challenge ends |
| 195 | + won_majority = player_wins > len(round_winners) * min_round_win_fraction |
| 196 | + won_last_k = all(w == player["name"] for w in round_winners[-win_last_k:]) |
| 197 | + |
| 198 | + if not won_majority or not won_last_k: |
| 199 | + # Player failed the advancement rule; the ladder challenge ends here. |
| 200 | + print("=" * 10) |
| 201 | + print( |
| 202 | + f"{player['name']} did not clear {opponent['name']} " |
| 203 | + f"(rank {opponent_rank}/{len(ladder)}): won {player_wins}/{len(round_winners)} rounds " |
| 204 | + f"(needed > {min_round_win_fraction:.0%}), last {win_last_k} round(s) won: {won_last_k}.\n" |
| 205 | + "Ladder challenge ends." |
| 206 | + ) |
| 207 | + print("=" * 10) |
152 | 208 | break |
153 | 209 |
|
154 | 210 | print("=" * 10) |
|
0 commit comments