Display current season guess state
All checks were successful
Build Formula10 Docker Image / build-docker (push) Successful in 14s

This commit is contained in:
2024-03-02 20:31:34 +01:00
parent a0051aacc3
commit dfb9360125
7 changed files with 236 additions and 34 deletions

View File

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

View File

@ -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/<user_name>", methods=["POST"])

View File

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

View File

@ -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="") %}
<div class="form-floating">
<select name="{{ name }}" id="{{ name }}" class="form-select" aria-label="{{ name }}" {% if disabled %}disabled="disabled"{% endif %}>
<select name="{{ name }}" id="{{ name }}" class="form-select {{ border }}" aria-label="{{ name }}" {% if disabled %}disabled="disabled"{% endif %}>
<option value="" selected disabled hidden></option>
{% 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="") %}
<div class="form-floating">
<select name="{{ name }}" id="{{ name }}" class="form-select" aria-label="{{ name }}" {% if disabled %}disabled="disabled"{% endif %}>
<select name="{{ name }}" id="{{ name }}" class="form-select {{ border }}" aria-label="{{ name }}" {% if disabled %}disabled="disabled"{% endif %}>
{# 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="") %}
<div class="form-floating">
<select name="{{ name }}" id="{{ name }}" class="form-select" aria-label="{{ name }}" {% if disabled %}disabled="disabled"{% endif %}>
<select name="{{ name }}" id="{{ name }}" class="form-select {{ border }}" aria-label="{{ name }}" {% if disabled %}disabled="disabled"{% endif %}>
<option value="" selected disabled hidden></option>
{% 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="") %}
<div class="form-floating">
<select name="{{ name }}" id="{{ name }}" class="form-select" aria-label="{{ name }}" {% if disabled %}disabled="disabled"{% endif %}>
<select name="{{ name }}" id="{{ name }}" class="form-select {{ border }}" aria-label="{{ name }}" {% if disabled %}disabled="disabled"{% endif %}>
{# Use namespace wrapper to persist scope between loop iterations #}
{% set user_has_chosen = namespace(teampre=false) %}

View File

@ -79,7 +79,7 @@
<input type="checkbox" class="form-check-input" value="{{ driver.name }}"
id="first-dnf-{{ driver.name }}" name="first-dnf-drivers"
{% if (model.active_result is not none) and (driver in model.active_result.initial_dnf) %}checked{% endif %}
{% if race_result_open == false %}disabled="disabled"{% endif %}>
{% if race_result_open == false %}readonly="readonly"{% endif %}>
<label for="first-dnf-{{ driver.name }}"
class="form-check-label text-muted">1. DNF</label>
</div>
@ -89,7 +89,7 @@
<input type="checkbox" class="form-check-input" value="{{ driver.name }}"
id="dnf-{{ driver.name }}" name="dnf-drivers"
{% if (model.active_result is not none) and (driver in model.active_result.all_dnfs) %}checked{% endif %}
{% if race_result_open == false %}disabled="disabled"{% endif %}>
{% if race_result_open == false %}readonly="readonly"{% endif %}>
<label for="dnf-{{ driver.name }}"
class="form-check-label text-muted">DNF</label>
</div>
@ -99,7 +99,7 @@
<input type="checkbox" class="form-check-input" value="{{ driver.name }}"
id="exclude-{{ driver.name }}" name="excluded-drivers"
{% if (model.active_result is not none) and (driver in model.active_result.standing_exclusions) %}checked{% endif %}
{% if race_result_open == false %}disabled="disabled"{% endif %}>
{% if race_result_open == false %}readonly="readonly"{% endif %}>
<label for="exclude-{{ driver.name }}"
class="form-check-label text-muted" data-bs-toggle="tooltip"
title="Driver is not counted for standing">NC</label>

View File

@ -129,7 +129,7 @@
{# Delete guess #}
<form action="{{ action_delete_href }}" method="post">
<input type="submit" class="btn btn-dark mt-2 w-100" value="Delete"
{% if race_guess_open == false %}disabled{% endif %}>
{% if race_guess_open == false %}disabled="disabled"{% endif %}>
</form>
</td>

View File

@ -38,9 +38,11 @@
{# Hot Take #}
<div class="form-floating">
<textarea class="form-control" id="hot-take-input-{{ user.name }}" name="hottakeselect"
style="height: 150px"
{% if season_guess_open == false %}disabled="disabled"{% endif %}>
<textarea
class="form-control {% if points.hot_take_correct(user.name) %}border-success{% endif %}"
id="hot-take-input-{{ user.name }}" name="hottakeselect"
style="height: 150px"
{% if season_guess_open == false %}disabled="disabled"{% endif %}>
{%- if user_guess is not none -%}{{ user_guess.hot_take_string() }}{%- endif -%}
</textarea>
@ -49,20 +51,31 @@
{# P2 Constructor #}
<div class="mt-2">
{{ 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 "")) }}
</div>
{# Most Overtakes + DNFs #}
<div class="input-group mt-2">
{{ 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 "")) }}
</div>
{# Most Gained + Lost #}
<div class="input-group mt-2" data-bs-toggle="tooltip"
title="Which driver will gain/lose the most places in comparison to last season's results?">
{{ 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 "")) }}
</div>
{# 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 %}>
<label class="form-check-label"
<label class="form-check-label {% if (user_guess is not none) and (driver_a in user_guess.team_winners) and points.is_team_winner(driver_a) %}text-success{% endif %}"
for="teamwinner-{{ team.name }}-1-{{ user.name }}">{{ driver_a.name }}</label>
</div>
</div>
@ -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 %}>
<label class="form-check-label"
<label class="form-check-label {% if (user_guess is not none) and (driver_b in user_guess.team_winners) and points.is_team_winner(driver_b) %}text-success{% endif %}"
for="teamwinner-{{ team.name }}-2-{{ user.name }}">{{ driver_b.name }}</label>
</div>
</div>
@ -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 %}>
<label class="form-check-label"
<label class="form-check-label {% if (user_guess is not none) and (driver_a in user_guess.podiums) and points.has_podium(driver_a) %}text-success{% endif %}"
for="podium-{{ driver_a.name }}-{{ user.name }}">{{ driver_a.name }}</label>
</div>
</div>
@ -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 %}>
<label class="form-check-label"
<label class="form-check-label {% if (user_guess is not none) and (driver_b in user_guess.podiums) and points.has_podium(driver_b) %}text-success{% endif %}"
for="podium-{{ driver_b.name }}-{{ user.name }}">{{ driver_b.name }}</label>
</div>
</div>
@ -139,7 +152,7 @@
</div>
<input type="submit" class="btn btn-danger mt-2 w-100" value="Save"
{% if season_guess_open == false %}disabled{% endif %}>
{% if season_guess_open == false %}disabled="disabled"{% endif %}>
</form>
</div>