from typing import Optional
from typeguard import typechecked
from rstt.stypes import SPlayer, Score
import rstt.config as cfg
[docs]
class Match():
@typechecked
def __init__(self, teams: list[list[SPlayer]], tracking: Optional[bool] = None) -> None:
"""Match base Class
General purpose match class. It can be used to create arbitrary game mode such as Many-versus-Many or Free-For-All games.
Parameters
----------
teams : List[List[SPlayer]]
Participants of the match organized in a list of list. Players in the same sublist are part of the same team.
tracking : bool, optional
If true, the match will try to add itself to the participants game history, by default None.
Raises
------
ValueError:
A Splayer can not be an element of two distinct sublist in the parameter teams.
"""
self.__teams = teams
self.__scores: list[float] = None
self.__tracking = tracking if tracking is not None else cfg.MATCH_HISTORY
if len(set(self.players())) != len(self.players()):
msg = "Teams must contain different players."
raise ValueError(msg)
# --- getter --- #
[docs]
def teams(self) -> list[list[SPlayer]]:
"""Getter method for teams
Returns
-------
List[List[SPlayer]]
All the players participating in the match grouped by teams.
"""
return self.__teams
[docs]
def players(self) -> list[SPlayer]:
"""Getter method for player
Unlike :func:`rstt.game.match.Match.teams`, it returns a simple list of SPlayer, without grouping them by teams.
Returns
-------
List[SPlayer]
All the participants of the match.
"""
return [player for team in self.__teams for player in team]
[docs]
def opponents(self, player: SPlayer) -> list[SPlayer]:
"""Getter method for opponents
Opponents are participants of the match that or not in the same team as the given player.
Parameters
----------
player : SPlayer
A player to get the opponents
Returns
-------
List[SPlayer]
A list of opponents playing against the player, in a single list, not grouped by teams
"""
return [p for p in self.players() if p not in self.teammates(player)]
[docs]
def teammates(self, player: SPlayer) -> list[SPlayer]:
"""Getter method for teammates
Teammates of a player are other players in the same teams
Parameters
----------
player : SPlayer
A player to get teammates
Returns
-------
List[SPlayer]
All the player's teammates
"""
for team in self.players():
if player in team:
return [p for p in team if p != player]
[docs]
def scores(self) -> Score:
"""Getter method for the match outcome
The result/outcome of the match. A :class:`rstt.stypes.Score` is a list of float.
The length of the Score is equal to the number of teams (i.e the length of the return value of :func: `rstt.game.match.Match.teams`).
Returns
-------
Score
The outcom of the match. None if the match has not been played yet. Ordering of the float value matches the ordering of the teams as return by :func:`rstt.game.match.Match.teams`
"""
return self.__scores
[docs]
def score(self, player: SPlayer) -> float:
"""Getter method for the score of a given player.
Unlike the :func:`rstt.game.match.Match.scores`, this function return a single float value representing the success the parameter player had.
Parameters
----------
player : SPlayer
A player to get the score.
Returns
-------
float
A value representing the score, result of the player.
Raises
------
RuntimeError
This method can only be called when the match has been played and assigned a score.
"""
if self.__scores is None:
msg = f"Undefined score of player {player} is undefined. The match has not yet been assigned a score."
raise RuntimeError(msg)
for team, score in zip(self.__teams, self.__scores):
if player in team:
return score
[docs]
def ranks(self) -> list[int]:
"""Getter method for the team ranks
The ranks method is an alternative to the scores method. For a Match between n teams, the return list contains the values 1,...,n.
The higher the score of a team, the lower the value in the return list.
Returns
-------
List[int]
The rank of each team.
Raises
------
RuntimeError
This method can only be called when the match has been played and assigned a score.
"""
if not self.__scores:
msg = "Undefined ranks. The match has not yet been assigned a score."
raise RuntimeError(msg)
# !!! What is the covention when multiple teams have the same score, are tied ?
return [len([other for other in self.__scores if other > value]) + 1 for value in self.__scores]
# --- user interface --- #
[docs]
def live(self) -> bool:
"""Getter method for the match status
A live match is a match that has not yet been assigned an outcome/result.
Returns
-------
bool
True if the match has yet to be played. False if the match has a scores assigned.
"""
return True if self.__scores is None else False
# --- internal mechanism --- #
def __set_result(self, result: Score):
# bunch of errors to raise
if self.__scores is not None:
msg = f'Attempt to assign a score to a game that has already one {self}'
raise RuntimeError(msg)
if not isinstance(result, list):
msg = f"result must be instance of List[float], received {type(result)}"
raise TypeError(msg)
if not isinstance(result[0], float):
msg = f'result must be instance of List[float], received List[{type(result[0])}]'
raise TypeError(msg)
if len(result) != len(self._Match__teams):
msg = f"""result lenght does not match number of teams,
len(result) == {len(result)}, excepted: {len(self._Match__teams)}"""
raise ValueError(msg)
# actual result assignement
self.__scores = result
# player may track match history
if self.__tracking:
self.__update_players_history()
def __update_players_history(self):
for player in self.players():
try:
player.add_game(self)
except AttributeError:
pass # ??? raise warning
# --- magic methods --- #
def __repr__(self) -> str:
return str(self)
def __str__(self) -> str:
return f"{type(self)} - teams: {self.__teams}, scores: {self.__scores}"
def __contains__(self, player: SPlayer) -> bool:
return player in self.players()
[docs]
class Duel(Match):
def __init__(self, player1: SPlayer, player2: SPlayer, tracking: Optional[bool] = None) -> None:
"""Duel class
Duel is a special type of :class:`rstt.game.match.Match` with only two teams each consisiting of one player.
In other words, two players facing each others.
Parameters
----------
player1 : SPlayer
A player considered at 'home'.
player2 : SPlayer
A player considered as 'visitor'
tracking : bool, optional
If true, the duel will try to add itself (once it has been assigned a score) to the both player's game history, by default None.
"""
tracking = tracking if tracking is not None else cfg.DUEL_HISTORY
super().__init__(teams=[[player1], [player2]], tracking=tracking)
# --- getter --- #
[docs]
def player1(self) -> SPlayer:
"""Getter method for player 1
Player1 can also be refer has the one playing at 'home'.
Returns
-------
SPlayer
the first player of the duel.
"""
return self._Match__teams[0][0]
[docs]
def player2(self) -> SPlayer:
"""Getter method for palyer 2
Player2 - the opponent of player1 - can also be refered as 'visitor' or playing 'away'.
Returns
-------
SPlayer
the 2nd player of the duel.
"""
return self._Match__teams[1][0]
[docs]
def opponent(self, player: SPlayer) -> SPlayer:
"""Getter method for the opponent of a player in a duel
Suger method that returns the same value has :func:`rstt.game.match.Match.opponents`, but grammatically more correct has there is only one opponent in a duel.
Parameters
----------
player : SPlayer
A player to get the opponent.
Returns
-------
Splayer
The opponent of the parameter player.
Raises:
-------
KeyError
When the parameter player is not a participant of the duel.
"""
players = set(self.players())
# this can raise a KeyError, which is what we want
players.remove(player)
return list(players)[0]
[docs]
def winner(self) -> SPlayer:
"""Getter method for the winner of the duel
In a direct confrontation between two competitors, there is usually a winner (the one with the highest score, lowest rank) and a loser.
Returns
-------
SPlayer
The winner of the duel. Can be None if the duel has not yet been played or in the case of a draw.
"""
if not self._Match__scores:
return None
if self._Match__scores[0] > self._Match__scores[1]:
return self._Match__teams[0][0]
elif self._Match__scores[0] < self._Match__scores[1]:
return self._Match__teams[1][0]
else:
return None
# return self._Match__teams[0][0] if self._Match__scores[0] > self._Match__scores[1] else self._Match__teams[1][0]
[docs]
def loser(self) -> SPlayer:
"""Getter method for the loser of the duel
The loser is the player with the lowest score value, highest rank.
Returns
-------
SPlayer
The loser of the duel. Can be None if the duel has not yet been played or in the case of a draw.
"""
if not self._Match__scores:
return None
if self._Match__scores[0] > self._Match__scores[1]:
return self._Match__teams[1][0]
elif self._Match__scores[0] < self._Match__scores[1]:
return self._Match__teams[0][0]
else:
return None
# return self._Match__teams[0][0] if self._Match__scores[1] > self._Match__scores[0] else self._Match__teams[1][0]
[docs]
def isdraw(self) -> bool:
"""Getter method indicating a draw
A draw is a match where both player have the same scores, ranks.
Returns
-------
bool
True if both player have the same score, in that case the :func:`rstt.game.match.Duel.winner` and :func:`rstt.game.match.Match.loser` return None.
False if the duel has a winner and a loser.
"""
if not self._Match__scores:
return False
return True if self._Match__scores[0] == self._Match__scores[1] else False