Skip to content

Commit 6c4c6bf

Browse files
committed
Add per-tournament elo
1 parent d117067 commit 6c4c6bf

1 file changed

Lines changed: 58 additions & 10 deletions

File tree

  • codeclash/analysis/metrics

codeclash/analysis/metrics/elo.py

Lines changed: 58 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#!/usr/bin/env python3
22
import argparse
33
import json
4+
from collections import Counter
45
from dataclasses import dataclass
56
from pathlib import Path
67

@@ -125,11 +126,12 @@ class SkipTournamentException(Exception):
125126

126127

127128
class ELOCalculator:
128-
def __init__(self, *, k_factor: float, starting_elo: float, weighting_function: str, alpha: float):
129+
def __init__(self, *, k_factor: float, starting_elo: float, weighting_function: str, alpha: float, unit: str):
129130
self._k_factor = k_factor
130131
self._starting_elo = starting_elo
131132
self._weighting_function = weighting_function
132133
self._alpha = alpha
134+
self._unit = unit
133135
self._player_profiles = {}
134136

135137
@property
@@ -161,6 +163,49 @@ def _analyze_round(self, stats: dict, player2profile: dict, *, total_rounds: int
161163
weighted_k_factor = self._k_factor * round_weight
162164
update_profiles(prof_and_score, weighted_k_factor)
163165

166+
def _score_per_round(self, metadata: dict, player2profile: dict, total_rounds: int) -> None:
167+
for idx, stats in metadata["round_stats"].items():
168+
idx = int(idx)
169+
if idx == 0:
170+
# Skip initial round
171+
continue
172+
assert idx == stats["round_num"], (idx, stats["round_num"])
173+
self._analyze_round(stats, player2profile, total_rounds=total_rounds)
174+
175+
def _score_per_tournament(self, metadata: dict, player2profile: dict, total_rounds: int) -> None:
176+
# Update score = number of rounds won per tournament (ties count as 0.5 each)
177+
round_stats = metadata["round_stats"]
178+
winners = []
179+
ties = 0
180+
for k, v in round_stats.items():
181+
try:
182+
if int(k) == 0:
183+
continue
184+
except Exception:
185+
pass
186+
w = v.get("winner")
187+
if w == RESULT_TIE:
188+
ties += 1
189+
elif w is not None:
190+
winners.append(w)
191+
192+
win_counts = Counter(winners)
193+
194+
# Ensure both players are considered (even if they have zero wins)
195+
players = list(player2profile.keys())
196+
for p in players:
197+
player2profile[p].rounds_played += 1 # Each player played all rounds
198+
wins_per_player = {p: win_counts.get(p, 0) + 0.5 * ties for p in players}
199+
200+
wins_total = sum(wins_per_player.values())
201+
if wins_total == 0:
202+
raise SkipTournamentException
203+
204+
prof_and_score = [(player2profile[p], wins_per_player[p] / wins_total) for p in players]
205+
206+
weighted_k_factor = self._k_factor * (1 / total_rounds) if total_rounds > 0 else self._k_factor
207+
update_profiles(prof_and_score, weighted_k_factor)
208+
164209
def _analyze_tournament(self, tournament_log_folder: Path) -> None:
165210
"""Update all profiles based on the results of one tournament"""
166211
with open(tournament_log_folder / "metadata.json") as f:
@@ -190,15 +235,10 @@ def _analyze_tournament(self, tournament_log_folder: Path) -> None:
190235
# Determine total rounds for weighting calculation
191236
total_rounds = len([k for k in metadata["round_stats"].keys() if k != "0"])
192237

193-
for idx, stats in metadata["round_stats"].items():
194-
idx = int(idx)
195-
if idx == 0:
196-
# Skip initial round
197-
continue
198-
199-
assert idx == stats["round_num"], (idx, stats["round_num"])
200-
201-
self._analyze_round(stats, player2profile, total_rounds=total_rounds)
238+
if self._unit == "round":
239+
self._score_per_round(metadata, player2profile, total_rounds=total_rounds)
240+
elif self._unit == "tournament":
241+
self._score_per_tournament(metadata, player2profile, total_rounds=total_rounds)
202242

203243
def analyze(self, log_dir: Path) -> None:
204244
print(f"Calculating weighted ELO ratings from logs in {log_dir} ...")
@@ -320,12 +360,20 @@ def print_results(self) -> None:
320360
default=2.0,
321361
help="Alpha parameter for exponential weighting (Default: 2.0, ignored for linear weighting)",
322362
)
363+
parser.add_argument(
364+
"-u",
365+
"--unit",
366+
choices=["round", "tournament"],
367+
default="round",
368+
help="Calculate Elo ratings on a per-round or per-tournament basis (Default: round)",
369+
)
323370
args = parser.parse_args()
324371
calculator = ELOCalculator(
325372
k_factor=args.k_factor,
326373
starting_elo=args.starting_elo,
327374
weighting_function=args.weighting_function,
328375
alpha=args.alpha,
376+
unit=args.unit,
329377
)
330378
calculator.analyze(args.log_dir)
331379
calculator.print_results()

0 commit comments

Comments
 (0)