-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathopening.py
222 lines (180 loc) · 9.4 KB
/
opening.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
import requests
import time
import copy
import chess
import chess.pgn
import re
class OpeningTree:
def __init__(self):
self.game = chess.pgn.Game()
self.current_node = self.game
self.memoization_dict = {}
def get_node_information(self, node=None):
node_information = {}
original_node = self.current_node
if node:
self.current_node = node
node_information = self.get_opening_details(node_information)
node_information = self.get_player_details(node_information)
node_information = self.get_engine_details(node_information)
self.current_node = original_node
return node_information
def get_opening_details(self, node_information):
comment = self.current_node.comment
opening_match = re.search(r'\[open: (.*?), (.*?)\]', comment)
if opening_match:
node_information['eco'] = opening_match.group(1)
node_information['openingname'] = opening_match.group(2)
else:
node_information['eco'] = ""
node_information['openingname'] = ""
# Recursively check parents if details are empty
if self.current_node.parent:
original_node = self.current_node
self.current_node = self.current_node.parent
parent_info = {}
parent_info = self.get_opening_details(parent_info)
# Update current node's information if parent's details are not empty
if parent_info['eco']:
node_information['eco'] = parent_info['eco']
node_information['openingname'] = parent_info['openingname']
self.current_node = original_node
return node_information
def get_player_details(self, node_information):
comment = self.current_node.comment
wdb_match = re.search(r'\[freq: (\d+), (\d+(?:\.\d+)?)\]\[wdb: (\d+), (\d+), (\d+)\]\[wdb%: (\d+(?:\.\d+)?), (\d+(?:\.\d+)?), (\d+(?:\.\d+)?)\]', comment)
if wdb_match:
node_information['total_occurrence'] = int(wdb_match.group(1))
node_information['frequency'] = float(wdb_match.group(2))
node_information['white_wins'] = int(wdb_match.group(3))
node_information['draws'] = int(wdb_match.group(4))
node_information['black_wins'] = int(wdb_match.group(5))
node_information['white_percentage'] = float(wdb_match.group(6))
node_information['draw_percentage'] = float(wdb_match.group(7))
node_information['black_percentage'] = float(wdb_match.group(8))
else:
node_information['total_occurrence'] = 0
node_information['frequency'] = 0.0
node_information['white_wins'] = 0
node_information['draws'] = 0
node_information['black_wins'] = 0
node_information['white_percentage'] = 0.0
node_information['draw_percentage'] = 0.0
node_information['black_percentage'] = 0.0
return node_information
def get_engine_details(self, node_information):
comment = self.current_node.comment
eval_match = re.search(r'\[%eval ([-+]?\d*\.\d+|\d+),(\d+)\]', comment)
if eval_match:
node_information['eval'] = float(eval_match.group(1))
node_information['evaldepth'] = int(eval_match.group(2))
else:
node_information['eval'] = "?"
node_information['evaldepth'] = 0
return node_information
def build_opening_tree(self, url="https://explorer.lichess.ovh/masters", relative_freq=100, min_occurrences=10000, engine_time=0.1):
fen = self.current_node.board().fen()
if fen in self.memoization_dict:
transposition = self.memoization_dict[fen]
# Replace the current node with the transposition in the parent node's variations
def add_subtree(transposition, counter):
# Replace only the relative frequency in the copied node's comment, keeping the original total occurrences
counter += 1
self.current_node.comment = transposition.comment
for transposition_variation in transposition.variations:
child_node = self.current_node.add_variation(chess.Move.from_uci(transposition_variation.move.uci()))
self.current_node = child_node
counter = add_subtree(transposition_variation, counter)
self.current_node = self.current_node.parent
return counter
counter = add_subtree(transposition, 0)
self.current_node.comment = re.sub(r'\[freq: (\d+), \d+\]', f'[freq: \g<1>, {relative_freq}]', self.current_node.comment)
print(f"Transposition encountered for {fen} (size: {counter})")
return self.current_node
position_info = get_position_info(fen, url)
eval = get_stockfish_eval(self.current_node.board(), engine_time=engine_time)
self.current_node.set_eval(eval[0], eval[1])
opening = position_info.get("opening")
white_wins = position_info.get("white")
black_wins = position_info.get("black")
draws = position_info.get("draws")
total_occurrences = white_wins + black_wins + draws
white_percentage = round(white_wins / total_occurrences * 100, 2)
black_percentage = round(black_wins / total_occurrences * 100, 2)
draw_percentage = round(draws / total_occurrences * 100, 2)
if opening:
self.current_node.comment += f'[open: {opening["eco"]}, {opening["name"]}]'
self.current_node.comment += f'[freq: {total_occurrences}, {relative_freq}][wdb: {white_wins}, {draws}, {black_wins}][wdb%: {white_percentage}, {draw_percentage}, {black_percentage}]'
print(self.current_node)
for move in position_info.get("moves"):
move_occurrences = move["white"] + move["draws"] + move["black"]
if move_occurrences >= min_occurrences:
child_node = self.current_node.add_variation(chess.Move.from_uci(move["uci"]))
self.current_node = child_node
relative_frequency = round(move_occurrences / total_occurrences * 100, 2)
self.build_opening_tree(relative_freq=relative_frequency, min_occurrences=min_occurrences, engine_time=engine_time)
self.current_node = self.current_node.parent
self.memoization_dict[fen] = self.current_node
return self.current_node
def get_size(self, node):
def calculate_size(current_node, current_depth):
nonlocal size
# Update nodes amount
size["nodes_amount"] += 1
# Update breadth information
current_breadth = len(current_node.variations)
size["total_breadth"] += current_breadth
size["min_breadth"] = min(size["min_breadth"], current_breadth)
size["max_breadth"] = max(size["max_breadth"], current_breadth)
# Update depth information
size["total_depth"] += current_depth
size["min_depth"] = min(size["min_depth"], current_depth)
size["max_depth"] = max(size["max_depth"], current_depth)
# Recursively calculate size for child nodes
for variation in current_node.variations:
calculate_size(variation, current_depth + 1)
# Initialize size dictionary
size = {
"nodes_amount": 0,
"min_breadth": float('inf'),
"max_breadth": 0,
"total_breadth": 0,
"avg_breadth": 0,
"min_depth": float('inf'),
"max_depth": 0,
"total_depth": 0,
"avg_depth": 0
}
# Start calculating size from the root node
calculate_size(node, 0)
# Calculate average breadth and depth
size["avg_breadth"] = size["total_breadth"] / size["nodes_amount"] if size["nodes_amount"] > 0 else 0
size["avg_depth"] = size["total_depth"] / size["nodes_amount"] if size["nodes_amount"] > 0 else 0
return size
def load_opening_tree(self, filename):
with open(filename, 'r') as file:
self.game = chess.pgn.read_game(file)
self.current_node = self.game
def save_opening_tree(self, filename):
with open(filename, 'w') as file:
file.write(str(self.game))
def get_position_info(fen, url):
params = {"fen": fen}
response = requests.get(url, params=params)
if response.status_code == 200:
return response.json()
elif response.status_code == 429:
print("Opening explorer: Too many requests -> Waiting 1 minute")
time.sleep(60)
return get_position_info(fen, url)
else:
print(f"Error: {response.status_code}")
print(response.text)
return []
def get_stockfish_eval(board, engine_time=0.1):
stockfish_path = "stockfish.exe"
with chess.engine.SimpleEngine.popen_uci(stockfish_path) as engine:
result = engine.analyse(board, chess.engine.Limit(time=engine_time))
pov_score = result["score"]
depth = result["depth"]
return pov_score, depth