Skip to content

Commit

Permalink
1.4
Browse files Browse the repository at this point in the history
+ add possibility to set the base to uncased (shusaku and Shusaku will be the same player)
+ you can get ordered ratings both in compact form and only current elo
~ you can put 'b' or 'w' in lower case for the winner
  • Loading branch information
Pierre-François Monville committed Aug 8, 2019
1 parent cd46a29 commit ede7ed0
Show file tree
Hide file tree
Showing 5 changed files with 143 additions and 61 deletions.
33 changes: 17 additions & 16 deletions LICENCE.txt
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
Copyright (c) 2016 The Python Packaging Authority (PyPA)
Copyright (c) 2012 Pete Schwamb

Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,7 @@ Rémi Coulom in his paper, used w2=14 to get his [results](https://www.remi-coul


whr = whole_history_rating.Base({'w2':14})

You can also set the base not case sensitive. "shusaku" and "ShUsAkU" will be the same.

whr = whole_history_rating.Base({'uncased':True})
2 changes: 1 addition & 1 deletion whr/game.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ def __init__(self,black, white, winner, time_step, handicap = 0, extras = None):
self.day = time_step
self.white_player = white
self.black_player = black
self.winner = winner
self.winner = winner.upper()
self.handicap = handicap
self.handicap_proc = handicap
if extras is None:
Expand Down
Binary file removed whr/test_whr
Binary file not shown.
165 changes: 121 additions & 44 deletions whr/whole_history_rating.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@
from collections import defaultdict
import time
import math
import ast
import pickle
class UnstableRatingException(Exception):
pass

class Base:

def __init__(self, config= None):
def __init__(self, config=None):
if config is None:
self.config = defaultdict(lambda: None)
else:
Expand All @@ -19,11 +20,16 @@ def __init__(self, config= None):
self.config["debug"] = False
if self.config.get("w2") is None:
self.config["w2"] = 300.0
if self.config.get("uncased") is None:
self.config["uncased"] = False
self.games = []
self.players = {}

def print_ordered_ratings(self, current=False):
"""displays all ratings for each player (for each of his playing days) ordered
Args:
current (bool, optional): True to let only the last estimation of the elo, False gets all estimation for each day played
"""
players = [x for x in self.players.values() if len(x.days) > 0]
players.sort(key=lambda x: x.days[-1].gamma())
Expand All @@ -34,7 +40,7 @@ def print_ordered_ratings(self, current=False):
else:
print(f"{p.name} => {[x.elo() for x in p.days]}")

def get_ordered_ratings(self, current = False, compact = False):
def get_ordered_ratings(self, current=False, compact=False):
"""gets all ratings for each player (for each of his playing days) ordered
Returns:
Expand All @@ -49,7 +55,9 @@ def get_ordered_ratings(self, current = False, compact = False):
players.sort(key=lambda x: x.days[-1].gamma())
for p in players:
if len(p.days) > 0:
if current:
if current and compact:
result.append(p.days[-1].elo())
elif current:
result.append((p.name, p.days[-1].elo()))
elif compact:
result.append([x.elo() for x in p.days])
Expand All @@ -59,7 +67,7 @@ def get_ordered_ratings(self, current = False, compact = False):

def log_likelihood(self):
"""gets the likelihood of the current state
the more iteration you do the higher the likelihood becomes
Returns:
Expand All @@ -71,7 +79,7 @@ def log_likelihood(self):
score += p.log_likelihood()
return score

def player_by_name(self,name):
def player_by_name(self, name):
"""gets the player object corresponding to the name
Args:
Expand All @@ -80,26 +88,31 @@ def player_by_name(self,name):
Returns:
Player: the corresponding player
"""
if self.config["uncased"]:
name = name.lower()
if self.players.get(name, None) is None:
self.players[name] = Player(name, self.config)
return self.players[name]

def ratings_for_player(self,name, current = False):
def ratings_for_player(self, name, current = False):
"""gets all rating for each day played for the player
Args:
name (str): the player's name
current (bool, optional): True to let only the last estimation of the elo and uncertainty, False gets all estimation for each day played
Returns:
list[list[int,float,float]]: for each day, the time_step the elo the uncertainty
list[list[int, float, float]]: for each day, the time_step the elo the uncertainty
"""
if self.config["uncased"]:
name = name.lower()
player = self.player_by_name(name)
if current:
return (round(player.days[-1].elo()), round(player.days[-1].uncertainty*100))
else:
return [[d.day, round(d.elo()), round(d.uncertainty*100)] for d in player.days]

def _setup_game(self,black,white,winner,time_step,handicap,extras={}):
def _setup_game(self, black, white, winner, time_step, handicap, extras={}):
if black == white:
raise(AttributeError("Invalid game (black player == white player)"))
return None
Expand All @@ -108,7 +121,7 @@ def _setup_game(self,black,white,winner,time_step,handicap,extras={}):
game = Game(black_player, white_player, winner, time_step, handicap, extras)
return game

def create_game(self, black, white, winner, time_step, handicap, extras = {}):
def create_game(self, black, white, winner, time_step, handicap, extras={}):
"""creates a new game to be added to the base
Args:
Expand All @@ -122,6 +135,9 @@ def create_game(self, black, white, winner, time_step, handicap, extras = {}):
Returns:
Game: the added game
"""
if self.config["uncased"]:
black = black.lower()
white = white.lower()
game = self._setup_game(black, white, winner, time_step, handicap, extras)
return self._add_game(game)

Expand All @@ -144,8 +160,9 @@ def iterate(self, count):
for name, player in self.players.items():
player.update_uncertainty()

def auto_iterate(self, time_limit = 10, precision = 10E-3):
"""Summary
def auto_iterate(self, time_limit=10, precision=10E-3):
"""iterates automatically until it converges or reaches the time limit
iterates iteratively ten by ten
Args:
time_limit (int, optional): the maximal time after which no more iteration are launched
Expand All @@ -168,7 +185,7 @@ def auto_iterate(self, time_limit = 10, precision = 10E-3):
return i, False
a = b

def _test_stability(self,v1,v2, precision = 10E-3):
def _test_stability(self, v1, v2, precision=10E-3):
"""tests if two lists of lists of floats are equal but a certain precision
Args:
Expand All @@ -186,21 +203,24 @@ def _test_stability(self,v1,v2, precision = 10E-3):
return False
return True

def probability_future_match(self, name1, name2, handicap = 0, extras = {}):
def probability_future_match(self, name1, name2, handicap=0, extras={}):
"""gets the probability of winning for an hypothetical match against name1 and name2
displays the probability of winning for name1 and name2 in percent rounded to the second decimal
Args:
name1 (str): name1's name
name2 (str): name2's name
handicap (int, optional): the handicap (in elo)
extras (dict, optional): extra parameters
name1 (str): name1's name
name2 (str): name2's name
handicap (int, optional): the handicap (in elo)
extras (dict, optional): extra parameters
Returns:
tuple(int,int): the probability between 0 and 1 for name1 first then name2
tuple(int, int): the probability between 0 and 1 for name1 first then name2
"""
# Avoid self-played games (no info)
if self.config["uncased"]:
name1 = name1.lower()
name2 = name2.lower()
if name1 == name2:
raise(AttributeError("Invalid game (black == white)"))
return None
Expand Down Expand Up @@ -237,15 +257,16 @@ def load_games(self, games, separator=' '):
this function loads all games in the base
all match must comply to this format:
"black_name white_name winner time_step handicap extras"
black_name is required
white_name is required
winner is B or W is required
time_step is required
handicap is optional (default 0)
extras is a dict {} and optional
black_name (required)
white_name (required)
winner is B or W (required)
time_step (required)
handicap (optional: default 0)
extras is a dict (optional)
Args:
games (str|list[str]): Description
games (str|list[str]): a path or a list of string representing games
separator (str, optional): the separator between all elements of a game, space by default (every element will be trim eventually)
"""
data = None
if isinstance(games, str):
Expand All @@ -259,27 +280,34 @@ def load_games(self, games, separator=' '):
arguments = [x.strip() for x in line.split(separator)]
is_correct = False
if len(arguments) == 6:
black, white, winner, time_step, handicap, extras = arguments
extras = eval(extras)
is_correct = True
try:
black, white, winner, time_step, handicap, extras = arguments
extras = last = ast.literal_eval(extras)
if isinstance(extras,dict):
is_correct = True
except Exception as e:
raise(AttributeError(f"the extras argument couldn't be evaluated as a dict: {extras}\n{e}"))
if len(arguments) == 5:
black, white, winner, time_step, last = arguments
try:
eval_last = eval(last)
eval_last = ast.literal_eval(last)
if isinstance(eval_last,dict):
extras = eval_last
is_correct = True
elif isinstance(eval_last,int):
handicap = eval_last
is_correct = True
except Exception as e:
raise(AttributeError(f"the last argument couldn't be evaluated as an int or a dict: {last}"))
raise(AttributeError(f"the last argument couldn't be evaluated as an int or a dict: {last}\n{e}"))
if len(arguments) == 4:
black, white, winner, time_step = arguments
is_correct = True
if not is_correct:
raise(AttributeError(f"loaded game must have this format: 'black_name white_name winner time_step handicap extras' with handicap and extras optional. the handicap|extras argument is: {last}"))
raise(AttributeError(f"loaded game must have this format: 'black_name white_name winner time_step handicap extras' with handicap (int or dict) and extras (dict) optional. the handicap|extras argument is: {last}"))
time_step, handicap = int(time_step), int(handicap)
if self.config["uncased"]:
black = black.lower()
white = white.lower()
self.create_game(black, white, winner, time_step, handicap, extras=extras)

def save_base(self, path):
Expand Down Expand Up @@ -308,23 +336,72 @@ def load_base(path):


if __name__ == "__main__":
whr = Base(config={"w2":14})
print("test creating the base with modified w2 and uncased")
whr = Base(config={"w2":14, "uncased":True})
print("Done")
print("-------------------------------------------------------")
print("test creating one game")
whr.create_game("shusaku", "shusai", "B", 4, 0)
print("Done")
print("-------------------------------------------------------")
print("test creating one game with winner uncased (b instead of B)")
whr.create_game("shusaku", "shusai", "w", 5, 0)
print("Done")
print("-------------------------------------------------------")
print("test creating one game with cased letters (ShUsAkU instead of shusaku and ShUsAi instead of shusai)")
game = whr.create_game("ShUsAkU", "ShUsAi", "W", 6, 0)
print("Done")
print("-------------------------------------------------------")
print("test loading several games at once")
games = ["shusaku; shusai; B; 1", "shusaku;shusai;W;2;0", " shusaku ; shusai ;W ; 3; {'w2':300}", "shusaku;nobody;B;3;0;{'w2':300}"]
# whr.create_game("shusaku", "shusai", "B", 1, 0)
# whr.create_game("shusaku", "shusai", "W", 2, 0)
# whr.create_game("shusaku", "shusai", "W", 3, 0)
# a = whr.create_game("shusaku", "nobody", "B", 3, 0)
# print(a.bpd)
whr.load_games(games, separator=";")
print("Done")
print("-------------------------------------------------------")
print("test auto iterating to get convergence")
print(whr.auto_iterate())
print("Done")
print("-------------------------------------------------------")
print("test getting ratings for player shusaku (day, elo, uncertainty)")
print(whr.ratings_for_player("shusaku"))
print(whr.ratings_for_player("shusai"))
print("Done")
print("-------------------------------------------------------")
print("test getting ratings for player shusai, only current elo and uncertainty")
print(whr.ratings_for_player("shusai", current=True))
print("Done")
print("-------------------------------------------------------")
print("test getting probability of future match between shusaku and nobody2 (which default to 1 win 1 loss)")
print(whr.probability_future_match("shusai", "nobody2", 0))
print("Done")
print("-------------------------------------------------------")
print("test getting log likelihood of base")
print(whr.log_likelihood())
print("Done")
print("-------------------------------------------------------")
print("test printing ordered ratings")
whr.print_ordered_ratings()
print("Done")
print("-------------------------------------------------------")
print("test printing ordered ratings, only current elo")
whr.print_ordered_ratings(current=True)
print("Done")
print("-------------------------------------------------------")
print("test getting ordered ratings, compact form")
print(whr.get_ordered_ratings(compact=True))
print("Done")
print("-------------------------------------------------------")
print("test getting ordered ratings, only current elo with compact form")
print(whr.get_ordered_ratings(compact=True, current=True))
print("Done")
print("-------------------------------------------------------")
print("test saving base")
Base.save_base(whr,"test_whr")
print("Done")
print("-------------------------------------------------------")
print("test loading base")
whr2 = Base.load_base('test_whr')
print("Done")
print("-------------------------------------------------------")
print("test inspecting the first game")
print(whr2.games[0].inspect())


print("Done")
print("-------------------------------------------------------")

0 comments on commit ede7ed0

Please sign in to comment.