1
\$\begingroup\$

I wanted to ask if this project is valid to state on a resume for entry-level python developer and if the code is presentable to say a job interviewer.

github link: full project

(If this is not the place to ask this, I will remove above part of this post. By all means let me know)

The code needing most attention is a method(moves_safety_check()) in a match class checking the legality of the pseudo legal moves. Code doesn't have to change but I think the function is too long and over complicated, thus I would like tips for condensing the function with readability. Also, is the way i'm writing docstrings acceptable?

from chess.board import Board
from copy import deepcopy
from chess.moves import *
from chess.king_attacking_line import KingTargetingPseudoLine, DangerLines, SquaresTargeted
import json

all_ps_moves = ps_moves_per_loc() # dict with all board locations as keys and 
                                  # dictionaries as values having keys for each direction
                                  # with a list of moves from that location in that
                                  # direction as values.


class Match:
    def __init__(self, state=Board().board, turn='white', wcks=True, wcqs=True, bcks=True, bcqs=True):
        self.white_can_castle = {'ks': wcks, 'qs': wcqs}
        self.black_can_castle = {'ks': bcks, 'qs': bcqs}
        self.state = state
        self.fen = convert_to_fen(self.state, turn, wcks, wcqs, bcks, bcqs)
        self.black_pieces_locations = {}
        self.white_pieces_locations = {}
        self.board_locations_occupied_by_black = {}
        self.board_locations_occupied_by_white = {}
        self.ps_black_moves = {}
        self.ps_white_moves = {}
        self.black_moves = {}
        self.white_moves = {}
        self.set_piece_locations()
        self.continuous_non_capped_turns = 0
        try:
            self.black_king_targeting_ps_lines = DangerLines(self.board_locations_occupied_by_black,
                                                             self.board_locations_occupied_by_white,
                                                             self.black_pieces_locations['K'])

            self.white_king_targeting_ps_lines = DangerLines(self.board_locations_occupied_by_white,
                                                             self.board_locations_occupied_by_black,
                                                             self.white_pieces_locations['K'])
        except KeyError:
            for row in self.state:
                print(row)
            raise KeyError('King is not in board!')
        self.king_attacking_line = []
        self.white_non_iterative = SquaresTargeted() # keeps track of pseudo legal moves belonging to a pawn, knight or king.
        self.black_non_iterative = SquaresTargeted()
        self.b_check = False
        self.w_check = False
        self.turn = turn
        self.king_attackers_locations = []
        self.check_mate = False
        self.winner = None
        self.draw = False
        self.moves_keys_history = {'wcc': deepcopy(self.white_can_castle), 'bcc': deepcopy(self.black_can_castle),
                                   'keys': [], 'moves': [], 'state': self.state, 'fen': self.fen}
        self.key = None
        self.trace = []
        self.set_all_ps_moves()

    def is_in_black_ps_moves(self, move):
        """
        Goes through black's pseudo moves and checks if given move is in the black's pseudo moves.
        :param move: see if it is in black pseudo moves
        :type move: tuple
        :return: True if move is in black pseudo moves else False
        :rtype: bool
        """
        for key in self.ps_black_moves:
            if 'Q' in key or 'B' in key or 'R' in key:
                for direction in self.ps_black_moves[key]:
                    if move in self.ps_black_moves[key][direction]:
                        return True
        return False

    def is_in_white_ps_moves(self, move):
        """
        Goes through white's pseudo moves and checks if given move is in the white's pseudo moves.
        :param move: see if it is in white pseudo moves
        :type move: tuple
        :return: True if move is in white pseudo moves else False
        :rtype: bool
        """
        for key in self.ps_white_moves:
            if 'Q' in key or 'B' in key or 'R' in key:
                for direction in self.ps_white_moves[key]:
                    if move in self.ps_white_moves[key][direction]:
                        return True
        return False

    def legal_line(self, moves, color, per_mov=False, board=False):
        """
        Returns legal moves.
        :param moves: pseudo moves
        :param color: side of pseudo moves given, must be 'black' or 'white'
        :param per_mov: set to True if moves given are of a pawn king or knight
        :param board: board to use check the moves given, default is self.state
        :type moves: list
        :type color: str
        :type per_mov: bool
        :type board: tuple
        :return: legal moves
        :rtype: list
        """
        board = self.state if not board else board
        new_moves = []
        if color == 'white':
            me, opp = 'w', 'b'
        else:
            me, opp = 'b', 'w'
        if not per_mov:
            for move in moves:
                if me in str(board[move[0]][move[1]]):
                    moves = new_moves
                    return moves
                elif opp in str(board[move[0]][move[1]]):
                    new_moves.append(move)
                    return new_moves
                elif me not in str(board[move[0]][move[1]]):
                    new_moves.append(move)
        else:
            for move in moves:
                if me not in str(board[move[0]][move[1]]):
                    new_moves.append(move)
        return new_moves

    def set_piece_locations(self):
        """
        Called one time at initialisation, sets locations for pieces of both sides.
        :return:
        """
        for num in range(2):
            pieces = {}
            locations_occupied = {}
            me = 'w' if num == 0 else 'b'
            for i, row in enumerate(self.state):
                for j, sq in enumerate(row):
                    if me in str(sq):
                        key = sq.replace(me, '')
                        pieces[key] = (i, j)
                        locations_occupied['({}, {})'.format(i, j)] = key
            if num == 0:
                self.white_pieces_locations = pieces
                self.board_locations_occupied_by_white = locations_occupied
            else:
                self.black_pieces_locations = pieces
                self.board_locations_occupied_by_black = locations_occupied

    def get_king_targeting_lines(self, color, loc=False, get_direction=False):
        """
        Returns the pieces and their lines that target the given location, also returns their direction at index 1*
        if get_direction is set to True.
        :param color: side who needs the targeting lines, must be 'white' or 'black'
        :param loc: location to use as the target
        :param get_direction: True if the direction key is also needed
        :type color: str
        :type loc: tuple
        :type get_direction: bool
        :return: a tuple of keys, directions*, lines that target the given location
        :rtype: tuple
        """
        if color == 'white':
            ps_move_dict = self.ps_black_moves
            if not loc:
                king = self.white_pieces_locations['K']
            else:
                king = loc
        else:
            ps_move_dict = self.ps_white_moves
            if not loc:
                king = self.black_pieces_locations['K']
            else:
                king = loc

        keys = []
        directions = []
        lines = []

        for key in ps_move_dict:
            if 'Q' in key or 'B' in key or 'R' in key:
                for direction in ps_move_dict[key]:
                    if king in ps_move_dict[key][direction]:
                        if get_direction:
                            directions.append(direction)

                        keys.append(key)
                        lines.append(ps_move_dict[key][direction])

        if get_direction:
            return keys, directions, lines
        else:
            return keys, lines

    def moves_safety_check(self, color):
        # self.state is a tuple of 8 lists with length 8 as chess board.
        # k_targeting_ps_lines is a list containing data of pieces(only queens, bishops or rooks) targeting the king
        # with their pseudo legal moves.
        # 
        # As an example say the black king is at position (3, 3) and a black pawn at (3, 4).
        # If a rook or queen in this case is at position (3, 5) or (3, 6) or (3, 7), then that rook or queen is pseudo-
        # targeting the king.
        # if no piece is pseudo targeting the king when this method is called, then we don't have to check for any
        # move except for the king's moves if that move would result in check. 

        # If however the king is targeted, then we check for each piece belonging to the side of aforementioned king if
        # that piece is in a pseudo legal line targeting it's king. If so, we see if it would result in check if that 
        # piece moved to the location provided. 
        # 
        # If not in a pseudo legal line targeting the king, then we know it's safe for that piece to move since it's not
        # protecting their king. Unless of course it's check, this means we only add moves which captures the piece
        # having king in check or which would protect the king. 
        # The line targeting the king when it's check is self.king_attacking_line
        """
        Checks if legal moves will cause check in which case the move is removed from legal moves.
        :param color: side to have moves checked, either 'black' or 'white'
        :type color: str
        """
        def kings_move_is_dangerous():
            """
            Is called if the move is in the opponent's pseudo moves.
            If move would result in check, does not add to leg_moves.
            """
            attackers, lines = self.get_king_targeting_lines(color, loc=move)
            row = piece_locations[key][0]
            col = piece_locations[key][1]
            safe = True
            for attacker, line in zip(attackers, lines):

                sim_board = deepcopy(self.state)
                sim_board[row][col] = 1
                sim_board[move[0]][move[1]] = me + key

                if move[1] == col - 3:
                    sim_board[row][0] = 1
                    sim_board[row][2] = me + 'R1'
                elif move[1] == col + 2:
                    sim_board[row][7] = 1
                    sim_board[row][5] = me + 'R2'

                if move in self.legal_line(line, opp_color, board=sim_board):
                    safe = False
                    break
            if safe:
                safe_moves.append(move)

        if color == 'white':
            move_dict = self.white_moves
            k_targeting_ps_lines = self.white_king_targeting_ps_lines
            opp_non_iter_moves = self.black_non_iterative
            is_in_opp_moves = self.is_in_black_ps_moves
            opp, me = 'b', 'w'
            opp_color = 'black'
            check = self.w_check
            piece_locations = self.white_pieces_locations
            pawn_one_step_two_steps, start_row = (-1, -2), 6
        else:
            move_dict = self.black_moves
            k_targeting_ps_lines = self.black_king_targeting_ps_lines
            opp_non_iter_moves = self.white_non_iterative
            is_in_opp_moves = self.is_in_white_ps_moves
            opp, me = 'w', 'b'
            opp_color = 'white'
            check = self.b_check
            piece_locations = self.black_pieces_locations
            pawn_one_step_two_steps, start_row = (1, 2), 1

        for key in move_dict:
            if 'Q' in key or 'R' in key or 'B' in key:
                if check:
                    if len(self.king_attackers_locations) > 1:
                        move_dict[key] = None
                        continue
                    if k_targeting_ps_lines.is_targeted(piece_locations[key]):
                        for direction in move_dict[key]:
                            safe_moves = []
                            for move in move_dict[key][direction]:
                                if k_targeting_ps_lines.would_be_check(piece_locations[key], move, already_check=check):
                                    break
                                elif move in self.king_attacking_line or move in self.king_attackers_locations:
                                    safe_moves.append(move)
                            move_dict[key][direction] = safe_moves
                    else:
                        for direction in move_dict[key]:
                            safe_moves = []
                            for move in move_dict[key][direction]:
                                if move in self.king_attacking_line or move in self.king_attackers_locations:
                                    safe_moves.append(move)

                            move_dict[key][direction] = safe_moves
                elif k_targeting_ps_lines:
                    if k_targeting_ps_lines.is_targeted(piece_locations[key]):
                        for direction in move_dict[key]:
                            safe_moves = []
                            for move in move_dict[key][direction]:
                                if k_targeting_ps_lines.would_be_check(piece_locations[key], move):
                                    break
                                safe_moves.append(move)
                            move_dict[key][direction] = safe_moves
                bad_directions = []
                for direction in move_dict[key]:
                    if not move_dict[key][direction]:
                        bad_directions.append(direction)
                for direction in bad_directions:
                    del move_dict[key][direction]
            elif 'N' in key:
                if not move_dict[key]:
                    continue
                if check:
                    if len(self.king_attackers_locations) > 1:
                        move_dict[key] = None
                        continue
                    if k_targeting_ps_lines.is_targeted(piece_locations[key]):
                        if k_targeting_ps_lines.would_be_check(piece_locations[key],
                                                               move_dict[key][0],
                                                               already_check=check):

                            move_dict[key] = None
                            continue

                        safe_moves = []
                        for move in move_dict[key]:
                            if move in self.king_attacking_line or move in self.king_attackers_locations:
                                safe_moves.append(move)
                        move_dict[key] = safe_moves if safe_moves else None
                        continue
                    else:
                        safe_moves = []
                        for move in move_dict[key]:
                            if move in self.king_attacking_line or move in self.king_attackers_locations:
                                safe_moves.append(move)
                        move_dict[key] = safe_moves if safe_moves else None
                        continue
                elif k_targeting_ps_lines:
                    if k_targeting_ps_lines.is_targeted(piece_locations[key]):
                        if k_targeting_ps_lines.would_be_check(piece_locations[key], move_dict[key][0]):
                            move_dict[key] = None
                            continue
            elif 'P' in key:
                if piece_locations[key][0] + pawn_one_step_two_steps[0] not in range(8):
                    move_dict[key] = None
                    continue
                one_step = (piece_locations[key][0] + pawn_one_step_two_steps[0], piece_locations[key][1])
                safe_moves = []
                if check:
                    if len(self.king_attackers_locations) > 1:
                        move_dict[key] = None
                        continue
                    danger = False
                    if k_targeting_ps_lines.is_targeted(piece_locations[key]):
                        danger = True
                        for move in move_dict[key]:
                            if k_targeting_ps_lines.would_be_check(piece_locations[key], move, already_check=check):
                                continue
                            if move in self.king_attackers_locations:
                                safe_moves.append(move)
                    else:
                        for move in move_dict[key]:
                            if move in self.king_attackers_locations:
                                safe_moves.append(move)
                    if danger:
                        if str(self.state[one_step[0]][one_step[1]]) == '1':
                            if k_targeting_ps_lines.would_be_check(piece_locations[key], one_step):
                                move_dict[key] = safe_moves if safe_moves else None
                                continue
                            if one_step in self.king_attacking_line:
                                safe_moves.append(one_step)
                            if piece_locations[key][0] == start_row:
                                two_steps = (piece_locations[key][0] + pawn_one_step_two_steps[1], piece_locations[key][1])
                                if str(self.state[two_steps[0]][two_steps[1]]) == '1':
                                    if two_steps in self.king_attacking_line:
                                        safe_moves.append(two_steps)
                    else:
                        if str(self.state[one_step[0]][one_step[1]]) == '1':
                            if one_step in self.king_attacking_line:
                                safe_moves.append(one_step)
                            if piece_locations[key][0] == start_row:
                                two_steps = (piece_locations[key][0] + pawn_one_step_two_steps[1],
                                             piece_locations[key][1])

                                if str(self.state[two_steps[0]][two_steps[1]]) == '1':
                                    if two_steps in self.king_attacking_line:
                                        safe_moves.append(two_steps)
                elif k_targeting_ps_lines:
                    danger = False
                    if k_targeting_ps_lines.is_targeted(piece_locations[key]):
                        danger = True
                        for move in move_dict[key]:
                            if k_targeting_ps_lines.would_be_check(piece_locations[key], move):
                                continue
                            else:
                                if opp in str(self.state[move[0]][move[1]]):
                                    safe_moves.append(move)
                    else:
                        for move in move_dict[key]:
                            if opp in str(self.state[move[0]][move[1]]):
                                safe_moves.append(move)
                    if str(self.state[one_step[0]][one_step[1]]) == '1':
                        if danger:
                            if k_targeting_ps_lines.would_be_check(piece_locations[key], one_step):
                                move_dict[key] = safe_moves if safe_moves else None
                                continue
                            else:
                                safe_moves.append(one_step)
                                if piece_locations[key][0] == start_row:
                                    two_steps = (piece_locations[key][0] + pawn_one_step_two_steps[1],
                                                 piece_locations[key][1])

                                    if str(self.state[two_steps[0]][two_steps[1]]) == '1':
                                        safe_moves.append(two_steps)
                        else:
                            safe_moves.append(one_step)
                            if piece_locations[key][0] == start_row:
                                two_steps = (piece_locations[key][0] + pawn_one_step_two_steps[1],
                                             piece_locations[key][1])

                                if str(self.state[two_steps[0]][two_steps[1]]) == '1':
                                    safe_moves.append(two_steps)
                else:
                    for move in move_dict[key]:
                        if opp in str(self.state[move[0]][move[1]]):
                            safe_moves.append(move)

                    if str(self.state[one_step[0]][one_step[1]]) == '1':
                        safe_moves.append(one_step)
                        if piece_locations[key][0] == start_row:
                            two_steps = (piece_locations[key][0] + pawn_one_step_two_steps[1], piece_locations[key][1])
                            if str(self.state[two_steps[0]][two_steps[1]]) == '1':
                                safe_moves.append(two_steps)

                move_dict[key] = safe_moves if safe_moves else None

            else:
                safe_moves = []
                if check:
                    for move in move_dict[key]:
                        if move in self.king_attacking_line or move in opp_non_iter_moves.move_set:
                            continue
                        elif is_in_opp_moves(move):
                            kings_move_is_dangerous()
                        else:
                            safe_moves.append(move)
                    move_dict[key] = safe_moves if safe_moves else None
                else:
                    for move in move_dict[key]:
                        if move in opp_non_iter_moves.move_set:
                            continue
                        elif is_in_opp_moves(move):
                            kings_move_is_dangerous()
                        else:
                            safe_moves.append(move)
                    move_dict[key] = safe_moves

        keys_to_del = []
        for key in move_dict:
            if not move_dict[key]:
                keys_to_del.append(key)

        for key in keys_to_del:
            del move_dict[key]

        if not move_dict:
            self.check_mate = True
            self.winner = 'black' if color == 'white' else 'white'

    def set_all_ps_moves(self):
        """
        Is called one time at initialisation, sets all the pseudo-legal moves for both sides.
        """
        for key in self.white_pieces_locations:
            if 'Q' in key or 'R' in key or 'B' in key:
                self.ps_white_moves[key] = {}
                self.update_ps_iterative_moves(key, 'white')
            elif 'P' in key or 'N' in key:
                self.update_ps_non_iterative_moves(key, 'white')
            else:
                self.update_king_moves('white')

        for key in self.black_pieces_locations:
            if 'Q' in key or 'R' in key or 'B' in key:
                self.ps_black_moves[key] = {}
                self.update_ps_iterative_moves(key, 'black')
            elif 'P' in key or 'N' in key:
                self.update_ps_non_iterative_moves(key, 'black')
            else:
                self.update_king_moves('black')

        if self.turn == 'white':
            self.moves_safety_check(color='black')
            self.moves_safety_check(color='white')
        else:
            self.moves_safety_check(color='white')
            self.moves_safety_check(color='black')

    def check_or_king_ps_targeted(self, key, loc, color):
        """
        Checks if it is check or if the opponent's king is targeted by the pseudo moves of the given piece.
        :param key: piece, cannot be a pawn, knight or king
        :param loc: location of given key
        :param color: side of given key must be 'black' or 'white'
        :type key: str
        :type loc: tuple
        :type color: str
        """
        if color == 'white':
            try:
                king_loc = self.black_pieces_locations['K']
            except KeyError:
                self.save_game(fn='king_loc_key_error.json')
                raise

            opp_k_targeting_ps_lines = self.black_king_targeting_ps_lines
            move_dict = self.white_moves
            ps_move_dict = self.ps_white_moves
        else:
            try:
                king_loc = self.white_pieces_locations['K']
            except KeyError:
                self.save_game(fn='king_loc_key_error.json')
                raise

            opp_k_targeting_ps_lines = self.white_king_targeting_ps_lines
            move_dict = self.black_moves
            ps_move_dict = self.ps_black_moves

        ps_moves = []
        for direction in ps_move_dict[key]:
            ps_moves.append(ps_move_dict[key][direction])

        if 'Q' in key:
            directions = ('tr', 'tl', 'br', 'bl', 'u', 'd', 'r', 'l')
        elif 'R' in key:
            directions = ('u', 'd', 'r', 'l')
        else:
            directions = ('tr', 'tl', 'br', 'bl')

        if len(ps_moves) == 4:
            leg_moves = (self.legal_line(ps_moves[0], color), self.legal_line(ps_moves[1], color),
                         self.legal_line(ps_moves[2], color), self.legal_line(ps_moves[3], color))
        else:
            leg_moves = (self.legal_line(ps_moves[0], color), self.legal_line(ps_moves[1], color),
                         self.legal_line(ps_moves[2], color), self.legal_line(ps_moves[3], color),
                         self.legal_line(ps_moves[4], color), self.legal_line(ps_moves[5], color),
                         self.legal_line(ps_moves[6], color), self.legal_line(ps_moves[7], color))

        move_dict[key] = {}
        for index, direction in enumerate(directions):
            if leg_moves[index]:
                move_dict[key][direction] = leg_moves[index]
        i = 0
        for ps_line, leg_line in zip(ps_moves, leg_moves):
            if king_loc in ps_line:
                opp_k_targeting_ps_lines.append_x(KingTargetingPseudoLine(key, loc, directions[i], ps_line))
                if king_loc in leg_line:
                    self.king_attackers_locations.append(loc)
                    self.king_attacking_line.extend(leg_line)
                    self.w_check = True if color == 'black' else self.w_check
                    self.b_check = True if color == 'white' else self.b_check
            i += 1

    def update_legal_iterative_moves(self, key, color):
        """
        This sets the reachable squares a.k.a legal moves. ps_moves must already be set!
        :param key: piece to have legal moves updated, cannot be a pawn, knight or king
        :param color: side of the piece, must be 'black' or 'white'
        :type key: str
        :type color: str
        """
        ps_move_dict = self.ps_white_moves if color == 'white' else self.ps_black_moves
        leg_move_dict = self.white_moves if color == 'white' else self.black_moves
        leg_move_dict[key] = {}

        if 'Q' in key:
            tr, tl = ps_move_dict[key]['tr'], ps_move_dict[key]['tl']
            br, bl = ps_move_dict[key]['br'], ps_move_dict[key]['bl']
            u, d = ps_move_dict[key]['u'], ps_move_dict[key]['d']
            r, l = ps_move_dict[key]['r'], ps_move_dict[key]['l']

            leg_move_dict[key]['tr'], leg_move_dict[key]['tl'] = self.legal_line(tr, color), self.legal_line(tl, color)
            leg_move_dict[key]['br'], leg_move_dict[key]['bl'] = self.legal_line(br, color), self.legal_line(bl, color)
            leg_move_dict[key]['u'], leg_move_dict[key]['d'] = self.legal_line(u, color), self.legal_line(d, color)
            leg_move_dict[key]['r'], leg_move_dict[key]['l'] = self.legal_line(r, color), self.legal_line(l, color)
        elif 'B' in key:
            tr, tl = ps_move_dict[key]['tr'], ps_move_dict[key]['tl']
            br, bl = ps_move_dict[key]['br'], ps_move_dict[key]['bl']

            leg_move_dict[key]['tr'], leg_move_dict[key]['tl'] = self.legal_line(tr, color), self.legal_line(tl, color)
            leg_move_dict[key]['br'], leg_move_dict[key]['bl'] = self.legal_line(br, color), self.legal_line(bl, color)
        else:
            u, d = ps_move_dict[key]['u'], ps_move_dict[key]['d']
            r, l = ps_move_dict[key]['r'], ps_move_dict[key]['l']

            leg_move_dict[key]['u'], leg_move_dict[key]['d'] = self.legal_line(u, color), self.legal_line(d, color)
            leg_move_dict[key]['r'], leg_move_dict[key]['l'] = self.legal_line(r, color), self.legal_line(l, color)

    def update_ps_iterative_moves(self, key, color):
        """
        This sets the pseudo legal lines.
        :param key: piece to have pseudo legal moves updated, cannot be a pawn, knight or king
        :param color: side of the piece, must be 'black' or 'white'
        :type key: str
        :type color: str
        """
        loc = self.white_pieces_locations[key] if color == 'white' else self.black_pieces_locations[key]
        ps_move_dict = self.ps_white_moves if color == 'white' else self.ps_black_moves
        ps_move_dict[key] = {}
        pmk = str(loc)
        if 'Q' in key:

            ps_move_dict[key]['tr'], ps_move_dict[key]['tl'] = all_ps_moves[pmk]['tr'], all_ps_moves[pmk]['tl']
            ps_move_dict[key]['br'], ps_move_dict[key]['bl'] = all_ps_moves[pmk]['br'], all_ps_moves[pmk]['bl']
            ps_move_dict[key]['u'], ps_move_dict[key]['d'] = all_ps_moves[pmk]['u'], all_ps_moves[pmk]['d']
            ps_move_dict[key]['r'], ps_move_dict[key]['l'] = all_ps_moves[pmk]['r'], all_ps_moves[pmk]['l']

        elif 'B' in key:

            ps_move_dict[key]['tr'], ps_move_dict[key]['tl'] = all_ps_moves[pmk]['tr'], all_ps_moves[pmk]['tl']
            ps_move_dict[key]['br'], ps_move_dict[key]['bl'] = all_ps_moves[pmk]['br'], all_ps_moves[pmk]['bl']

        else:

            ps_move_dict[key]['u'], ps_move_dict[key]['d'] = all_ps_moves[pmk]['u'], all_ps_moves[pmk]['d']
            ps_move_dict[key]['r'], ps_move_dict[key]['l'] = all_ps_moves[pmk]['r'], all_ps_moves[pmk]['l']

        self.check_or_king_ps_targeted(key, loc, color)

    def update_legal_non_iterative_moves(self, key, color):
        """
        This sets the legal moves. ps_moves must already be set!
        :param key: piece to have legal moves updated, must be a pawn, knight or king
        :param color: side of the piece, must be 'black' or 'white'
        :type key: str
        :type color: str
        """
        if key == 'K':
            self.update_king_moves(color)
            return
        moves = self.ps_white_moves[key] if color == 'white' else self.ps_black_moves[key]
        leg_move_dict = self.white_moves if color == 'white' else self.black_moves
        leg_move_dict[key] = self.legal_line(moves, color, per_mov=True)

    def update_ps_non_iterative_moves(self, key, color):
        """
        This sets pseudo legal moves.
        :param key: piece to have legal moves updated, must be a pawn, knight or king
        :param color: side of the piece, must be 'black' or 'white'
        :type key: str
        :type color: str
        """
        if color == 'white':
            loc = self.white_pieces_locations[key]
            try:
                king_loc = self.black_pieces_locations['K']
            except KeyError:
                self.save_game(fn='king_loc_non_it.json')
                raise
            ps_moves = self.ps_white_moves
            move_dict = self.white_moves
            non_it_moves = self.white_non_iterative
        else:
            loc = self.black_pieces_locations[key]
            try:
                king_loc = self.white_pieces_locations['K']
            except KeyError:
                self.save_game(fn='king_loc_non_it.json')
                raise
            ps_moves = self.ps_black_moves
            move_dict = self.black_moves
            non_it_moves = self.black_non_iterative

        if 'P' in key:
            moves = ps_pawn_cap_moves(loc, color)
        else:
            moves = ps_knight_moves(loc)

        non_it_moves.assign(key, moves)
        ps_moves[key] = moves

        leg_moves = self.legal_line(moves, color, per_mov=True)
        move_dict[key] = leg_moves

        if king_loc in moves:
            if color == 'white':
                self.b_check = True
            else:
                self.w_check = True
            self.king_attackers_locations.append(loc)

    def king_moved(self, color):
        """
        Updates king_targeting_ps_lines of the side whose king moved.
        :param color: side whose king moved, must be 'black' or 'white'
        :type color: str
        """
        if color == 'white':
            loc = self.white_pieces_locations['K']
            is_in_opp_moves = self.is_in_black_ps_moves
            danger_lines = self.white_king_targeting_ps_lines
            opp_pieces = self.black_pieces_locations
        else:
            loc = self.black_pieces_locations['K']
            is_in_opp_moves = self.is_in_white_ps_moves
            danger_lines = self.black_king_targeting_ps_lines
            opp_pieces = self.white_pieces_locations

        danger_lines.king_moved(loc)
        if is_in_opp_moves(loc):
            keys, dirs, lines = self.get_king_targeting_lines(color, loc=loc, get_direction=True)
            for key, direction, line in zip(keys, dirs, lines):
                if key in danger_lines.keys:
                    continue
                attacker_loc = opp_pieces[key]
                danger_lines.append_x(KingTargetingPseudoLine(key, attacker_loc, direction, line))

    def update_king_moves(self, color):
        """
        Updates moves for given color's king, checks if can castle.
        :param color: side of king to have moves updated
        """
        if color == 'white':
            loc = self.white_pieces_locations['K']
            can_castle = self.white_can_castle
            ps_moves = self.ps_white_moves
            move_dict, pieces = self.white_moves, self.white_pieces_locations
            non_it_moves = self.white_non_iterative
        else:
            loc = self.black_pieces_locations['K']
            can_castle = self.black_can_castle
            ps_moves = self.ps_black_moves
            move_dict, pieces = self.black_moves, self.black_pieces_locations
            non_it_moves = self.black_non_iterative

        moves = ps_king_moves(loc, color)
        ps_moves['K'] = moves
        non_it_moves.assign('K', moves)
        leg_moves = self.legal_line(moves, color, per_mov=True)
        if can_castle['ks']:
            ps_moves['K'].append((pieces['K'][0], 6))
            for col in range(5, 7):
                if self.state[pieces['K'][0]][col] != 1:
                    break
                if col == 6:
                    leg_moves.append((pieces['K'][0], 6))

        if can_castle['qs']:
            ps_moves['K'].append((pieces['K'][0], 1))
            for col in range(3, 0, -1):
                if self.state[pieces['K'][0]][col] != 1:
                    break
                if col == 1:
                    leg_moves.append((pieces['K'][0], 1))
        move_dict['K'] = None
        if leg_moves:
            move_dict['K'] = leg_moves

        else:
            del move_dict['K']

    def update_logs(self, key, move, old_loc, castled):
        """
        Updates classes whose definitions are located in king_attacking_line.py.
        These check if certain moves are allowed and won't cause check.
        Also checks if pawns reached the final rank.
        :param key: last moved piece
        :param move: last move made
        :param old_loc: old location of last moved piece
        :param castled: did a king castle, only True if piece is king
        """
        if self.turn == 'white':
            opp_k_targeting_ps_lines, opp_non_iter_moves = self.black_king_targeting_ps_lines, self.black_non_iterative
            pieces_locations, inv_pieces = self.white_pieces_locations, self.board_locations_occupied_by_white
            ps_moves, moves = self.ps_white_moves, self.white_moves
            pawn_edge_row = 0
            can_castle = self.white_can_castle
        else:
            opp_k_targeting_ps_lines, opp_non_iter_moves = self.white_king_targeting_ps_lines, self.white_non_iterative
            pieces_locations, inv_pieces = self.black_pieces_locations, self.board_locations_occupied_by_black
            ps_moves, moves = self.ps_black_moves, self.black_moves
            pawn_edge_row = 7
            can_castle = self.black_can_castle

        if key == 'R1':
            can_castle['qs'] = False
        elif key == 'R2':
            can_castle['ks'] = False
        elif key == 'K':
            can_castle['ks'], can_castle['qs'] = False, False
            if castled:
                if move[1] == 1:
                    pieces_locations['R1'] = (move[0], 2)
                    inv_pieces[str((move[0], 2))] = 'R1'
                    try:
                        del inv_pieces[str((move[0], 0))]
                    except KeyError:
                        self.save_game(fn='castled_r1_.json')
                        raise KeyError("couldn't delete old location of R1")

                    opp_k_targeting_ps_lines.delete_line('R1')
                    self.update_ps_iterative_moves('R1', self.turn)

                elif move[1] == 6:
                    pieces_locations['R2'] = (move[0], 5)
                    inv_pieces[str((move[0], 5))] = 'R2'
                    try:
                        del inv_pieces[str((move[0], 7))]
                    except KeyError:
                        self.save_game(fn='castled_r2_.json')
                        raise KeyError("couldn't delete old location of R2")

                    opp_k_targeting_ps_lines.delete_line('R2')
                    self.update_ps_iterative_moves('R2', self.turn)
                else:
                    raise ValueError('Castled was passed as true but the move is not a legal castle move.')

        elif 'P' in key and move[0] == pawn_edge_row:
            non_it_moves = self.white_non_iterative if self.turn == 'white' else self.black_non_iterative
            non_it_moves.piece_died(key)
            del pieces_locations[key], ps_moves[key], moves[key]
            self.key = 'Q' + key[1]
            key = self.key
            ps_moves[key] = {}
            self.state[move[0]][move[1]] = self.turn[0] + key

        pieces_locations[key] = move
        inv_pieces[str(move)] = key
        try:
            del inv_pieces[str(old_loc)]
        except KeyError:
            self.save_game(fn='del_inv_pieces.json')
            raise

        if opp_k_targeting_ps_lines.is_king_attacker(key):
            opp_k_targeting_ps_lines.delete_line(key)

        if opp_k_targeting_ps_lines.is_targeted(old_loc):
            def is_check():
                attacker = opp_k_targeting_ps_lines.get_attacker()
                self.king_attackers_locations.append(attacker[0])
                try:
                    att_key = inv_pieces[str(attacker[0])]
                except KeyError:
                    print(opp_k_targeting_ps_lines.keys)
                    self.save_game(fn='att_key_error.json')
                    raise KeyError('Something went wrong!')
                ps_line = ps_moves[att_key][attacker[1]]
                self.king_attacking_line.extend(self.legal_line(ps_line, self.turn))

            if self.turn == 'white':
                self.b_check = opp_k_targeting_ps_lines.will_be_check(old_loc)
                if self.b_check:
                    is_check()
            else:
                self.w_check = opp_k_targeting_ps_lines.will_be_check(old_loc)
                if self.w_check:
                    is_check()

    def is_piece_capped(self, move):
        """
        Checks if a piece was captured, and updates data in response.
        :param move: last move made
        """
        if self.turn == 'white':
            danger_lines = self.white_king_targeting_ps_lines
            opp_ps_moves, opp_moves, opp_non_it_moves = self.ps_black_moves, self.black_moves, self.black_non_iterative
            opp_pieces, opp_inv_pieces = self.black_pieces_locations, self.board_locations_occupied_by_black
            opp_can_castle = self.black_can_castle
        else:
            danger_lines = self.black_king_targeting_ps_lines
            opp_ps_moves, opp_moves, opp_non_it_moves = self.ps_white_moves, self.white_moves, self.white_non_iterative
            opp_pieces, opp_inv_pieces = self.white_pieces_locations, self.board_locations_occupied_by_white
            opp_can_castle = self.white_can_castle

        if self.state[move[0]][move[1]] != 1:
            self.continuous_non_capped_turns = 0
            capped = self.state[move[0]][move[1]][1:]
            if 'P' not in capped and 'N' not in capped:
                if 'R1' in capped:
                    opp_can_castle['qs'] = False
                elif 'R2' in capped:
                    opp_can_castle['ks'] = False
                if danger_lines.is_king_attacker(capped):
                    danger_lines.delete_line(capped)
            else:
                opp_non_it_moves.piece_died(capped)
            try:
                del opp_pieces[capped], opp_ps_moves[capped], opp_inv_pieces[str(move)], opp_moves[capped]
            except KeyError:
                if capped not in opp_moves.keys() or capped not in opp_ps_moves.keys():
                    pass
                else:
                    self.save_game(fn='del_key_error_301.json')
                    raise
        else:
            self.continuous_non_capped_turns += 1

    def save_game(self, fn='match.json'):
        """
        Saves full match in match.json with starting board, all moves made including the pieces and
        whether black and white could castle.
        loading a file with data for a match can be played using rerun_game(fn=fn).
        :param fn: default is match.json, file to have match stored in
        :type fn: str
        """
        with open(fn, 'w') as f:
            json.dump(self.moves_keys_history, f, indent=1)

    def make_move(self, move, key):
        """
        Places piece in new location and updates data:
        self.is_piece_capped(...),
        self.update_logs(...)
        and updates moves for pieces needing it.
        :param move: move to be made
        :param key: piece to move
        :type move: tuple
        :type key: str
        """

        self.key = key
        self.moves_keys_history['keys'].append(key)
        self.moves_keys_history['moves'].append(move)

        if self.turn == 'white':
            ps_moves, pieces_locations = self.ps_white_moves, self.white_pieces_locations
            opp_ps_moves = self.ps_black_moves
            old_loc = pieces_locations[self.key]
            opp = 'black'
        else:
            ps_moves, pieces_locations = self.ps_black_moves, self.black_pieces_locations
            opp_ps_moves = self.ps_white_moves
            old_loc = pieces_locations[self.key]
            opp = 'white'

        self.is_piece_capped(move)
        if self.continuous_non_capped_turns == 50:
            self.draw, self.winner = True, None
            return

        piece = self.state[old_loc[0]][old_loc[1]]
        self.state[old_loc[0]][old_loc[1]] = 1
        self.state[move[0]][move[1]] = piece

        castled = False
        if self.key == 'K':
            if move[1] == pieces_locations[self.key][1] - 3:
                castled = True
                rook1, self.state[move[0]][0] = self.state[move[0]][0], 1
                self.state[move[0]][2] = rook1

            elif move[1] == pieces_locations[self.key][1] + 2:
                castled = True
                rook2, self.state[move[0]][7] = self.state[move[0]][7], 1
                self.state[move[0]][5] = rook2

        self.update_logs(self.key, move, old_loc, castled)

        def update_moves(_key, _color):
            if 'P' not in _key and 'N' not in _key and 'K' not in _key:
                self.update_ps_iterative_moves(_key, _color)
            elif 'K' not in _key:
                self.update_ps_non_iterative_moves(_key, _color)
            else:
                if self.key == 'K':
                    self.king_moved(_color)
                self.update_king_moves(_color)

        def update_legal_moves(_key, _color):
            if 'P' not in _key and 'N' not in _key and 'K' not in _key:
                self.update_legal_iterative_moves(_key, _color)
            else:
                self.update_legal_non_iterative_moves(_key, _color)
        for key_2 in ps_moves:
            if key_2 == self.key:
                update_moves(key_2, self.turn)
                continue
            update_legal_moves(key_2, self.turn)
        for key_2 in opp_ps_moves:
            update_legal_moves(key_2, opp)

        self.moves_safety_check(opp)

        if len(self.black_pieces_locations) == 1 and len(self.white_pieces_locations) == 1:
            self.draw = True
            self.winner = None

        self.turn = 'black' if self.turn == 'white' else 'white'
        self.w_check = False
        self.b_check = False
        self.king_attackers_locations = []
        self.king_attacking_line = []

This class is by far the largest, I'm not sure how to make this understandable. Feel free to dive in all the relevant code at the link above.

\$\endgroup\$
  • 2
    \$\begingroup\$ Since this is part of a class and you actually use other parts of that class within this method, I think that this is not really reviewable without those other parts (what is self.state, self.get_king_targeting_lines, self.white_moves, self.white_king_targeting_ps_lines, self.black_non_iterative, self.is_in_black_ps_moves, self.w_check...) \$\endgroup\$ – Graipher Mar 9 at 8:34
  • 1
    \$\begingroup\$ I agree with Graipher, we need to see the rest of the class at least. A usage example of the class with it would be preferred as well. \$\endgroup\$ – Mast Mar 9 at 13:18
  • \$\begingroup\$ @Mast What do you mean with a usage example? \$\endgroup\$ – Bas Velden Mar 9 at 17:34
  • \$\begingroup\$ Well, this is a library. How would it be used? How would an actual program be implemented with this? \$\endgroup\$ – Mast Mar 10 at 5:36
  • \$\begingroup\$ Your link is broken. \$\endgroup\$ – Oscar Smith Mar 10 at 7:39

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.