The library python chess can determine if a position has a legal ep move. I use this library to calculate if an ep move is better than the played move in the game using stockfish 16 engine at 1 sec/pos.
I took the rapid team champ games from the weekinchess and analyze each position.
Result
num_games: 1296, possible_ep: 146, played_ep: 82, better_epmove_than_played_move: 17 (11.64%)
You can run other pgn file with the following code.
Code
"""Get stats on unplayed but better ep move.
Read every position in the game from pgn file. Check
if position has a legal ep move, the played move in the game
and if ep move is better than the played move by analyzing
the position with stockfish engine.
requirements:
pip install chess
"""
import chess
import chess.pgn
import chess.engine
fn = 'wrapteam23.pgn'
gcnt = 0
engine = chess.engine.SimpleEngine.popen_uci(
r'F:\Chess\Engines\stockfish\sf16\sf16pop.exe'
)
engine.configure({'Hash': 256})
possible_ep, played_ep, better_epmove = 0, 0, 0
at = 1.0 # analysis time in seconds
epds = []
with open(fn) as pgn:
while True:
game = chess.pgn.read_game(pgn)
if game is None:
break
gcnt += 1
print(f'game {gcnt}')
for node in game.mainline():
board = node.parent.board()
gmove = node.move # the actual move played in the game
# Check if there is legal ep capture.
if not board.has_legal_en_passant():
continue
possible_ep += 1
ep_square = board.ep_square
# Get the best move and score of the engine from the current position.
info = engine.analyse(board, chess.engine.Limit(time=at))
bm = info['pv'][0]
bscore = info['score'].relative.score(mate_score=32000)
# Determine the ep move score.
if bm.to_square == ep_square:
epmove = bm
epscore = bscore
else:
for mv in board.legal_moves:
to_sq = mv.to_square
if to_sq == ep_square:
tboard = board.copy()
# Push this move and analyze the position with the engine.
# Negate the score because we push it.
tboard.push(mv)
info = engine.analyse(tboard, chess.engine.Limit(time=at))
epscore = -info['score'].relative.score(mate_score=32000)
epmove = mv
break
# Determine the game move score.
if bm == gmove:
gscore = bscore
elif gmove == epmove:
gscore = epscore
else:
tboard = board.copy()
tboard.push(gmove)
info = engine.analyse(tboard, chess.engine.Limit(time=at))
gscore = -info['score'].relative.score(mate_score=32000)
# Determine if epmove is played or epmove is better than gmove.
if gmove == epmove:
played_ep += 1
elif epscore > gscore:
better_epmove += 1
# Save the epd where the ep move is better but not played.
epdr = f'{board.epd()} c0 "engine_move: {bm}, engine_score: {bscore}, game_move: {gmove}, game_score: {gscore}, ep_move: {epmove}, ep_score: {epscore}";'
epds.append(epdr)
engine.quit()
print('Better ep move but not played:')
for e in epds:
print(e)
print(f'num_games: {gcnt}, possible_ep: {possible_ep}, played_ep: {played_ep}, better_epmove_than_played_move: {better_epmove} ({100*better_epmove / possible_ep:0.2f}%)')
Sample output
World Rapid Team Championship 2023 games.
Better ep move but not played by the player in the game.
rnbqk1nr/pp3p1p/3p2p1/2pPp3/2P1P3/2P5/P4PPP/R1BQKBNR w KQkq e6 c0 "engine_move: a2a4, engine_score: 53, game_move: g1e2, game_score: 7, ep_move: d5e6, ep_score: 28";
2r1r1k1/5pbp/pp1p1np1/n2Pp2q/3NP3/PP4PP/1B1Q1PB1/R4RK1 w - e6 c0 "engine_move: d5e6, engine_score: 243, game_move: g3g4, game_score: -132, ep_move: d5e6, ep_score: 243";
8/1p6/6kp/pPp5/P3P3/8/5KPP/8 w - c6 c0 "engine_move: b5c6, engine_score: 581, game_move: b5b6, game_score: 532, ep_move: b5c6, ep_score: 581";
r2r2k1/1ppqbppp/2n1p3/8/pPPPb3/P1Q1BN1P/4BPP1/R2R2K1 b - b3 c0 "engine_move: a4b3, engine_score: 44, game_move: e7f6, game_score: -98, ep_move: a4b3, ep_score: 44";
rn1qkb1r/6p1/p2pp3/4p1Pp/4P3/1PN1B3/1PP4P/R2QK1R1 w Qkq h6 c0 "engine_move: g5h6, engine_score: 333, game_move: d1f3, game_score: 296, ep_move: g5h6, ep_score: 333";
2rr2k1/1p1b1pbp/nqpp1np1/p2Pp3/2PNP3/PPN3PP/1B3PB1/1R1Q1RK1 w - e6 c0 "engine_move: d5e6, engine_score: 138, game_move: d4e2, game_score: -50, ep_move: d5e6, ep_score: 138";
r3k2r/pp3p2/3P4/2pPp3/3q2pp/1R4P1/P1bN1PP1/4QRK1 w kq e6 c0 "engine_move: d5e6, engine_score: 0, game_move: b3e3, game_score: -67, ep_move: d5e6, ep_score: 0";
8/1pp1r1p1/p3k2p/P3Pp2/1P1RK2P/1PP3P1/8/8 w - f6 c0 "engine_move: e5f6, engine_score: 165, game_move: e4f4, game_score: 9, ep_move: e5f6, ep_score: 165";
3r1nk1/p5p1/1p2p1p1/5pP1/2P2P1P/P1P1RK2/3r4/2Q5 w - f6 c0 "engine_move: g5f6, engine_score: 31, game_move: e3e2, game_score: -57, ep_move: g5f6, ep_score: 31";
2r2r2/1b2b1kp/1q2p1p1/p1npPp2/2pN1PP1/PpP1PR2/1P1BQ2P/RB4K1 w - f6 c0 "engine_move: d2e1, engine_score: -101, game_move: g4g5, game_score: -222, ep_move: e5f6, ep_score: -158";
2r1r2k/5p2/5Pp1/1p4Pp/4PQ1P/3R4/1q6/3R1K2 w - h6 c0 "engine_move: g5h6, engine_score: -178, game_move: d3d7, game_score: -247, ep_move: g5h6, ep_score: -178";
rnbqkb1r/pp3ppp/4pn2/2p5/2PpP3/3P1NP1/PP3PBP/RNBQK2R b KQkq e3 c0 "engine_move: d4e3, engine_score: -43, game_move: b8c6, game_score: -54, ep_move: d4e3, ep_score: -43";
r2qr1k1/1p3pbp/p2p1np1/2pPp3/2PnP3/2N1B1P1/PP1Q1PBP/R4RK1 w - c6 c0 "engine_move: d5c6, engine_score: 29, game_move: c3e2, game_score: -41, ep_move: d5c6, ep_score: 29";
r1b2rk1/pp2b1pp/2q1p3/3pPp2/2pP3P/2N2P2/1PPQN1P1/R3K2R w KQ f6 c0 "engine_move: h4h5, engine_score: -193, game_move: e1f2, game_score: -174, ep_move: e5f6, ep_score: -133";
r1bq1rk1/1pb3pp/p4n2/3nNp2/P1NPpP2/2P5/1P2B1PP/R1BQ1RK1 b - f3 c0 "engine_move: c8e6, engine_score: -128, game_move: c8d7, game_score: -224, ep_move: e4f3, ep_score: -158";
1r1r2k1/pq5p/6pQ/4Pp2/3P3R/8/1n4PP/2B2RK1 w - f6 c0 "engine_move: c1b2, engine_score: 433, game_move: c1b2, game_score: 433, ep_move: e5f6, ep_score: 558";
3r1bk1/5p2/6p1/1Q4Pp/4P1K1/4RNP1/8/3q4 w - h6 c0 "engine_move: g5h6, engine_score: -26, game_move: g4f4, game_score: -471, ep_move: g5h6, ep_score: -26";
Code 2
Revised code to calculate if ep move is top engine move, good move (score_rate >= 55%), bad move (score_rate <= 45) and equal move (score_rate > 45% and score_rate < 55%), regardless if this ep move is played or not in the actual game.
"""Get stats on unplayed but better ep move.
Read every positions in the game from pgn file. Check
if position has a legal ep move, the played move in the game
and if ep move is better than the played move by analyzing
the position with stockfish engine
requirements:
pip install chess
"""
import chess
import chess.pgn
import chess.engine
from chess.engine import Cp
fn = 'wrapteam23.pgn'
gcnt = 0
# Modify engine here.
engine = chess.engine.SimpleEngine.popen_uci(
r'F:\Chess\Engines\stockfish\sf16\sf16pop.exe'
)
# modify this if you use other stockfish version
WDL_MODEL = 'sf16'
engine.configure({'Hash': 256})
engine_id = engine.id['name']
possible_ep, played_ep, better_epmove = 0, 0, 0
top_engine_move, good_move, equal_move, bad_move = 0, 0, 0, 0
at = 0.5 # 1.0 # analysis time in seconds
epds = []
with open(fn) as pgn:
while True:
game = chess.pgn.read_game(pgn)
if game is None:
break
gcnt += 1
print(f'game {gcnt}')
for node in game.mainline():
board = node.parent.board()
gmove = node.move # the actual move played in the game
# Check if there is legal ep capture.
if not board.has_legal_en_passant():
continue
possible_ep += 1
ep_square = board.ep_square
# Get the best move and score of the engine from the current position.
info = engine.analyse(board, chess.engine.Limit(time=at))
bm = info['pv'][0]
bscore = info['score'].relative.score(mate_score=32000)
# Determine the ep move score.
if bm.to_square == ep_square:
epmove = bm
epscore = bscore
else:
for mv in board.legal_moves:
to_sq = mv.to_square
if to_sq == ep_square:
tboard = board.copy()
# Push this move and analyze the position with the engine.
# Negate the score because we push it.
tboard.push(mv)
info = engine.analyse(tboard, chess.engine.Limit(time=at))
epscore = -info['score'].relative.score(mate_score=32000)
epmove = mv
epexp = Cp(epscore).wdl(model=WDL_MODEL, ply=board.ply()).expectation()
break
# Determine the game move score.
if bm == gmove:
gscore = bscore
elif gmove == epmove:
gscore = epscore
else:
tboard = board.copy()
tboard.push(gmove)
info = engine.analyse(tboard, chess.engine.Limit(time=at))
gscore = -info['score'].relative.score(mate_score=32000)
# Determine if epmove is played or epmove is better than gmove.
if gmove == epmove:
played_ep += 1
elif epscore > gscore:
better_epmove += 1
# Save the epd where the ep move is better but not played.
epdr = f'{board.epd()} c0 "engine_move: {bm}, engine_score: {bscore}, game_move: {gmove}, game_score: {gscore}, ep_move: {epmove}, ep_score: {epscore}";'
epds.append(epdr)
# Determine if ep move is strong or weak or close to equal.
# Strong ep move:
# 1. Top engine move.
# 2. If not top, win percentage must be 55 or more percent.
# weak ep move:
# 1. win percentage is 45 and below percent
# close to equal ep move
# 1. win percentage is within [46 to 54] percent
if epmove == bm:
top_engine_move += 1
elif epexp >= 0.55:
good_move += 1
elif epexp <= 0.45:
bad_move += 1
else:
equal_move += 1
engine.quit()
# Summaries
print('Better ep move but not played:')
for e in epds:
print(e)
print()
print(f'analysis engine: {engine_id}')
print(f'position analysis time (sec): {at}')
print(f'pgn file: {fn}')
print()
print(f'num games: {gcnt}')
print(f'possible ep move: {possible_ep}')
print(f'played ep move: {played_ep} ({100*played_ep/possible_ep:0.2f}%)')
print(f'better ep move than played move: {better_epmove} ({100*better_epmove / possible_ep:0.2f}%)')
print()
print(f'top engine ep move: {top_engine_move} ({100*top_engine_move/possible_ep:0.2f}%)')
print(f'good ep move: {good_move} ({100*good_move/possible_ep:0.2f}%)')
print(f'equal ep move: {equal_move} ({100*equal_move/possible_ep:0.2f}%)')
print(f'bad ep move: {bad_move} ({100*bad_move/possible_ep:0.2f}%)')
Algorithm on good, bad, and equal ep move
if epmove == bm:
top_engine_move += 1
elif epexp >= 0.55:
good_move += 1
elif epexp <= 0.45:
bad_move += 1
else:
equal_move += 1
The epexp is the score probability (1, 0) of the ep move according to stockfish 16 at 0.5 second of analysis.
sample output
analysis engine: Stockfish 16
position analysis time (sec): 0.5
pgn file: wrapteam23.pgn
num games: 1296
possible ep move: 146
played ep move: 81 (55.48%)
better ep move than played move: 18 (12.33%)
top engine ep move: 79 (54.11%)
good ep move: 10 (6.85%)
equal ep move: 19 (13.01%)
bad ep move: 38 (26.03%)
Sample result from world cup 2023
analysis engine: Stockfish 16
position analysis time (sec): 0.5
pgn file: wcup23.pgn
num games: 677
possible ep move: 92
played ep move: 47 (51.09%)
better ep move than played move: 7 (7.61%)
top engine ep move: 39 (42.39%)
good ep move: 6 (6.52%)
equal ep move: 18 (19.57%)
bad ep move: 29 (31.52%)
Code 3
Add ep stats by eco, opening and variation.
"""Get stats on unplayed but better ep move.
Read every positions in the game from pgn file. Check
if position has a legal ep move, the played move in the game
and if ep move is better than the played move by analyzing
the position with stockfish engine
requirements:
pip install chess
"""
import chess
import chess.pgn
import chess.engine
from chess.engine import Cp
fn = 'wcup23.pgn' # 'wrapteam23.pgn'
gcnt = 0
# Modify engine here.
engine = chess.engine.SimpleEngine.popen_uci(
r'F:\Chess\Engines\stockfish\sf16\sf16pop.exe'
)
# modify this if you use other stockfish version
WDL_MODEL = 'sf16'
engine.configure({'Hash': 256})
engine_id = engine.id['name']
possible_ep, played_ep, better_epmove = 0, 0, 0
top_engine_move, good_move, equal_move, bad_move = 0, 0, 0, 0
at = 0.5 # 1.0 # analysis time in seconds
epds = []
eco_ep_data = {}
with open(fn) as pgn:
while True:
game = chess.pgn.read_game(pgn)
if game is None:
break
gcnt += 1
print(f'game {gcnt}')
eco = game.headers.get('ECO', None)
opening = game.headers.get('Opening', None)
variation = game.headers.get('Variation', None)
eco_cnt = 0
for node in game.mainline():
board = node.parent.board()
gmove = node.move # the actual move played in the game
# Check if there is legal ep capture.
if not board.has_legal_en_passant():
continue
possible_ep += 1
ep_square = board.ep_square
eco_cnt += 1
# Get the best move and score of the engine from the current position.
info = engine.analyse(board, chess.engine.Limit(time=at))
bm = info['pv'][0]
bscore = info['score'].relative.score(mate_score=32000)
# Determine the ep move score.
if bm.to_square == ep_square:
epmove = bm
epscore = bscore
else:
for mv in board.legal_moves:
to_sq = mv.to_square
if to_sq == ep_square:
tboard = board.copy()
# Push this move and analyze the position with the engine.
# Negate the score because we push it.
tboard.push(mv)
info = engine.analyse(tboard, chess.engine.Limit(time=at))
epscore = -info['score'].relative.score(mate_score=32000)
epmove = mv
epexp = Cp(epscore).wdl(model=WDL_MODEL, ply=board.ply()).expectation()
break
# Determine the game move score.
if bm == gmove:
gscore = bscore
elif gmove == epmove:
gscore = epscore
else:
tboard = board.copy()
tboard.push(gmove)
info = engine.analyse(tboard, chess.engine.Limit(time=at))
gscore = -info['score'].relative.score(mate_score=32000)
# Determine if epmove is played or epmove is better than gmove.
if gmove == epmove:
played_ep += 1
elif epscore > gscore:
better_epmove += 1
# Save the epd where the ep move is better but not played.
epdr = f'{board.epd()} c0 "engine_move: {bm}, engine_score: {bscore}, game_move: {gmove}, game_score: {gscore}, ep_move: {epmove}, ep_score: {epscore}";'
epds.append(epdr)
# Determine if ep move is strong or weak or close to equal.
# Strong ep move:
# 1. Top engine move.
# 2. If not top, win percentage must be 55 or more percent.
# weak ep move:
# 1. win percentage is 45 and below percent
# close to equal ep move
# 1. win percentage is within [46 to 54] percent
if epmove == bm:
top_engine_move += 1
elif epexp >= 0.55:
good_move += 1
elif epexp <= 0.45:
bad_move += 1
else:
equal_move += 1
# Save ep counts by ECO opening.
try:
eco_ep_data[(eco, f'opening: {opening}', f'variation: {variation}')] += eco_cnt
except KeyError:
eco_ep_data.update({(eco, f'opening: {opening}', f'variation: {variation}'): eco_cnt})
engine.quit()
# Summaries
print(f'analysis engine: {engine_id}')
print(f'position analysis time (sec): {at}')
print(f'pgn file: {fn}')
print()
print('Better ep move but not played:')
for e in epds:
print(e)
print()
print(f'num games: {gcnt}')
print(f'possible ep move: {possible_ep}')
print(f'played ep move: {played_ep} ({100*played_ep/possible_ep:0.2f}%)')
print(f'better ep move than played move: {better_epmove} ({100*better_epmove / possible_ep:0.2f}%)')
print()
print(f'top engine ep move: {top_engine_move} ({100*top_engine_move/possible_ep:0.2f}%)')
print(f'good ep move: {good_move} ({100*good_move/possible_ep:0.2f}%)')
print(f'equal ep move: {equal_move} ({100*equal_move/possible_ep:0.2f}%)')
print(f'bad ep move: {bad_move} ({100*bad_move/possible_ep:0.2f}%)')
print()
# ep move possibility by ECO.
eco_ep_data = dict(sorted(eco_ep_data.items()))
eco_ep_data = dict(sorted(eco_ep_data.items(), key=lambda item: item[1], reverse=True))
for k, v in eco_ep_data.items():
print(f'{k}: {v}')
Sample output
...
top engine ep move: 42 (45.65%)
good ep move: 8 (8.70%)
equal ep move: 16 (17.39%)
bad ep move: 26 (28.26%)
('C43', 'opening: Petrov', 'variation: modern attack, Symmetrical variation'): 4
('C53', 'opening: Giuoco Piano', 'variation: None'): 4
('A13', 'opening: English opening', 'variation: None'): 3
('A20', 'opening: English opening', 'variation: None'): 3
('B12', 'opening: Caro-Kann', 'variation: advance variation'): 3
('B40', 'opening: Sicilian', 'variation: Anderssen variation'): 3
('B45', 'opening: Sicilian', 'variation: Taimanov variation'): 3
('A22', 'opening: English', 'variation: Bremen, Smyslov system'): 2
('A22', 'opening: English opening', 'variation: None'): 2
('B33', 'opening: Sicilian', 'variation: Pelikan (Lasker/Sveshnikov) variation'): 2
('B50', 'opening: Sicilian', 'variation: None'): 2
('B51', 'opening: Sicilian', 'variation: Canal-Sokolsky (Nimzovich-Rossolimo, Moscow) attack'): 2
('C02', 'opening: French', 'variation: advance, Paulsen attack'): 2
('C44', 'opening: Scotch gambit', 'variation: Dubois-Reti defence'): 2
('C67', 'opening: Ruy Lopez', 'variation: Berlin defence, open variation'): 2
('D35', 'opening: QGD', 'variation: exchange, positional line'): 2
('D37', 'opening: QGD', 'variation: 4.Nf3'): 2
('D43', 'opening: QGD semi-Slav', 'variation: None'): 2
('D80', 'opening: Gruenfeld', 'variation: Stockholm variation'): 2
('A07', 'opening: Reti', "variation: King's Indian attack"): 1
('A08', 'opening: Reti', "variation: King's Indian attack, French variation"): 1
('A09', 'opening: Reti opening', 'variation: None'): 1
('A13', 'opening: English', 'variation: Neo-Catalan'): 1
('A13', 'opening: English', 'variation: Neo-Catalan accepted'): 1
('A13', 'opening: English opening', 'variation: Agincourt variation'): 1
('A28', 'opening: English', 'variation: four knights, Stean variation'): 1
('A29', 'opening: English', 'variation: four knights, kingside fianchetto'): 1
('A40', "opening: Queen's pawn", 'variation: English defence'): 1
('A42', 'opening: Modern defence', 'variation: Averbakh system'): 1
('A45', 'opening: Trompovsky attack (Ruth, Opovcensky opening)', 'variation: None'): 1
('A46', "opening: Queen's pawn game", 'variation: None'): 1
('A56', 'opening: Benoni defence', 'variation: None'): 1
('A62', 'opening: Benoni', 'variation: fianchetto variation'): 1
('A81', 'opening: Dutch defence', 'variation: None'): 1
('B11', 'opening: Caro-Kann', 'variation: two knights, 3...Bg4'): 1
('B23', 'opening: Sicilian', 'variation: closed'): 1
('B30', 'opening: Sicilian', 'variation: Nimzovich-Rossolimo attack (without ...d6)'): 1
('B31', 'opening: Sicilian', 'variation: Nimzovich-Rossolimo attack (with ...g6, without ...d6)'): 1
('B33', 'opening: Sicilian defence', 'variation: None'): 1
('B39', 'opening: Sicilian', 'variation: accelerated fianchetto, Breyer variation'): 1
('B52', 'opening: Sicilian', 'variation: Canal-Sokolsky attack, 3...Bd7'): 1
('B67', 'opening: Sicilian', 'variation: Richter-Rauzer, Rauzer attack, 7...a6 defence, 8...Bd7'): 1
('B90', 'opening: Sicilian', 'variation: Najdorf'): 1
('B90', 'opening: Sicilian', 'variation: Najdorf, Byrne (English) attack'): 1
('C07', 'opening: French', 'variation: Tarrasch, open variation'): 1
('C22', 'opening: Centre game', 'variation: Berger variation'): 1
('C42', 'opening: Petrov', 'variation: classical attack'): 1
('C44', 'opening: Scotch gambit', 'variation: None'): 1
('C46', 'opening: Four knights game', 'variation: None'): 1
('C50', 'opening: Giuoco Pianissimo', 'variation: None'): 1
('C65', 'opening: Ruy Lopez', 'variation: Berlin defence, Mortimer variation'): 1
('C72', 'opening: Ruy Lopez', 'variation: modern Steinitz defence, 5.O-O'): 1
('C80', 'opening: Ruy Lopez', 'variation: open, 8...Be6'): 1
('C80', 'opening: Ruy Lopez', 'variation: open, Bernstein variation'): 1
('C85', 'opening: Ruy Lopez', 'variation: Exchange variation doubly deferred (DERLD)'): 1
('D02', "opening: Queen's pawn game", 'variation: None'): 1
('D10', 'opening: QGD Slav defence', 'variation: exchange variation'): 1
('D31', 'opening: QGD', 'variation: semi-Slav, Marshall gambit'): 1
('D35', 'opening: QGD', 'variation: exchange variation'): 1
('D47', 'opening: QGD semi-Slav', 'variation: Meran variation'): 1
('D78', 'opening: Neo-Gruenfeld, 6.O-O c6', 'variation: None'): 1
('E04', 'opening: Catalan', 'variation: open, 5.Nf3'): 1
('E10', "opening: Queen's pawn game", 'variation: None'): 1
('E29', 'opening: Nimzo-Indian', 'variation: Saemisch, main line'): 1
('A01', 'opening: Nimzovich-Larsen attack', 'variation: classical variation'): 0
('A01', 'opening: Nimzovich-Larsen attack', 'variation: modern variation'): 0
('A04', 'opening: Reti opening', 'variation: None'): 0
('A05', 'opening: Reti', "variation: King's Indian attack"): 0
('A05', 'opening: Reti', "variation: King's Indian attack, Spassky's variation"): 0
...