Source code for pgnhelper.swiss

"""Generates a swiss result table.

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

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

13.16.4. Individual Swiss Tournaments where not all the ratings are consistent:

   * Buchholz Cut 1
   * Buchholz
   * Sonneborn-Berger
   * Cumulative system - Sum of Progressive Scores
   * Direct encounter
   * The greater number of wins including forfeits
   * The greater number of wins with Black pieces

13.16.5. Individual Swiss Tournaments where all the ratings are consistent:

   * Buchholz Cut 1
   * Buchholz
   * Direct encounter
   * AROC
   * The greater number of wins including forfeits
   * The greater number of wins with Black pieces
   * The greater number of games with Black (unplayed games shall be counted as played with White)
   * Sonneborn-Berger 

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

Other reference:
FIDE Chess.com Grand Swiss 2021

4. 8. 3. Tie-breaks
If two (2) or more players score the same points, the tie is to be decided by the following criteria, in order of priority:

   a) Buchholz Cut 1;
   b) Buchholz;
   c) Sonneborn-Berger;
   d) Direct encounter between the players in tie;
   e) Drawing of lots.

All tie-breaks are calculated as described in C.02.13 of the FIDE Handbook.

Tie-break supported by this library:

TB1 = Buchholz Cut 1
TB2 = Buchholz
TB3 = Sonneborn-Berger
TB4 = Direct Encounter
TB5 = Number of wins
TB6 = Number of wins as black
"""


from typing import List, Tuple

import pandas as pd

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


# Define the tie-break ordering.
swiss_tiebreak = {
    0: {'name': 'Buchholz Cut 1', 'label': 'TB1', 'cut': 1},
    1: {'name': 'Buchholz', 'label': 'TB2', 'cut': 0}
}


