Skip to content

Commit a02fcf4

Browse files
Create stalemate_tally.py
for someone on discord
1 parent 8e86a4e commit a02fcf4

1 file changed

Lines changed: 143 additions & 0 deletions

File tree

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
"""Count stalemate positions in PGN files grouped by ratings buckets.
2+
This script reads PGN files, processes the games, and counts the number of stalemate positions
3+
for each Elo rating bucket. It uses the chess library to handle PGN parsing and stalemate detection.
4+
5+
Context Request:
6+
I have a list of chess games downloaded from the lichess db
7+
Like this some billion games
8+
I want to study all these games individually to see how many games out of how many games ended in a stalemate!
9+
The games which ended in a stalemate will be grouped under these rating categories of players:
10+
1000-1200, 1200-1500, 1500-1700, 1700-1900, 1900-2100, 2100-2500
11+
Only games which will be considered is when a game ended in a sudden stalemate by a blunder by a player who was
12+
in a completely winning position and lost the game due to stalemate.
13+
It generally happens in time pressure.
14+
I want to make a study how many players commit these mistakes in that rating ranges.
15+
Can anybody help me with that?
16+
"""
17+
from argparse import ArgumentParser, Namespace
18+
from os import listdir, path
19+
from typing import Dict, List, Optional, TextIO, Union
20+
21+
from chess import Board, Move
22+
from chess.pgn import Game, read_game
23+
24+
if __name__ == "__main__":
25+
parser: ArgumentParser = ArgumentParser(
26+
description="Count stalemate positions in PGN files grouped by ratings buckets.")
27+
parser.add_argument(
28+
"--pgn_dir",
29+
type=str,
30+
default=path.join('.', "pgns"),
31+
help="Directory to PGN files to process"
32+
)
33+
parser.add_argument(
34+
'--elo_bucket_thresholds',
35+
nargs='+',
36+
type=List[int],
37+
default=[1200, 1500, 1700, 1900, 2100],
38+
help="Thresholds of the Elo rating buckets to use for grouping"
39+
)
40+
parser.add_argument(
41+
'--min_elo',
42+
type=Optional[int],
43+
default=None,
44+
help="Minimum Elo rating to consider"
45+
)
46+
parser.add_argument(
47+
'--max_elo',
48+
type=Optional[int],
49+
default=None,
50+
help="Maximum Elo rating to consider"
51+
)
52+
args: Namespace = parser.parse_args()
53+
pgn_dir: Union[str, bytes] = args.pgn_dir
54+
elo_bucket_thresholds: List[int] = args.elo_bucket_thresholds
55+
min_elo: Optional[int] = args.min_elo
56+
max_elo: Optional[int] = args.max_elo
57+
58+
# Ensure elo_bucket_thresholds is sorted
59+
elo_bucket_thresholds.sort()
60+
if path.exists(pgn_dir):
61+
print(f"PGN directory: {pgn_dir}")
62+
else:
63+
print(f"PGN directory does not exist: {pgn_dir}")
64+
exit(1)
65+
f: Union[str, bytes]
66+
pgs: List[Union[str, bytes]] = [f for f in listdir(pgn_dir) if str(f).lower().endswith(".pgn")]
67+
if len(pgs) == 0:
68+
print(f"No PGN files found in directory: {pgn_dir}")
69+
exit(1)
70+
# Initialize the stalemate count for each Elo bucket
71+
elo_buckets: Dict[int, Union[Dict[str, Union[int, str]], Dict[str, Union[int, None, str]]]] = {}
72+
last_bucket: Optional[int] = None
73+
i: int
74+
k: int
75+
for i, k in enumerate(elo_bucket_thresholds):
76+
if i == 0:
77+
if min_elo is not None:
78+
elo_buckets[k] = {"count": 0, 'min': min_elo, 'max': k, 'name': f"{min_elo}-{k}"}
79+
else:
80+
elo_buckets[k] = {"count": 0, 'min': None, 'max': k, 'name': f"{k}-down"}
81+
else:
82+
elo_buckets[k] = {"count": 0, 'min': last_bucket, 'max': k, 'name': f"{last_bucket}-{k}"}
83+
last_bucket = k
84+
if max_elo is not None:
85+
elo_buckets[max_elo] = {"count": 0, 'min': last_bucket, 'max': max_elo, 'name': f"{last_bucket}-{max_elo}"}
86+
else:
87+
elo_buckets[last_bucket + 1] = {"count": 0, 'min': last_bucket, 'max': None, 'name': f"{last_bucket}+up"}
88+
89+
# Process each PGN file
90+
pgn: Union[str, bytes]
91+
for pgn in pgs:
92+
print(f"Processing PGN file: {pgn}")
93+
file: TextIO
94+
with open(path.join(pgn_dir, pgn), "r") as file:
95+
game: Optional[Game] = read_game(file)
96+
if game is None or len(game.errors) > 0:
97+
print(f"Skipping file '{pgn}' due to read errors.")
98+
continue
99+
if game is not None:
100+
# Get the players' ratings
101+
white_elo_s: Optional[str] = game.headers.get("WhiteElo")
102+
black_elo_s: Optional[str] = game.headers.get("BlackElo")
103+
if white_elo_s is None or black_elo_s is None:
104+
print(f"Skipping game due to missing Elo ratings: {game.headers}")
105+
continue
106+
try:
107+
white_elo: int = int(white_elo_s)
108+
black_elo: int = int(black_elo_s)
109+
except ValueError:
110+
print(f"Invalid Elo ratings: {white_elo}, {black_elo}")
111+
continue
112+
113+
# Check if the game ended in stalemate
114+
board: Board = Board()
115+
move: Move
116+
for move in game.mainline_moves():
117+
try:
118+
board.push(move)
119+
except Exception:
120+
print(f"Invalid move in game: {move}")
121+
break
122+
if board.is_stalemate():
123+
elo: int
124+
for elo in (white_elo, black_elo):
125+
bucket: Union[Dict[str, Union[int, str]], Dict[str, Union[int, None, str]]]
126+
for bucket in elo_buckets.values():
127+
if bucket['min'] is not None and bucket['max'] is not None:
128+
if bucket['min'] <= elo < bucket['max']:
129+
bucket['count'] += 1
130+
continue
131+
if bucket['min'] is None:
132+
if elo < bucket['max']:
133+
bucket['count'] += 1
134+
continue
135+
if bucket['max'] is None:
136+
if elo > bucket['min']:
137+
bucket['count'] += 1
138+
continue
139+
140+
# Print the results
141+
print("Stalemate counts by Elo bucket:")
142+
for value in elo_buckets.values():
143+
print(f"{value['name']}: {value['count']} Stalemates")

0 commit comments

Comments
 (0)