Skip to content

Commit d8834c2

Browse files
committed
Add ELO calculation
1 parent 04d51c0 commit d8834c2

1 file changed

Lines changed: 87 additions & 0 deletions

File tree

codeclash/ratings/elo.py

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import argparse
2+
import json
3+
from dataclasses import dataclass
4+
from pathlib import Path
5+
6+
K_FACTOR = 32 # ELO constant, changeable
7+
8+
9+
@dataclass
10+
class PlayerEloProfile:
11+
player_id: str
12+
game_id: str
13+
rating: float = 1200.0 # Default starting ELO
14+
games_played: int = 0
15+
16+
17+
def expected_score(rating_a, rating_b):
18+
return 1 / (1 + 10 ** ((rating_b - rating_a) / 400))
19+
20+
21+
def main(log_dir: Path):
22+
player_profiles = {}
23+
for game_log_folder in log_dir.iterdir():
24+
if game_log_folder.is_dir():
25+
print(f"Processing game log folder: {game_log_folder}")
26+
27+
game_id = game_log_folder.name.split(".")[1]
28+
player_ids = [x.name for x in (game_log_folder / "players").iterdir() if x.is_dir()]
29+
# Initialize profiles
30+
for player in player_ids:
31+
key = f"{game_id}.{player}"
32+
if key not in player_profiles:
33+
player_profiles[key] = PlayerEloProfile(player_id=player, game_id=game_id)
34+
35+
for round_folder in (game_log_folder / "rounds").iterdir():
36+
round_results = json.load(open(round_folder / "results.json"))
37+
winner = round_results.get("winner")
38+
players = round_results.get("players", player_ids)
39+
# Only process if there are exactly 2 players
40+
if len(players) == 2:
41+
p1_key = f"{game_id}.{players[0]}"
42+
p2_key = f"{game_id}.{players[1]}"
43+
p1 = player_profiles[p1_key]
44+
p2 = player_profiles[p2_key]
45+
p1.games_played += 1
46+
p2.games_played += 1
47+
# Determine scores
48+
if winner == players[0]:
49+
s1, s2 = 1, 0
50+
elif winner == players[1]:
51+
s1, s2 = 0, 1
52+
else:
53+
s1, s2 = 0.5, 0.5 # Tie
54+
# Calculate expected scores
55+
e1 = expected_score(p1.rating, p2.rating)
56+
e2 = expected_score(p2.rating, p1.rating)
57+
# Update ratings
58+
p1.rating += K_FACTOR * (s1 - e1)
59+
p2.rating += K_FACTOR * (s2 - e2)
60+
61+
print("Player ELO profiles:")
62+
for profile in player_profiles.values():
63+
print(
64+
f" - {profile.player_id} (Game: {profile.game_id}) - ELO: {profile.rating:.1f} (Games: {profile.games_played})"
65+
)
66+
67+
# Average ELO per player across all games
68+
aggregated_elo = {}
69+
game_counts = {}
70+
total_games = {}
71+
for profile in player_profiles.values():
72+
pid = profile.player_id
73+
aggregated_elo[pid] = aggregated_elo.get(pid, 0) + profile.rating
74+
game_counts[pid] = game_counts.get(pid, 0) + 1
75+
total_games[pid] = total_games.get(pid, 0) + profile.games_played
76+
77+
print("\nAverage ELO per player (across all games):")
78+
for pid in aggregated_elo:
79+
avg_elo = aggregated_elo[pid] / game_counts[pid]
80+
print(f" - {pid}: Avg ELO {avg_elo:.1f} (Total Games: {total_games[pid]})")
81+
82+
83+
if __name__ == "__main__":
84+
parser = argparse.ArgumentParser()
85+
parser.add_argument("log_dir", type=Path, help="Path to `logs/<user>` folder containing game logs")
86+
args = parser.parse_args()
87+
main(args.log_dir)

0 commit comments

Comments
 (0)