from __future__ import annotations from dataclasses import dataclass from datetime import datetime from enum import Enum from typing import Protocol class Player(Enum): Emma = "Emma" Kim = "Kim" Pierre = "Pierre" Theo = "Théo" Leila = "Leïla" Martin = "Martin" Clementine = "Clémentine" Thomas = "Thomas" Baptiste = "Baptiste" Tanguy = "Tanguy" Frost = "Frost" Felix = "Félix" Axel = "Axel" @dataclass(frozen=True) class PlayerStatus: player: Player elo: int total_games: int @staticmethod def create(player: Player) -> PlayerStatus: return PlayerStatus(player=player, elo=1000, total_games=0) def update_elo(self, elo_update: int) -> PlayerStatus: return PlayerStatus(player=self.player, elo=self.elo + elo_update, total_games=self.total_games + 1) @dataclass(frozen=True) class Game: played_at: datetime scores: dict[Player, int] def __str__(self) -> str: total_points = sum(self.scores.values()) return f"Game played at {self.played_at} ({total_points} total points)" class GameEloChangeStrategy(Protocol): def proccess_game(self, game: Game, current_elo: dict[Player, int]) -> dict[Player, float]: ... class SimpleEloChangeStrategy(GameEloChangeStrategy): def proccess_game(self, game: Game, current_elo: dict[Player, int]) -> dict[Player, float]: players_elo_updates = {player: 0.0 for player in game.scores} for player_1, score_1 in game.scores.items(): for player_2, score_2 in game.scores.items(): if player_1 != player_2: expected_score = 1 / (1 + 10 ** ((current_elo[player_2] - current_elo[player_1]) / D)) result = ( 1.0 if score_1 > score_2 else 0.0 if score_1 < score_2 else 0.5 ) players_elo_updates[player_1] += K * (result - expected_score) return players_elo_updates class Ladder: strategy: GameEloChangeStrategy players: dict[Player, PlayerStatus] = {player: PlayerStatus.create(player) for player in Player} def current_elo(self) -> dict[Player, int]: return {player: player_status.elo for player, player_status in self.players.items()} def proccess_game(self, game: Game) -> dict[Player, float]: players_elo_updates = self.strategy.proccess_game(game, self.current_elo()) for player, elo_update in players_elo_updates.items(): self.players[player] = self.players[player].update_elo(int(elo_update)) return players_elo_updates def print_current(self): print(f"Ladder ({datetime.now().strftime('%d/%m/%Y %H:%M')}):") print("=" * 30) for rank, player in enumerate( sorted(self.players.values(), key=lambda player: player.elo, reverse=True) ): print( f"{rank+1}.\t{player.player.value:<20}: {player.elo:>4d} points, {player.total_games:>3d} games" ) ladder = Ladder() ladder.strategy = SimpleEloChangeStrategy() D = 400 K = 32 games: list[Game] = [ Game( datetime(2022, 11, 11, 16, 30), {Player.Theo: 53, Player.Leila: 32, Player.Clementine: 53, Player.Emma: 48, Player.Kim: 50, Player.Martin: 51}, ), Game( datetime(2022, 11, 11, 17, 0), {Player.Emma: 53, Player.Kim: 57, Player.Pierre: 48, Player.Theo: 44, Player.Leila: 32, Player.Martin: 57}, ), Game( datetime(2022, 11, 11, 18, 45), {Player.Thomas: 58, Player.Baptiste: 45, Player.Pierre: 54, Player.Theo: 65, Player.Emma: 42, Player.Martin: 53}, ), Game( datetime(2022, 11, 11, 21, 0), {Player.Kim: 50, Player.Theo: 50, Player.Emma: 53, Player.Clementine: 55, Player.Pierre: 46, Player.Baptiste: 54}, ), Game( datetime(2022, 11, 11, 21, 30), { Player.Kim: 41, Player.Leila: 32, Player.Theo: 48, Player.Emma: 49, Player.Clementine: 52, Player.Pierre: 54, Player.Baptiste: 59, }, ), Game( datetime(2022, 11, 11, 22, 0), {Player.Clementine: 47, Player.Pierre: 46, Player.Theo: 57, Player.Emma: 42} ), Game( datetime(2022, 11, 11, 22, 32), {Player.Clementine: 43, Player.Pierre: 51, Player.Theo: 40, Player.Emma: 39} ), Game( datetime(2022, 11, 12, 12, 48), {Player.Pierre: 58, Player.Theo: 52, Player.Emma: 59, Player.Thomas: 50, Player.Baptiste: 52}, ), Game(datetime(2022, 11, 12, 13, 18), {Player.Emma: 41, Player.Theo: 53, Player.Pierre: 44}), Game(datetime(2022, 11, 12, 13, 47), {Player.Emma: 65, Player.Thomas: 59, Player.Pierre: 54}), Game( datetime(2022, 11, 12, 14, 15), {Player.Pierre: 57, Player.Emma: 52, Player.Martin: 45, Player.Clementine: 47}, ), Game(datetime(2022, 11, 12, 14, 47), {Player.Pierre: 51, Player.Emma: 47, Player.Clementine: 56}), Game( datetime(2022, 11, 12, 19, 6), {Player.Clementine: 43, Player.Kim: 47, Player.Theo: 13, Player.Pierre: 47, Player.Emma: 62}, ), Game( datetime(2022, 11, 13, 12, 31), {Player.Theo: 43, Player.Thomas: 37, Player.Tanguy: 44, Player.Pierre: 47, Player.Emma: 61}, ), Game( datetime(2022, 11, 13, 12, 54), { Player.Tanguy: 54, Player.Clementine: 47, Player.Theo: 65, Player.Thomas: 44, Player.Pierre: 60, Player.Kim: 58, Player.Emma: 46, }, ), Game(datetime(2022, 11, 13, 13, 25), {Player.Pierre: 52, Player.Emma: 68, Player.Clementine: 57}), Game( datetime(2023, 1, 2, 10, 0), {Player.Felix: 34, Player.Frost: 64, Player.Thomas: 51, Player.Baptiste: 53, Player.Emma: 68}, ), Game(datetime(2023, 1, 2, 10, 30), {Player.Felix: 64, Player.Frost: 55, Player.Thomas: 62, Player.Emma: 67}), Game( datetime(2023, 2, 19, 12, 0), {Player.Thomas: 56, Player.Axel: 53, Player.Emma: 61, Player.Theo: 57, Player.Pierre: 55, Player.Martin: 36}, ), ] for game in games: print(game) print("=" * 30) players_elo_updates = ladder.proccess_game(game) for (player, elo_update) in players_elo_updates.items(): print(f"\t{player.value:<20}{int(elo_update):+}\t({ladder.players[player].elo:>4d})") ladder.print_current()