4158 lines
147 KiB
Python
4158 lines
147 KiB
Python
|
# This file is part of the python-chess library.
|
||
|
# Copyright (C) 2012-2021 Niklas Fiekas <niklas.fiekas@backscattering.de>
|
||
|
#
|
||
|
# This program is free software: you can redistribute it and/or modify
|
||
|
# it under the terms of the GNU General Public License as published by
|
||
|
# the Free Software Foundation, either version 3 of the License, or
|
||
|
# (at your option) any later version.
|
||
|
#
|
||
|
# This program is distributed in the hope that it will be useful,
|
||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
|
# GNU General Public License for more details.
|
||
|
#
|
||
|
# You should have received a copy of the GNU General Public License
|
||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||
|
|
||
|
"""
|
||
|
A chess library with move generation and validation,
|
||
|
Polyglot opening book probing, PGN reading and writing,
|
||
|
Gaviota tablebase probing,
|
||
|
Syzygy tablebase probing, and XBoard/UCI engine communication.
|
||
|
"""
|
||
|
|
||
|
from __future__ import annotations
|
||
|
|
||
|
__author__ = "Niklas Fiekas"
|
||
|
|
||
|
__email__ = "niklas.fiekas@backscattering.de"
|
||
|
|
||
|
__version__ = "1.9.4"
|
||
|
|
||
|
import collections
|
||
|
import copy
|
||
|
import dataclasses
|
||
|
import enum
|
||
|
import math
|
||
|
import re
|
||
|
import itertools
|
||
|
import typing
|
||
|
|
||
|
from typing import ClassVar, Callable, Counter, Dict, Generic, Hashable, Iterable, Iterator, List, Mapping, Optional, SupportsInt, Tuple, Type, TypeVar, Union
|
||
|
|
||
|
try:
|
||
|
from typing import Literal
|
||
|
_EnPassantSpec = Literal["legal", "fen", "xfen"]
|
||
|
except ImportError:
|
||
|
# Before Python 3.8.
|
||
|
_EnPassantSpec = str # type: ignore
|
||
|
|
||
|
|
||
|
Color = bool
|
||
|
COLORS = [WHITE, BLACK] = [True, False]
|
||
|
COLOR_NAMES = ["black", "white"]
|
||
|
|
||
|
PieceType = int
|
||
|
PIECE_TYPES = [PAWN, KNIGHT, BISHOP, ROOK, QUEEN, KING] = range(1, 7)
|
||
|
PIECE_SYMBOLS = [None, "p", "n", "b", "r", "q", "k"]
|
||
|
PIECE_NAMES = [None, "pawn", "knight", "bishop", "rook", "queen", "king"]
|
||
|
|
||
|
def piece_symbol(piece_type: PieceType) -> str:
|
||
|
return typing.cast(str, PIECE_SYMBOLS[piece_type])
|
||
|
|
||
|
def piece_name(piece_type: PieceType) -> str:
|
||
|
return typing.cast(str, PIECE_NAMES[piece_type])
|
||
|
|
||
|
UNICODE_PIECE_SYMBOLS = {
|
||
|
"R": "♖", "r": "♜",
|
||
|
"N": "♘", "n": "♞",
|
||
|
"B": "♗", "b": "♝",
|
||
|
"Q": "♕", "q": "♛",
|
||
|
"K": "♔", "k": "♚",
|
||
|
"P": "♙", "p": "♟",
|
||
|
}
|
||
|
|
||
|
FILE_NAMES = ["a", "b", "c", "d", "e", "f", "g", "h"]
|
||
|
|
||
|
RANK_NAMES = ["1", "2", "3", "4", "5", "6", "7", "8"]
|
||
|
|
||
|
STARTING_FEN = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"
|
||
|
"""The FEN for the standard chess starting position."""
|
||
|
|
||
|
STARTING_BOARD_FEN = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR"
|
||
|
"""The board part of the FEN for the standard chess starting position."""
|
||
|
|
||
|
|
||
|
class Status(enum.IntFlag):
|
||
|
VALID = 0
|
||
|
NO_WHITE_KING = 1 << 0
|
||
|
NO_BLACK_KING = 1 << 1
|
||
|
TOO_MANY_KINGS = 1 << 2
|
||
|
TOO_MANY_WHITE_PAWNS = 1 << 3
|
||
|
TOO_MANY_BLACK_PAWNS = 1 << 4
|
||
|
PAWNS_ON_BACKRANK = 1 << 5
|
||
|
TOO_MANY_WHITE_PIECES = 1 << 6
|
||
|
TOO_MANY_BLACK_PIECES = 1 << 7
|
||
|
BAD_CASTLING_RIGHTS = 1 << 8
|
||
|
INVALID_EP_SQUARE = 1 << 9
|
||
|
OPPOSITE_CHECK = 1 << 10
|
||
|
EMPTY = 1 << 11
|
||
|
RACE_CHECK = 1 << 12
|
||
|
RACE_OVER = 1 << 13
|
||
|
RACE_MATERIAL = 1 << 14
|
||
|
TOO_MANY_CHECKERS = 1 << 15
|
||
|
IMPOSSIBLE_CHECK = 1 << 16
|
||
|
|
||
|
STATUS_VALID = Status.VALID
|
||
|
STATUS_NO_WHITE_KING = Status.NO_WHITE_KING
|
||
|
STATUS_NO_BLACK_KING = Status.NO_BLACK_KING
|
||
|
STATUS_TOO_MANY_KINGS = Status.TOO_MANY_KINGS
|
||
|
STATUS_TOO_MANY_WHITE_PAWNS = Status.TOO_MANY_WHITE_PAWNS
|
||
|
STATUS_TOO_MANY_BLACK_PAWNS = Status.TOO_MANY_BLACK_PAWNS
|
||
|
STATUS_PAWNS_ON_BACKRANK = Status.PAWNS_ON_BACKRANK
|
||
|
STATUS_TOO_MANY_WHITE_PIECES = Status.TOO_MANY_WHITE_PIECES
|
||
|
STATUS_TOO_MANY_BLACK_PIECES = Status.TOO_MANY_BLACK_PIECES
|
||
|
STATUS_BAD_CASTLING_RIGHTS = Status.BAD_CASTLING_RIGHTS
|
||
|
STATUS_INVALID_EP_SQUARE = Status.INVALID_EP_SQUARE
|
||
|
STATUS_OPPOSITE_CHECK = Status.OPPOSITE_CHECK
|
||
|
STATUS_EMPTY = Status.EMPTY
|
||
|
STATUS_RACE_CHECK = Status.RACE_CHECK
|
||
|
STATUS_RACE_OVER = Status.RACE_OVER
|
||
|
STATUS_RACE_MATERIAL = Status.RACE_MATERIAL
|
||
|
STATUS_TOO_MANY_CHECKERS = Status.TOO_MANY_CHECKERS
|
||
|
STATUS_IMPOSSIBLE_CHECK = Status.IMPOSSIBLE_CHECK
|
||
|
|
||
|
|
||
|
class Termination(enum.Enum):
|
||
|
"""Enum with reasons for a game to be over."""
|
||
|
|
||
|
CHECKMATE = enum.auto()
|
||
|
"""See :func:`chess.Board.is_checkmate()`."""
|
||
|
STALEMATE = enum.auto()
|
||
|
"""See :func:`chess.Board.is_stalemate()`."""
|
||
|
INSUFFICIENT_MATERIAL = enum.auto()
|
||
|
"""See :func:`chess.Board.is_insufficient_material()`."""
|
||
|
SEVENTYFIVE_MOVES = enum.auto()
|
||
|
"""See :func:`chess.Board.is_seventyfive_moves()`."""
|
||
|
FIVEFOLD_REPETITION = enum.auto()
|
||
|
"""See :func:`chess.Board.is_fivefold_repetition()`."""
|
||
|
FIFTY_MOVES = enum.auto()
|
||
|
"""See :func:`chess.Board.can_claim_fifty_moves()`."""
|
||
|
THREEFOLD_REPETITION = enum.auto()
|
||
|
"""See :func:`chess.Board.can_claim_threefold_repetition()`."""
|
||
|
VARIANT_WIN = enum.auto()
|
||
|
"""See :func:`chess.Board.is_variant_win()`."""
|
||
|
VARIANT_LOSS = enum.auto()
|
||
|
"""See :func:`chess.Board.is_variant_loss()`."""
|
||
|
VARIANT_DRAW = enum.auto()
|
||
|
"""See :func:`chess.Board.is_variant_draw()`."""
|
||
|
|
||
|
@dataclasses.dataclass
|
||
|
class Outcome:
|
||
|
"""
|
||
|
Information about the outcome of an ended game, usually obtained from
|
||
|
:func:`chess.Board.outcome()`.
|
||
|
"""
|
||
|
|
||
|
termination: Termination
|
||
|
"""The reason for the game to have ended."""
|
||
|
|
||
|
winner: Optional[Color]
|
||
|
"""The winning color or ``None`` if drawn."""
|
||
|
|
||
|
def result(self) -> str:
|
||
|
"""Returns ``1-0``, ``0-1`` or ``1/2-1/2``."""
|
||
|
return "1/2-1/2" if self.winner is None else ("1-0" if self.winner else "0-1")
|
||
|
|
||
|
|
||
|
class InvalidMoveError(ValueError):
|
||
|
"""Raised when move notation is not syntactically valid"""
|
||
|
|
||
|
|
||
|
class IllegalMoveError(ValueError):
|
||
|
"""Raised when the attempted move is illegal in the current position"""
|
||
|
|
||
|
|
||
|
class AmbiguousMoveError(ValueError):
|
||
|
"""Raised when the attempted move is ambiguous in the current position"""
|
||
|
|
||
|
|
||
|
Square = int
|
||
|
SQUARES = [
|
||
|
A1, B1, C1, D1, E1, F1, G1, H1,
|
||
|
A2, B2, C2, D2, E2, F2, G2, H2,
|
||
|
A3, B3, C3, D3, E3, F3, G3, H3,
|
||
|
A4, B4, C4, D4, E4, F4, G4, H4,
|
||
|
A5, B5, C5, D5, E5, F5, G5, H5,
|
||
|
A6, B6, C6, D6, E6, F6, G6, H6,
|
||
|
A7, B7, C7, D7, E7, F7, G7, H7,
|
||
|
A8, B8, C8, D8, E8, F8, G8, H8,
|
||
|
] = range(64)
|
||
|
|
||
|
SQUARE_NAMES = [f + r for r in RANK_NAMES for f in FILE_NAMES]
|
||
|
|
||
|
def parse_square(name: str) -> Square:
|
||
|
"""
|
||
|
Gets the square index for the given square *name*
|
||
|
(e.g., ``a1`` returns ``0``).
|
||
|
|
||
|
:raises: :exc:`ValueError` if the square name is invalid.
|
||
|
"""
|
||
|
return SQUARE_NAMES.index(name)
|
||
|
|
||
|
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:
|
||
|
"""Gets a square number by file and rank index."""
|
||
|
return rank_index * 8 + file_index
|
||
|
|
||
|
def square_file(square: Square) -> int:
|
||
|
"""Gets the file index of the square where ``0`` is the a-file."""
|
||
|
return square & 7
|
||
|
|
||
|
def square_rank(square: Square) -> int:
|
||
|
"""Gets the rank index of the square where ``0`` is the first rank."""
|
||
|
return square >> 3
|
||
|
|
||
|
def square_distance(a: Square, b: Square) -> int:
|
||
|
"""
|
||
|
Gets the distance (i.e., the number of king steps) from square *a* to *b*.
|
||
|
"""
|
||
|
return max(abs(square_file(a) - square_file(b)), abs(square_rank(a) - square_rank(b)))
|
||
|
|
||
|
def square_mirror(square: Square) -> Square:
|
||
|
"""Mirrors the square vertically."""
|
||
|
return square ^ 0x38
|
||
|
|
||
|
SQUARES_180 = [square_mirror(sq) for sq in SQUARES]
|
||
|
|
||
|
|
||
|
Bitboard = int
|
||
|
BB_EMPTY = 0
|
||
|
BB_ALL = 0xffff_ffff_ffff_ffff
|
||
|
|
||
|
BB_SQUARES = [
|
||
|
BB_A1, BB_B1, BB_C1, BB_D1, BB_E1, BB_F1, BB_G1, BB_H1,
|
||
|
BB_A2, BB_B2, BB_C2, BB_D2, BB_E2, BB_F2, BB_G2, BB_H2,
|
||
|
BB_A3, BB_B3, BB_C3, BB_D3, BB_E3, BB_F3, BB_G3, BB_H3,
|
||
|
BB_A4, BB_B4, BB_C4, BB_D4, BB_E4, BB_F4, BB_G4, BB_H4,
|
||
|
BB_A5, BB_B5, BB_C5, BB_D5, BB_E5, BB_F5, BB_G5, BB_H5,
|
||
|
BB_A6, BB_B6, BB_C6, BB_D6, BB_E6, BB_F6, BB_G6, BB_H6,
|
||
|
BB_A7, BB_B7, BB_C7, BB_D7, BB_E7, BB_F7, BB_G7, BB_H7,
|
||
|
BB_A8, BB_B8, BB_C8, BB_D8, BB_E8, BB_F8, BB_G8, BB_H8,
|
||
|
] = [1 << sq for sq in SQUARES]
|
||
|
|
||
|
BB_CORNERS = BB_A1 | BB_H1 | BB_A8 | BB_H8
|
||
|
BB_CENTER = BB_D4 | BB_E4 | BB_D5 | BB_E5
|
||
|
|
||
|
BB_LIGHT_SQUARES = 0x55aa_55aa_55aa_55aa
|
||
|
BB_DARK_SQUARES = 0xaa55_aa55_aa55_aa55
|
||
|
|
||
|
BB_FILES = [
|
||
|
BB_FILE_A,
|
||
|
BB_FILE_B,
|
||
|
BB_FILE_C,
|
||
|
BB_FILE_D,
|
||
|
BB_FILE_E,
|
||
|
BB_FILE_F,
|
||
|
BB_FILE_G,
|
||
|
BB_FILE_H,
|
||
|
] = [0x0101_0101_0101_0101 << i for i in range(8)]
|
||
|
|
||
|
BB_RANKS = [
|
||
|
BB_RANK_1,
|
||
|
BB_RANK_2,
|
||
|
BB_RANK_3,
|
||
|
BB_RANK_4,
|
||
|
BB_RANK_5,
|
||
|
BB_RANK_6,
|
||
|
BB_RANK_7,
|
||
|
BB_RANK_8,
|
||
|
] = [0xff << (8 * i) for i in range(8)]
|
||
|
|
||
|
BB_BACKRANKS = BB_RANK_1 | BB_RANK_8
|
||
|
|
||
|
|
||
|
def lsb(bb: Bitboard) -> int:
|
||
|
return (bb & -bb).bit_length() - 1
|
||
|
|
||
|
def scan_forward(bb: Bitboard) -> Iterator[Square]:
|
||
|
while bb:
|
||
|
r = bb & -bb
|
||
|
yield r.bit_length() - 1
|
||
|
bb ^= r
|
||
|
|
||
|
def msb(bb: Bitboard) -> int:
|
||
|
return bb.bit_length() - 1
|
||
|
|
||
|
def scan_reversed(bb: Bitboard) -> Iterator[Square]:
|
||
|
while bb:
|
||
|
r = bb.bit_length() - 1
|
||
|
yield r
|
||
|
bb ^= BB_SQUARES[r]
|
||
|
|
||
|
# Python 3.10 or fallback.
|
||
|
popcount: Callable[[Bitboard], int] = getattr(int, "bit_count", lambda bb: bin(bb).count("1"))
|
||
|
|
||
|
def flip_vertical(bb: Bitboard) -> Bitboard:
|
||
|
# https://www.chessprogramming.org/Flipping_Mirroring_and_Rotating#FlipVertically
|
||
|
bb = ((bb >> 8) & 0x00ff_00ff_00ff_00ff) | ((bb & 0x00ff_00ff_00ff_00ff) << 8)
|
||
|
bb = ((bb >> 16) & 0x0000_ffff_0000_ffff) | ((bb & 0x0000_ffff_0000_ffff) << 16)
|
||
|
bb = (bb >> 32) | ((bb & 0x0000_0000_ffff_ffff) << 32)
|
||
|
return bb
|
||
|
|
||
|
def flip_horizontal(bb: Bitboard) -> Bitboard:
|
||
|
# https://www.chessprogramming.org/Flipping_Mirroring_and_Rotating#MirrorHorizontally
|
||
|
bb = ((bb >> 1) & 0x5555_5555_5555_5555) | ((bb & 0x5555_5555_5555_5555) << 1)
|
||
|
bb = ((bb >> 2) & 0x3333_3333_3333_3333) | ((bb & 0x3333_3333_3333_3333) << 2)
|
||
|
bb = ((bb >> 4) & 0x0f0f_0f0f_0f0f_0f0f) | ((bb & 0x0f0f_0f0f_0f0f_0f0f) << 4)
|
||
|
return bb
|
||
|
|
||
|
def flip_diagonal(bb: Bitboard) -> Bitboard:
|
||
|
# https://www.chessprogramming.org/Flipping_Mirroring_and_Rotating#FlipabouttheDiagonal
|
||
|
t = (bb ^ (bb << 28)) & 0x0f0f_0f0f_0000_0000
|
||
|
bb = bb ^ t ^ (t >> 28)
|
||
|
t = (bb ^ (bb << 14)) & 0x3333_0000_3333_0000
|
||
|
bb = bb ^ t ^ (t >> 14)
|
||
|
t = (bb ^ (bb << 7)) & 0x5500_5500_5500_5500
|
||
|
bb = bb ^ t ^ (t >> 7)
|
||
|
return bb
|
||
|
|
||
|
def flip_anti_diagonal(bb: Bitboard) -> Bitboard:
|
||
|
# https://www.chessprogramming.org/Flipping_Mirroring_and_Rotating#FlipabouttheAntidiagonal
|
||
|
t = bb ^ (bb << 36)
|
||
|
bb = bb ^ ((t ^ (bb >> 36)) & 0xf0f0_f0f0_0f0f_0f0f)
|
||
|
t = (bb ^ (bb << 18)) & 0xcccc_0000_cccc_0000
|
||
|
bb = bb ^ t ^ (t >> 18)
|
||
|
t = (bb ^ (bb << 9)) & 0xaa00_aa00_aa00_aa00
|
||
|
bb = bb ^ t ^ (t >> 9)
|
||
|
return bb
|
||
|
|
||
|
|
||
|
def shift_down(b: Bitboard) -> Bitboard:
|
||
|
return b >> 8
|
||
|
|
||
|
def shift_2_down(b: Bitboard) -> Bitboard:
|
||
|
return b >> 16
|
||
|
|
||
|
def shift_up(b: Bitboard) -> Bitboard:
|
||
|
return (b << 8) & BB_ALL
|
||
|
|
||
|
def shift_2_up(b: Bitboard) -> Bitboard:
|
||
|
return (b << 16) & BB_ALL
|
||
|
|
||
|
def shift_right(b: Bitboard) -> Bitboard:
|
||
|
return (b << 1) & ~BB_FILE_A & BB_ALL
|
||
|
|
||
|
def shift_2_right(b: Bitboard) -> Bitboard:
|
||
|
return (b << 2) & ~BB_FILE_A & ~BB_FILE_B & BB_ALL
|
||
|
|
||
|
def shift_left(b: Bitboard) -> Bitboard:
|
||
|
return (b >> 1) & ~BB_FILE_H
|
||
|
|
||
|
def shift_2_left(b: Bitboard) -> Bitboard:
|
||
|
return (b >> 2) & ~BB_FILE_G & ~BB_FILE_H
|
||
|
|
||
|
def shift_up_left(b: Bitboard) -> Bitboard:
|
||
|
return (b << 7) & ~BB_FILE_H & BB_ALL
|
||
|
|
||
|
def shift_up_right(b: Bitboard) -> Bitboard:
|
||
|
return (b << 9) & ~BB_FILE_A & BB_ALL
|
||
|
|
||
|
def shift_down_left(b: Bitboard) -> Bitboard:
|
||
|
return (b >> 9) & ~BB_FILE_H
|
||
|
|
||
|
def shift_down_right(b: Bitboard) -> Bitboard:
|
||
|
return (b >> 7) & ~BB_FILE_A
|
||
|
|
||
|
|
||
|
def _sliding_attacks(square: Square, occupied: Bitboard, deltas: Iterable[int]) -> Bitboard:
|
||
|
attacks = BB_EMPTY
|
||
|
|
||
|
for delta in deltas:
|
||
|
sq = square
|
||
|
|
||
|
while True:
|
||
|
sq += delta
|
||
|
if not (0 <= sq < 64) or square_distance(sq, sq - delta) > 2:
|
||
|
break
|
||
|
|
||
|
attacks |= BB_SQUARES[sq]
|
||
|
|
||
|
if occupied & BB_SQUARES[sq]:
|
||
|
break
|
||
|
|
||
|
return attacks
|
||
|
|
||
|
def _step_attacks(square: Square, deltas: Iterable[int]) -> Bitboard:
|
||
|
return _sliding_attacks(square, BB_ALL, deltas)
|
||
|
|
||
|
BB_KNIGHT_ATTACKS = [_step_attacks(sq, [17, 15, 10, 6, -17, -15, -10, -6]) for sq in SQUARES]
|
||
|
BB_KING_ATTACKS = [_step_attacks(sq, [9, 8, 7, 1, -9, -8, -7, -1]) for sq in SQUARES]
|
||
|
BB_PAWN_ATTACKS = [[_step_attacks(sq, deltas) for sq in SQUARES] for deltas in [[-7, -9], [7, 9]]]
|
||
|
|
||
|
|
||
|
def _edges(square: Square) -> Bitboard:
|
||
|
return (((BB_RANK_1 | BB_RANK_8) & ~BB_RANKS[square_rank(square)]) |
|
||
|
((BB_FILE_A | BB_FILE_H) & ~BB_FILES[square_file(square)]))
|
||
|
|
||
|
def _carry_rippler(mask: Bitboard) -> Iterator[Bitboard]:
|
||
|
# Carry-Rippler trick to iterate subsets of mask.
|
||
|
subset = BB_EMPTY
|
||
|
while True:
|
||
|
yield subset
|
||
|
subset = (subset - mask) & mask
|
||
|
if not subset:
|
||
|
break
|
||
|
|
||
|
def _attack_table(deltas: List[int]) -> Tuple[List[Bitboard], List[Dict[Bitboard, Bitboard]]]:
|
||
|
mask_table = []
|
||
|
attack_table = []
|
||
|
|
||
|
for square in SQUARES:
|
||
|
attacks = {}
|
||
|
|
||
|
mask = _sliding_attacks(square, 0, deltas) & ~_edges(square)
|
||
|
for subset in _carry_rippler(mask):
|
||
|
attacks[subset] = _sliding_attacks(square, subset, deltas)
|
||
|
|
||
|
attack_table.append(attacks)
|
||
|
mask_table.append(mask)
|
||
|
|
||
|
return mask_table, attack_table
|
||
|
|
||
|
BB_DIAG_MASKS, BB_DIAG_ATTACKS = _attack_table([-9, -7, 7, 9])
|
||
|
BB_FILE_MASKS, BB_FILE_ATTACKS = _attack_table([-8, 8])
|
||
|
BB_RANK_MASKS, BB_RANK_ATTACKS = _attack_table([-1, 1])
|
||
|
|
||
|
|
||
|
def _rays() -> List[List[Bitboard]]:
|
||
|
rays = []
|
||
|
for a, bb_a in enumerate(BB_SQUARES):
|
||
|
rays_row = []
|
||
|
for b, bb_b in enumerate(BB_SQUARES):
|
||
|
if BB_DIAG_ATTACKS[a][0] & bb_b:
|
||
|
rays_row.append((BB_DIAG_ATTACKS[a][0] & BB_DIAG_ATTACKS[b][0]) | bb_a | bb_b)
|
||
|
elif BB_RANK_ATTACKS[a][0] & bb_b:
|
||
|
rays_row.append(BB_RANK_ATTACKS[a][0] | bb_a)
|
||
|
elif BB_FILE_ATTACKS[a][0] & bb_b:
|
||
|
rays_row.append(BB_FILE_ATTACKS[a][0] | bb_a)
|
||
|
else:
|
||
|
rays_row.append(BB_EMPTY)
|
||
|
rays.append(rays_row)
|
||
|
return rays
|
||
|
|
||
|
BB_RAYS = _rays()
|
||
|
|
||
|
def ray(a: Square, b: Square) -> Bitboard:
|
||
|
return BB_RAYS[a][b]
|
||
|
|
||
|
def between(a: Square, b: Square) -> Bitboard:
|
||
|
bb = BB_RAYS[a][b] & ((BB_ALL << a) ^ (BB_ALL << b))
|
||
|
return bb & (bb - 1)
|
||
|
|
||
|
|
||
|
SAN_REGEX = re.compile(r"^([NBKRQ])?([a-h])?([1-8])?[\-x]?([a-h][1-8])(=?[nbrqkNBRQK])?[\+#]?\Z")
|
||
|
|
||
|
FEN_CASTLING_REGEX = re.compile(r"^(?:-|[KQABCDEFGH]{0,2}[kqabcdefgh]{0,2})\Z")
|
||
|
|
||
|
|
||
|
@dataclasses.dataclass
|
||
|
class Piece:
|
||
|
"""A piece with type and color."""
|
||
|
|
||
|
piece_type: PieceType
|
||
|
"""The piece type."""
|
||
|
|
||
|
color: Color
|
||
|
"""The piece color."""
|
||
|
|
||
|
def symbol(self) -> str:
|
||
|
"""
|
||
|
Gets the symbol ``P``, ``N``, ``B``, ``R``, ``Q`` or ``K`` for white
|
||
|
pieces or the lower-case variants for the black pieces.
|
||
|
"""
|
||
|
symbol = piece_symbol(self.piece_type)
|
||
|
return symbol.upper() if self.color else symbol
|
||
|
|
||
|
def unicode_symbol(self, *, invert_color: bool = False) -> str:
|
||
|
"""
|
||
|
Gets the Unicode character for the piece.
|
||
|
"""
|
||
|
symbol = self.symbol().swapcase() if invert_color else self.symbol()
|
||
|
return UNICODE_PIECE_SYMBOLS[symbol]
|
||
|
|
||
|
def __hash__(self) -> int:
|
||
|
return self.piece_type + (-1 if self.color else 5)
|
||
|
|
||
|
def __repr__(self) -> str:
|
||
|
return f"Piece.from_symbol({self.symbol()!r})"
|
||
|
|
||
|
def __str__(self) -> str:
|
||
|
return self.symbol()
|
||
|
|
||
|
def _repr_svg_(self) -> str:
|
||
|
import chess.svg
|
||
|
return chess.svg.piece(self, size=45)
|
||
|
|
||
|
@classmethod
|
||
|
def from_symbol(cls, symbol: str) -> Piece:
|
||
|
"""
|
||
|
Creates a :class:`~chess.Piece` instance from a piece symbol.
|
||
|
|
||
|
:raises: :exc:`ValueError` if the symbol is invalid.
|
||
|
"""
|
||
|
return cls(PIECE_SYMBOLS.index(symbol.lower()), symbol.isupper())
|
||
|
|
||
|
|
||
|
@dataclasses.dataclass(unsafe_hash=True)
|
||
|
class Move:
|
||
|
"""
|
||
|
Represents a move from a square to a square and possibly the promotion
|
||
|
piece type.
|
||
|
|
||
|
Drops and null moves are supported.
|
||
|
"""
|
||
|
|
||
|
from_square: Square
|
||
|
"""The source square."""
|
||
|
|
||
|
to_square: Square
|
||
|
"""The target square."""
|
||
|
|
||
|
promotion: Optional[PieceType] = None
|
||
|
"""The promotion piece type or ``None``."""
|
||
|
|
||
|
drop: Optional[PieceType] = None
|
||
|
"""The drop piece type or ``None``."""
|
||
|
|
||
|
def uci(self) -> str:
|
||
|
"""
|
||
|
Gets a UCI string for the move.
|
||
|
|
||
|
For example, a move from a7 to a8 would be ``a7a8`` or ``a7a8q``
|
||
|
(if the latter is a promotion to a queen).
|
||
|
|
||
|
The UCI representation of a null move is ``0000``.
|
||
|
"""
|
||
|
if self.drop:
|
||
|
return piece_symbol(self.drop).upper() + "@" + SQUARE_NAMES[self.to_square]
|
||
|
elif self.promotion:
|
||
|
return SQUARE_NAMES[self.from_square] + SQUARE_NAMES[self.to_square] + piece_symbol(self.promotion)
|
||
|
elif self:
|
||
|
return SQUARE_NAMES[self.from_square] + SQUARE_NAMES[self.to_square]
|
||
|
else:
|
||
|
return "0000"
|
||
|
|
||
|
def xboard(self) -> str:
|
||
|
return self.uci() if self else "@@@@"
|
||
|
|
||
|
def __bool__(self) -> bool:
|
||
|
return bool(self.from_square or self.to_square or self.promotion or self.drop)
|
||
|
|
||
|
def __repr__(self) -> str:
|
||
|
return f"Move.from_uci({self.uci()!r})"
|
||
|
|
||
|
def __str__(self) -> str:
|
||
|
return self.uci()
|
||
|
|
||
|
@classmethod
|
||
|
def from_uci(cls, uci: str) -> Move:
|
||
|
"""
|
||
|
Parses a UCI string.
|
||
|
|
||
|
:raises: :exc:`InvalidMoveError` if the UCI string is invalid.
|
||
|
"""
|
||
|
if uci == "0000":
|
||
|
return cls.null()
|
||
|
elif len(uci) == 4 and "@" == uci[1]:
|
||
|
try:
|
||
|
drop = PIECE_SYMBOLS.index(uci[0].lower())
|
||
|
square = SQUARE_NAMES.index(uci[2:])
|
||
|
except ValueError:
|
||
|
raise InvalidMoveError(f"invalid uci: {uci!r}")
|
||
|
return cls(square, square, drop=drop)
|
||
|
elif 4 <= len(uci) <= 5:
|
||
|
try:
|
||
|
from_square = SQUARE_NAMES.index(uci[0:2])
|
||
|
to_square = SQUARE_NAMES.index(uci[2:4])
|
||
|
promotion = PIECE_SYMBOLS.index(uci[4]) if len(uci) == 5 else None
|
||
|
except ValueError:
|
||
|
raise InvalidMoveError(f"invalid uci: {uci!r}")
|
||
|
if from_square == to_square:
|
||
|
raise InvalidMoveError(f"invalid uci (use 0000 for null moves): {uci!r}")
|
||
|
return cls(from_square, to_square, promotion=promotion)
|
||
|
else:
|
||
|
raise InvalidMoveError(f"expected uci string to be of length 4 or 5: {uci!r}")
|
||
|
|
||
|
@classmethod
|
||
|
def null(cls) -> Move:
|
||
|
"""
|
||
|
Gets a null move.
|
||
|
|
||
|
A null move just passes the turn to the other side (and possibly
|
||
|
forfeits en passant capturing). Null moves evaluate to ``False`` in
|
||
|
boolean contexts.
|
||
|
|
||
|
>>> import chess
|
||
|
>>>
|
||
|
>>> bool(chess.Move.null())
|
||
|
False
|
||
|
"""
|
||
|
return cls(0, 0)
|
||
|
|
||
|
|
||
|
BaseBoardT = TypeVar("BaseBoardT", bound="BaseBoard")
|
||
|
|
||
|
class BaseBoard:
|
||
|
"""
|
||
|
A board representing the position of chess pieces. See
|
||
|
:class:`~chess.Board` for a full board with move generation.
|
||
|
|
||
|
The board is initialized with the standard chess starting position, unless
|
||
|
otherwise specified in the optional *board_fen* argument. If *board_fen*
|
||
|
is ``None``, an empty board is created.
|
||
|
"""
|
||
|
|
||
|
def __init__(self, board_fen: Optional[str] = STARTING_BOARD_FEN) -> None:
|
||
|
self.occupied_co = [BB_EMPTY, BB_EMPTY]
|
||
|
|
||
|
if board_fen is None:
|
||
|
self._clear_board()
|
||
|
elif board_fen == STARTING_BOARD_FEN:
|
||
|
self._reset_board()
|
||
|
else:
|
||
|
self._set_board_fen(board_fen)
|
||
|
|
||
|
def _reset_board(self) -> None:
|
||
|
self.pawns = BB_RANK_2 | BB_RANK_7
|
||
|
self.knights = BB_B1 | BB_G1 | BB_B8 | BB_G8
|
||
|
self.bishops = BB_C1 | BB_F1 | BB_C8 | BB_F8
|
||
|
self.rooks = BB_CORNERS
|
||
|
self.queens = BB_D1 | BB_D8
|
||
|
self.kings = BB_E1 | BB_E8
|
||
|
|
||
|
self.promoted = BB_EMPTY
|
||
|
|
||
|
self.occupied_co[WHITE] = BB_RANK_1 | BB_RANK_2
|
||
|
self.occupied_co[BLACK] = BB_RANK_7 | BB_RANK_8
|
||
|
self.occupied = BB_RANK_1 | BB_RANK_2 | BB_RANK_7 | BB_RANK_8
|
||
|
|
||
|
def reset_board(self) -> None:
|
||
|
"""
|
||
|
Resets pieces to the starting position.
|
||
|
|
||
|
:class:`~chess.Board` also resets the move stack, but not turn,
|
||
|
castling rights and move counters. Use :func:`chess.Board.reset()` to
|
||
|
fully restore the starting position.
|
||
|
"""
|
||
|
self._reset_board()
|
||
|
|
||
|
def _clear_board(self) -> None:
|
||
|
self.pawns = BB_EMPTY
|
||
|
self.knights = BB_EMPTY
|
||
|
self.bishops = BB_EMPTY
|
||
|
self.rooks = BB_EMPTY
|
||
|
self.queens = BB_EMPTY
|
||
|
self.kings = BB_EMPTY
|
||
|
|
||
|
self.promoted = BB_EMPTY
|
||
|
|
||
|
self.occupied_co[WHITE] = BB_EMPTY
|
||
|
self.occupied_co[BLACK] = BB_EMPTY
|
||
|
self.occupied = BB_EMPTY
|
||
|
|
||
|
def clear_board(self) -> None:
|
||
|
"""
|
||
|
Clears the board.
|
||
|
|
||
|
:class:`~chess.Board` also clears the move stack.
|
||
|
"""
|
||
|
self._clear_board()
|
||
|
|
||
|
def pieces_mask(self, piece_type: PieceType, color: Color) -> Bitboard:
|
||
|
if piece_type == PAWN:
|
||
|
bb = self.pawns
|
||
|
elif piece_type == KNIGHT:
|
||
|
bb = self.knights
|
||
|
elif piece_type == BISHOP:
|
||
|
bb = self.bishops
|
||
|
elif piece_type == ROOK:
|
||
|
bb = self.rooks
|
||
|
elif piece_type == QUEEN:
|
||
|
bb = self.queens
|
||
|
elif piece_type == KING:
|
||
|
bb = self.kings
|
||
|
else:
|
||
|
assert False, f"expected PieceType, got {piece_type!r}"
|
||
|
|
||
|
return bb & self.occupied_co[color]
|
||
|
|
||
|
def pieces(self, piece_type: PieceType, color: Color) -> SquareSet:
|
||
|
"""
|
||
|
Gets pieces of the given type and color.
|
||
|
|
||
|
Returns a :class:`set of squares <chess.SquareSet>`.
|
||
|
"""
|
||
|
return SquareSet(self.pieces_mask(piece_type, color))
|
||
|
|
||
|
def piece_at(self, square: Square) -> Optional[Piece]:
|
||
|
"""Gets the :class:`piece <chess.Piece>` at the given square."""
|
||
|
piece_type = self.piece_type_at(square)
|
||
|
if piece_type:
|
||
|
mask = BB_SQUARES[square]
|
||
|
color = bool(self.occupied_co[WHITE] & mask)
|
||
|
return Piece(piece_type, color)
|
||
|
else:
|
||
|
return None
|
||
|
|
||
|
def piece_type_at(self, square: Square) -> Optional[PieceType]:
|
||
|
"""Gets the piece type at the given square."""
|
||
|
mask = BB_SQUARES[square]
|
||
|
|
||
|
if not self.occupied & mask:
|
||
|
return None # Early return
|
||
|
elif self.pawns & mask:
|
||
|
return PAWN
|
||
|
elif self.knights & mask:
|
||
|
return KNIGHT
|
||
|
elif self.bishops & mask:
|
||
|
return BISHOP
|
||
|
elif self.rooks & mask:
|
||
|
return ROOK
|
||
|
elif self.queens & mask:
|
||
|
return QUEEN
|
||
|
else:
|
||
|
return KING
|
||
|
|
||
|
def color_at(self, square: Square) -> Optional[Color]:
|
||
|
"""Gets the color of the piece at the given square."""
|
||
|
mask = BB_SQUARES[square]
|
||
|
if self.occupied_co[WHITE] & mask:
|
||
|
return WHITE
|
||
|
elif self.occupied_co[BLACK] & mask:
|
||
|
return BLACK
|
||
|
else:
|
||
|
return None
|
||
|
|
||
|
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.
|
||
|
|
||
|
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 else None
|
||
|
|
||
|
def attacks_mask(self, square: Square) -> Bitboard:
|
||
|
bb_square = BB_SQUARES[square]
|
||
|
|
||
|
if bb_square & self.pawns:
|
||
|
color = bool(bb_square & self.occupied_co[WHITE])
|
||
|
return BB_PAWN_ATTACKS[color][square]
|
||
|
elif bb_square & self.knights:
|
||
|
return BB_KNIGHT_ATTACKS[square]
|
||
|
elif bb_square & self.kings:
|
||
|
return BB_KING_ATTACKS[square]
|
||
|
else:
|
||
|
attacks = 0
|
||
|
if bb_square & self.bishops or bb_square & self.queens:
|
||
|
attacks = BB_DIAG_ATTACKS[square][BB_DIAG_MASKS[square] & self.occupied]
|
||
|
if bb_square & self.rooks or bb_square & self.queens:
|
||
|
attacks |= (BB_RANK_ATTACKS[square][BB_RANK_MASKS[square] & self.occupied] |
|
||
|
BB_FILE_ATTACKS[square][BB_FILE_MASKS[square] & self.occupied])
|
||
|
return attacks
|
||
|
|
||
|
def attacks(self, square: Square) -> SquareSet:
|
||
|
"""
|
||
|
Gets the set of attacked squares from the given square.
|
||
|
|
||
|
There will be no attacks if the square is empty. Pinned pieces are
|
||
|
still attacking other squares.
|
||
|
|
||
|
Returns a :class:`set of squares <chess.SquareSet>`.
|
||
|
"""
|
||
|
return SquareSet(self.attacks_mask(square))
|
||
|
|
||
|
def _attackers_mask(self, color: Color, square: Square, occupied: Bitboard) -> Bitboard:
|
||
|
rank_pieces = BB_RANK_MASKS[square] & occupied
|
||
|
file_pieces = BB_FILE_MASKS[square] & occupied
|
||
|
diag_pieces = BB_DIAG_MASKS[square] & occupied
|
||
|
|
||
|
queens_and_rooks = self.queens | self.rooks
|
||
|
queens_and_bishops = self.queens | self.bishops
|
||
|
|
||
|
attackers = (
|
||
|
(BB_KING_ATTACKS[square] & self.kings) |
|
||
|
(BB_KNIGHT_ATTACKS[square] & self.knights) |
|
||
|
(BB_RANK_ATTACKS[square][rank_pieces] & queens_and_rooks) |
|
||
|
(BB_FILE_ATTACKS[square][file_pieces] & queens_and_rooks) |
|
||
|
(BB_DIAG_ATTACKS[square][diag_pieces] & queens_and_bishops) |
|
||
|
(BB_PAWN_ATTACKS[not color][square] & self.pawns))
|
||
|
|
||
|
return attackers & self.occupied_co[color]
|
||
|
|
||
|
def attackers_mask(self, color: Color, square: Square) -> Bitboard:
|
||
|
return self._attackers_mask(color, square, self.occupied)
|
||
|
|
||
|
def is_attacked_by(self, color: Color, square: Square) -> bool:
|
||
|
"""
|
||
|
Checks if the given side attacks the given square.
|
||
|
|
||
|
Pinned pieces still count as attackers. Pawns that can be captured
|
||
|
en passant are **not** considered attacked.
|
||
|
"""
|
||
|
return bool(self.attackers_mask(color, square))
|
||
|
|
||
|
def attackers(self, color: Color, square: Square) -> SquareSet:
|
||
|
"""
|
||
|
Gets the set of attackers of the given color for the given square.
|
||
|
|
||
|
Pinned pieces still count as attackers.
|
||
|
|
||
|
Returns a :class:`set of squares <chess.SquareSet>`.
|
||
|
"""
|
||
|
return SquareSet(self.attackers_mask(color, square))
|
||
|
|
||
|
def pin_mask(self, color: Color, square: Square) -> Bitboard:
|
||
|
king = self.king(color)
|
||
|
if king is None:
|
||
|
return BB_ALL
|
||
|
|
||
|
square_mask = BB_SQUARES[square]
|
||
|
|
||
|
for attacks, sliders in [(BB_FILE_ATTACKS, self.rooks | self.queens),
|
||
|
(BB_RANK_ATTACKS, self.rooks | self.queens),
|
||
|
(BB_DIAG_ATTACKS, self.bishops | self.queens)]:
|
||
|
rays = attacks[king][0]
|
||
|
if rays & square_mask:
|
||
|
snipers = rays & sliders & self.occupied_co[not color]
|
||
|
for sniper in scan_reversed(snipers):
|
||
|
if between(sniper, king) & (self.occupied | square_mask) == square_mask:
|
||
|
return ray(king, sniper)
|
||
|
|
||
|
break
|
||
|
|
||
|
return BB_ALL
|
||
|
|
||
|
def pin(self, color: Color, square: Square) -> SquareSet:
|
||
|
"""
|
||
|
Detects an absolute pin (and its direction) of the given square to
|
||
|
the king of the given color.
|
||
|
|
||
|
>>> import chess
|
||
|
>>>
|
||
|
>>> board = chess.Board("rnb1k2r/ppp2ppp/5n2/3q4/1b1P4/2N5/PP3PPP/R1BQKBNR w KQkq - 3 7")
|
||
|
>>> board.is_pinned(chess.WHITE, chess.C3)
|
||
|
True
|
||
|
>>> direction = board.pin(chess.WHITE, chess.C3)
|
||
|
>>> direction
|
||
|
SquareSet(0x0000_0001_0204_0810)
|
||
|
>>> print(direction)
|
||
|
. . . . . . . .
|
||
|
. . . . . . . .
|
||
|
. . . . . . . .
|
||
|
1 . . . . . . .
|
||
|
. 1 . . . . . .
|
||
|
. . 1 . . . . .
|
||
|
. . . 1 . . . .
|
||
|
. . . . 1 . . .
|
||
|
|
||
|
Returns a :class:`set of squares <chess.SquareSet>` that mask the rank,
|
||
|
file or diagonal of the pin. If there is no pin, then a mask of the
|
||
|
entire board is returned.
|
||
|
"""
|
||
|
return SquareSet(self.pin_mask(color, square))
|
||
|
|
||
|
def is_pinned(self, color: Color, square: Square) -> bool:
|
||
|
"""
|
||
|
Detects if the given square is pinned to the king of the given color.
|
||
|
"""
|
||
|
return self.pin_mask(color, square) != BB_ALL
|
||
|
|
||
|
def _remove_piece_at(self, square: Square) -> Optional[PieceType]:
|
||
|
piece_type = self.piece_type_at(square)
|
||
|
mask = BB_SQUARES[square]
|
||
|
|
||
|
if piece_type == PAWN:
|
||
|
self.pawns ^= mask
|
||
|
elif piece_type == KNIGHT:
|
||
|
self.knights ^= mask
|
||
|
elif piece_type == BISHOP:
|
||
|
self.bishops ^= mask
|
||
|
elif piece_type == ROOK:
|
||
|
self.rooks ^= mask
|
||
|
elif piece_type == QUEEN:
|
||
|
self.queens ^= mask
|
||
|
elif piece_type == KING:
|
||
|
self.kings ^= mask
|
||
|
else:
|
||
|
return None
|
||
|
|
||
|
self.occupied ^= mask
|
||
|
self.occupied_co[WHITE] &= ~mask
|
||
|
self.occupied_co[BLACK] &= ~mask
|
||
|
|
||
|
self.promoted &= ~mask
|
||
|
|
||
|
return piece_type
|
||
|
|
||
|
def remove_piece_at(self, square: Square) -> Optional[Piece]:
|
||
|
"""
|
||
|
Removes the piece from the given square. Returns the
|
||
|
:class:`~chess.Piece` or ``None`` if the square was already empty.
|
||
|
|
||
|
:class:`~chess.Board` also clears the move stack.
|
||
|
"""
|
||
|
color = bool(self.occupied_co[WHITE] & BB_SQUARES[square])
|
||
|
piece_type = self._remove_piece_at(square)
|
||
|
return Piece(piece_type, color) if piece_type else None
|
||
|
|
||
|
def _set_piece_at(self, square: Square, piece_type: PieceType, color: Color, promoted: bool = False) -> None:
|
||
|
self._remove_piece_at(square)
|
||
|
|
||
|
mask = BB_SQUARES[square]
|
||
|
|
||
|
if piece_type == PAWN:
|
||
|
self.pawns |= mask
|
||
|
elif piece_type == KNIGHT:
|
||
|
self.knights |= mask
|
||
|
elif piece_type == BISHOP:
|
||
|
self.bishops |= mask
|
||
|
elif piece_type == ROOK:
|
||
|
self.rooks |= mask
|
||
|
elif piece_type == QUEEN:
|
||
|
self.queens |= mask
|
||
|
elif piece_type == KING:
|
||
|
self.kings |= mask
|
||
|
else:
|
||
|
return
|
||
|
|
||
|
self.occupied ^= mask
|
||
|
self.occupied_co[color] ^= mask
|
||
|
|
||
|
if promoted:
|
||
|
self.promoted ^= mask
|
||
|
|
||
|
def set_piece_at(self, square: Square, piece: Optional[Piece], promoted: bool = False) -> None:
|
||
|
"""
|
||
|
Sets a piece at the given square.
|
||
|
|
||
|
An existing piece is replaced. Setting *piece* to ``None`` is
|
||
|
equivalent to :func:`~chess.Board.remove_piece_at()`.
|
||
|
|
||
|
:class:`~chess.Board` also clears the move stack.
|
||
|
"""
|
||
|
if piece is None:
|
||
|
self._remove_piece_at(square)
|
||
|
else:
|
||
|
self._set_piece_at(square, piece.piece_type, piece.color, promoted)
|
||
|
|
||
|
def board_fen(self, *, promoted: Optional[bool] = False) -> str:
|
||
|
"""
|
||
|
Gets the board FEN (e.g.,
|
||
|
``rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR``).
|
||
|
"""
|
||
|
builder = []
|
||
|
empty = 0
|
||
|
|
||
|
for square in SQUARES_180:
|
||
|
piece = self.piece_at(square)
|
||
|
|
||
|
if not piece:
|
||
|
empty += 1
|
||
|
else:
|
||
|
if empty:
|
||
|
builder.append(str(empty))
|
||
|
empty = 0
|
||
|
builder.append(piece.symbol())
|
||
|
if promoted and BB_SQUARES[square] & self.promoted:
|
||
|
builder.append("~")
|
||
|
|
||
|
if BB_SQUARES[square] & BB_FILE_H:
|
||
|
if empty:
|
||
|
builder.append(str(empty))
|
||
|
empty = 0
|
||
|
|
||
|
if square != H1:
|
||
|
builder.append("/")
|
||
|
|
||
|
return "".join(builder)
|
||
|
|
||
|
def _set_board_fen(self, fen: str) -> None:
|
||
|
# Compatibility with set_fen().
|
||
|
fen = fen.strip()
|
||
|
if " " in fen:
|
||
|
raise ValueError(f"expected position part of fen, got multiple parts: {fen!r}")
|
||
|
|
||
|
# Ensure the FEN is valid.
|
||
|
rows = fen.split("/")
|
||
|
if len(rows) != 8:
|
||
|
raise ValueError(f"expected 8 rows in position part of fen: {fen!r}")
|
||
|
|
||
|
# Validate each row.
|
||
|
for row in rows:
|
||
|
field_sum = 0
|
||
|
previous_was_digit = False
|
||
|
previous_was_piece = False
|
||
|
|
||
|
for c in row:
|
||
|
if c in ["1", "2", "3", "4", "5", "6", "7", "8"]:
|
||
|
if previous_was_digit:
|
||
|
raise ValueError(f"two subsequent digits in position part of fen: {fen!r}")
|
||
|
field_sum += int(c)
|
||
|
previous_was_digit = True
|
||
|
previous_was_piece = False
|
||
|
elif c == "~":
|
||
|
if not previous_was_piece:
|
||
|
raise ValueError(f"'~' not after piece in position part of fen: {fen!r}")
|
||
|
previous_was_digit = False
|
||
|
previous_was_piece = False
|
||
|
elif c.lower() in PIECE_SYMBOLS:
|
||
|
field_sum += 1
|
||
|
previous_was_digit = False
|
||
|
previous_was_piece = True
|
||
|
else:
|
||
|
raise ValueError(f"invalid character in position part of fen: {fen!r}")
|
||
|
|
||
|
if field_sum != 8:
|
||
|
raise ValueError(f"expected 8 columns per row in position part of fen: {fen!r}")
|
||
|
|
||
|
# Clear the board.
|
||
|
self._clear_board()
|
||
|
|
||
|
# Put pieces on the board.
|
||
|
square_index = 0
|
||
|
for c in fen:
|
||
|
if c in ["1", "2", "3", "4", "5", "6", "7", "8"]:
|
||
|
square_index += int(c)
|
||
|
elif c.lower() in PIECE_SYMBOLS:
|
||
|
piece = Piece.from_symbol(c)
|
||
|
self._set_piece_at(SQUARES_180[square_index], piece.piece_type, piece.color)
|
||
|
square_index += 1
|
||
|
elif c == "~":
|
||
|
self.promoted |= BB_SQUARES[SQUARES_180[square_index - 1]]
|
||
|
|
||
|
def set_board_fen(self, fen: str) -> None:
|
||
|
"""
|
||
|
Parses *fen* and sets up the board, where *fen* is the board part of
|
||
|
a FEN.
|
||
|
|
||
|
:class:`~chess.Board` also clears the move stack.
|
||
|
|
||
|
:raises: :exc:`ValueError` if syntactically invalid.
|
||
|
"""
|
||
|
self._set_board_fen(fen)
|
||
|
|
||
|
def piece_map(self, *, mask: Bitboard = BB_ALL) -> Dict[Square, Piece]:
|
||
|
"""
|
||
|
Gets a dictionary of :class:`pieces <chess.Piece>` by square index.
|
||
|
"""
|
||
|
result = {}
|
||
|
for square in scan_reversed(self.occupied & mask):
|
||
|
result[square] = typing.cast(Piece, self.piece_at(square))
|
||
|
return result
|
||
|
|
||
|
def _set_piece_map(self, pieces: Mapping[Square, Piece]) -> None:
|
||
|
self._clear_board()
|
||
|
for square, piece in pieces.items():
|
||
|
self._set_piece_at(square, piece.piece_type, piece.color)
|
||
|
|
||
|
def set_piece_map(self, pieces: Mapping[Square, Piece]) -> None:
|
||
|
"""
|
||
|
Sets up the board from a dictionary of :class:`pieces <chess.Piece>`
|
||
|
by square index.
|
||
|
|
||
|
:class:`~chess.Board` also clears the move stack.
|
||
|
"""
|
||
|
self._set_piece_map(pieces)
|
||
|
|
||
|
def _set_chess960_pos(self, scharnagl: int) -> None:
|
||
|
if not 0 <= scharnagl <= 959:
|
||
|
raise ValueError(f"chess960 position index not 0 <= {scharnagl!r} <= 959")
|
||
|
|
||
|
# See http://www.russellcottrell.com/Chess/Chess960.htm for
|
||
|
# a description of the algorithm.
|
||
|
n, bw = divmod(scharnagl, 4)
|
||
|
n, bb = divmod(n, 4)
|
||
|
n, q = divmod(n, 6)
|
||
|
|
||
|
for n1 in range(0, 4):
|
||
|
n2 = n + (3 - n1) * (4 - n1) // 2 - 5
|
||
|
if n1 < n2 and 1 <= n2 <= 4:
|
||
|
break
|
||
|
|
||
|
# Bishops.
|
||
|
bw_file = bw * 2 + 1
|
||
|
bb_file = bb * 2
|
||
|
self.bishops = (BB_FILES[bw_file] | BB_FILES[bb_file]) & BB_BACKRANKS
|
||
|
|
||
|
# Queens.
|
||
|
q_file = q
|
||
|
q_file += int(min(bw_file, bb_file) <= q_file)
|
||
|
q_file += int(max(bw_file, bb_file) <= q_file)
|
||
|
self.queens = BB_FILES[q_file] & BB_BACKRANKS
|
||
|
|
||
|
used = [bw_file, bb_file, q_file]
|
||
|
|
||
|
# Knights.
|
||
|
self.knights = BB_EMPTY
|
||
|
for i in range(0, 8):
|
||
|
if i not in used:
|
||
|
if n1 == 0 or n2 == 0:
|
||
|
self.knights |= BB_FILES[i] & BB_BACKRANKS
|
||
|
used.append(i)
|
||
|
n1 -= 1
|
||
|
n2 -= 1
|
||
|
|
||
|
# RKR.
|
||
|
for i in range(0, 8):
|
||
|
if i not in used:
|
||
|
self.rooks = BB_FILES[i] & BB_BACKRANKS
|
||
|
used.append(i)
|
||
|
break
|
||
|
for i in range(1, 8):
|
||
|
if i not in used:
|
||
|
self.kings = BB_FILES[i] & BB_BACKRANKS
|
||
|
used.append(i)
|
||
|
break
|
||
|
for i in range(2, 8):
|
||
|
if i not in used:
|
||
|
self.rooks |= BB_FILES[i] & BB_BACKRANKS
|
||
|
break
|
||
|
|
||
|
# Finalize.
|
||
|
self.pawns = BB_RANK_2 | BB_RANK_7
|
||
|
self.occupied_co[WHITE] = BB_RANK_1 | BB_RANK_2
|
||
|
self.occupied_co[BLACK] = BB_RANK_7 | BB_RANK_8
|
||
|
self.occupied = BB_RANK_1 | BB_RANK_2 | BB_RANK_7 | BB_RANK_8
|
||
|
self.promoted = BB_EMPTY
|
||
|
|
||
|
def set_chess960_pos(self, scharnagl: int) -> None:
|
||
|
"""
|
||
|
Sets up a Chess960 starting position given its index between 0 and 959.
|
||
|
Also see :func:`~chess.BaseBoard.from_chess960_pos()`.
|
||
|
"""
|
||
|
self._set_chess960_pos(scharnagl)
|
||
|
|
||
|
def chess960_pos(self) -> Optional[int]:
|
||
|
"""
|
||
|
Gets the Chess960 starting position index between 0 and 959,
|
||
|
or ``None``.
|
||
|
"""
|
||
|
if self.occupied_co[WHITE] != BB_RANK_1 | BB_RANK_2:
|
||
|
return None
|
||
|
if self.occupied_co[BLACK] != BB_RANK_7 | BB_RANK_8:
|
||
|
return None
|
||
|
if self.pawns != BB_RANK_2 | BB_RANK_7:
|
||
|
return None
|
||
|
if self.promoted:
|
||
|
return None
|
||
|
|
||
|
# Piece counts.
|
||
|
brnqk = [self.bishops, self.rooks, self.knights, self.queens, self.kings]
|
||
|
if [popcount(pieces) for pieces in brnqk] != [4, 4, 4, 2, 2]:
|
||
|
return None
|
||
|
|
||
|
# Symmetry.
|
||
|
if any((BB_RANK_1 & pieces) << 56 != BB_RANK_8 & pieces for pieces in brnqk):
|
||
|
return None
|
||
|
|
||
|
# Algorithm from ChessX, src/database/bitboard.cpp, r2254.
|
||
|
x = self.bishops & (2 + 8 + 32 + 128)
|
||
|
if not x:
|
||
|
return None
|
||
|
bs1 = (lsb(x) - 1) // 2
|
||
|
cc_pos = bs1
|
||
|
x = self.bishops & (1 + 4 + 16 + 64)
|
||
|
if not x:
|
||
|
return None
|
||
|
bs2 = lsb(x) * 2
|
||
|
cc_pos += bs2
|
||
|
|
||
|
q = 0
|
||
|
qf = False
|
||
|
n0 = 0
|
||
|
n1 = 0
|
||
|
n0f = False
|
||
|
n1f = False
|
||
|
rf = 0
|
||
|
n0s = [0, 4, 7, 9]
|
||
|
for square in range(A1, H1 + 1):
|
||
|
bb = BB_SQUARES[square]
|
||
|
if bb & self.queens:
|
||
|
qf = True
|
||
|
elif bb & self.rooks or bb & self.kings:
|
||
|
if bb & self.kings:
|
||
|
if rf != 1:
|
||
|
return None
|
||
|
else:
|
||
|
rf += 1
|
||
|
|
||
|
if not qf:
|
||
|
q += 1
|
||
|
|
||
|
if not n0f:
|
||
|
n0 += 1
|
||
|
elif not n1f:
|
||
|
n1 += 1
|
||
|
elif bb & self.knights:
|
||
|
if not qf:
|
||
|
q += 1
|
||
|
|
||
|
if not n0f:
|
||
|
n0f = True
|
||
|
elif not n1f:
|
||
|
n1f = True
|
||
|
|
||
|
if n0 < 4 and n1f and qf:
|
||
|
cc_pos += q * 16
|
||
|
krn = n0s[n0] + n1
|
||
|
cc_pos += krn * 96
|
||
|
return cc_pos
|
||
|
else:
|
||
|
return None
|
||
|
|
||
|
def __repr__(self) -> str:
|
||
|
return f"{type(self).__name__}({self.board_fen()!r})"
|
||
|
|
||
|
def __str__(self) -> str:
|
||
|
builder = []
|
||
|
|
||
|
for square in SQUARES_180:
|
||
|
piece = self.piece_at(square)
|
||
|
|
||
|
if piece:
|
||
|
builder.append(piece.symbol())
|
||
|
else:
|
||
|
builder.append(".")
|
||
|
|
||
|
if BB_SQUARES[square] & BB_FILE_H:
|
||
|
if square != H1:
|
||
|
builder.append("\n")
|
||
|
else:
|
||
|
builder.append(" ")
|
||
|
|
||
|
return "".join(builder)
|
||
|
|
||
|
def unicode(self, *, invert_color: bool = False, borders: bool = False, empty_square: str = "⭘", orientation: Color = WHITE) -> str:
|
||
|
"""
|
||
|
Returns a string representation of the board with Unicode pieces.
|
||
|
Useful for pretty-printing to a terminal.
|
||
|
|
||
|
:param invert_color: Invert color of the Unicode pieces.
|
||
|
:param borders: Show borders and a coordinate margin.
|
||
|
"""
|
||
|
builder = []
|
||
|
for rank_index in (range(7, -1, -1) if orientation else range(8)):
|
||
|
if borders:
|
||
|
builder.append(" ")
|
||
|
builder.append("-" * 17)
|
||
|
builder.append("\n")
|
||
|
|
||
|
builder.append(RANK_NAMES[rank_index])
|
||
|
builder.append(" ")
|
||
|
|
||
|
for i, file_index in enumerate(range(8) if orientation else range(7, -1, -1)):
|
||
|
square_index = square(file_index, rank_index)
|
||
|
|
||
|
if borders:
|
||
|
builder.append("|")
|
||
|
elif i > 0:
|
||
|
builder.append(" ")
|
||
|
|
||
|
piece = self.piece_at(square_index)
|
||
|
|
||
|
if piece:
|
||
|
builder.append(piece.unicode_symbol(invert_color=invert_color))
|
||
|
else:
|
||
|
builder.append(empty_square)
|
||
|
|
||
|
if borders:
|
||
|
builder.append("|")
|
||
|
|
||
|
if borders or (rank_index > 0 if orientation else rank_index < 7):
|
||
|
builder.append("\n")
|
||
|
|
||
|
if borders:
|
||
|
builder.append(" ")
|
||
|
builder.append("-" * 17)
|
||
|
builder.append("\n")
|
||
|
letters = "a b c d e f g h" if orientation else "h g f e d c b a"
|
||
|
builder.append(" " + letters)
|
||
|
|
||
|
return "".join(builder)
|
||
|
|
||
|
def _repr_svg_(self) -> str:
|
||
|
import chess.svg
|
||
|
return chess.svg.board(board=self, size=400)
|
||
|
|
||
|
def __eq__(self, board: object) -> bool:
|
||
|
if isinstance(board, BaseBoard):
|
||
|
return (
|
||
|
self.occupied == board.occupied and
|
||
|
self.occupied_co[WHITE] == board.occupied_co[WHITE] and
|
||
|
self.pawns == board.pawns and
|
||
|
self.knights == board.knights and
|
||
|
self.bishops == board.bishops and
|
||
|
self.rooks == board.rooks and
|
||
|
self.queens == board.queens and
|
||
|
self.kings == board.kings)
|
||
|
else:
|
||
|
return NotImplemented
|
||
|
|
||
|
def apply_transform(self, f: Callable[[Bitboard], Bitboard]) -> None:
|
||
|
self.pawns = f(self.pawns)
|
||
|
self.knights = f(self.knights)
|
||
|
self.bishops = f(self.bishops)
|
||
|
self.rooks = f(self.rooks)
|
||
|
self.queens = f(self.queens)
|
||
|
self.kings = f(self.kings)
|
||
|
|
||
|
self.occupied_co[WHITE] = f(self.occupied_co[WHITE])
|
||
|
self.occupied_co[BLACK] = f(self.occupied_co[BLACK])
|
||
|
self.occupied = f(self.occupied)
|
||
|
self.promoted = f(self.promoted)
|
||
|
|
||
|
def transform(self: BaseBoardT, f: Callable[[Bitboard], Bitboard]) -> BaseBoardT:
|
||
|
"""
|
||
|
Returns a transformed copy of the board (without move stack)
|
||
|
by applying a bitboard transformation function.
|
||
|
|
||
|
Available transformations include :func:`chess.flip_vertical()`,
|
||
|
:func:`chess.flip_horizontal()`, :func:`chess.flip_diagonal()`,
|
||
|
:func:`chess.flip_anti_diagonal()`, :func:`chess.shift_down()`,
|
||
|
:func:`chess.shift_up()`, :func:`chess.shift_left()`, and
|
||
|
:func:`chess.shift_right()`.
|
||
|
|
||
|
Alternatively, :func:`~chess.BaseBoard.apply_transform()` can be used
|
||
|
to apply the transformation on the board.
|
||
|
"""
|
||
|
board = self.copy()
|
||
|
board.apply_transform(f)
|
||
|
return board
|
||
|
|
||
|
def apply_mirror(self: BaseBoardT) -> None:
|
||
|
self.apply_transform(flip_vertical)
|
||
|
self.occupied_co[WHITE], self.occupied_co[BLACK] = self.occupied_co[BLACK], self.occupied_co[WHITE]
|
||
|
|
||
|
def mirror(self: BaseBoardT) -> BaseBoardT:
|
||
|
"""
|
||
|
Returns a mirrored copy of the board (without move stack).
|
||
|
|
||
|
The board is mirrored vertically and piece colors are swapped, so that
|
||
|
the position is equivalent modulo color.
|
||
|
|
||
|
Alternatively, :func:`~chess.BaseBoard.apply_mirror()` can be used
|
||
|
to mirror the board.
|
||
|
"""
|
||
|
board = self.copy()
|
||
|
board.apply_mirror()
|
||
|
return board
|
||
|
|
||
|
def copy(self: BaseBoardT) -> BaseBoardT:
|
||
|
"""Creates a copy of the board."""
|
||
|
board = type(self)(None)
|
||
|
|
||
|
board.pawns = self.pawns
|
||
|
board.knights = self.knights
|
||
|
board.bishops = self.bishops
|
||
|
board.rooks = self.rooks
|
||
|
board.queens = self.queens
|
||
|
board.kings = self.kings
|
||
|
|
||
|
board.occupied_co[WHITE] = self.occupied_co[WHITE]
|
||
|
board.occupied_co[BLACK] = self.occupied_co[BLACK]
|
||
|
board.occupied = self.occupied
|
||
|
board.promoted = self.promoted
|
||
|
|
||
|
return board
|
||
|
|
||
|
def __copy__(self: BaseBoardT) -> BaseBoardT:
|
||
|
return self.copy()
|
||
|
|
||
|
def __deepcopy__(self: BaseBoardT, memo: Dict[int, object]) -> BaseBoardT:
|
||
|
board = self.copy()
|
||
|
memo[id(self)] = board
|
||
|
return board
|
||
|
|
||
|
@classmethod
|
||
|
def empty(cls: Type[BaseBoardT]) -> BaseBoardT:
|
||
|
"""
|
||
|
Creates a new empty board. Also see
|
||
|
:func:`~chess.BaseBoard.clear_board()`.
|
||
|
"""
|
||
|
return cls(None)
|
||
|
|
||
|
@classmethod
|
||
|
def from_chess960_pos(cls: Type[BaseBoardT], scharnagl: int) -> BaseBoardT:
|
||
|
"""
|
||
|
Creates a new board, initialized with a Chess960 starting position.
|
||
|
|
||
|
>>> import chess
|
||
|
>>> import random
|
||
|
>>>
|
||
|
>>> board = chess.Board.from_chess960_pos(random.randint(0, 959))
|
||
|
"""
|
||
|
board = cls.empty()
|
||
|
board.set_chess960_pos(scharnagl)
|
||
|
return board
|
||
|
|
||
|
|
||
|
BoardT = TypeVar("BoardT", bound="Board")
|
||
|
|
||
|
class _BoardState(Generic[BoardT]):
|
||
|
|
||
|
def __init__(self, board: BoardT) -> None:
|
||
|
self.pawns = board.pawns
|
||
|
self.knights = board.knights
|
||
|
self.bishops = board.bishops
|
||
|
self.rooks = board.rooks
|
||
|
self.queens = board.queens
|
||
|
self.kings = board.kings
|
||
|
|
||
|
self.occupied_w = board.occupied_co[WHITE]
|
||
|
self.occupied_b = board.occupied_co[BLACK]
|
||
|
self.occupied = board.occupied
|
||
|
|
||
|
self.promoted = board.promoted
|
||
|
|
||
|
self.turn = board.turn
|
||
|
self.castling_rights = board.castling_rights
|
||
|
self.ep_square = board.ep_square
|
||
|
self.halfmove_clock = board.halfmove_clock
|
||
|
self.fullmove_number = board.fullmove_number
|
||
|
|
||
|
def restore(self, board: BoardT) -> None:
|
||
|
board.pawns = self.pawns
|
||
|
board.knights = self.knights
|
||
|
board.bishops = self.bishops
|
||
|
board.rooks = self.rooks
|
||
|
board.queens = self.queens
|
||
|
board.kings = self.kings
|
||
|
|
||
|
board.occupied_co[WHITE] = self.occupied_w
|
||
|
board.occupied_co[BLACK] = self.occupied_b
|
||
|
board.occupied = self.occupied
|
||
|
|
||
|
board.promoted = self.promoted
|
||
|
|
||
|
board.turn = self.turn
|
||
|
board.castling_rights = self.castling_rights
|
||
|
board.ep_square = self.ep_square
|
||
|
board.halfmove_clock = self.halfmove_clock
|
||
|
board.fullmove_number = self.fullmove_number
|
||
|
|
||
|
class Board(BaseBoard):
|
||
|
"""
|
||
|
A :class:`~chess.BaseBoard`, additional information representing
|
||
|
a chess position, and a :data:`move stack <chess.Board.move_stack>`.
|
||
|
|
||
|
Provides :data:`move generation <chess.Board.legal_moves>`, validation,
|
||
|
:func:`parsing <chess.Board.parse_san()>`, attack generation,
|
||
|
:func:`game end detection <chess.Board.is_game_over()>`,
|
||
|
and the capability to :func:`make <chess.Board.push()>` and
|
||
|
:func:`unmake <chess.Board.pop()>` moves.
|
||
|
|
||
|
The board is initialized to the standard chess starting position,
|
||
|
unless otherwise specified in the optional *fen* argument.
|
||
|
If *fen* is ``None``, an empty board is created.
|
||
|
|
||
|
Optionally supports *chess960*. In Chess960, castling moves are encoded
|
||
|
by a king move to the corresponding rook square.
|
||
|
Use :func:`chess.Board.from_chess960_pos()` to create a board with one
|
||
|
of the Chess960 starting positions.
|
||
|
|
||
|
It's safe to set :data:`~Board.turn`, :data:`~Board.castling_rights`,
|
||
|
:data:`~Board.ep_square`, :data:`~Board.halfmove_clock` and
|
||
|
:data:`~Board.fullmove_number` directly.
|
||
|
|
||
|
.. warning::
|
||
|
It is possible to set up and work with invalid positions. In this
|
||
|
case, :class:`~chess.Board` implements a kind of "pseudo-chess"
|
||
|
(useful to gracefully handle errors or to implement chess variants).
|
||
|
Use :func:`~chess.Board.is_valid()` to detect invalid positions.
|
||
|
"""
|
||
|
|
||
|
aliases: ClassVar[List[str]] = ["Standard", "Chess", "Classical", "Normal", "Illegal", "From Position"]
|
||
|
uci_variant: ClassVar[Optional[str]] = "chess"
|
||
|
xboard_variant: ClassVar[Optional[str]] = "normal"
|
||
|
starting_fen: ClassVar[str] = STARTING_FEN
|
||
|
|
||
|
tbw_suffix: ClassVar[Optional[str]] = ".rtbw"
|
||
|
tbz_suffix: ClassVar[Optional[str]] = ".rtbz"
|
||
|
tbw_magic: ClassVar[Optional[bytes]] = b"\x71\xe8\x23\x5d"
|
||
|
tbz_magic: ClassVar[Optional[bytes]] = b"\xd7\x66\x0c\xa5"
|
||
|
pawnless_tbw_suffix: ClassVar[Optional[str]] = None
|
||
|
pawnless_tbz_suffix: ClassVar[Optional[str]] = None
|
||
|
pawnless_tbw_magic: ClassVar[Optional[bytes]] = None
|
||
|
pawnless_tbz_magic: ClassVar[Optional[bytes]] = None
|
||
|
connected_kings: ClassVar[bool] = False
|
||
|
one_king: ClassVar[bool] = True
|
||
|
captures_compulsory: ClassVar[bool] = False
|
||
|
|
||
|
turn: Color
|
||
|
"""The side to move (``chess.WHITE`` or ``chess.BLACK``)."""
|
||
|
|
||
|
castling_rights: Bitboard
|
||
|
"""
|
||
|
Bitmask of the rooks with castling rights.
|
||
|
|
||
|
To test for specific squares:
|
||
|
|
||
|
>>> import chess
|
||
|
>>>
|
||
|
>>> board = chess.Board()
|
||
|
>>> bool(board.castling_rights & chess.BB_H1) # White can castle with the h1 rook
|
||
|
True
|
||
|
|
||
|
To add a specific square:
|
||
|
|
||
|
>>> board.castling_rights |= chess.BB_A1
|
||
|
|
||
|
Use :func:`~chess.Board.set_castling_fen()` to set multiple castling
|
||
|
rights. Also see :func:`~chess.Board.has_castling_rights()`,
|
||
|
:func:`~chess.Board.has_kingside_castling_rights()`,
|
||
|
:func:`~chess.Board.has_queenside_castling_rights()`,
|
||
|
:func:`~chess.Board.has_chess960_castling_rights()`,
|
||
|
:func:`~chess.Board.clean_castling_rights()`.
|
||
|
"""
|
||
|
|
||
|
ep_square: Optional[Square]
|
||
|
"""
|
||
|
The potential en passant square on the third or sixth rank or ``None``.
|
||
|
|
||
|
Use :func:`~chess.Board.has_legal_en_passant()` to test if en passant
|
||
|
capturing would actually be possible on the next move.
|
||
|
"""
|
||
|
|
||
|
fullmove_number: int
|
||
|
"""
|
||
|
Counts move pairs. Starts at `1` and is incremented after every move
|
||
|
of the black side.
|
||
|
"""
|
||
|
|
||
|
halfmove_clock: int
|
||
|
"""The number of half-moves since the last capture or pawn move."""
|
||
|
|
||
|
promoted: Bitboard
|
||
|
"""A bitmask of pieces that have been promoted."""
|
||
|
|
||
|
chess960: bool
|
||
|
"""
|
||
|
Whether the board is in Chess960 mode. In Chess960 castling moves are
|
||
|
represented as king moves to the corresponding rook square.
|
||
|
"""
|
||
|
|
||
|
move_stack: List[Move]
|
||
|
"""
|
||
|
The move stack. Use :func:`Board.push() <chess.Board.push()>`,
|
||
|
:func:`Board.pop() <chess.Board.pop()>`,
|
||
|
:func:`Board.peek() <chess.Board.peek()>` and
|
||
|
:func:`Board.clear_stack() <chess.Board.clear_stack()>` for
|
||
|
manipulation.
|
||
|
"""
|
||
|
|
||
|
def __init__(self: BoardT, fen: Optional[str] = STARTING_FEN, *, chess960: bool = False) -> None:
|
||
|
BaseBoard.__init__(self, None)
|
||
|
|
||
|
self.chess960 = chess960
|
||
|
|
||
|
self.ep_square = None
|
||
|
self.move_stack = []
|
||
|
self._stack: List[_BoardState[BoardT]] = []
|
||
|
|
||
|
if fen is None:
|
||
|
self.clear()
|
||
|
elif fen == type(self).starting_fen:
|
||
|
self.reset()
|
||
|
else:
|
||
|
self.set_fen(fen)
|
||
|
|
||
|
@property
|
||
|
def legal_moves(self) -> LegalMoveGenerator:
|
||
|
"""
|
||
|
A dynamic list of legal moves.
|
||
|
|
||
|
>>> import chess
|
||
|
>>>
|
||
|
>>> board = chess.Board()
|
||
|
>>> board.legal_moves.count()
|
||
|
20
|
||
|
>>> bool(board.legal_moves)
|
||
|
True
|
||
|
>>> move = chess.Move.from_uci("g1f3")
|
||
|
>>> move in board.legal_moves
|
||
|
True
|
||
|
|
||
|
Wraps :func:`~chess.Board.generate_legal_moves()` and
|
||
|
:func:`~chess.Board.is_legal()`.
|
||
|
"""
|
||
|
return LegalMoveGenerator(self)
|
||
|
|
||
|
@property
|
||
|
def pseudo_legal_moves(self) -> PseudoLegalMoveGenerator:
|
||
|
"""
|
||
|
A dynamic list of pseudo-legal moves, much like the legal move list.
|
||
|
|
||
|
Pseudo-legal moves might leave or put the king in check, but are
|
||
|
otherwise valid. Null moves are not pseudo-legal. Castling moves are
|
||
|
only included if they are completely legal.
|
||
|
|
||
|
Wraps :func:`~chess.Board.generate_pseudo_legal_moves()` and
|
||
|
:func:`~chess.Board.is_pseudo_legal()`.
|
||
|
"""
|
||
|
return PseudoLegalMoveGenerator(self)
|
||
|
|
||
|
def reset(self) -> None:
|
||
|
"""Restores the starting position."""
|
||
|
self.turn = WHITE
|
||
|
self.castling_rights = BB_CORNERS
|
||
|
self.ep_square = None
|
||
|
self.halfmove_clock = 0
|
||
|
self.fullmove_number = 1
|
||
|
|
||
|
self.reset_board()
|
||
|
|
||
|
def reset_board(self) -> None:
|
||
|
super().reset_board()
|
||
|
self.clear_stack()
|
||
|
|
||
|
def clear(self) -> None:
|
||
|
"""
|
||
|
Clears the board.
|
||
|
|
||
|
Resets move stack and move counters. The side to move is white. There
|
||
|
are no rooks or kings, so castling rights are removed.
|
||
|
|
||
|
In order to be in a valid :func:`~chess.Board.status()`, at least kings
|
||
|
need to be put on the board.
|
||
|
"""
|
||
|
self.turn = WHITE
|
||
|
self.castling_rights = BB_EMPTY
|
||
|
self.ep_square = None
|
||
|
self.halfmove_clock = 0
|
||
|
self.fullmove_number = 1
|
||
|
|
||
|
self.clear_board()
|
||
|
|
||
|
def clear_board(self) -> None:
|
||
|
super().clear_board()
|
||
|
self.clear_stack()
|
||
|
|
||
|
def clear_stack(self) -> None:
|
||
|
"""Clears the move stack."""
|
||
|
self.move_stack.clear()
|
||
|
self._stack.clear()
|
||
|
|
||
|
def root(self: BoardT) -> BoardT:
|
||
|
"""Returns a copy of the root position."""
|
||
|
if self._stack:
|
||
|
board = type(self)(None, chess960=self.chess960)
|
||
|
self._stack[0].restore(board)
|
||
|
return board
|
||
|
else:
|
||
|
return self.copy(stack=False)
|
||
|
|
||
|
def ply(self) -> int:
|
||
|
"""
|
||
|
Returns the number of half-moves since the start of the game, as
|
||
|
indicated by :data:`~chess.Board.fullmove_number` and
|
||
|
:data:`~chess.Board.turn`.
|
||
|
|
||
|
If moves have been pushed from the beginning, this is usually equal to
|
||
|
``len(board.move_stack)``. But note that a board can be set up with
|
||
|
arbitrary starting positions, and the stack can be cleared.
|
||
|
"""
|
||
|
return 2 * (self.fullmove_number - 1) + (self.turn == BLACK)
|
||
|
|
||
|
def remove_piece_at(self, square: Square) -> Optional[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:
|
||
|
super().set_piece_at(square, piece, promoted=promoted)
|
||
|
self.clear_stack()
|
||
|
|
||
|
def generate_pseudo_legal_moves(self, from_mask: Bitboard = BB_ALL, to_mask: Bitboard = BB_ALL) -> Iterator[Move]:
|
||
|
our_pieces = self.occupied_co[self.turn]
|
||
|
|
||
|
# Generate piece moves.
|
||
|
non_pawns = our_pieces & ~self.pawns & from_mask
|
||
|
for from_square in scan_reversed(non_pawns):
|
||
|
moves = self.attacks_mask(from_square) & ~our_pieces & to_mask
|
||
|
for to_square in scan_reversed(moves):
|
||
|
yield Move(from_square, to_square)
|
||
|
|
||
|
# Generate castling moves.
|
||
|
if from_mask & self.kings:
|
||
|
yield from self.generate_castling_moves(from_mask, to_mask)
|
||
|
|
||
|
# The remaining moves are all pawn moves.
|
||
|
pawns = self.pawns & self.occupied_co[self.turn] & from_mask
|
||
|
if not pawns:
|
||
|
return
|
||
|
|
||
|
# Generate pawn captures.
|
||
|
capturers = pawns
|
||
|
for from_square in scan_reversed(capturers):
|
||
|
targets = (
|
||
|
BB_PAWN_ATTACKS[self.turn][from_square] &
|
||
|
self.occupied_co[not self.turn] & to_mask)
|
||
|
|
||
|
for to_square in scan_reversed(targets):
|
||
|
if square_rank(to_square) in [0, 7]:
|
||
|
yield Move(from_square, to_square, QUEEN)
|
||
|
yield Move(from_square, to_square, ROOK)
|
||
|
yield Move(from_square, to_square, BISHOP)
|
||
|
yield Move(from_square, to_square, KNIGHT)
|
||
|
else:
|
||
|
yield Move(from_square, to_square)
|
||
|
|
||
|
# Prepare pawn advance generation.
|
||
|
if self.turn == WHITE:
|
||
|
single_moves = pawns << 8 & ~self.occupied
|
||
|
double_moves = single_moves << 8 & ~self.occupied & (BB_RANK_3 | BB_RANK_4)
|
||
|
else:
|
||
|
single_moves = pawns >> 8 & ~self.occupied
|
||
|
double_moves = single_moves >> 8 & ~self.occupied & (BB_RANK_6 | BB_RANK_5)
|
||
|
|
||
|
single_moves &= to_mask
|
||
|
double_moves &= to_mask
|
||
|
|
||
|
# Generate single pawn moves.
|
||
|
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]:
|
||
|
yield Move(from_square, to_square, QUEEN)
|
||
|
yield Move(from_square, to_square, ROOK)
|
||
|
yield Move(from_square, to_square, BISHOP)
|
||
|
yield Move(from_square, to_square, KNIGHT)
|
||
|
else:
|
||
|
yield Move(from_square, to_square)
|
||
|
|
||
|
# Generate double pawn moves.
|
||
|
for to_square in scan_reversed(double_moves):
|
||
|
from_square = to_square + (16 if self.turn == BLACK else -16)
|
||
|
yield Move(from_square, to_square)
|
||
|
|
||
|
# Generate en passant captures.
|
||
|
if self.ep_square:
|
||
|
yield from self.generate_pseudo_legal_ep(from_mask, to_mask)
|
||
|
|
||
|
def generate_pseudo_legal_ep(self, from_mask: Bitboard = BB_ALL, to_mask: Bitboard = BB_ALL) -> Iterator[Move]:
|
||
|
if not self.ep_square or not BB_SQUARES[self.ep_square] & to_mask:
|
||
|
return
|
||
|
|
||
|
if BB_SQUARES[self.ep_square] & self.occupied:
|
||
|
return
|
||
|
|
||
|
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])
|
||
|
|
||
|
for capturer in scan_reversed(capturers):
|
||
|
yield Move(capturer, self.ep_square)
|
||
|
|
||
|
def generate_pseudo_legal_captures(self, from_mask: Bitboard = BB_ALL, to_mask: Bitboard = BB_ALL) -> Iterator[Move]:
|
||
|
return itertools.chain(
|
||
|
self.generate_pseudo_legal_moves(from_mask, to_mask & self.occupied_co[not self.turn]),
|
||
|
self.generate_pseudo_legal_ep(from_mask, to_mask))
|
||
|
|
||
|
def checkers_mask(self) -> Bitboard:
|
||
|
king = self.king(self.turn)
|
||
|
return BB_EMPTY if king is None else self.attackers_mask(not self.turn, king)
|
||
|
|
||
|
def checkers(self) -> SquareSet:
|
||
|
"""
|
||
|
Gets the pieces currently giving check.
|
||
|
|
||
|
Returns a :class:`set of squares <chess.SquareSet>`.
|
||
|
"""
|
||
|
return SquareSet(self.checkers_mask())
|
||
|
|
||
|
def is_check(self) -> bool:
|
||
|
"""Tests if the current side to move is in check."""
|
||
|
return bool(self.checkers_mask())
|
||
|
|
||
|
def gives_check(self, move: Move) -> bool:
|
||
|
"""
|
||
|
Probes if the given move would put the opponent in check. The move
|
||
|
must be at least pseudo-legal.
|
||
|
"""
|
||
|
self.push(move)
|
||
|
try:
|
||
|
return self.is_check()
|
||
|
finally:
|
||
|
self.pop()
|
||
|
|
||
|
def is_into_check(self, move: Move) -> bool:
|
||
|
king = self.king(self.turn)
|
||
|
if king is None:
|
||
|
return False
|
||
|
|
||
|
# If already in check, look if it is an evasion.
|
||
|
checkers = self.attackers_mask(not self.turn, king)
|
||
|
if checkers and move not in self._generate_evasions(king, checkers, BB_SQUARES[move.from_square], BB_SQUARES[move.to_square]):
|
||
|
return True
|
||
|
|
||
|
return not self._is_safe(king, self._slider_blockers(king), move)
|
||
|
|
||
|
def was_into_check(self) -> bool:
|
||
|
king = self.king(not self.turn)
|
||
|
return king is not None and self.is_attacked_by(self.turn, king)
|
||
|
|
||
|
def is_pseudo_legal(self, move: Move) -> bool:
|
||
|
# Null moves are not pseudo-legal.
|
||
|
if not move:
|
||
|
return False
|
||
|
|
||
|
# Drops are not pseudo-legal.
|
||
|
if move.drop:
|
||
|
return False
|
||
|
|
||
|
# Source square must not be vacant.
|
||
|
piece = self.piece_type_at(move.from_square)
|
||
|
if not piece:
|
||
|
return False
|
||
|
|
||
|
# Get square masks.
|
||
|
from_mask = BB_SQUARES[move.from_square]
|
||
|
to_mask = BB_SQUARES[move.to_square]
|
||
|
|
||
|
# Check turn.
|
||
|
if not self.occupied_co[self.turn] & from_mask:
|
||
|
return False
|
||
|
|
||
|
# Only pawns can promote and only on the backrank.
|
||
|
if move.promotion:
|
||
|
if piece != PAWN:
|
||
|
return False
|
||
|
|
||
|
if self.turn == WHITE and square_rank(move.to_square) != 7:
|
||
|
return False
|
||
|
elif self.turn == BLACK and square_rank(move.to_square) != 0:
|
||
|
return False
|
||
|
|
||
|
# Handle castling.
|
||
|
if piece == KING:
|
||
|
move = self._from_chess960(self.chess960, move.from_square, move.to_square)
|
||
|
if move in self.generate_castling_moves():
|
||
|
return True
|
||
|
|
||
|
# Destination square can not be occupied.
|
||
|
if self.occupied_co[self.turn] & to_mask:
|
||
|
return False
|
||
|
|
||
|
# Handle pawn moves.
|
||
|
if piece == PAWN:
|
||
|
return move in self.generate_pseudo_legal_moves(from_mask, to_mask)
|
||
|
|
||
|
# Handle all other pieces.
|
||
|
return bool(self.attacks_mask(move.from_square) & to_mask)
|
||
|
|
||
|
def is_legal(self, move: Move) -> bool:
|
||
|
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:
|
||
|
"""
|
||
|
Checks if the game is over due to a special variant end condition.
|
||
|
|
||
|
Note, for example, that stalemate is not considered a variant-specific
|
||
|
end condition (this method will return ``False``), yet it can have a
|
||
|
special **result** in suicide chess (any of
|
||
|
:func:`~chess.Board.is_variant_loss()`,
|
||
|
:func:`~chess.Board.is_variant_win()`,
|
||
|
:func:`~chess.Board.is_variant_draw()` might return ``True``).
|
||
|
"""
|
||
|
return False
|
||
|
|
||
|
def is_variant_loss(self) -> bool:
|
||
|
"""
|
||
|
Checks if the current side to move lost due to a variant-specific
|
||
|
condition.
|
||
|
"""
|
||
|
return False
|
||
|
|
||
|
def is_variant_win(self) -> bool:
|
||
|
"""
|
||
|
Checks if the current side to move won due to a variant-specific
|
||
|
condition.
|
||
|
"""
|
||
|
return False
|
||
|
|
||
|
def is_variant_draw(self) -> bool:
|
||
|
"""
|
||
|
Checks if a variant-specific drawing condition is fulfilled.
|
||
|
"""
|
||
|
return False
|
||
|
|
||
|
def is_game_over(self, *, claim_draw: bool = False) -> bool:
|
||
|
return self.outcome(claim_draw=claim_draw) is not None
|
||
|
|
||
|
def result(self, *, claim_draw: bool = False) -> str:
|
||
|
outcome = self.outcome(claim_draw=claim_draw)
|
||
|
return outcome.result() if outcome else "*"
|
||
|
|
||
|
def outcome(self, *, claim_draw: bool = False) -> Optional[Outcome]:
|
||
|
"""
|
||
|
Checks if the game is over due to
|
||
|
:func:`checkmate <chess.Board.is_checkmate()>`,
|
||
|
:func:`stalemate <chess.Board.is_stalemate()>`,
|
||
|
:func:`insufficient material <chess.Board.is_insufficient_material()>`,
|
||
|
the :func:`seventyfive-move rule <chess.Board.is_seventyfive_moves()>`,
|
||
|
:func:`fivefold repetition <chess.Board.is_fivefold_repetition()>`,
|
||
|
or a :func:`variant end condition <chess.Board.is_variant_end()>`.
|
||
|
Returns the :class:`chess.Outcome` if the game has ended, otherwise
|
||
|
``None``.
|
||
|
|
||
|
Alternatively, use :func:`~chess.Board.is_game_over()` if you are not
|
||
|
interested in who won the game and why.
|
||
|
|
||
|
The game is not considered to be over by the
|
||
|
:func:`fifty-move rule <chess.Board.can_claim_fifty_moves()>` or
|
||
|
:func:`threefold repetition <chess.Board.can_claim_threefold_repetition()>`,
|
||
|
unless *claim_draw* is given. Note that checking the latter can be
|
||
|
slow.
|
||
|
"""
|
||
|
# Variant support.
|
||
|
if self.is_variant_loss():
|
||
|
return Outcome(Termination.VARIANT_LOSS, not self.turn)
|
||
|
if self.is_variant_win():
|
||
|
return Outcome(Termination.VARIANT_WIN, self.turn)
|
||
|
if self.is_variant_draw():
|
||
|
return Outcome(Termination.VARIANT_DRAW, None)
|
||
|
|
||
|
# Normal game end.
|
||
|
if self.is_checkmate():
|
||
|
return Outcome(Termination.CHECKMATE, not self.turn)
|
||
|
if self.is_insufficient_material():
|
||
|
return Outcome(Termination.INSUFFICIENT_MATERIAL, None)
|
||
|
if not any(self.generate_legal_moves()):
|
||
|
return Outcome(Termination.STALEMATE, None)
|
||
|
|
||
|
# Automatic draws.
|
||
|
if self.is_seventyfive_moves():
|
||
|
return Outcome(Termination.SEVENTYFIVE_MOVES, None)
|
||
|
if self.is_fivefold_repetition():
|
||
|
return Outcome(Termination.FIVEFOLD_REPETITION, None)
|
||
|
|
||
|
# Claimable draws.
|
||
|
if claim_draw:
|
||
|
if self.can_claim_fifty_moves():
|
||
|
return Outcome(Termination.FIFTY_MOVES, None)
|
||
|
if self.can_claim_threefold_repetition():
|
||
|
return Outcome(Termination.THREEFOLD_REPETITION, None)
|
||
|
|
||
|
return None
|
||
|
|
||
|
def is_checkmate(self) -> bool:
|
||
|
"""Checks if the current position is a checkmate."""
|
||
|
if not self.is_check():
|
||
|
return False
|
||
|
|
||
|
return not any(self.generate_legal_moves())
|
||
|
|
||
|
def is_stalemate(self) -> bool:
|
||
|
"""Checks if the current position is a stalemate."""
|
||
|
if self.is_check():
|
||
|
return False
|
||
|
|
||
|
if self.is_variant_end():
|
||
|
return False
|
||
|
|
||
|
return not any(self.generate_legal_moves())
|
||
|
|
||
|
def is_insufficient_material(self) -> bool:
|
||
|
"""
|
||
|
Checks if neither side has sufficient winning material
|
||
|
(:func:`~chess.Board.has_insufficient_material()`).
|
||
|
"""
|
||
|
return all(self.has_insufficient_material(color) for color in COLORS)
|
||
|
|
||
|
def has_insufficient_material(self, color: Color) -> bool:
|
||
|
"""
|
||
|
Checks if *color* has insufficient winning material.
|
||
|
|
||
|
This is guaranteed to return ``False`` if *color* can still win the
|
||
|
game.
|
||
|
|
||
|
The converse does not necessarily hold:
|
||
|
The implementation only looks at the material, including the colors
|
||
|
of bishops, but not considering piece positions. So fortress
|
||
|
positions or positions with forced lines may return ``False``, even
|
||
|
though there is no possible winning line.
|
||
|
"""
|
||
|
if self.occupied_co[color] & (self.pawns | self.rooks | self.queens):
|
||
|
return False
|
||
|
|
||
|
# Knights are only insufficient material if:
|
||
|
# (1) We do not have any other pieces, including more than one knight.
|
||
|
# (2) The opponent does not have pawns, knights, bishops or rooks.
|
||
|
# These would allow selfmate.
|
||
|
if self.occupied_co[color] & self.knights:
|
||
|
return (popcount(self.occupied_co[color]) <= 2 and
|
||
|
not (self.occupied_co[not color] & ~self.kings & ~self.queens))
|
||
|
|
||
|
# Bishops are only insufficient material if:
|
||
|
# (1) We do not have any other pieces, including bishops of the
|
||
|
# opposite color.
|
||
|
# (2) The opponent does not have bishops of the opposite color,
|
||
|
# pawns or knights. These would allow selfmate.
|
||
|
if self.occupied_co[color] & self.bishops:
|
||
|
same_color = (not self.bishops & BB_DARK_SQUARES) or (not self.bishops & BB_LIGHT_SQUARES)
|
||
|
return same_color and not self.pawns and not self.knights
|
||
|
|
||
|
return True
|
||
|
|
||
|
def _is_halfmoves(self, n: int) -> bool:
|
||
|
return self.halfmove_clock >= n and any(self.generate_legal_moves())
|
||
|
|
||
|
def is_seventyfive_moves(self) -> bool:
|
||
|
"""
|
||
|
Since the 1st of July 2014, a game is automatically drawn (without
|
||
|
a claim by one of the players) if the half-move clock since a capture
|
||
|
or pawn move is equal to or greater than 150. Other means to end a game
|
||
|
take precedence.
|
||
|
"""
|
||
|
return self._is_halfmoves(150)
|
||
|
|
||
|
def is_fivefold_repetition(self) -> bool:
|
||
|
"""
|
||
|
Since the 1st of July 2014 a game is automatically drawn (without
|
||
|
a claim by one of the players) if a position occurs for the fifth time.
|
||
|
Originally this had to occur on consecutive alternating moves, but
|
||
|
this has since been revised.
|
||
|
"""
|
||
|
return self.is_repetition(5)
|
||
|
|
||
|
def can_claim_draw(self) -> bool:
|
||
|
"""
|
||
|
Checks if the player to move can claim a draw by the fifty-move rule or
|
||
|
by threefold repetition.
|
||
|
|
||
|
Note that checking the latter can be slow.
|
||
|
"""
|
||
|
return self.can_claim_fifty_moves() or self.can_claim_threefold_repetition()
|
||
|
|
||
|
def is_fifty_moves(self) -> bool:
|
||
|
"""
|
||
|
Checks that the clock of halfmoves since the last capture or pawn move
|
||
|
is greater or equal to 100, and that no other means of ending the game
|
||
|
(like checkmate) take precedence.
|
||
|
"""
|
||
|
return self._is_halfmoves(100)
|
||
|
|
||
|
def can_claim_fifty_moves(self) -> bool:
|
||
|
"""
|
||
|
Checks if the player to move can claim a draw by the fifty-move rule.
|
||
|
|
||
|
In addition to :func:`~chess.Board.is_fifty_moves()`, the fifty-move
|
||
|
rule can also be claimed if there is a legal move that achieves this
|
||
|
condition.
|
||
|
"""
|
||
|
if self.is_fifty_moves():
|
||
|
return True
|
||
|
|
||
|
if self.halfmove_clock >= 99:
|
||
|
for move in self.generate_legal_moves():
|
||
|
if not self.is_zeroing(move):
|
||
|
self.push(move)
|
||
|
try:
|
||
|
if self.is_fifty_moves():
|
||
|
return True
|
||
|
finally:
|
||
|
self.pop()
|
||
|
|
||
|
return False
|
||
|
|
||
|
def can_claim_threefold_repetition(self) -> bool:
|
||
|
"""
|
||
|
Checks if the player to move can claim a draw by threefold repetition.
|
||
|
|
||
|
Draw by threefold repetition can be claimed if the position on the
|
||
|
board occurred for the third time or if such a repetition is reached
|
||
|
with one of the possible legal moves.
|
||
|
|
||
|
Note that checking this can be slow: In the worst case
|
||
|
scenario, every legal move has to be tested and the entire game has to
|
||
|
be replayed because there is no incremental transposition table.
|
||
|
"""
|
||
|
transposition_key = self._transposition_key()
|
||
|
transpositions: Counter[Hashable] = collections.Counter()
|
||
|
transpositions.update((transposition_key, ))
|
||
|
|
||
|
# Count positions.
|
||
|
switchyard = []
|
||
|
while self.move_stack:
|
||
|
move = self.pop()
|
||
|
switchyard.append(move)
|
||
|
|
||
|
if self.is_irreversible(move):
|
||
|
break
|
||
|
|
||
|
transpositions.update((self._transposition_key(), ))
|
||
|
|
||
|
while switchyard:
|
||
|
self.push(switchyard.pop())
|
||
|
|
||
|
# Threefold repetition occurred.
|
||
|
if transpositions[transposition_key] >= 3:
|
||
|
return True
|
||
|
|
||
|
# The next legal move is a threefold repetition.
|
||
|
for move in self.generate_legal_moves():
|
||
|
self.push(move)
|
||
|
try:
|
||
|
if transpositions[self._transposition_key()] >= 2:
|
||
|
return True
|
||
|
finally:
|
||
|
self.pop()
|
||
|
|
||
|
return False
|
||
|
|
||
|
def is_repetition(self, count: int = 3) -> bool:
|
||
|
"""
|
||
|
Checks if the current position has repeated 3 (or a given number of)
|
||
|
times.
|
||
|
|
||
|
Unlike :func:`~chess.Board.can_claim_threefold_repetition()`,
|
||
|
this does not consider a repetition that can be played on the next
|
||
|
move.
|
||
|
|
||
|
Note that checking this can be slow: In the worst case, the entire
|
||
|
game has to be replayed because there is no incremental transposition
|
||
|
table.
|
||
|
"""
|
||
|
# Fast check, based on occupancy only.
|
||
|
maybe_repetitions = 1
|
||
|
for state in reversed(self._stack):
|
||
|
if state.occupied == self.occupied:
|
||
|
maybe_repetitions += 1
|
||
|
if maybe_repetitions >= count:
|
||
|
break
|
||
|
if maybe_repetitions < count:
|
||
|
return False
|
||
|
|
||
|
# Check full replay.
|
||
|
transposition_key = self._transposition_key()
|
||
|
switchyard = []
|
||
|
|
||
|
try:
|
||
|
while True:
|
||
|
if count <= 1:
|
||
|
return True
|
||
|
|
||
|
if len(self.move_stack) < count - 1:
|
||
|
break
|
||
|
|
||
|
move = self.pop()
|
||
|
switchyard.append(move)
|
||
|
|
||
|
if self.is_irreversible(move):
|
||
|
break
|
||
|
|
||
|
if self._transposition_key() == transposition_key:
|
||
|
count -= 1
|
||
|
finally:
|
||
|
while switchyard:
|
||
|
self.push(switchyard.pop())
|
||
|
|
||
|
return False
|
||
|
|
||
|
def _board_state(self: BoardT) -> _BoardState[BoardT]:
|
||
|
return _BoardState(self)
|
||
|
|
||
|
def _push_capture(self, move: Move, capture_square: Square, piece_type: PieceType, was_promoted: bool) -> None:
|
||
|
pass
|
||
|
|
||
|
def push(self: BoardT, move: Move) -> None:
|
||
|
"""
|
||
|
Updates the position with the given *move* and puts it onto the
|
||
|
move stack.
|
||
|
|
||
|
>>> import chess
|
||
|
>>>
|
||
|
>>> board = chess.Board()
|
||
|
>>>
|
||
|
>>> Nf3 = chess.Move.from_uci("g1f3")
|
||
|
>>> board.push(Nf3) # Make the move
|
||
|
|
||
|
>>> board.pop() # Unmake the last move
|
||
|
Move.from_uci('g1f3')
|
||
|
|
||
|
Null moves just increment the move counters, switch turns and forfeit
|
||
|
en passant capturing.
|
||
|
|
||
|
.. warning::
|
||
|
Moves are not checked for legality. It is the caller's
|
||
|
responsibility to ensure that the move is at least pseudo-legal or
|
||
|
a null move.
|
||
|
"""
|
||
|
# Push move and remember board state.
|
||
|
move = self._to_chess960(move)
|
||
|
board_state = self._board_state()
|
||
|
self.castling_rights = self.clean_castling_rights() # Before pushing stack
|
||
|
self.move_stack.append(self._from_chess960(self.chess960, move.from_square, move.to_square, move.promotion, move.drop))
|
||
|
self._stack.append(board_state)
|
||
|
|
||
|
# Reset en passant square.
|
||
|
ep_square = self.ep_square
|
||
|
self.ep_square = None
|
||
|
|
||
|
# Increment move counters.
|
||
|
self.halfmove_clock += 1
|
||
|
if self.turn == BLACK:
|
||
|
self.fullmove_number += 1
|
||
|
|
||
|
# On a null move, simply swap turns and reset the en passant square.
|
||
|
if not move:
|
||
|
self.turn = not self.turn
|
||
|
return
|
||
|
|
||
|
# Drops.
|
||
|
if move.drop:
|
||
|
self._set_piece_at(move.to_square, move.drop, self.turn)
|
||
|
self.turn = not self.turn
|
||
|
return
|
||
|
|
||
|
# Zero the half-move clock.
|
||
|
if self.is_zeroing(move):
|
||
|
self.halfmove_clock = 0
|
||
|
|
||
|
from_bb = BB_SQUARES[move.from_square]
|
||
|
to_bb = BB_SQUARES[move.to_square]
|
||
|
|
||
|
promoted = bool(self.promoted & from_bb)
|
||
|
piece_type = self._remove_piece_at(move.from_square)
|
||
|
assert piece_type is not None, f"push() expects move to be pseudo-legal, but got {move} in {self.board_fen()}"
|
||
|
capture_square = move.to_square
|
||
|
captured_piece_type = self.piece_type_at(capture_square)
|
||
|
|
||
|
# Update castling rights.
|
||
|
self.castling_rights &= ~to_bb & ~from_bb
|
||
|
if piece_type == KING and not promoted:
|
||
|
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:
|
||
|
if self.turn == WHITE and square_rank(move.to_square) == 7:
|
||
|
self.castling_rights &= ~BB_RANK_8
|
||
|
elif self.turn == BLACK and square_rank(move.to_square) == 0:
|
||
|
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:
|
||
|
self.ep_square = move.from_square + 8
|
||
|
elif diff == -16 and square_rank(move.from_square) == 6:
|
||
|
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.
|
||
|
down = -8 if self.turn == WHITE else 8
|
||
|
capture_square = ep_square + down
|
||
|
captured_piece_type = self._remove_piece_at(capture_square)
|
||
|
|
||
|
# Promotion.
|
||
|
if move.promotion:
|
||
|
promoted = True
|
||
|
piece_type = move.promotion
|
||
|
|
||
|
# Castling.
|
||
|
castling = piece_type == KING and self.occupied_co[self.turn] & to_bb
|
||
|
if castling:
|
||
|
a_side = square_file(move.to_square) < square_file(move.from_square)
|
||
|
|
||
|
self._remove_piece_at(move.from_square)
|
||
|
self._remove_piece_at(move.to_square)
|
||
|
|
||
|
if a_side:
|
||
|
self._set_piece_at(C1 if self.turn == WHITE else C8, KING, self.turn)
|
||
|
self._set_piece_at(D1 if self.turn == WHITE else D8, ROOK, self.turn)
|
||
|
else:
|
||
|
self._set_piece_at(G1 if self.turn == WHITE else G8, KING, self.turn)
|
||
|
self._set_piece_at(F1 if self.turn == WHITE else F8, ROOK, self.turn)
|
||
|
|
||
|
# Put the piece on the target square.
|
||
|
if not castling:
|
||
|
was_promoted = bool(self.promoted & to_bb)
|
||
|
self._set_piece_at(move.to_square, piece_type, self.turn, promoted)
|
||
|
|
||
|
if captured_piece_type:
|
||
|
self._push_capture(move, capture_square, captured_piece_type, was_promoted)
|
||
|
|
||
|
# Swap turn.
|
||
|
self.turn = not self.turn
|
||
|
|
||
|
def pop(self: BoardT) -> Move:
|
||
|
"""
|
||
|
Restores the previous position and returns the last move from the stack.
|
||
|
|
||
|
:raises: :exc:`IndexError` if the move stack is empty.
|
||
|
"""
|
||
|
move = self.move_stack.pop()
|
||
|
self._stack.pop().restore(self)
|
||
|
return move
|
||
|
|
||
|
def peek(self) -> Move:
|
||
|
"""
|
||
|
Gets the last move from the move stack.
|
||
|
|
||
|
:raises: :exc:`IndexError` if the move stack is empty.
|
||
|
"""
|
||
|
return self.move_stack[-1]
|
||
|
|
||
|
def find_move(self, from_square: Square, to_square: Square, promotion: Optional[PieceType] = None) -> Move:
|
||
|
"""
|
||
|
Finds a matching legal move for an origin square, a target square, and
|
||
|
an optional promotion piece type.
|
||
|
|
||
|
For pawn moves to the backrank, the promotion piece type defaults to
|
||
|
:data:`chess.QUEEN`, unless otherwise specified.
|
||
|
|
||
|
Castling moves are normalized to king moves by two steps, except in
|
||
|
Chess960.
|
||
|
|
||
|
:raises: :exc:`IllegalMoveError` if no matching legal move is found.
|
||
|
"""
|
||
|
if promotion is None and self.pawns & BB_SQUARES[from_square] and BB_SQUARES[to_square] & BB_BACKRANKS:
|
||
|
promotion = QUEEN
|
||
|
|
||
|
move = self._from_chess960(self.chess960, from_square, to_square, promotion)
|
||
|
if not self.is_legal(move):
|
||
|
raise IllegalMoveError(f"no matching legal move for {move.uci()} ({SQUARE_NAMES[from_square]} -> {SQUARE_NAMES[to_square]}) in {self.fen()}")
|
||
|
|
||
|
return move
|
||
|
|
||
|
def castling_shredder_fen(self) -> str:
|
||
|
castling_rights = self.clean_castling_rights()
|
||
|
if not castling_rights:
|
||
|
return "-"
|
||
|
|
||
|
builder = []
|
||
|
|
||
|
for square in scan_reversed(castling_rights & BB_RANK_1):
|
||
|
builder.append(FILE_NAMES[square_file(square)].upper())
|
||
|
|
||
|
for square in scan_reversed(castling_rights & BB_RANK_8):
|
||
|
builder.append(FILE_NAMES[square_file(square)])
|
||
|
|
||
|
return "".join(builder)
|
||
|
|
||
|
def castling_xfen(self) -> str:
|
||
|
builder = []
|
||
|
|
||
|
for color in COLORS:
|
||
|
king = self.king(color)
|
||
|
if king is None:
|
||
|
continue
|
||
|
|
||
|
king_file = square_file(king)
|
||
|
backrank = BB_RANK_1 if color == WHITE else BB_RANK_8
|
||
|
|
||
|
for rook_square in scan_reversed(self.clean_castling_rights() & backrank):
|
||
|
rook_file = square_file(rook_square)
|
||
|
a_side = rook_file < king_file
|
||
|
|
||
|
other_rooks = self.occupied_co[color] & self.rooks & backrank & ~BB_SQUARES[rook_square]
|
||
|
|
||
|
if any((square_file(other) < rook_file) == a_side for other in scan_reversed(other_rooks)):
|
||
|
ch = FILE_NAMES[rook_file]
|
||
|
else:
|
||
|
ch = "q" if a_side else "k"
|
||
|
|
||
|
builder.append(ch.upper() if color == WHITE else ch)
|
||
|
|
||
|
if builder:
|
||
|
return "".join(builder)
|
||
|
else:
|
||
|
return "-"
|
||
|
|
||
|
def has_pseudo_legal_en_passant(self) -> bool:
|
||
|
"""Checks if there is a pseudo-legal en passant capture."""
|
||
|
return self.ep_square is not None and any(self.generate_pseudo_legal_ep())
|
||
|
|
||
|
def has_legal_en_passant(self) -> bool:
|
||
|
"""Checks if there is a legal en passant capture."""
|
||
|
return self.ep_square is not None and any(self.generate_legal_ep())
|
||
|
|
||
|
def fen(self, *, shredder: bool = False, en_passant: _EnPassantSpec = "legal", promoted: Optional[bool] = None) -> str:
|
||
|
"""
|
||
|
Gets a FEN representation of the position.
|
||
|
|
||
|
A FEN string (e.g.,
|
||
|
``rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1``) consists
|
||
|
of the board part :func:`~chess.Board.board_fen()`, the
|
||
|
:data:`~chess.Board.turn`, the castling part
|
||
|
(:data:`~chess.Board.castling_rights`),
|
||
|
the en passant square (:data:`~chess.Board.ep_square`),
|
||
|
the :data:`~chess.Board.halfmove_clock`
|
||
|
and the :data:`~chess.Board.fullmove_number`.
|
||
|
|
||
|
:param shredder: Use :func:`~chess.Board.castling_shredder_fen()`
|
||
|
and encode castling rights by the file of the rook
|
||
|
(like ``HAha``) instead of the default
|
||
|
:func:`~chess.Board.castling_xfen()` (like ``KQkq``).
|
||
|
:param en_passant: By default, only fully legal en passant squares
|
||
|
are included (:func:`~chess.Board.has_legal_en_passant()`).
|
||
|
Pass ``fen`` to strictly follow the FEN specification
|
||
|
(always include the en passant square after a two-step pawn move)
|
||
|
or ``xfen`` to follow the X-FEN specification
|
||
|
(:func:`~chess.Board.has_pseudo_legal_en_passant()`).
|
||
|
:param promoted: Mark promoted pieces like ``Q~``. By default, this is
|
||
|
only enabled in chess variants where this is relevant.
|
||
|
"""
|
||
|
return " ".join([
|
||
|
self.epd(shredder=shredder, en_passant=en_passant, promoted=promoted),
|
||
|
str(self.halfmove_clock),
|
||
|
str(self.fullmove_number)
|
||
|
])
|
||
|
|
||
|
def shredder_fen(self, *, en_passant: _EnPassantSpec = "legal", promoted: Optional[bool] = None) -> str:
|
||
|
return " ".join([
|
||
|
self.epd(shredder=True, en_passant=en_passant, promoted=promoted),
|
||
|
str(self.halfmove_clock),
|
||
|
str(self.fullmove_number)
|
||
|
])
|
||
|
|
||
|
def set_fen(self, fen: str) -> None:
|
||
|
"""
|
||
|
Parses a FEN and sets the position from it.
|
||
|
|
||
|
:raises: :exc:`ValueError` if syntactically invalid. Use
|
||
|
:func:`~chess.Board.is_valid()` to detect invalid positions.
|
||
|
"""
|
||
|
parts = fen.split()
|
||
|
|
||
|
# Board part.
|
||
|
try:
|
||
|
board_part = parts.pop(0)
|
||
|
except IndexError:
|
||
|
raise ValueError("empty fen")
|
||
|
|
||
|
# Turn.
|
||
|
try:
|
||
|
turn_part = parts.pop(0)
|
||
|
except IndexError:
|
||
|
turn = WHITE
|
||
|
else:
|
||
|
if turn_part == "w":
|
||
|
turn = WHITE
|
||
|
elif turn_part == "b":
|
||
|
turn = BLACK
|
||
|
else:
|
||
|
raise ValueError(f"expected 'w' or 'b' for turn part of fen: {fen!r}")
|
||
|
|
||
|
# Validate castling part.
|
||
|
try:
|
||
|
castling_part = parts.pop(0)
|
||
|
except IndexError:
|
||
|
castling_part = "-"
|
||
|
else:
|
||
|
if not FEN_CASTLING_REGEX.match(castling_part):
|
||
|
raise ValueError(f"invalid castling part in fen: {fen!r}")
|
||
|
|
||
|
# En passant square.
|
||
|
try:
|
||
|
ep_part = parts.pop(0)
|
||
|
except IndexError:
|
||
|
ep_square = None
|
||
|
else:
|
||
|
try:
|
||
|
ep_square = None if ep_part == "-" else SQUARE_NAMES.index(ep_part)
|
||
|
except ValueError:
|
||
|
raise ValueError(f"invalid en passant part in fen: {fen!r}")
|
||
|
|
||
|
# Check that the half-move part is valid.
|
||
|
try:
|
||
|
halfmove_part = parts.pop(0)
|
||
|
except IndexError:
|
||
|
halfmove_clock = 0
|
||
|
else:
|
||
|
try:
|
||
|
halfmove_clock = int(halfmove_part)
|
||
|
except ValueError:
|
||
|
raise ValueError(f"invalid half-move clock in fen: {fen!r}")
|
||
|
|
||
|
if halfmove_clock < 0:
|
||
|
raise ValueError(f"half-move clock cannot be negative: {fen!r}")
|
||
|
|
||
|
# Check that the full-move number part is valid.
|
||
|
# 0 is allowed for compatibility, but later replaced with 1.
|
||
|
try:
|
||
|
fullmove_part = parts.pop(0)
|
||
|
except IndexError:
|
||
|
fullmove_number = 1
|
||
|
else:
|
||
|
try:
|
||
|
fullmove_number = int(fullmove_part)
|
||
|
except ValueError:
|
||
|
raise ValueError(f"invalid fullmove number in fen: {fen!r}")
|
||
|
|
||
|
if fullmove_number < 0:
|
||
|
raise ValueError(f"fullmove number cannot be negative: {fen!r}")
|
||
|
|
||
|
fullmove_number = max(fullmove_number, 1)
|
||
|
|
||
|
# All parts should be consumed now.
|
||
|
if parts:
|
||
|
raise ValueError(f"fen string has more parts than expected: {fen!r}")
|
||
|
|
||
|
# Validate the board part and set it.
|
||
|
self._set_board_fen(board_part)
|
||
|
|
||
|
# Apply.
|
||
|
self.turn = turn
|
||
|
self._set_castling_fen(castling_part)
|
||
|
self.ep_square = ep_square
|
||
|
self.halfmove_clock = halfmove_clock
|
||
|
self.fullmove_number = fullmove_number
|
||
|
self.clear_stack()
|
||
|
|
||
|
def _set_castling_fen(self, castling_fen: str) -> None:
|
||
|
if not castling_fen or castling_fen == "-":
|
||
|
self.castling_rights = BB_EMPTY
|
||
|
return
|
||
|
|
||
|
if not FEN_CASTLING_REGEX.match(castling_fen):
|
||
|
raise ValueError(f"invalid castling fen: {castling_fen!r}")
|
||
|
|
||
|
self.castling_rights = BB_EMPTY
|
||
|
|
||
|
for flag in castling_fen:
|
||
|
color = WHITE if flag.isupper() else BLACK
|
||
|
flag = flag.lower()
|
||
|
backrank = BB_RANK_1 if color == WHITE else BB_RANK_8
|
||
|
rooks = self.occupied_co[color] & self.rooks & backrank
|
||
|
king = self.king(color)
|
||
|
|
||
|
if flag == "q":
|
||
|
# Select the leftmost rook.
|
||
|
if king is not None and lsb(rooks) < king:
|
||
|
self.castling_rights |= rooks & -rooks
|
||
|
else:
|
||
|
self.castling_rights |= BB_FILE_A & backrank
|
||
|
elif flag == "k":
|
||
|
# Select the rightmost rook.
|
||
|
rook = msb(rooks)
|
||
|
if king is not None and king < rook:
|
||
|
self.castling_rights |= BB_SQUARES[rook]
|
||
|
else:
|
||
|
self.castling_rights |= BB_FILE_H & backrank
|
||
|
else:
|
||
|
self.castling_rights |= BB_FILES[FILE_NAMES.index(flag)] & backrank
|
||
|
|
||
|
def set_castling_fen(self, castling_fen: str) -> None:
|
||
|
"""
|
||
|
Sets castling rights from a string in FEN notation like ``Qqk``.
|
||
|
|
||
|
Also clears the move stack.
|
||
|
|
||
|
:raises: :exc:`ValueError` if the castling FEN is syntactically
|
||
|
invalid.
|
||
|
"""
|
||
|
self._set_castling_fen(castling_fen)
|
||
|
self.clear_stack()
|
||
|
|
||
|
def set_board_fen(self, fen: str) -> None:
|
||
|
super().set_board_fen(fen)
|
||
|
self.clear_stack()
|
||
|
|
||
|
def set_piece_map(self, pieces: Mapping[Square, Piece]) -> None:
|
||
|
super().set_piece_map(pieces)
|
||
|
self.clear_stack()
|
||
|
|
||
|
def set_chess960_pos(self, scharnagl: int) -> None:
|
||
|
super().set_chess960_pos(scharnagl)
|
||
|
self.chess960 = True
|
||
|
self.turn = WHITE
|
||
|
self.castling_rights = self.rooks
|
||
|
self.ep_square = None
|
||
|
self.halfmove_clock = 0
|
||
|
self.fullmove_number = 1
|
||
|
|
||
|
self.clear_stack()
|
||
|
|
||
|
def chess960_pos(self, *, ignore_turn: bool = False, ignore_castling: bool = False, ignore_counters: bool = True) -> Optional[int]:
|
||
|
"""
|
||
|
Gets the Chess960 starting position index between 0 and 956,
|
||
|
or ``None`` if the current position is not a Chess960 starting
|
||
|
position.
|
||
|
|
||
|
By default, white to move (**ignore_turn**) and full castling rights
|
||
|
(**ignore_castling**) are required, but move counters
|
||
|
(**ignore_counters**) are ignored.
|
||
|
"""
|
||
|
if self.ep_square:
|
||
|
return None
|
||
|
|
||
|
if not ignore_turn:
|
||
|
if self.turn != WHITE:
|
||
|
return None
|
||
|
|
||
|
if not ignore_castling:
|
||
|
if self.clean_castling_rights() != self.rooks:
|
||
|
return None
|
||
|
|
||
|
if not ignore_counters:
|
||
|
if self.fullmove_number != 1 or self.halfmove_clock != 0:
|
||
|
return None
|
||
|
|
||
|
return super().chess960_pos()
|
||
|
|
||
|
def _epd_operations(self, operations: Mapping[str, Union[None, str, int, float, Move, Iterable[Move]]]) -> str:
|
||
|
epd = []
|
||
|
first_op = True
|
||
|
|
||
|
for opcode, operand in operations.items():
|
||
|
assert opcode != "-", "dash (-) is not a valid epd opcode"
|
||
|
for blacklisted in [" ", "\n", "\t", "\r"]:
|
||
|
assert blacklisted not in opcode, f"invalid character {blacklisted!r} in epd opcode: {opcode!r}"
|
||
|
|
||
|
if not first_op:
|
||
|
epd.append(" ")
|
||
|
first_op = False
|
||
|
epd.append(opcode)
|
||
|
|
||
|
if operand is None:
|
||
|
epd.append(";")
|
||
|
elif isinstance(operand, Move):
|
||
|
epd.append(" ")
|
||
|
epd.append(self.san(operand))
|
||
|
epd.append(";")
|
||
|
elif isinstance(operand, int):
|
||
|
epd.append(f" {operand};")
|
||
|
elif isinstance(operand, float):
|
||
|
assert math.isfinite(operand), f"expected numeric epd operand to be finite, got: {operand}"
|
||
|
epd.append(f" {operand};")
|
||
|
elif opcode == "pv" and not isinstance(operand, str) and hasattr(operand, "__iter__"):
|
||
|
position = self.copy(stack=False)
|
||
|
for move in operand:
|
||
|
epd.append(" ")
|
||
|
epd.append(position.san_and_push(move))
|
||
|
epd.append(";")
|
||
|
elif opcode in ["am", "bm"] and not isinstance(operand, str) and hasattr(operand, "__iter__"):
|
||
|
for san in sorted(self.san(move) for move in operand):
|
||
|
epd.append(" ")
|
||
|
epd.append(san)
|
||
|
epd.append(";")
|
||
|
else:
|
||
|
# Append as escaped string.
|
||
|
epd.append(" \"")
|
||
|
epd.append(str(operand).replace("\\", "\\\\").replace("\t", "\\t").replace("\r", "\\r").replace("\n", "\\n").replace("\"", "\\\""))
|
||
|
epd.append("\";")
|
||
|
|
||
|
return "".join(epd)
|
||
|
|
||
|
def epd(self, *, shredder: bool = False, en_passant: _EnPassantSpec = "legal", promoted: Optional[bool] = None, **operations: Union[None, str, int, float, Move, Iterable[Move]]) -> str:
|
||
|
"""
|
||
|
Gets an EPD representation of the current position.
|
||
|
|
||
|
See :func:`~chess.Board.fen()` for FEN formatting options (*shredder*,
|
||
|
*ep_square* and *promoted*).
|
||
|
|
||
|
EPD operations can be given as keyword arguments. Supported operands
|
||
|
are strings, integers, finite floats, legal moves and ``None``.
|
||
|
Additionally, the operation ``pv`` accepts a legal variation as
|
||
|
a list of moves. The operations ``am`` and ``bm`` accept a list of
|
||
|
legal moves in the current position.
|
||
|
|
||
|
The name of the field cannot be a lone dash and cannot contain spaces,
|
||
|
newlines, carriage returns or tabs.
|
||
|
|
||
|
*hmvc* and *fmvn* are not included by default. You can use:
|
||
|
|
||
|
>>> import chess
|
||
|
>>>
|
||
|
>>> board = chess.Board()
|
||
|
>>> board.epd(hmvc=board.halfmove_clock, fmvn=board.fullmove_number)
|
||
|
'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - hmvc 0; fmvn 1;'
|
||
|
"""
|
||
|
if en_passant == "fen":
|
||
|
ep_square = self.ep_square
|
||
|
elif en_passant == "xfen":
|
||
|
ep_square = self.ep_square if self.has_pseudo_legal_en_passant() else None
|
||
|
else:
|
||
|
ep_square = self.ep_square if self.has_legal_en_passant() else None
|
||
|
|
||
|
epd = [self.board_fen(promoted=promoted),
|
||
|
"w" if self.turn == WHITE else "b",
|
||
|
self.castling_shredder_fen() if shredder else self.castling_xfen(),
|
||
|
SQUARE_NAMES[ep_square] if ep_square is not None else "-"]
|
||
|
|
||
|
if operations:
|
||
|
epd.append(self._epd_operations(operations))
|
||
|
|
||
|
return " ".join(epd)
|
||
|
|
||
|
def _parse_epd_ops(self: BoardT, operation_part: str, make_board: Callable[[], BoardT]) -> Dict[str, Union[None, str, int, float, Move, List[Move]]]:
|
||
|
operations: Dict[str, Union[None, str, int, float, Move, List[Move]]] = {}
|
||
|
state = "opcode"
|
||
|
opcode = ""
|
||
|
operand = ""
|
||
|
position = None
|
||
|
|
||
|
for ch in itertools.chain(operation_part, [None]):
|
||
|
if state == "opcode":
|
||
|
if ch in [" ", "\t", "\r", "\n"]:
|
||
|
if opcode == "-":
|
||
|
opcode = ""
|
||
|
elif opcode:
|
||
|
state = "after_opcode"
|
||
|
elif ch is None or ch == ";":
|
||
|
if opcode == "-":
|
||
|
opcode = ""
|
||
|
elif opcode:
|
||
|
operations[opcode] = [] if opcode in ["pv", "am", "bm"] else None
|
||
|
opcode = ""
|
||
|
else:
|
||
|
opcode += ch
|
||
|
elif state == "after_opcode":
|
||
|
if ch in [" ", "\t", "\r", "\n"]:
|
||
|
pass
|
||
|
elif ch == "\"":
|
||
|
state = "string"
|
||
|
elif ch is None or ch == ";":
|
||
|
if opcode:
|
||
|
operations[opcode] = [] if opcode in ["pv", "am", "bm"] else None
|
||
|
opcode = ""
|
||
|
state = "opcode"
|
||
|
elif ch in "+-.0123456789":
|
||
|
operand = ch
|
||
|
state = "numeric"
|
||
|
else:
|
||
|
operand = ch
|
||
|
state = "san"
|
||
|
elif state == "numeric":
|
||
|
if ch is None or ch == ";":
|
||
|
if "." in operand or "e" in operand or "E" in operand:
|
||
|
parsed = float(operand)
|
||
|
if not math.isfinite(parsed):
|
||
|
raise ValueError(f"invalid numeric operand for epd operation {opcode!r}: {operand!r}")
|
||
|
operations[opcode] = parsed
|
||
|
else:
|
||
|
operations[opcode] = int(operand)
|
||
|
opcode = ""
|
||
|
operand = ""
|
||
|
state = "opcode"
|
||
|
else:
|
||
|
operand += ch
|
||
|
elif state == "string":
|
||
|
if ch is None or ch == "\"":
|
||
|
operations[opcode] = operand
|
||
|
opcode = ""
|
||
|
operand = ""
|
||
|
state = "opcode"
|
||
|
elif ch == "\\":
|
||
|
state = "string_escape"
|
||
|
else:
|
||
|
operand += ch
|
||
|
elif state == "string_escape":
|
||
|
if ch is None:
|
||
|
operations[opcode] = operand
|
||
|
opcode = ""
|
||
|
operand = ""
|
||
|
state = "opcode"
|
||
|
elif ch == "r":
|
||
|
operand += "\r"
|
||
|
state = "string"
|
||
|
elif ch == "n":
|
||
|
operand += "\n"
|
||
|
state = "string"
|
||
|
elif ch == "t":
|
||
|
operand += "\t"
|
||
|
state = "string"
|
||
|
else:
|
||
|
operand += ch
|
||
|
state = "string"
|
||
|
elif state == "san":
|
||
|
if ch is None or ch == ";":
|
||
|
if position is None:
|
||
|
position = make_board()
|
||
|
|
||
|
if opcode == "pv":
|
||
|
# A variation.
|
||
|
variation = []
|
||
|
for token in operand.split():
|
||
|
move = position.parse_xboard(token)
|
||
|
variation.append(move)
|
||
|
position.push(move)
|
||
|
|
||
|
# Reset the position.
|
||
|
while position.move_stack:
|
||
|
position.pop()
|
||
|
|
||
|
operations[opcode] = variation
|
||
|
elif opcode in ["bm", "am"]:
|
||
|
# A set of moves.
|
||
|
operations[opcode] = [position.parse_xboard(token) for token in operand.split()]
|
||
|
else:
|
||
|
# A single move.
|
||
|
operations[opcode] = position.parse_xboard(operand)
|
||
|
|
||
|
opcode = ""
|
||
|
operand = ""
|
||
|
state = "opcode"
|
||
|
else:
|
||
|
operand += ch
|
||
|
|
||
|
assert state == "opcode"
|
||
|
return operations
|
||
|
|
||
|
def set_epd(self, epd: str) -> Dict[str, Union[None, str, int, float, Move, List[Move]]]:
|
||
|
"""
|
||
|
Parses the given EPD string and uses it to set the position.
|
||
|
|
||
|
If present, ``hmvc`` and ``fmvn`` are used to set the half-move
|
||
|
clock and the full-move number. Otherwise, ``0`` and ``1`` are used.
|
||
|
|
||
|
Returns a dictionary of parsed operations. Values can be strings,
|
||
|
integers, floats, move objects, or lists of moves.
|
||
|
|
||
|
:raises: :exc:`ValueError` if the EPD string is invalid.
|
||
|
"""
|
||
|
parts = epd.strip().rstrip(";").split(None, 4)
|
||
|
|
||
|
# Parse ops.
|
||
|
if len(parts) > 4:
|
||
|
operations = self._parse_epd_ops(parts.pop(), lambda: type(self)(" ".join(parts) + " 0 1"))
|
||
|
parts.append(str(operations["hmvc"]) if "hmvc" in operations else "0")
|
||
|
parts.append(str(operations["fmvn"]) if "fmvn" in operations else "1")
|
||
|
self.set_fen(" ".join(parts))
|
||
|
return operations
|
||
|
else:
|
||
|
self.set_fen(epd)
|
||
|
return {}
|
||
|
|
||
|
def san(self, move: Move) -> str:
|
||
|
"""
|
||
|
Gets the standard algebraic notation of the given move in the context
|
||
|
of the current position.
|
||
|
"""
|
||
|
return self._algebraic(move)
|
||
|
|
||
|
def lan(self, move: Move) -> str:
|
||
|
"""
|
||
|
Gets the long algebraic notation of the given move in the context of
|
||
|
the current position.
|
||
|
"""
|
||
|
return self._algebraic(move, long=True)
|
||
|
|
||
|
def san_and_push(self, move: Move) -> str:
|
||
|
return self._algebraic_and_push(move)
|
||
|
|
||
|
def _algebraic(self, move: Move, *, long: bool = False) -> str:
|
||
|
san = self._algebraic_and_push(move, long=long)
|
||
|
self.pop()
|
||
|
return san
|
||
|
|
||
|
def _algebraic_and_push(self, move: Move, *, long: bool = False) -> str:
|
||
|
san = self._algebraic_without_suffix(move, long=long)
|
||
|
|
||
|
# Look ahead for check or checkmate.
|
||
|
self.push(move)
|
||
|
is_check = self.is_check()
|
||
|
is_checkmate = (is_check and self.is_checkmate()) or self.is_variant_loss() or self.is_variant_win()
|
||
|
|
||
|
# Add check or checkmate suffix.
|
||
|
if is_checkmate and move:
|
||
|
return san + "#"
|
||
|
elif is_check and move:
|
||
|
return san + "+"
|
||
|
else:
|
||
|
return san
|
||
|
|
||
|
def _algebraic_without_suffix(self, move: Move, *, long: bool = False) -> str:
|
||
|
# Null move.
|
||
|
if not move:
|
||
|
return "--"
|
||
|
|
||
|
# Drops.
|
||
|
if move.drop:
|
||
|
san = ""
|
||
|
if move.drop != PAWN:
|
||
|
san = piece_symbol(move.drop).upper()
|
||
|
san += "@" + SQUARE_NAMES[move.to_square]
|
||
|
return san
|
||
|
|
||
|
# Castling.
|
||
|
if self.is_castling(move):
|
||
|
if square_file(move.to_square) < square_file(move.from_square):
|
||
|
return "O-O-O"
|
||
|
else:
|
||
|
return "O-O"
|
||
|
|
||
|
piece_type = self.piece_type_at(move.from_square)
|
||
|
assert piece_type, f"san() and lan() expect move to be legal or null, but got {move} in {self.fen()}"
|
||
|
capture = self.is_capture(move)
|
||
|
|
||
|
if piece_type == PAWN:
|
||
|
san = ""
|
||
|
else:
|
||
|
san = piece_symbol(piece_type).upper()
|
||
|
|
||
|
if long:
|
||
|
san += SQUARE_NAMES[move.from_square]
|
||
|
elif piece_type != PAWN:
|
||
|
# Get ambiguous move candidates.
|
||
|
# Relevant candidates: not exactly the current move,
|
||
|
# but to the same square.
|
||
|
others = 0
|
||
|
from_mask = self.pieces_mask(piece_type, self.turn)
|
||
|
from_mask &= ~BB_SQUARES[move.from_square]
|
||
|
to_mask = BB_SQUARES[move.to_square]
|
||
|
for candidate in self.generate_legal_moves(from_mask, to_mask):
|
||
|
others |= BB_SQUARES[candidate.from_square]
|
||
|
|
||
|
# Disambiguate.
|
||
|
if others:
|
||
|
row, column = False, False
|
||
|
|
||
|
if others & BB_RANKS[square_rank(move.from_square)]:
|
||
|
column = True
|
||
|
|
||
|
if others & BB_FILES[square_file(move.from_square)]:
|
||
|
row = True
|
||
|
else:
|
||
|
column = True
|
||
|
|
||
|
if column:
|
||
|
san += FILE_NAMES[square_file(move.from_square)]
|
||
|
if row:
|
||
|
san += RANK_NAMES[square_rank(move.from_square)]
|
||
|
elif capture:
|
||
|
san += FILE_NAMES[square_file(move.from_square)]
|
||
|
|
||
|
# Captures.
|
||
|
if capture:
|
||
|
san += "x"
|
||
|
elif long:
|
||
|
san += "-"
|
||
|
|
||
|
# Destination square.
|
||
|
san += SQUARE_NAMES[move.to_square]
|
||
|
|
||
|
# Promotion.
|
||
|
if move.promotion:
|
||
|
san += "=" + piece_symbol(move.promotion).upper()
|
||
|
|
||
|
return san
|
||
|
|
||
|
def variation_san(self, variation: Iterable[Move]) -> str:
|
||
|
"""
|
||
|
Given a sequence of moves, returns a string representing the sequence
|
||
|
in standard algebraic notation (e.g., ``1. e4 e5 2. Nf3 Nc6`` or
|
||
|
``37...Bg6 38. fxg6``).
|
||
|
|
||
|
The board will not be modified as a result of calling this.
|
||
|
|
||
|
:raises: :exc:`IllegalMoveError` if any moves in the sequence are illegal.
|
||
|
"""
|
||
|
board = self.copy(stack=False)
|
||
|
san = []
|
||
|
|
||
|
for move in variation:
|
||
|
if not board.is_legal(move):
|
||
|
raise IllegalMoveError(f"illegal move {move} in position {board.fen()}")
|
||
|
|
||
|
if board.turn == WHITE:
|
||
|
san.append(f"{board.fullmove_number}. {board.san_and_push(move)}")
|
||
|
elif not san:
|
||
|
san.append(f"{board.fullmove_number}...{board.san_and_push(move)}")
|
||
|
else:
|
||
|
san.append(board.san_and_push(move))
|
||
|
|
||
|
return " ".join(san)
|
||
|
|
||
|
def parse_san(self, san: str) -> Move:
|
||
|
"""
|
||
|
Uses the current position as the context to parse a move in standard
|
||
|
algebraic notation and returns the corresponding move object.
|
||
|
|
||
|
Ambiguous moves are rejected. Overspecified moves (including long
|
||
|
algebraic notation) are accepted. Some common syntactical deviations
|
||
|
are also accepted.
|
||
|
|
||
|
The returned move is guaranteed to be either legal or a null move.
|
||
|
|
||
|
:raises:
|
||
|
:exc:`ValueError` (specifically an exception specified below) if the SAN is invalid, illegal or ambiguous.
|
||
|
- :exc:`InvalidMoveError` if the SAN is syntactically invalid.
|
||
|
- :exc:`IllegalMoveError` if the SAN is illegal.
|
||
|
- :exc:`AmbiguousMoveError` if the SAN is ambiguous.
|
||
|
"""
|
||
|
# Castling.
|
||
|
try:
|
||
|
if san in ["O-O", "O-O+", "O-O#", "0-0", "0-0+", "0-0#"]:
|
||
|
return next(move for move in self.generate_castling_moves() if self.is_kingside_castling(move))
|
||
|
elif san in ["O-O-O", "O-O-O+", "O-O-O#", "0-0-0", "0-0-0+", "0-0-0#"]:
|
||
|
return next(move for move in self.generate_castling_moves() if self.is_queenside_castling(move))
|
||
|
except StopIteration:
|
||
|
raise IllegalMoveError(f"illegal san: {san!r} in {self.fen()}")
|
||
|
|
||
|
# Match normal moves.
|
||
|
match = SAN_REGEX.match(san)
|
||
|
if not match:
|
||
|
# Null moves.
|
||
|
if san in ["--", "Z0", "0000", "@@@@"]:
|
||
|
return Move.null()
|
||
|
elif "," in san:
|
||
|
raise InvalidMoveError(f"unsupported multi-leg move: {san!r}")
|
||
|
else:
|
||
|
raise InvalidMoveError(f"invalid san: {san!r}")
|
||
|
|
||
|
# Get target square. Mask our own pieces to exclude castling moves.
|
||
|
to_square = SQUARE_NAMES.index(match.group(4))
|
||
|
to_mask = BB_SQUARES[to_square] & ~self.occupied_co[self.turn]
|
||
|
|
||
|
# Get the promotion piece type.
|
||
|
p = match.group(5)
|
||
|
promotion = PIECE_SYMBOLS.index(p[-1].lower()) if p else None
|
||
|
|
||
|
# Filter by original square.
|
||
|
from_mask = BB_ALL
|
||
|
if match.group(2):
|
||
|
from_file = FILE_NAMES.index(match.group(2))
|
||
|
from_mask &= BB_FILES[from_file]
|
||
|
if match.group(3):
|
||
|
from_rank = int(match.group(3)) - 1
|
||
|
from_mask &= BB_RANKS[from_rank]
|
||
|
|
||
|
# Filter by piece type.
|
||
|
if match.group(1):
|
||
|
piece_type = PIECE_SYMBOLS.index(match.group(1).lower())
|
||
|
from_mask &= self.pieces_mask(piece_type, self.turn)
|
||
|
elif match.group(2) and match.group(3):
|
||
|
# Allow fully specified moves, even if they are not pawn moves,
|
||
|
# including castling moves.
|
||
|
move = self.find_move(square(from_file, from_rank), to_square, promotion)
|
||
|
if move.promotion == promotion:
|
||
|
return move
|
||
|
else:
|
||
|
raise IllegalMoveError(f"missing promotion piece type: {san!r} in {self.fen()}")
|
||
|
else:
|
||
|
from_mask &= self.pawns
|
||
|
|
||
|
# Do not allow pawn captures if file is not specified.
|
||
|
if not match.group(2):
|
||
|
from_mask &= BB_FILES[square_file(to_square)]
|
||
|
|
||
|
# Match legal moves.
|
||
|
matched_move = None
|
||
|
for move in self.generate_legal_moves(from_mask, to_mask):
|
||
|
if move.promotion != promotion:
|
||
|
continue
|
||
|
|
||
|
if matched_move:
|
||
|
raise AmbiguousMoveError(f"ambiguous san: {san!r} in {self.fen()}")
|
||
|
|
||
|
matched_move = move
|
||
|
|
||
|
if not matched_move:
|
||
|
raise IllegalMoveError(f"illegal san: {san!r} in {self.fen()}")
|
||
|
|
||
|
return matched_move
|
||
|
|
||
|
def push_san(self, san: str) -> Move:
|
||
|
"""
|
||
|
Parses a move in standard algebraic notation, makes the move and puts
|
||
|
it onto the move stack.
|
||
|
|
||
|
Returns the move.
|
||
|
|
||
|
:raises:
|
||
|
:exc:`ValueError` (specifically an exception specified below) if neither legal nor a null move.
|
||
|
- :exc:`InvalidMoveError` if the SAN is syntactically invalid.
|
||
|
- :exc:`IllegalMoveError` if the SAN is illegal.
|
||
|
- :exc:`AmbiguousMoveError` if the SAN is ambiguous.
|
||
|
"""
|
||
|
move = self.parse_san(san)
|
||
|
self.push(move)
|
||
|
return move
|
||
|
|
||
|
def uci(self, move: Move, *, chess960: Optional[bool] = None) -> str:
|
||
|
"""
|
||
|
Gets the UCI notation of the move.
|
||
|
|
||
|
*chess960* defaults to the mode of the board. Pass ``True`` to force
|
||
|
Chess960 mode.
|
||
|
"""
|
||
|
if chess960 is None:
|
||
|
chess960 = self.chess960
|
||
|
|
||
|
move = self._to_chess960(move)
|
||
|
move = self._from_chess960(chess960, move.from_square, move.to_square, move.promotion, move.drop)
|
||
|
return move.uci()
|
||
|
|
||
|
def parse_uci(self, uci: str) -> Move:
|
||
|
"""
|
||
|
Parses the given move in UCI notation.
|
||
|
|
||
|
Supports both Chess960 and standard UCI notation.
|
||
|
|
||
|
The returned move is guaranteed to be either legal or a null move.
|
||
|
|
||
|
:raises:
|
||
|
:exc:`ValueError` (specifically an exception specified below) if the move is invalid or illegal in the
|
||
|
current position (but not a null move).
|
||
|
- :exc:`InvalidMoveError` if the UCI is syntactically invalid.
|
||
|
- :exc:`IllegalMoveError` if the UCI is illegal.
|
||
|
"""
|
||
|
move = Move.from_uci(uci)
|
||
|
|
||
|
if not move:
|
||
|
return move
|
||
|
|
||
|
move = self._to_chess960(move)
|
||
|
move = self._from_chess960(self.chess960, move.from_square, move.to_square, move.promotion, move.drop)
|
||
|
|
||
|
if not self.is_legal(move):
|
||
|
raise IllegalMoveError(f"illegal uci: {uci!r} in {self.fen()}")
|
||
|
|
||
|
return move
|
||
|
|
||
|
def push_uci(self, uci: str) -> Move:
|
||
|
"""
|
||
|
Parses a move in UCI notation and puts it on the move stack.
|
||
|
|
||
|
Returns the move.
|
||
|
|
||
|
:raises:
|
||
|
:exc:`ValueError` (specifically an exception specified below) if the move is invalid or illegal in the
|
||
|
current position (but not a null move).
|
||
|
- :exc:`InvalidMoveError` if the UCI is syntactically invalid.
|
||
|
- :exc:`IllegalMoveError` if the UCI is illegal.
|
||
|
"""
|
||
|
move = self.parse_uci(uci)
|
||
|
self.push(move)
|
||
|
return move
|
||
|
|
||
|
def xboard(self, move: Move, chess960: Optional[bool] = None) -> str:
|
||
|
if chess960 is None:
|
||
|
chess960 = self.chess960
|
||
|
|
||
|
if not chess960 or not self.is_castling(move):
|
||
|
return move.xboard()
|
||
|
elif self.is_kingside_castling(move):
|
||
|
return "O-O"
|
||
|
else:
|
||
|
return "O-O-O"
|
||
|
|
||
|
def parse_xboard(self, xboard: str) -> Move:
|
||
|
return self.parse_san(xboard)
|
||
|
|
||
|
push_xboard = push_san
|
||
|
|
||
|
def is_en_passant(self, move: Move) -> bool:
|
||
|
"""Checks if the given pseudo-legal move is an en passant capture."""
|
||
|
return (self.ep_square == move.to_square and
|
||
|
bool(self.pawns & BB_SQUARES[move.from_square]) and
|
||
|
abs(move.to_square - move.from_square) in [7, 9] and
|
||
|
not self.occupied & BB_SQUARES[move.to_square])
|
||
|
|
||
|
def is_capture(self, move: Move) -> bool:
|
||
|
"""Checks if the given pseudo-legal move is a capture."""
|
||
|
touched = BB_SQUARES[move.from_square] ^ BB_SQUARES[move.to_square]
|
||
|
return bool(touched & self.occupied_co[not self.turn]) or self.is_en_passant(move)
|
||
|
|
||
|
def is_zeroing(self, move: Move) -> bool:
|
||
|
"""Checks if the given pseudo-legal move is a capture or pawn move."""
|
||
|
touched = BB_SQUARES[move.from_square] ^ BB_SQUARES[move.to_square]
|
||
|
return bool(touched & self.pawns or touched & self.occupied_co[not self.turn] or move.drop == PAWN)
|
||
|
|
||
|
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)
|
||
|
|
||
|
def is_irreversible(self, move: Move) -> bool:
|
||
|
"""
|
||
|
Checks if the given pseudo-legal move is irreversible.
|
||
|
|
||
|
In standard chess, pawn moves, captures, moves that destroy castling
|
||
|
rights and moves that cede en passant are irreversible.
|
||
|
|
||
|
This method has false-negatives with forced lines. For example, a check
|
||
|
that will force the king to lose castling rights is not considered
|
||
|
irreversible. Only the actual king move is.
|
||
|
"""
|
||
|
return self.is_zeroing(move) or self._reduces_castling_rights(move) or self.has_legal_en_passant()
|
||
|
|
||
|
def is_castling(self, move: Move) -> bool:
|
||
|
"""Checks if the given pseudo-legal move is a castling move."""
|
||
|
if self.kings & BB_SQUARES[move.from_square]:
|
||
|
diff = square_file(move.from_square) - square_file(move.to_square)
|
||
|
return abs(diff) > 1 or bool(self.rooks & self.occupied_co[self.turn] & BB_SQUARES[move.to_square])
|
||
|
return False
|
||
|
|
||
|
def is_kingside_castling(self, move: Move) -> bool:
|
||
|
"""
|
||
|
Checks if the given pseudo-legal move is a kingside castling move.
|
||
|
"""
|
||
|
return self.is_castling(move) and square_file(move.to_square) > square_file(move.from_square)
|
||
|
|
||
|
def is_queenside_castling(self, move: Move) -> bool:
|
||
|
"""
|
||
|
Checks if the given pseudo-legal move is a queenside castling move.
|
||
|
"""
|
||
|
return self.is_castling(move) and square_file(move.to_square) < square_file(move.from_square)
|
||
|
|
||
|
def clean_castling_rights(self) -> Bitboard:
|
||
|
"""
|
||
|
Returns valid castling rights filtered from
|
||
|
:data:`~chess.Board.castling_rights`.
|
||
|
"""
|
||
|
if self._stack:
|
||
|
# No new castling rights are assigned in a game, so we can assume
|
||
|
# they were filtered already.
|
||
|
return self.castling_rights
|
||
|
|
||
|
castling = self.castling_rights & self.rooks
|
||
|
white_castling = castling & BB_RANK_1 & self.occupied_co[WHITE]
|
||
|
black_castling = castling & BB_RANK_8 & self.occupied_co[BLACK]
|
||
|
|
||
|
if not self.chess960:
|
||
|
# The rooks must be on a1, h1, a8 or h8.
|
||
|
white_castling &= (BB_A1 | BB_H1)
|
||
|
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:
|
||
|
white_castling = 0
|
||
|
if not self.occupied_co[BLACK] & self.kings & ~self.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
|
||
|
if not white_king_mask:
|
||
|
white_castling = 0
|
||
|
if not black_king_mask:
|
||
|
black_castling = 0
|
||
|
|
||
|
# There are only two ways of castling, a-side and h-side, and the
|
||
|
# king must be between the rooks.
|
||
|
white_a_side = white_castling & -white_castling
|
||
|
white_h_side = BB_SQUARES[msb(white_castling)] if white_castling else 0
|
||
|
|
||
|
if white_a_side and msb(white_a_side) > msb(white_king_mask):
|
||
|
white_a_side = 0
|
||
|
if white_h_side and msb(white_h_side) < msb(white_king_mask):
|
||
|
white_h_side = 0
|
||
|
|
||
|
black_a_side = (black_castling & -black_castling)
|
||
|
black_h_side = BB_SQUARES[msb(black_castling)] if black_castling else BB_EMPTY
|
||
|
|
||
|
if black_a_side and msb(black_a_side) > msb(black_king_mask):
|
||
|
black_a_side = 0
|
||
|
if black_h_side and msb(black_h_side) < msb(black_king_mask):
|
||
|
black_h_side = 0
|
||
|
|
||
|
# Done.
|
||
|
return black_a_side | black_h_side | white_a_side | white_h_side
|
||
|
|
||
|
def has_castling_rights(self, color: Color) -> bool:
|
||
|
"""Checks if the given side has castling rights."""
|
||
|
backrank = BB_RANK_1 if color == WHITE else BB_RANK_8
|
||
|
return bool(self.clean_castling_rights() & backrank)
|
||
|
|
||
|
def has_kingside_castling_rights(self, color: Color) -> bool:
|
||
|
"""
|
||
|
Checks if the given side has kingside (that is h-side in Chess960)
|
||
|
castling rights.
|
||
|
"""
|
||
|
backrank = BB_RANK_1 if color == WHITE else BB_RANK_8
|
||
|
king_mask = self.kings & self.occupied_co[color] & backrank & ~self.promoted
|
||
|
if not king_mask:
|
||
|
return False
|
||
|
|
||
|
castling_rights = self.clean_castling_rights() & backrank
|
||
|
while castling_rights:
|
||
|
rook = castling_rights & -castling_rights
|
||
|
|
||
|
if rook > king_mask:
|
||
|
return True
|
||
|
|
||
|
castling_rights &= castling_rights - 1
|
||
|
|
||
|
return False
|
||
|
|
||
|
def has_queenside_castling_rights(self, color: Color) -> bool:
|
||
|
"""
|
||
|
Checks if the given side has queenside (that is a-side in Chess960)
|
||
|
castling rights.
|
||
|
"""
|
||
|
backrank = BB_RANK_1 if color == WHITE else BB_RANK_8
|
||
|
king_mask = self.kings & self.occupied_co[color] & backrank & ~self.promoted
|
||
|
if not king_mask:
|
||
|
return False
|
||
|
|
||
|
castling_rights = self.clean_castling_rights() & backrank
|
||
|
while castling_rights:
|
||
|
rook = castling_rights & -castling_rights
|
||
|
|
||
|
if rook < king_mask:
|
||
|
return True
|
||
|
|
||
|
castling_rights &= castling_rights - 1
|
||
|
|
||
|
return False
|
||
|
|
||
|
def has_chess960_castling_rights(self) -> bool:
|
||
|
"""
|
||
|
Checks if there are castling rights that are only possible in Chess960.
|
||
|
"""
|
||
|
# Get valid Chess960 castling rights.
|
||
|
chess960 = self.chess960
|
||
|
self.chess960 = True
|
||
|
castling_rights = self.clean_castling_rights()
|
||
|
self.chess960 = chess960
|
||
|
|
||
|
# Standard chess castling rights can only be on the standard
|
||
|
# starting rook squares.
|
||
|
if castling_rights & ~BB_CORNERS:
|
||
|
return True
|
||
|
|
||
|
# If there are any castling rights in standard chess, the king must be
|
||
|
# on e1 or e8.
|
||
|
if castling_rights & BB_RANK_1 and not self.occupied_co[WHITE] & self.kings & BB_E1:
|
||
|
return True
|
||
|
if castling_rights & BB_RANK_8 and not self.occupied_co[BLACK] & self.kings & BB_E8:
|
||
|
return True
|
||
|
|
||
|
return False
|
||
|
|
||
|
def status(self) -> Status:
|
||
|
"""
|
||
|
Gets a bitmask of possible problems with the position.
|
||
|
|
||
|
:data:`~chess.STATUS_VALID` if all basic validity requirements are met.
|
||
|
This does not imply that the position is actually reachable with a
|
||
|
series of legal moves from the starting position.
|
||
|
|
||
|
Otherwise, bitwise combinations of:
|
||
|
:data:`~chess.STATUS_NO_WHITE_KING`,
|
||
|
:data:`~chess.STATUS_NO_BLACK_KING`,
|
||
|
:data:`~chess.STATUS_TOO_MANY_KINGS`,
|
||
|
:data:`~chess.STATUS_TOO_MANY_WHITE_PAWNS`,
|
||
|
:data:`~chess.STATUS_TOO_MANY_BLACK_PAWNS`,
|
||
|
:data:`~chess.STATUS_PAWNS_ON_BACKRANK`,
|
||
|
:data:`~chess.STATUS_TOO_MANY_WHITE_PIECES`,
|
||
|
:data:`~chess.STATUS_TOO_MANY_BLACK_PIECES`,
|
||
|
:data:`~chess.STATUS_BAD_CASTLING_RIGHTS`,
|
||
|
:data:`~chess.STATUS_INVALID_EP_SQUARE`,
|
||
|
:data:`~chess.STATUS_OPPOSITE_CHECK`,
|
||
|
:data:`~chess.STATUS_EMPTY`,
|
||
|
:data:`~chess.STATUS_RACE_CHECK`,
|
||
|
:data:`~chess.STATUS_RACE_OVER`,
|
||
|
:data:`~chess.STATUS_RACE_MATERIAL`,
|
||
|
:data:`~chess.STATUS_TOO_MANY_CHECKERS`,
|
||
|
:data:`~chess.STATUS_IMPOSSIBLE_CHECK`.
|
||
|
"""
|
||
|
errors = STATUS_VALID
|
||
|
|
||
|
# There must be at least one piece.
|
||
|
if not self.occupied:
|
||
|
errors |= STATUS_EMPTY
|
||
|
|
||
|
# There must be exactly one king of each color.
|
||
|
if not self.occupied_co[WHITE] & self.kings:
|
||
|
errors |= STATUS_NO_WHITE_KING
|
||
|
if not self.occupied_co[BLACK] & self.kings:
|
||
|
errors |= STATUS_NO_BLACK_KING
|
||
|
if popcount(self.occupied & self.kings) > 2:
|
||
|
errors |= STATUS_TOO_MANY_KINGS
|
||
|
|
||
|
# There can not be more than 16 pieces of any color.
|
||
|
if popcount(self.occupied_co[WHITE]) > 16:
|
||
|
errors |= STATUS_TOO_MANY_WHITE_PIECES
|
||
|
if popcount(self.occupied_co[BLACK]) > 16:
|
||
|
errors |= STATUS_TOO_MANY_BLACK_PIECES
|
||
|
|
||
|
# There can not be more than 8 pawns of any color.
|
||
|
if popcount(self.occupied_co[WHITE] & self.pawns) > 8:
|
||
|
errors |= STATUS_TOO_MANY_WHITE_PAWNS
|
||
|
if popcount(self.occupied_co[BLACK] & self.pawns) > 8:
|
||
|
errors |= STATUS_TOO_MANY_BLACK_PAWNS
|
||
|
|
||
|
# Pawns can not be on the back rank.
|
||
|
if self.pawns & BB_BACKRANKS:
|
||
|
errors |= STATUS_PAWNS_ON_BACKRANK
|
||
|
|
||
|
# Castling rights.
|
||
|
if self.castling_rights != self.clean_castling_rights():
|
||
|
errors |= STATUS_BAD_CASTLING_RIGHTS
|
||
|
|
||
|
# En passant.
|
||
|
valid_ep_square = self._valid_ep_square()
|
||
|
if self.ep_square != valid_ep_square:
|
||
|
errors |= STATUS_INVALID_EP_SQUARE
|
||
|
|
||
|
# Side to move giving check.
|
||
|
if self.was_into_check():
|
||
|
errors |= STATUS_OPPOSITE_CHECK
|
||
|
|
||
|
# 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
|
||
|
if checkers:
|
||
|
if popcount(checkers) > 2:
|
||
|
errors |= STATUS_TOO_MANY_CHECKERS
|
||
|
|
||
|
if valid_ep_square is not None:
|
||
|
pushed_to = valid_ep_square ^ A2
|
||
|
pushed_from = valid_ep_square ^ A4
|
||
|
occupied_before = (self.occupied & ~BB_SQUARES[pushed_to]) | BB_SQUARES[pushed_from]
|
||
|
if popcount(checkers) > 1 or (
|
||
|
msb(checkers) != pushed_to and
|
||
|
self._attacked_for_king(our_kings, occupied_before)):
|
||
|
errors |= STATUS_IMPOSSIBLE_CHECK
|
||
|
else:
|
||
|
if popcount(checkers) > 2 or (popcount(checkers) == 2 and ray(lsb(checkers), msb(checkers)) & our_kings):
|
||
|
errors |= STATUS_IMPOSSIBLE_CHECK
|
||
|
|
||
|
return errors
|
||
|
|
||
|
def _valid_ep_square(self) -> Optional[Square]:
|
||
|
if not self.ep_square:
|
||
|
return None
|
||
|
|
||
|
if self.turn == WHITE:
|
||
|
ep_rank = 5
|
||
|
pawn_mask = shift_down(BB_SQUARES[self.ep_square])
|
||
|
seventh_rank_mask = shift_up(BB_SQUARES[self.ep_square])
|
||
|
else:
|
||
|
ep_rank = 2
|
||
|
pawn_mask = shift_up(BB_SQUARES[self.ep_square])
|
||
|
seventh_rank_mask = shift_down(BB_SQUARES[self.ep_square])
|
||
|
|
||
|
# The en passant square must be on the third or sixth rank.
|
||
|
if square_rank(self.ep_square) != ep_rank:
|
||
|
return None
|
||
|
|
||
|
# The last move must have been a double pawn push, so there must
|
||
|
# be a pawn of the correct color on the fourth or fifth rank.
|
||
|
if not self.pawns & self.occupied_co[not self.turn] & pawn_mask:
|
||
|
return None
|
||
|
|
||
|
# And the en passant square must be empty.
|
||
|
if self.occupied & BB_SQUARES[self.ep_square]:
|
||
|
return None
|
||
|
|
||
|
# And the second rank must be empty.
|
||
|
if self.occupied & seventh_rank_mask:
|
||
|
return None
|
||
|
|
||
|
return self.ep_square
|
||
|
|
||
|
def is_valid(self) -> bool:
|
||
|
"""
|
||
|
Checks some basic validity requirements.
|
||
|
|
||
|
See :func:`~chess.Board.status()` for details.
|
||
|
"""
|
||
|
return self.status() == STATUS_VALID
|
||
|
|
||
|
def _ep_skewered(self, king: Square, capturer: Square) -> bool:
|
||
|
# Handle the special case where the king would be in check if the
|
||
|
# pawn and its capturer disappear from the rank.
|
||
|
|
||
|
# Vertical skewers of the captured pawn are not possible. (Pins on
|
||
|
# the capturer are not handled here.)
|
||
|
assert self.ep_square is not None
|
||
|
|
||
|
last_double = self.ep_square + (-8 if self.turn == WHITE else 8)
|
||
|
|
||
|
occupancy = (self.occupied & ~BB_SQUARES[last_double] &
|
||
|
~BB_SQUARES[capturer] | BB_SQUARES[self.ep_square])
|
||
|
|
||
|
# Horizontal attack on the fifth or fourth rank.
|
||
|
horizontal_attackers = self.occupied_co[not self.turn] & (self.rooks | self.queens)
|
||
|
if BB_RANK_ATTACKS[king][BB_RANK_MASKS[king] & occupancy] & horizontal_attackers:
|
||
|
return True
|
||
|
|
||
|
# Diagonal skewers. These are not actually possible in a real game,
|
||
|
# because if the latest double pawn move covers a diagonal attack,
|
||
|
# then the other side would have been in check already.
|
||
|
diagonal_attackers = self.occupied_co[not self.turn] & (self.bishops | self.queens)
|
||
|
if BB_DIAG_ATTACKS[king][BB_DIAG_MASKS[king] & occupancy] & diagonal_attackers:
|
||
|
return True
|
||
|
|
||
|
return False
|
||
|
|
||
|
def _slider_blockers(self, king: Square) -> Bitboard:
|
||
|
rooks_and_queens = self.rooks | self.queens
|
||
|
bishops_and_queens = self.bishops | self.queens
|
||
|
|
||
|
snipers = ((BB_RANK_ATTACKS[king][0] & rooks_and_queens) |
|
||
|
(BB_FILE_ATTACKS[king][0] & rooks_and_queens) |
|
||
|
(BB_DIAG_ATTACKS[king][0] & bishops_and_queens))
|
||
|
|
||
|
blockers = 0
|
||
|
|
||
|
for sniper in scan_reversed(snipers & self.occupied_co[not self.turn]):
|
||
|
b = between(king, sniper) & self.occupied
|
||
|
|
||
|
# Add to blockers if exactly one piece in-between.
|
||
|
if b and BB_SQUARES[msb(b)] == b:
|
||
|
blockers |= b
|
||
|
|
||
|
return blockers & self.occupied_co[self.turn]
|
||
|
|
||
|
def _is_safe(self, king: Square, blockers: Bitboard, move: Move) -> bool:
|
||
|
if move.from_square == king:
|
||
|
if self.is_castling(move):
|
||
|
return True
|
||
|
else:
|
||
|
return not self.is_attacked_by(not self.turn, move.to_square)
|
||
|
elif self.is_en_passant(move):
|
||
|
return bool(self.pin_mask(self.turn, move.from_square) & BB_SQUARES[move.to_square] and
|
||
|
not self._ep_skewered(king, move.from_square))
|
||
|
else:
|
||
|
return bool(not blockers & BB_SQUARES[move.from_square] or
|
||
|
ray(move.from_square, move.to_square) & BB_SQUARES[king])
|
||
|
|
||
|
def _generate_evasions(self, king: Square, checkers: Bitboard, from_mask: Bitboard = BB_ALL, to_mask: Bitboard = BB_ALL) -> Iterator[Move]:
|
||
|
sliders = checkers & (self.bishops | self.rooks | self.queens)
|
||
|
|
||
|
attacked = 0
|
||
|
for checker in scan_reversed(sliders):
|
||
|
attacked |= ray(king, checker) & ~BB_SQUARES[checker]
|
||
|
|
||
|
if BB_SQUARES[king] & from_mask:
|
||
|
for to_square in scan_reversed(BB_KING_ATTACKS[king] & ~self.occupied_co[self.turn] & ~attacked & to_mask):
|
||
|
yield Move(king, to_square)
|
||
|
|
||
|
checker = msb(checkers)
|
||
|
if BB_SQUARES[checker] == checkers:
|
||
|
# Capture or block a single checker.
|
||
|
target = between(king, checker) | checkers
|
||
|
|
||
|
yield from self.generate_pseudo_legal_moves(~self.kings & from_mask, target & to_mask)
|
||
|
|
||
|
# Capture the checking pawn en passant (but avoid yielding
|
||
|
# duplicate moves).
|
||
|
if self.ep_square and not BB_SQUARES[self.ep_square] & target:
|
||
|
last_double = self.ep_square + (-8 if self.turn == WHITE else 8)
|
||
|
if last_double == checker:
|
||
|
yield from self.generate_pseudo_legal_ep(from_mask, to_mask)
|
||
|
|
||
|
def generate_legal_moves(self, from_mask: Bitboard = BB_ALL, to_mask: Bitboard = BB_ALL) -> Iterator[Move]:
|
||
|
if self.is_variant_end():
|
||
|
return
|
||
|
|
||
|
king_mask = self.kings & self.occupied_co[self.turn]
|
||
|
if king_mask:
|
||
|
king = msb(king_mask)
|
||
|
blockers = self._slider_blockers(king)
|
||
|
checkers = self.attackers_mask(not self.turn, king)
|
||
|
if checkers:
|
||
|
for move in self._generate_evasions(king, checkers, from_mask, to_mask):
|
||
|
if self._is_safe(king, blockers, move):
|
||
|
yield move
|
||
|
else:
|
||
|
for move in self.generate_pseudo_legal_moves(from_mask, to_mask):
|
||
|
if self._is_safe(king, blockers, move):
|
||
|
yield move
|
||
|
else:
|
||
|
yield from self.generate_pseudo_legal_moves(from_mask, to_mask)
|
||
|
|
||
|
def generate_legal_ep(self, from_mask: Bitboard = BB_ALL, to_mask: Bitboard = BB_ALL) -> Iterator[Move]:
|
||
|
if self.is_variant_end():
|
||
|
return
|
||
|
|
||
|
for move in self.generate_pseudo_legal_ep(from_mask, to_mask):
|
||
|
if not self.is_into_check(move):
|
||
|
yield move
|
||
|
|
||
|
def generate_legal_captures(self, from_mask: Bitboard = BB_ALL, to_mask: Bitboard = BB_ALL) -> Iterator[Move]:
|
||
|
return itertools.chain(
|
||
|
self.generate_legal_moves(from_mask, to_mask & self.occupied_co[not self.turn]),
|
||
|
self.generate_legal_ep(from_mask, to_mask))
|
||
|
|
||
|
def _attacked_for_king(self, path: Bitboard, occupied: Bitboard) -> bool:
|
||
|
return any(self._attackers_mask(not self.turn, sq, occupied) for sq in scan_reversed(path))
|
||
|
|
||
|
def generate_castling_moves(self, from_mask: Bitboard = BB_ALL, to_mask: Bitboard = BB_ALL) -> Iterator[Move]:
|
||
|
if self.is_variant_end():
|
||
|
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 &= -king
|
||
|
if not king:
|
||
|
return
|
||
|
|
||
|
bb_c = BB_FILE_C & backrank
|
||
|
bb_d = BB_FILE_D & backrank
|
||
|
bb_f = BB_FILE_F & backrank
|
||
|
bb_g = BB_FILE_G & backrank
|
||
|
|
||
|
for candidate in scan_reversed(self.clean_castling_rights() & backrank & to_mask):
|
||
|
rook = BB_SQUARES[candidate]
|
||
|
|
||
|
a_side = rook < king
|
||
|
king_to = bb_c if a_side else bb_g
|
||
|
rook_to = bb_d if a_side else bb_f
|
||
|
|
||
|
king_path = between(msb(king), msb(king_to))
|
||
|
rook_path = between(candidate, msb(rook_to))
|
||
|
|
||
|
if not ((self.occupied ^ king ^ rook) & (king_path | rook_path | king_to | rook_to) or
|
||
|
self._attacked_for_king(king_path | king, self.occupied ^ king) or
|
||
|
self._attacked_for_king(king_to, self.occupied ^ king ^ rook ^ rook_to)):
|
||
|
yield self._from_chess960(self.chess960, msb(king), candidate)
|
||
|
|
||
|
def _from_chess960(self, chess960: bool, from_square: Square, to_square: Square, promotion: Optional[PieceType] = None, drop: Optional[PieceType] = None) -> Move:
|
||
|
if not chess960 and promotion is None and drop is None:
|
||
|
if from_square == E1 and self.kings & BB_E1:
|
||
|
if to_square == H1:
|
||
|
return Move(E1, G1)
|
||
|
elif to_square == A1:
|
||
|
return Move(E1, C1)
|
||
|
elif from_square == E8 and self.kings & BB_E8:
|
||
|
if to_square == H8:
|
||
|
return Move(E8, G8)
|
||
|
elif to_square == A8:
|
||
|
return Move(E8, C8)
|
||
|
|
||
|
return Move(from_square, to_square, promotion, drop)
|
||
|
|
||
|
def _to_chess960(self, move: Move) -> Move:
|
||
|
if move.from_square == E1 and self.kings & BB_E1:
|
||
|
if move.to_square == G1 and not self.rooks & BB_G1:
|
||
|
return Move(E1, H1)
|
||
|
elif move.to_square == C1 and not self.rooks & BB_C1:
|
||
|
return Move(E1, A1)
|
||
|
elif move.from_square == E8 and self.kings & BB_E8:
|
||
|
if move.to_square == G8 and not self.rooks & BB_G8:
|
||
|
return Move(E8, H8)
|
||
|
elif move.to_square == C8 and not self.rooks & BB_C8:
|
||
|
return Move(E8, A8)
|
||
|
|
||
|
return move
|
||
|
|
||
|
def _transposition_key(self) -> Hashable:
|
||
|
return (self.pawns, self.knights, self.bishops, self.rooks,
|
||
|
self.queens, self.kings,
|
||
|
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)
|
||
|
|
||
|
def __repr__(self) -> str:
|
||
|
if not self.chess960:
|
||
|
return f"{type(self).__name__}({self.fen()!r})"
|
||
|
else:
|
||
|
return f"{type(self).__name__}({self.fen()!r}, chess960=True)"
|
||
|
|
||
|
def _repr_svg_(self) -> str:
|
||
|
import chess.svg
|
||
|
return chess.svg.board(
|
||
|
board=self,
|
||
|
size=390,
|
||
|
lastmove=self.peek() if self.move_stack else None,
|
||
|
check=self.king(self.turn) if self.is_check() else None)
|
||
|
|
||
|
def __eq__(self, board: object) -> bool:
|
||
|
if isinstance(board, Board):
|
||
|
return (
|
||
|
self.halfmove_clock == board.halfmove_clock and
|
||
|
self.fullmove_number == board.fullmove_number and
|
||
|
type(self).uci_variant == type(board).uci_variant and
|
||
|
self._transposition_key() == board._transposition_key())
|
||
|
else:
|
||
|
return NotImplemented
|
||
|
|
||
|
def apply_transform(self, f: Callable[[Bitboard], Bitboard]) -> None:
|
||
|
super().apply_transform(f)
|
||
|
self.clear_stack()
|
||
|
self.ep_square = None if self.ep_square is None else msb(f(BB_SQUARES[self.ep_square]))
|
||
|
self.castling_rights = f(self.castling_rights)
|
||
|
|
||
|
def transform(self: BoardT, f: Callable[[Bitboard], Bitboard]) -> BoardT:
|
||
|
board = self.copy(stack=False)
|
||
|
board.apply_transform(f)
|
||
|
return board
|
||
|
|
||
|
def apply_mirror(self: BoardT) -> None:
|
||
|
super().apply_mirror()
|
||
|
self.turn = not self.turn
|
||
|
|
||
|
def mirror(self: BoardT) -> BoardT:
|
||
|
"""
|
||
|
Returns a mirrored copy of the board.
|
||
|
|
||
|
The board is mirrored vertically and piece colors are swapped, so that
|
||
|
the position is equivalent modulo color. Also swap the "en passant"
|
||
|
square, castling rights and turn.
|
||
|
|
||
|
Alternatively, :func:`~chess.Board.apply_mirror()` can be used
|
||
|
to mirror the board.
|
||
|
"""
|
||
|
board = self.copy()
|
||
|
board.apply_mirror()
|
||
|
return board
|
||
|
|
||
|
def copy(self: BoardT, *, stack: Union[bool, int] = True) -> BoardT:
|
||
|
"""
|
||
|
Creates a copy of the board.
|
||
|
|
||
|
Defaults to copying the entire move stack. Alternatively, *stack* can
|
||
|
be ``False``, or an integer to copy a limited number of moves.
|
||
|
"""
|
||
|
board = super().copy()
|
||
|
|
||
|
board.chess960 = self.chess960
|
||
|
|
||
|
board.ep_square = self.ep_square
|
||
|
board.castling_rights = self.castling_rights
|
||
|
board.turn = self.turn
|
||
|
board.fullmove_number = self.fullmove_number
|
||
|
board.halfmove_clock = self.halfmove_clock
|
||
|
|
||
|
if stack:
|
||
|
stack = len(self.move_stack) if stack is True else stack
|
||
|
board.move_stack = [copy.copy(move) for move in self.move_stack[-stack:]]
|
||
|
board._stack = self._stack[-stack:]
|
||
|
|
||
|
return board
|
||
|
|
||
|
@classmethod
|
||
|
def empty(cls: Type[BoardT], *, chess960: bool = False) -> BoardT:
|
||
|
"""Creates a new empty board. Also see :func:`~chess.Board.clear()`."""
|
||
|
return cls(None, chess960=chess960)
|
||
|
|
||
|
@classmethod
|
||
|
def from_epd(cls: Type[BoardT], epd: str, *, chess960: bool = False) -> Tuple[BoardT, Dict[str, Union[None, str, int, float, Move, List[Move]]]]:
|
||
|
"""
|
||
|
Creates a new board from an EPD string. See
|
||
|
:func:`~chess.Board.set_epd()`.
|
||
|
|
||
|
Returns the board and the dictionary of parsed operations as a tuple.
|
||
|
"""
|
||
|
board = cls.empty(chess960=chess960)
|
||
|
return board, board.set_epd(epd)
|
||
|
|
||
|
@classmethod
|
||
|
def from_chess960_pos(cls: Type[BoardT], scharnagl: int) -> BoardT:
|
||
|
board = cls.empty(chess960=True)
|
||
|
board.set_chess960_pos(scharnagl)
|
||
|
return board
|
||
|
|
||
|
|
||
|
class PseudoLegalMoveGenerator:
|
||
|
|
||
|
def __init__(self, board: Board) -> None:
|
||
|
self.board = board
|
||
|
|
||
|
def __bool__(self) -> bool:
|
||
|
return any(self.board.generate_pseudo_legal_moves())
|
||
|
|
||
|
def count(self) -> int:
|
||
|
# List conversion is faster than iterating.
|
||
|
return len(list(self))
|
||
|
|
||
|
def __iter__(self) -> Iterator[Move]:
|
||
|
return self.board.generate_pseudo_legal_moves()
|
||
|
|
||
|
def __contains__(self, move: Move) -> bool:
|
||
|
return self.board.is_pseudo_legal(move)
|
||
|
|
||
|
def __repr__(self) -> str:
|
||
|
builder = []
|
||
|
|
||
|
for move in self:
|
||
|
if self.board.is_legal(move):
|
||
|
builder.append(self.board.san(move))
|
||
|
else:
|
||
|
builder.append(self.board.uci(move))
|
||
|
|
||
|
sans = ", ".join(builder)
|
||
|
return f"<PseudoLegalMoveGenerator at {id(self):#x} ({sans})>"
|
||
|
|
||
|
|
||
|
class LegalMoveGenerator:
|
||
|
|
||
|
def __init__(self, board: Board) -> None:
|
||
|
self.board = board
|
||
|
|
||
|
def __bool__(self) -> bool:
|
||
|
return any(self.board.generate_legal_moves())
|
||
|
|
||
|
def count(self) -> int:
|
||
|
# List conversion is faster than iterating.
|
||
|
return len(list(self))
|
||
|
|
||
|
def __iter__(self) -> Iterator[Move]:
|
||
|
return self.board.generate_legal_moves()
|
||
|
|
||
|
def __contains__(self, move: Move) -> bool:
|
||
|
return self.board.is_legal(move)
|
||
|
|
||
|
def __repr__(self) -> str:
|
||
|
sans = ", ".join(self.board.san(move) for move in self)
|
||
|
return f"<LegalMoveGenerator at {id(self):#x} ({sans})>"
|
||
|
|
||
|
|
||
|
IntoSquareSet = Union[SupportsInt, Iterable[Square]]
|
||
|
|
||
|
class SquareSet:
|
||
|
"""
|
||
|
A set of squares.
|
||
|
|
||
|
>>> import chess
|
||
|
>>>
|
||
|
>>> squares = chess.SquareSet([chess.A8, chess.A1])
|
||
|
>>> squares
|
||
|
SquareSet(0x0100_0000_0000_0001)
|
||
|
|
||
|
>>> squares = chess.SquareSet(chess.BB_A8 | chess.BB_RANK_1)
|
||
|
>>> squares
|
||
|
SquareSet(0x0100_0000_0000_00ff)
|
||
|
|
||
|
>>> print(squares)
|
||
|
1 . . . . . . .
|
||
|
. . . . . . . .
|
||
|
. . . . . . . .
|
||
|
. . . . . . . .
|
||
|
. . . . . . . .
|
||
|
. . . . . . . .
|
||
|
. . . . . . . .
|
||
|
1 1 1 1 1 1 1 1
|
||
|
|
||
|
>>> len(squares)
|
||
|
9
|
||
|
|
||
|
>>> bool(squares)
|
||
|
True
|
||
|
|
||
|
>>> chess.B1 in squares
|
||
|
True
|
||
|
|
||
|
>>> for square in squares:
|
||
|
... # 0 -- chess.A1
|
||
|
... # 1 -- chess.B1
|
||
|
... # 2 -- chess.C1
|
||
|
... # 3 -- chess.D1
|
||
|
... # 4 -- chess.E1
|
||
|
... # 5 -- chess.F1
|
||
|
... # 6 -- chess.G1
|
||
|
... # 7 -- chess.H1
|
||
|
... # 56 -- chess.A8
|
||
|
... print(square)
|
||
|
...
|
||
|
0
|
||
|
1
|
||
|
2
|
||
|
3
|
||
|
4
|
||
|
5
|
||
|
6
|
||
|
7
|
||
|
56
|
||
|
|
||
|
>>> list(squares)
|
||
|
[0, 1, 2, 3, 4, 5, 6, 7, 56]
|
||
|
|
||
|
Square sets are internally represented by 64-bit integer masks of the
|
||
|
included squares. Bitwise operations can be used to compute unions,
|
||
|
intersections and shifts.
|
||
|
|
||
|
>>> int(squares)
|
||
|
72057594037928191
|
||
|
|
||
|
Also supports common set operations like
|
||
|
:func:`~chess.SquareSet.issubset()`, :func:`~chess.SquareSet.issuperset()`,
|
||
|
:func:`~chess.SquareSet.union()`, :func:`~chess.SquareSet.intersection()`,
|
||
|
:func:`~chess.SquareSet.difference()`,
|
||
|
:func:`~chess.SquareSet.symmetric_difference()` and
|
||
|
:func:`~chess.SquareSet.copy()` as well as
|
||
|
:func:`~chess.SquareSet.update()`,
|
||
|
:func:`~chess.SquareSet.intersection_update()`,
|
||
|
:func:`~chess.SquareSet.difference_update()`,
|
||
|
:func:`~chess.SquareSet.symmetric_difference_update()` and
|
||
|
:func:`~chess.SquareSet.clear()`.
|
||
|
"""
|
||
|
|
||
|
def __init__(self, squares: IntoSquareSet = BB_EMPTY) -> None:
|
||
|
try:
|
||
|
self.mask = squares.__int__() & BB_ALL # type: ignore
|
||
|
return
|
||
|
except AttributeError:
|
||
|
self.mask = 0
|
||
|
|
||
|
# Try squares as an iterable. Not under except clause for nicer
|
||
|
# backtraces.
|
||
|
for square in squares: # type: ignore
|
||
|
self.add(square)
|
||
|
|
||
|
# Set
|
||
|
|
||
|
def __contains__(self, square: Square) -> bool:
|
||
|
return bool(BB_SQUARES[square] & self.mask)
|
||
|
|
||
|
def __iter__(self) -> Iterator[Square]:
|
||
|
return scan_forward(self.mask)
|
||
|
|
||
|
def __reversed__(self) -> Iterator[Square]:
|
||
|
return scan_reversed(self.mask)
|
||
|
|
||
|
def __len__(self) -> int:
|
||
|
return popcount(self.mask)
|
||
|
|
||
|
# MutableSet
|
||
|
|
||
|
def add(self, square: Square) -> None:
|
||
|
"""Adds a square to the set."""
|
||
|
self.mask |= BB_SQUARES[square]
|
||
|
|
||
|
def discard(self, square: Square) -> None:
|
||
|
"""Discards a square from the set."""
|
||
|
self.mask &= ~BB_SQUARES[square]
|
||
|
|
||
|
# frozenset
|
||
|
|
||
|
def isdisjoint(self, other: IntoSquareSet) -> bool:
|
||
|
"""Tests if the square sets are disjoint."""
|
||
|
return not bool(self & other)
|
||
|
|
||
|
def issubset(self, other: IntoSquareSet) -> bool:
|
||
|
"""Tests if this square set is a subset of another."""
|
||
|
return not bool(self & ~SquareSet(other))
|
||
|
|
||
|
def issuperset(self, other: IntoSquareSet) -> bool:
|
||
|
"""Tests if this square set is a superset of another."""
|
||
|
return not bool(~self & other)
|
||
|
|
||
|
def union(self, other: IntoSquareSet) -> SquareSet:
|
||
|
return self | other
|
||
|
|
||
|
def __or__(self, other: IntoSquareSet) -> SquareSet:
|
||
|
r = SquareSet(other)
|
||
|
r.mask |= self.mask
|
||
|
return r
|
||
|
|
||
|
def intersection(self, other: IntoSquareSet) -> SquareSet:
|
||
|
return self & other
|
||
|
|
||
|
def __and__(self, other: IntoSquareSet) -> SquareSet:
|
||
|
r = SquareSet(other)
|
||
|
r.mask &= self.mask
|
||
|
return r
|
||
|
|
||
|
def difference(self, other: IntoSquareSet) -> SquareSet:
|
||
|
return self - other
|
||
|
|
||
|
def __sub__(self, other: IntoSquareSet) -> SquareSet:
|
||
|
r = SquareSet(other)
|
||
|
r.mask = self.mask & ~r.mask
|
||
|
return r
|
||
|
|
||
|
def symmetric_difference(self, other: IntoSquareSet) -> SquareSet:
|
||
|
return self ^ other
|
||
|
|
||
|
def __xor__(self, other: IntoSquareSet) -> SquareSet:
|
||
|
r = SquareSet(other)
|
||
|
r.mask ^= self.mask
|
||
|
return r
|
||
|
|
||
|
def copy(self) -> SquareSet:
|
||
|
return SquareSet(self.mask)
|
||
|
|
||
|
# set
|
||
|
|
||
|
def update(self, *others: IntoSquareSet) -> None:
|
||
|
for other in others:
|
||
|
self |= other
|
||
|
|
||
|
def __ior__(self, other: IntoSquareSet) -> SquareSet:
|
||
|
self.mask |= SquareSet(other).mask
|
||
|
return self
|
||
|
|
||
|
def intersection_update(self, *others: IntoSquareSet) -> None:
|
||
|
for other in others:
|
||
|
self &= other
|
||
|
|
||
|
def __iand__(self, other: IntoSquareSet) -> SquareSet:
|
||
|
self.mask &= SquareSet(other).mask
|
||
|
return self
|
||
|
|
||
|
def difference_update(self, other: IntoSquareSet) -> None:
|
||
|
self -= other
|
||
|
|
||
|
def __isub__(self, other: IntoSquareSet) -> SquareSet:
|
||
|
self.mask &= ~SquareSet(other).mask
|
||
|
return self
|
||
|
|
||
|
def symmetric_difference_update(self, other: IntoSquareSet) -> None:
|
||
|
self ^= other
|
||
|
|
||
|
def __ixor__(self, other: IntoSquareSet) -> SquareSet:
|
||
|
self.mask ^= SquareSet(other).mask
|
||
|
return self
|
||
|
|
||
|
def remove(self, square: Square) -> None:
|
||
|
"""
|
||
|
Removes a square from the set.
|
||
|
|
||
|
:raises: :exc:`KeyError` if the given *square* was not in the set.
|
||
|
"""
|
||
|
mask = BB_SQUARES[square]
|
||
|
if self.mask & mask:
|
||
|
self.mask ^= mask
|
||
|
else:
|
||
|
raise KeyError(square)
|
||
|
|
||
|
def pop(self) -> Square:
|
||
|
"""
|
||
|
Removes and returns a square from the set.
|
||
|
|
||
|
:raises: :exc:`KeyError` if the set is empty.
|
||
|
"""
|
||
|
if not self.mask:
|
||
|
raise KeyError("pop from empty SquareSet")
|
||
|
|
||
|
square = lsb(self.mask)
|
||
|
self.mask &= (self.mask - 1)
|
||
|
return square
|
||
|
|
||
|
def clear(self) -> None:
|
||
|
"""Removes all elements from this set."""
|
||
|
self.mask = BB_EMPTY
|
||
|
|
||
|
# SquareSet
|
||
|
|
||
|
def carry_rippler(self) -> Iterator[Bitboard]:
|
||
|
"""Iterator over the subsets of this set."""
|
||
|
return _carry_rippler(self.mask)
|
||
|
|
||
|
def mirror(self) -> SquareSet:
|
||
|
"""Returns a vertically mirrored copy of this square set."""
|
||
|
return SquareSet(flip_vertical(self.mask))
|
||
|
|
||
|
def tolist(self) -> List[bool]:
|
||
|
"""Converts the set to a list of 64 bools."""
|
||
|
result = [False] * 64
|
||
|
for square in self:
|
||
|
result[square] = True
|
||
|
return result
|
||
|
|
||
|
def __bool__(self) -> bool:
|
||
|
return bool(self.mask)
|
||
|
|
||
|
def __eq__(self, other: object) -> bool:
|
||
|
try:
|
||
|
return self.mask == SquareSet(other).mask # type: ignore
|
||
|
except (TypeError, ValueError):
|
||
|
return NotImplemented
|
||
|
|
||
|
def __lshift__(self, shift: int) -> SquareSet:
|
||
|
return SquareSet((self.mask << shift) & BB_ALL)
|
||
|
|
||
|
def __rshift__(self, shift: int) -> SquareSet:
|
||
|
return SquareSet(self.mask >> shift)
|
||
|
|
||
|
def __ilshift__(self, shift: int) -> SquareSet:
|
||
|
self.mask = (self.mask << shift) & BB_ALL
|
||
|
return self
|
||
|
|
||
|
def __irshift__(self, shift: int) -> SquareSet:
|
||
|
self.mask >>= shift
|
||
|
return self
|
||
|
|
||
|
def __invert__(self) -> SquareSet:
|
||
|
return SquareSet(~self.mask & BB_ALL)
|
||
|
|
||
|
def __int__(self) -> int:
|
||
|
return self.mask
|
||
|
|
||
|
def __index__(self) -> int:
|
||
|
return self.mask
|
||
|
|
||
|
def __repr__(self) -> str:
|
||
|
return f"SquareSet({self.mask:#021_x})"
|
||
|
|
||
|
def __str__(self) -> str:
|
||
|
builder = []
|
||
|
|
||
|
for square in SQUARES_180:
|
||
|
mask = BB_SQUARES[square]
|
||
|
builder.append("1" if self.mask & mask else ".")
|
||
|
|
||
|
if not mask & BB_FILE_H:
|
||
|
builder.append(" ")
|
||
|
elif square != H1:
|
||
|
builder.append("\n")
|
||
|
|
||
|
return "".join(builder)
|
||
|
|
||
|
def _repr_svg_(self) -> str:
|
||
|
import chess.svg
|
||
|
return chess.svg.board(squares=self, size=390)
|
||
|
|
||
|
@classmethod
|
||
|
def ray(cls, a: Square, b: Square) -> SquareSet:
|
||
|
"""
|
||
|
All squares on the rank, file or diagonal with the two squares, if they
|
||
|
are aligned.
|
||
|
|
||
|
>>> import chess
|
||
|
>>>
|
||
|
>>> print(chess.SquareSet.ray(chess.E2, chess.B5))
|
||
|
. . . . . . . .
|
||
|
. . . . . . . .
|
||
|
1 . . . . . . .
|
||
|
. 1 . . . . . .
|
||
|
. . 1 . . . . .
|
||
|
. . . 1 . . . .
|
||
|
. . . . 1 . . .
|
||
|
. . . . . 1 . .
|
||
|
"""
|
||
|
return cls(ray(a, b))
|
||
|
|
||
|
@classmethod
|
||
|
def between(cls, a: Square, b: Square) -> SquareSet:
|
||
|
"""
|
||
|
All squares on the rank, file or diagonal between the two squares
|
||
|
(bounds not included), if they are aligned.
|
||
|
|
||
|
>>> import chess
|
||
|
>>>
|
||
|
>>> print(chess.SquareSet.between(chess.E2, chess.B5))
|
||
|
. . . . . . . .
|
||
|
. . . . . . . .
|
||
|
. . . . . . . .
|
||
|
. . . . . . . .
|
||
|
. . 1 . . . . .
|
||
|
. . . 1 . . . .
|
||
|
. . . . . . . .
|
||
|
. . . . . . . .
|
||
|
"""
|
||
|
return cls(between(a, b))
|
||
|
|
||
|
@classmethod
|
||
|
def from_square(cls, square: Square) -> SquareSet:
|
||
|
"""
|
||
|
Creates a :class:`~chess.SquareSet` from a single square.
|
||
|
|
||
|
>>> import chess
|
||
|
>>>
|
||
|
>>> chess.SquareSet.from_square(chess.A1) == chess.BB_A1
|
||
|
True
|
||
|
"""
|
||
|
return cls(BB_SQUARES[square])
|