2367 lines
89 KiB
Python
2367 lines
89 KiB
Python
# -*- coding: utf-8 -*-
|
|
|
|
import collections
|
|
import random
|
|
import re
|
|
import datetime
|
|
import itertools
|
|
import math
|
|
import plugin_super_class
|
|
|
|
from PySide.QtCore import *
|
|
from PySide.QtGui import *
|
|
from PySide.QtSvg import *
|
|
|
|
|
|
START_FEN = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"
|
|
|
|
|
|
def opposite_color(color):
|
|
""":return: The opposite color.
|
|
|
|
:param color:
|
|
"w", "white, "b" or "black".
|
|
"""
|
|
if color == "w":
|
|
return "b"
|
|
elif color == "white":
|
|
return "black"
|
|
elif color == "b":
|
|
return "w"
|
|
elif color == "black":
|
|
return "white"
|
|
else:
|
|
raise ValueError("Expected w, b, white or black, got: %s." % color)
|
|
|
|
|
|
class Piece(object):
|
|
"""Representss a chess piece.
|
|
|
|
:param symbol:
|
|
THe symbol of the piece as used in `FENs <http://en.wikipedia.org/wiki/Forsyth%E2%80%93Edwards_Notation#Definition>`_.
|
|
|
|
Piece objects that have the same type and color compare as equal.
|
|
|
|
>>> import chess
|
|
>>> chess.Piece("Q") == chess.Piece.from_color_and_type("w", "q")
|
|
True
|
|
"""
|
|
|
|
__cache = dict()
|
|
|
|
def __init__(self, symbol):
|
|
self.__symbol = symbol
|
|
|
|
self.__color = "w" if symbol != symbol.lower() else "b"
|
|
self.__full_color = "white" if self.__color == "w" else "black"
|
|
|
|
self.__type = symbol.lower()
|
|
if self.__type == "p":
|
|
self.__full_type = "pawn"
|
|
elif self.__type == "n":
|
|
self.__full_type = "knight"
|
|
elif self.__type == "b":
|
|
self.__full_type = "bishop"
|
|
elif self.__type == "r":
|
|
self.__full_type = "rook"
|
|
elif self.__type == "q":
|
|
self.__full_type = "queen"
|
|
elif self.__type == "k":
|
|
self.__full_type = "king"
|
|
else:
|
|
raise ValueError("Expected valid piece symbol, got: %s." % symbol)
|
|
|
|
self.__hash = ord(self.__symbol)
|
|
|
|
@classmethod
|
|
def from_color_and_type(cls, color, type):
|
|
"""Creates a piece object from color and type.
|
|
|
|
An alternate way of creating pieces is from color and type.
|
|
|
|
:param color:
|
|
`"w"`, `"b"`, `"white"` or `"black"`.
|
|
:param type:
|
|
`"p"`, `"pawn"`, `"r"`, `"rook"`, `"n"`, `"knight"`, `"b"`,
|
|
`"bishop"`, `"q"`, `"queen"`, `"k"` or `"king"`.
|
|
|
|
>>> chess.Piece.from_color_and_type("w", "pawn")
|
|
Piece('P')
|
|
>>> chess.Piece.from_color_and_type("black", "q")
|
|
Piece('q')
|
|
"""
|
|
if type == "p" or type == "pawn":
|
|
symbol = "p"
|
|
elif type == "n" or type == "knight":
|
|
symbol = "n"
|
|
elif type == "b" or type == "bishop":
|
|
symbol = "b"
|
|
elif type == "r" or type == "rook":
|
|
symbol = "r"
|
|
elif type == "q" or type == "queen":
|
|
symbol = "q"
|
|
elif type == "k" or type == "king":
|
|
symbol = "k"
|
|
else:
|
|
raise ValueError("Expected piece type, got: %s." % type)
|
|
|
|
if color == "w" or color == "white":
|
|
return cls(symbol.upper())
|
|
elif color == "b" or color == "black":
|
|
return cls(symbol)
|
|
else:
|
|
raise ValueError("Expected w, b, white or black, got: %s." % color)
|
|
|
|
@property
|
|
def symbol(self):
|
|
"""The symbol of the piece as used in `FENs <http://en.wikipedia.org/wiki/Forsyth%E2%80%93Edwards_Notation#Definition>`_."""
|
|
return self.__symbol
|
|
|
|
@property
|
|
def color(self):
|
|
"""The color of the piece as `"b"` or `"w"`."""
|
|
return self.__color
|
|
|
|
@property
|
|
def full_color(self):
|
|
"""The full color of the piece as `"black"` or `"white`."""
|
|
return self.__full_color
|
|
|
|
@property
|
|
def type(self):
|
|
"""The type of the piece as `"p"`, `"b"`, `"n"`, `"r"`, `"k"`,
|
|
or `"q"` for pawn, bishop, knight, rook, king or queen.
|
|
"""
|
|
return self.__type
|
|
|
|
@property
|
|
def full_type(self):
|
|
"""The full type of the piece as `"pawn"`, `"bishop"`,
|
|
`"knight"`, `"rook"`, `"king"` or `"queen"`.
|
|
"""
|
|
return self.__full_type
|
|
|
|
def __str__(self):
|
|
return self.__symbol
|
|
|
|
def __repr__(self):
|
|
return "Piece('%s')" % self.__symbol
|
|
|
|
def __eq__(self, other):
|
|
return isinstance(other, Piece) and self.__symbol == other.symbol
|
|
|
|
def __ne__(self, other):
|
|
return not self.__eq__(other)
|
|
|
|
def __hash__(self):
|
|
return self.__hash
|
|
|
|
|
|
class Square(object):
|
|
"""Represents a square on the chess board.
|
|
|
|
:param name: The name of the square in algebraic notation.
|
|
|
|
Square objects that represent the same square compare as equal.
|
|
|
|
>>> import chess
|
|
>>> chess.Square("e4") == chess.Square("e4")
|
|
True
|
|
"""
|
|
|
|
__cache = dict()
|
|
|
|
def __init__(self, name):
|
|
if not len(name) == 2:
|
|
raise ValueError("Expected square name, got: %s." % repr(name))
|
|
self.__name = name
|
|
|
|
if not name[0] in ["a", "b", "c", "d", "e", "f", "g", "h"]:
|
|
raise ValueError("Expected file, got: %s." % repr(name[0]))
|
|
self.__file = name[0]
|
|
self.__x = ord(self.__name[0]) - ord("a")
|
|
|
|
if not name[1] in ["1", "2", "3", "4", "5", "6", "7", "8"]:
|
|
raise ValueError("Expected rank, got: %s." % repr(name[1]))
|
|
self.__rank = int(name[1])
|
|
self.__y = ord(self.__name[1]) - ord("1")
|
|
|
|
self.__x88 = self.__x + 16 * (7 - self.__y)
|
|
|
|
@classmethod
|
|
def from_x88(cls, x88):
|
|
"""Creates a square object from an `x88 <http://en.wikipedia.org/wiki/Board_representation_(chess)#0x88_method>`_
|
|
index.
|
|
|
|
:param x88:
|
|
The x88 index as integer between 0 and 128.
|
|
"""
|
|
if x88 < 0 or x88 > 128:
|
|
raise ValueError("x88 index is out of range: %s." % repr(x88))
|
|
|
|
if x88 & 0x88:
|
|
raise ValueError("x88 is not on the board: %s." % repr(x88))
|
|
|
|
return cls("abcdefgh"[x88 & 7] + "87654321"[x88 >> 4])
|
|
|
|
@classmethod
|
|
def from_rank_and_file(cls, rank, file):
|
|
"""Creates a square object from rank and file.
|
|
|
|
:param rank:
|
|
An integer between 1 and 8.
|
|
:param file:
|
|
The rank as a letter between `"a"` and `"h"`.
|
|
"""
|
|
if rank < 1 or rank > 8:
|
|
raise ValueError("Expected rank to be between 1 and 8: %s." % repr(rank))
|
|
|
|
if not file in ["a", "b", "c", "d", "e", "f", "g", "h"]:
|
|
raise ValueError("Expected the file to be a letter between 'a' and 'h': %s." % repr(file))
|
|
|
|
return cls(file + str(rank))
|
|
|
|
@classmethod
|
|
def from_x_and_y(cls, x, y):
|
|
"""Creates a square object from x and y coordinates.
|
|
|
|
:param x:
|
|
An integer between 0 and 7 where 0 is the a-file.
|
|
:param y:
|
|
An integer between 0 and 7 where 0 is the first rank.
|
|
"""
|
|
return cls("abcdefgh"[x] + "12345678"[y])
|
|
|
|
@property
|
|
def name(self):
|
|
"""The algebraic name of the square."""
|
|
return self.__name
|
|
|
|
@property
|
|
def file(self):
|
|
"""The file as a letter between `"a"` and `"h"`."""
|
|
return self.__file
|
|
|
|
@property
|
|
def x(self):
|
|
"""The x-coordinate, starting with 0 for the a-file."""
|
|
return self.__x
|
|
|
|
@property
|
|
def rank(self):
|
|
"""The rank as an integer between 1 and 8."""
|
|
return self.__rank
|
|
|
|
@property
|
|
def y(self):
|
|
"""The y-coordinate, starting with 0 for the first rank."""
|
|
return self.__y
|
|
|
|
@property
|
|
def x88(self):
|
|
"""The `x88 <http://en.wikipedia.org/wiki/Board_representation_(chess)#0x88_method>`_
|
|
index of the square."""
|
|
return self.__x88
|
|
|
|
def is_dark(self):
|
|
""":return: Whether it is a dark square."""
|
|
return (self.__x - self.__y % 2) == 0
|
|
|
|
def is_light(self):
|
|
""":return: Whether it is a light square."""
|
|
return not self.is_dark()
|
|
|
|
def is_backrank(self):
|
|
""":return: Whether the square is on either sides backrank."""
|
|
return self.__y == 0 or self.__y == 7
|
|
|
|
def __str__(self):
|
|
return self.__name
|
|
|
|
def __repr__(self):
|
|
return "Square('%s')" % self.__name
|
|
|
|
def __eq__(self, other):
|
|
return isinstance(other, Square) and self.__name == other.name
|
|
|
|
def __ne__(self, other):
|
|
return not self.__eq__(other)
|
|
|
|
def __hash__(self):
|
|
return self.__x88
|
|
|
|
|
|
class Move(object):
|
|
"""Represents a move.
|
|
|
|
:param source:
|
|
The source square.
|
|
:param target:
|
|
The target square.
|
|
:param promotion:
|
|
Optional. If given this indicates which piece a pawn has been
|
|
promoted to: `"r"`, `"n"`, `"b"` or `"q"`.
|
|
|
|
Identical moves compare as equal.
|
|
True
|
|
"""
|
|
|
|
__uci_move_regex = re.compile(r"^([a-h][1-8])([a-h][1-8])([rnbq]?)$")
|
|
|
|
def __init__(self, source, target, promotion=None):
|
|
if not isinstance(source, Square):
|
|
raise TypeError("Expected source to be a Square.")
|
|
self.__source = source
|
|
|
|
if not isinstance(target, Square):
|
|
raise TypeError("Expected target to be a Square.")
|
|
self.__target = target
|
|
|
|
if not promotion:
|
|
self.__promotion = None
|
|
self.__full_promotion = None
|
|
else:
|
|
promotion = promotion.lower()
|
|
if promotion == "n" or promotion == "knight":
|
|
self.__promotion = "n"
|
|
self.__full_promotion = "knight"
|
|
elif promotion == "b" or promotion == "bishop":
|
|
self.__promotion = "b"
|
|
self.__full_promotion = "bishop"
|
|
elif promotion == "r" or promotion == "rook":
|
|
self.__promotion = "r"
|
|
self.__full_promotion = "rook"
|
|
elif promotion == "q" or promotion == "queen":
|
|
self.__promotion = "q"
|
|
self.__full_promotion = "queen"
|
|
else:
|
|
raise ValueError("Expected promotion type, got: %s." % repr(promotion))
|
|
|
|
@classmethod
|
|
def from_uci(cls, uci):
|
|
"""The UCI move string like `"a1a2"` or `"b7b8q"`."""
|
|
if uci == "0000":
|
|
return cls.get_null()
|
|
|
|
match = cls.__uci_move_regex.match(uci)
|
|
|
|
return cls(
|
|
source=Square(match.group(1)),
|
|
target=Square(match.group(2)),
|
|
promotion=match.group(3) or None)
|
|
|
|
@classmethod
|
|
def get_null(cls):
|
|
""":return: A null move."""
|
|
return cls(Square("a1"), Square("a1"))
|
|
|
|
@property
|
|
def source(self):
|
|
"""The source square."""
|
|
return self.__source
|
|
|
|
@property
|
|
def target(self):
|
|
"""The target square."""
|
|
return self.__target
|
|
|
|
@property
|
|
def promotion(self):
|
|
"""The promotion type as `None`, `"r"`, `"n"`, `"b"` or `"q"`."""
|
|
return self.__promotion
|
|
|
|
@property
|
|
def full_promotion(self):
|
|
"""Like `promotion`, but with full piece type names."""
|
|
return self.__full_promotion
|
|
|
|
@property
|
|
def uci(self):
|
|
"""The UCI move string like `"a1a2"` or `"b7b8q"`."""
|
|
if self.is_null():
|
|
return "0000"
|
|
else:
|
|
if self.__promotion:
|
|
return self.__source.name + self.__target.name + self.__promotion
|
|
else:
|
|
return self.__source.name + self.__target.name
|
|
|
|
def is_null(self):
|
|
""":return: Whether the move is a null move."""
|
|
return self.__source == self.__target
|
|
|
|
def __nonzero__(self):
|
|
return not self.is_null()
|
|
|
|
def __str__(self):
|
|
return self.uci
|
|
|
|
def __repr__(self):
|
|
return "Move.from_uci(%s)" % repr(self.uci)
|
|
|
|
def __eq__(self, other):
|
|
return isinstance(other, Move) and self.uci == other.uci
|
|
|
|
def __ne__(self, other):
|
|
return not self.__eq__(other)
|
|
|
|
def __hash__(self):
|
|
return hash(self.uci)
|
|
|
|
|
|
MoveInfo = collections.namedtuple("MoveInfo", [
|
|
"move",
|
|
"piece",
|
|
"captured",
|
|
"san",
|
|
"is_enpassant",
|
|
"is_king_side_castle",
|
|
"is_queen_side_castle",
|
|
"is_castle",
|
|
"is_check",
|
|
"is_checkmate"])
|
|
|
|
|
|
class Position(object):
|
|
"""Represents a chess position.
|
|
|
|
:param fen:
|
|
Optional. The FEN of the position. Defaults to the standard
|
|
chess start position.
|
|
"""
|
|
|
|
__san_regex = re.compile('^([NBKRQ])?([a-h])?([1-8])?x?([a-h][1-8])(=[NBRQ])?(\+|#)?$')
|
|
|
|
def __init__(self, fen=START_FEN):
|
|
self.__castling = "KQkq"
|
|
self.fen = fen
|
|
|
|
def copy(self):
|
|
"""Gets a copy of the position. The copy will not change when the
|
|
original instance is changed.
|
|
|
|
:return:
|
|
An exact copy of the positon.
|
|
"""
|
|
return Position(self.fen)
|
|
|
|
def __get_square_index(self, square_or_int):
|
|
if type(square_or_int) is int:
|
|
# Validate the index by passing it through the constructor.
|
|
return Square.from_x88(square_or_int).x88
|
|
elif isinstance(square_or_int, str):
|
|
return Square(square_or_int).x88
|
|
elif type(square_or_int) is Square:
|
|
return square_or_int.x88
|
|
else:
|
|
raise TypeError(
|
|
"Expected integer or Square, got: %s." % repr(square_or_int))
|
|
|
|
def __getitem__(self, key):
|
|
return self.__board[self.__get_square_index(key)]
|
|
|
|
def __setitem__(self, key, value):
|
|
if value is None or type(value) is Piece:
|
|
self.__board[self.__get_square_index(key)] = value
|
|
else:
|
|
raise TypeError("Expected Piece or None, got: %s." % repr(value))
|
|
|
|
def __delitem__(self, key):
|
|
self.__board[self.__get_square_index(key)] = None
|
|
|
|
def clear_board(self):
|
|
"""Removes all pieces from the board."""
|
|
self.__board = [None] * 128
|
|
|
|
def reset(self):
|
|
"""Resets to the standard chess start position."""
|
|
self.set_fen(START_FEN)
|
|
|
|
def __get_disambiguator(self, move):
|
|
same_rank = False
|
|
same_file = False
|
|
piece = self[move.source]
|
|
|
|
for m in self.get_legal_moves():
|
|
ambig_piece = self[m.source]
|
|
if (piece == ambig_piece and move.source != m.source and
|
|
move.target == m.target):
|
|
if move.source.rank == m.source.rank:
|
|
same_rank = True
|
|
|
|
if move.source.file == m.source.file:
|
|
same_file = True
|
|
|
|
if same_rank and same_file:
|
|
break
|
|
|
|
if same_rank and same_file:
|
|
return move.source.name
|
|
elif same_file:
|
|
return str(move.source.rank)
|
|
elif same_rank:
|
|
return move.source.file
|
|
else:
|
|
return ""
|
|
|
|
def get_move_from_san(self, san):
|
|
"""Gets a move from standard algebraic notation.
|
|
|
|
:param san:
|
|
A move string in standard algebraic notation.
|
|
|
|
:return:
|
|
A Move object.
|
|
|
|
:raise Exception:
|
|
If not exactly one legal move matches.
|
|
"""
|
|
# Castling moves.
|
|
if san == "O-O" or san == "O-O-O":
|
|
rank = 1 if self.turn == "w" else 8
|
|
if san == "O-O":
|
|
return Move(
|
|
source=Square.from_rank_and_file(rank, 'e'),
|
|
target=Square.from_rank_and_file(rank, 'g'))
|
|
else:
|
|
return Move(
|
|
source=Square.from_rank_and_file(rank, 'e'),
|
|
target=Square.from_rank_and_file(rank, 'c'))
|
|
# Regular moves.
|
|
else:
|
|
matches = Position.__san_regex.match(san)
|
|
if not matches:
|
|
raise ValueError("Invalid SAN: %s." % repr(san))
|
|
|
|
piece = Piece.from_color_and_type(
|
|
color=self.turn,
|
|
type=matches.group(1).lower() if matches.group(1) else 'p')
|
|
target = Square(matches.group(4))
|
|
|
|
source = None
|
|
for m in self.get_legal_moves():
|
|
if self[m.source] != piece or m.target != target:
|
|
continue
|
|
|
|
if matches.group(2) and matches.group(2) != m.source.file:
|
|
continue
|
|
if matches.group(3) and matches.group(3) != str(m.source.rank):
|
|
continue
|
|
|
|
# Move matches. Assert it is not ambiguous.
|
|
if source:
|
|
raise Exception(
|
|
"Move is ambiguous: %s matches %s and %s."
|
|
% san, source, m)
|
|
source = m.source
|
|
|
|
if not source:
|
|
raise Exception("No legal move matches %s." % san)
|
|
|
|
return Move(source, target, matches.group(5) or None)
|
|
|
|
def get_move_info(self, move):
|
|
"""Gets information about a move.
|
|
|
|
:param move:
|
|
The move to get information about.
|
|
|
|
:return:
|
|
A named tuple with these properties:
|
|
|
|
`move`:
|
|
The move object.
|
|
`piece`:
|
|
The piece that has been moved.
|
|
`san`:
|
|
The standard algebraic notation of the move.
|
|
`captured`:
|
|
The piece that has been captured or `None`.
|
|
`is_enpassant`:
|
|
A boolean indicating if the move is an en-passant
|
|
capture.
|
|
`is_king_side_castle`:
|
|
Whether it is a king-side castling move.
|
|
`is_queen_side_castle`:
|
|
Whether it is a queen-side castling move.
|
|
`is_castle`:
|
|
Whether it is a castling move.
|
|
`is_check`:
|
|
Whether the move gives check.
|
|
`is_checkmate`:
|
|
Whether the move gives checkmate.
|
|
|
|
:raise Exception:
|
|
If the move is not legal in the position.
|
|
"""
|
|
resulting_position = self.copy().make_move(move)
|
|
|
|
capture = self[move.target]
|
|
piece = self[move.source]
|
|
|
|
# Pawn moves.
|
|
enpassant = False
|
|
if piece.type == "p":
|
|
# En-passant.
|
|
if move.target.file != move.source.file and not capture:
|
|
enpassant = True
|
|
capture = Piece.from_color_and_type(
|
|
color=resulting_position.turn, type='p')
|
|
|
|
# Castling.
|
|
if piece.type == "k":
|
|
is_king_side_castle = move.target.x - move.source.x == 2
|
|
is_queen_side_castle = move.target.x - move.source.x == -2
|
|
else:
|
|
is_king_side_castle = is_queen_side_castle = False
|
|
|
|
# Checks.
|
|
is_check = resulting_position.is_check()
|
|
is_checkmate = resulting_position.is_checkmate()
|
|
|
|
# Generate the SAN.
|
|
san = ""
|
|
if is_king_side_castle:
|
|
san += "o-o"
|
|
elif is_queen_side_castle:
|
|
san += "o-o-o"
|
|
else:
|
|
if piece.type != 'p':
|
|
san += piece.type.upper()
|
|
|
|
san += self.__get_disambiguator(move)
|
|
|
|
if capture:
|
|
if piece.type == 'p':
|
|
san += move.source.file
|
|
san += "x"
|
|
san += move.target.name
|
|
|
|
if move.promotion:
|
|
san += "="
|
|
san += move.promotion.upper()
|
|
|
|
if is_checkmate:
|
|
san += "#"
|
|
elif is_check:
|
|
san += "+"
|
|
|
|
if enpassant:
|
|
san += " (e.p.)"
|
|
|
|
# Return the named tuple.
|
|
return MoveInfo(
|
|
move=move,
|
|
piece=piece,
|
|
captured=capture,
|
|
san=san,
|
|
is_enpassant=enpassant,
|
|
is_king_side_castle=is_king_side_castle,
|
|
is_queen_side_castle=is_queen_side_castle,
|
|
is_castle=is_king_side_castle or is_queen_side_castle,
|
|
is_check=is_check,
|
|
is_checkmate=is_checkmate)
|
|
|
|
def make_move(self, move, validate=True):
|
|
"""Makes a move.
|
|
|
|
:param move:
|
|
The move to make.
|
|
:param validate:
|
|
Defaults to `True`. Whether the move should be validated.
|
|
|
|
:return:
|
|
Making a move changes the position object. The same
|
|
(changed) object is returned for chainability.
|
|
|
|
:raise Exception:
|
|
If the validate parameter is `True` and the move is not
|
|
legal in the position.
|
|
"""
|
|
if validate and move not in self.get_legal_moves():
|
|
raise Exception(
|
|
"%s is not a legal move in the position %s." % (move, self.fen))
|
|
piece = self[move.source]
|
|
capture = self[move.target]
|
|
|
|
# Move the piece.
|
|
self[move.target] = self[move.source]
|
|
del self[move.source]
|
|
|
|
# It is the next players turn.
|
|
self.toggle_turn()
|
|
|
|
# Pawn moves.
|
|
self.ep_file = None
|
|
if piece.type == "p":
|
|
# En-passant.
|
|
if move.target.file != move.source.file and not capture:
|
|
if self.turn == "w":
|
|
self[move.target.x88 - 16] = None
|
|
else:
|
|
self[move.target.x88 + 16] = None
|
|
capture = True
|
|
# If big pawn move, set the en-passant file.
|
|
if abs(move.target.rank - move.source.rank) == 2:
|
|
if self.get_theoretical_ep_right(move.target.file):
|
|
self.ep_file = move.target.file
|
|
|
|
# Promotion.
|
|
if move.promotion:
|
|
self[move.target] = Piece.from_color_and_type(
|
|
color=piece.color, type=move.promotion)
|
|
|
|
# Potential castling.
|
|
if piece.type == "k":
|
|
steps = move.target.x - move.source.x
|
|
if abs(steps) == 2:
|
|
# Queen-side castling.
|
|
if steps == -2:
|
|
rook_target = move.target.x88 + 1
|
|
rook_source = move.target.x88 - 2
|
|
# King-side castling.
|
|
else:
|
|
rook_target = move.target.x88 - 1
|
|
rook_source = move.target.x88 + 1
|
|
self[rook_target] = self[rook_source]
|
|
del self[rook_source]
|
|
|
|
# Increment the half move counter.
|
|
if piece.type == "p" or capture:
|
|
self.half_moves = 0
|
|
else:
|
|
self.half_moves += 1
|
|
|
|
# Increment the move number.
|
|
if self.turn == "w":
|
|
self.ply += 1
|
|
|
|
# Update castling rights.
|
|
for type in ["K", "Q", "k", "q"]:
|
|
if not self.get_theoretical_castling_right(type):
|
|
self.set_castling_right(type, False)
|
|
|
|
return self
|
|
|
|
@property
|
|
def turn(self):
|
|
"""Whos turn it is as `"w"` or `"b"`."""
|
|
return self.__turn
|
|
|
|
@turn.setter
|
|
def turn(self, value):
|
|
if value not in ["w", "b"]:
|
|
raise ValueError(
|
|
"Expected 'w' or 'b' for turn, got: %s." % repr(value))
|
|
self.__turn = value
|
|
|
|
def toggle_turn(self):
|
|
"""Toggles whos turn it is."""
|
|
self.turn = opposite_color(self.turn)
|
|
|
|
def get_castling_right(self, type):
|
|
"""Checks the castling rights.
|
|
|
|
:param type:
|
|
The castling move to check. "K" for king-side castling of
|
|
the white player, "Q" for queen-side castling of the white
|
|
player. "k" and "q" for the corresponding castling moves of
|
|
the black player.
|
|
|
|
:return:
|
|
A boolean indicating whether the player has that castling
|
|
right.
|
|
"""
|
|
if not type in ["K", "Q", "k", "q"]:
|
|
raise KeyError(
|
|
"Expected 'K', 'Q', 'k' or 'q' as a castling type, got: %s." % repr(type))
|
|
return type in self.__castling
|
|
|
|
def get_theoretical_castling_right(self, type):
|
|
"""Checks if a player could have a castling right in theory from
|
|
looking just at the piece positions.
|
|
|
|
:param type:
|
|
The castling move to check. See
|
|
`Position.get_castling_right(type)` for values.
|
|
|
|
:return:
|
|
A boolean indicating whether the player could theoretically
|
|
have that castling right.
|
|
"""
|
|
if not type in ["K", "Q", "k", "q"]:
|
|
raise KeyError(
|
|
"Expected 'K', 'Q', 'k' or 'q' as a castling type, got: %s."
|
|
% repr(type))
|
|
if type == "K" or type == "Q":
|
|
if self["e1"] != Piece("K"):
|
|
return False
|
|
if type == "K":
|
|
return self["h1"] == Piece("R")
|
|
elif type == "Q":
|
|
return self["a1"] == Piece("R")
|
|
elif type == "k" or type == "q":
|
|
if self["e8"] != Piece("k"):
|
|
return False
|
|
if type == "k":
|
|
return self["h8"] == Piece("r")
|
|
elif type == "q":
|
|
return self["a8"] == Piece("r")
|
|
|
|
def get_theoretical_ep_right(self, file):
|
|
"""Checks if a player could have an ep-move in theory from
|
|
looking just at the piece positions.
|
|
|
|
:param file:
|
|
The file to check as a letter between `"a"` and `"h"`.
|
|
|
|
:return:
|
|
A boolean indicating whether the player could theoretically
|
|
have that en-passant move.
|
|
"""
|
|
if not file in ["a", "b", "c", "d", "e", "f", "g", "h"]:
|
|
raise KeyError(
|
|
"Expected a letter between 'a' and 'h' for the file, got: %s."
|
|
% repr(file))
|
|
|
|
# Check there is a pawn.
|
|
pawn_square = Square.from_rank_and_file(
|
|
rank=4 if self.turn == "b" else 5, file=file)
|
|
opposite_color_pawn = Piece.from_color_and_type(
|
|
color=opposite_color(self.turn), type="p")
|
|
if self[pawn_square] != opposite_color_pawn:
|
|
return False
|
|
|
|
# Check the square below is empty.
|
|
square_below = Square.from_rank_and_file(
|
|
rank=3 if self.turn == "b" else 6, file=file)
|
|
if self[square_below]:
|
|
return False
|
|
|
|
# Check there is a pawn of the other color on a neighbor file.
|
|
f = ord(file) - ord("a")
|
|
p = Piece("p")
|
|
P = Piece("P")
|
|
if self.turn == "b":
|
|
if f > 0 and self[Square.from_x_and_y(f - 1, 3)] == p:
|
|
return True
|
|
elif f < 7 and self[Square.from_x_and_y(f + 1, 3)] == p:
|
|
return True
|
|
else:
|
|
if f > 0 and self[Square.from_x_and_y(f - 1, 4)] == P:
|
|
return True
|
|
elif f < 7 and self[Square.from_x_and_y(f + 1, 4)] == P:
|
|
return True
|
|
return False
|
|
|
|
def set_castling_right(self, type, status):
|
|
"""Sets a castling right.
|
|
|
|
:param type:
|
|
`"K"`, `"Q"`, `"k"`, or `"q"` as used by
|
|
`Position.get_castling_right(type)`.
|
|
:param status:
|
|
A boolean indicating whether that castling right should be
|
|
granted or denied.
|
|
"""
|
|
if not type in ["K", "Q", "k", "q"]:
|
|
raise KeyError(
|
|
"Expected 'K', 'Q', 'k' or 'q' as a castling type, got: %s."
|
|
% repr(type))
|
|
|
|
castling = ""
|
|
for t in ["K", "Q", "k", "q"]:
|
|
if type == t:
|
|
if status:
|
|
castling += t
|
|
elif self.get_castling_right(t):
|
|
castling += t
|
|
self.__castling = castling
|
|
|
|
@property
|
|
def ep_file(self):
|
|
"""The en-passant file as a lowercase letter between `"a"` and
|
|
`"h"` or `None`."""
|
|
return self.__ep_file
|
|
|
|
@ep_file.setter
|
|
def ep_file(self, value):
|
|
if not value in ["a", "b", "c", "d", "e", "f", "g", "h", None]:
|
|
raise ValueError(
|
|
"Expected None or a letter between 'a' and 'h' for the "
|
|
"en-passant file, got: %s." % repr(value))
|
|
|
|
self.__ep_file = value
|
|
|
|
@property
|
|
def half_moves(self):
|
|
"""The number of half-moves since the last capture or pawn move."""
|
|
return self.__half_moves
|
|
|
|
@half_moves.setter
|
|
def half_moves(self, value):
|
|
if type(value) is not int:
|
|
raise TypeError(
|
|
"Expected integer for half move count, got: %s." % repr(value))
|
|
if value < 0:
|
|
raise ValueError("Half move count must be >= 0.")
|
|
|
|
self.__half_moves = value
|
|
|
|
@property
|
|
def ply(self):
|
|
"""The number of this move. The game starts at 1 and the counter
|
|
is incremented every time white moves.
|
|
"""
|
|
return self.__ply
|
|
|
|
@ply.setter
|
|
def ply(self, value):
|
|
if type(value) is not int:
|
|
raise TypeError(
|
|
"Expected integer for ply count, got: %s." % repr(value))
|
|
if value < 1:
|
|
raise ValueError("Ply count must be >= 1.")
|
|
self.__ply = value
|
|
|
|
def get_piece_counts(self, color = "wb"):
|
|
"""Counts the pieces on the board.
|
|
|
|
:param color:
|
|
Defaults to `"wb"`. A color to filter the pieces by. Valid
|
|
values are "w", "b", "wb" and "bw".
|
|
|
|
:return:
|
|
A dictionary of piece counts, keyed by lowercase piece type
|
|
letters.
|
|
"""
|
|
if not color in ["w", "b", "wb", "bw"]:
|
|
raise KeyError(
|
|
"Expected color filter to be one of 'w', 'b', 'wb', 'bw', "
|
|
"got: %s." % repr(color))
|
|
|
|
counts = {
|
|
"p": 0,
|
|
"b": 0,
|
|
"n": 0,
|
|
"r": 0,
|
|
"k": 0,
|
|
"q": 0,
|
|
}
|
|
for piece in self.__board:
|
|
if piece and piece.color in color:
|
|
counts[piece.type] += 1
|
|
return counts
|
|
|
|
def get_king(self, color):
|
|
"""Gets the square of the king.
|
|
|
|
:param color:
|
|
`"w"` for the white players king. `"b"` for the black
|
|
players king.
|
|
|
|
:return:
|
|
The first square with a matching king or `None` if that
|
|
player has no king.
|
|
"""
|
|
if not color in ["w", "b"]:
|
|
raise KeyError("Invalid color: %s." % repr(color))
|
|
|
|
for x88, piece in enumerate(self.__board):
|
|
if piece and piece.color == color and piece.type == "k":
|
|
return Square.from_x88(x88)
|
|
|
|
@property
|
|
def fen(self):
|
|
"""The FEN string representing the position."""
|
|
# Board setup.
|
|
empty = 0
|
|
fen = ""
|
|
for y in range(7, -1, -1):
|
|
for x in range(0, 8):
|
|
square = Square.from_x_and_y(x, y)
|
|
|
|
# Add pieces.
|
|
if not self[square]:
|
|
empty += 1
|
|
else:
|
|
if empty > 0:
|
|
fen += str(empty)
|
|
empty = 0
|
|
fen += self[square].symbol
|
|
|
|
# Boarder of the board.
|
|
if empty > 0:
|
|
fen += str(empty)
|
|
if not (x == 7 and y == 0):
|
|
fen += "/"
|
|
empty = 0
|
|
|
|
if self.ep_file and self.get_theoretical_ep_right(self.ep_file):
|
|
ep_square = self.ep_file + ("3" if self.turn == "b" else "6")
|
|
else:
|
|
ep_square = "-"
|
|
|
|
# Join the parts together.
|
|
return " ".join([
|
|
fen,
|
|
self.turn,
|
|
self.__castling if self.__castling else "-",
|
|
ep_square,
|
|
str(self.half_moves),
|
|
str(self.__ply)])
|
|
|
|
@fen.setter
|
|
def fen(self, fen):
|
|
# Split into 6 parts.
|
|
tokens = fen.split()
|
|
if len(tokens) != 6:
|
|
raise Exception("A FEN does not consist of 6 parts.")
|
|
|
|
# Check that the position part is valid.
|
|
rows = tokens[0].split("/")
|
|
assert len(rows) == 8
|
|
for row in rows:
|
|
field_sum = 0
|
|
previous_was_number = False
|
|
for char in row:
|
|
if char in "12345678":
|
|
if previous_was_number:
|
|
raise Exception(
|
|
"Position part of the FEN is invalid: "
|
|
"Multiple numbers immediately after each other.")
|
|
field_sum += int(char)
|
|
previous_was_number = True
|
|
elif char in "pnbrkqPNBRKQ":
|
|
field_sum += 1
|
|
previous_was_number = False
|
|
else:
|
|
raise Exception(
|
|
"Position part of the FEN is invalid: "
|
|
"Invalid character in the position part of the FEN.")
|
|
|
|
if field_sum != 8:
|
|
Exception(
|
|
"Position part of the FEN is invalid: "
|
|
"Row with invalid length.")
|
|
|
|
# Check that the other parts are valid.
|
|
if not tokens[1] in ["w", "b"]:
|
|
raise Exception(
|
|
"Turn part of the FEN is invalid: Expected b or w.")
|
|
if not re.compile(r"^(KQ?k?q?|Qk?q?|kq?|q|-)$").match(tokens[2]):
|
|
raise Exception("Castling part of the FEN is invalid.")
|
|
if not re.compile(r"^(-|[a-h][36])$").match(tokens[3]):
|
|
raise Exception("En-passant part of the FEN is invalid.")
|
|
if not re.compile(r"^(0|[1-9][0-9]*)$").match(tokens[4]):
|
|
raise Exception("Half move part of the FEN is invalid.")
|
|
if not re.compile(r"^[1-9][0-9]*$").match(tokens[5]):
|
|
raise Exception("Ply part of the FEN is invalid.")
|
|
|
|
# Set pieces on the board.
|
|
self.__board = [None] * 128
|
|
i = 0
|
|
for symbol in tokens[0]:
|
|
if symbol == "/":
|
|
i += 8
|
|
elif symbol in "12345678":
|
|
i += int(symbol)
|
|
else:
|
|
self.__board[i] = Piece(symbol)
|
|
i += 1
|
|
|
|
# Set the turn.
|
|
self.__turn = tokens[1]
|
|
|
|
# Set the castling rights.
|
|
for type in ["K", "Q", "k", "q"]:
|
|
self.set_castling_right(type, type in tokens[2])
|
|
|
|
# Set the en-passant file.
|
|
if tokens[3] == "-":
|
|
self.__ep_file = None
|
|
else:
|
|
self.__ep_file = tokens[3][0]
|
|
|
|
# Set the move counters.
|
|
self.__half_moves = int(tokens[4])
|
|
self.__ply = int(tokens[5])
|
|
|
|
def is_king_attacked(self, color):
|
|
""":return: Whether the king of the given color is attacked.
|
|
|
|
:param color: `"w"` or `"b"`.
|
|
"""
|
|
square = self.get_king(color)
|
|
if square:
|
|
return self.is_attacked(opposite_color(color), square)
|
|
else:
|
|
return False
|
|
|
|
def get_pseudo_legal_moves(self):
|
|
""":yield: Pseudo legal moves in the current position."""
|
|
PAWN_OFFSETS = {
|
|
"b": [16, 32, 17, 15],
|
|
"w": [-16, -32, -17, -15]
|
|
}
|
|
|
|
PIECE_OFFSETS = {
|
|
"n": [-18, -33, -31, -14, 18, 33, 31, 14],
|
|
"b": [-17, -15, 17, 15],
|
|
"r": [-16, 1, 16, -1],
|
|
"q": [-17, -16, -15, 1, 17, 16, 15, -1],
|
|
"k": [-17, -16, -15, 1, 17, 16, 15, -1]
|
|
}
|
|
|
|
for x88, piece in enumerate(self.__board):
|
|
# Skip pieces of the opponent.
|
|
if not piece or piece.color != self.turn:
|
|
continue
|
|
|
|
square = Square.from_x88(x88)
|
|
|
|
# Pawn moves.
|
|
if piece.type == "p":
|
|
# Single square ahead. Do not capture.
|
|
target = Square.from_x88(x88 + PAWN_OFFSETS[self.turn][0])
|
|
if not self[target]:
|
|
# Promotion.
|
|
if target.is_backrank():
|
|
for promote_to in "bnrq":
|
|
yield Move(square, target, promote_to)
|
|
else:
|
|
yield Move(square, target)
|
|
|
|
# Two squares ahead. Do not capture.
|
|
if (self.turn == "w" and square.rank == 2) or (self.turn == "b" and square.rank == 7):
|
|
target = Square.from_x88(square.x88 + PAWN_OFFSETS[self.turn][1])
|
|
if not self[target]:
|
|
yield Move(square, target)
|
|
|
|
# Pawn captures.
|
|
for j in [2, 3]:
|
|
target_index = square.x88 + PAWN_OFFSETS[self.turn][j]
|
|
if target_index & 0x88:
|
|
continue
|
|
target = Square.from_x88(target_index)
|
|
if self[target] and self[target].color != self.turn:
|
|
# Promotion.
|
|
if target.is_backrank():
|
|
for promote_to in "bnrq":
|
|
yield Move(square, target, promote_to)
|
|
else:
|
|
yield Move(square, target)
|
|
# En-passant.
|
|
elif not self[target] and target.file == self.ep_file:
|
|
yield Move(square, target)
|
|
# Other pieces.
|
|
else:
|
|
for offset in PIECE_OFFSETS[piece.type]:
|
|
target_index = square.x88
|
|
while True:
|
|
target_index += offset
|
|
if target_index & 0x88:
|
|
break
|
|
target = Square.from_x88(target_index)
|
|
if not self[target]:
|
|
yield Move(square, target)
|
|
else:
|
|
if self[target].color == self.turn:
|
|
break
|
|
yield Move(square, target)
|
|
break
|
|
|
|
# Knight and king do not go multiple times in their
|
|
# direction.
|
|
if piece.type in ["n", "k"]:
|
|
break
|
|
|
|
opponent = opposite_color(self.turn)
|
|
|
|
# King-side castling.
|
|
k = "k" if self.turn == "b" else "K"
|
|
if self.get_castling_right(k):
|
|
of = self.get_king(self.turn).x88
|
|
to = of + 2
|
|
if not self[of + 1] and not self[to] and not self.is_check() and not self.is_attacked(opponent, Square.from_x88(of + 1)) and not self.is_attacked(opponent, Square.from_x88(to)):
|
|
yield Move(Square.from_x88(of), Square.from_x88(to))
|
|
|
|
# Queen-side castling
|
|
q = "q" if self.turn == "b" else "Q"
|
|
if self.get_castling_right(q):
|
|
of = self.get_king(self.turn).x88
|
|
to = of - 2
|
|
|
|
if not self[of - 1] and not self[of - 2] and not self[of - 3] and not self.is_check() and not self.is_attacked(opponent, Square.from_x88(of - 1)) and not self.is_attacked(opponent, Square.from_x88(to)):
|
|
yield Move(Square.from_x88(of), Square.from_x88(to))
|
|
|
|
def get_legal_moves(self):
|
|
""":yield: All legal moves in the current position."""
|
|
for move in self.get_pseudo_legal_moves():
|
|
potential_position = self.copy()
|
|
potential_position.make_move(move, False)
|
|
if not potential_position.is_king_attacked(self.turn):
|
|
yield move
|
|
|
|
def get_attackers(self, color, square):
|
|
"""Gets the attackers of a specific square.
|
|
|
|
:param color:
|
|
Filter attackers by this piece color.
|
|
:param square:
|
|
The square to check for.
|
|
|
|
:yield:
|
|
Source squares of the attack.
|
|
"""
|
|
if color not in ["b", "w"]:
|
|
raise KeyError("Invalid color: %s." % repr(color))
|
|
|
|
ATTACKS = [
|
|
20, 0, 0, 0, 0, 0, 0, 24, 0, 0, 0, 0, 0, 0, 20, 0,
|
|
0, 20, 0, 0, 0, 0, 0, 24, 0, 0, 0, 0, 0, 20, 0, 0,
|
|
0, 0, 20, 0, 0, 0, 0, 24, 0, 0, 0, 0, 20, 0, 0, 0,
|
|
0, 0, 0, 20, 0, 0, 0, 24, 0, 0, 0, 20, 0, 0, 0, 0,
|
|
0, 0, 0, 0, 20, 0, 0, 24, 0, 0, 20, 0, 0, 0, 0, 0,
|
|
0, 0, 0, 0, 0, 20, 2, 24, 2, 20, 0, 0, 0, 0, 0, 0,
|
|
0, 0, 0, 0, 0, 2, 53, 56, 53, 2, 0, 0, 0, 0, 0, 0,
|
|
24, 24, 24, 24, 24, 24, 56, 0, 56, 24, 24, 24, 24, 24, 24, 0,
|
|
0, 0, 0, 0, 0, 2, 53, 56, 53, 2, 0, 0, 0, 0, 0, 0,
|
|
0, 0, 0, 0, 0, 20, 2, 24, 2, 20, 0, 0, 0, 0, 0, 0,
|
|
0, 0, 0, 0, 20, 0, 0, 24, 0, 0, 20, 0, 0, 0, 0, 0,
|
|
0, 0, 0, 20, 0, 0, 0, 24, 0, 0, 0, 20, 0, 0, 0, 0,
|
|
0, 0, 20, 0, 0, 0, 0, 24, 0, 0, 0, 0, 20, 0, 0, 0,
|
|
0, 20, 0, 0, 0, 0, 0, 24, 0, 0, 0, 0, 0, 20, 0, 0,
|
|
20, 0, 0, 0, 0, 0, 0, 24, 0, 0, 0, 0, 0, 0, 20
|
|
]
|
|
|
|
RAYS = [
|
|
17, 0, 0, 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 15, 0,
|
|
0, 17, 0, 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 15, 0, 0,
|
|
0, 0, 17, 0, 0, 0, 0, 16, 0, 0, 0, 0, 15, 0, 0, 0,
|
|
0, 0, 0, 17, 0, 0, 0, 16, 0, 0, 0, 15, 0, 0, 0, 0,
|
|
0, 0, 0, 0, 17, 0, 0, 16, 0, 0, 15, 0, 0, 0, 0, 0,
|
|
0, 0, 0, 0, 0, 17, 0, 16, 0, 15, 0, 0, 0, 0, 0, 0,
|
|
0, 0, 0, 0, 0, 0, 17, 16, 15, 0, 0, 0, 0, 0, 0, 0,
|
|
1, 1, 1, 1, 1, 1, 1, 0, -1, -1, -1, -1, -1, -1, -1, 0,
|
|
0, 0, 0, 0, 0, 0, -15, -16, -17, 0, 0, 0, 0, 0, 0, 0,
|
|
0, 0, 0, 0, 0, -15, 0, -16, 0, -17, 0, 0, 0, 0, 0, 0,
|
|
0, 0, 0, 0, -15, 0, 0, -16, 0, 0, -17, 0, 0, 0, 0, 0,
|
|
0, 0, 0, -15, 0, 0, 0, -16, 0, 0, 0, -17, 0, 0, 0, 0,
|
|
0, 0, -15, 0, 0, 0, 0, -16, 0, 0, 0, 0, -17, 0, 0, 0,
|
|
0, -15, 0, 0, 0, 0, 0, -16, 0, 0, 0, 0, 0, -17, 0, 0,
|
|
-15, 0, 0, 0, 0, 0, 0, -16, 0, 0, 0, 0, 0, 0, -17
|
|
]
|
|
|
|
SHIFTS = {
|
|
"p": 0,
|
|
"n": 1,
|
|
"b": 2,
|
|
"r": 3,
|
|
"q": 4,
|
|
"k": 5
|
|
}
|
|
|
|
for x88, piece in enumerate(self.__board):
|
|
if not piece or piece.color != color:
|
|
continue
|
|
source = Square.from_x88(x88)
|
|
|
|
difference = source.x88 - square.x88
|
|
index = difference + 119
|
|
|
|
if ATTACKS[index] & (1 << SHIFTS[piece.type]):
|
|
# Handle pawns.
|
|
if piece.type == "p":
|
|
if difference > 0:
|
|
if piece.color == "w":
|
|
yield source
|
|
else:
|
|
if piece.color == "b":
|
|
yield source
|
|
continue
|
|
|
|
# Handle knights and king.
|
|
if piece.type in ["n", "k"]:
|
|
yield source
|
|
|
|
# Handle the others.
|
|
offset = RAYS[index]
|
|
j = source.x88 + offset
|
|
blocked = False
|
|
while j != square.x88:
|
|
if self[j]:
|
|
blocked = True
|
|
break
|
|
j += offset
|
|
if not blocked:
|
|
yield source
|
|
|
|
def is_attacked(self, color, square):
|
|
"""Checks whether a square is attacked.
|
|
|
|
:param color:
|
|
Check if this player is attacking.
|
|
:param square:
|
|
The square the player might be attacking.
|
|
|
|
:return:
|
|
A boolean indicating whether the given square is attacked
|
|
by the player of the given color.
|
|
"""
|
|
x = list(self.get_attackers(color, square))
|
|
return len(x) > 0
|
|
|
|
def is_check(self):
|
|
""":return: Whether the current player is in check."""
|
|
return self.is_king_attacked(self.turn)
|
|
|
|
def is_checkmate(self):
|
|
""":return: Whether the current player has been checkmated."""
|
|
if not self.is_check():
|
|
return False
|
|
else:
|
|
arr = list(self.get_legal_moves())
|
|
return len(arr) == 0
|
|
|
|
def is_stalemate(self):
|
|
""":return: Whether the current player is in stalemate."""
|
|
if self.is_check():
|
|
return False
|
|
else:
|
|
arr = list(self.get_legal_moves())
|
|
return len(arr) == 0
|
|
|
|
def is_insufficient_material(self):
|
|
"""Checks if there is sufficient material to mate.
|
|
|
|
Mating is impossible in:
|
|
|
|
* A king versus king endgame.
|
|
* A king with bishop versus king endgame.
|
|
* A king with knight versus king endgame.
|
|
* A king with bishop versus king with bishop endgame, where both
|
|
bishops are on the same color. Same goes for additional
|
|
bishops on the same color.
|
|
|
|
Assumes that the position is valid and each player has exactly
|
|
one king.
|
|
|
|
:return:
|
|
Whether there is insufficient material to mate.
|
|
"""
|
|
piece_counts = self.get_piece_counts()
|
|
if sum(piece_counts.values()) == 2:
|
|
# King versus king.
|
|
return True
|
|
elif sum(piece_counts.values()) == 3:
|
|
# King and knight or bishop versus king.
|
|
if piece_counts["b"] == 1 or piece_counts["n"] == 1:
|
|
return True
|
|
elif sum(piece_counts.values()) == 2 + piece_counts["b"]:
|
|
# Each player with only king and any number of bishops, where all
|
|
# bishops are on the same color.
|
|
white_has_bishop = self.get_piece_counts("w")["b"] != 0
|
|
black_has_bishop = self.get_piece_counts("b")["b"] != 0
|
|
if white_has_bishop and black_has_bishop:
|
|
color = None
|
|
for x88, piece in enumerate(self.__board):
|
|
if piece and piece.type == "b":
|
|
square = Square.from_x88(x88)
|
|
if color is not None and color != square.is_light():
|
|
return False
|
|
color = square.is_light()
|
|
return True
|
|
return False
|
|
|
|
def is_game_over(self):
|
|
"""Checks if the game is over.
|
|
|
|
:return:
|
|
Whether the game is over by the rules of chess,
|
|
disregarding that players can agree on a draw, claim a draw
|
|
or resign.
|
|
"""
|
|
return (self.is_checkmate() or self.is_stalemate() or
|
|
self.is_insufficient_material())
|
|
|
|
def __str__(self):
|
|
return self.fen
|
|
|
|
def __repr__(self):
|
|
return "Position.from_fen(%s)" % repr(self.fen)
|
|
|
|
def __eq__(self, other):
|
|
return self.fen == other.fen
|
|
|
|
def __ne__(self, other):
|
|
return self.fen != other.fen
|
|
|
|
def __hash__(self):
|
|
hasher = ZobristHasher(ZobristHasher.POLYGLOT_RANDOM_ARRAY)
|
|
return hasher.hash_position(self)
|
|
|
|
|
|
class ZobristHasher(object):
|
|
"""Represents a zobrist-hash function.
|
|
|
|
:param random_array:
|
|
An tuple or list of 781 random 64 bit integers.
|
|
|
|
`POLYGLOT_RANDOM_ARRAY`:
|
|
A tuple of 781 random numbers as used by the Polyglot opening
|
|
book format.
|
|
"""
|
|
|
|
POLYGLOT_RANDOM_ARRAY = (
|
|
0x9D39247E33776D41, 0x2AF7398005AAA5C7, 0x44DB015024623547,
|
|
0x9C15F73E62A76AE2, 0x75834465489C0C89, 0x3290AC3A203001BF,
|
|
0x0FBBAD1F61042279, 0xE83A908FF2FB60CA, 0x0D7E765D58755C10,
|
|
0x1A083822CEAFE02D, 0x9605D5F0E25EC3B0, 0xD021FF5CD13A2ED5,
|
|
0x40BDF15D4A672E32, 0x011355146FD56395, 0x5DB4832046F3D9E5,
|
|
0x239F8B2D7FF719CC, 0x05D1A1AE85B49AA1, 0x679F848F6E8FC971,
|
|
0x7449BBFF801FED0B, 0x7D11CDB1C3B7ADF0, 0x82C7709E781EB7CC,
|
|
0xF3218F1C9510786C, 0x331478F3AF51BBE6, 0x4BB38DE5E7219443,
|
|
0xAA649C6EBCFD50FC, 0x8DBD98A352AFD40B, 0x87D2074B81D79217,
|
|
0x19F3C751D3E92AE1, 0xB4AB30F062B19ABF, 0x7B0500AC42047AC4,
|
|
0xC9452CA81A09D85D, 0x24AA6C514DA27500, 0x4C9F34427501B447,
|
|
0x14A68FD73C910841, 0xA71B9B83461CBD93, 0x03488B95B0F1850F,
|
|
0x637B2B34FF93C040, 0x09D1BC9A3DD90A94, 0x3575668334A1DD3B,
|
|
0x735E2B97A4C45A23, 0x18727070F1BD400B, 0x1FCBACD259BF02E7,
|
|
0xD310A7C2CE9B6555, 0xBF983FE0FE5D8244, 0x9F74D14F7454A824,
|
|
0x51EBDC4AB9BA3035, 0x5C82C505DB9AB0FA, 0xFCF7FE8A3430B241,
|
|
0x3253A729B9BA3DDE, 0x8C74C368081B3075, 0xB9BC6C87167C33E7,
|
|
0x7EF48F2B83024E20, 0x11D505D4C351BD7F, 0x6568FCA92C76A243,
|
|
0x4DE0B0F40F32A7B8, 0x96D693460CC37E5D, 0x42E240CB63689F2F,
|
|
0x6D2BDCDAE2919661, 0x42880B0236E4D951, 0x5F0F4A5898171BB6,
|
|
0x39F890F579F92F88, 0x93C5B5F47356388B, 0x63DC359D8D231B78,
|
|
0xEC16CA8AEA98AD76, 0x5355F900C2A82DC7, 0x07FB9F855A997142,
|
|
0x5093417AA8A7ED5E, 0x7BCBC38DA25A7F3C, 0x19FC8A768CF4B6D4,
|
|
0x637A7780DECFC0D9, 0x8249A47AEE0E41F7, 0x79AD695501E7D1E8,
|
|
0x14ACBAF4777D5776, 0xF145B6BECCDEA195, 0xDABF2AC8201752FC,
|
|
0x24C3C94DF9C8D3F6, 0xBB6E2924F03912EA, 0x0CE26C0B95C980D9,
|
|
0xA49CD132BFBF7CC4, 0xE99D662AF4243939, 0x27E6AD7891165C3F,
|
|
0x8535F040B9744FF1, 0x54B3F4FA5F40D873, 0x72B12C32127FED2B,
|
|
0xEE954D3C7B411F47, 0x9A85AC909A24EAA1, 0x70AC4CD9F04F21F5,
|
|
0xF9B89D3E99A075C2, 0x87B3E2B2B5C907B1, 0xA366E5B8C54F48B8,
|
|
0xAE4A9346CC3F7CF2, 0x1920C04D47267BBD, 0x87BF02C6B49E2AE9,
|
|
0x092237AC237F3859, 0xFF07F64EF8ED14D0, 0x8DE8DCA9F03CC54E,
|
|
0x9C1633264DB49C89, 0xB3F22C3D0B0B38ED, 0x390E5FB44D01144B,
|
|
0x5BFEA5B4712768E9, 0x1E1032911FA78984, 0x9A74ACB964E78CB3,
|
|
0x4F80F7A035DAFB04, 0x6304D09A0B3738C4, 0x2171E64683023A08,
|
|
0x5B9B63EB9CEFF80C, 0x506AACF489889342, 0x1881AFC9A3A701D6,
|
|
0x6503080440750644, 0xDFD395339CDBF4A7, 0xEF927DBCF00C20F2,
|
|
0x7B32F7D1E03680EC, 0xB9FD7620E7316243, 0x05A7E8A57DB91B77,
|
|
0xB5889C6E15630A75, 0x4A750A09CE9573F7, 0xCF464CEC899A2F8A,
|
|
0xF538639CE705B824, 0x3C79A0FF5580EF7F, 0xEDE6C87F8477609D,
|
|
0x799E81F05BC93F31, 0x86536B8CF3428A8C, 0x97D7374C60087B73,
|
|
0xA246637CFF328532, 0x043FCAE60CC0EBA0, 0x920E449535DD359E,
|
|
0x70EB093B15B290CC, 0x73A1921916591CBD, 0x56436C9FE1A1AA8D,
|
|
0xEFAC4B70633B8F81, 0xBB215798D45DF7AF, 0x45F20042F24F1768,
|
|
0x930F80F4E8EB7462, 0xFF6712FFCFD75EA1, 0xAE623FD67468AA70,
|
|
0xDD2C5BC84BC8D8FC, 0x7EED120D54CF2DD9, 0x22FE545401165F1C,
|
|
0xC91800E98FB99929, 0x808BD68E6AC10365, 0xDEC468145B7605F6,
|
|
0x1BEDE3A3AEF53302, 0x43539603D6C55602, 0xAA969B5C691CCB7A,
|
|
0xA87832D392EFEE56, 0x65942C7B3C7E11AE, 0xDED2D633CAD004F6,
|
|
0x21F08570F420E565, 0xB415938D7DA94E3C, 0x91B859E59ECB6350,
|
|
0x10CFF333E0ED804A, 0x28AED140BE0BB7DD, 0xC5CC1D89724FA456,
|
|
0x5648F680F11A2741, 0x2D255069F0B7DAB3, 0x9BC5A38EF729ABD4,
|
|
0xEF2F054308F6A2BC, 0xAF2042F5CC5C2858, 0x480412BAB7F5BE2A,
|
|
0xAEF3AF4A563DFE43, 0x19AFE59AE451497F, 0x52593803DFF1E840,
|
|
0xF4F076E65F2CE6F0, 0x11379625747D5AF3, 0xBCE5D2248682C115,
|
|
0x9DA4243DE836994F, 0x066F70B33FE09017, 0x4DC4DE189B671A1C,
|
|
0x51039AB7712457C3, 0xC07A3F80C31FB4B4, 0xB46EE9C5E64A6E7C,
|
|
0xB3819A42ABE61C87, 0x21A007933A522A20, 0x2DF16F761598AA4F,
|
|
0x763C4A1371B368FD, 0xF793C46702E086A0, 0xD7288E012AEB8D31,
|
|
0xDE336A2A4BC1C44B, 0x0BF692B38D079F23, 0x2C604A7A177326B3,
|
|
0x4850E73E03EB6064, 0xCFC447F1E53C8E1B, 0xB05CA3F564268D99,
|
|
0x9AE182C8BC9474E8, 0xA4FC4BD4FC5558CA, 0xE755178D58FC4E76,
|
|
0x69B97DB1A4C03DFE, 0xF9B5B7C4ACC67C96, 0xFC6A82D64B8655FB,
|
|
0x9C684CB6C4D24417, 0x8EC97D2917456ED0, 0x6703DF9D2924E97E,
|
|
0xC547F57E42A7444E, 0x78E37644E7CAD29E, 0xFE9A44E9362F05FA,
|
|
0x08BD35CC38336615, 0x9315E5EB3A129ACE, 0x94061B871E04DF75,
|
|
0xDF1D9F9D784BA010, 0x3BBA57B68871B59D, 0xD2B7ADEEDED1F73F,
|
|
0xF7A255D83BC373F8, 0xD7F4F2448C0CEB81, 0xD95BE88CD210FFA7,
|
|
0x336F52F8FF4728E7, 0xA74049DAC312AC71, 0xA2F61BB6E437FDB5,
|
|
0x4F2A5CB07F6A35B3, 0x87D380BDA5BF7859, 0x16B9F7E06C453A21,
|
|
0x7BA2484C8A0FD54E, 0xF3A678CAD9A2E38C, 0x39B0BF7DDE437BA2,
|
|
0xFCAF55C1BF8A4424, 0x18FCF680573FA594, 0x4C0563B89F495AC3,
|
|
0x40E087931A00930D, 0x8CFFA9412EB642C1, 0x68CA39053261169F,
|
|
0x7A1EE967D27579E2, 0x9D1D60E5076F5B6F, 0x3810E399B6F65BA2,
|
|
0x32095B6D4AB5F9B1, 0x35CAB62109DD038A, 0xA90B24499FCFAFB1,
|
|
0x77A225A07CC2C6BD, 0x513E5E634C70E331, 0x4361C0CA3F692F12,
|
|
0xD941ACA44B20A45B, 0x528F7C8602C5807B, 0x52AB92BEB9613989,
|
|
0x9D1DFA2EFC557F73, 0x722FF175F572C348, 0x1D1260A51107FE97,
|
|
0x7A249A57EC0C9BA2, 0x04208FE9E8F7F2D6, 0x5A110C6058B920A0,
|
|
0x0CD9A497658A5698, 0x56FD23C8F9715A4C, 0x284C847B9D887AAE,
|
|
0x04FEABFBBDB619CB, 0x742E1E651C60BA83, 0x9A9632E65904AD3C,
|
|
0x881B82A13B51B9E2, 0x506E6744CD974924, 0xB0183DB56FFC6A79,
|
|
0x0ED9B915C66ED37E, 0x5E11E86D5873D484, 0xF678647E3519AC6E,
|
|
0x1B85D488D0F20CC5, 0xDAB9FE6525D89021, 0x0D151D86ADB73615,
|
|
0xA865A54EDCC0F019, 0x93C42566AEF98FFB, 0x99E7AFEABE000731,
|
|
0x48CBFF086DDF285A, 0x7F9B6AF1EBF78BAF, 0x58627E1A149BBA21,
|
|
0x2CD16E2ABD791E33, 0xD363EFF5F0977996, 0x0CE2A38C344A6EED,
|
|
0x1A804AADB9CFA741, 0x907F30421D78C5DE, 0x501F65EDB3034D07,
|
|
0x37624AE5A48FA6E9, 0x957BAF61700CFF4E, 0x3A6C27934E31188A,
|
|
0xD49503536ABCA345, 0x088E049589C432E0, 0xF943AEE7FEBF21B8,
|
|
0x6C3B8E3E336139D3, 0x364F6FFA464EE52E, 0xD60F6DCEDC314222,
|
|
0x56963B0DCA418FC0, 0x16F50EDF91E513AF, 0xEF1955914B609F93,
|
|
0x565601C0364E3228, 0xECB53939887E8175, 0xBAC7A9A18531294B,
|
|
0xB344C470397BBA52, 0x65D34954DAF3CEBD, 0xB4B81B3FA97511E2,
|
|
0xB422061193D6F6A7, 0x071582401C38434D, 0x7A13F18BBEDC4FF5,
|
|
0xBC4097B116C524D2, 0x59B97885E2F2EA28, 0x99170A5DC3115544,
|
|
0x6F423357E7C6A9F9, 0x325928EE6E6F8794, 0xD0E4366228B03343,
|
|
0x565C31F7DE89EA27, 0x30F5611484119414, 0xD873DB391292ED4F,
|
|
0x7BD94E1D8E17DEBC, 0xC7D9F16864A76E94, 0x947AE053EE56E63C,
|
|
0xC8C93882F9475F5F, 0x3A9BF55BA91F81CA, 0xD9A11FBB3D9808E4,
|
|
0x0FD22063EDC29FCA, 0xB3F256D8ACA0B0B9, 0xB03031A8B4516E84,
|
|
0x35DD37D5871448AF, 0xE9F6082B05542E4E, 0xEBFAFA33D7254B59,
|
|
0x9255ABB50D532280, 0xB9AB4CE57F2D34F3, 0x693501D628297551,
|
|
0xC62C58F97DD949BF, 0xCD454F8F19C5126A, 0xBBE83F4ECC2BDECB,
|
|
0xDC842B7E2819E230, 0xBA89142E007503B8, 0xA3BC941D0A5061CB,
|
|
0xE9F6760E32CD8021, 0x09C7E552BC76492F, 0x852F54934DA55CC9,
|
|
0x8107FCCF064FCF56, 0x098954D51FFF6580, 0x23B70EDB1955C4BF,
|
|
0xC330DE426430F69D, 0x4715ED43E8A45C0A, 0xA8D7E4DAB780A08D,
|
|
0x0572B974F03CE0BB, 0xB57D2E985E1419C7, 0xE8D9ECBE2CF3D73F,
|
|
0x2FE4B17170E59750, 0x11317BA87905E790, 0x7FBF21EC8A1F45EC,
|
|
0x1725CABFCB045B00, 0x964E915CD5E2B207, 0x3E2B8BCBF016D66D,
|
|
0xBE7444E39328A0AC, 0xF85B2B4FBCDE44B7, 0x49353FEA39BA63B1,
|
|
0x1DD01AAFCD53486A, 0x1FCA8A92FD719F85, 0xFC7C95D827357AFA,
|
|
0x18A6A990C8B35EBD, 0xCCCB7005C6B9C28D, 0x3BDBB92C43B17F26,
|
|
0xAA70B5B4F89695A2, 0xE94C39A54A98307F, 0xB7A0B174CFF6F36E,
|
|
0xD4DBA84729AF48AD, 0x2E18BC1AD9704A68, 0x2DE0966DAF2F8B1C,
|
|
0xB9C11D5B1E43A07E, 0x64972D68DEE33360, 0x94628D38D0C20584,
|
|
0xDBC0D2B6AB90A559, 0xD2733C4335C6A72F, 0x7E75D99D94A70F4D,
|
|
0x6CED1983376FA72B, 0x97FCAACBF030BC24, 0x7B77497B32503B12,
|
|
0x8547EDDFB81CCB94, 0x79999CDFF70902CB, 0xCFFE1939438E9B24,
|
|
0x829626E3892D95D7, 0x92FAE24291F2B3F1, 0x63E22C147B9C3403,
|
|
0xC678B6D860284A1C, 0x5873888850659AE7, 0x0981DCD296A8736D,
|
|
0x9F65789A6509A440, 0x9FF38FED72E9052F, 0xE479EE5B9930578C,
|
|
0xE7F28ECD2D49EECD, 0x56C074A581EA17FE, 0x5544F7D774B14AEF,
|
|
0x7B3F0195FC6F290F, 0x12153635B2C0CF57, 0x7F5126DBBA5E0CA7,
|
|
0x7A76956C3EAFB413, 0x3D5774A11D31AB39, 0x8A1B083821F40CB4,
|
|
0x7B4A38E32537DF62, 0x950113646D1D6E03, 0x4DA8979A0041E8A9,
|
|
0x3BC36E078F7515D7, 0x5D0A12F27AD310D1, 0x7F9D1A2E1EBE1327,
|
|
0xDA3A361B1C5157B1, 0xDCDD7D20903D0C25, 0x36833336D068F707,
|
|
0xCE68341F79893389, 0xAB9090168DD05F34, 0x43954B3252DC25E5,
|
|
0xB438C2B67F98E5E9, 0x10DCD78E3851A492, 0xDBC27AB5447822BF,
|
|
0x9B3CDB65F82CA382, 0xB67B7896167B4C84, 0xBFCED1B0048EAC50,
|
|
0xA9119B60369FFEBD, 0x1FFF7AC80904BF45, 0xAC12FB171817EEE7,
|
|
0xAF08DA9177DDA93D, 0x1B0CAB936E65C744, 0xB559EB1D04E5E932,
|
|
0xC37B45B3F8D6F2BA, 0xC3A9DC228CAAC9E9, 0xF3B8B6675A6507FF,
|
|
0x9FC477DE4ED681DA, 0x67378D8ECCEF96CB, 0x6DD856D94D259236,
|
|
0xA319CE15B0B4DB31, 0x073973751F12DD5E, 0x8A8E849EB32781A5,
|
|
0xE1925C71285279F5, 0x74C04BF1790C0EFE, 0x4DDA48153C94938A,
|
|
0x9D266D6A1CC0542C, 0x7440FB816508C4FE, 0x13328503DF48229F,
|
|
0xD6BF7BAEE43CAC40, 0x4838D65F6EF6748F, 0x1E152328F3318DEA,
|
|
0x8F8419A348F296BF, 0x72C8834A5957B511, 0xD7A023A73260B45C,
|
|
0x94EBC8ABCFB56DAE, 0x9FC10D0F989993E0, 0xDE68A2355B93CAE6,
|
|
0xA44CFE79AE538BBE, 0x9D1D84FCCE371425, 0x51D2B1AB2DDFB636,
|
|
0x2FD7E4B9E72CD38C, 0x65CA5B96B7552210, 0xDD69A0D8AB3B546D,
|
|
0x604D51B25FBF70E2, 0x73AA8A564FB7AC9E, 0x1A8C1E992B941148,
|
|
0xAAC40A2703D9BEA0, 0x764DBEAE7FA4F3A6, 0x1E99B96E70A9BE8B,
|
|
0x2C5E9DEB57EF4743, 0x3A938FEE32D29981, 0x26E6DB8FFDF5ADFE,
|
|
0x469356C504EC9F9D, 0xC8763C5B08D1908C, 0x3F6C6AF859D80055,
|
|
0x7F7CC39420A3A545, 0x9BFB227EBDF4C5CE, 0x89039D79D6FC5C5C,
|
|
0x8FE88B57305E2AB6, 0xA09E8C8C35AB96DE, 0xFA7E393983325753,
|
|
0xD6B6D0ECC617C699, 0xDFEA21EA9E7557E3, 0xB67C1FA481680AF8,
|
|
0xCA1E3785A9E724E5, 0x1CFC8BED0D681639, 0xD18D8549D140CAEA,
|
|
0x4ED0FE7E9DC91335, 0xE4DBF0634473F5D2, 0x1761F93A44D5AEFE,
|
|
0x53898E4C3910DA55, 0x734DE8181F6EC39A, 0x2680B122BAA28D97,
|
|
0x298AF231C85BAFAB, 0x7983EED3740847D5, 0x66C1A2A1A60CD889,
|
|
0x9E17E49642A3E4C1, 0xEDB454E7BADC0805, 0x50B704CAB602C329,
|
|
0x4CC317FB9CDDD023, 0x66B4835D9EAFEA22, 0x219B97E26FFC81BD,
|
|
0x261E4E4C0A333A9D, 0x1FE2CCA76517DB90, 0xD7504DFA8816EDBB,
|
|
0xB9571FA04DC089C8, 0x1DDC0325259B27DE, 0xCF3F4688801EB9AA,
|
|
0xF4F5D05C10CAB243, 0x38B6525C21A42B0E, 0x36F60E2BA4FA6800,
|
|
0xEB3593803173E0CE, 0x9C4CD6257C5A3603, 0xAF0C317D32ADAA8A,
|
|
0x258E5A80C7204C4B, 0x8B889D624D44885D, 0xF4D14597E660F855,
|
|
0xD4347F66EC8941C3, 0xE699ED85B0DFB40D, 0x2472F6207C2D0484,
|
|
0xC2A1E7B5B459AEB5, 0xAB4F6451CC1D45EC, 0x63767572AE3D6174,
|
|
0xA59E0BD101731A28, 0x116D0016CB948F09, 0x2CF9C8CA052F6E9F,
|
|
0x0B090A7560A968E3, 0xABEEDDB2DDE06FF1, 0x58EFC10B06A2068D,
|
|
0xC6E57A78FBD986E0, 0x2EAB8CA63CE802D7, 0x14A195640116F336,
|
|
0x7C0828DD624EC390, 0xD74BBE77E6116AC7, 0x804456AF10F5FB53,
|
|
0xEBE9EA2ADF4321C7, 0x03219A39EE587A30, 0x49787FEF17AF9924,
|
|
0xA1E9300CD8520548, 0x5B45E522E4B1B4EF, 0xB49C3B3995091A36,
|
|
0xD4490AD526F14431, 0x12A8F216AF9418C2, 0x001F837CC7350524,
|
|
0x1877B51E57A764D5, 0xA2853B80F17F58EE, 0x993E1DE72D36D310,
|
|
0xB3598080CE64A656, 0x252F59CF0D9F04BB, 0xD23C8E176D113600,
|
|
0x1BDA0492E7E4586E, 0x21E0BD5026C619BF, 0x3B097ADAF088F94E,
|
|
0x8D14DEDB30BE846E, 0xF95CFFA23AF5F6F4, 0x3871700761B3F743,
|
|
0xCA672B91E9E4FA16, 0x64C8E531BFF53B55, 0x241260ED4AD1E87D,
|
|
0x106C09B972D2E822, 0x7FBA195410E5CA30, 0x7884D9BC6CB569D8,
|
|
0x0647DFEDCD894A29, 0x63573FF03E224774, 0x4FC8E9560F91B123,
|
|
0x1DB956E450275779, 0xB8D91274B9E9D4FB, 0xA2EBEE47E2FBFCE1,
|
|
0xD9F1F30CCD97FB09, 0xEFED53D75FD64E6B, 0x2E6D02C36017F67F,
|
|
0xA9AA4D20DB084E9B, 0xB64BE8D8B25396C1, 0x70CB6AF7C2D5BCF0,
|
|
0x98F076A4F7A2322E, 0xBF84470805E69B5F, 0x94C3251F06F90CF3,
|
|
0x3E003E616A6591E9, 0xB925A6CD0421AFF3, 0x61BDD1307C66E300,
|
|
0xBF8D5108E27E0D48, 0x240AB57A8B888B20, 0xFC87614BAF287E07,
|
|
0xEF02CDD06FFDB432, 0xA1082C0466DF6C0A, 0x8215E577001332C8,
|
|
0xD39BB9C3A48DB6CF, 0x2738259634305C14, 0x61CF4F94C97DF93D,
|
|
0x1B6BACA2AE4E125B, 0x758F450C88572E0B, 0x959F587D507A8359,
|
|
0xB063E962E045F54D, 0x60E8ED72C0DFF5D1, 0x7B64978555326F9F,
|
|
0xFD080D236DA814BA, 0x8C90FD9B083F4558, 0x106F72FE81E2C590,
|
|
0x7976033A39F7D952, 0xA4EC0132764CA04B, 0x733EA705FAE4FA77,
|
|
0xB4D8F77BC3E56167, 0x9E21F4F903B33FD9, 0x9D765E419FB69F6D,
|
|
0xD30C088BA61EA5EF, 0x5D94337FBFAF7F5B, 0x1A4E4822EB4D7A59,
|
|
0x6FFE73E81B637FB3, 0xDDF957BC36D8B9CA, 0x64D0E29EEA8838B3,
|
|
0x08DD9BDFD96B9F63, 0x087E79E5A57D1D13, 0xE328E230E3E2B3FB,
|
|
0x1C2559E30F0946BE, 0x720BF5F26F4D2EAA, 0xB0774D261CC609DB,
|
|
0x443F64EC5A371195, 0x4112CF68649A260E, 0xD813F2FAB7F5C5CA,
|
|
0x660D3257380841EE, 0x59AC2C7873F910A3, 0xE846963877671A17,
|
|
0x93B633ABFA3469F8, 0xC0C0F5A60EF4CDCF, 0xCAF21ECD4377B28C,
|
|
0x57277707199B8175, 0x506C11B9D90E8B1D, 0xD83CC2687A19255F,
|
|
0x4A29C6465A314CD1, 0xED2DF21216235097, 0xB5635C95FF7296E2,
|
|
0x22AF003AB672E811, 0x52E762596BF68235, 0x9AEBA33AC6ECC6B0,
|
|
0x944F6DE09134DFB6, 0x6C47BEC883A7DE39, 0x6AD047C430A12104,
|
|
0xA5B1CFDBA0AB4067, 0x7C45D833AFF07862, 0x5092EF950A16DA0B,
|
|
0x9338E69C052B8E7B, 0x455A4B4CFE30E3F5, 0x6B02E63195AD0CF8,
|
|
0x6B17B224BAD6BF27, 0xD1E0CCD25BB9C169, 0xDE0C89A556B9AE70,
|
|
0x50065E535A213CF6, 0x9C1169FA2777B874, 0x78EDEFD694AF1EED,
|
|
0x6DC93D9526A50E68, 0xEE97F453F06791ED, 0x32AB0EDB696703D3,
|
|
0x3A6853C7E70757A7, 0x31865CED6120F37D, 0x67FEF95D92607890,
|
|
0x1F2B1D1F15F6DC9C, 0xB69E38A8965C6B65, 0xAA9119FF184CCCF4,
|
|
0xF43C732873F24C13, 0xFB4A3D794A9A80D2, 0x3550C2321FD6109C,
|
|
0x371F77E76BB8417E, 0x6BFA9AAE5EC05779, 0xCD04F3FF001A4778,
|
|
0xE3273522064480CA, 0x9F91508BFFCFC14A, 0x049A7F41061A9E60,
|
|
0xFCB6BE43A9F2FE9B, 0x08DE8A1C7797DA9B, 0x8F9887E6078735A1,
|
|
0xB5B4071DBFC73A66, 0x230E343DFBA08D33, 0x43ED7F5A0FAE657D,
|
|
0x3A88A0FBBCB05C63, 0x21874B8B4D2DBC4F, 0x1BDEA12E35F6A8C9,
|
|
0x53C065C6C8E63528, 0xE34A1D250E7A8D6B, 0xD6B04D3B7651DD7E,
|
|
0x5E90277E7CB39E2D, 0x2C046F22062DC67D, 0xB10BB459132D0A26,
|
|
0x3FA9DDFB67E2F199, 0x0E09B88E1914F7AF, 0x10E8B35AF3EEAB37,
|
|
0x9EEDECA8E272B933, 0xD4C718BC4AE8AE5F, 0x81536D601170FC20,
|
|
0x91B534F885818A06, 0xEC8177F83F900978, 0x190E714FADA5156E,
|
|
0xB592BF39B0364963, 0x89C350C893AE7DC1, 0xAC042E70F8B383F2,
|
|
0xB49B52E587A1EE60, 0xFB152FE3FF26DA89, 0x3E666E6F69AE2C15,
|
|
0x3B544EBE544C19F9, 0xE805A1E290CF2456, 0x24B33C9D7ED25117,
|
|
0xE74733427B72F0C1, 0x0A804D18B7097475, 0x57E3306D881EDB4F,
|
|
0x4AE7D6A36EB5DBCB, 0x2D8D5432157064C8, 0xD1E649DE1E7F268B,
|
|
0x8A328A1CEDFE552C, 0x07A3AEC79624C7DA, 0x84547DDC3E203C94,
|
|
0x990A98FD5071D263, 0x1A4FF12616EEFC89, 0xF6F7FD1431714200,
|
|
0x30C05B1BA332F41C, 0x8D2636B81555A786, 0x46C9FEB55D120902,
|
|
0xCCEC0A73B49C9921, 0x4E9D2827355FC492, 0x19EBB029435DCB0F,
|
|
0x4659D2B743848A2C, 0x963EF2C96B33BE31, 0x74F85198B05A2E7D,
|
|
0x5A0F544DD2B1FB18, 0x03727073C2E134B1, 0xC7F6AA2DE59AEA61,
|
|
0x352787BAA0D7C22F, 0x9853EAB63B5E0B35, 0xABBDCDD7ED5C0860,
|
|
0xCF05DAF5AC8D77B0, 0x49CAD48CEBF4A71E, 0x7A4C10EC2158C4A6,
|
|
0xD9E92AA246BF719E, 0x13AE978D09FE5557, 0x730499AF921549FF,
|
|
0x4E4B705B92903BA4, 0xFF577222C14F0A3A, 0x55B6344CF97AAFAE,
|
|
0xB862225B055B6960, 0xCAC09AFBDDD2CDB4, 0xDAF8E9829FE96B5F,
|
|
0xB5FDFC5D3132C498, 0x310CB380DB6F7503, 0xE87FBB46217A360E,
|
|
0x2102AE466EBB1148, 0xF8549E1A3AA5E00D, 0x07A69AFDCC42261A,
|
|
0xC4C118BFE78FEAAE, 0xF9F4892ED96BD438, 0x1AF3DBE25D8F45DA,
|
|
0xF5B4B0B0D2DEEEB4, 0x962ACEEFA82E1C84, 0x046E3ECAAF453CE9,
|
|
0xF05D129681949A4C, 0x964781CE734B3C84, 0x9C2ED44081CE5FBD,
|
|
0x522E23F3925E319E, 0x177E00F9FC32F791, 0x2BC60A63A6F3B3F2,
|
|
0x222BBFAE61725606, 0x486289DDCC3D6780, 0x7DC7785B8EFDFC80,
|
|
0x8AF38731C02BA980, 0x1FAB64EA29A2DDF7, 0xE4D9429322CD065A,
|
|
0x9DA058C67844F20C, 0x24C0E332B70019B0, 0x233003B5A6CFE6AD,
|
|
0xD586BD01C5C217F6, 0x5E5637885F29BC2B, 0x7EBA726D8C94094B,
|
|
0x0A56A5F0BFE39272, 0xD79476A84EE20D06, 0x9E4C1269BAA4BF37,
|
|
0x17EFEE45B0DEE640, 0x1D95B0A5FCF90BC6, 0x93CBE0B699C2585D,
|
|
0x65FA4F227A2B6D79, 0xD5F9E858292504D5, 0xC2B5A03F71471A6F,
|
|
0x59300222B4561E00, 0xCE2F8642CA0712DC, 0x7CA9723FBB2E8988,
|
|
0x2785338347F2BA08, 0xC61BB3A141E50E8C, 0x150F361DAB9DEC26,
|
|
0x9F6A419D382595F4, 0x64A53DC924FE7AC9, 0x142DE49FFF7A7C3D,
|
|
0x0C335248857FA9E7, 0x0A9C32D5EAE45305, 0xE6C42178C4BBB92E,
|
|
0x71F1CE2490D20B07, 0xF1BCC3D275AFE51A, 0xE728E8C83C334074,
|
|
0x96FBF83A12884624, 0x81A1549FD6573DA5, 0x5FA7867CAF35E149,
|
|
0x56986E2EF3ED091B, 0x917F1DD5F8886C61, 0xD20D8C88C8FFE65F,
|
|
0x31D71DCE64B2C310, 0xF165B587DF898190, 0xA57E6339DD2CF3A0,
|
|
0x1EF6E6DBB1961EC9, 0x70CC73D90BC26E24, 0xE21A6B35DF0C3AD7,
|
|
0x003A93D8B2806962, 0x1C99DED33CB890A1, 0xCF3145DE0ADD4289,
|
|
0xD0E4427A5514FB72, 0x77C621CC9FB3A483, 0x67A34DAC4356550B,
|
|
0xF8D626AAAF278509)
|
|
|
|
def __init__(self, random_array):
|
|
self.random_array = random_array
|
|
|
|
@property
|
|
def random_array(self):
|
|
"""The tuple of 781 random 64 bit integers used by the hasher."""
|
|
return self.__random_array
|
|
|
|
@random_array.setter
|
|
def random_array(self, value):
|
|
if len(value) < 781:
|
|
raise ValueError(
|
|
"Expected at least 781 random numbers, got: %d." % len(value))
|
|
|
|
self.__random_array = value
|
|
|
|
def hash_position(self, position):
|
|
"""Computes the Zobrist hash of a position.
|
|
|
|
:param position:
|
|
The position to hash.
|
|
|
|
:return:
|
|
The hash as an integer.
|
|
"""
|
|
key = 0
|
|
|
|
# Hash in the board setup.
|
|
for x in range(0, 8):
|
|
for y in range(0, 8):
|
|
piece = position[Square.from_x_and_y(x, y)]
|
|
if piece:
|
|
i = "pPnNbBrRqQkK".index(piece.symbol)
|
|
key ^= self.__random_array[64 * i + 8 * y + x]
|
|
|
|
# Hash in the castling flags.
|
|
if position.get_castling_right("K"):
|
|
key ^= self.__random_array[768]
|
|
if position.get_castling_right("Q"):
|
|
key ^= self.__random_array[768 + 1]
|
|
if position.get_castling_right("k"):
|
|
key ^= self.__random_array[768 + 2]
|
|
if position.get_castling_right("q"):
|
|
key ^= self.__random_array[768 + 3]
|
|
|
|
# Hash in the en-passant file.
|
|
if (position.ep_file and
|
|
position.get_theoretical_ep_right(position.ep_file)):
|
|
i = ord(position.ep_file) - ord("a")
|
|
key ^= self.__random_array[772 + i]
|
|
|
|
# Hash in the turn.
|
|
if position.turn == "w":
|
|
key ^= self.__random_array[780]
|
|
|
|
return key
|
|
|
|
@classmethod
|
|
def create_random(cls):
|
|
"""Generates a new random number array using the random module
|
|
and creates a new ZobristHasher from it.
|
|
|
|
Its up to the caller to seed the random number generator (or not).
|
|
"""
|
|
return cls(tuple(random.randint(0, 2**64) for i in range(0, 781)))
|
|
|
|
|
|
class GameHeaderBag(collections.MutableMapping):
|
|
"""A glorified dictionary of game headers as used in PGNs.
|
|
|
|
:param game:
|
|
Defaults to `None`. If bound to a game, any value set for
|
|
`"PlyCount"` is ignored and instead the real ply count of the
|
|
game is the value.
|
|
Aditionally the `"FEN"` header can not be modified if the game
|
|
already contains a move.
|
|
|
|
These headers are required by the PGN standard and can not be
|
|
removed:
|
|
|
|
`"Event`":
|
|
The name of the tournament or match event. Default is `"?"`.
|
|
`"Site`":
|
|
The location of the event. Default is `"?"`.
|
|
`"Date`":
|
|
The starting date of the game. Defaults to `"????.??.??"`. Must
|
|
be a valid date of the form YYYY.MM.DD. `"??"` can be used as a
|
|
placeholder for unknown month or day. `"????"` can be used as a
|
|
placeholder for an unknown year.
|
|
`"Round`":
|
|
The playing round ordinal of the game within the event. Must be
|
|
digits only or `"?"`. Defaults to `"?"`.
|
|
`"White`":
|
|
The player of the white pieces. Defaults to `"?"`.
|
|
`"Black`":
|
|
The player of the black pieces. Defaults to `"?"`.
|
|
`"Result`":
|
|
Defaults to `"*"`. Other values are `"1-0"` (white won),
|
|
`"0-1"` (black won) and `"1/2-1/2"` (drawn).
|
|
|
|
These additional headers are known:
|
|
|
|
`"Annotator"`:
|
|
The person providing notes to the game.
|
|
`"PlyCount"`:
|
|
The total moves played. Must be digits only. If a `game`
|
|
parameter is given any value set will be ignored and the real
|
|
ply count of the game will be used as the value.
|
|
`"TimeControl"`:
|
|
For example `"40/7200:3600"` (moves per seconds : sudden death
|
|
seconds). Validated to be of this form.
|
|
`"Time"`:
|
|
Time the game started as a valid HH:MM:SS string.
|
|
`"Termination"`:
|
|
Can be one of `"abandoned"`, `"adjudication"`, `"death"`,
|
|
`"emergency"`, `"normal"`, `"rules infraction"`,
|
|
`"time forfeit"` or `"unterminated"`.
|
|
`"Mode"`:
|
|
Can be `"OTB"` (over-the-board) or `"ICS"` (Internet chess
|
|
server).
|
|
`"FEN"`:
|
|
The initial position if the board as a FEN. If a game parameter
|
|
was given and the game already contains moves, this header can
|
|
not be set. The header will be deleted when set to the standard
|
|
chess start FEN.
|
|
`"SetUp"`:
|
|
Any value set is ignored. Instead the value is `"1"` is the
|
|
`"FEN"` header is set. Otherwise this header does not exist.
|
|
|
|
An arbitrary amount of other headers can be set. The capitalization
|
|
of the first occurence of a new header is used to normalize all
|
|
further occurences to it. Additional headers are not validated.
|
|
|
|
>>> import chess
|
|
>>> bag = chess.GameHeaderBag()
|
|
>>> bag["Annotator"] = "Alekhine"
|
|
>>> bag["annOTator"]
|
|
'Alekhine'
|
|
>>> del bag["Annotator"]
|
|
>>> "Annotator" in bag
|
|
False
|
|
|
|
`KNOWN_HEADERS`:
|
|
The known headers in the order they will appear (if set) when
|
|
iterating over the keys.
|
|
"""
|
|
|
|
__date_regex = re.compile(r"^(\?{4}|[0-9]{4})(\.(\?\?|[0-9]{2})\.(\?\?|[0-9]{2}))?$")
|
|
__round_part_regex = re.compile(r"^(\?|-|[0-9]+)$")
|
|
__time_control_regex = re.compile(r"^([0-9]+)\/([0-9]+):([0-9]+)$")
|
|
__time_regex = re.compile(r"^([0-9]{2}):([0-9]{2}):([0-9]{2})$")
|
|
|
|
KNOWN_HEADERS = [
|
|
"Event", "Site", "Date", "Round", "White", "Black", "Result",
|
|
"Annotator", "PlyCount", "TimeControl", "Time", "Termination", "Mode",
|
|
"FEN", "SetUp"]
|
|
|
|
def __init__(self, game=None):
|
|
self.__game = game
|
|
self.__headers = {
|
|
"Event": "?",
|
|
"Site": "?",
|
|
"Date": "????.??.??",
|
|
"Round": "?",
|
|
"White": "?",
|
|
"Black": "?",
|
|
"Result": "*",
|
|
}
|
|
|
|
def __normalize_key(self, key):
|
|
if not isinstance(key, str):
|
|
raise TypeError(
|
|
"Expected string for GameHeaderBag key, got: %s." % repr(key))
|
|
for header in itertools.chain(GameHeaderBag.KNOWN_HEADERS,
|
|
self.__headers):
|
|
if header.lower() == key.lower():
|
|
return header
|
|
return key
|
|
|
|
def __len__(self):
|
|
i = 0
|
|
for header in self:
|
|
i += 1
|
|
return i
|
|
|
|
def __iter__(self):
|
|
for known_header in GameHeaderBag.KNOWN_HEADERS:
|
|
if known_header in self:
|
|
yield known_header
|
|
for header in self.__headers:
|
|
if not header in GameHeaderBag.KNOWN_HEADERS:
|
|
yield header
|
|
|
|
def __getitem__(self, key):
|
|
key = self.__normalize_key(key)
|
|
if self.__game and key == "PlyCount":
|
|
return self.__game.ply
|
|
elif key == "SetUp":
|
|
return "1" if "FEN" in self else "0"
|
|
elif key in self.__headers:
|
|
return self.__headers[key]
|
|
else:
|
|
raise KeyError(key)
|
|
|
|
def __setitem__(self, key, value):
|
|
key = self.__normalize_key(key)
|
|
if not isinstance(value, str):
|
|
raise TypeError(
|
|
"Expected value to be a string, got: %s." % repr(value))
|
|
|
|
if key == "Date":
|
|
matches = GameHeaderBag.__date_regex.match(value)
|
|
if not matches:
|
|
raise ValueError(
|
|
"Invalid value for Date header: %s." % repr(value))
|
|
if not matches.group(2):
|
|
value = "%s.??.??" % matches.group(1)
|
|
else:
|
|
year = matches.group(1) if matches.group(1) != "????" else "2000"
|
|
month = int(matches.group(3)) if matches.group(3) != "??" else "10"
|
|
day = int(matches.group(4)) if matches.group(4) != "??" else "1"
|
|
datetime.date(int(year), int(month), int(day))
|
|
elif key == "Round":
|
|
parts = value.split(".")
|
|
for part in parts:
|
|
if not GameHeaderBag.__round_part_regex.match(part):
|
|
raise ValueError("Invalid value for Round header: %s." % repr(value))
|
|
elif key == "Result":
|
|
if not value in ["1-0", "0-1", "1/2-1/2", "*"]:
|
|
raise ValueError(
|
|
"Invalid value for Result header: %s." % repr(value))
|
|
elif key == "PlyCount":
|
|
if not value.isdigit():
|
|
raise ValueError(
|
|
"Invalid value for PlyCount header: %s." % repr(value))
|
|
else:
|
|
value = str(int(value))
|
|
elif key == "TimeControl":
|
|
pass
|
|
# TODO: Implement correct parsing.
|
|
#if not GameHeaderBag.__time_control_regex.match(value):
|
|
# raise ValueError(
|
|
# "Invalid value for TimeControl header: %s." % repr(value))
|
|
elif key == "Time":
|
|
matches = GameHeaderBag.__time_regex.match(value)
|
|
if (not matches or
|
|
int(matches.group(1)) < 0 or int(matches.group(1)) >= 24 or
|
|
int(matches.group(2)) < 0 or int(matches.group(2)) >= 60 or
|
|
int(matches.group(3)) < 0 or int(matches.group(3)) >= 60):
|
|
raise ValueError(
|
|
"Invalid value for Time header: %s." % repr(value))
|
|
elif key == "Termination":
|
|
value = value.lower()
|
|
|
|
# Support chess.com PGN files.
|
|
if value.endswith(" won by resignation"):
|
|
value = "normal"
|
|
elif value.endswith(" drawn by repetition"):
|
|
value = "normal"
|
|
elif value.endswith(" won by checkmate"):
|
|
value = "normal"
|
|
elif value.endswith(" game abandoned"):
|
|
value = "abandoned"
|
|
|
|
# Ensure value matches the PGN standard.
|
|
if not value in ["abandoned", "adjudication", "death", "emergency",
|
|
"normal", "rules infraction", "time forfeit",
|
|
"unterminated"]:
|
|
raise ValueError(
|
|
"Invalid value for Termination header: %s." % repr(value))
|
|
elif key == "Mode":
|
|
value = value.upper()
|
|
if not value in ["OTB", "ICS"]:
|
|
raise ValueError(
|
|
"Invalid value for Mode header: %s." % repr(value))
|
|
elif key == "FEN":
|
|
value = Position(value).fen
|
|
|
|
if value == START_FEN:
|
|
if not "FEN" in self:
|
|
return
|
|
else:
|
|
if "FEN" in self and self["FEN"] == value:
|
|
return
|
|
|
|
if self.__game and self.__game.ply > 0:
|
|
raise ValueError(
|
|
"FEN header can not be set, when there are already moves.")
|
|
|
|
if value == START_FEN:
|
|
del self["FEN"]
|
|
del self["SetUp"]
|
|
return
|
|
else:
|
|
self["SetUp"] = "1"
|
|
|
|
self.__headers[key] = value
|
|
|
|
def __delitem__(self, key):
|
|
k = self.__normalize_key(key)
|
|
if k in ["Event", "Site", "Date", "Round", "White", "Black", "Result"]:
|
|
raise KeyError(
|
|
"The %s key can not be deleted because it is required." % k)
|
|
del self.__headers[k]
|
|
|
|
def __contains__(self, key):
|
|
key = self.__normalize_key(key)
|
|
if self.__game and key == "PlyCount":
|
|
return True
|
|
elif key == "SetUp":
|
|
return "FEN" in self
|
|
else:
|
|
return self.__normalize_key(key) in self.__headers
|
|
|
|
|
|
class EcoFileParser(object):
|
|
|
|
__move_regex = re.compile(r"([0-9]+\.)?(.*)")
|
|
|
|
ECO_STATE = 0
|
|
NAME_STATE = 1
|
|
MOVETEXT_STATE = 2
|
|
|
|
def __init__(self, create_classification=True, create_lookup=True):
|
|
self.classification = dict() if create_classification else None
|
|
self.lookup = dict() if create_lookup else None
|
|
|
|
self.current_state = EcoFileParser.ECO_STATE
|
|
self.current_eco = None
|
|
self.current_name = None
|
|
self.current_position = None
|
|
|
|
self.chunks = collections.deque()
|
|
|
|
def tokenize(self, filename):
|
|
handle = open(filename, "r")
|
|
for line in handle:
|
|
line = line.strip().decode("latin-1")
|
|
if not line or line.startswith("#"):
|
|
continue
|
|
|
|
self.chunks += line.split()
|
|
|
|
def read_chunk(self):
|
|
chunk = self.chunks.popleft()
|
|
|
|
if self.classification is None and self.lookup is None:
|
|
return
|
|
|
|
if self.current_state == EcoFileParser.ECO_STATE:
|
|
self.current_eco = chunk[0:3]
|
|
self.current_name = ""
|
|
self.current_position = Position()
|
|
self.current_state = EcoFileParser.NAME_STATE
|
|
elif self.current_state == EcoFileParser.NAME_STATE:
|
|
if chunk.startswith("\""):
|
|
chunk = chunk[1:]
|
|
if chunk.endswith("\""):
|
|
chunk = chunk[:-1]
|
|
self.current_state = EcoFileParser.MOVETEXT_STATE
|
|
self.current_name = (self.current_name + " " + chunk).strip()
|
|
elif self.current_state == EcoFileParser.MOVETEXT_STATE:
|
|
if chunk == "*":
|
|
self.current_state = EcoFileParser.ECO_STATE
|
|
if not self.classification is None:
|
|
self.classification[hash(self.current_position)] = {
|
|
"eco": self.current_eco,
|
|
"name": self.current_name,
|
|
"fen": self.current_position.fen,
|
|
}
|
|
if not self.lookup is None and not self.current_eco in self.lookup:
|
|
self.lookup[self.current_eco] = {
|
|
"name": self.current_name,
|
|
"fen": self.current_position.fen,
|
|
}
|
|
else:
|
|
if self.classification is not None or not self.current_eco in self.lookup:
|
|
match = EcoFileParser.__move_regex.match(chunk)
|
|
if match.group(2):
|
|
self.current_position.make_move(self.current_position.get_move_from_san(match.group(2)))
|
|
|
|
def read_all(self):
|
|
while self.chunks:
|
|
self.read_chunk()
|
|
assert self.current_state == EcoFileParser.ECO_STATE
|
|
|
|
|
|
class Board(QWidget):
|
|
|
|
def __init__(self, parent):
|
|
super(Board, self).__init__()
|
|
self.margin = 0.1
|
|
self.padding = 0.06
|
|
self.showCoordinates = True
|
|
self.lightSquareColor = QColor(255, 255, 255)
|
|
self.darkSquareColor = QColor(100, 100, 255)
|
|
self.borderColor = QColor(100, 100, 200)
|
|
self.shadowWidth = 2
|
|
self.rotation = 0
|
|
self.ply = 1
|
|
self.setWindowTitle('Chess')
|
|
self.backgroundPixmap = QPixmap(plugin_super_class.path_to_data('chess') + "background.png")
|
|
|
|
self.draggedSquare = None
|
|
self.dragPosition = None
|
|
|
|
self.position = Position()
|
|
|
|
self.parent = parent
|
|
|
|
# Load piece set.
|
|
self.pieceRenderers = dict()
|
|
for symbol in "PNBRQKpnbrqk":
|
|
piece = Piece(symbol)
|
|
self.pieceRenderers[piece] = QSvgRenderer(plugin_super_class.path_to_data('chess') + "classic-pieces/%s-%s.svg" % (piece.full_color, piece.full_type))
|
|
|
|
def update_title(self):
|
|
if self.position.is_checkmate():
|
|
self.setWindowTitle('Checkmate')
|
|
elif self.position.is_stalemate():
|
|
self.setWindowTitle('Stalemate')
|
|
|
|
def mousePressEvent(self, e):
|
|
self.dragPosition = e.pos()
|
|
square = self.squareAt(e.pos())
|
|
if self.canDragSquare(square):
|
|
self.draggedSquare = square
|
|
|
|
def mouseMoveEvent(self, e):
|
|
if self.draggedSquare:
|
|
self.dragPosition = e.pos()
|
|
self.repaint()
|
|
|
|
def mouseReleaseEvent(self, e):
|
|
if self.draggedSquare:
|
|
dropSquare = self.squareAt(e.pos())
|
|
if dropSquare == self.draggedSquare:
|
|
self.onSquareClicked(self.draggedSquare)
|
|
elif dropSquare:
|
|
move = self.moveFromDragDrop(self.draggedSquare, dropSquare)
|
|
if move:
|
|
self.position.make_move(move)
|
|
self.parent.move(move)
|
|
self.ply += 1
|
|
self.draggedSquare = None
|
|
self.repaint()
|
|
|
|
def paintEvent(self, event):
|
|
painter = QPainter()
|
|
painter.begin(self)
|
|
|
|
# Light shines from upper left.
|
|
if math.cos(math.radians(self.rotation)) >= 0:
|
|
lightBorderColor = self.borderColor.lighter()
|
|
darkBorderColor = self.borderColor.darker()
|
|
else:
|
|
lightBorderColor = self.borderColor.darker()
|
|
darkBorderColor = self.borderColor.lighter()
|
|
|
|
# Draw the background.
|
|
backgroundBrush = QBrush(Qt.red, self.backgroundPixmap)
|
|
backgroundBrush.setStyle(Qt.TexturePattern)
|
|
painter.fillRect(QRect(QPoint(0, 0), self.size()), backgroundBrush)
|
|
|
|
# Do the rotation.
|
|
painter.save()
|
|
painter.translate(self.width() / 2, self.height() / 2)
|
|
painter.rotate(self.rotation)
|
|
|
|
# Draw the border.
|
|
frameSize = min(self.width(), self.height()) * (1 - self.margin * 2)
|
|
borderSize = min(self.width(), self.height()) * self.padding
|
|
painter.translate(-frameSize / 2, -frameSize / 2)
|
|
painter.fillRect(QRect(0, 0, frameSize, frameSize), self.borderColor)
|
|
painter.setPen(QPen(QBrush(lightBorderColor), self.shadowWidth))
|
|
painter.drawLine(0, 0, 0, frameSize)
|
|
painter.drawLine(0, 0, frameSize, 0)
|
|
painter.setPen(QPen(QBrush(darkBorderColor), self.shadowWidth))
|
|
painter.drawLine(frameSize, 0, frameSize, frameSize)
|
|
painter.drawLine(0, frameSize, frameSize, frameSize)
|
|
|
|
# Draw the squares.
|
|
painter.translate(borderSize, borderSize)
|
|
squareSize = (frameSize - 2 * borderSize) / 8.0
|
|
for x in range(0, 8):
|
|
for y in range(0, 8):
|
|
rect = QRect(x * squareSize, y * squareSize, squareSize, squareSize)
|
|
if (x - y) % 2 == 0:
|
|
painter.fillRect(rect, QBrush(self.lightSquareColor))
|
|
else:
|
|
painter.fillRect(rect, QBrush(self.darkSquareColor))
|
|
|
|
# Draw the inset.
|
|
painter.setPen(QPen(QBrush(darkBorderColor), self.shadowWidth))
|
|
painter.drawLine(0, 0, 0, squareSize * 8)
|
|
painter.drawLine(0, 0, squareSize * 8, 0)
|
|
painter.setPen(QPen(QBrush(lightBorderColor), self.shadowWidth))
|
|
painter.drawLine(squareSize * 8, 0, squareSize * 8, squareSize * 8)
|
|
painter.drawLine(0, squareSize * 8, squareSize * 8, squareSize * 8)
|
|
|
|
# Display coordinates.
|
|
if self.showCoordinates:
|
|
painter.setPen(QPen(QBrush(self.borderColor.lighter()), self.shadowWidth))
|
|
coordinateSize = min(borderSize, squareSize)
|
|
font = QFont()
|
|
font.setPixelSize(coordinateSize * 0.6)
|
|
painter.setFont(font)
|
|
for i, rank in enumerate(["8", "7", "6", "5", "4", "3", "2", "1"]):
|
|
pos = QRect(-borderSize, squareSize * i, borderSize, squareSize).center()
|
|
painter.save()
|
|
painter.translate(pos.x(), pos.y())
|
|
painter.rotate(-self.rotation)
|
|
painter.drawText(QRect(-coordinateSize / 2, -coordinateSize / 2, coordinateSize, coordinateSize), Qt.AlignCenter, rank)
|
|
painter.restore()
|
|
for i, file in enumerate(["a", "b", "c", "d", "e", "f", "g", "h"]):
|
|
pos = QRect(squareSize * i, squareSize * 8, squareSize, borderSize).center()
|
|
painter.save()
|
|
painter.translate(pos.x(), pos.y())
|
|
painter.rotate(-self.rotation)
|
|
painter.drawText(QRect(-coordinateSize / 2, -coordinateSize / 2, coordinateSize, coordinateSize), Qt.AlignCenter, file)
|
|
painter.restore()
|
|
|
|
# Draw pieces.
|
|
for x in range(0, 8):
|
|
for y in range(0, 8):
|
|
square = Square.from_x_and_y(x, 7 - y)
|
|
piece = self.position[square]
|
|
if piece and square != self.draggedSquare:
|
|
painter.save()
|
|
painter.translate((x + 0.5) * squareSize, (y + 0.5) * squareSize)
|
|
painter.rotate(-self.rotation)
|
|
self.pieceRenderers[piece].render(painter, QRect(-squareSize / 2, -squareSize / 2, squareSize, squareSize))
|
|
painter.restore()
|
|
|
|
# Draw a floating piece.
|
|
painter.restore()
|
|
if self.draggedSquare:
|
|
piece = self.position[self.draggedSquare]
|
|
if piece:
|
|
painter.save()
|
|
painter.translate(self.dragPosition.x(), self.dragPosition.y())
|
|
painter.rotate(-self.rotation)
|
|
self.pieceRenderers[piece].render(painter, QRect(-squareSize / 2, -squareSize / 2, squareSize, squareSize))
|
|
painter.restore()
|
|
|
|
painter.end()
|
|
|
|
def squareAt(self, point):
|
|
# Undo the rotation.
|
|
transform = QTransform()
|
|
transform.translate(self.width() / 2, self.height() / 2)
|
|
transform.rotate(self.rotation)
|
|
logicalPoint = transform.inverted()[0].map(point)
|
|
|
|
frameSize = min(self.width(), self.height()) * (1 - self.margin * 2)
|
|
borderSize = min(self.width(), self.height()) * self.padding
|
|
squareSize = (frameSize - 2 * borderSize) / 8.0
|
|
x = int(logicalPoint.x() / squareSize + 4)
|
|
y = 7 - int(logicalPoint.y() / squareSize + 4)
|
|
try:
|
|
return Square.from_x_and_y(x, y)
|
|
except IndexError:
|
|
return None
|
|
|
|
def canDragSquare(self, square):
|
|
if (self.ply % 2 == 0 and self.parent.white) or (self.ply % 2 == 1 and not self.parent.white):
|
|
return False
|
|
for move in self.position.get_legal_moves():
|
|
if move.source == square:
|
|
return True
|
|
return False
|
|
|
|
def onSquareClicked(self, square):
|
|
pass
|
|
|
|
def moveFromDragDrop(self, source, target):
|
|
for move in self.position.get_legal_moves():
|
|
if move.source == source and move.target == target:
|
|
if move.promotion:
|
|
dialog = PromotionDialog(self.position[move.source].color, self)
|
|
if dialog.exec_():
|
|
return Move(source, target, dialog.selectedType())
|
|
else:
|
|
return move
|
|
return move
|
|
|
|
|
|
class PromotionDialog(QDialog):
|
|
|
|
def __init__(self, color, parent=None):
|
|
super(PromotionDialog, self).__init__(parent)
|
|
|
|
self.promotionTypes = ["q", "n", "r", "b"]
|
|
|
|
grid = QGridLayout()
|
|
hbox = QHBoxLayout()
|
|
grid.addLayout(hbox, 0, 0)
|
|
|
|
# Add the piece buttons.
|
|
self.buttonGroup = QButtonGroup(self)
|
|
for i, promotionType in enumerate(self.promotionTypes):
|
|
# Create an icon for the piece.
|
|
piece = Piece.from_color_and_type(color, promotionType)
|
|
renderer = QSvgRenderer(plugin_super_class.path_to_data('chess') + "classic-pieces/%s-%s.svg" % (piece.full_color, piece.full_type))
|
|
pixmap = QPixmap(32, 32)
|
|
pixmap.fill(Qt.transparent)
|
|
painter = QPainter()
|
|
painter.begin(pixmap)
|
|
renderer.render(painter, QRect(0, 0, 32, 32))
|
|
painter.end()
|
|
|
|
# Add the button.
|
|
button = QPushButton(QIcon(pixmap), '', self)
|
|
button.setCheckable(True)
|
|
self.buttonGroup.addButton(button, i)
|
|
hbox.addWidget(button)
|
|
|
|
self.buttonGroup.button(0).setChecked(True)
|
|
|
|
# Add the ok and cancel buttons.
|
|
buttons = QDialogButtonBox(QDialogButtonBox.Cancel | QDialogButtonBox.Ok)
|
|
buttons.rejected.connect(self.reject)
|
|
buttons.accepted.connect(self.accept)
|
|
grid.addWidget(buttons, 1, 0)
|
|
|
|
self.setLayout(grid)
|
|
self.setWindowFlags(self.windowFlags() & ~Qt.WindowContextHelpButtonHint)
|
|
|
|
def selectedType(self):
|
|
return self.promotionTypes[self.buttonGroup.checkedId()]
|
|
|
|
|
|
class Chess(plugin_super_class.PluginSuperClass):
|
|
|
|
def __init__(self, *args):
|
|
super(Chess, self).__init__('Chess', 'chess', *args)
|
|
self.game = -1
|
|
self.board = None
|
|
self.white = True
|
|
self.pre = None
|
|
|
|
def get_description(self):
|
|
return QApplication.translate("Chess", 'Plugin which allows you to play chess with your friends.', None, QApplication.UnicodeUTF8)
|
|
|
|
def lossless_packet(self, data, friend_number):
|
|
print('data ' + data)
|
|
if data == 'new':
|
|
self.pre = None
|
|
reply = QMessageBox.question(None,
|
|
'New game',
|
|
'New game',
|
|
QMessageBox.Yes,
|
|
QMessageBox.No)
|
|
if reply != QMessageBox.Yes:
|
|
self.send_lossless('no', friend_number)
|
|
else:
|
|
self.send_lossless('yes', friend_number)
|
|
self.board = Board(self)
|
|
self.board.show()
|
|
self.game = friend_number
|
|
self.white = False
|
|
elif data == 'yes' and friend_number == self.game:
|
|
self.board = Board(self)
|
|
self.board.show()
|
|
elif data == 'no':
|
|
self.game = -1
|
|
else: # move
|
|
if data != self.pre:
|
|
self.pre = data
|
|
a = Square.from_x_and_y(ord(data[0]) - ord('a'), ord(data[1]) - ord('1'))
|
|
b = Square.from_x_and_y(ord(data[2]) - ord('a'), ord(data[3]) - ord('1'))
|
|
self.board.position.make_move(Move(a, b, data[4] if len(data) == 5 else None))
|
|
self.board.update_title()
|
|
self.board.ply += 1
|
|
|
|
def start_game(self, num):
|
|
self.white = True
|
|
self.send_lossless('new', num)
|
|
self.game = num
|
|
|
|
def move(self, move):
|
|
self.send_lossless(str(move), self.game)
|
|
self.board.update_title()
|
|
|
|
def get_menu(self, menu, num):
|
|
act = QAction(QApplication.translate("TOXID", "Start game", None, QApplication.UnicodeUTF8), menu)
|
|
act.connect(act, SIGNAL("triggered()"), lambda: self.start_game(num))
|
|
return [act]
|