[docs]class Swiss: """Manages swiss 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. round: The number of rounds. """ def __init__(self, infn: str, round: int = 20): self.infn = infn self.record = pd.DataFrame() self.rank = pd.DataFrame() self.winpoint = 1.0 self.drawpoint = 0.5 self.israting = False self.round = round
[docs] def player_ranking(self) -> pd.DataFrame: """Generates a dataframe of player ranking. Returns: A pandas dataframe of players ranking. """ data_p = [] for p in self.players: 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 if self.israting: rating = pgnhelper.elo.get_rating(self.record, p) if rating == '?': print(f'Warning player {p} has a rating of {rating}! Rating is disabled.') data_p.append([p, len(df_w) + len(df_b), final_score]) else: 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 convert_score(self, score: float): """Convert 1.0 to 1, 0.0 to 0 and 0.5 to = """ if score == 1.0: return '1' if score == 0.0: return '0' if score == 0.5: return '=' return '*'
[docs] def get_opp_info(self, opp_data: List, df_final: pd.DataFrame, dfr: pd.DataFrame, p: str) -> Tuple[List, bool]: """Creates result data to build swiss table. """ is_value = False dfp = dfr.loc[dfr.White == p] if len(dfp) > 0: # We are white, find the opponent rank, and our score. opp_name = dfp.Black.iloc[0] myscore = dfp.Wpt.iloc[0] dfopp = df_final.loc[df_final.Name == opp_name] opprank = int(dfopp.index[0]) + 1 opp_data.append(f'{opprank}{"W"}{self.convert_score(myscore)}') is_value = True else: dfp = dfr.loc[dfr.Black == p] if len(dfp) > 0: # We are black, find the opponent rank, and our score. opp_name = dfp.White.iloc[0] myscore = dfp.Bpt.iloc[0] dfopp = df_final.loc[df_final.Name == opp_name] opprank = int(dfopp.index[0]) + 1 opp_data.append(f'{opprank}{"B"}{self.convert_score(myscore)}') is_value = True return opp_data, is_value
[docs] def table(self) -> pd.DataFrame: """Generates a swiss result table. The table is sorted by [score, buchholz cut 1, buchholz, sonneborn-berger, direct encounter]. Returns: A pandas dataframe of swiss table. """ self.record, self.players, self.israting = pgnhelper.record.get_pgn_data(self.infn) # self.israting = False # 1. Create a dataframe of player ranking. self.rank = self.player_ranking() # 1.1 Apply buchholz tie-break. [buchholz cut 1, buchholz] tb_label = [] cut = swiss_tiebreak[0]['cut'] label = swiss_tiebreak[0]['label'] tb_label.append(label) df_tb1 = pgnhelper.tiebreak.tb_buchholz(self.record, self.rank, cut=cut, label=label) cut = swiss_tiebreak[1]['cut'] label = swiss_tiebreak[1]['label'] tb_label.append(label) df_tb2 = pgnhelper.tiebreak.tb_buchholz(self.record, df_tb1, cut=cut, label=label) # 1.2 Apply Sonneborn-Berger. df_tb3 = pgnhelper.tiebreak.sonneborn_berger(self.record, df_tb2, label='TB3') df_tb3 = df_tb3.sort_values( by=['Score', 'TB1', 'TB2', 'TB3', 'Name'], ascending=[False, False, False, False, True] ) tb_label.append('TB3') df_tb3 = df_tb3.reset_index(drop=True) # 1.3 Apply Direct Encounter tie-break df_tb4 = pgnhelper.tiebreak.direct_encounter(self.record, df_tb3, label='TB4') df_tb4 = df_tb4.sort_values( by=['Score', 'TB1', 'TB2', 'TB3', 'TB4', 'Name'], ascending=[False, False, False, False, False, True] ) tb_label.append('TB4') df_tb4 = df_tb4.reset_index(drop=True) # 1.4 Apply most number of wins tie-break df_tb5 = pgnhelper.tiebreak.num_wins(self.record, df_tb4, label='TB5') df_tb5 = df_tb5.sort_values( by=['Score', 'TB1', 'TB2', 'TB3', 'TB4', 'TB5'], ascending=[False, False, False, False, False, False] ) tb_label.append('TB5') df_tb5 = df_tb5.reset_index(drop=True) # 1.5 Apply most number of wins with black. df_tb6 = pgnhelper.tiebreak.num_wins(self.record, df_tb5, label='TB6', bwins=True) df_tb6 = df_tb6.sort_values( by=['Score', 'TB1', 'TB2', 'TB3', 'TB4', 'TB5', 'TB6'], ascending=[False, False, False, False, False, False, False] ) tb_label.append('TB6') df_tb6 = df_tb6.reset_index(drop=True) df_final = df_tb6.copy() # 2. Build a swiss table 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_swiss = {'Name': df_final.Name, 'Rating': df_final.Rating, 'RChg': rc} else: data_swiss = {'Name': df_final.Name.unique()} # Build a list of opp info per round and add it incrementally to the data_swiss dict. # round = 11 for grand swiss 2021 for r in range(1, self.round + 1): opp_data = [] for p in df_final.Name.unique(): for s in range(1, len(self.players) + 1): # grand swiss 2021 round values: 1.1, 1.2, 1.3, ... rs = f'{r}.{s}' dfr = self.record.loc[self.record.Round == rs] if len(dfr) < 1: continue opp_data, is_value = self.get_opp_info(opp_data, df_final, dfr, p) if is_value: break if len(opp_data): data_swiss.update({f'R{r}': opp_data}) df_swiss = pd.DataFrame(data_swiss) # 3. Add other columns at the end. df_swiss['Games'] = df_final['Games'] df_swiss['Score'] = df_final['Score'] df_swiss['Score%'] = 100 * df_final['Score'] / df_final['Games'] df_swiss['Score%'] = df_swiss['Score%'].round(2) df_swiss[tb_label[0]] = df_tb1[tb_label[0]].round(2) df_swiss[tb_label[1]] = df_tb2[tb_label[1]].round(2) df_swiss[tb_label[2]] = df_tb3[tb_label[2]].round(2) df_swiss[tb_label[3]] = df_tb4[tb_label[3]].round(2) df_swiss[tb_label[4]] = df_tb5[tb_label[4]].round(2) df_swiss[tb_label[5]] = df_tb6[tb_label[5]].round(2) # 4. Insert rank column at first column. df_swiss.insert(loc=0, column='Rank', value=range(1, len(df_swiss) + 1)) return df_swiss