"""Module dedictated to RatingSystem
RatingSystem are ranking component acting as defaultdictionary to store rating for player.
It provide get(), set() and ordinal() methods.
"""
from typing import List, Any, Callable, Optional
from collections import defaultdict
from typeguard import typechecked
from rstt.stypes import SPlayer
from rstt import BasicPlayer
import copy
[docs]
class keydefaultdict(defaultdict[SPlayer, Any]):
def __init__(self, default_factory: Callable[[SPlayer], Any]):
"""Defaultdict with default values as function of the missing key.
This allows rating system to have a default rating based on some data available in a Player instance, such has the level
or a win rate ratio.
Parameters
----------
default_factory : Callable[[Player], Any]
A function computing a default value for a missing key.
"""
super().__init__()
self.default_factory = default_factory
def __missing__(self, key: SPlayer) -> Any:
# Source: https://stackoverflow.com/a/73975965
if self.default_factory is None:
raise KeyError(key)
else:
ret = self[key] = self.default_factory(key)
return ret
[docs]
class KeyModel:
@typechecked
def __init__(self, default: Optional[Any] = None, template: Optional[Callable] = None, factory: Optional[Callable] = None, **kwargs):
r"""Basic Rating system
The KeyModel is a intuitive implementation of :class:`rstt.stypes.RatingSystem` that strores ratings of player in a defauldict.
The default rating value can be specified in three different fashions.
Parameters
----------
default : Any, optional
A default rating value, by default None.
template : Callable, optional
A lambda function to generate default ratings, by default None
factory : _type_, optional
A Callable of the form lambda x: ..., where x is the SPlayer with no rating, by default None. If you pass a factory,
the KeyModel stores ratings in a :class:`rstt.ranking.datamodel.keydefauldict`.
.. note::
If you are using the \'template\' or \'factory\', you can additionaly specify \**kwargs to be passed to the template every time it is called to generate a new rating.
And for the \'default\', no additonal parameters are allowed.
Raises
------
ValueError
Error are raised in the case of incompatible parameters. Examples of valid calls are:
KeyModel(default=1500)
KeyModel(template=GaussianRating, 1500, 250)
KeyModel(factory=GaussianRating, mu=1500, sigma=250)
"""
self.__ratings = self.__init_ratings(
default, template, factory, **kwargs)
self.__rtype = self.__get_rating_type()
self.__default = self.__get_default_rating()
# --- setter --- #
[docs]
@typechecked
def set(self, key: SPlayer, rating):
"""Set method to manualy assign a rating to a player.
Parameters
----------
key : SPlayer
A Player to modify the rating
rating : Any
a new rating.
.. warning::
The KeyModel class is a 'Generic' container in the sense that it can store any sort of rating, however, all values must be of the same type.
With the current implementation it is unclear how to enforce it. So it is the responsability of the user to be consistent.
The set operation will not throw any error. Yet, following calls to :func:`rstt.ranking.datamodel.KeyModel.ordinal` ordinal method could fail without proper error traceback.
Hopefull future version will fix this issue.
"""
# TODO: test rating type before assignement and thorw TypeError
# !!! the problem is how the type is defined? built-in, class, protocol 'attribute protocol' ? isinstance does not work for all cases.
self.__ratings[key] = rating
# --- getter --- #
[docs]
@typechecked
def get(self, key: SPlayer) -> Any:
"""getter method for the rating of a player
Parameters
----------
key : SPlayer
A player to get the rating
Returns
-------
Any
The rating of the player
"""
# QUEST: __getitem__ ?
return self.__ratings[key]
[docs]
def items(self):
"""Dict like items() method
return the view object returned by the underlying dict.items method
Returns
-------
view object
items stored in the KeyModel
"""
return self.__ratings.items()
[docs]
def keys(self):
"""Dict like keys() method
return the view object returned by the underlying dict.keys method
Returns
-------
view object
keys, i.e. SPlayer.
"""
return self.__ratings.keys()
# --- general purpose methords --- #
[docs]
def rtype(self) -> type:
"""Getter for the rating type
.. warning::
The type of ratings is infered at instanciation of the KeyModel using the type() built-in functions.
However this can be inconsistant with the user's intention.
Returns
-------
type
The rating type.
"""
return self.__rtype
[docs]
def default(self) -> Any:
"""Getter for default rating
When instanciated, a default rating is generated. In the case of a 'factory' instanciation, the default rating is built with the help of a 'Dummy' :class:`rstt.player.basicplayer.BasicPlayer`
Returns
-------
Any
The default rating an unrank player gets.
"""
return self.__default
[docs]
def ordinal(self, rating: Any) -> float:
"""Convert a rating into a float value
The returned value is used to compare players to each other. An higher value is understood as a 'better' player
.. note::
This method assume the existence of a magic __float__ method for the ratings.
If one uses custom ratings object, it is good to either write such method, override the ordinal methods, or design an associated
:class:`rstt.stypes.RatingSystem`.
Parameters
----------
rating : Any
A rating compatible with the KeyModel.rtypes()
Returns
-------
float
The corresponding value used to compare players in an ordered fashion.
"""
# REQ: Should not support arg type Player. This is the job of a Ranking/Standing
# NOTE: name source -> https://fr.wikipedia.org/wiki/Nombre_ordinal
return float(rating)
[docs]
def tiebreaker(self, rating: Any) -> List[Any]:
"""
.. warning::
DO NOT USE.
Boilerplate code for future features.
"""
try:
return list(rating)
except TypeError:
return 0
# --- internal mechanism --- #
def __init_ratings(self, default, template, factory, **kwargs):
''' rating initialization
return: defaultdict {key: rating}
where key are the one contained in the standing
and rating are object used to compute the associated value.
REQ:
- each key as its own rating
- user can provide a rating object (a) or a type to use as model (b).
- these to option are imcompatible and an error need to be raised
- match the default value with the self.__rate_model(rating) method
(a) We use deepcopy to create a new rating instance with equal value.
(b) We use a Constructor with params to generate a new rating object.
TODO:
- check functool.partial to improve code quaity and readabiltiy.
'''
if default is not None and not template and not factory:
if kwargs:
msg = "Can not pass additional argument when using the 'default' parameter."
raise ValueError(msg)
ratings = self.__default_ratings(value=default)
elif template and not default and not factory:
ratings = self.__template_ratings(template, **kwargs)
elif factory and not default and not template:
ratings = self.__factory_ratings(factory, **kwargs)
else:
msg = "Exactly one parameter among 'default', 'template' and 'factory' should be passed."
raise ValueError(msg)
return ratings
def __default_ratings(self, value):
return defaultdict(lambda: copy.deepcopy(value))
def __template_ratings(self, template, **kwargs):
return defaultdict(lambda: template(**kwargs))
def __factory_ratings(self, func: Callable, **kwargs):
return keydefaultdict(default_factory=lambda x: func(x, **kwargs))
def __get_rating_type(self):
dummy = BasicPlayer('dummy', 0.0)
# !!! This may not always work as intended
rtype = type(self.__ratings[dummy])
del self.__ratings[dummy]
return rtype
def __get_default_rating(self):
dummy = BasicPlayer('dummy', 0.0)
self.__ratings[dummy]
return self.__ratings.pop(dummy)
# --- magic methods --- #
def __delitem__(self, key: SPlayer):
del self.__ratings[key]
# ??? __setitem__
# ??? __getitem__
# ??? __contain__
[docs]
class GaussianModel(KeyModel):
def __init__(self, *arg, **kwargs):
"""Gaussian Rating Systems
Associated ratings must have a mu and a sigma attributes.
"""
super().__init__(*arg, **kwargs)
'''
TODO: type check the ratings, something like:
if not hasattr(self.default(), 'mu') or not hasattr(self.default(), 'sigma'):
# raise some kind of error
Or maybe use Protocol with attributes
'''
# ??? How to typecheck: must at least support trueskill/openskill ratings and rstt GlickoRating
[docs]
def ordinal(self, rating) -> float:
# TODO: cite a good source for the used policy
"""Ordinal method for gaussian values
The methods implements the mean - 2 * standard deviation policy
Parameters
----------
rating : Gaussian like object
A floating value
Returns
-------
float
The corresponding value used to compare players in an ordered fashion
"""
return rating.mu - 2*rating.sigma
[docs]
def tiebreaker(self, rating):
return [rating.mu, rating.sigma]