|
| 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