Source code for bascenev1._gameresults
# Released under the MIT License. See LICENSE for details.
#
"""Functionality related to game results."""
from __future__ import annotations
import copy
import weakref
from dataclasses import dataclass
from typing import TYPE_CHECKING
from efro.util import asserttype
import babase
from bascenev1._team import Team, SessionTeam
if TYPE_CHECKING:
from typing import Sequence
import bascenev1
[docs]
@dataclass
class WinnerGroup:
"""Winning team or teams as calculated by a :class:`GameResults`."""
score: int | None
teams: list[bascenev1.SessionTeam]
[docs]
class GameResults:
"""Results for a completed game.
Upon completion, a game should fill one of these out and pass it to
its :meth:`~bascenev1.Activity.end()` call.
"""
def __init__(self) -> None:
self._game_set = False
self._scores: dict[
int, tuple[weakref.ref[bascenev1.SessionTeam], int | None]
] = {}
self._sessionteams: list[weakref.ref[bascenev1.SessionTeam]] | None = (
None
)
self._playerinfos: list[bascenev1.PlayerInfo] | None = None
self._lower_is_better: bool | None = None
self._score_label: str | None = None
self._none_is_winner: bool | None = None
self._scoretype: bascenev1.ScoreType | None = None
[docs]
def set_game(self, game: bascenev1.GameActivity) -> None:
"""Set the game instance these results are applying to."""
if self._game_set:
raise RuntimeError('Game set twice for GameResults.')
self._game_set = True
self._sessionteams = [
weakref.ref(team.sessionteam) for team in game.teams
]
scoreconfig = game.getscoreconfig()
self._playerinfos = copy.deepcopy(game.initialplayerinfos)
self._lower_is_better = scoreconfig.lower_is_better
self._score_label = scoreconfig.label
self._none_is_winner = scoreconfig.none_is_winner
self._scoretype = scoreconfig.scoretype
[docs]
def set_team_score(self, team: bascenev1.Team, score: int | None) -> None:
"""Set the score for a given team.
This can be a number or None (see the ``none_is_winner`` arg in
the constructor).
"""
assert isinstance(team, Team)
sessionteam = team.sessionteam
self._scores[sessionteam.id] = (weakref.ref(sessionteam), score)
[docs]
def get_sessionteam_score(
self, sessionteam: bascenev1.SessionTeam
) -> int | None:
"""Return the score for a given team."""
assert isinstance(sessionteam, SessionTeam)
for score in list(self._scores.values()):
if score[0]() is sessionteam:
return score[1]
# If we have no score value, assume None.
return None
@property
def sessionteams(self) -> list[bascenev1.SessionTeam]:
"""Return all teams in the results."""
if not self._game_set:
raise RuntimeError("Can't get teams until game is set.")
teams = []
assert self._sessionteams is not None
for team_ref in self._sessionteams:
team = team_ref()
if team is not None:
teams.append(team)
return teams
[docs]
def has_score_for_sessionteam(
self, sessionteam: bascenev1.SessionTeam
) -> bool:
"""Return whether there is a score for a given team."""
return any(s[0]() is sessionteam for s in self._scores.values())
[docs]
def get_sessionteam_score_str(
self, sessionteam: bascenev1.SessionTeam
) -> babase.Lstr:
"""Return the score for the given team as an :class:`~bascenev1.Lstr`.
(properly formatted for the score type.)
"""
from bascenev1._score import ScoreType
if not self._game_set:
raise RuntimeError("Can't get team-score-str until game is set.")
for score in list(self._scores.values()):
if score[0]() is sessionteam:
if score[1] is None:
return babase.Lstr(value='-')
if self._scoretype is ScoreType.SECONDS:
return babase.timestring(score[1], centi=False)
if self._scoretype is ScoreType.MILLISECONDS:
return babase.timestring(score[1] / 1000.0, centi=True)
return babase.Lstr(value=str(score[1]))
return babase.Lstr(value='-')
@property
def playerinfos(self) -> list[bascenev1.PlayerInfo]:
"""Get info about the players represented by the results."""
if not self._game_set:
raise RuntimeError("Can't get player-info until game is set.")
assert self._playerinfos is not None
return self._playerinfos
@property
def scoretype(self) -> bascenev1.ScoreType:
"""The type of score."""
if not self._game_set:
raise RuntimeError("Can't get score-type until game is set.")
assert self._scoretype is not None
return self._scoretype
@property
def score_label(self) -> str:
"""The label associated with scores ('points', etc)."""
if not self._game_set:
raise RuntimeError("Can't get score-label until game is set.")
assert self._score_label is not None
return self._score_label
@property
def lower_is_better(self) -> bool:
"""Whether lower scores are better."""
if not self._game_set:
raise RuntimeError("Can't get lower-is-better until game is set.")
assert self._lower_is_better is not None
return self._lower_is_better
@property
def winning_sessionteam(self) -> bascenev1.SessionTeam | None:
"""The winning team if there is exactly one, or else None."""
if not self._game_set:
raise RuntimeError("Can't get winners until game is set.")
winners = self.winnergroups
if winners and len(winners[0].teams) == 1:
return winners[0].teams[0]
return None
@property
def winnergroups(self) -> list[WinnerGroup]:
"""The ordered list of winner-groups."""
if not self._game_set:
raise RuntimeError("Can't get winners until game is set.")
# Group by best scoring teams.
winners: dict[int, list[bascenev1.SessionTeam]] = {}
scores = [
score
for score in self._scores.values()
if score[0]() is not None and score[1] is not None
]
for score in scores:
assert score[1] is not None
sval = winners.setdefault(score[1], [])
team = score[0]()
assert team is not None
sval.append(team)
results: list[tuple[int | None, list[bascenev1.SessionTeam]]] = list(
winners.items()
)
results.sort(
reverse=not self._lower_is_better,
key=lambda x: asserttype(x[0], int),
)
# Also group the 'None' scores.
none_sessionteams: list[bascenev1.SessionTeam] = []
for score in self._scores.values():
scoreteam = score[0]()
if scoreteam is not None and score[1] is None:
none_sessionteams.append(scoreteam)
# Add the Nones to the list (either as winners or losers
# depending on the rules).
if none_sessionteams:
nones: list[tuple[int | None, list[bascenev1.SessionTeam]]] = [
(None, none_sessionteams)
]
if self._none_is_winner:
results = nones + results
else:
results = results + nones
return [WinnerGroup(score, team) for score, team in results]
# Docs-generation hack; import some stuff that we likely only forward-declared
# in our actual source code so that docs tools can find it.
from typing import (Coroutine, Any, Literal, Callable,
Generator, Awaitable, Sequence, Self)
import asyncio
from concurrent.futures import Future
from pathlib import Path
from enum import Enum