from logic.move import CastleSide, Move from logic.pieces.bishop import Bishop from logic.pieces.king import King from logic.pieces.knight import Knight from logic.pieces.queen import Queen from logic.pieces.rook import Rook from logic.pieces.pawn import Pawn from logic.pieces.piece import Colour, Piece from logic.position import Position from typing import Type class Board: def __init__(self) -> None: self._white: dict[Position, Piece] = {} self._black: dict[Position, Piece] = {} self._turn = None self._white_castling_rights = set() self._black_castling_rights = set() self._en_passant_target = None @staticmethod def _piece_class_from_char(c: str) -> Type[Piece]: assert len(c) == 1, f"The piece {c} isn't denoted by 1 character" c = c.lower() if c == "p": return Pawn if c == "r": return Rook if c == "n": return Knight if c == "b": return Bishop if c == "q": return Queen if c == "k": return King raise ValueError(f"Unknown piece '{c}'") @staticmethod def setup_FEN_position(position: str) -> "Board": ret = Board() index = 0 # -- Pieces pieces = "prnbqk" # possible pieces numbers = "12345678" # possible number of empty squares x = 0 y = 7 # FEN starts from the top left, so 8th rank for c in position: index += 1 if c == " ": break if c in pieces or c in pieces.upper(): pos = Position(x, y) piece = Board._piece_class_from_char(c) if c.isupper(): ret._white[pos] = piece(pos, Colour.WHITE) else: ret._black[pos] = piece(pos, Colour.BLACK) x += 1 continue if c in numbers: x += int(c) if c == '/': x = 0 y -= 1 # -- Active colour if position[index] == "w": ret._turn = Colour.WHITE elif position[index] == "b": ret._turn = Colour.BLACK else: raise ValueError(f"The FEN position is malformed, the active colour should be either 'w' or 'b', but is '{position[index]}'") index += 2 # -- Castling Rights for c in position[index:]: index += 1 if c == "-" or c == " ": break sides = "kq" assert c in sides or c in sides.upper(), f"The FEN position is malformed, the castling rights should be either k or q (both either lower- or upper-case), instead is '{c}'" if c == "K": ret._white_castling_rights.add(CastleSide.King) if c == "Q": ret._white_castling_rights.add(CastleSide.Queen) if c == "k": ret._black_castling_rights.add(CastleSide.King) if c == "q": ret._black_castling_rights.add(CastleSide.Queen) # -- En passant target if position[index] != "-": ret._en_passant_target = position[index:index+2] return ret def piece_at(self, x: int, y: int) -> Piece | None: pos = Position(x, y) white_piece = self._white.get(pos, None) black_piece = self._black.get(pos, None) assert white_piece == None or black_piece == None, f"There are two pieces at the same position {pos}, this shouldn't happen!" if white_piece != None: return white_piece return black_piece def is_check_for(self, colour: Colour) -> bool: """ Is it check for the defending colour passed as parameter """ defending_pieces, attacking_pieces = (self._white, self._black) if colour == Colour.WHITE else (self._black, self._white) kings = [piece for piece in defending_pieces.values() if type(piece) == King] assert len(kings) == 1, f"We have more than one king for {colour}, that is no buono..." king = kings[0] for piece in attacking_pieces.values(): possible_pos = [] if type(piece) == King: # special case for the king, because it creates infinite recursion (since he looks if he's walking into a check) for dx in [-1, 0, 1]: for dy in [-1, 0, 1]: x, y = piece.pos.x + dx, piece.pos.y + dy if Position.is_within_bounds(x, y): possible_pos.append(Position(x, y)) else: possible_pos += [move.pos for move in piece.legal_moves(self, looking_for_check=True)] if king.pos in possible_pos: return True return False def is_checkmate_for(self, colour: Colour) -> bool: """ Is it checkmate for the defending colour passed as parameter """ return self.is_check_for(colour) and self._no_legal_moves_for(colour) def is_stalemate_for(self, colour: Colour) -> bool: """ Is it stalemate for the defending colour passed as parameter """ return not self.is_check_for(colour) and self._no_legal_moves_for(colour) def _no_legal_moves_for(self, colour: Colour) -> bool: """ Return true if there are indeed no legal moves for the given colour (for checkmate or stalemate)""" pieces = self._white if colour == Colour.WHITE else self._black for piece in pieces.values(): if len(piece.legal_moves(self)) > 0: return False return True def castling_rights_for(self, colour: Colour) -> set[CastleSide]: return self._white_castling_rights if colour == Colour.WHITE else self._black_castling_rights def make_move(self, move: Move) -> "Board": dest_piece = self.piece_at(move.pos.x, move.pos.y) if dest_piece: assert dest_piece.colour != move.piece.colour, "A piece cannot cannot eat another piece of the same colour" # -- Copy current state ret = Board() ret._white = self._white.copy() ret._black = self._black.copy() ret._turn = Colour.WHITE if self._turn == Colour.BLACK else Colour.BLACK ret._white_castling_rights = self._white_castling_rights.copy() ret._black_castling_rights = self._black_castling_rights.copy() piece = move.piece # -- Set en passant target if needed ret._en_passant_target = self._en_passant_target # -- Actually make the move pieces_moving, other_pieces = (ret._white, ret._black) if piece.colour == Colour.WHITE else (ret._black, ret._white) del pieces_moving[piece.pos] pieces_moving[move.pos] = piece.move_to(move.pos) if move.pos in other_pieces: del other_pieces[move.pos] # -- Handle castling (just move the rook over) if move.castle_side == CastleSide.King: rook_pos = Position(7, piece.pos.y) assert rook_pos in pieces_moving and type(pieces_moving[rook_pos]) == Rook, "Either rook is absent from the king side or you are trying to castle with something else than a rook..." del pieces_moving[rook_pos] new_rook_pos = Position(5, piece.pos.y) pieces_moving[new_rook_pos] = Rook(new_rook_pos, piece.colour) elif move.castle_side == CastleSide.Queen: rook_pos = Position(0, piece.pos.y) assert rook_pos in pieces_moving and type(pieces_moving[rook_pos]) == Rook, "Either rook is absent from the queen side or you are trying to castle with something else than a rook..." del pieces_moving[rook_pos] new_rook_pos = Position(3, piece.pos.y) pieces_moving[new_rook_pos] = Rook(new_rook_pos, piece.colour) # -- Check for castling rights if piece.colour == Colour.WHITE: if type(piece) == King: ret._white_castling_rights = set() if type(piece) == Rook: if piece.pos.x == 0 and CastleSide.Queen in ret._white_castling_rights: ret._white_castling_rights.remove(CastleSide.Queen) elif piece.pos.x == 7 and CastleSide.King in ret._white_castling_rights: ret._white_castling_rights.remove(CastleSide.King) else: if type(piece) == King: ret._black_castling_rights = set() if type(piece) == Rook: if piece.pos.x == 0 and CastleSide.Queen in ret._black_castling_rights: ret._black_castling_rights.remove(CastleSide.Queen) elif piece.pos.x == 7 and CastleSide.King in ret._black_castling_rights: ret._black_castling_rights.remove(CastleSide.King) return ret