"""Generates tie-break points on tied players.
Tie-breaks supported:
* Direct Encounter
* Number of wins
* Sonneborn-Berger
* Koya system
"""
from typing import List, Dict
import pandas as pd
import pgnhelper.utility
def played_each_other(result_df, tie_df) -> bool:
players = list(tie_df.Name)
for p in players:
for m in players:
if p == m:
continue
dfw = result_df.loc[(result_df.White == p) & (result_df.Black == m)]
dfb = result_df.loc[(result_df.Black == p) & (result_df.White == m)]
if len(dfw) + len(dfb) == 0:
return False
return True
[docs]def num_wins(result_df: pd.DataFrame, ranking_df: pd.DataFrame,
label: str = 'Wins', bwins: bool = False) -> pd.DataFrame:
"""Creates a dataframe with Win column.
If a game has an armageddon tie-break, we will only count the number of wins
based from the normal game only.
Args:
result_df: The result dataframe.
ranking_df: Ranking of players based on score.
label: The label or header of the resulting dataframe.
bwins: If true then only count wins by black. If not count all wins.
Returns:
A dataframe of ranking with Wins column for tie-break.
"""
ret = ranking_df.copy()
players = list(ret.Name)
tb = {}
for _, g in ret.groupby(['Score']):
if len(g) > 1:
for p in g.Name:
if not bwins:
df_w = result_df.loc[(result_df.White == p) & (result_df.Result == '1-0') & (result_df.Arm == 0)]
else:
df_w = pd.DataFrame()
df_b = result_df.loc[(result_df.Black == p) & (result_df.Result == '0-1') & (result_df.Arm == 0)]
num_wins = len(df_w) + len(df_b)
tb.update({p: num_wins})
# Create new column Wins.
wins = []
for p in players:
if p in tb:
wins.append(tb[p])
else:
wins.append(0)
ret[label] = wins
return ret
[docs]def direct_encounter(result_df: pd.DataFrame, ranking_df: pd.DataFrame,
winpoint: float = 1.0, drawpoint: float = 0.5,
winpointarm: float = 1.5, losspointarm: float = 1.0,
label: str = 'DE') -> pd.DataFrame:
"""Creates a dataframe with DE column or direct encounter.
Requirement:
It is only applied when tied players have played each other.
In round-robin format this can be applied automatically. But for
swiss format, the tied players have to be checked.
"""
players = list(ranking_df.Name)
tb = {}
ret: pd.DataFrame = ranking_df.copy()
for _, g in ret.groupby(['Score']):
if len(g) > 1:
if played_each_other(result_df, g):
for p in g.Name:
s = 0
for op in g.Name:
if p == op:
continue
score = pgnhelper.utility.get_encounter_score(
result_df, p, op, winpoint, drawpoint,
winpointarm, losspointarm)
s += score[0]
tb.update({p: {op: s}})
# Create new column DE.
de = []
for p in players:
if p in tb:
s = 0
for k, v in tb.items():
if k == p:
for _, v1 in v.items():
s += v1
de.append(s)
else:
de.append(0)
ret[label] = de
return ret
[docs]def sonneborn_berger(result_df: pd.DataFrame, ranking_df: pd.DataFrame,
gpe: int = 1, winpoint: float = 1.0,
drawpoint: float = 0.5,
label: str = 'SB') -> pd.DataFrame:
"""Creates a dataframe with SB column for Sonneborn-Berger score.
Armageddon games currently are excluded in the calculation.
Args:
result_df: A dataframe of [Round, White, Black, Result].
ranking_df: A dataframe of standing, [Name, Games, Score].
gpe: games per encounter
Returns:
A dataframe of round-robin result table.
"""
tb: Dict[str, int] = {}
ret: pd.DataFrame = ranking_df.copy()
players = list(ret.Name)
# 1. Loop thru the tied players.
for _, g in ret.groupby(['Score']):
if len(g) > 1:
for p in g['Name']:
tb_score = 0
for m in players:
if p == m:
continue
match_score = 0
# 2. Get the score when player wins or draws.
df_ww = result_df.loc[
(result_df.White == p) & (result_df.Black == m) &
(result_df.Result == '1-0') & (result_df.Arm == 0)]
df_wd = result_df.loc[
(result_df.White == p) & (result_df.Black == m) &
(result_df.Result == '1/2-1/2') & (result_df.Arm == 0)]
df_bw = result_df.loc[
(result_df.Black == p) & (result_df.White == m) &
(result_df.Result == '0-1') & (result_df.Arm == 0)]
df_bd = result_df.loc[
(result_df.Black == p) & (result_df.White == m) &
(result_df.Result == '1/2-1/2') & (result_df.Arm == 0)]
# 3. Calculate the scores.
match_score += (winpoint * len(df_ww) + winpoint * len(df_bw) +
len(df_wd) * drawpoint + len(df_bd) * drawpoint)
if match_score > drawpoint * gpe:
tb_score += ret.loc[ret.Name == m].Score.iloc[0]
elif match_score == drawpoint * gpe:
tb_score += ret.loc[ret.Name == m].Score.iloc[0] / 2
tb.update({p: tb_score})
# 4. Create new column SB.
tb_sb: List = []
for p in players:
if p not in tb:
tb_sb.append(0)
continue
tb_sb.append(tb[p])
ret[label] = tb_sb
return ret
[docs]def koya_system(result_df: pd.DataFrame, ranking_df: pd.DataFrame,
winpoint: float = 1.0,
drawpoint: float = 0.5) -> pd.DataFrame:
"""Creates a dataframe with Koya column for Koya system score.
Koya system - the number of points achieved against all opponents
who have achieved 50 % or more.
11.5.4.3, https://handbook.fide.com/files/handbook/C02Standards.pdf
Args:
result_df: A dataframe of [Round, White, Black, Result ...].
ranking_df: A dataframe of standing, [Name, Games, Score].
Returns:
A ranking dataframe with Koya column.
"""
tb: Dict[str, int] = {}
ret: pd.DataFrame = ranking_df.copy()
players = list(ret.Name)
# Get player dataframe who score 50% and above.
df50 = ranking_df.copy()
df50['Score%'] = 100 * df50['Score'] / df50['Games']
df50 = df50.loc[df50['Score%'] >= 50.0]
# 1. Loop thru the tied players.
for _, g in ret.groupby(['Score']):
if len(g) > 1:
for p in g['Name']:
tb_score = 0
for m in players:
if p == m:
continue
if m not in df50.Name.unique():
continue
# 2. Get the score when player wins or draws, excluding armageddon games.
df_ww = result_df.loc[
(result_df.White == p) & (result_df.Black == m) &
(result_df.Result == '1-0') & (result_df.Arm == 0)]
df_wd = result_df.loc[
(result_df.White == p) & (result_df.Black == m) &
(result_df.Result == '1/2-1/2') & (result_df.Arm == 0)]
df_bw = result_df.loc[
(result_df.Black == p) & (result_df.White == m) &
(result_df.Result == '0-1') & (result_df.Arm == 0)]
df_bd = result_df.loc[
(result_df.Black == p) & (result_df.White == m) &
(result_df.Result == '1/2-1/2') & (result_df.Arm == 0)]
# 3. Get the total score.
tb_score += (winpoint * len(df_ww) + winpoint * len(df_bw) +
len(df_wd) * drawpoint + len(df_bd) * drawpoint)
tb.update({p: tb_score})
# 4. Create new column Koya.
tb_sb: List = []
for p in players:
if p not in tb:
tb_sb.append(0)
continue
tb_sb.append(tb[p])
ret['Koya'] = tb_sb
return ret
[docs]def tb_buchholz(record_df: pd.DataFrame, rank_df: pd.DataFrame,
cut: int = 0, label: str = 'TB1') -> pd.DataFrame:
"""Calculates buchholz score or sum of opponents score.
This tie-break system is only applied for a tournament with swiss format.
Args:
record_df: A dataframe of tournament games records.
rank_df: A dataframe with player ranking, initially
at ['Name, Games, Score]. Later ['Name, Games, Score, Buchholz, ... tie-break system]
cut: Cut the player opponent score, if cut is 0 the default then
no one will be cut, this is the normal buchholz. If this is 1 then
the lowest score will be cut. If this is 2 then the last two lowest
scores will be cut. If value is -1 this is median or cut the highest
and lowest. If value is -2 then cut the 2 highest and 2 lowest.
Returns:
A dataframe of name and buchholz score.
"""
ret = rank_df.copy()
players = ret.Name.unique()
tb: Dict[str, int] = {}
for _, g in ret.groupby(['Score']):
if len(g) > 1:
for p in g['Name']:
opp_scores = []
dfw = record_df.loc[record_df.White == p]
for i in range(len(dfw)):
opp = dfw.iloc[i]['Black']
dfm = rank_df.loc[rank_df.Name == opp]
pts = dfm['Score'].iloc[0]
opp_scores.append(pts)
dfb = record_df.loc[record_df.Black == p]
for i in range(len(dfb)):
opp = dfb.iloc[i]['White']
dfm = rank_df.loc[rank_df.Name == opp]
pts = dfm['Score'].iloc[0]
opp_scores.append(pts)
# Sort the opponents scores so we can determine what to cut.
if cut != 0:
opp_scores.sort(reverse=True) # high to low
if cut > 0:
# Apply Buchholz cut 1 or 2 and so on.
opp_scores = opp_scores[:-cut]
# Else apply Median Buchholz.
else:
# 1. Median Buchholz, cut the highest and lowest.
# 2. Median Buchholz 2, cut the 2 highest and 2 lowest.
opp_scores = opp_scores[-cut:cut]
tb.update({p: sum(opp_scores)})
# Add Buchholz column.
tbs: List = []
for p in players:
if p not in tb:
tbs.append(0)
continue
tbs.append(tb[p])
ret[label] = tbs
# Sort tie-break scores.
sort_columns = list(ret.columns)
if 'Rating' in sort_columns:
sort_columns.remove('Rating')
sort_columns.remove('Games')
sort_columns.remove('Name')
sort_columns.append(label)
sort_order = [False for _ in sort_columns]
ret = ret.sort_values(
by=sort_columns,
ascending=sort_order
)
ret = ret.reset_index(drop=True)
return ret