From cf0dc882848257c30c123707aeb1e7ab40e308be Mon Sep 17 00:00:00 2001 From: Christoph Urlacher Date: Tue, 27 Feb 2024 21:01:09 +0100 Subject: [PATCH] Add initial (untested) race guess points calculation --- formula10/domain/domain_model.py | 215 ++++++++++++++++++++++++-- formula10/domain/model/race_result.py | 14 +- formula10/domain/points_model.py | 156 ++++++++++++++++++- formula10/domain/template_model.py | 172 +-------------------- 4 files changed, 379 insertions(+), 178 deletions(-) diff --git a/formula10/domain/domain_model.py b/formula10/domain/domain_model.py index c4a0d01..41b92ad 100644 --- a/formula10/domain/domain_model.py +++ b/formula10/domain/domain_model.py @@ -1,4 +1,4 @@ -from typing import Callable, List +from typing import Callable, Dict, List, overload from sqlalchemy import desc from formula10.database.model.db_driver import DbDriver @@ -8,7 +8,7 @@ from formula10.database.model.db_race_result import DbRaceResult from formula10.database.model.db_season_guess import DbSeasonGuess from formula10.database.model.db_team import DbTeam from formula10.database.model.db_user import DbUser -from formula10.database.validation import find_multiple_strict +from formula10.database.validation import find_multiple_strict, find_single_or_none_strict, find_single_strict from formula10.domain.model.driver import NONE_DRIVER, Driver from formula10.domain.model.race import Race from formula10.domain.model.race_guess import RaceGuess @@ -30,7 +30,7 @@ class Model(): def all_users(self) -> List[User]: """ - Returns a list of all users in the database. + Returns a list of all enabled users. """ if self._all_users is None: self._all_users = [ @@ -42,7 +42,7 @@ class Model(): def all_race_results(self) -> List[RaceResult]: """ - Returns a list of all race results in the database, in descending order (most recent first). + Returns a list of all race results, in descending order (most recent first). """ if self._all_race_results is None: self._all_race_results = [ @@ -54,7 +54,7 @@ class Model(): def all_race_guesses(self) -> List[RaceGuess]: """ - Returns a list of all race guesses in the database. + Returns a list of all race guesses (of enabled users). """ if self._all_race_guesses is None: self._all_race_guesses = [ @@ -65,6 +65,9 @@ class Model(): return self._all_race_guesses def all_season_guesses(self) -> List[SeasonGuess]: + """ + Returns a list of all season guesses (of enabled users). + """ if self._all_season_guesses is None: self._all_season_guesses = [ SeasonGuess.from_db_season_guess(db_season_guess) @@ -75,7 +78,7 @@ class Model(): def all_races(self) -> List[Race]: """ - Returns a list of all races in the database. + Returns a list of all races, in descending order (last race first). """ if self._all_races is None: self._all_races = [ @@ -87,7 +90,7 @@ class Model(): def all_drivers(self, *, include_none: bool) -> List[Driver]: """ - Returns a list of all drivers in the database. + Returns a list of all drivers. """ if self._all_drivers is None: self._all_drivers = [ @@ -103,7 +106,7 @@ class Model(): def all_teams(self, *, include_none: bool) -> List[Team]: """ - Returns a list of all teams in the database. + Returns a list of all teams. """ if self._all_teams is None: self._all_teams = [ @@ -115,4 +118,198 @@ class Model(): return self._all_teams else: predicate: Callable[[Team], bool] = lambda team: team != NONE_TEAM - return find_multiple_strict(predicate, self._all_teams) \ No newline at end of file + return find_multiple_strict(predicate, self._all_teams) + + # + # User queries + # + + @overload + def user_by(self, *, user_name: str) -> User: + """ + Tries to obtain the user object for a specific username. + """ + return self.user_by(user_name=user_name) + + @overload + def user_by(self, *, user_name: str, ignore: List[str]) -> User | None: + """ + Tries to obtain the user object for a specific username, but ignores certain usernames. + """ + return self.user_by(user_name=user_name, ignore=ignore) + + def user_by(self, *, user_name: str, ignore: List[str] | None = None) -> User | None: + if ignore is None: + ignore = [] + + if len(ignore) > 0 and user_name in ignore: + return None + + predicate: Callable[[User], bool] = lambda user: user.name == user_name + return find_single_strict(predicate, self.all_users()) + + # + # Race result queries + # + + def race_result_by(self, *, race_name: str) -> RaceResult | None: + """ + Tries to obtain the race result corresponding to a race name. + """ + predicate: Callable[[RaceResult], bool] = lambda result: result.race.name == race_name + return find_single_or_none_strict(predicate, self.all_race_results()) + + # + # Race guess queries + # + + @overload + def race_guesses_by(self, *, user_name: str) -> List[RaceGuess]: + """ + Returns a list of all race guesses made by a specific user. + """ + return self.race_guesses_by(user_name=user_name) + + @overload + def race_guesses_by(self, *, race_name: str) -> List[RaceGuess]: + """ + Returns a list of all race guesses made for a specific race. + """ + return self.race_guesses_by(race_name=race_name) + + @overload + def race_guesses_by(self, *, user_name: str, race_name: str) -> RaceGuess | None: + """ + Returns a single race guess by a specific user for a specific race, or None, if this guess doesn't exist. + """ + return self.race_guesses_by(user_name=user_name, race_name=race_name) + + @overload + def race_guesses_by(self) -> Dict[str, Dict[str, RaceGuess]]: + """ + Returns a dictionary that maps race-ids to user-id - guess dictionaries. + """ + return self.race_guesses_by() + + def race_guesses_by(self, *, user_name: str | None = None, race_name: str | None = None) -> RaceGuess | List[RaceGuess] | Dict[str, Dict[str, RaceGuess]] | None: + # List of all guesses by a single user + if user_name is not None and race_name is None: + predicate: Callable[[RaceGuess], bool] = lambda guess: guess.user.name == user_name + return find_multiple_strict(predicate, self.all_race_guesses()) + + # List of all guesses for a single race + if user_name is None and race_name is not None: + predicate: Callable[[RaceGuess], bool] = lambda guess: guess.race.name == race_name + return find_multiple_strict(predicate, self.all_race_guesses()) + + # Guess for a single race by a single user + if user_name is not None and race_name is not None: + predicate: Callable[[RaceGuess], bool] = lambda guess: guess.user.name == user_name and guess.race.name == race_name + return find_single_or_none_strict(predicate, self.all_race_guesses()) + + # Dict with all guesses + if user_name is None and race_name is None: + guesses_by: Dict[str, Dict[str, RaceGuess]] = dict() + guess: RaceGuess + + for guess in self.all_race_guesses(): + if guess.race.name not in guesses_by: + guesses_by[guess.race.name] = dict() + + guesses_by[guess.race.name][guess.user.name] = guess + + return guesses_by + + raise Exception("race_guesses_by encountered illegal combination of arguments") + + # + # Season guess queries + # + + @overload + def season_guesses_by(self, *, user_name: str) -> SeasonGuess: + """ + Returns the season guess made by a specific user. + """ + return self.season_guesses_by(user_name=user_name) + + @overload + def season_guesses_by(self) -> Dict[str, SeasonGuess]: + """ + Returns a dictionary of season guesses mapped to usernames. + """ + return self.season_guesses_by() + + def season_guesses_by(self, *, user_name: str | None = None) -> SeasonGuess | Dict[str, SeasonGuess] | None: + if user_name is not None: + predicate: Callable[[SeasonGuess], bool] = lambda guess: guess.user.name == user_name + return find_single_or_none_strict(predicate, self.all_season_guesses()) + + if user_name is None: + guesses_by: Dict[str, SeasonGuess] = dict() + guess: SeasonGuess + + for guess in self.all_season_guesses(): + guesses_by[guess.user.name] = guess + + return guesses_by + + raise Exception("season_guesses_by encountered illegal combination of arguments") + + # + # Team queries + # + + def none_team(self) -> Team: + return NONE_TEAM + + # + # Driver queries + # + + def none_driver(self) -> Driver: + return NONE_DRIVER + + @overload + def drivers_by(self, *, team_name: str) -> List[Driver]: + """ + Returns a list of all drivers driving for a certain team. + """ + return self.drivers_by(team_name=team_name) + + @overload + def drivers_by(self) -> Dict[str, List[Driver]]: + """ + Returns a dictionary of drivers mapped to team names. + """ + return self.drivers_by() + + def drivers_by(self, *, team_name: str | None = None) -> List[Driver] | Dict[str, List[Driver]]: + if team_name is not None: + predicate: Callable[[Driver], bool] = lambda driver: driver.team.name == team_name + return find_multiple_strict(predicate, self.all_drivers(include_none=False), 2) + + if team_name is None: + drivers_by: Dict[str, List[Driver]] = dict() + driver: Driver + team: Team + + for team in self.all_teams(include_none=False): + drivers_by[team.name] = [] + for driver in self.all_drivers(include_none=False): + drivers_by[driver.team.name] += [driver] + + return drivers_by + + raise Exception("drivers_by encountered illegal combination of arguments") + + # + # Race queries + # + + def race_by(self, *, race_name: str) -> Race: + for race in self.all_races(): + if race.name == race_name: + return race + + raise Exception(f"Couldn't find race {race_name}") \ No newline at end of file diff --git a/formula10/domain/model/race_result.py b/formula10/domain/model/race_result.py index 28dd503..f02d1cf 100644 --- a/formula10/domain/model/race_result.py +++ b/formula10/domain/model/race_result.py @@ -70,8 +70,8 @@ class RaceResult: return NotImplemented race: Race - standing: Dict[str, Driver] - initial_dnf: List[Driver] + standing: Dict[str, Driver] # Always contains all 20 drivers, even if DNF'ed or excluded + initial_dnf: List[Driver] # initial_dnf is empty if no-one DNF'ed all_dnfs: List[Driver] standing_exclusions: List[Driver] @@ -86,6 +86,16 @@ class RaceResult: return self.standing[position] + def driver_standing_position(self, driver: Driver) -> int | None: + if driver == NONE_DRIVER: + return None + + for position, _driver in self.standing.items(): + if driver == _driver and driver not in self.standing_exclusions: + return int(position) + + return None + def driver_standing_position_string(self, driver: Driver) -> str: if driver == NONE_DRIVER: return "" diff --git a/formula10/domain/points_model.py b/formula10/domain/points_model.py index 684057c..f4cb02c 100644 --- a/formula10/domain/points_model.py +++ b/formula10/domain/points_model.py @@ -1,5 +1,159 @@ +from typing import Dict, List, overload +import numpy as np + from formula10.domain.domain_model import Model +from formula10.domain.model.driver import NONE_DRIVER +from formula10.domain.model.race_guess import RaceGuess +from formula10.domain.model.race_result import RaceResult + +RACE_GUESS_OFFSET_POINTS: Dict[int, int] = { + 3: 1, + 2: 3, + 1: 6, + 0: 10 +} +RACE_GUESS_DNF_POINTS: int = 10 + +SEASON_GUESS_HOT_TAKE_POINTS: int = 10 +SEASON_GUESS_P2_POINTS: int = 10 +SEASON_GUESS_OVERTAKES_POINTS: int = 10 +SEASON_GUESS_DNF_POINTS: int = 10 +SEASON_GUESS_GAINED_POINTS: int = 10 +SEASON_GUESS_LOST_POINTS: int = 10 +SEASON_GUESS_TEAMWINNER_CORRECT_POINTS: int = 3 +SEASON_GUESS_TEAMWINNER_FALSE_POINTS: int = -3 +SEASON_GUESS_PODIUMS_CORRECT_POINTS: int = 3 +SEASON_GUESS_PODIUMS_FALSE_POINTS: int = -2 + +STANDING_2023: Dict[str, int] = { + "Max Verstappen": 1, + "Sergio Perez": 2, + "Lewis Hamilton": 3, + "Fernando Alonso": 4, + "Charles Leclerc": 5, + "Lando Norris": 6, + "Carlos Sainz": 7, + "George Russel": 8, + "Oscar Piastri": 9, + "Lance Stroll": 10, + "Pierre Gasly": 11, + "Esteban Ocon": 12, + "Alexander Albon": 13, + "Yuki Tsunoda": 14, + "Valtteri Bottas": 15, + "Nico Hulkenberg": 16, + "Daniel Ricciardo": 17, + "Zhou Guanyu": 18, + "Kevin Magnussen": 19, + "Logan Sargeant": 21 +} + +def standing_points(race_guess: RaceGuess, race_result: RaceResult) -> int: + guessed_driver_position: int | None = race_result.driver_standing_position(driver=race_guess.pxx_guess) + if guessed_driver_position is None: + return 0 + + position_offset: int = abs(guessed_driver_position - race_guess.race.place_to_guess) + if position_offset not in RACE_GUESS_OFFSET_POINTS: + return 0 + + return RACE_GUESS_OFFSET_POINTS[position_offset] + +def dnf_points(race_guess: RaceGuess, race_result: RaceResult) -> int: + if race_guess.dnf_guess in race_result.initial_dnf: + return RACE_GUESS_DNF_POINTS + + if race_guess.dnf_guess == NONE_DRIVER and len(race_result.initial_dnf) == 0: + return RACE_GUESS_DNF_POINTS + + return 0 class PointsModel(Model): + """ + This class bundles all data + functionality required to do points calculations. + """ + + _points_per_step: Dict[str, List[int]] | None = None + def __init__(self): - Model.__init__(self) \ No newline at end of file + Model.__init__(self) + + def points_per_step(self) -> Dict[str, List[int]]: + """ + Returns a dictionary of lists, containing points per race for each user. + """ + if self._points_per_step is None: + self._points_per_step = dict() + for user in self.all_users(): + self._points_per_step[user.name] = [0] * 24 # Start at index 1, like the race numbers + + for race_guess in self.all_race_guesses(): + user_name: str = race_guess.user.name + race_number: int = race_guess.race.number + race_result: RaceResult | None = self.race_result_by(race_name=race_guess.race.name) + + if race_result is None: + continue + + self._points_per_step[user_name][race_number] = standing_points(race_guess, race_result) + dnf_points(race_guess, race_result) + + return dict() + + return self._points_per_step + + def points_per_step_cumulative(self) -> Dict[str, List[int]]: + """ + Returns a dictionary of lists, containing cumulative points per race for each user. + """ + points_per_step_cumulative: Dict[str, List[int]] = dict() + for user_name, points in self.points_per_step().items(): + points_per_step_cumulative[user_name] = np.cumsum(points).tolist() + + return points_per_step_cumulative + + @overload + def points_by(self, *, user_name: str) -> List[int]: + """ + Returns a list of points per race for a specific user. + """ + return self.points_by(user_name=user_name) + + @overload + def points_by(self, *, race_name: str) -> Dict[str, int]: + """ + Returns a dictionary of points per user for a specific race. + """ + return self.points_by(race_name=race_name) + + @overload + def points_by(self, *, user_name: str, race_name: str) -> int: + """ + Returns the points for a specific race for a specific user. + """ + return self.points_by(user_name=user_name, race_name=race_name) + + def points_by(self, *, user_name: str | None = None, race_name: str | None = None) -> List[int] | Dict[str, int] | int: + if user_name is not None and race_name is None: + return self.points_per_step()[user_name] + + if user_name is None and race_name is not None: + race_number: int = self.race_by(race_name=race_name).number + points_by_race: Dict[str, int] = dict() + + for _user_name, points in self.points_per_step().items(): + points_by_race[_user_name] = points[race_number] + + return points_by_race + + if user_name is not None and race_name is not None: + race_number: int = self.race_by(race_name=race_name).number + + return self.points_per_step()[user_name][race_number] + + raise Exception("points_by received an illegal combination of arguments") + + def total_points_by(self, user_name: str) -> int: + """ + Returns the total number of points for a specific user. + """ + return sum(self.points_by(user_name=user_name)) diff --git a/formula10/domain/template_model.py b/formula10/domain/template_model.py index 3ee3d2f..ebc6408 100644 --- a/formula10/domain/template_model.py +++ b/formula10/domain/template_model.py @@ -1,19 +1,16 @@ -from typing import List, Callable, Dict, overload +from typing import List, Callable from formula10.domain.domain_model import Model -from formula10.domain.model.driver import NONE_DRIVER, Driver +from formula10.domain.model.driver import Driver from formula10.domain.model.race import Race -from formula10.domain.model.race_guess import RaceGuess from formula10.domain.model.race_result import RaceResult -from formula10.domain.model.season_guess import SeasonGuess -from formula10.domain.model.team import NONE_TEAM, Team from formula10.domain.model.user import User -from formula10.database.validation import find_first_else_none, find_multiple_strict, find_single_strict, find_single_or_none_strict, race_has_started +from formula10.database.validation import find_first_else_none, find_multiple_strict, race_has_started class TemplateModel(Model): """ - This class bundles all data required from inside a template. + This class bundles all data + functionality required from inside a template. """ active_user: User | None = None @@ -23,6 +20,8 @@ class TemplateModel(Model): _wdc_gained_excluded_abbrs: List[str] = ["RIC"] def __init__(self, *, active_user_name: str | None, active_result_race_name: str | None): + Model.__init__(self) + if active_user_name is not None: self.active_user = self.user_by(user_name=active_user_name, ignore=["Everyone"]) @@ -47,126 +46,6 @@ class TemplateModel(Model): return self.all_users() - @overload - def user_by(self, *, user_name: str) -> User: - """ - Tries to obtain the user object for a specific username. - """ - return self.user_by(user_name=user_name) - - @overload - def user_by(self, *, user_name: str, ignore: List[str]) -> User | None: - """ - Tries to obtain the user object for a specific username, but ignores certain usernames. - """ - return self.user_by(user_name=user_name, ignore=ignore) - - def user_by(self, *, user_name: str, ignore: List[str] | None = None) -> User | None: - if ignore is None: - ignore = [] - - if len(ignore) > 0 and user_name in ignore: - return None - - predicate: Callable[[User], bool] = lambda user: user.name == user_name - return find_single_strict(predicate, self.all_users()) - - def race_result_by(self, *, race_name: str) -> RaceResult | None: - """ - Tries to obtain the race result corresponding to a race name. - """ - predicate: Callable[[RaceResult], bool] = lambda result: result.race.name == race_name - return find_single_or_none_strict(predicate, self.all_race_results()) - - @overload - def race_guesses_by(self, *, user_name: str) -> List[RaceGuess]: - """ - Returns a list of all race guesses made by a specific user. - """ - return self.race_guesses_by(user_name=user_name) - - @overload - def race_guesses_by(self, *, race_name: str) -> List[RaceGuess]: - """ - Returns a list of all race guesses made for a specific race. - """ - return self.race_guesses_by(race_name=race_name) - - @overload - def race_guesses_by(self, *, user_name: str, race_name: str) -> RaceGuess | None: - """ - Returns a single race guess by a specific user for a specific race, or None, if this guess doesn't exist. - """ - return self.race_guesses_by(user_name=user_name, race_name=race_name) - - @overload - def race_guesses_by(self) -> Dict[str, Dict[str, RaceGuess]]: - """ - Returns a dictionary that maps race-ids to user-id - guess dictionaries. - """ - return self.race_guesses_by() - - def race_guesses_by(self, *, user_name: str | None = None, race_name: str | None = None) -> RaceGuess | List[RaceGuess] | Dict[str, Dict[str, RaceGuess]] | None: - # List of all guesses by a single user - if user_name is not None and race_name is None: - predicate: Callable[[RaceGuess], bool] = lambda guess: guess.user.name == user_name - return find_multiple_strict(predicate, self.all_race_guesses()) - - # List of all guesses for a single race - if user_name is None and race_name is not None: - predicate: Callable[[RaceGuess], bool] = lambda guess: guess.race.name == race_name - return find_multiple_strict(predicate, self.all_race_guesses()) - - # Guess for a single race by a single user - if user_name is not None and race_name is not None: - predicate: Callable[[RaceGuess], bool] = lambda guess: guess.user.name == user_name and guess.race.name == race_name - return find_single_or_none_strict(predicate, self.all_race_guesses()) - - # Dict with all guesses - if user_name is None and race_name is None: - guesses_by: Dict[str, Dict[str, RaceGuess]] = dict() - guess: RaceGuess - - for guess in self.all_race_guesses(): - if guess.race.name not in guesses_by: - guesses_by[guess.race.name] = dict() - - guesses_by[guess.race.name][guess.user.name] = guess - - return guesses_by - - raise Exception("race_guesses_by encountered illegal combination of arguments") - - @overload - def season_guesses_by(self, *, user_name: str) -> SeasonGuess: - """ - Returns the season guess made by a specific user. - """ - return self.season_guesses_by(user_name=user_name) - - @overload - def season_guesses_by(self) -> Dict[str, SeasonGuess]: - """ - Returns a dictionary of season guesses mapped to usernames. - """ - return self.season_guesses_by() - - def season_guesses_by(self, *, user_name: str | None = None) -> SeasonGuess | Dict[str, SeasonGuess] | None: - if user_name is not None: - predicate: Callable[[SeasonGuess], bool] = lambda guess: guess.user.name == user_name - return find_single_or_none_strict(predicate, self.all_season_guesses()) - - if user_name is None: - guesses_by: Dict[str, SeasonGuess] = dict() - guess: SeasonGuess - - for guess in self.all_season_guesses(): - guesses_by[guess.user.name] = guess - - return guesses_by - - raise Exception("season_guesses_by encountered illegal combination of arguments") - def first_race_without_result(self) -> Race | None: """ Returns the first race-object with no associated race result. @@ -200,48 +79,9 @@ class TemplateModel(Model): else: return self.all_races()[0].name_sanitized - def none_team(self) -> Team: - return NONE_TEAM - def all_drivers_or_active_result_standing_drivers(self) -> List[Driver]: return self.active_result.ordered_standing_list() if self.active_result is not None else self.all_drivers(include_none=False) def drivers_for_wdc_gained(self) -> List[Driver]: predicate: Callable[[Driver], bool] = lambda driver: driver.abbr not in self._wdc_gained_excluded_abbrs return find_multiple_strict(predicate, self.all_drivers(include_none=False)) - - def none_driver(self) -> Driver: - return NONE_DRIVER - - @overload - def drivers_by(self, *, team_name: str) -> List[Driver]: - """ - Returns a list of all drivers driving for a certain team. - """ - return self.drivers_by(team_name=team_name) - - @overload - def drivers_by(self) -> Dict[str, List[Driver]]: - """ - Returns a dictionary of drivers mapped to team names. - """ - return self.drivers_by() - - def drivers_by(self, *, team_name: str | None = None) -> List[Driver] | Dict[str, List[Driver]]: - if team_name is not None: - predicate: Callable[[Driver], bool] = lambda driver: driver.team.name == team_name - return find_multiple_strict(predicate, self.all_drivers(include_none=False), 2) - - if team_name is None: - drivers_by: Dict[str, List[Driver]] = dict() - driver: Driver - team: Team - - for team in self.all_teams(include_none=False): - drivers_by[team.name] = [] - for driver in self.all_drivers(include_none=False): - drivers_by[driver.team.name] += [driver] - - return drivers_by - - raise Exception("drivers_by encountered illegal combination of arguments")