33from io import StringIO
44
55import chess .pgn
6+ from stockfish import Stockfish
67
78PIECE_VALUES = {
89 chess .PAWN : 1 ,
2728def opposite_colour (colour : str ):
2829 return "black" if colour == "white" else "white"
2930
31+
32+ stockfish_path = '/home/alexli/fun/stockfish/stockfish-ubuntu-x86-64-avx2'
33+ fish_params = {"Threads" : 12 , "Hash" : 8192 * 2 , "Slow Mover" : 0 }
34+ engine = Stockfish (path = stockfish_path , parameters = fish_params )
35+ engine .set_depth (25 )
36+
3037# takes in a parsed PGN, estimates game craziness
3138# and returns a score
3239def estimate_game_craziness (game : chess .pgn .Game ):
33- score = 0
34-
3540 game_moves = list (game .mainline ())
36-
41+ if game .headers ['Result' ] != '1/2-1/2' :
42+ if len (game_moves ) <= 50 :
43+ engine .set_fen_position (game_moves [40 ].board ().fen ())
44+ evaluation = engine .get_evaluation ()
45+ if evaluation ['type' ] != 'cp' :
46+ return 0 , None # It's a mate-in-N
47+ elif evaluation ['value' ] > 0.5 and game .headers ['Result' ] == '1-0' :
48+ return 0 , None # Black lost and black's position is worse
49+ elif evaluation ['value' ] < - 0.5 and game .headers ['Result' ] == '0-1' :
50+ return 0 , None # White lost and white's position is worse
51+ offset = 20
3752 pieces_moved : list [chess .PieceType ] = []
38- material_differences = []
39-
40- for node_index , move_node in enumerate (game_moves ):
53+ move_scores = [0 ] * offset
54+ total_material = [78 ] * offset
55+ for node_index , move_node in list (enumerate (game_moves ))[offset :]:
56+ score = 0
57+ if node_index >= 40 or total_material [- 1 ] < 20 :
58+ break
59+
4160 move = move_node .move
4261 board = move_node .board ()
4362
44- turn_colour = "black " if board .turn else "white "
63+ turn_colour = "white " if board .turn else "black "
4564
4665 try :
4766 pieces_moved .append (
@@ -56,10 +75,6 @@ def estimate_game_craziness(game: chess.pgn.Game):
5675 "black" : 0
5776 }
5877
59- material_differences .append (
60- abs (material ["white" ] - material ["black" ])
61- )
62-
6378 piece_counts = {
6479 "white" : {},
6580 "black" : {}
@@ -90,21 +105,11 @@ def estimate_game_craziness(game: chess.pgn.Game):
90105 king_square [piece_colour ] = square
91106
92107 pieces_remaining += 1
93-
94- # if material difference has been high for too long, discard game
95- if len (material_differences ) >= 14 :
96- balanced_position_found = False
97-
98- for i in range (14 ):
99- current_difference = material_differences [- (i + 1 )]
100-
101- if current_difference <= 11 :
102- balanced_position_found = True
103- break
104-
105- if not balanced_position_found :
106- return - 1
107-
108+ total_material .append (material ['white' ] + material ['black' ])
109+ imbalance = 0
110+ for piece_type in chess .PIECE_TYPES :
111+ imbalance += abs (piece_counts ["white" ][piece_type ] - piece_counts ["black" ][piece_type ]) * PIECE_VALUES [piece_type ] ** .5
112+ score += imbalance
108113 # number of simultaneously hanging pieces
109114 for square in chess .SQUARES :
110115 piece = board .piece_at (square )
@@ -123,58 +128,29 @@ def estimate_game_craziness(game: chess.pgn.Game):
123128 # there's no sacrifice
124129 last_position = game_moves [node_index - 1 ].board ()
125130 last_piece = last_position .piece_at (square )
131+ piece_has_been_sitting_here = last_piece is not None and last_piece .color == piece .color
126132
127- if (
128- last_piece is not None
129- and PIECE_VALUES [last_piece .piece_type ] >= PIECE_VALUES [piece .piece_type ]
130- ):
133+ if not piece_has_been_sitting_here :
131134 continue
132135
133136 # Get the attackers of the current square
134137 attacker_squares = board .attackers (not piece .color , square )
138+ attacker_values = sorted ([PIECE_VALUES [board .piece_at (attacker ).piece_type ] for attacker in attacker_squares ])
135139
136140 # Get defenders of the current square
137141 defender_squares = board .attackers (piece .color , square )
142+ defender_values = [PIECE_VALUES [piece .piece_type ]] + sorted ([PIECE_VALUES [board .piece_at (defender ).piece_type ] for defender in defender_squares ])
138143
139- if len (attacker_squares ) > len (defender_squares ):
140- score += PIECE_VALUES [piece .piece_type ]
141- else :
142- # Count attackers that are of less value than the piece
143- for attacker_square in attacker_squares :
144- attacker = board .piece_at (attacker_square )
145-
146- if PIECE_VALUES [attacker .piece_type ] < PIECE_VALUES [piece .piece_type ]:
147- score += PIECE_VALUES [piece .piece_type ]
148- break
149-
150- # discard threefold repetitions
151- if board .can_claim_threefold_repetition ():
152- return - 1
153-
154- # reward castling or king mates
155- if (
156- board .is_checkmate ()
157- and (
158- "O-" in move_node .san ()
159- or "K" in move_node .san ()
160- )
161- ):
162- score += 20
163-
164- # number of pieces on the board ABOVE that which is typical
165- # weighted towards rarity of this happening
166- for colour in piece_counts .keys ():
167- for piece_type , count in piece_counts [colour ].items ():
168- if count > TYPICAL_PIECE_COUNTS [piece_type ]:
169- extra_count = count - TYPICAL_PIECE_COUNTS [piece_type ]
170-
171- if piece_type == chess .QUEEN :
172- if extra_count == 1 :
173- score += 0.5
174- else :
175- score += 0.5 + (5 * (extra_count - 1 ))
176- else :
177- score += 4 * extra_count
144+ for i in range (len (attacker_values )):
145+ if i == len (defender_values ):
146+ # There is nothing to take
147+ break
148+ if i + 1 == len (defender_values ):
149+ # There is nothing defending this piece, we can simply take.
150+ score += defender_values [i ] ** .5
151+ elif attacker_values [i ] < defender_values [i ] and i <= len (defender_values ):
152+ # Can take this piece and, when the next defender takes back, we have gained material
153+ score += (defender_values [i ] - attacker_values [i ]) ** .5
178154
179155 # underpromotions, weighted towards their rarity
180156 if move .promotion == chess .QUEEN :
@@ -191,42 +167,35 @@ def estimate_game_craziness(game: chess.pgn.Game):
191167 if (
192168 "O-" not in move_node .san ()
193169 and move .to_square in CORNER_SQUARES
170+ and board .piece_at (move .to_square ).piece_type == chess .KNIGHT
194171 ):
195- moved_piece_type = board .piece_at (move .to_square ).piece_type
196-
197- if moved_piece_type in [chess .QUEEN , chess .BISHOP ]:
198- score += 2
199- elif moved_piece_type == chess .KNIGHT :
200- score += 3
172+ score += 0.5
201173
202174 # is king in the centre of the board when there are lots of pieces left
203175 if (
204- king_square [turn_colour ] > 23
205- and king_square [turn_colour ] < 40
206- and node_index <= 30
207- and piece_counts [opposite_colour (turn_colour )][chess .QUEEN ] > 0
208- ):
209- score += 2.5
210-
211- # consecutive moves of the king
212- if (
213- node_index <= 30
214- and pieces_remaining >= 20
215- and pieces_moved [- 1 ] == chess .KING
176+ 23 < king_square [turn_colour ] < 40
177+ and material [opposite_colour (turn_colour )] > 30
216178 ):
217- pieces_moved_index = 1
179+ score + = 1.5
218180
219- while pieces_moved_index <= len (pieces_moved ):
220- last_moved_piece = pieces_moved [- pieces_moved_index ]
221-
222- if last_moved_piece == chess .KING :
223- score += 1.5 * (1.075 ** ((pieces_moved_index - 1 ) / 2 ))
224- else :
225- break
226-
227- pieces_moved_index += 2
228-
229- return round (score , 2 )
181+ # discard threefold repetitions
182+ if board .can_claim_threefold_repetition ():
183+ score -= 30
184+
185+ move_scores .append (score )
186+ best_node_value = 0
187+ best_node = game_moves [0 ]
188+ ws = [1.3 ,1.5 ,1 ,1 ,.5 ,.5 ,.2 ,.2 ,.2 ,.2 ]
189+
190+ for i in range (offset , len (move_scores ) - len (ws )):
191+ if game_moves [i ].board ().turn : # white to move only
192+ cur_node_value = total_material [min (len (total_material ) - 1 , i + 4 )] ** .3
193+ for j , w in enumerate (ws ):
194+ cur_node_value += w * move_scores [i + j ]
195+ if cur_node_value > best_node_value :
196+ best_node_value = cur_node_value
197+ best_node = game_moves [i ]
198+ return best_node_value , best_node .board ()
230199
231200
232201# takes in a PGN string and returns the estimated
0 commit comments