def can_promote_pawn(self, start_position, end_position): """ Test if pawn promotion is possible for the provided position. :param start_position: string Algebraic notation position. :param end_position: string Algebraic notation position. :return: """ try: ChessHelper.validate_position(start_position) ChessHelper.validate_position(end_position) except InvalidPositionError as e: # Maybe log error here # Reraise exception either way raise else: if self._board[start_position] is None: return False piece = self._board[start_position] if piece.type != Type.PAWN: return False _, start_row = self._board.position_to_row_and_column( start_position, piece.color) _, end_row = self._board.position_to_row_and_column( end_position, piece.color) if start_row == self._board.get_dimension( ) - 2 and end_row == self._board.get_dimension() - 1: return True return False
def _get_nearest_piece_in_direction(self, start_position, move_direction, piece_color, ghost_pieces=None): """ Get the nearest piece from the starting position heading in the direction specified. Not expecting MoveDirection.l_shape as a direction. :param start_position: string Position to start searching from :param move_direction: int Direction to search in. Ex MoveDirection.forward :param piece_color: Color Color of player who's perspective should be used. :param ghost_pieces: dict Positions to either ignore or contain ghost pieces. :return: dict [position] [color] [offset] [type] """ ChessHelper.validate_position(start_position) offset = 0 positions = self._get_possible_positions(start_position, move_direction, piece_color) for position in positions: offset += 1 piece_on_destination = self.is_position_occupied(position) ghost_pieces = {} if not ghost_pieces else ghost_pieces if position in ghost_pieces and not ghost_pieces[position]: continue elif position in ghost_pieces: return { 'position': position, 'color': ghost_pieces[position]['color'], 'offset': offset, 'type': ghost_pieces[position]['type'] } elif piece_on_destination: piece = self[position] return { 'position': position, 'color': piece.color, 'offset': offset, 'type': piece.type } return None
def is_position_occupied(self, position): """ Check if a position is occupied with a piece. :param position: string Algebraic notation position. :return: bool True if piece on position, False otherwise """ ChessHelper.validate_position(position) return self._pieces[position] is not None
def _position_to_index(self, position, piece_color): """ Convert an algebraic notation position to an index. :param position: string Algebraic notation for a position. :param piece_color: Color Color of player who's perspective should be used. :return: int Index for that position. """ ChessHelper.validate_position(position) return self._indexes[piece_color][position]
def _remove_piece(self, position): """ Remove a piece from the board. :param position: string Algebraic notation for a position. :return: :raises: InvalidPositionError If position is not valid, exception will be raised. """ ChessHelper.validate_position(position) # Color does not matter since both indexes point to the same board index = self._indexes[Color.WHITE][position] self._pieces[self._board_positions[index]] = None
def __setitem__(self, position, piece): """ Put a piece on the board at the specified position. :param position: string Algebraic notation for a position. :param piece: Piece Piece object. :return: """ ChessHelper.validate_position(position) self._pieces[position] = copy.deepcopy(piece) if piece.type == Type.KING: self._king_positions[piece.color] = position
def get_legal_moves(self, position): """ Retrieve possible legal moves for a piece on a position. :param position: string Algebraic notation position. :return: """ try: ChessHelper.validate_position(position) except InvalidPositionError as e: # Maybe log error here # Reraise exception either way raise else: return self._board.get_legal_moves(position)
def _get_possible_positions(self, start_position, move_direction, piece_color): """ Get a list of all possible positions in the direction provided as if the board were empty. Positions returned are in algebraic notation. :param start_position: string Algebraic notation position. :param move_direction: int Direction to move. Ex MoveDirection.forward :param piece_color: Color Color of player who's perspective should be used. :return: list List of positions in algebraic notation. Positions in the list are sorted by the offset from the start position. Index 0 is the nearest position in the specified direction. """ ChessHelper.validate_position(start_position) possible_positions = [] current_index = next_index = self._position_to_index( start_position, piece_color) current_column, current_row = self.position_to_row_and_column( self._index_to_position(current_index, piece_color), piece_color) if move_direction == MoveDirection.L_SHAPE: for shift in self.PIECE_SHIFTING[MoveDirection.L_SHAPE]: try: next_index = current_index + shift self._index_to_position(next_index, piece_color) except InvalidIndexError: continue else: next_column, next_row = self.position_to_row_and_column( self._index_to_position(next_index, piece_color), piece_color) column_diff = abs(next_column - current_column) if current_row != next_row and column_diff <= 2: possible_positions.append( self._index_to_position(next_index, piece_color)) else: num_spaces = self._get_direction_square_count( start_position, move_direction, piece_color) for count in range(0, num_spaces): next_index += self.PIECE_SHIFTING[move_direction] possible_positions.append( self._index_to_position(next_index, piece_color)) return possible_positions
def _get_direction_square_count(self, start_position, move_direction, piece_color): """ Retrieve the number of squares from the starting position to the edge of the board in the direction specified. :param start_position: string Algebraic notation position. :param move_direction: int Direction to work in. Ex MoveDirection.forward :param piece_color: Color Color of player who's perspective should be used. :return: int Number of squares till the edge of the board. """ ChessHelper.validate_position(start_position) board_length = self.get_dimension() max_movement = board_length - 1 num_spaces = max_movement current_x_coord, current_y_coord = self.position_to_row_and_column( start_position, piece_color) if move_direction == MoveDirection.FORWARD: max_movement = board_length - current_y_coord - 1 elif move_direction == MoveDirection.F_RIGHT_DIAG: from_right = board_length - current_x_coord - 1 from_top = board_length - current_y_coord - 1 max_movement = min(from_top, from_right) elif move_direction == MoveDirection.RIGHT: max_movement = board_length - current_x_coord - 1 elif move_direction == MoveDirection.B_RIGHT_DIAG: from_right = board_length - current_x_coord - 1 max_movement = min(current_y_coord, from_right) elif move_direction == MoveDirection.BACKWARD: max_movement = current_y_coord elif move_direction == MoveDirection.B_LEFT_DIAG: max_movement = min(current_y_coord, current_x_coord) elif move_direction == MoveDirection.LEFT: max_movement = current_x_coord elif move_direction == MoveDirection.F_LEFT_DIAG: from_top = board_length - current_y_coord - 1 max_movement = min(from_top, current_x_coord) return min(num_spaces, max_movement)
def _get_position_shifted_by_offset(self, position, direction, offset, piece_color): """ Retrieve position after shifting over by offset in the direction specified. Ex position=a1, direction=MoveDirection.right, offset=1, piece_color=Color.white. Return value is a2 :param position: string Algebraic notation position. :param offset: int Number of times to shift over in direction :param piece_color: int Color.white or Colore.black :return: string Algebraic notation position. """ ChessHelper.validate_position(position) index = self._position_to_index(position, piece_color) shifted_index = index + self.PIECE_SHIFTING[direction] * offset return self._index_to_position(shifted_index, piece_color)
def promote_pawn(self, start_position, end_position, piece_type): """ Promote a pawn to another piece type. :param start_position: string Algebraic notation for pawn position. :param end_position: string Algebraic notation for destination position. :param piece_type: Type Value from Type enum :return: """ try: ChessHelper.validate_position(start_position) ChessHelper.validate_position(end_position) except InvalidPositionError as e: # Maybe log error here # Reraise exception either way raise else: if piece_type not in self.get_pawn_promote_types(): raise PieceTypeError( piece_type, 'Cannot promote pawn to supplied piece type') if self._board[start_position] is None: raise EmptyPositionError(start_position) # TODO confirm pawn on second to last row piece = self._board[start_position] piece_class = { Type.ROOK: Rook, Type.KNIGHT: Knight, Type.BISHOP: Bishop, Type.QUEEN: Queen } self._board[start_position] = piece_class[piece_type](piece.color) return self.move_piece(start_position, end_position)
def move_piece(json): if 'current_game' in session and 'game_room' in session: game = ChessGame.load_by_id(session['current_game']) try: ChessHelper.validate_position(json['start_position']) ChessHelper.validate_position(json['end_position']) except InvalidPositionError as e: data = {'error': str(e)} emit('error', data, room=session['game_room']) else: try: result = game.move_piece(json['start_position'], json['end_position']) except EmptyPositionError as e: data = {'error': str(e)} emit('error', data, room=session['game_room']) else: game.save_to_db() game_dict = game.to_dict() game_dict['result'] = result.to_dict() game_dict['board_string'] = str(game) emit('update_game', game_dict, room=session['game_room'])
def position_to_row_and_column(self, position, piece_color): """ Treat position at bottom left corner as the origin in x,y coordinate system. Return offset the passed in position is from the origin. :param position: string Algebraic notation for chess position :param piece_color: Color Color of player who's perspective should be used. :return: tuple The coordinates for the position. Ex (1,1) from white perspective is B2 but G7 from black perspective. """ ChessHelper.validate_position(position) if piece_color == Color.WHITE: column = 'abcdefgh'.index(position[0]) row = int(position[1]) - 1 else: column = 'hgfedcba'.index(position[0]) dimension = self.get_dimension() row = dimension - int(position[1]) return column, row
def _direction_and_position_offset(self, start_position, end_position, color): """ Determine the direction and number of positions away end_position is relative to start_position. :param start_position: string Position in algebraic notation. :param end_position: string Position in algebraic notation. :param color: int Dictionary where MoveDirection is the key and value is a tuple containing the number of rows and columns end_position is away. Treats start_position as 0,0 on x,y coordinate system. Ex a2 would be (1,1) from a1 :return: MoveDirection value """ ChessHelper.validate_position(start_position) ChessHelper.validate_position(end_position) start_column, start_row = self.position_to_row_and_column( start_position, color) end_column, end_row = self.position_to_row_and_column( end_position, color) row_diff = end_row - start_row column_diff = end_column - start_column # Compute offsets is_forward = row_diff > 0 is_back = row_diff < 0 is_right = column_diff > 0 is_left = column_diff < 0 # Determine direction if is_forward and is_right: positions_away = { 'direction': MoveDirection.F_RIGHT_DIAG, 'offset': (column_diff, row_diff) } elif is_forward and is_left: positions_away = { 'direction': MoveDirection.F_LEFT_DIAG, 'offset': (column_diff, row_diff) } elif is_back and is_right: positions_away = { 'direction': MoveDirection.B_RIGHT_DIAG, 'offset': (column_diff, row_diff) } elif is_back and is_left: positions_away = { 'direction': MoveDirection.B_LEFT_DIAG, 'offset': (column_diff, row_diff) } elif is_forward: positions_away = { 'direction': MoveDirection.FORWARD, 'offset': (column_diff, row_diff) } elif is_back: positions_away = { 'direction': MoveDirection.BACKWARD, 'offset': (column_diff, row_diff) } elif is_right: positions_away = { 'direction': MoveDirection.RIGHT, 'offset': (column_diff, row_diff) } elif is_left: positions_away = { 'direction': MoveDirection.LEFT, 'offset': (column_diff, row_diff) } else: positions_away = {'direction': None, 'offset': (0, 0)} return positions_away
def is_check(self, king_color, position=None, ghost_pieces=None): """ Test for check against king of the specified color. :param king_color: Color Color of king to test for check against. :param position: string If set, look for check from this position instead of the position of the king specified by king_color. :param ghost_pieces: dict Positions to ignore or contain ghost pieces. :return: bool True if king is in check, False otherwise. """ if position: ChessHelper.validate_position(position) king_position = position if position else self._king_positions[ king_color] if not self._king_positions[king_color]: return False # Check file and rank diagonal_pieces = (Type.QUEEN, Type.BISHOP) file_and_rank_pieces = (Type.ROOK, Type.QUEEN) pawn_capture_directions = (MoveDirection.F_RIGHT_DIAG, MoveDirection.F_LEFT_DIAG) opponent_piece_list = { MoveDirection.FORWARD: file_and_rank_pieces, MoveDirection.F_RIGHT_DIAG: diagonal_pieces, MoveDirection.RIGHT: file_and_rank_pieces, MoveDirection.B_RIGHT_DIAG: diagonal_pieces, MoveDirection.BACKWARD: file_and_rank_pieces, MoveDirection.B_LEFT_DIAG: diagonal_pieces, MoveDirection.LEFT: file_and_rank_pieces, MoveDirection.F_LEFT_DIAG: diagonal_pieces } for direction, pieces in opponent_piece_list.items(): ghost_pieces = {} if not ghost_pieces else ghost_pieces nearest_piece = self._get_nearest_piece_in_direction( king_position, direction, king_color, ghost_pieces) if nearest_piece and nearest_piece['color'] != king_color: offset = nearest_piece['offset'] piece_type = nearest_piece['type'] if direction in pawn_capture_directions and offset == 1 and piece_type == Type.PAWN: return True elif offset == 1 and piece_type == Type.KING: return True elif piece_type in pieces: return True # Color doesnt matter here l_shape_positions = self._get_possible_positions( king_position, MoveDirection.L_SHAPE, king_color) for position in l_shape_positions: if self.is_position_occupied(position): piece = self[position] if piece.type == Type.KNIGHT and piece.color != king_color: return True return False
def move_piece(self, start_position, end_position): """ Move piece from starting position to end position. Does not check if end position is valid. Ex. king in check or if overtaking square with piece of same color. Also does not check if moving piece has passed through other pieces. :param start_position: string Algebraic notation position. :param end_position: string Algebraic notation position. :return: dict [position]: Piece. Use None if position is now empty """ ChessHelper.validate_position(start_position) ChessHelper.validate_position(end_position) start_position_piece = self[start_position] if not start_position_piece: raise EmptyPositionError(start_position) updated_positions = {} move_info = self._direction_and_position_offset( start_position, end_position, start_position_piece.color) direction = move_info['direction'] column_offset, row_offset = move_info['offset'] # Copy of en_passant info en_passant_info = copy.copy(self._en_passant_info) # Have to clone the piece before moving it. Otherwise, it makes it really hard to compare # expected move result to actual move result since the piece here could have a modified # move_directions . Plus, we don't really care about any values other than piece type and color updated_positions[start_position] = None updated_positions[end_position] = copy.deepcopy(start_position_piece) if start_position_piece.type == Type.PAWN and row_offset == 2: tartget_position = self._get_position_shifted_by_offset( start_position, MoveDirection.FORWARD, 1, start_position_piece.color) en_passant_info['pawn_position'] = end_position en_passant_info['target_position'] = tartget_position else: en_passant_info['pawn_position'] = None en_passant_info['target_position'] = None # Check pawn special movements if start_position_piece.type == Type.PAWN: # Update move_directions after pawn moves forward twice if start_position_piece.move_directions[MoveDirection.FORWARD] > 1: start_position_piece.move_directions[MoveDirection.FORWARD] = 1 # Check for en passant if self.can_en_passant(start_position, direction): self._remove_piece(self._en_passant_info['pawn_position']) updated_positions[ self._en_passant_info['pawn_position']] = None # Check for castling elif start_position_piece.type == Type.KING: if abs(column_offset) == 2 and self.can_castle( start_position_piece.color, direction): rook_positions = { Color.WHITE: { MoveDirection.LEFT: 'a1', MoveDirection.RIGHT: 'h1' }, Color.BLACK: { MoveDirection.LEFT: 'h8', MoveDirection.RIGHT: 'a8' } } new_rook_position = self._get_position_shifted_by_offset( start_position, direction, 1, start_position_piece.color) self[new_rook_position] = self[rook_positions[ start_position_piece.color][direction]] self._remove_piece( rook_positions[start_position_piece.color][direction]) updated_positions[new_rook_position] = copy.deepcopy( self[new_rook_position]) updated_positions[rook_positions[start_position_piece.color] [direction]] = None # Update King left and right move_directions start_position_piece.move_directions[MoveDirection.LEFT] = 1 start_position_piece.move_directions[MoveDirection.RIGHT] = 1 start_position_piece.move_directions[MoveDirection.LEFT] = 1 start_position_piece.move_directions[MoveDirection.RIGHT] = 1 elif start_position_piece.type == Type.ROOK: king_position = self._king_positions[start_position_piece.color] column, row = self.position_to_row_and_column( start_position, start_position_piece.color) rook_direction = None if column == 0: rook_direction = MoveDirection.LEFT elif column == self.get_dimension() - 1: rook_direction = MoveDirection.RIGHT # Check needed because might be dealing with a board without a king if king_position and rook_direction: king = self[king_position] if king.move_directions[rook_direction] > 1: king.move_directions[rook_direction] = 1 self._en_passant_info = en_passant_info self._remove_piece(start_position) self._remove_piece(end_position) self[end_position] = start_position_piece return updated_positions
def move_piece(self, start_position, end_position): """ Move a piece from start_position to end_position. :param start_position: :param end_position: :return: """ try: ChessHelper.validate_position(start_position) ChessHelper.validate_position(end_position) except InvalidPositionError as e: # Maybe log error here # Reraise exception either way raise else: current_fen = Fen(self.fen) current_player = current_fen.current_player next_player = Color.WHITE if current_player == Color.BLACK else Color.BLACK move_result = MoveResult() # If moving a pawn to the end of the board, dont update anything on the board. # Instead, just return the pawn promote info. if self.can_promote_pawn(start_position, end_position): player = self._get_player_by_color(current_player) promote_info = { 'player': player, 'promote_types': self.get_pawn_promote_types() } move_result.pawn_promote_info = promote_info return move_result move_result.update_positions = self._board.move_piece( start_position, end_position) # Determine which directions castling is possible for. castle_info = {Color.BLACK: [], Color.WHITE: []} for color in [Color.WHITE, Color.BLACK]: for direction in [MoveDirection.LEFT, MoveDirection.RIGHT]: if self._board.can_castle(color, direction): castle_info[color].append(direction) # Generate and save fen string after move next_fen = Fen() next_fen_str = next_fen.generate_fen( self._board.get_board_pieces(), next_player, castle_info[Color.WHITE], castle_info[Color.BLACK], self._board.get_enpassant_position()) self.fen = next_fen_str # If checkmate or draw, set game over flag. Also create game_score object and fill # the move results object. is_checkmate = self._board.is_checkmate(next_player) is_stalemate = self._board.is_stalemate(next_player) if is_checkmate or is_stalemate: self.is_over = True if is_checkmate: if current_player == Color.WHITE: self.score = GameScore(game=self, white_score=1) player_in_checkmate = self.black_player player_in_checkmate.color = Color.BLACK else: self.score = GameScore(game=self, black_score=1) player_in_checkmate = self.white_player player_in_checkmate.color = Color.WHITE move_result.king_in_checkmate = player_in_checkmate else: self.score = GameScore(game=self, white_score=0.5, black_score=0.5) move_result.draw = True # If it is check, add info to move_result. is_check = self._board.is_check(next_player) if is_check: if current_player == Color.WHITE: player_in_check = self._get_player_by_color(Color.BLACK) else: player_in_check = self._get_player_by_color(Color.WHITE) move_result.king_in_check = player_in_check return move_result