From 636e95fbf292f322fc4ab31b8c4add51f7534362 Mon Sep 17 00:00:00 2001 From: Niklas Fiekas Date: Sun, 23 Feb 2025 21:05:39 +0100 Subject: [PATCH 01/27] Revert simplifications of chess.gaviota from #1130 The resulting code is not equivalent, that is, when the full tablebase is available ./test.py GaviotaTestCase fails with ERROR: test_dm_4 (__main__.GaviotaTestCase.test_dm_4) ---------------------------------------------------------------------- Traceback (most recent call last): File "/home/niklas/Projekte/python-chess/./test.py", line 35, in _wrapper return f(self) File "/home/niklas/Projekte/python-chess/./test.py", line 4305, in test_dm_4 dtm = self.tablebase.probe_dtm(board) File "/home/niklas/Projekte/python-chess/chess/gaviota.py", line 1393, in probe_dtm dtm = self.egtb_get_dtm(req) File "/home/niklas/Projekte/python-chess/chess/gaviota.py", line 1518, in egtb_get_dtm dtm = self._tb_probe(req) File "/home/niklas/Projekte/python-chess/chess/gaviota.py", line 1621, in _tb_probe buffer_zipped = stream.read(z) ValueError: read length must be non-negative or -1 --- chess/gaviota.py | 553 ++++++++++++++++++++++++++++++++--------------- 1 file changed, 374 insertions(+), 179 deletions(-) diff --git a/chess/gaviota.py b/chess/gaviota.py index 0bbe41b81..39173b593 100644 --- a/chess/gaviota.py +++ b/chess/gaviota.py @@ -4,7 +4,6 @@ import ctypes.util import dataclasses import fnmatch -import itertools import logging import lzma import os @@ -12,11 +11,11 @@ import struct import typing +import chess + from types import TracebackType from typing import BinaryIO, Callable, Dict, List, Optional, Tuple, Type, Union -import chess - LOGGER = logging.getLogger(__name__) @@ -110,19 +109,30 @@ def idx_is_empty(x: int) -> int: def flip_type(x: chess.Square, y: chess.Square) -> int: ret = 0 - file_x, rank_x = chess.square_file(x), chess.square_rank(x) - file_y, rank_y = chess.square_file(y), chess.square_rank(y) - if file_x > 3: - x, y = flip_we(x), flip_we(y) + if chess.square_file(x) > 3: + x = flip_we(x) + y = flip_we(y) ret |= 1 - if rank_x > 3: - x, y = flip_ns(x), flip_ns(y) + if chess.square_rank(x) > 3: + x = flip_ns(x) + y = flip_ns(y) ret |= 2 - if (rank_x, file_x) > (rank_y, file_y): - x, y = flip_nw_se(x), flip_nw_se(y) + rowx = chess.square_rank(x) + colx = chess.square_file(x) + + if rowx > colx: + x = flip_nw_se(x) + y = flip_nw_se(y) + ret |= 4 + + rowy = chess.square_rank(y) + coly = chess.square_file(y) + if rowx == colx and rowy > coly: + x = flip_nw_se(x) + y = flip_nw_se(y) ret |= 4 return ret @@ -135,6 +145,7 @@ def init_flipt() -> List[List[int]]: def init_pp48_idx() -> Tuple[List[List[int]], List[int], List[int]]: MAX_I = MAX_J = 48 + idx = 0 pp48_idx = [[-1] * MAX_J for _ in range(MAX_I)] pp48_sq_x = [NOSQUARE] * MAX_PP48_INDEX pp48_sq_y = [NOSQUARE] * MAX_PP48_INDEX @@ -146,8 +157,10 @@ def init_pp48_idx() -> Tuple[List[List[int]], List[int], List[int]]: j = flip_we(flip_ns(b)) - 8 if idx_is_empty(pp48_idx[i][j]): - pp48_idx[i][j] = pp48_idx[j][i] = idx - pp48_sq_x[idx], pp48_sq_y[idx] = i, j + pp48_idx[i][j] = idx + pp48_idx[j][i] = idx + pp48_sq_x[idx] = i + pp48_sq_y[idx] = j idx += 1 return pp48_idx, pp48_sq_x, pp48_sq_y @@ -166,15 +179,26 @@ def init_ppp48_idx() -> Tuple[List[List[List[int]]], List[int], List[int], List[ for x in range(48): for y in range(x + 1, 48): for z in range(y + 1, 48): - if not (in_queenside(ITOSQ[y]) and in_queenside(ITOSQ[z])): + a = ITOSQ[x] + b = ITOSQ[y] + c = ITOSQ[z] + if not in_queenside(b) or not in_queenside(c): continue - i, j, k = ITOSQ[x] - 8, ITOSQ[y] - 8, ITOSQ[z] - 8 + i = a - 8 + j = b - 8 + k = c - 8 if idx_is_empty(ppp48_idx[i][j][k]): - for ii, jj, kk in itertools.permutations((i, j, k)): - ppp48_idx[ii][jj][kk] = idx - ppp48_sq_x[idx], ppp48_sq_y[idx], ppp48_sq_z[idx] = i, j, k + ppp48_idx[i][j][k] = idx + ppp48_idx[i][k][j] = idx + ppp48_idx[j][i][k] = idx + ppp48_idx[j][k][i] = idx + ppp48_idx[k][i][j] = idx + ppp48_idx[k][j][i] = idx + ppp48_sq_x[idx] = i + ppp48_sq_y[idx] = j + ppp48_sq_z[idx] = k idx += 1 return ppp48_idx, ppp48_sq_x, ppp48_sq_y, ppp48_sq_z @@ -192,7 +216,8 @@ def init_aaidx() -> Tuple[List[int], List[List[int]]]: if idx_is_empty(aaidx[x][y]): # Still empty. - aaidx[x][y] = aaidx[y][x] = idx + aaidx[x][y] = idx + aaidx[y][x] = idx aabase[idx] = x idx += 1 @@ -204,10 +229,24 @@ def init_aaidx() -> Tuple[List[int], List[List[int]]]: def init_aaa() -> Tuple[List[int], List[List[int]]]: # Get aaa_base. comb = [a * (a - 1) // 2 for a in range(64)] - aaa_base = list(itertools.accumulate(comb)) + + accum = 0 + aaa_base = [0] * 64 + for a in range(64 - 1): + accum += comb[a] + aaa_base[a + 1] = accum # Get aaa_xyz. - aaa_xyz = [[x, y, z] for z in range(64) for y in range(z) for x in range(y)] + aaa_xyz = [[-1] * 3 for _ in range(MAX_AAAINDEX)] + + idx = 0 + for z in range(64): + for y in range(z): + for x in range(y): + aaa_xyz[idx][0] = x + aaa_xyz[idx][1] = y + aaa_xyz[idx][2] = z + idx += 1 return aaa_base, aaa_xyz @@ -312,33 +351,34 @@ def init_ppidx() -> Tuple[List[List[int]], List[int], List[int]]: def norm_kkindex(x: chess.Square, y: chess.Square) -> Tuple[int, int]: - file_x, rank_x = chess.square_file(x), chess.square_rank(x) - - if file_x > 3: - x, y = flip_we(x), flip_we(y) + if chess.square_file(x) > 3: + x = flip_we(x) + y = flip_we(y) - if rank_x > 3: - x, y = flip_ns(x), flip_ns(y) + if chess.square_rank(x) > 3: + x = flip_ns(x) + y = flip_ns(y) rowx = chess.square_rank(x) colx = chess.square_file(x) if rowx > colx: - x, y = flip_nw_se(x), flip_nw_se(y) + x = flip_nw_se(x) + y = flip_nw_se(y) rowy = chess.square_rank(y) coly = chess.square_file(y) if rowx == colx and rowy > coly: - x, y = flip_nw_se(x), flip_nw_se(y) + x = flip_nw_se(x) + y = flip_nw_se(y) return x, y - def init_kkidx() -> Tuple[List[List[int]], List[int], List[int]]: kkidx = [[-1] * 64 for _ in range(64)] - bksq, wksq = [-1] * MAX_KKINDEX, [-1] * MAX_KKINDEX - + bksq = [-1] * MAX_KKINDEX + wksq = [-1] * MAX_KKINDEX idx = 0 for x in range(64): for y in range(64): @@ -348,8 +388,10 @@ def init_kkidx() -> Tuple[List[List[int]], List[int], List[int]]: i, j = norm_kkindex(x, y) if idx_is_empty(kkidx[i][j]): - kkidx[i][j] = kkidx[x][y] = idx - bksq[idx], wksq[idx] = i, j + kkidx[i][j] = idx + kkidx[x][y] = idx + bksq[idx] = i + wksq[idx] = j idx += 1 return kkidx, wksq, bksq @@ -362,12 +404,20 @@ def kxk_pctoindex(c: Request) -> int: ft = flip_type(c.black_piece_squares[0], c.white_piece_squares[0]) - ws, bs = c.white_piece_squares, c.black_piece_squares + ws = c.white_piece_squares + bs = c.black_piece_squares + + if (ft & 1) != 0: + ws = [flip_we(b) for b in ws] + bs = [flip_we(b) for b in bs] + + if (ft & 2) != 0: + ws = [flip_ns(b) for b in ws] + bs = [flip_ns(b) for b in bs] - for f, flip in [(1, flip_we), (2, flip_ns), (4, flip_nw_se)]: - if ft & f: - ws = [flip(b) for b in ws] - bs = [flip(b) for b in bs] + if (ft & 4) != 0: + ws = [flip_nw_se(b) for b in ws] + bs = [flip_nw_se(b) for b in bs] ki = KKIDX[bs[0]][ws[0]] # KKIDX[black king][white king] @@ -376,14 +426,6 @@ def kxk_pctoindex(c: Request) -> int: return ki * BLOCK_Ax + ws[1] - -def flip_we_col(*pieces: int) -> tuple[int, ...]: - if (pieces[0] & 7) > 3: - # Column is more than 3, i.e., e, f, g or h. - pieces = tuple(flip_we(piece) for piece in pieces) - return pieces - - def kapkb_pctoindex(c: Request) -> int: BLOCK_A = 64 * 64 * 64 * 64 BLOCK_B = 64 * 64 * 64 @@ -396,17 +438,24 @@ def kapkb_pctoindex(c: Request) -> int: bk = c.black_piece_squares[0] ba = c.black_piece_squares[1] - if not chess.A2 <= pawn < chess.A8: + if not (chess.A2 <= pawn < chess.A8): return NOINDEX - pawn, wk, bk, wa, ba = flip_we_col(pawn, wk, bk, wa, ba) + if (pawn & 7) > 3: + # Column is more than 3, i.e., e, f, g or h. + pawn = flip_we(pawn) + wk = flip_we(wk) + bk = flip_we(bk) + wa = flip_we(wa) + ba = flip_we(ba) - # flip_ns, down one row - pslice = ((pawn ^ 56) - 8 + (pawn & 3)) >> 1 + sq = pawn + sq ^= 56 # flip_ns + sq -= 8 # Down one row + pslice = (sq + (sq & 3)) >> 1 return pslice * BLOCK_A + wk * BLOCK_B + bk * BLOCK_C + wa * BLOCK_D + ba - def kabpk_pctoindex(c: Request) -> int: BLOCK_A = 64 * 64 * 64 * 64 BLOCK_B = 64 * 64 * 64 @@ -419,13 +468,18 @@ def kabpk_pctoindex(c: Request) -> int: pawn = c.white_piece_squares[3] bk = c.black_piece_squares[0] - pawn, wk, bk, wa, wb = flip_we_col(pawn, wk, bk, wa, wb) + if (pawn & 7) > 3: + # Column is more than 3, i.e., e, f, g or h. + pawn = flip_we(pawn) + wk = flip_we(wk) + bk = flip_we(bk) + wa = flip_we(wa) + wb = flip_we(wb) pslice = wsq_to_pidx24(pawn) return pslice * BLOCK_A + wk * BLOCK_B + bk * BLOCK_C + wa * BLOCK_D + wb - def kabkp_pctoindex(c: Request) -> int: BLOCK_A = 64 * 64 * 64 * 64 BLOCK_B = 64 * 64 * 64 @@ -438,17 +492,23 @@ def kabkp_pctoindex(c: Request) -> int: bk = c.black_piece_squares[0] wb = c.white_piece_squares[2] - if not chess.A2 <= pawn < chess.A8: + if not (chess.A2 <= pawn < chess.A8): return NOINDEX - pawn, wk, bk, wa, wb = flip_we_col(pawn, wk, bk, wa, wb) + if (pawn & 7) > 3: + # Column is more than 3, i.e., e, f, g or h. + pawn = flip_we(pawn) + wk = flip_we(wk) + bk = flip_we(bk) + wa = flip_we(wa) + wb = flip_we(wb) - # Down one row - pslice = ((pawn - 8) + (pawn & 3)) >> 1 + sq = pawn + sq -= 8 # Down one row + pslice = (sq + (sq & 3)) >> 1 return pslice * BLOCK_A + wk * BLOCK_B + bk * BLOCK_C + wa * BLOCK_D + wb - def kaapk_pctoindex(c: Request) -> int: BLOCK_C = MAX_AAINDEX BLOCK_B = 64 * BLOCK_C @@ -460,7 +520,13 @@ def kaapk_pctoindex(c: Request) -> int: pawn = c.white_piece_squares[3] bk = c.black_piece_squares[0] - pawn, wk, bk, wa, wa2 = flip_we_col(pawn, wk, bk, wa, wa2) + if (pawn & 7) > 3: + # Column is more than 3, i.e., e, f, g or h. + pawn = flip_we(pawn) + wk = flip_we(wk) + bk = flip_we(bk) + wa = flip_we(wa) + wa2 = flip_we(wa2) pslice = wsq_to_pidx24(pawn) @@ -471,7 +537,6 @@ def kaapk_pctoindex(c: Request) -> int: return pslice * BLOCK_A + wk * BLOCK_B + bk * BLOCK_C + aa_combo - def kaakp_pctoindex(c: Request) -> int: BLOCK_C = MAX_AAINDEX BLOCK_B = 64 * BLOCK_C @@ -483,7 +548,13 @@ def kaakp_pctoindex(c: Request) -> int: bk = c.black_piece_squares[0] pawn = c.black_piece_squares[1] - pawn, wk, bk, wa, wa2 = flip_we_col(pawn, wk, bk, wa, wa2) + if (pawn & 7) > 3: + # Column is more than 3, i.e., e, f, g or h. + pawn = flip_we(pawn) + wk = flip_we(wk) + bk = flip_we(bk) + wa = flip_we(wa) + wa2 = flip_we(wa2) pawn = flip_ns(pawn) pslice = wsq_to_pidx24(pawn) @@ -495,7 +566,6 @@ def kaakp_pctoindex(c: Request) -> int: return pslice * BLOCK_A + wk * BLOCK_B + bk * BLOCK_C + aa_combo - def kapkp_pctoindex(c: Request) -> int: BLOCK_A = 64 * 64 * 64 BLOCK_B = 64 * 64 @@ -503,20 +573,30 @@ def kapkp_pctoindex(c: Request) -> int: wk = c.white_piece_squares[0] wa = c.white_piece_squares[1] - anchor = c.white_piece_squares[2] + pawn_a = c.white_piece_squares[2] bk = c.black_piece_squares[0] - loosen = c.black_piece_squares[1] + pawn_b = c.black_piece_squares[1] + + anchor = pawn_a + loosen = pawn_b - anchor, loosen, wk, bk, wa = flip_we_col(anchor, loosen, wk, bk, wa) + if (anchor & 7) > 3: + # Column is more than 3, i.e., e, f, g or h. + anchor = flip_we(anchor) + loosen = flip_we(loosen) + wk = flip_we(wk) + bk = flip_we(bk) + wa = flip_we(wa) - pp_slice = wsq_to_pidx24(anchor) * 48 + loosen - 8 + m = wsq_to_pidx24(anchor) + n = loosen - 8 + pp_slice = m * 48 + n if idx_is_empty(pp_slice): return NOINDEX return pp_slice * BLOCK_A + wk * BLOCK_B + bk * BLOCK_C + wa - def kappk_pctoindex(c: Request) -> int: BLOCK_A = 64 * 64 * 64 BLOCK_B = 64 * 64 @@ -530,7 +610,13 @@ def kappk_pctoindex(c: Request) -> int: anchor, loosen = pp_putanchorfirst(pawn_a, pawn_b) - anchor, loosen, wk, bk, wa = flip_we_col(anchor, loosen, wk, bk, wa) + if (anchor & 7) > 3: + # Column is more than 3, i.e., e, f, g or h. + anchor = flip_we(anchor) + loosen = flip_we(loosen) + wk = flip_we(wk) + bk = flip_we(bk) + wa = flip_we(wa) i = wsq_to_pidx24(anchor) j = wsq_to_pidx48(loosen) @@ -542,7 +628,6 @@ def kappk_pctoindex(c: Request) -> int: return pp_slice * BLOCK_A + wk * BLOCK_B + bk * BLOCK_C + wa - def kppka_pctoindex(c: Request) -> int: BLOCK_A = 64 * 64 * 64 BLOCK_B = 64 * 64 @@ -556,7 +641,12 @@ def kppka_pctoindex(c: Request) -> int: anchor, loosen = pp_putanchorfirst(pawn_a, pawn_b) - anchor, loosen, wk, bk, ba = flip_we_col(anchor, loosen, wk, bk, ba) + if (anchor & 7) > 3: + anchor = flip_we(anchor) + loosen = flip_we(loosen) + wk = flip_we(wk) + bk = flip_we(bk) + ba = flip_we(ba) i = wsq_to_pidx24(anchor) j = wsq_to_pidx48(loosen) @@ -568,7 +658,6 @@ def kppka_pctoindex(c: Request) -> int: return pp_slice * BLOCK_A + wk * BLOCK_B + bk * BLOCK_C + ba - def kabck_pctoindex(c: Request) -> int: N_WHITE = 4 N_BLACK = 1 @@ -600,7 +689,6 @@ def kabck_pctoindex(c: Request) -> int: return ki * BLOCK_A + ws[1] * BLOCK_B + ws[2] * BLOCK_C + ws[3] - def kabbk_pctoindex(c: Request) -> int: N_WHITE = 4 N_BLACK = 1 @@ -632,7 +720,6 @@ def kabbk_pctoindex(c: Request) -> int: return ki * BLOCK_Ax + ai * BLOCK_Bx + ws[1] - def kaabk_pctoindex(c: Request) -> int: N_WHITE = 4 N_BLACK = 1 @@ -664,23 +751,45 @@ def kaabk_pctoindex(c: Request) -> int: return ki * BLOCK_Ax + ai * BLOCK_Bx + ws[3] - def aaa_getsubi(x: int, y: int, z: int) -> int: - return x + (y - 1) * y // 2 + AAA_BASE[z] - + bse = AAA_BASE[z] + calc_idx = x + (y - 1) * y // 2 + bse + return calc_idx def kaaak_pctoindex(c: Request) -> int: + N_WHITE = 4 + N_BLACK = 1 BLOCK_Ax = MAX_AAAINDEX - ws = c.white_piece_squares[:4] - bs = c.black_piece_squares[:1] + + ws = c.white_piece_squares[:N_WHITE] + bs = c.black_piece_squares[:N_BLACK] + ft = FLIPT[c.black_piece_squares[0]][c.white_piece_squares[0]] - for flag, flip in [(WE_FLAG, flip_we), (NS_FLAG, flip_ns), (NW_SE_FLAG, flip_nw_se)]: - if ft & flag: - ws = [flip(i) for i in ws] - bs = [flip(i) for i in bs] + if (ft & WE_FLAG) != 0: + ws = [flip_we(i) for i in ws] + bs = [flip_we(i) for i in bs] + + if (ft & NS_FLAG) != 0: + ws = [flip_ns(i) for i in ws] + bs = [flip_ns(i) for i in bs] - ws[1:4] = sorted(ws[1:4]) + if (ft & NW_SE_FLAG) != 0: + ws = [flip_nw_se(i) for i in ws] + bs = [flip_nw_se(i) for i in bs] + + if ws[2] < ws[1]: + tmp = ws[1] + ws[1] = ws[2] + ws[2] = tmp + if ws[3] < ws[2]: + tmp = ws[2] + ws[2] = ws[3] + ws[3] = tmp + if ws[2] < ws[1]: + tmp = ws[1] + ws[1] = ws[2] + ws[2] = tmp ki = KKIDX[bs[0]][ws[0]] @@ -694,7 +803,6 @@ def kaaak_pctoindex(c: Request) -> int: return ki * BLOCK_Ax + ai - def kppkp_pctoindex(c: Request) -> int: BLOCK_Ax = MAX_PP48_INDEX * 64 * 64 BLOCK_Bx = 64 * 64 @@ -706,7 +814,12 @@ def kppkp_pctoindex(c: Request) -> int: bk = c.black_piece_squares[0] pawn_c = c.black_piece_squares[1] - pawn_c, wk, pawn_a, pawn_b, bk = flip_we_col(pawn_c, wk, pawn_a, pawn_b, bk) + if (pawn_c & 7) > 3: + wk = flip_we(wk) + pawn_a = flip_we(pawn_a) + pawn_b = flip_we(pawn_b) + bk = flip_we(bk) + pawn_c = flip_we(pawn_c) i = flip_we(flip_ns(pawn_a)) - 8 j = flip_we(flip_ns(pawn_b)) - 8 @@ -721,19 +834,28 @@ def kppkp_pctoindex(c: Request) -> int: return k * BLOCK_Ax + pp48_slice * BLOCK_Bx + wk * BLOCK_Cx + bk - def kaakb_pctoindex(c: Request) -> int: + N_WHITE = 3 + N_BLACK = 2 BLOCK_Bx = 64 BLOCK_Ax = BLOCK_Bx * MAX_AAINDEX - ws = c.white_piece_squares[:3] - bs = c.black_piece_squares[:2] ft = FLIPT[c.black_piece_squares[0]][c.white_piece_squares[0]] - for flag, flip in [(WE_FLAG, flip_we), (NS_FLAG, flip_ns), (NW_SE_FLAG, flip_nw_se)]: - if ft & flag: - ws = [flip(i) for i in ws] - bs = [flip(i) for i in bs] + ws = c.white_piece_squares[:N_WHITE] + bs = c.black_piece_squares[:N_BLACK] + + if (ft & WE_FLAG) != 0: + ws = [flip_we(i) for i in ws] + bs = [flip_we(i) for i in bs] + + if (ft & NS_FLAG) != 0: + ws = [flip_ns(i) for i in ws] + bs = [flip_ns(i) for i in bs] + + if (ft & NW_SE_FLAG) != 0: + ws = [flip_nw_se(i) for i in ws] + bs = [flip_nw_se(i) for i in bs] ki = KKIDX[bs[0]][ws[0]] # KKIDX[black king][white king] ai = AAIDX[ws[1]][ws[2]] @@ -743,7 +865,6 @@ def kaakb_pctoindex(c: Request) -> int: return ki * BLOCK_Ax + ai * BLOCK_Bx + bs[1] - def kabkc_pctoindex(c: Request) -> int: N_WHITE = 3 N_BLACK = 2 @@ -757,10 +878,17 @@ def kabkc_pctoindex(c: Request) -> int: ws = c.white_piece_squares[:N_WHITE] bs = c.black_piece_squares[:N_BLACK] - for flag, flip in [(WE_FLAG, flip_we), (NS_FLAG, flip_ns), (NW_SE_FLAG, flip_nw_se)]: - if ft & flag: - ws = [flip(i) for i in ws] - bs = [flip(i) for i in bs] + if (ft & WE_FLAG) != 0: + ws = [flip_we(i) for i in ws] + bs = [flip_we(i) for i in bs] + + if (ft & NS_FLAG) != 0: + ws = [flip_ns(i) for i in ws] + bs = [flip_ns(i) for i in bs] + + if (ft & NW_SE_FLAG) != 0: + ws = [flip_nw_se(i) for i in ws] + bs = [flip_nw_se(i) for i in bs] ki = KKIDX[bs[0]][ws[0]] # KKIDX [black king] [white king] @@ -769,17 +897,23 @@ def kabkc_pctoindex(c: Request) -> int: return ki * BLOCK_Ax + ws[1] * BLOCK_Bx + ws[2] * BLOCK_Cx + bs[1] - def kpkp_pctoindex(c: Request) -> int: BLOCK_Ax = 64 * 64 BLOCK_Bx = 64 wk = c.white_piece_squares[0] bk = c.black_piece_squares[0] - anchor = c.white_piece_squares[1] - loosen = c.black_piece_squares[1] + pawn_a = c.white_piece_squares[1] + pawn_b = c.black_piece_squares[1] - anchor, loosen, wk, bk = flip_we_col(anchor, loosen, wk, bk) + anchor = pawn_a + loosen = pawn_b + + if (anchor & 7) > 3: + anchor = flip_we(anchor) + loosen = flip_we(loosen) + wk = flip_we(wk) + bk = flip_we(bk) m = wsq_to_pidx24(anchor) n = loosen - 8 @@ -791,7 +925,6 @@ def kpkp_pctoindex(c: Request) -> int: return pp_slice * BLOCK_Ax + wk * BLOCK_Bx + bk - def kppk_pctoindex(c: Request) -> int: BLOCK_Ax = 64 * 64 BLOCK_Bx = 64 @@ -802,7 +935,11 @@ def kppk_pctoindex(c: Request) -> int: anchor, loosen = pp_putanchorfirst(pawn_a, pawn_b) - anchor, loosen, wk, bk = flip_we_col(anchor, loosen, wk, bk) + if (anchor & 7) > 3: + anchor = flip_we(anchor) + loosen = flip_we(loosen) + wk = flip_we(wk) + bk = flip_we(bk) i = wsq_to_pidx24(anchor) j = wsq_to_pidx48(loosen) @@ -814,7 +951,6 @@ def kppk_pctoindex(c: Request) -> int: return pp_slice * BLOCK_Ax + wk * BLOCK_Bx + bk - def kapk_pctoindex(c: Request) -> int: BLOCK_Ax = 64 * 64 * 64 BLOCK_Bx = 64 * 64 @@ -825,17 +961,22 @@ def kapk_pctoindex(c: Request) -> int: wk = c.white_piece_squares[0] bk = c.black_piece_squares[0] - if not chess.A2 <= pawn < chess.A8: + if not (chess.A2 <= pawn < chess.A8): return NOINDEX - pawn, wk, bk, wa = flip_we_col(pawn, wk, bk, wa) + if (pawn & 7) > 3: + pawn = flip_we(pawn) + wk = flip_we(wk) + bk = flip_we(bk) + wa = flip_we(wa) - sq = pawn ^ 56 - 8 # flip_ns and down one row + sq = pawn + sq ^= 56 # flip_ns + sq -= 8 # Down one row pslice = ((sq + (sq & 3)) >> 1) return pslice * BLOCK_Ax + wk * BLOCK_Bx + bk * BLOCK_Cx + wa - def kabk_pctoindex(c: Request) -> int: BLOCK_Ax = 64 * 64 BLOCK_Bx = 64 @@ -845,10 +986,17 @@ def kabk_pctoindex(c: Request) -> int: ws = c.white_piece_squares bs = c.black_piece_squares - for flag, flip in [(1, flip_we), (2, flip_ns), (4, flip_nw_se)]: - if ft & flag: - ws = [flip(b) for b in ws] - bs = [flip(b) for b in bs] + if (ft & 1) != 0: + ws = [flip_we(b) for b in ws] + bs = [flip_we(b) for b in bs] + + if (ft & 2) != 0: + ws = [flip_ns(b) for b in ws] + bs = [flip_ns(b) for b in bs] + + if (ft & 4) != 0: + ws = [flip_nw_se(b) for b in ws] + bs = [flip_nw_se(b) for b in bs] ki = KKIDX[bs[0]][ws[0]] # KKIDX[black king][white king] @@ -857,7 +1005,6 @@ def kabk_pctoindex(c: Request) -> int: return ki * BLOCK_Ax + ws[1] * BLOCK_Bx + ws[2] - def kakp_pctoindex(c: Request) -> int: BLOCK_Ax = 64 * 64 * 64 BLOCK_Bx = 64 * 64 @@ -868,29 +1015,42 @@ def kakp_pctoindex(c: Request) -> int: wk = c.white_piece_squares[0] bk = c.black_piece_squares[0] - if not chess.A2 <= pawn < chess.A8: + if not (chess.A2 <= pawn < chess.A8): return NOINDEX - pawn, wk, bk, wa = flip_we_col(pawn, wk, bk, wa) + if (pawn & 7) > 3: + pawn = flip_we(pawn) + wk = flip_we(wk) + bk = flip_we(bk) + wa = flip_we(wa) - # Down one row - pslice = ((pawn - 8) + ((pawn - 8) & 3)) >> 1 + sq = pawn + sq -= 8 # Down one row + pslice = (sq + (sq & 3)) >> 1 return pslice * BLOCK_Ax + wk * BLOCK_Bx + bk * BLOCK_Cx + wa - def kaak_pctoindex(c: Request) -> int: + N_WHITE = 3 + N_BLACK = 1 BLOCK_Ax = MAX_AAINDEX ft = FLIPT[c.black_piece_squares[0]][c.white_piece_squares[0]] - ws = c.white_piece_squares[:3] - bs = c.black_piece_squares[:1] + ws = c.white_piece_squares[:N_WHITE] + bs = c.black_piece_squares[:N_BLACK] + + if (ft & WE_FLAG) != 0: + ws = [flip_we(i) for i in ws] + bs = [flip_we(i) for i in bs] - for flag, flip in [(WE_FLAG, flip_we), (NS_FLAG, flip_ns), (NW_SE_FLAG, flip_nw_se)]: - if ft & flag: - ws = [flip(i) for i in ws] - bs = [flip(i) for i in bs] + if (ft & NS_FLAG) != 0: + ws = [flip_ns(i) for i in ws] + bs = [flip_ns(i) for i in bs] + + if (ft & NW_SE_FLAG) != 0: + ws = [flip_nw_se(i) for i in ws] + bs = [flip_nw_se(i) for i in bs] ki = KKIDX[bs[0]][ws[0]] # KKIDX[black king][white king] ai = AAIDX[ws[1]][ws[2]] @@ -900,7 +1060,6 @@ def kaak_pctoindex(c: Request) -> int: return ki * BLOCK_Ax + ai - def kakb_pctoindex(c: Request) -> int: BLOCK_Ax = 64 * 64 BLOCK_Bx = 64 @@ -910,9 +1069,23 @@ def kakb_pctoindex(c: Request) -> int: ws = c.white_piece_squares[:] bs = c.black_piece_squares[:] - for bit, flip in [(1, flip_we), (2, flip_ns), (4, flip_nw_se)]: - if ft & bit: - ws[0], ws[1], bs[0], bs[1] = flip(ws[0]), flip(ws[1]), flip(bs[0]), flip(bs[1]) + if (ft & 1) != 0: + ws[0] = flip_we(ws[0]) + ws[1] = flip_we(ws[1]) + bs[0] = flip_we(bs[0]) + bs[1] = flip_we(bs[1]) + + if (ft & 2) != 0: + ws[0] = flip_ns(ws[0]) + ws[1] = flip_ns(ws[1]) + bs[0] = flip_ns(bs[0]) + bs[1] = flip_ns(bs[1]) + + if (ft & 4) != 0: + ws[0] = flip_nw_se(ws[0]) + ws[1] = flip_nw_se(ws[1]) + bs[0] = flip_nw_se(bs[0]) + bs[1] = flip_nw_se(bs[1]) ki = KKIDX[bs[0]][ws[0]] # KKIDX[black king][white king] @@ -921,7 +1094,6 @@ def kakb_pctoindex(c: Request) -> int: return ki * BLOCK_Ax + ws[1] * BLOCK_Bx + bs[1] - def kpk_pctoindex(c: Request) -> int: BLOCK_A = 64 * 64 BLOCK_B = 64 @@ -930,16 +1102,21 @@ def kpk_pctoindex(c: Request) -> int: wk = c.white_piece_squares[0] bk = c.black_piece_squares[0] - if not chess.A2 <= pawn < chess.A8: + if not (chess.A2 <= pawn < chess.A8): return NOINDEX - pawn, wk, bk = flip_we_col(pawn, wk, bk) + if (pawn & 7) > 3: + pawn = flip_we(pawn) + wk = flip_we(wk) + bk = flip_we(bk) - sq = (pawn ^ 56) - 8 # flip_ns, down one row + sq = pawn + sq ^= 56 # flip_ns + sq -= 8 # Down one row pslice = ((sq + (sq & 3)) >> 1) - return pslice * BLOCK_A + wk * BLOCK_B + bk - + res = pslice * BLOCK_A + wk * BLOCK_B + bk + return res def kpppk_pctoindex(c: Request) -> int: BLOCK_A = 64 * 64 @@ -949,16 +1126,26 @@ def kpppk_pctoindex(c: Request) -> int: pawn_a = c.white_piece_squares[1] pawn_b = c.white_piece_squares[2] pawn_c = c.white_piece_squares[3] + bk = c.black_piece_squares[0] - i, j, k = [x - 8 for x in (pawn_a, pawn_b, pawn_c)] + i = pawn_a - 8 + j = pawn_b - 8 + k = pawn_c - 8 + ppp48_slice = PPP48_IDX[i][j][k] if idx_is_empty(ppp48_slice): - wk, pawn_a, pawn_b, pawn_c, bk = [flip_we(x) for x in (wk, pawn_a, pawn_b, pawn_c, bk)] + wk = flip_we(wk) + pawn_a = flip_we(pawn_a) + pawn_b = flip_we(pawn_b) + pawn_c = flip_we(pawn_c) + bk = flip_we(bk) + + i = pawn_a - 8 + j = pawn_b - 8 + k = pawn_c - 8 - # removing this doesn't impact the tests? - i, j, k = [x - 8 for x in (pawn_a, pawn_b, pawn_c)] ppp48_slice = PPP48_IDX[i][j][k] if idx_is_empty(ppp48_slice): @@ -1143,15 +1330,13 @@ def __init__(self, maxindex: int, slice_n: int, pctoi: Callable[[Request], int]) def sortlists(ws: List[int], wp: List[int]) -> Tuple[List[int], List[int]]: - z = sorted(zip(wp, ws), reverse=True) + z = sorted(zip(wp, ws), key=lambda x: x[0], reverse=True) wp2, ws2 = zip(*z) return list(ws2), list(wp2) - def egtb_block_unpack(side: int, n: int, bp: bytes) -> List[int]: return [dtm_unpack(side, i) for i in bp[:n]] - def split_index(i: int) -> Tuple[int, int]: return divmod(i, ENTRIES_PER_BLOCK) @@ -1175,23 +1360,19 @@ def removepiece(ys: List[int], yp: List[int], j: int) -> None: del ys[j] del yp[j] - def opp(side: int) -> int: return 1 if side == 0 else 0 - def adjust_up(dist: int) -> int: - if (dist & INFOMASK) in [iWMATE, iWMATEt, iBMATE, iBMATEt]: - return dist + (1 << PLYSHIFT) - return dist + udist = dist + sw = udist & INFOMASK + if sw in [iWMATE, iWMATEt, iBMATE, iBMATEt]: + udist += (1 << PLYSHIFT) -def bestx(side: int, a: int, b: int) -> int: - if a == iFORBID: - return b - if b == iFORBID: - return a + return udist +def bestx(side: int, a: int, b: int) -> int: # 0 = selectfirst # 1 = selectlowest # 2 = selecthighest @@ -1204,25 +1385,33 @@ def bestx(side: int, a: int, b: int) -> int: [3, 3, 3, 0], # forbid ] + xorkey = [0, 3] + + if a == iFORBID: + return b + if b == iFORBID: + return a + retu = [a, a, b, b] if b < a: - retu[1:2] = [b, a] + retu[1] = b + retu[2] = a - key = comparison[a & 3][b & 3] ^ [0, 3][side] # ^ xorkey + key = comparison[a & 3][b & 3] ^ xorkey[side] return retu[key] - def unpackdist(d: int) -> Tuple[int, int]: return d >> PLYSHIFT, d & INFOMASK - def dtm_unpack(stm: int, packed: int) -> int: - if packed in [iDRAW, iFORBID]: - return packed + p = packed + + if p in [iDRAW, iFORBID]: + return p - info = packed & 3 - store = packed >> 2 + info = p & 3 + store = p >> 2 if stm == 0: if info == iWMATE: @@ -1398,23 +1587,28 @@ def probe_dtm(self, board: chess.Board) -> int: if req.realside == 1: if req.is_reversed: return ply - return -ply - if req.is_reversed: - return -ply - return ply - - if res == iBMATE: + else: + return -ply + else: + if req.is_reversed: + return -ply + else: + return ply + elif res == iBMATE: # Black mates in the stored position. if req.realside == 0: if req.is_reversed: return ply - return -ply - if req.is_reversed: - return -ply - return ply - - # Draw. - return 0 + else: + return -ply + else: + if req.is_reversed: + return -ply + else: + return ply + else: + # Draw. + return 0 def get_dtm(self, board: chess.Board, default: Optional[int] = None) -> Optional[int]: try: @@ -1450,12 +1644,12 @@ def probe_wdl(self, board: chess.Board) -> int: if dtm == 0: if board.is_checkmate(): return -1 - return 0 - - if dtm > 0: + else: + return 0 + elif dtm > 0: return 1 - - return -1 + else: + return -1 def get_wdl(self, board: chess.Board, default: Optional[int] = None) -> Optional[int]: try: @@ -1601,7 +1795,8 @@ def egtb_block_getsize(self, req: Request, idx: int) -> int: if (offset + blocksz) > maxindex: return maxindex - offset # Last block size - return blocksz # Size of a normal block + else: + return blocksz # Size of a normal block def _tb_probe(self, req: Request) -> int: stream = self._setup_tablebase(req) From dd4d9c1285d70f1aaffa276244101d9373053c1d Mon Sep 17 00:00:00 2001 From: Niklas Fiekas Date: Sun, 23 Feb 2025 22:55:23 +0100 Subject: [PATCH 02/27] Fix ep resolution in chess.gaviota.PythonTablebase (fixes #1132) --- chess/gaviota.py | 142 +++++++---------------------------------------- test.py | 5 ++ 2 files changed, 26 insertions(+), 121 deletions(-) diff --git a/chess/gaviota.py b/chess/gaviota.py index 39173b593..83fb7bb64 100644 --- a/chess/gaviota.py +++ b/chess/gaviota.py @@ -1356,51 +1356,9 @@ def split_index(i: int) -> Tuple[int, int]: iBMATEt = tb_BMATE | 4 -def removepiece(ys: List[int], yp: List[int], j: int) -> None: - del ys[j] - del yp[j] - def opp(side: int) -> int: return 1 if side == 0 else 0 -def adjust_up(dist: int) -> int: - udist = dist - sw = udist & INFOMASK - - if sw in [iWMATE, iWMATEt, iBMATE, iBMATEt]: - udist += (1 << PLYSHIFT) - - return udist - -def bestx(side: int, a: int, b: int) -> int: - # 0 = selectfirst - # 1 = selectlowest - # 2 = selecthighest - # 3 = selectsecond - comparison = [ - # draw, wmate, bmate, forbid - [0, 3, 0, 0], # draw - [0, 1, 0, 0], # wmate - [3, 3, 2, 0], # bmate - [3, 3, 3, 0], # forbid - ] - - xorkey = [0, 3] - - if a == iFORBID: - return b - if b == iFORBID: - return a - - retu = [a, a, b, b] - - if b < a: - retu[1] = b - retu[2] = a - - key = comparison[a & 3][b & 3] ^ xorkey[side] - return retu[key] - def unpackdist(d: int) -> Tuple[int, int]: return d >> PLYSHIFT, d & INFOMASK @@ -1492,12 +1450,11 @@ class Request: black_piece_types: List[int] is_reversed: bool - def __init__(self, white_squares: List[int], white_types: List[chess.PieceType], black_squares: List[int], black_types: List[chess.PieceType], side: int, epsq: int): + def __init__(self, white_squares: List[int], white_types: List[chess.PieceType], black_squares: List[int], black_types: List[chess.PieceType], side: int): self.white_squares, self.white_types = sortlists(white_squares, white_types) self.black_squares, self.black_types = sortlists(black_squares, black_types) self.realside = side self.side = side - self.epsq = epsq @dataclasses.dataclass @@ -1569,17 +1526,34 @@ def probe_dtm(self, board: chess.Board) -> int: if board.occupied == board.kings: return 0 + # Resolve en passant. + dtm = self._probe_dtm_no_ep(board) + for move in board.generate_legal_ep(): + try: + board.push(move) + + child_dtm = -self._probe_dtm_no_ep(board) + if child_dtm > 0: + child_dtm += 1 + elif child_dtm < 0: + child_dtm -= 1 + + dtm = min(dtm, child_dtm) if dtm * child_dtm > 0 else max(dtm, child_dtm) + finally: + board.pop() + return dtm + + def _probe_dtm_no_ep(self, board: chess.Board) -> int: # Prepare the tablebase request. white_squares = list(chess.SquareSet(board.occupied_co[chess.WHITE])) white_types = [typing.cast(chess.PieceType, board.piece_type_at(sq)) for sq in white_squares] black_squares = list(chess.SquareSet(board.occupied_co[chess.BLACK])) black_types = [typing.cast(chess.PieceType, board.piece_type_at(sq)) for sq in black_squares] side = 0 if (board.turn == chess.WHITE) else 1 - epsq = board.ep_square if board.ep_square else NOSQUARE - req = Request(white_squares, white_types, black_squares, black_types, side, epsq) + req = Request(white_squares, white_types, black_squares, black_types, side) # Probe. - dtm = self.egtb_get_dtm(req) + dtm = self._tb_probe(req) ply, res = unpackdist(dtm) if res == iWMATE: @@ -1675,10 +1649,7 @@ def _setup_tablebase(self, req: Request) -> BinaryIO: req.white_piece_types = req.black_types req.black_piece_squares = [flip_ns(s) for s in req.white_squares] req.black_piece_types = req.white_types - req.side = opp(req.side) - if req.epsq != NOSQUARE: - req.epsq = flip_ns(req.epsq) else: raise MissingTableError(f"no gaviota table available for: {white_letters.upper()}v{black_letters.upper()}") @@ -1708,77 +1679,6 @@ def close(self) -> None: _, stream = self.streams.popitem() stream.close() - def egtb_get_dtm(self, req: Request) -> int: - dtm = self._tb_probe(req) - - if req.epsq != NOSQUARE: - capturer_a = 0 - capturer_b = 0 - xed = 0 - - # Flip for move generation. - if req.side == 0: - xs = list(req.white_piece_squares) - xp = list(req.white_piece_types) - ys = list(req.black_piece_squares) - yp = list(req.black_piece_types) - else: - xs = list(req.black_piece_squares) - xp = list(req.black_piece_types) - ys = list(req.white_piece_squares) - yp = list(req.white_piece_types) - - # Captured pawn trick: from ep square to captured. - xed = req.epsq ^ (1 << 3) - - # Find captured index (j). - try: - j = ys.index(xed) - except ValueError: - j = -1 - - # Try first possible ep capture. - if 0 == (0x88 & (map88(xed) + 1)): - capturer_a = xed + 1 - - # Try second possible ep capture. - if 0 == (0x88 & (map88(xed) - 1)): - capturer_b = xed - 1 - - if (j > -1) and (ys[j] == xed): - # Find capturers (i). - for i in range(len(xs)): - if xp[i] == chess.PAWN and (xs[i] == capturer_a or xs[i] == capturer_b): - epscore = iFORBID - - # Copy position. - xs_after = xs[:] - ys_after = ys[:] - xp_after = xp[:] - yp_after = yp[:] - - # Execute capture. - xs_after[i] = req.epsq - removepiece(ys_after, yp_after, j) - - # Flip back. - if req.side == 1: - xs_after, ys_after = ys_after, xs_after - xp_after, yp_after = yp_after, xp_after - - # Make subrequest. - subreq = Request(xs_after, xp_after, ys_after, yp_after, opp(req.side), NOSQUARE) - try: - epscore = self._tb_probe(subreq) - epscore = adjust_up(epscore) - - # Choose to ep or not. - dtm = bestx(req.side, epscore, dtm) - except IndexError: - break - - return dtm - def egtb_block_getnumber(self, req: Request, idx: int) -> int: maxindex = EGKEY[req.egkey].maxindex diff --git a/test.py b/test.py index 6db84d254..7a3a3cdef 100755 --- a/test.py +++ b/test.py @@ -4348,6 +4348,11 @@ def test_two_ep(self): board = chess.Board("K7/8/8/6k1/5pPp/8/8/8 b - g3 0 61") self.assertEqual(self.tablebase.probe_dtm(board), 17) + @catchAndSkip(chess.gaviota.MissingTableError, "need KPvKP.gtb.cp4") + def test_ep_is_best(self): + board = chess.Board("8/8/7k/8/1pP5/7K/8/8 b - c3 0 1") + self.assertEqual(self.tablebase.probe_dtm(board), 19) + class SvgTestCase(unittest.TestCase): From 06de70e2e87969743dfa2196db1e2cbe687a08a8 Mon Sep 17 00:00:00 2001 From: Niklas Fiekas Date: Mon, 24 Feb 2025 21:52:23 +0100 Subject: [PATCH 03/27] Fix checkmating ep capture in chess.gaviota.PythonTablebase --- chess/gaviota.py | 17 ++++++++++------- test.py | 10 ++++++++++ 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/chess/gaviota.py b/chess/gaviota.py index 83fb7bb64..281df836b 100644 --- a/chess/gaviota.py +++ b/chess/gaviota.py @@ -1493,8 +1493,8 @@ def probe_dtm(self, board: chess.Board) -> int: Probes for depth to mate information. The absolute value is the number of half-moves until forced mate - (or ``0`` in drawn positions). The value is positive if the - side to move is winning, otherwise it is negative. + (or ``0`` in drawn or checkmated positions). The value is positive if + the side to move is winning, otherwise it is negative or 0. In the example position, white to move will get mated in 10 half-moves: @@ -1532,11 +1532,14 @@ def probe_dtm(self, board: chess.Board) -> int: try: board.push(move) - child_dtm = -self._probe_dtm_no_ep(board) - if child_dtm > 0: - child_dtm += 1 - elif child_dtm < 0: - child_dtm -= 1 + if board.is_checkmate(): + child_dtm = 1 + else: + child_dtm = -self._probe_dtm_no_ep(board) + if child_dtm > 0: + child_dtm += 1 + elif child_dtm < 0: + child_dtm -= 1 dtm = min(dtm, child_dtm) if dtm * child_dtm > 0 else max(dtm, child_dtm) finally: diff --git a/test.py b/test.py index 7a3a3cdef..c120c049f 100755 --- a/test.py +++ b/test.py @@ -4353,6 +4353,16 @@ def test_ep_is_best(self): board = chess.Board("8/8/7k/8/1pP5/7K/8/8 b - c3 0 1") self.assertEqual(self.tablebase.probe_dtm(board), 19) + @catchAndSkip(chess.gaviota.MissingTableError, "need KQPvKP.gtb.cp4") + def test_ep_is_mate(self): + # The resulting mate. + board = chess.Board("5Q2/7k/6P1/5K2/8/8/8/8 b - - 0 1") + self.assertEqual(self.tablebase.probe_dtm(board), 0) + + # Ep leads to the previously tested mate position. + board = chess.Board("5Q2/7k/8/5KpP/8/8/8/8 w - g6 0 1") + self.assertEqual(self.tablebase.probe_dtm(board), 1) + class SvgTestCase(unittest.TestCase): From 45f616fae51a2b08ca4b4d0c01ddddf175d81ff9 Mon Sep 17 00:00:00 2001 From: Niklas Fiekas Date: Tue, 25 Feb 2025 20:05:16 +0100 Subject: [PATCH 04/27] Supprt Python 3.13 --- .github/workflows/test.yml | 8 ++++---- setup.py | 1 + tox.ini | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 035b7f3d1..22d3b278a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -7,7 +7,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, windows-latest] - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 @@ -24,7 +24,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-python@v4 with: - python-version: "3.12" + python-version: "3.13" - run: pip install -e . - run: python examples/perft/perft.py -t 1 examples/perft/random.perft --max-nodes 10000 - run: python examples/perft/perft.py -t 1 examples/perft/chess960.perft --max-nodes 100000 @@ -39,7 +39,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, windows-latest] - python-version: ["3.9", "3.10", "3.11", "3.12"] + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -59,7 +59,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-python@v4 with: - python-version: "3.12" + python-version: "3.13" - run: sudo apt-get update && sudo apt-get install -y docutils-common - run: python setup.py --long-description | rst2html --strict --no-raw > /dev/null - run: pip install -e . diff --git a/setup.py b/setup.py index b825adb44..9d6aa8c58 100755 --- a/setup.py +++ b/setup.py @@ -83,6 +83,7 @@ def read_description(): "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Topic :: Games/Entertainment :: Board Games", "Topic :: Games/Entertainment :: Turn Based Strategy", "Topic :: Software Development :: Libraries :: Python Modules", diff --git a/tox.ini b/tox.ini index 8d8033676..5970b2c79 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py38,py39,py310,py311 +envlist = py38,py39,py310,py311,py312,py313 [testenv] passenv = LD_LIBRARY_PATH From b3c1f62c82b5fc40b14fa33bc9edd31cef68a944 Mon Sep 17 00:00:00 2001 From: Niklas Fiekas Date: Tue, 25 Feb 2025 19:20:27 +0100 Subject: [PATCH 05/27] Prepare 1.11.2 --- CHANGELOG.rst | 8 ++++++++ chess/__init__.py | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 05b899a20..03a9555d1 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,14 @@ Changelog for python-chess ========================== +New in v1.11.2 (25th Feb 2025) +------------------------------ + +Bugfixes: + +* Fix ``chess.gaviota.PythonTablebase`` does not properly resolve positions + where en passant captures are the best move. + New in v1.11.1 (9th Oct 2024) ----------------------------- diff --git a/chess/__init__.py b/chess/__init__.py index 8d0a68258..268f91cd5 100644 --- a/chess/__init__.py +++ b/chess/__init__.py @@ -11,7 +11,7 @@ __email__ = "niklas.fiekas@backscattering.de" -__version__ = "1.11.1" +__version__ = "1.11.2" import collections import copy From 2b8b0eb06accff3c4dea2e1cec108b3900a4f1d5 Mon Sep 17 00:00:00 2001 From: Mark Harrison Date: Mon, 24 Mar 2025 01:54:37 -0700 Subject: [PATCH 06/27] Add some missing docstrings This should cause these methods to appear in the readthedocs docs. --- chess/__init__.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/chess/__init__.py b/chess/__init__.py index 268f91cd5..28bcad0cd 100644 --- a/chess/__init__.py +++ b/chess/__init__.py @@ -1810,11 +1810,13 @@ def ply(self) -> int: return 2 * (self.fullmove_number - 1) + (self.turn == BLACK) def remove_piece_at(self, square: Square) -> Optional[Piece]: + """Remove a piece, if any, from the given square and return the removed piece.""" piece = super().remove_piece_at(square) self.clear_stack() return piece def set_piece_at(self, square: Square, piece: Optional[Piece], promoted: bool = False) -> None: + """Place a piece on a square.""" super().set_piece_at(square, piece, promoted=promoted) self.clear_stack() @@ -1998,6 +2000,7 @@ def is_pseudo_legal(self, move: Move) -> bool: return bool(self.attacks_mask(move.from_square) & to_mask) def is_legal(self, move: Move) -> bool: + """Check if a move is legal in the current position.""" return not self.is_variant_end() and self.is_pseudo_legal(move) and not self.is_into_check(move) def is_variant_end(self) -> bool: @@ -2034,9 +2037,27 @@ def is_variant_draw(self) -> bool: return False def is_game_over(self, *, claim_draw: bool = False) -> bool: + """ + Check if the game is over by any rule. + + The game is not considered to be over by the + :func:`fifty-move rule ` or + :func:`threefold repetition `, + unless *claim_draw* is given. Note that checking the latter can be + slow. + """ return self.outcome(claim_draw=claim_draw) is not None def result(self, *, claim_draw: bool = False) -> str: + """ + Return the result of a game: 1-0, 0-1, 1/2-1/2, or *. + + The game is not considered to be over by the + :func:`fifty-move rule ` or + :func:`threefold repetition `, + unless *claim_draw* is given. Note that checking the latter can be + slow. + """ outcome = self.outcome(claim_draw=claim_draw) return outcome.result() if outcome else "*" From b2144c2564f740ea120d037e9ec6129d8a3bea2c Mon Sep 17 00:00:00 2001 From: Niklas Fiekas Date: Fri, 3 Oct 2025 23:01:01 +0200 Subject: [PATCH 07/27] Remove explicit CodeQL configuration --- .github/workflows/codeql.yml | 18 ------------------ 1 file changed, 18 deletions(-) delete mode 100644 .github/workflows/codeql.yml diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml deleted file mode 100644 index bc991c885..000000000 --- a/.github/workflows/codeql.yml +++ /dev/null @@ -1,18 +0,0 @@ -name: CodeQL - -on: [push, pull_request] - -jobs: - analyze: - name: Analyze - runs-on: ubuntu-latest - permissions: - actions: read - contents: read - security-events: write - steps: - - uses: actions/checkout@v4 - - uses: github/codeql-action/init@v3 - with: - languages: python - - uses: github/codeql-action/analyze@v3 From 760360b8ddb65129aea46f84d99b5491e6ed6435 Mon Sep 17 00:00:00 2001 From: Niklas Fiekas Date: Fri, 3 Oct 2025 23:04:49 +0200 Subject: [PATCH 08/27] Explicitly specify CI workflow permissions --- .github/workflows/test.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 22d3b278a..9eca0fce3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -2,6 +2,9 @@ name: Test on: [push, pull_request] +permissions: + contents: read + jobs: test: strategy: From 6b1cfedd442a05767ee28c7752a800ad4190f423 Mon Sep 17 00:00:00 2001 From: Niklas Fiekas Date: Fri, 3 Oct 2025 23:14:56 +0200 Subject: [PATCH 09/27] Fix chess.gaviota bytearray usage does not pass mypy 1.18 --- chess/gaviota.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chess/gaviota.py b/chess/gaviota.py index 281df836b..dc19557cd 100644 --- a/chess/gaviota.py +++ b/chess/gaviota.py @@ -1716,7 +1716,7 @@ def _tb_probe(self, req: Request) -> int: z = self.egtb_block_getsize_zipped(req.egkey, block) self.egtb_block_park(req.egkey, block, stream) - buffer_zipped = stream.read(z) + buffer_zipped: bytearray | bytes = stream.read(z) if buffer_zipped[0] == 0: # If flag is zero, plain LZMA is following. From 71f5a21fe9a83770081fa9f7d2deb3835db984e4 Mon Sep 17 00:00:00 2001 From: Jackson Hall Date: Sat, 4 Oct 2025 16:52:41 -0400 Subject: [PATCH 10/27] Add and use rank/file constants --- chess/__init__.py | 80 +++++++++++++++++++++++++++++------------------ chess/gaviota.py | 8 ++--- chess/syzygy.py | 2 +- 3 files changed, 55 insertions(+), 35 deletions(-) diff --git a/chess/__init__.py b/chess/__init__.py index 28bcad0cd..3d3dad4f0 100644 --- a/chess/__init__.py +++ b/chess/__init__.py @@ -64,8 +64,28 @@ def piece_name(piece_type: PieceType) -> str: "P": "♙", "p": "♟", } +File: TypeAlias = int +FILE_A: File = 0 +FILE_B: File = 1 +FILE_C: File = 2 +FILE_D: File = 3 +FILE_E: File = 4 +FILE_F: File = 5 +FILE_G: File = 6 +FILE_H: File = 7 +FILES = [FILE_A, FILE_B, FILE_C, FILE_D, FILE_E, FILE_F, FILE_G, FILE_H] FILE_NAMES = ["a", "b", "c", "d", "e", "f", "g", "h"] +Rank: TypeAlias = int +RANK_1: Rank = 0 +RANK_2: Rank = 1 +RANK_3: Rank = 2 +RANK_4: Rank = 3 +RANK_5: Rank = 4 +RANK_6: Rank = 5 +RANK_7: Rank = 6 +RANK_8: Rank = 7 +RANKS = [RANK_1, RANK_2, RANK_3, RANK_4, RANK_5, RANK_6, RANK_7, RANK_8] RANK_NAMES = ["1", "2", "3", "4", "5", "6", "7", "8"] STARTING_FEN = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1" @@ -251,15 +271,15 @@ def square_name(square: Square) -> str: """Gets the name of the square, like ``a3``.""" return SQUARE_NAMES[square] -def square(file_index: int, rank_index: int) -> Square: +def square(file_index: File, rank_index: Rank) -> Square: """Gets a square number by file and rank index.""" return rank_index * 8 + file_index -def square_file(square: Square) -> int: +def square_file(square: Square) -> File: """Gets the file index of the square where ``0`` is the a-file.""" return square & 7 -def square_rank(square: Square) -> int: +def square_rank(square: Square) -> Rank: """Gets the rank index of the square where ``0`` is the first rank.""" return square >> 3 @@ -376,24 +396,24 @@ def square_mirror(square: Square) -> Square: BB_LIGHT_SQUARES: Bitboard = 0x55aa_55aa_55aa_55aa BB_DARK_SQUARES: Bitboard = 0xaa55_aa55_aa55_aa55 -BB_FILE_A: Bitboard = 0x0101_0101_0101_0101 << 0 -BB_FILE_B: Bitboard = 0x0101_0101_0101_0101 << 1 -BB_FILE_C: Bitboard = 0x0101_0101_0101_0101 << 2 -BB_FILE_D: Bitboard = 0x0101_0101_0101_0101 << 3 -BB_FILE_E: Bitboard = 0x0101_0101_0101_0101 << 4 -BB_FILE_F: Bitboard = 0x0101_0101_0101_0101 << 5 -BB_FILE_G: Bitboard = 0x0101_0101_0101_0101 << 6 -BB_FILE_H: Bitboard = 0x0101_0101_0101_0101 << 7 +BB_FILE_A: Bitboard = 0x0101_0101_0101_0101 << FILE_A +BB_FILE_B: Bitboard = 0x0101_0101_0101_0101 << FILE_B +BB_FILE_C: Bitboard = 0x0101_0101_0101_0101 << FILE_C +BB_FILE_D: Bitboard = 0x0101_0101_0101_0101 << FILE_D +BB_FILE_E: Bitboard = 0x0101_0101_0101_0101 << FILE_E +BB_FILE_F: Bitboard = 0x0101_0101_0101_0101 << FILE_F +BB_FILE_G: Bitboard = 0x0101_0101_0101_0101 << FILE_G +BB_FILE_H: Bitboard = 0x0101_0101_0101_0101 << FILE_H BB_FILES: List[Bitboard] = [BB_FILE_A, BB_FILE_B, BB_FILE_C, BB_FILE_D, BB_FILE_E, BB_FILE_F, BB_FILE_G, BB_FILE_H] -BB_RANK_1: Bitboard = 0xff << (8 * 0) -BB_RANK_2: Bitboard = 0xff << (8 * 1) -BB_RANK_3: Bitboard = 0xff << (8 * 2) -BB_RANK_4: Bitboard = 0xff << (8 * 3) -BB_RANK_5: Bitboard = 0xff << (8 * 4) -BB_RANK_6: Bitboard = 0xff << (8 * 5) -BB_RANK_7: Bitboard = 0xff << (8 * 6) -BB_RANK_8: Bitboard = 0xff << (8 * 7) +BB_RANK_1: Bitboard = 0xff << (8 * RANK_1) +BB_RANK_2: Bitboard = 0xff << (8 * RANK_2) +BB_RANK_3: Bitboard = 0xff << (8 * RANK_3) +BB_RANK_4: Bitboard = 0xff << (8 * RANK_4) +BB_RANK_5: Bitboard = 0xff << (8 * RANK_5) +BB_RANK_6: Bitboard = 0xff << (8 * RANK_6) +BB_RANK_7: Bitboard = 0xff << (8 * RANK_7) +BB_RANK_8: Bitboard = 0xff << (8 * RANK_8) BB_RANKS: List[Bitboard] = [BB_RANK_1, BB_RANK_2, BB_RANK_3, BB_RANK_4, BB_RANK_5, BB_RANK_6, BB_RANK_7, BB_RANK_8] BB_BACKRANKS: Bitboard = BB_RANK_1 | BB_RANK_8 @@ -1847,7 +1867,7 @@ def generate_pseudo_legal_moves(self, from_mask: Bitboard = BB_ALL, to_mask: Bit self.occupied_co[not self.turn] & to_mask) for to_square in scan_reversed(targets): - if square_rank(to_square) in [0, 7]: + if square_rank(to_square) in [RANK_1, RANK_8]: yield Move(from_square, to_square, QUEEN) yield Move(from_square, to_square, ROOK) yield Move(from_square, to_square, BISHOP) @@ -1870,7 +1890,7 @@ def generate_pseudo_legal_moves(self, from_mask: Bitboard = BB_ALL, to_mask: Bit for to_square in scan_reversed(single_moves): from_square = to_square + (8 if self.turn == BLACK else -8) - if square_rank(to_square) in [0, 7]: + if square_rank(to_square) in [RANK_1, RANK_8]: yield Move(from_square, to_square, QUEEN) yield Move(from_square, to_square, ROOK) yield Move(from_square, to_square, BISHOP) @@ -1897,7 +1917,7 @@ def generate_pseudo_legal_ep(self, from_mask: Bitboard = BB_ALL, to_mask: Bitboa capturers = ( self.pawns & self.occupied_co[self.turn] & from_mask & BB_PAWN_ATTACKS[not self.turn][self.ep_square] & - BB_RANKS[4 if self.turn else 3]) + BB_RANKS[RANK_5 if self.turn else RANK_4]) for capturer in scan_reversed(capturers): yield Move(capturer, self.ep_square) @@ -1977,9 +1997,9 @@ def is_pseudo_legal(self, move: Move) -> bool: if piece != PAWN: return False - if self.turn == WHITE and square_rank(move.to_square) != 7: + if self.turn == WHITE and square_rank(move.to_square) != RANK_8: return False - elif self.turn == BLACK and square_rank(move.to_square) != 0: + elif self.turn == BLACK and square_rank(move.to_square) != RANK_1: return False # Handle castling. @@ -2401,18 +2421,18 @@ def push(self, move: Move) -> None: else: self.castling_rights &= ~BB_RANK_8 elif captured_piece_type == KING and not self.promoted & to_bb: - if self.turn == WHITE and square_rank(move.to_square) == 7: + if self.turn == WHITE and square_rank(move.to_square) == RANK_8: self.castling_rights &= ~BB_RANK_8 - elif self.turn == BLACK and square_rank(move.to_square) == 0: + elif self.turn == BLACK and square_rank(move.to_square) == RANK_1: self.castling_rights &= ~BB_RANK_1 # Handle special pawn moves. if piece_type == PAWN: diff = move.to_square - move.from_square - if diff == 16 and square_rank(move.from_square) == 1: + if diff == 16 and square_rank(move.from_square) == RANK_2: self.ep_square = move.from_square + 8 - elif diff == -16 and square_rank(move.from_square) == 6: + elif diff == -16 and square_rank(move.from_square) == RANK_7: self.ep_square = move.from_square - 8 elif move.to_square == ep_square and abs(diff) in [7, 9] and not captured_piece_type: # Remove pawns captured en passant. @@ -3605,11 +3625,11 @@ def _valid_ep_square(self) -> Optional[Square]: return None if self.turn == WHITE: - ep_rank = 5 + ep_rank = RANK_6 pawn_mask = shift_down(BB_SQUARES[self.ep_square]) seventh_rank_mask = shift_up(BB_SQUARES[self.ep_square]) else: - ep_rank = 2 + ep_rank = RANK_3 pawn_mask = shift_up(BB_SQUARES[self.ep_square]) seventh_rank_mask = shift_down(BB_SQUARES[self.ep_square]) diff --git a/chess/gaviota.py b/chess/gaviota.py index dc19557cd..c352a27ca 100644 --- a/chess/gaviota.py +++ b/chess/gaviota.py @@ -110,12 +110,12 @@ def idx_is_empty(x: int) -> int: def flip_type(x: chess.Square, y: chess.Square) -> int: ret = 0 - if chess.square_file(x) > 3: + if chess.square_file(x) > chess.FILE_D: x = flip_we(x) y = flip_we(y) ret |= 1 - if chess.square_rank(x) > 3: + if chess.square_rank(x) > chess.RANK_4: x = flip_ns(x) y = flip_ns(y) ret |= 2 @@ -351,11 +351,11 @@ def init_ppidx() -> Tuple[List[List[int]], List[int], List[int]]: def norm_kkindex(x: chess.Square, y: chess.Square) -> Tuple[int, int]: - if chess.square_file(x) > 3: + if chess.square_file(x) > chess.FILE_D: x = flip_we(x) y = flip_we(y) - if chess.square_rank(x) > 3: + if chess.square_rank(x) > chess.RANK_4: x = flip_ns(x) y = flip_ns(y) diff --git a/chess/syzygy.py b/chess/syzygy.py index c61890550..0c6b7822c 100644 --- a/chess/syzygy.py +++ b/chess/syzygy.py @@ -761,7 +761,7 @@ def calc_symlen(self, d: PairsData, s: int, tmp: List[int]) -> None: d.symlen[s] = d.symlen[s1] + d.symlen[s2] + 1 tmp[s] = 1 - def pawn_file(self, pos: List[chess.Square]) -> int: + def pawn_file(self, pos: List[chess.Square]) -> chess.File: for i in range(1, self.pawns[0]): if FLAP[pos[0]] > FLAP[pos[i]]: pos[0], pos[i] = pos[i], pos[0] From bd8074d20e7aa667315b54470d0f3aae6390e69d Mon Sep 17 00:00:00 2001 From: Jackson Hall Date: Sat, 4 Oct 2025 20:47:45 -0400 Subject: [PATCH 11/27] Add `parse_file`/`parse_rank`, `file_name`/rank_name` --- chess/__init__.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/chess/__init__.py b/chess/__init__.py index 3d3dad4f0..84bfa632a 100644 --- a/chess/__init__.py +++ b/chess/__init__.py @@ -275,6 +275,32 @@ def square(file_index: File, rank_index: Rank) -> Square: """Gets a square number by file and rank index.""" return rank_index * 8 + file_index +def parse_file(name: str) -> File: + """ + Gets the file index for the given file *name* + (e.g., ``a`` returns ``0``). + + :raises: :exc:`ValueError` if the file name is invalid. + """ + return FILE_NAMES.index(name) + +def file_name(file: File) -> str: + """Gets the name of the file, like ``a``.""" + return FILE_NAMES[file] + +def parse_rank(name: str) -> File: + """ + Gets the rank index for the given rank *name* + (e.g., ``1`` returns ``0``). + + :raises: :exc:`ValueError` if the rank name is invalid. + """ + return FILE_NAMES.index(name) + +def rank_name(rank: Rank) -> str: + """Gets the name of the rank, like ``1``.""" + return FILE_NAMES[rank] + def square_file(square: Square) -> File: """Gets the file index of the square where ``0`` is the a-file.""" return square & 7 From 376d603694913a82bcb2efa594972a5fba5804f6 Mon Sep 17 00:00:00 2001 From: Niklas Fiekas Date: Sat, 11 Oct 2025 09:20:54 +0200 Subject: [PATCH 12/27] Explicitly support Python 3.14 --- .github/workflows/test.yml | 24 ++++++++++++------------ setup.py | 1 + tox.ini | 2 +- 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9eca0fce3..6b31bce24 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,11 +10,11 @@ jobs: strategy: matrix: os: [ubuntu-latest, windows-latest] - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v4 + - uses: actions/checkout@v5 + - uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} - run: .github/workflows/setup-${{ matrix.os }}.sh @@ -24,10 +24,10 @@ jobs: perft: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v4 + - uses: actions/checkout@v5 + - uses: actions/setup-python@v6 with: - python-version: "3.13" + python-version: "3.14" - run: pip install -e . - run: python examples/perft/perft.py -t 1 examples/perft/random.perft --max-nodes 10000 - run: python examples/perft/perft.py -t 1 examples/perft/chess960.perft --max-nodes 100000 @@ -42,11 +42,11 @@ jobs: strategy: matrix: os: [ubuntu-latest, windows-latest] - python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v4 + - uses: actions/checkout@v5 + - uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} - run: pip install -e . @@ -59,10 +59,10 @@ jobs: readme: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v4 + - uses: actions/checkout@v5 + - uses: actions/setup-python@v6 with: - python-version: "3.13" + python-version: "3.14" - run: sudo apt-get update && sudo apt-get install -y docutils-common - run: python setup.py --long-description | rst2html --strict --no-raw > /dev/null - run: pip install -e . diff --git a/setup.py b/setup.py index 9d6aa8c58..5815947cd 100755 --- a/setup.py +++ b/setup.py @@ -84,6 +84,7 @@ def read_description(): "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Topic :: Games/Entertainment :: Board Games", "Topic :: Games/Entertainment :: Turn Based Strategy", "Topic :: Software Development :: Libraries :: Python Modules", diff --git a/tox.ini b/tox.ini index 5970b2c79..493a46feb 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py38,py39,py310,py311,py312,py313 +envlist = py38,py39,py310,py311,py312,py313,py314 [testenv] passenv = LD_LIBRARY_PATH From e4386c2f1efcb686c1d6222681cc84d1f0b06ded Mon Sep 17 00:00:00 2001 From: Niklas Fiekas Date: Sat, 11 Oct 2025 15:25:33 +0200 Subject: [PATCH 13/27] Remove chess.engine.DefaultEventLoopPolicy (breaking change forced by Python) --- chess/engine.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/chess/engine.py b/chess/engine.py index b979b278f..8482c31ba 100644 --- a/chess/engine.py +++ b/chess/engine.py @@ -50,10 +50,6 @@ def override(fn: F, /) -> F: MANAGED_OPTIONS = ["uci_chess960", "uci_variant", "multipv", "ponder"] -# No longer needed, but alias kept around for compatibility. -EventLoopPolicy = asyncio.DefaultEventLoopPolicy - - def run_in_background(coroutine: Callable[[concurrent.futures.Future[T]], Coroutine[Any, Any, None]], *, name: Optional[str] = None, debug: Optional[bool] = None) -> T: """ Runs ``coroutine(future)`` in a new event loop on a background thread. From e974a37e52a59709a0988872a12c1f01244a8c15 Mon Sep 17 00:00:00 2001 From: Niklas Fiekas Date: Sat, 11 Oct 2025 15:30:39 +0200 Subject: [PATCH 14/27] Fix test_sf_forced_mates() failing due to ambiguous mate --- test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test.py b/test.py index c120c049f..2ebd357d0 100755 --- a/test.py +++ b/test.py @@ -3079,7 +3079,7 @@ def test_sf_forced_mates(self): for epd in epds: operations = board.set_epd(epd) - result = engine.play(board, chess.engine.Limit(mate=5), game=object()) + result = engine.play(board, chess.engine.Limit(mate=3), game=object()) self.assertIn(result.move, operations["bm"], operations["id"]) @catchAndSkip(FileNotFoundError, "need stockfish") From 8412bd56a282f7fe7071a8b1788b6b791d5e7b0e Mon Sep 17 00:00:00 2001 From: Niklas Fiekas Date: Sat, 11 Oct 2025 15:33:30 +0200 Subject: [PATCH 15/27] asyncio.iscoroutinefunction() -> inspect.iscoroutinefunction() --- chess/engine.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/chess/engine.py b/chess/engine.py index 8482c31ba..913940190 100644 --- a/chess/engine.py +++ b/chess/engine.py @@ -8,6 +8,7 @@ import copy import dataclasses import enum +import inspect import logging import math import shlex @@ -58,7 +59,7 @@ def run_in_background(coroutine: Callable[[concurrent.futures.Future[T]], Corout The coroutine and all remaining tasks continue running in the background until complete. """ - assert asyncio.iscoroutinefunction(coroutine) + assert inspect.iscoroutinefunction(coroutine) future: concurrent.futures.Future[T] = concurrent.futures.Future() From 624d3a730c180e749ea04a473a828b9c31ff52a4 Mon Sep 17 00:00:00 2001 From: Niklas Fiekas Date: Sat, 11 Oct 2025 15:38:19 +0200 Subject: [PATCH 16/27] Do not fail-fast matrix jobs that may have interesting results --- .github/workflows/test.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6b31bce24..9d0880bb4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,6 +11,7 @@ jobs: matrix: os: [ubuntu-latest, windows-latest] python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] + fail-fast: false runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v5 @@ -43,6 +44,7 @@ jobs: matrix: os: [ubuntu-latest, windows-latest] python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] + fail-fast: false runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 From 4d9b3bfd860bfa95731d4e208fd98c7c10a15533 Mon Sep 17 00:00:00 2001 From: Niklas Fiekas Date: Mon, 27 Oct 2025 21:24:16 +0100 Subject: [PATCH 17/27] Fix Gaviota tables opened as writable (fixes #1166) --- chess/gaviota.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chess/gaviota.py b/chess/gaviota.py index c352a27ca..8beb18d4d 100644 --- a/chess/gaviota.py +++ b/chess/gaviota.py @@ -1663,7 +1663,7 @@ def _open_tablebase(self, req: Request) -> BinaryIO: if stream is None: path = self.available_tables[req.egkey] - stream = open(path, "rb+") + stream = open(path, "rb") self.egtb_loadindexes(req.egkey, stream) self.streams[req.egkey] = stream From f8575f962caf3fcddcf95d935d1d75a1cceb586b Mon Sep 17 00:00:00 2001 From: Jackson Hall Date: Sun, 2 Nov 2025 18:46:03 -0500 Subject: [PATCH 18/27] Add `Board.gives_checkmate()` --- chess/__init__.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/chess/__init__.py b/chess/__init__.py index 84bfa632a..b51c31ca3 100644 --- a/chess/__init__.py +++ b/chess/__init__.py @@ -1980,6 +1980,17 @@ def gives_check(self, move: Move) -> bool: finally: self.pop() + def gives_checkmate(self, move: Move) -> bool: + """ + Probes if the given move would put the opponent in checkmate. The move + must be at least pseudo-legal. + """ + self.push(move) + try: + return self.is_checkmate() + finally: + self.pop() + def is_into_check(self, move: Move) -> bool: king = self.king(self.turn) if king is None: From d59bad55df4b8759e53b0ee8673ec8e69c5f5a82 Mon Sep 17 00:00:00 2001 From: johndoknjas Date: Fri, 2 Jan 2026 10:02:39 -0800 Subject: [PATCH 19/27] Ensure hash is always set after threads. --- chess/engine.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/chess/engine.py b/chess/engine.py index 913940190..72b579dc9 100644 --- a/chess/engine.py +++ b/chess/engine.py @@ -1875,12 +1875,16 @@ def _parse_uci_bestmove(board: chess.Board, args: str) -> BestMove: return BestMove(move, ponder) -def _chain_config(a: ConfigMapping, b: ConfigMapping) -> Iterator[Tuple[str, ConfigValue]]: - for name, value in a.items(): +def _chain_config(a: ConfigMapping, b: ConfigMapping, with_hash_reordering: bool = True) -> Iterator[Tuple[str, ConfigValue]]: + merged = dict(a) + for k, v in b.items(): + merged.setdefault(k, v) + if with_hash_reordering and 'Hash' in merged and 'Threads' in merged: + hash_val = merged['Hash'] + del merged['Hash'] + merged['Hash'] = hash_val + for name, value in merged.items(): yield name, value - for name, value in b.items(): - if name not in a: - yield name, value class UciOptionMap(MutableMapping[str, T]): From a5bbe3ea49f04b6a153efd278d1d17b073a11fc0 Mon Sep 17 00:00:00 2001 From: johndoknjas Date: Fri, 2 Jan 2026 20:46:19 -0800 Subject: [PATCH 20/27] Remove optional parameter. --- chess/engine.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/chess/engine.py b/chess/engine.py index 72b579dc9..0a9af75d9 100644 --- a/chess/engine.py +++ b/chess/engine.py @@ -1875,11 +1875,11 @@ def _parse_uci_bestmove(board: chess.Board, args: str) -> BestMove: return BestMove(move, ponder) -def _chain_config(a: ConfigMapping, b: ConfigMapping, with_hash_reordering: bool = True) -> Iterator[Tuple[str, ConfigValue]]: +def _chain_config(a: ConfigMapping, b: ConfigMapping) -> Iterator[Tuple[str, ConfigValue]]: merged = dict(a) for k, v in b.items(): merged.setdefault(k, v) - if with_hash_reordering and 'Hash' in merged and 'Threads' in merged: + if 'Hash' in merged and 'Threads' in merged: hash_val = merged['Hash'] del merged['Hash'] merged['Hash'] = hash_val From a28315bbfe31410120e97aa2f2fe56a19043e242 Mon Sep 17 00:00:00 2001 From: Niklas Fiekas Date: Sat, 3 Jan 2026 12:29:26 +0100 Subject: [PATCH 21/27] Comment Hash after Threads --- chess/engine.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/chess/engine.py b/chess/engine.py index 0a9af75d9..c66bc0c45 100644 --- a/chess/engine.py +++ b/chess/engine.py @@ -1875,16 +1875,16 @@ def _parse_uci_bestmove(board: chess.Board, args: str) -> BestMove: return BestMove(move, ponder) -def _chain_config(a: ConfigMapping, b: ConfigMapping) -> Iterator[Tuple[str, ConfigValue]]: +def _chain_config(a: ConfigMapping, b: ConfigMapping) -> Iterable[Tuple[str, ConfigValue]]: merged = dict(a) for k, v in b.items(): merged.setdefault(k, v) - if 'Hash' in merged and 'Threads' in merged: - hash_val = merged['Hash'] - del merged['Hash'] - merged['Hash'] = hash_val - for name, value in merged.items(): - yield name, value + if "Hash" in merged and "Threads" in merged: + # Move Hash after Threads, as recommended by Stockfish. + hash_val = merged["Hash"] + del merged["Hash"] + merged["Hash"] = hash_val + return merged.items() class UciOptionMap(MutableMapping[str, T]): From 76cbe9843b7be94676cf19ea2a446e4eb3ac4291 Mon Sep 17 00:00:00 2001 From: winapiadmin <138602885+winapiadmin@users.noreply.github.com> Date: Fri, 13 Feb 2026 21:41:52 +0000 Subject: [PATCH 22/27] Multiple kings per color (was_into_check and more affected) (#1179) * fixed multiple kings for was_into_check * fixed king() behavior on multiple kings minus prev commit * add test cases (very little) * using the precomputed king mask * removed testcase of was_into_check() on multiple kings... ... because it would cause the function to return False (because king() didn't detect any king because of decision) --- chess/__init__.py | 2 +- test.py | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/chess/__init__.py b/chess/__init__.py index b51c31ca3..347f22ea1 100644 --- a/chess/__init__.py +++ b/chess/__init__.py @@ -916,7 +916,7 @@ def king(self, color: Color) -> Optional[Square]: considered. """ king_mask = self.occupied_co[color] & self.kings & ~self.promoted - return msb(king_mask) if king_mask else None + return msb(king_mask) if king_mask and popcount(king_mask) == 1 else None def attacks_mask(self, square: Square) -> Bitboard: bb_square = BB_SQUARES[square] diff --git a/test.py b/test.py index 2ebd357d0..4927a2b80 100755 --- a/test.py +++ b/test.py @@ -1720,6 +1720,10 @@ def test_impossible_check_due_to_en_passant(self): self.assertFalse(board.has_legal_en_passant()) self.assertEqual(len(list(board.legal_moves)), 2) + def test_multiple_kings(self): + board = chess.Board("KKKK1kkk/8/8/8/8/8/8/8 w - - 0 1") + self.assertEqual(board.king(chess.WHITE), None) + class LegalMoveGeneratorTestCase(unittest.TestCase): From 312f3bf07758628e4ee9befbd9e3df7dd5eccea6 Mon Sep 17 00:00:00 2001 From: Niklas Fiekas Date: Fri, 13 Feb 2026 22:52:23 +0100 Subject: [PATCH 23/27] Introduce Board._effective_promoted() --- chess/__init__.py | 55 ++++++++++++++++++++++++++++------------------- chess/variant.py | 27 +++++++---------------- 2 files changed, 41 insertions(+), 41 deletions(-) diff --git a/chess/__init__.py b/chess/__init__.py index 347f22ea1..7fe4cb9cc 100644 --- a/chess/__init__.py +++ b/chess/__init__.py @@ -907,16 +907,19 @@ def color_at(self, square: Square) -> Optional[Color]: else: return None + def _effective_promoted(self) -> Bitboard: + return BB_EMPTY + def king(self, color: Color) -> Optional[Square]: """ - Finds the king square of the given side. Returns ``None`` if there - is no king of that color. + Finds the unique king square of the given side. Returns ``None`` if + there is no king or multiple kings of that color. In variants with king promotions, only non-promoted kings are considered. """ - king_mask = self.occupied_co[color] & self.kings & ~self.promoted - return msb(king_mask) if king_mask and popcount(king_mask) == 1 else None + king_mask = self.occupied_co[color] & self.kings & ~self._effective_promoted() + return msb(king_mask) if king_mask and not king_mask & (king_mask - 1) else None def attacks_mask(self, square: Square) -> Bitboard: bb_square = BB_SQUARES[square] @@ -1135,7 +1138,7 @@ def set_piece_at(self, square: Square, piece: Optional[Piece], promoted: bool = else: self._set_piece_at(square, piece.piece_type, piece.color, promoted) - def board_fen(self, *, promoted: Optional[bool] = False) -> str: + def board_fen(self, *, promoted: Optional[bool] = None) -> str: """ Gets the board FEN (e.g., ``rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR``). @@ -1153,7 +1156,14 @@ def board_fen(self, *, promoted: Optional[bool] = False) -> str: builder.append(str(empty)) empty = 0 builder.append(piece.symbol()) - if promoted and BB_SQUARES[square] & self.promoted: + + if promoted is None: + promoted_mask = self._effective_promoted() + elif promoted: + promoted_mask = self.promoted + else: + promoted_mask = BB_EMPTY + if BB_SQUARES[square] & promoted_mask: builder.append("~") if BB_SQUARES[square] & BB_FILE_H: @@ -1335,7 +1345,7 @@ def chess960_pos(self) -> Optional[int]: return None if self.pawns != BB_RANK_2 | BB_RANK_7: return None - if self.promoted: + if self._effective_promoted(): return None # Piece counts. @@ -2452,12 +2462,12 @@ def push(self, move: Move) -> None: # Update castling rights. self.castling_rights &= ~to_bb & ~from_bb - if piece_type == KING and not promoted: + if piece_type == KING and not self._effective_promoted() & from_bb: if self.turn == WHITE: self.castling_rights &= ~BB_RANK_1 else: self.castling_rights &= ~BB_RANK_8 - elif captured_piece_type == KING and not self.promoted & to_bb: + elif captured_piece_type == KING and not self._effective_promoted() & to_bb: if self.turn == WHITE and square_rank(move.to_square) == RANK_8: self.castling_rights &= ~BB_RANK_8 elif self.turn == BLACK and square_rank(move.to_square) == RANK_1: @@ -3404,8 +3414,8 @@ def _reduces_castling_rights(self, move: Move) -> bool: cr = self.clean_castling_rights() touched = BB_SQUARES[move.from_square] ^ BB_SQUARES[move.to_square] return bool(touched & cr or - cr & BB_RANK_1 and touched & self.kings & self.occupied_co[WHITE] & ~self.promoted or - cr & BB_RANK_8 and touched & self.kings & self.occupied_co[BLACK] & ~self.promoted) + cr & BB_RANK_1 and touched & self.kings & self.occupied_co[WHITE] & ~self._effective_promoted() or + cr & BB_RANK_8 and touched & self.kings & self.occupied_co[BLACK] & ~self._effective_promoted()) def is_irreversible(self, move: Move) -> bool: """ @@ -3459,16 +3469,16 @@ def clean_castling_rights(self) -> Bitboard: black_castling &= (BB_A8 | BB_H8) # The kings must be on e1 or e8. - if not self.occupied_co[WHITE] & self.kings & ~self.promoted & BB_E1: + if not self.occupied_co[WHITE] & self.kings & ~self._effective_promoted() & BB_E1: white_castling = 0 - if not self.occupied_co[BLACK] & self.kings & ~self.promoted & BB_E8: + if not self.occupied_co[BLACK] & self.kings & ~self._effective_promoted() & BB_E8: black_castling = 0 return white_castling | black_castling else: # The kings must be on the back rank. - white_king_mask = self.occupied_co[WHITE] & self.kings & BB_RANK_1 & ~self.promoted - black_king_mask = self.occupied_co[BLACK] & self.kings & BB_RANK_8 & ~self.promoted + white_king_mask = self.occupied_co[WHITE] & self.kings & BB_RANK_1 & ~self._effective_promoted() + black_king_mask = self.occupied_co[BLACK] & self.kings & BB_RANK_8 & ~self._effective_promoted() if not white_king_mask: white_castling = 0 if not black_king_mask: @@ -3506,7 +3516,7 @@ def has_kingside_castling_rights(self, color: Color) -> bool: castling rights. """ backrank = BB_RANK_1 if color == WHITE else BB_RANK_8 - king_mask = self.kings & self.occupied_co[color] & backrank & ~self.promoted + king_mask = self.kings & self.occupied_co[color] & backrank & ~self._effective_promoted() if not king_mask: return False @@ -3527,7 +3537,7 @@ def has_queenside_castling_rights(self, color: Color) -> bool: castling rights. """ backrank = BB_RANK_1 if color == WHITE else BB_RANK_8 - king_mask = self.kings & self.occupied_co[color] & backrank & ~self.promoted + king_mask = self.kings & self.occupied_co[color] & backrank & ~self._effective_promoted() if not king_mask: return False @@ -3600,11 +3610,11 @@ def status(self) -> Status: errors |= STATUS_EMPTY # There must be exactly one king of each color. - if not self.occupied_co[WHITE] & self.kings: + if not self.occupied_co[WHITE] & self.kings & ~self._effective_promoted(): errors |= STATUS_NO_WHITE_KING - if not self.occupied_co[BLACK] & self.kings: + if not self.occupied_co[BLACK] & self.kings & ~self._effective_promoted(): errors |= STATUS_NO_BLACK_KING - if popcount(self.occupied & self.kings) > 2: + if popcount(self.occupied & self.kings & ~self._effective_promoted()) > 2: errors |= STATUS_TOO_MANY_KINGS # There can not be more than 16 pieces of any color. @@ -3638,7 +3648,7 @@ def status(self) -> Status: # More than the maximum number of possible checkers in the variant. checkers = self.checkers_mask() - our_kings = self.kings & self.occupied_co[self.turn] & ~self.promoted + our_kings = self.kings & self.occupied_co[self.turn] & ~self._effective_promoted() if checkers: if popcount(checkers) > 2: errors |= STATUS_TOO_MANY_CHECKERS @@ -3822,7 +3832,7 @@ def generate_castling_moves(self, from_mask: Bitboard = BB_ALL, to_mask: Bitboar return backrank = BB_RANK_1 if self.turn == WHITE else BB_RANK_8 - king = self.occupied_co[self.turn] & self.kings & ~self.promoted & backrank & from_mask + king = self.occupied_co[self.turn] & self.kings & ~self._effective_promoted() & backrank & from_mask king &= -king if not king: return @@ -3879,6 +3889,7 @@ def _to_chess960(self, move: Move) -> Move: def _transposition_key(self) -> Hashable: return (self.pawns, self.knights, self.bishops, self.rooks, self.queens, self.kings, + self._effective_promoted(), self.occupied_co[WHITE], self.occupied_co[BLACK], self.turn, self.clean_castling_rights(), self.ep_square if self.has_legal_en_passant() else None) diff --git a/chess/variant.py b/chess/variant.py index 6e9161dc8..ba4c0f1ce 100644 --- a/chess/variant.py +++ b/chess/variant.py @@ -127,16 +127,8 @@ def is_legal(self, move: chess.Move) -> bool: else: return not any(self.generate_pseudo_legal_captures()) - def _transposition_key(self) -> Hashable: - if self.has_chess960_castling_rights(): - return (super()._transposition_key(), self.kings & self.promoted) - else: - return super()._transposition_key() - - def board_fen(self, *, promoted: Optional[bool] = None) -> str: - if promoted is None: - promoted = self.has_chess960_castling_rights() - return super().board_fen(promoted=promoted) + def _effective_promoted(self) -> chess.Bitboard: + return self.kings & self.promoted if self.castling_rights else chess.BB_EMPTY def status(self) -> chess.Status: status = super().status() @@ -261,9 +253,9 @@ def _push_capture(self, move: chess.Move, capture_square: chess.Square, piece_ty # Destroy castling rights. self.castling_rights &= ~explosion_radius - if explosion_radius & self.kings & self.occupied_co[chess.WHITE] & ~self.promoted: + if explosion_radius & self.kings & self.occupied_co[chess.WHITE] & ~self._effective_promoted(): self.castling_rights &= ~chess.BB_RANK_1 - if explosion_radius & self.kings & self.occupied_co[chess.BLACK] & ~self.promoted: + if explosion_radius & self.kings & self.occupied_co[chess.BLACK] & ~self._effective_promoted(): self.castling_rights &= ~chess.BB_RANK_8 # Explode the capturing piece. @@ -930,9 +922,11 @@ def _is_halfmoves(self, n: int) -> bool: def is_irreversible(self, move: chess.Move) -> bool: return self._reduces_castling_rights(move) + def _effective_promoted(self) -> chess.Bitboard: + return self.promoted & ~self.kings & ~self.pawns + def _transposition_key(self) -> Hashable: return (super()._transposition_key(), - self.promoted, str(self.pockets[chess.WHITE]), str(self.pockets[chess.BLACK])) def legal_drop_squares_mask(self) -> chess.Bitboard: @@ -1009,7 +1003,7 @@ def has_insufficient_material(self, color: chess.Color) -> bool: # a different color complex. return ( chess.popcount(self.occupied) + sum(len(pocket) for pocket in self.pockets) <= 3 and - not self.promoted and + not self._effective_promoted() and not self.pawns and not self.rooks and not self.queens and @@ -1041,11 +1035,6 @@ def set_fen(self, fen: str) -> None: self.pockets[chess.WHITE] = white_pocket self.pockets[chess.BLACK] = black_pocket - def board_fen(self, *, promoted: Optional[bool] = None) -> str: - if promoted is None: - promoted = True - return super().board_fen(promoted=promoted) - def epd(self, shredder: bool = False, en_passant: chess.EnPassantSpec = "legal", promoted: Optional[bool] = None, **operations: Union[None, str, int, float, chess.Move, Iterable[chess.Move]]) -> str: epd = super().epd(shredder=shredder, en_passant=en_passant, promoted=promoted) board_part, info_part = epd.split(" ", 1) From fc50a27fa3cfa07243f78eca93ea6126347ea1fa Mon Sep 17 00:00:00 2001 From: Christopher Akiki Date: Fri, 13 Mar 2026 17:49:05 +0100 Subject: [PATCH 24/27] [MINOR:TYPO] Update pgn.py instanciate -> instantiate --- chess/pgn.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chess/pgn.py b/chess/pgn.py index f40980d48..5ae5b43b0 100644 --- a/chess/pgn.py +++ b/chess/pgn.py @@ -402,7 +402,7 @@ def remove_variation(self, move: Union[int, chess.Move, GameNode]) -> None: def add_variation(self, move: chess.Move, *, comment: Union[str, list[str]] = "", starting_comment: Union[str, list[str]] = "", nags: Iterable[int] = []) -> ChildNode: """Creates a child node with the given attributes.""" - # Instanciate ChildNode only in this method. + # Instantiate ChildNode only in this method. return ChildNode(self, move, comment=comment, starting_comment=starting_comment, nags=nags) def add_main_variation(self, move: chess.Move, *, comment: str = "", nags: Iterable[int] = []) -> ChildNode: From 5e2a2bc153b7646497f3e811f3cfd28aaca1b1ea Mon Sep 17 00:00:00 2001 From: Cady Date: Mon, 30 Mar 2026 16:21:49 -0400 Subject: [PATCH 25/27] Fixed typo in README.rst, issue 1183 --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index a6c3185c6..d4d51a128 100644 --- a/README.rst +++ b/README.rst @@ -314,7 +314,7 @@ If you like, share interesting things you are using python-chess for, for exampl | .. image:: https://github.com/niklasf/python-chess/blob/master/docs/images/clente-chess.png?raw=true | `clente/chess `_ | | :height: 64 | | | :width: 64 | | -| :target: https://github.com/clente/chess | Oppinionated wrapper to use python-chess from the R programming language | +| :target: https://github.com/clente/chess | Opinionated wrapper to use python-chess from the R programming language | +------------------------------------------------------------------------------------------------------+----------------------------------------------------------------------------------------------+ | .. image:: https://github.com/niklasf/python-chess/blob/master/docs/images/crazyara.png?raw=true | https://crazyara.org/ | | :height: 64 | | From efb8b4278c85e0145b29b867de03dc715456dd86 Mon Sep 17 00:00:00 2001 From: Litschi Date: Fri, 27 Mar 2026 19:31:11 +0100 Subject: [PATCH 26/27] feat: added piece_count function instead of chess.popcount(board.occupied) --- CHANGELOG.rst | 9 +++++++++ chess/__init__.py | 3 +++ chess/gaviota.py | 8 ++++---- chess/syzygy.py | 10 +++++----- test.py | 2 +- 5 files changed, 22 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 03a9555d1..212db89b1 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,15 @@ Changelog for python-chess ========================== +New in unreleased (27th Mar 2026) +--------------------------------- + +Bugfixes: +* Fixed typo in README.rst. + +Changes: +* Added ``board.piece_count`` function. + New in v1.11.2 (25th Feb 2025) ------------------------------ diff --git a/chess/__init__.py b/chess/__init__.py index 7fe4cb9cc..12b249bd5 100644 --- a/chess/__init__.py +++ b/chess/__init__.py @@ -841,6 +841,9 @@ def clear_board(self) -> None: :class:`~chess.Board` also clears the move stack. """ self._clear_board() + + def piece_count(self) -> int: + return popcount(self.occupied) def pieces_mask(self, piece_type: PieceType, color: Color) -> Bitboard: if piece_type == PAWN: diff --git a/chess/gaviota.py b/chess/gaviota.py index 8beb18d4d..7152a18f0 100644 --- a/chess/gaviota.py +++ b/chess/gaviota.py @@ -1519,8 +1519,8 @@ def probe_dtm(self, board: chess.Board) -> int: raise KeyError(f"gaviota tables do not contain positions with castling rights: {board.fen()}") # Supports only up to 5 pieces. - if chess.popcount(board.occupied) > 5: - raise KeyError(f"gaviota tables support up to 5 pieces, not {chess.popcount(board.occupied)}: {board.fen()}") + if board.piece_count() > 5: + raise KeyError(f"gaviota tables support up to 5 pieces, not {board.piece_count()}: {board.fen()}") # KvK is a draw. if board.occupied == board.kings: @@ -1885,8 +1885,8 @@ def _probe_hard(self, board: chess.Board, wdl_only: bool = False) -> int: if board.castling_rights: raise KeyError(f"gaviota tables do not contain positions with castling rights: {board.fen()}") - if chess.popcount(board.occupied) > 5: - raise KeyError(f"gaviota tables support up to 5 pieces, not {chess.popcount(board.occupied)}: {board.fen()}") + if board.piece_count() > 5: + raise KeyError(f"gaviota tables support up to 5 pieces, not {board.piece_count()}: {board.fen()}") stm = ctypes.c_uint(0 if board.turn == chess.WHITE else 1) ep_square = ctypes.c_uint(board.ep_square if board.ep_square else 64) diff --git a/chess/syzygy.py b/chess/syzygy.py index 0c6b7822c..2250db5b5 100644 --- a/chess/syzygy.py +++ b/chess/syzygy.py @@ -1548,8 +1548,8 @@ def probe_wdl_table(self, board: chess.Board) -> int: try: table = typing.cast(WdlTable, self.wdl[key]) except KeyError: - if chess.popcount(board.occupied) > TBPIECES: - raise KeyError(f"syzygy tables support up to {TBPIECES} pieces, not {chess.popcount(board.occupied)}: {board.fen()}") + if board.piece_count() > TBPIECES: + raise KeyError(f"syzygy tables support up to {TBPIECES} pieces, not {board.piece_count()}: {board.fen()}") raise MissingTableError(f"did not find wdl table {key}") self._bump_lru(table) @@ -1567,8 +1567,8 @@ def probe_ab(self, board: chess.Board, alpha: int, beta: int, threats: bool = Fa # positions that have more pieces than the maximum number of supported # pieces. We artificially limit this to one additional level, to # make sure search remains somewhat bounded. - if chess.popcount(board.occupied) > TBPIECES + 1: - raise KeyError(f"syzygy tables support up to {TBPIECES} pieces, not {chess.popcount(board.occupied)}: {board.fen()}") + if board.piece_count() > TBPIECES + 1: + raise KeyError(f"syzygy tables support up to {TBPIECES} pieces, not {board.piece_count()}: {board.fen()}") # Special case: Variant with compulsory captures. if self.variant.captures_compulsory: @@ -1613,7 +1613,7 @@ def sprobe_ab(self, board: chess.Board, alpha: int, beta: int, threats: bool = F threats_found = False - if threats or chess.popcount(board.occupied) >= 6: + if threats or board.piece_count() >= 6: for threat in board.generate_legal_moves(~board.pawns): board.push(threat) try: diff --git a/test.py b/test.py index 4927a2b80..228c62f3f 100755 --- a/test.py +++ b/test.py @@ -1220,7 +1220,7 @@ def test_clear(self): self.assertFalse(board.ep_square) self.assertFalse(board.piece_at(chess.E1)) - self.assertEqual(chess.popcount(board.occupied), 0) + self.assertEqual(board.piece_count(), 0) def test_threefold_repetition(self): board = chess.Board() From a345dbd131fb5cbcbffdc9e50901d359480926c5 Mon Sep 17 00:00:00 2001 From: Niklas Fiekas Date: Fri, 3 Apr 2026 21:24:06 +0200 Subject: [PATCH 27/27] Document board.piece_count() --- CHANGELOG.rst | 9 --------- chess/__init__.py | 7 ++++++- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 212db89b1..03a9555d1 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,15 +1,6 @@ Changelog for python-chess ========================== -New in unreleased (27th Mar 2026) ---------------------------------- - -Bugfixes: -* Fixed typo in README.rst. - -Changes: -* Added ``board.piece_count`` function. - New in v1.11.2 (25th Feb 2025) ------------------------------ diff --git a/chess/__init__.py b/chess/__init__.py index 12b249bd5..9ea44f36e 100644 --- a/chess/__init__.py +++ b/chess/__init__.py @@ -841,8 +841,13 @@ def clear_board(self) -> None: :class:`~chess.Board` also clears the move stack. """ self._clear_board() - + def piece_count(self) -> int: + """ + Gets the number of pieces on the board. + + Does not include Crazyhouse pockets. + """ return popcount(self.occupied) def pieces_mask(self, piece_type: PieceType, color: Color) -> Bitboard: