From dfb9360125f822d1c07d5067183e138de1f6ada8 Mon Sep 17 00:00:00 2001 From: Christoph Urlacher Date: Sat, 2 Mar 2024 20:31:34 +0100 Subject: [PATCH] Display current season guess state --- formula10/__init__.py | 8 +- formula10/controller/season_controller.py | 4 +- formula10/domain/points_model.py | 195 +++++++++++++++++++++- formula10/templates/base.jinja | 16 +- formula10/templates/enter.jinja | 6 +- formula10/templates/race.jinja | 2 +- formula10/templates/season.jinja | 39 +++-- 7 files changed, 236 insertions(+), 34 deletions(-) diff --git a/formula10/__init__.py b/formula10/__init__.py index a8ece89..d4bdc3f 100644 --- a/formula10/__init__.py +++ b/formula10/__init__.py @@ -31,7 +31,10 @@ import formula10.controller.error_controller # TODO -# Statistics +# Leaderboard + +# - For season guess calc there is missing: Fastest laps + sprint points + sprint DNFs (in race result) + # - Display total points somewhere in race table? Below the name in the table header. # - Auto calculate season points # - Highlight currently correct values for some season guesses (e.g. current most dnfs, team winners, podiums) @@ -39,6 +42,9 @@ import formula10.controller.error_controller # - Interesting stats: # - Which driver was voted most for dnf (top 5)? +# Statistics +# - Display stats: Driver standing, Team standing, DNFs, Fastest laps + # General # - Decouple names from IDs + Fix Valtteri/Russel spelling errors # - Unit testing (as much as possible, but especially points calculation) diff --git a/formula10/controller/season_controller.py b/formula10/controller/season_controller.py index 8830c66..eed6983 100644 --- a/formula10/controller/season_controller.py +++ b/formula10/controller/season_controller.py @@ -6,6 +6,7 @@ from werkzeug import Response from formula10.database.model.db_team import DbTeam from formula10.database.update_queries import update_season_guess from formula10.domain.model.team import NONE_TEAM +from formula10.domain.points_model import PointsModel from formula10.domain.template_model import TemplateModel from formula10 import app, db @@ -20,8 +21,9 @@ def season_active_user(user_name: str) -> str: user_name = unquote(user_name) model = TemplateModel(active_user_name=user_name, active_result_race_name=None) + points = PointsModel() - return render_template("season.jinja", model=model) + return render_template("season.jinja", model=model, points=points) @app.route("/season-guess/", methods=["POST"]) diff --git a/formula10/domain/points_model.py b/formula10/domain/points_model.py index 2062e82..1eab6b4 100644 --- a/formula10/domain/points_model.py +++ b/formula10/domain/points_model.py @@ -1,10 +1,12 @@ -from typing import Callable, Dict, List, overload +from typing import Callable, Dict, List, Tuple, overload import numpy as np from formula10.domain.domain_model import Model -from formula10.domain.model.driver import NONE_DRIVER +from formula10.domain.model.driver import NONE_DRIVER, Driver 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.season_guess_result import SeasonGuessResult from formula10.domain.model.user import User RACE_GUESS_OFFSET_POINTS: Dict[int, int] = { @@ -26,6 +28,19 @@ SEASON_GUESS_TEAMWINNER_FALSE_POINTS: int = -3 SEASON_GUESS_PODIUMS_CORRECT_POINTS: int = 3 SEASON_GUESS_PODIUMS_FALSE_POINTS: int = -2 +DRIVER_RACE_POINTS: Dict[int, int] = { + 1: 25, + 2: 18, + 3: 15, + 4: 12, + 5: 10, + 6: 8, + 7: 6, + 8: 4, + 9: 2, + 10: 1 +} + STANDING_2023: Dict[str, int] = { "Max Verstappen": 1, "Sergio Perez": 2, @@ -34,14 +49,14 @@ STANDING_2023: Dict[str, int] = { "Charles Leclerc": 5, "Lando Norris": 6, "Carlos Sainz": 7, - "George Russel": 8, + "George Russel": 8, # @todo typo "Oscar Piastri": 9, "Lance Stroll": 10, "Pierre Gasly": 11, "Esteban Ocon": 12, "Alexander Albon": 13, "Yuki Tsunoda": 14, - "Valtteri Bottas": 15, + "Valteri Bottas": 15, # @todo typo "Nico Hulkenberg": 16, "Daniel Ricciardo": 17, "Zhou Guanyu": 18, @@ -75,6 +90,9 @@ class PointsModel(Model): """ _points_per_step: Dict[str, List[int]] | None = None + _wdc_points: Dict[str, int] | None = None + _wcc_points: Dict[str, int] | None = None + _dnfs: Dict[str, int] | None = None def __init__(self): Model.__init__(self) @@ -100,6 +118,104 @@ class PointsModel(Model): return self._points_per_step + # @todo Doesn't include fastest lap + sprint points + def wdc_points(self) -> Dict[str, int]: + if self._wdc_points is None: + self._wdc_points = dict() + + for driver in self.all_drivers(include_none=False): + self._wdc_points[driver.name] = 0 + + for race_result in self.all_race_results(): + for position, driver in race_result.standing.items(): + self._wdc_points[driver.name] += DRIVER_RACE_POINTS[int(position)] if int(position) in DRIVER_RACE_POINTS else 0 + + + return self._wdc_points + + def wcc_points(self) -> Dict[str, int]: + if self._wcc_points is None: + self._wcc_points = dict() + + for team in self.all_teams(include_none=False): + self._wcc_points[team.name] = 0 + + for race_result in self.all_race_results(): + for driver in race_result.standing.values(): + self._wcc_points[driver.team.name] += self.wdc_points()[driver.name] + + return self._wcc_points + + # @todo Doesn't include sprint dnfs + def dnfs(self) -> Dict[str, int]: + if self._dnfs is None: + self._dnfs = dict() + + for driver in self.all_drivers(include_none=False): + self._dnfs[driver.name] = 0 + + for race_result in self.all_race_results(): + for driver in race_result.all_dnfs: + self._dnfs[driver.name] += 1 + + return self._dnfs + + def wdc_diff_2023(self) -> Dict[str, int]: + diff: Dict[str, int] = dict() + + for driver in self.all_drivers(include_none=False): + diff[driver.name] = STANDING_2023[driver.name] - self.wdc_standing_by_driver()[driver.name] + + return diff + + def wdc_standing_by_position(self) -> Dict[int, str]: + standing: Dict[int, str] = dict() + + comparator: Callable[[Tuple[str, int]], int] = lambda item: item[1] + for position, (driver_name, _) in enumerate(sorted(self.wdc_points().items(), key=comparator)): + standing[position] = driver_name + + return standing + + # @note Doesn't handle shared places (also applies to the following 3 method) + def wdc_standing_by_driver(self) -> Dict[str, int]: + standing: Dict[str, int] = dict() + + comparator: Callable[[Tuple[str, int]], int] = lambda item: item[1] + for position, (driver_name, _) in enumerate(sorted(self.wdc_points().items(), key=comparator)): + standing[driver_name] = position + + return standing + + def wcc_standing_by_position(self) -> Dict[int, str]: + standing: Dict[int, str] = dict() + + comparator: Callable[[Tuple[str, int]], int] = lambda item: item[1] + for position, (team_name, _) in enumerate(sorted(self.wcc_points().items(), key=comparator)): + standing[position] = team_name + + return standing + + def wcc_standing_by_team(self) -> Dict[str, int]: + standing: Dict[str, int] = dict() + + comparator: Callable[[Tuple[str, int]], int] = lambda item: item[1] + for position, (team_name, _) in enumerate(sorted(self.wcc_points().items(), key=comparator)): + standing[team_name] = position + + return standing + + def most_dnfs_name(self) -> str: + most_dnfs: Tuple[str, int] | None = None + for driver_name, dnfs in self.dnfs().items(): + if most_dnfs is None or dnfs > most_dnfs[1]: + most_dnfs = (driver_name, dnfs) + + if most_dnfs is None: + raise Exception("Failed to find driver with most dnfs") + + return most_dnfs[0] + def points_per_step_cumulative(self) -> Dict[str, List[int]]: """ Returns a dictionary of lists, containing cumulative points per race for each user. @@ -171,7 +287,7 @@ class PointsModel(Model): last_points: int = 0 for user in self.users_sorted_by_points(): if self.total_points_by(user.name) < last_points: - position = position + 1 + position += 1 standing[user.name] = position @@ -192,9 +308,9 @@ class PointsModel(Model): continue if standing_points(race_guess, race_result) > 0: - count = count + 1 + count += 1 if dnf_points(race_guess, race_result) > 0: - count = count + 1 + count += 1 return count @@ -203,3 +319,68 @@ class PointsModel(Model): return 0.0 return self.total_points_by(user_name) / self.picks_count(user_name) + + # + # Season guess evaluation + # + + def hot_take_correct(self, user_name: str) -> bool: + season_guess_result: SeasonGuessResult | None = self.season_guess_result_by(user_name=user_name) + + return season_guess_result.hot_take_correct if season_guess_result is not None else False + + def p2_constructor_correct(self, user_name: str) -> bool: + season_guess: SeasonGuess | None = self.season_guesses_by(user_name=user_name) + + if season_guess is None or season_guess.p2_wcc is None: + return False + + if 2 in self.wcc_standing_by_position(): + return self.wcc_standing_by_position()[2] == season_guess.p2_wcc.name + + return False + + def overtakes_correct(self, user_name: str) -> bool: + season_guess_result: SeasonGuessResult | None = self.season_guess_result_by(user_name=user_name) + + return season_guess_result.overtakes_correct if season_guess_result is not None else False + + def dnfs_correct(self, user_name: str) -> bool: + season_guess: SeasonGuess | None = self.season_guesses_by(user_name=user_name) + + if season_guess is None or season_guess.most_dnfs is None: + return False + + return season_guess.most_dnfs.name == self.most_dnfs_name() + + def most_gained_correct(self, user_name: str) -> bool: + season_guess: SeasonGuess | None = self.season_guesses_by(user_name=user_name) + + if season_guess is None or season_guess.most_wdc_gained is None: + return False + + comparator: Callable[[Tuple[str, int]], int] = lambda item: item[1] + return season_guess.most_wdc_gained.name == max(self.wdc_diff_2023().items(), key=comparator)[0] + + def most_lost_correct(self, user_name: str) -> bool: + season_guess: SeasonGuess | None = self.season_guesses_by(user_name=user_name) + + if season_guess is None or season_guess.most_wdc_lost is None: + return False + + comparator: Callable[[Tuple[str, int]], int] = lambda item: item[1] + return season_guess.most_wdc_lost.name == min(self.wdc_diff_2023().items(), key=comparator)[0] + + def is_team_winner(self, driver: Driver) -> bool: + teammates: List[Driver] = self.drivers_by(team_name=driver.team.name) + teammate: Driver = teammates[0] if teammates[1] == driver else teammates[1] + + return self.wdc_standing_by_driver()[driver.name] >= self.wdc_standing_by_driver()[teammate.name] + + def has_podium(self, driver: Driver) -> bool: + for race_result in self.all_race_results(): + position: int | None = race_result.driver_standing_position(driver) + if position is not None and position <= 3: + return True + + return False diff --git a/formula10/templates/base.jinja b/formula10/templates/base.jinja index 3525e65..4a9d974 100644 --- a/formula10/templates/base.jinja +++ b/formula10/templates/base.jinja @@ -24,9 +24,9 @@ {% endmacro %} {# Simple driver select for forms #} -{% macro driver_select(name, label, include_none, drivers=none, disabled=false) %} +{% macro driver_select(name, label, include_none, drivers=none, disabled=false, border="") %}
- {% if drivers == none %} @@ -42,9 +42,9 @@ {% endmacro %} {# Driver select for forms where a value might be preselected #} -{% macro driver_select_with_preselect(driver_match, name, label, include_none, drivers=none, disabled=false) %} +{% macro driver_select_with_preselect(driver_match, name, label, include_none, drivers=none, disabled=false, border="") %}
- {# Use namespace wrapper to persist scope between loop iterations #} {% set user_has_chosen = namespace(driverpre=false) %} @@ -75,9 +75,9 @@ {% endmacro %} {# Simple team select for forms #} -{% macro team_select(name, label, include_none, teams=none, disabled=false) %} +{% macro team_select(name, label, include_none, teams=none, disabled=false, border="") %}
- {% if teams == none %} @@ -93,9 +93,9 @@ {% endmacro %} {# Team select for forms where a value might be preselected #} -{% macro team_select_with_preselect(team_match, name, label, include_none, teams=none, disabled=false) %} +{% macro team_select_with_preselect(team_match, name, label, include_none, teams=none, disabled=false, border="") %}
- {# Use namespace wrapper to persist scope between loop iterations #} {% set user_has_chosen = namespace(teampre=false) %} diff --git a/formula10/templates/enter.jinja b/formula10/templates/enter.jinja index 328af53..696ff91 100644 --- a/formula10/templates/enter.jinja +++ b/formula10/templates/enter.jinja @@ -79,7 +79,7 @@ + {% if race_result_open == false %}readonly="readonly"{% endif %}>
@@ -89,7 +89,7 @@ + {% if race_result_open == false %}readonly="readonly"{% endif %}>
@@ -99,7 +99,7 @@ + {% if race_result_open == false %}readonly="readonly"{% endif %}> diff --git a/formula10/templates/race.jinja b/formula10/templates/race.jinja index e6a6e44..9f67d2f 100644 --- a/formula10/templates/race.jinja +++ b/formula10/templates/race.jinja @@ -129,7 +129,7 @@ {# Delete guess #}
+ {% if race_guess_open == false %}disabled="disabled"{% endif %}>
diff --git a/formula10/templates/season.jinja b/formula10/templates/season.jinja index 60158d5..c803d4f 100644 --- a/formula10/templates/season.jinja +++ b/formula10/templates/season.jinja @@ -38,9 +38,11 @@ {# Hot Take #}
- @@ -49,20 +51,31 @@ {# P2 Constructor #}
- {{ team_select_with_preselect(team_match=user_guess.p2_wcc, name="p2select", label="P2 in WCC:", include_none=false, disabled=not season_guess_open) }} + {{ team_select_with_preselect(team_match=user_guess.p2_wcc, name="p2select", label="P2 in WCC:", + include_none=false, disabled=not season_guess_open, + border=("border-success" if points.p2_constructor_correct(user.name) else "")) }}
{# Most Overtakes + DNFs #}
- {{ driver_select_with_preselect(driver_match=user_guess.most_overtakes, name="overtakeselect", label="Most overtakes:", include_none=false, disabled=not season_guess_open) }} - {{ driver_select_with_preselect(driver_match=user_guess.most_dnfs, name="dnfselect", label="Most DNFs:", include_none=false, disabled=not season_guess_open) }} + {{ driver_select_with_preselect(driver_match=user_guess.most_overtakes, name="overtakeselect", + label="Most overtakes:", include_none=false, disabled=not season_guess_open, + border=("border-success" if points.overtakes_correct(user.name) else "")) }} + {{ driver_select_with_preselect(driver_match=user_guess.most_dnfs, name="dnfselect", label="Most DNFs:", + include_none=false, disabled=not season_guess_open, + border=("border-success" if points.dnfs_correct(user.name) else "")) }}
{# Most Gained + Lost #}
- {{ driver_select_with_preselect(driver_match=user_guess.most_wdc_gained, name="gainedselect", label="Most WDC places gained:", include_none=false, drivers=model.drivers_for_wdc_gained(), disabled=not season_guess_open) }} - {{ driver_select_with_preselect(driver_match=user_guess.most_wdc_lost, name="lostselect", label="Most WDC places lost:", include_none=false, disabled=not season_guess_open) }} + {{ driver_select_with_preselect(driver_match=user_guess.most_wdc_gained, name="gainedselect", + label="Most WDC places gained:", include_none=false, drivers=model.drivers_for_wdc_gained(), + disabled=not season_guess_open, + border=("border-success" if points.most_gained_correct(user.name) else "")) }} + {{ driver_select_with_preselect(driver_match=user_guess.most_wdc_lost, name="lostselect", + label="Most WDC places lost:", include_none=false, disabled=not season_guess_open, + border=("border-success" if points.most_lost_correct(user.name) else "")) }}
{# Team-internal Winners #} @@ -82,7 +95,7 @@ value="{{ driver_a.name }}" {% if (user_guess is not none) and (driver_a in user_guess.team_winners) %}checked="checked"{% endif %} {% if season_guess_open == false %}disabled="disabled"{% endif %}> -
@@ -95,7 +108,7 @@ value="{{ driver_b.name }}" {% if (user_guess is not none) and (driver_b in user_guess.team_winners) %}checked="checked"{% endif %} {% if season_guess_open == false %}disabled="disabled"{% endif %}> -
@@ -118,7 +131,7 @@ value="{{ driver_a.name }}" {% if (user_guess is not none) and (driver_a in user_guess.podiums) %}checked="checked"{% endif %} {% if season_guess_open == false %}disabled="disabled"{% endif %}> - @@ -131,7 +144,7 @@ value="{{ driver_b.name }}" {% if (user_guess is not none) and (driver_b in user_guess.podiums) %}checked="checked"{% endif %} {% if season_guess_open == false %}disabled="disabled"{% endif %}> - @@ -139,7 +152,7 @@ + {% if season_guess_open == false %}disabled="disabled"{% endif %}>