toxygen_plugins/Chess/chess.py

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]