# -*- coding: utf-8 -*- import collections import re import math import plugin_super_class from PyQt5.QtCore import * from PyQt5.QtWidgets import * from PyQt5.QtGui import * from PyQt5.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): __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. """ 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): 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. """ __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 `_ 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 `_ 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. """ __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 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, my_move=False): if self.position.is_checkmate(): self.setWindowTitle('Checkmate') elif self.position.is_stalemate(): self.setWindowTitle('Stalemate') else: self.setWindowTitle('Chess' + (' [Your move]' if my_move else '')) 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 closeEvent(self, *args): self.parent.stop_game() 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 self.last_move = None self.is_my_move = False def get_description(self): return QApplication.translate("Chess", 'Plugin which allows you to play chess with your friends.') def lossless_packet(self, data, friend_number): if data == 'new': self.pre = None friend = self._profile.get_friend_by_number(friend_number) reply = QMessageBox.question(None, 'New chess game', 'Friend {} wants to play chess game against you. Start?'.format(friend.name), 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 self.is_my_move = False elif data == 'yes' and friend_number == self.game: self.board = Board(self) self.board.show() self.board.update_title(True) self.is_my_move = True self.last_move = None elif data == 'no': self.game = -1 elif data != self.pre: # move self.pre = data self.is_my_move = True self.last_move = None 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.repaint() self.board.update_title(True) self.board.ply += 1 def start_game(self, num): self.white = True self.send_lossless('new', num) self.game = num def resend_move(self): if self.is_my_move or self.last_move is None: return self.send_lossless(str(self.last_move), self.game) QTimer.singleShot(1000, self.resend_move) def stop_game(self): self.last_move = None def move(self, move): self.is_my_move = False self.last_move = move self.send_lossless(str(move), self.game) self.board.update_title() QTimer.singleShot(1000, self.resend_move) def get_menu(self, menu, num): act = QAction(QApplication.translate("Chess", "Start chess game"), menu) act.triggered.connect(lambda: self.start_game(num)) return [act]