Source code for pgnhelper.roundrobin

"""Generates a round-robin result table.

It reads the input pgn file and generates a dataframe of round-robin table.
It also add columns for tie-break scores for tied players.

Typical tie-break system that can be applied to a round-robin tournament
according to FIDE.

13.16.2. Individual Round-Robin Tournaments:
    * Direct encounter
    * The greater number of wins, including forfeits
    * Sonneborn-Berger
    * Koya System (to be implemented in pgnhelper)

https://handbook.fide.com/files/handbook/C02Standards.pdf
"""


from pathlib import Path
from typing import Optional

import pandas as pd

import pgnhelper.tiebreak
import pgnhelper.utility
import pgnhelper.elo
import pgnhelper.record


[docs]class RoundRobin: """Manages round-robin result table generation. Attributes: infn: The input pgn file. infnarm: The input pgn file with armageddon games. winpoint: The point for the winner. drawpoint: The point when player draws. winpointarm: The point for the winner in armageddon game. losspointarm: The point for the loser in armageddon game. """ def __init__(self, infn: str, infnarm: Optional[str] = None, winpoint: float = 1.0, drawpoint: float = 0.5, winpointarm: float = 1.0, losspointarm: float = 0.0, showmaxscore: bool = False): self.infn = infn self.infnarm = infnarm self.winpoint = winpoint self.drawpoint = drawpoint self.winpointarm = winpointarm self.losspointarm = losspointarm self.showmaxscore = showmaxscore self.record = pd.DataFrame() self.rank = pd.DataFrame() self.players = [] self.israting = False
[docs] def games_per_encounter(self) -> int: """Counts the number of games per encounter excluding armageddon. Returns: The number of games per encounter. """ for p in self.players: for m in self.players: if p == m: continue dfw = self.record.loc[ (self.record.White == p) & (self.record.Black == m) & (self.record.Arm == 0)] dfb = self.record.loc[ (self.record.Black == p) & (self.record.White == m) & (self.record.Arm == 0)] return len(dfw) + len(dfb) return 0
[docs] def player_ranking(self) -> pd.DataFrame: """Generates a dataframe of player ranking. Returns: A pandas dataframe of players ranking. """ is_arm = True if 1 in self.record['Arm'].unique() else False data_p = [] for p in self.players: if not is_arm: df_w = self.record[self.record.White == p] df_b = self.record[self.record.Black == p] score_w = len(df_w[df_w.Result == '1-0']) * self.winpoint score_w += len(df_w[df_w.Result == '1/2-1/2']) * self.drawpoint score_b = len(df_b[df_b.Result == '0-1']) * self.winpoint score_b += len(df_b[df_b.Result == '1/2-1/2']) * self.drawpoint final_score = score_w + score_b else: # Used to get the number of games for normal games. df_w = self.record[(self.record.White == p) & (self.record.Arm == 0)] df_b = self.record[(self.record.Black == p) & (self.record.Arm == 0)] # Get the score for normal games without draws. df_w_normal = self.record.loc[ (self.record.White == p) & (self.record.Arm == 0) & (self.record.Result != '1/2-1/2')] df_b_normal = self.record.loc[ (self.record.Black == p) & (self.record.Arm == 0) & (self.record.Result != '1/2-1/2')] score_w_n = len(df_w_normal.loc[df_w_normal.Result == '1-0']) * self.winpoint score_b_n = len(df_b_normal.loc[df_b_normal.Result == '0-1']) * self.winpoint # Get score for arm games. df_w_arm = self.record.loc[(self.record.White == p) & (self.record.Arm == 1)] df_b_arm = self.record.loc[(self.record.Black == p) & (self.record.Arm == 1)] # wins score_w_aw = len(df_w_arm.loc[df_w_arm.Result == '1-0']) * self.winpointarm score_b_aw = len(df_b_arm.loc[df_b_arm.Result == '0-1']) * self.winpointarm score_b_aw += len(df_b_arm.loc[df_b_arm.Result == '1/2-1/2']) * self.winpointarm # loses score_w_al = len(df_w_arm.loc[df_w_arm.Result == '0-1']) * self.losspointarm score_w_al += len(df_w_arm.loc[df_w_arm.Result == '1/2-1/2']) * self.losspointarm score_b_al = len(df_b_arm.loc[(df_b_arm.Result == '1-0')]) * self.losspointarm final_score = (score_w_n + score_b_n + score_w_aw + score_b_aw + score_w_al + score_b_al) if self.israting: rating = df_w.WElo.iloc[0] # all players have at least played a game with white data_p.append([p, rating, len(df_w) + len(df_b), final_score]) else: data_p.append([p, len(df_w) + len(df_b), final_score]) if self.israting: self.rank = pd.DataFrame( data_p, columns=['Name', 'Rating', 'Games', 'Score']) else: self.rank = pd.DataFrame(data_p, columns=['Name', 'Games', 'Score']) self.rank = self.rank.sort_values(by=['Score', 'Name'], ascending=[False, True]) self.rank = self.rank.reset_index(drop=True) return self.rank
[docs] def table(self) -> pd.DataFrame: """Generates a round-robin result table. The table is sorted by DE or Direct Encounter, Number of Wins and SB (sonneborn-Berger) Returns: A pandas dataframe of round-robin table. """ dfall = [] dfn, self.players, self.israting = pgnhelper.record.get_pgn_data(self.infn, is_arm=False) dfall.append(dfn) if self.infnarm is not None: dfa, _, _ = pgnhelper.record.get_pgn_data(self.infnarm, is_arm=True) dfall.append(dfa) self.record = pd.concat(dfall, ignore_index=False) is_arm = True if 1 in self.record.Arm.unique() else False # 1. Create a dataframe of player ranking. self.rank = self.player_ranking() gpe = self.games_per_encounter() # 1.1 Apply Direct Encounter tie-break df_de = pgnhelper.tiebreak.direct_encounter( self.record, self.rank, self.winpoint, self.drawpoint, self.winpointarm, self.losspointarm) df_de = df_de.sort_values(by=['Score', 'DE', 'Name'], ascending=[False, False, True]) df_de = df_de.reset_index(drop=True) # 1.2 Apply Number of Wins tie-break df_wins = pgnhelper.tiebreak.num_wins(self.record, df_de) df_wins = df_wins.sort_values(by=['Score', 'DE', 'Wins', 'Name'], ascending=[False, False, False, True]) df_wins = df_wins.reset_index(drop=True) # 1.3 Apply Sonneborn-Berger tie-break df_sb = pgnhelper.tiebreak.sonneborn_berger( self.record, df_wins, gpe=gpe, winpoint=1.0, drawpoint=0.5) df_sb = df_sb.sort_values( by=['Score', 'DE', 'Wins', 'SB', 'Name'], ascending=[False, False, False, False, True]) df_sb = df_sb.reset_index(drop=True) # 1.4 Apply the Koya system. df_koya = pgnhelper.tiebreak.koya_system( self.record, df_sb, winpoint=1.0, drawpoint=0.5) df_koya = df_koya.sort_values( by=['Score', 'DE', 'Wins', 'SB', 'Koya', 'Name'], ascending=[False, False, False, False, False, True]) df_koya = df_koya.reset_index(drop=True) df_final = df_koya.copy() # 2. Build a round-robin dataframe. if self.israting: # Add rating change. rc = [] for p in list(df_final.Name): r = pgnhelper.elo.get_rating_change(self.record, p, k=10) rc.append(round(r, 2)) data_rr = {'Name': df_final.Name, 'Rating': df_final.Rating, 'RChg': rc} else: data_rr = {'Name': df_final.Name.unique()} cnt = 1 for p in df_final.Name.unique(): data_v = [] for op in df_final.Name.unique(): if p == op: v = 'x' else: score = pgnhelper.utility.get_encounter_score( self.record, p, op, self.winpoint, self.drawpoint, self.winpointarm, self.losspointarm) # Todo: Properly detect if players have already played # or both received a score of 0 for some reason. # For now if both have zero score, it is assumed that # they have not played yet. We use a - symbol on the # round-robin table. if score[0] == 0 and score[1] == 0: v = '-' else: v = score[1] # use the score of op only data_v.append(v) data_rr.update({cnt: data_v}) cnt += 1 df_rr = pd.DataFrame(data_rr) # 3. Add other columns at the end. df_rr['Games'] = df_final['Games'] df_rr['Score'] = df_final['Score'] max_score = df_final['Games'] * self.winpoint if self.showmaxscore: df_rr['MaxScore'] = max_score df_rr['Score%'] = 100 * df_final['Score'] / max_score df_rr['Score%'] = df_rr['Score%'].round(2) df_rr['DE'] = df_final['DE'].round(2) df_rr['Wins'] = df_final['Wins'].round(0) df_rr['SB'] = df_final['SB'].round(2) df_rr['Koya'] = df_final['Koya'].round(2) # 4. Insert rank column at first column. df_rr.insert(loc=0, column='Rank', value=range(1, len(df_rr) + 1)) return df_rr
[docs] def standing(self) -> pd.DataFrame: """Returns a dataframe of player standing. The standing is sorted by score, with tie-breaks DE, Wins and SB. """ df = self.table() if 'Rating' in df.columns: dfs = df[['Rank', 'Name', 'Rating', 'RChg', 'Games', 'Score', 'Score%', 'DE', 'Wins', 'SB', 'Koya']] else: dfs = df[['Rank', 'Name', 'Games', 'Score', 'Score%', 'DE', 'Wins', 'SB', 'Koya']] return dfs