Implement season guess evaluation
All checks were successful
Build Formula10 Docker Image / build-docker (push) Successful in 15s
All checks were successful
Build Formula10 Docker Image / build-docker (push) Successful in 15s
This commit is contained in:
@ -1,5 +1,5 @@
|
|||||||
import json
|
import json
|
||||||
from typing import Any, Callable, Dict, List, overload
|
from typing import Any, Callable, Dict, List, overload, Tuple
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
from formula10 import cache
|
from formula10 import cache
|
||||||
@ -11,6 +11,7 @@ from formula10.domain.model.season_guess import SeasonGuess
|
|||||||
from formula10.domain.model.season_guess_result import SeasonGuessResult
|
from formula10.domain.model.season_guess_result import SeasonGuessResult
|
||||||
from formula10.domain.model.team import Team
|
from formula10.domain.model.team import Team
|
||||||
from formula10.domain.model.user import User
|
from formula10.domain.model.user import User
|
||||||
|
from formula10.database.validation import find_single_or_none_strict
|
||||||
|
|
||||||
# Guess points
|
# Guess points
|
||||||
|
|
||||||
@ -68,6 +69,7 @@ WDC_STANDING_2023: Dict[str, int] = {
|
|||||||
"Kevin Magnussen": 19,
|
"Kevin Magnussen": 19,
|
||||||
"Logan Sargeant": 21,
|
"Logan Sargeant": 21,
|
||||||
}
|
}
|
||||||
|
|
||||||
WCC_STANDING_2023: Dict[str, int] = {
|
WCC_STANDING_2023: Dict[str, int] = {
|
||||||
"Red Bull": 1,
|
"Red Bull": 1,
|
||||||
"Mercedes": 2,
|
"Mercedes": 2,
|
||||||
@ -81,6 +83,12 @@ WCC_STANDING_2023: Dict[str, int] = {
|
|||||||
"Haas": 10,
|
"Haas": 10,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# In case a substitute driver is driving, those points have to be subtracted from the actual driver
|
||||||
|
# (Driver_ID, Race_ID, Points)
|
||||||
|
WDC_SUBSTITUTE_POINTS: List[Tuple[int, int, int]] = [
|
||||||
|
(15, 2, 6), # Bearman raced for Sainz in Saudi Arabia
|
||||||
|
(8, 17, 1), # Bearman raced for Magnussen in Azerbaijan
|
||||||
|
]
|
||||||
|
|
||||||
def standing_points(race_guess: RaceGuess, race_result: RaceResult) -> int:
|
def standing_points(race_guess: RaceGuess, race_result: RaceResult) -> int:
|
||||||
guessed_driver_position: int | None = race_result.driver_standing_position(
|
guessed_driver_position: int | None = race_result.driver_standing_position(
|
||||||
@ -106,6 +114,16 @@ def dnf_points(race_guess: RaceGuess, race_result: RaceResult) -> int:
|
|||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def substitute_points(driver: Driver, race_number: int) -> int:
|
||||||
|
predicate: Callable[[Tuple[int, int, int]], bool] = lambda substitution: driver.id == substitution[0] and race_number == substitution[1]
|
||||||
|
substitution: Tuple[int, int, int] = find_single_or_none_strict(predicate, WDC_SUBSTITUTE_POINTS)
|
||||||
|
|
||||||
|
if substitution is not None:
|
||||||
|
return substitution[2]
|
||||||
|
else:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
class PointsModel(Model):
|
class PointsModel(Model):
|
||||||
"""
|
"""
|
||||||
This class bundles all data + functionality required to do points calculations.
|
This class bundles all data + functionality required to do points calculations.
|
||||||
@ -170,8 +188,10 @@ class PointsModel(Model):
|
|||||||
driver_points_per_step[driver.name][race_number] += (
|
driver_points_per_step[driver.name][race_number] += (
|
||||||
DRIVER_FASTEST_LAP_POINTS
|
DRIVER_FASTEST_LAP_POINTS
|
||||||
if race_result.fastest_lap_driver == driver
|
if race_result.fastest_lap_driver == driver
|
||||||
|
and int(position) <= 10
|
||||||
else 0
|
else 0
|
||||||
)
|
)
|
||||||
|
driver_points_per_step[driver.name][race_number] -= substitute_points(driver, race_number)
|
||||||
|
|
||||||
for position, driver in race_result.sprint_standing.items():
|
for position, driver in race_result.sprint_standing.items():
|
||||||
driver_name: str = driver.name
|
driver_name: str = driver.name
|
||||||
@ -604,30 +624,64 @@ class PointsModel(Model):
|
|||||||
|
|
||||||
raise Exception("points_by received an illegal combination of arguments")
|
raise Exception("points_by received an illegal combination of arguments")
|
||||||
|
|
||||||
def total_points_by(self, user_name: str) -> int:
|
def season_points_by(self, *, user_name: str) -> int:
|
||||||
|
"""
|
||||||
|
Returns the number of points from seasonguesses for a specific user.
|
||||||
|
"""
|
||||||
|
big_picks = (int(self.hot_take_correct(user_name=user_name)) * 10
|
||||||
|
+ int(self.p2_constructor_correct(user_name=user_name)) * 10
|
||||||
|
+ int(self.overtakes_correct(user_name=user_name)) * 10
|
||||||
|
+ int(self.dnfs_correct(user_name=user_name)) * 10
|
||||||
|
+ int(self.most_gained_correct(user_name=user_name)) * 10
|
||||||
|
+ int(self.most_lost_correct(user_name=user_name)) * 10)
|
||||||
|
|
||||||
|
small_picks = 0
|
||||||
|
guess: SeasonGuess = self.season_guesses_by(user_name=user_name)
|
||||||
|
|
||||||
|
for driver in guess.team_winners:
|
||||||
|
if self.is_team_winner(driver):
|
||||||
|
small_picks += 3
|
||||||
|
else:
|
||||||
|
small_picks -= 3
|
||||||
|
|
||||||
|
# NOTE: Not picked drivers that had a podium are also wrong
|
||||||
|
for driver in self.all_drivers(include_none=False, include_inactive=True):
|
||||||
|
if driver in guess.podiums and self.has_podium(driver):
|
||||||
|
small_picks += 3
|
||||||
|
elif driver in guess.podiums and not self.has_podium(driver):
|
||||||
|
small_picks -=2
|
||||||
|
elif driver not in guess.podiums and self.has_podium(driver):
|
||||||
|
small_picks -=2
|
||||||
|
|
||||||
|
return big_picks + small_picks
|
||||||
|
|
||||||
|
def total_points_by(self, *, user_name: str, include_season: bool) -> int:
|
||||||
"""
|
"""
|
||||||
Returns the total number of points for a specific user.
|
Returns the total number of points for a specific user.
|
||||||
"""
|
"""
|
||||||
|
if include_season:
|
||||||
|
return sum(self.points_by(user_name=user_name)) + self.season_points_by(user_name=user_name)
|
||||||
|
else:
|
||||||
return sum(self.points_by(user_name=user_name))
|
return sum(self.points_by(user_name=user_name))
|
||||||
|
|
||||||
def users_sorted_by_points(self) -> List[User]:
|
def users_sorted_by_points(self, *, include_season: bool) -> List[User]:
|
||||||
"""
|
"""
|
||||||
Returns the list of users, sorted by their points from race guesses (in descending order).
|
Returns the list of users, sorted by their points from race guesses (in descending order).
|
||||||
"""
|
"""
|
||||||
comparator: Callable[[User], int] = lambda user: self.total_points_by(user.name)
|
comparator: Callable[[User], int] = lambda user: self.total_points_by(user_name=user.name, include_season=include_season)
|
||||||
return sorted(self.all_users(), key=comparator, reverse=True)
|
return sorted(self.all_users(), key=comparator, reverse=True)
|
||||||
|
|
||||||
@cache.cached(
|
@cache.cached(
|
||||||
timeout=None, key_prefix="points_user_standing"
|
timeout=None, key_prefix="points_user_standing"
|
||||||
) # Cleanup when adding/updating race results or users
|
) # Cleanup when adding/updating race results or users
|
||||||
def user_standing(self) -> Dict[str, int]:
|
def user_standing(self, *, include_season: bool) -> Dict[str, int]:
|
||||||
standing: Dict[str, int] = dict()
|
standing: Dict[str, int] = dict()
|
||||||
|
|
||||||
position: int = 1
|
position: int = 1
|
||||||
last_points: int = 0
|
last_points: int = 0
|
||||||
|
|
||||||
for user in self.users_sorted_by_points():
|
for user in self.users_sorted_by_points(include_season=include_season):
|
||||||
if self.total_points_by(user.name) < last_points:
|
if self.total_points_by(user_name=user.name, include_season=include_season) < last_points:
|
||||||
users_with_this_position = 0
|
users_with_this_position = 0
|
||||||
for _user, _position in standing.items():
|
for _user, _position in standing.items():
|
||||||
if _position == position:
|
if _position == position:
|
||||||
@ -639,7 +693,7 @@ class PointsModel(Model):
|
|||||||
|
|
||||||
standing[user.name] = position
|
standing[user.name] = position
|
||||||
|
|
||||||
last_points = self.total_points_by(user.name)
|
last_points = self.total_points_by(user_name=user.name, include_season=include_season)
|
||||||
|
|
||||||
return standing
|
return standing
|
||||||
|
|
||||||
@ -671,7 +725,7 @@ class PointsModel(Model):
|
|||||||
if self.picks_count(user_name) == 0:
|
if self.picks_count(user_name) == 0:
|
||||||
return 0.0
|
return 0.0
|
||||||
|
|
||||||
return self.total_points_by(user_name) / self.picks_count(user_name)
|
return self.total_points_by(user_name=user_name, include_season=False) / self.picks_count(user_name)
|
||||||
|
|
||||||
#
|
#
|
||||||
# Season guess evaluation
|
# Season guess evaluation
|
||||||
@ -738,12 +792,11 @@ class PointsModel(Model):
|
|||||||
teammates: List[Driver] = self.drivers_by(
|
teammates: List[Driver] = self.drivers_by(
|
||||||
team_name=driver.team.name, include_inactive=True
|
team_name=driver.team.name, include_inactive=True
|
||||||
)
|
)
|
||||||
teammate: Driver = teammates[0] if teammates[1] == driver else teammates[1]
|
|
||||||
|
|
||||||
return (
|
# Min - Highest position is the lowest place number
|
||||||
self.wdc_standing_by_driver()[driver.name]
|
winner: Driver = min(teammates, key=lambda driver: self.wdc_standing_by_driver()[driver.name])
|
||||||
<= self.wdc_standing_by_driver()[teammate.name]
|
|
||||||
)
|
return driver == winner
|
||||||
|
|
||||||
@cache.memoize(
|
@cache.memoize(
|
||||||
timeout=None, args_to_ignore=["self"]
|
timeout=None, args_to_ignore=["self"]
|
||||||
|
@ -29,6 +29,8 @@
|
|||||||
<th scope="col" class="text-center" style="min-width: 50px;">Place</th>
|
<th scope="col" class="text-center" style="min-width: 50px;">Place</th>
|
||||||
<th scope="col" class="text-center" style="min-width: 50px;">User</th>
|
<th scope="col" class="text-center" style="min-width: 50px;">User</th>
|
||||||
<th scope="col" class="text-center" style="min-width: 100px;">Points</th>
|
<th scope="col" class="text-center" style="min-width: 100px;">Points</th>
|
||||||
|
<th scope="col" class="text-center" style="min-width: 100px;">Points (Race)</th>
|
||||||
|
<th scope="col" class="text-center" style="min-width: 100px;">Points (Season)</th>
|
||||||
<th scope="col" class="text-center" style="min-width: 100px;">Total picks</th>
|
<th scope="col" class="text-center" style="min-width: 100px;">Total picks</th>
|
||||||
<th scope="col" class="text-center" style="min-width: 100px;" data-bs-toggle="tooltip"
|
<th scope="col" class="text-center" style="min-width: 100px;" data-bs-toggle="tooltip"
|
||||||
title="Any points count as correct">Correct picks
|
title="Any points count as correct">Correct picks
|
||||||
@ -38,12 +40,14 @@
|
|||||||
</thead>
|
</thead>
|
||||||
|
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for user in points.users_sorted_by_points() %}
|
{% for user in points.users_sorted_by_points(include_season=True) %}
|
||||||
{% set user_standing = points.user_standing()[user.name] %}
|
{% set user_standing = points.user_standing(include_season=True)[user.name] %}
|
||||||
<tr class="{% if user_standing == 1 %}table-danger{% endif %}">
|
<tr class="{% if user_standing == 1 %}table-danger{% endif %}">
|
||||||
<td class="text-center text-nowrap">{{ user_standing }}</td>
|
<td class="text-center text-nowrap">{{ user_standing }}</td>
|
||||||
<td class="text-center text-nowrap">{{ user.name }}</td>
|
<td class="text-center text-nowrap">{{ user.name }}</td>
|
||||||
<td class="text-center text-nowrap">{{ points.total_points_by(user.name) }}</td>
|
<td class="text-center text-nowrap">{{ points.total_points_by(user_name=user.name, include_season=True) }}</td>
|
||||||
|
<td class="text-center text-nowrap">{{ points.total_points_by(user_name=user.name, include_season=False) }}</td>
|
||||||
|
<td class="text-center text-nowrap">{{ points.season_points_by(user_name=user.name) }}</td>
|
||||||
<td class="text-center text-nowrap">{{ points.picks_count(user.name) }}</td>
|
<td class="text-center text-nowrap">{{ points.picks_count(user.name) }}</td>
|
||||||
<td class="text-center text-nowrap">{{ points.picks_with_points_count(user.name) }}</td>
|
<td class="text-center text-nowrap">{{ points.picks_with_points_count(user.name) }}</td>
|
||||||
<td class="text-center text-nowrap">{{ "%0.2f" % points.points_per_pick(user.name) }}</td>
|
<td class="text-center text-nowrap">{{ "%0.2f" % points.points_per_pick(user.name) }}</td>
|
||||||
@ -57,7 +61,7 @@
|
|||||||
|
|
||||||
<div class="card shadow-sm mb-2">
|
<div class="card shadow-sm mb-2">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
History
|
History (Race)
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
|
@ -45,7 +45,7 @@
|
|||||||
{% for user in model.all_users() %}
|
{% for user in model.all_users() %}
|
||||||
<td class="text-center text-nowrap" style="min-width: 100px;">
|
<td class="text-center text-nowrap" style="min-width: 100px;">
|
||||||
<a href="/race/{{ user.name_sanitized }}" class="link-dark">{{ user.name }}
|
<a href="/race/{{ user.name_sanitized }}" class="link-dark">{{ user.name }}
|
||||||
({{ points.total_points_by(user.name) }})</a>
|
({{ points.total_points_by(user_name=user.name, include_season=False) }})</a>
|
||||||
</td>
|
</td>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -95,8 +95,10 @@
|
|||||||
winners:</h6>
|
winners:</h6>
|
||||||
<div class="grid mt-2 container" style="row-gap: 0;">
|
<div class="grid mt-2 container" style="row-gap: 0;">
|
||||||
{% for team in model.all_teams(include_none=false) %}
|
{% for team in model.all_teams(include_none=false) %}
|
||||||
{% set driver_a = model.drivers_by(team_name=team.name, include_inactive=False)[0] %}
|
{# HACK: Choosing 0 and 1 will chose the drivers with the lowest IDs (although there could be others). #}
|
||||||
{% set driver_b = model.drivers_by(team_name=team.name, include_inactive=False)[1] %}
|
{# This means the drivers chosen from at the start of the season will be visible. #}
|
||||||
|
{% set driver_a = model.drivers_by(team_name=team.name, include_inactive=True)[0] %}
|
||||||
|
{% set driver_b = model.drivers_by(team_name=team.name, include_inactive=True)[1] %}
|
||||||
|
|
||||||
<div class="g-col-6">
|
<div class="g-col-6">
|
||||||
<div class="form-check form-check-inline">
|
<div class="form-check form-check-inline">
|
||||||
@ -133,8 +135,10 @@
|
|||||||
title="Which driver will reach at least a single podium?">Drivers with podium(s):</h6>
|
title="Which driver will reach at least a single podium?">Drivers with podium(s):</h6>
|
||||||
<div class="grid mt-2 container" style="row-gap: 0;">
|
<div class="grid mt-2 container" style="row-gap: 0;">
|
||||||
{% for team in model.all_teams(include_none=false) %}
|
{% for team in model.all_teams(include_none=false) %}
|
||||||
{% set driver_a = model.drivers_by(team_name=team.name, include_inactive=False)[0] %}
|
{# HACK: Choosing 0 and 1 will chose the drivers with the lowest IDs (although there could be others). #}
|
||||||
{% set driver_b = model.drivers_by(team_name=team.name, include_inactive=False)[1] %}
|
{# This means the drivers chosen from at the start of the season will be visible. #}
|
||||||
|
{% set driver_a = model.drivers_by(team_name=team.name, include_inactive=True)[0] %}
|
||||||
|
{% set driver_b = model.drivers_by(team_name=team.name, include_inactive=True)[1] %}
|
||||||
|
|
||||||
<div class="g-col-6">
|
<div class="g-col-6">
|
||||||
<div class="form-check form-check-inline">
|
<div class="form-check form-check-inline">
|
||||||
@ -145,7 +149,8 @@
|
|||||||
{% if (user_guess is not none) and (driver_a in user_guess.podiums) %}checked="checked"{% endif %}
|
{% if (user_guess is not none) and (driver_a in user_guess.podiums) %}checked="checked"{% endif %}
|
||||||
{% if season_guess_open == false %}disabled="disabled"{% endif %}>
|
{% if season_guess_open == false %}disabled="disabled"{% endif %}>
|
||||||
<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
|
<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
|
||||||
{% elif (user_guess is not none) and (driver_a in user_guess.podiums) and (not points.has_podium(driver_a)) %}text-danger{% endif %}"
|
{% elif (user_guess is not none) and (driver_a in user_guess.podiums) and (not points.has_podium(driver_a)) %}text-danger
|
||||||
|
{% elif (user_guess is not none) and (driver_a not in user_guess.podiums) and (points.has_podium(driver_a)) %}text-danger{% endif %}"
|
||||||
for="podium-{{ driver_a.id }}-{{ user.id }}">{{ driver_a.name }}</label>
|
for="podium-{{ driver_a.id }}-{{ user.id }}">{{ driver_a.name }}</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -159,7 +164,8 @@
|
|||||||
{% if (user_guess is not none) and (driver_b in user_guess.podiums) %}checked="checked"{% endif %}
|
{% if (user_guess is not none) and (driver_b in user_guess.podiums) %}checked="checked"{% endif %}
|
||||||
{% if season_guess_open == false %}disabled="disabled"{% endif %}>
|
{% if season_guess_open == false %}disabled="disabled"{% endif %}>
|
||||||
<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
|
<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
|
||||||
{% elif (user_guess is not none) and (driver_b in user_guess.podiums) and (not points.has_podium(driver_b)) %}text-danger{% endif %}"
|
{% elif (user_guess is not none) and (driver_b in user_guess.podiums) and (not points.has_podium(driver_b)) %}text-danger
|
||||||
|
{% elif (user_guess is not none) and (driver_b not in user_guess.podiums) and (points.has_podium(driver_b)) %}text-danger{% endif %}"
|
||||||
for="podium-{{ driver_b.id }}-{{ user.id }}">{{ driver_b.name }}</label>
|
for="podium-{{ driver_b.id }}-{{ user.id }}">{{ driver_b.name }}</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
Reference in New Issue
Block a user