commit 3eb99eb80924cf03e3d94cc31f48f91c41773376 Author: Crizomb <62544756+Crizomb@users.noreply.github.com> Date: Sat Mar 4 18:42:17 2023 +0100 Add files via upload diff --git a/chess_ai.py b/chess_ai.py new file mode 100644 index 0000000..b96c842 --- /dev/null +++ b/chess_ai.py @@ -0,0 +1,414 @@ +from chess_tools import get_possible_moves, move_piece, init_board, get_time +import numpy as np +import time +import random + + + + +inf = float("inf") + +score_table = {"Q" : 9, "R" : 5, "B" : 3.3, "N" : 3.2, "P" : 1, "K" : 10**5 ,"q" : -9, "r" : -5, "b" : -3, "n" : -3, "p" : -1, "k" : -10**5, '':0, '.':0} + +count = 0 + + + +class Heuristics: + + # The tables denote the points scored for the position of the chess pieces on the board. + # Source : https://www.chessprogramming.org/Simplified_Evaluation_Function + + PAWN_TABLE = [ + [ 0, 0, 0, 0, 0, 0, 0, 0], + [ 5, 10, 10,-20,-20, 10, 10, 5], + [ 5, -5,-10, 0, 0,-10, -5, 5], + [ 0, 0, 0, 20, 20, 0, 0, 0], + [ 5, 5, 10, 25, 25, 10, 5, 5], + [10, 10, 20, 30, 30, 20, 10, 10], + [50, 50, 50, 50, 50, 50, 50, 50], + [ 0, 0, 0, 0, 0, 0, 0, 0] + ] + + KNIGHT_TABLE = [ + [-50, -40, -30, -30, -30, -30, -40, -50], + [-40, -20, 0, 5, 5, 0, -20, -40], + [-30, 5, 10, 15, 15, 10, 5, -30], + [-30, 0, 15, 20, 20, 15, 0, -30], + [-30, 5, 15, 20, 20, 15, 0, -30], + [-30, 0, 10, 15, 15, 10, 0, -30], + [-40, -20, 0, 0, 0, 0, -20, -40], + [-50, -40, -30, -30, -30, -30, -40, -50] + ] + + BISHOP_TABLE = [ + [-20, -10, -10, -10, -10, -10, -10, -20], + [-10, 5, 0, 0, 0, 0, 5, -10], + [-10, 10, 10, 10, 10, 10, 10, -10], + [-10, 0, 10, 10, 10, 10, 0, -10], + [-10, 5, 5, 10, 10, 5, 5, -10], + [-10, 0, 5, 10, 10, 5, 0, -10], + [-10, 0, 0, 0, 0, 0, 0, -10], + [-20, -10, -10, -10, -10, -10, -10, -20] + ] + + ROOK_TABLE = [ + [ 0, 0, 0, 5, 5, 0, 0, 0], + [-5, 0, 0, 0, 0, 0, 0, -5], + [-5, 0, 0, 0, 0, 0, 0, -5], + [-5, 0, 0, 0, 0, 0, 0, -5], + [-5, 0, 0, 0, 0, 0, 0, -5], + [-5, 0, 0, 0, 0, 0, 0, -5], + [ 5, 10, 10, 10, 10, 10, 10, 5], + [ 0, 0, 0, 0, 0, 0, 0, 0] + ] + + QUEEN_TABLE = [ + [-20, -10, -10, -5, -5, -10, -10, -20], + [-10, 0, 0, 0, 0, 0, 0, -10], + [-10, 0, 0, 0, 0, 0, 0, -10], + [ 0, 0, 0, 0, 0, 0, 0, -5], + [ -5, 0, 0, 0, 0, 0, 0, -5], + [-10, 0, 0, 0, 0, 0, 0, -10], + [-10, 0, 0, 0, 0, 0, 0, -10], + [-20, -10, -10, -5, -5, -10, -10, -20] + ] + + KING_TABLE_MG = [ + [ 20, 50, 10, 0, 0, 10, 10, 20], + [ 20, 20, 0, 0, 0, 0, 20, 20], + [-10, -20, -20, -20, -20, -20, -20, -10], + [-20, -30, -30, -40, -40, -30, -30, -20], + [-30, -40, -40, -50, -50, -40, -40, -30], + [-30, -40, -40, -50, -50, -40, -40, -30], + [-30, -40, -40, -50, -50, -40, -40, -30], + [-30, -40, -40, -50, -50, -40, -40, -30] + ] + + KING_TABLE_EG = [ + [-50, -30, -30, -30, -30, -30, -30, -50], + [-30, -30, 0, 0, 0, 0, -30, -30], + [-30, -10, 20, 30, 30, 20, -10, -30], + [-30, -10, 30, 40, 40, 30, -10, -30], + [-30, -10, 30, 40, 40, 30, -10, -30], + [-30, -10, 20, 30, 30, 20, -10, -30], + [-30, -20, -10, 0, 0, -10, -20, -30], + [-50, -40, -30, -20, -20, -30, -40, -50] + ] + + + + + + +def get_position_score(start, board): + """Evaluate the position of a piece on the board.""" + global game_phase + piece = board[start] + if piece == 'P': + return Heuristics.PAWN_TABLE[start[0]][start[1]] + if piece == 'p': + return -Heuristics.PAWN_TABLE[7 - start[0]][start[1]] + if piece == 'N': + return Heuristics.KNIGHT_TABLE[start[0]][start[1]] + if piece == 'n': + return -Heuristics.KNIGHT_TABLE[7 - start[0]][start[1]] + if piece == 'B': + return Heuristics.BISHOP_TABLE[start[0]][start[1]] + if piece == 'b': + return -Heuristics.BISHOP_TABLE[7 - start[0]][start[1]] + if piece == 'R': + return Heuristics.ROOK_TABLE[start[0]][start[1]] + if piece == 'r': + return -Heuristics.ROOK_TABLE[7 - start[0]][start[1]] + if piece == 'Q': + return Heuristics.QUEEN_TABLE[start[0]][start[1]] + if piece == 'q': + return -Heuristics.QUEEN_TABLE[7 - start[0]][start[1]] + if piece == 'K': + if game_phase < 45: + return Heuristics.KING_TABLE_EG[start[0]][start[1]] + else: + return Heuristics.KING_TABLE_MG[start[0]][start[1]] + if piece == 'k': + if game_phase < 45: + return -Heuristics.KING_TABLE_EG[7 - start[0]][start[1]] + else: + return -Heuristics.KING_TABLE_MG[7 - start[0]][start[1]] + + return 0 + + +def get_king_safety(start, board): + """King is safe if protected by a wall of pawns""" + x, y = start + piece = board[x, y] + if piece == "K": + king_safety = (board[max(x-1, 0), y+1] == "P") + (board[x, y+1] == "P") + (board[min(x+1, 7), y+1] == "P") + elif piece == "k": + king_safety = (board[max(x-1, 0), y-1] == "p") + (board[x, y-1] == "p") + (board[min(x+1, 7), y-1] == "p") + king_safety = -king_safety + else: + king_safety = 0 + return king_safety + +def get_pawn_defend(start, board): + """A good pawn is a pawn that defend another piece""" + x, y = start + piece = board[x, y] + if piece == "P" and y+1<=7: + if x-1 >= 0: + if x+1<=7: + pawn_defend = board[x-1, y+1].isupper() + board[x+1, y+1].isupper() + else: + pawn_defend = board[x-1, y+1].isupper() + else: + pawn_defend = board[x+1, y+1].isupper() + + elif piece == "p" and y-1>=0: + if x-1 >= 0: + if x+1<=7: + pawn_defend = board[x-1, y-1].islower() + board[x+1, y-1].islower() + else: + pawn_defend = board[x-1, y-1].islower() + else: + pawn_defend = board[x+1, y-1].islower() + + else: + pawn_defend = 0 + + return pawn_defend + +def get_rook_score(start, board): + """A good rook is a rook that can move in a straight line""" + x, y = start + piece = board[x, y] + rook_score = 0 + if piece == "R" or piece == "r": + for i in range(1, 8): + if x-i >= 0: + if board[x-i, y] == ".": + rook_score += 1 + else: + break + else: + break + for i in range(1, 8): + if x+i <= 7: + if board[x+i, y] == ".": + rook_score += 1 + else: + break + else: + break + for i in range(1, 8): + if y-i >= 0: + if board[x, y-i] == ".": + rook_score += 2 + else: + break + else: + break + for i in range(1, 8): + if y+i <= 7: + if board[x, y+i] == ".": + rook_score += 2 + else: + break + else: + break + return rook_score + + + +def get_other_eval(start, board): + """Evaluation not based on the conventional chess pieces values and on the position""" + king_safety = get_king_safety(start, board) + pawn_defend = get_pawn_defend(start, board) + rook_score = get_rook_score(start, board) + + return king_safety*10 + pawn_defend*10 + rook_score*2 + + +def leaf_eval(board): + """Evaluate a certain board position""" + global count + count += 1 + #evalute using score_table, position score and other eval + evaluate = lambda x,y: score_table[board[x, y]] * 100 + get_position_score((x, y), board) + get_other_eval((x, y), board) + return sum(evaluate(x, y) for y in range(8) for x in range(8)) + random.random()*10 + +game_phase = 0 +def get_game_phase(board): + global game_phase + game_phase = sum(abs(score_table[board[x, y]]) for y in range(8) for x in range(8)) - 2 * score_table['K'] + + +def minmax(node, depth, alpha=-inf, beta=inf): + is_maximizing = node.color == 'white' + if depth == 0 or node.is_leaf: + return node.score + if is_maximizing: + score = -inf + for child in node.children: + score = max(score, minmax(child, depth - 1, alpha, beta)) + alpha = max(alpha, score) + if beta <= alpha: + break + return score + else: + score = inf + for child in node.children: + score = min(score, minmax(child, depth - 1, alpha, beta)) + beta = min(beta, score) + if beta <= alpha: + break + return score + +def evaluate_move(start, end, board): + """Evaluate a move. Just used in "create_and_evaluate_tree" to sort the moves to optimize alpha-beta pruning""" + position_score = get_position_score(start, board) + take_score = score_table[board[end]]*100 + return position_score + take_score + + +class Node: + __slots__ = 'board', 'move', 'parent', 'children', 'score', 'depth', 'color', 'board_score' + def __init__(self, board, move, parent, children, score, depth, color): + self.board = board + self.move = move + self.parent = parent + self.children = children + self.score = score + self.depth = depth + self.color = color + + @property + def is_leaf(self): + return not self.children + + @property + def is_root(self): + return self.parent is None + + def evalute_tree(self): + if self.is_leaf: + self.score = leaf_eval(self.board) + else: + for child in self.children: + child.evalute_tree() + self.score = minmax(self, self.depth) + + + def create_and_evaluate_tree(self, depth, alpha=-inf, beta=inf): + if depth == 0: + self.score = leaf_eval(self.board) + else: + movable_pieces = ((x, y) for x in range(8) for y in range(8) if + self.board[x, y] != '.' and self.color == 'white' and self.board[x, y].isupper() or self.color == 'black' and self.board[x, y].islower()) + for piece in movable_pieces: + possible_moves = get_possible_moves(piece, self.board) + possible_moves.sort(key=lambda x: abs(evaluate_move(piece, x, self.board)), reverse=True) + #possible_moves.sort(key=lambda x: evaluate_move(piece, x, self.board), reverse= self.color == "white") + for move in possible_moves: + new_board = move_piece(piece, move, self.board) + new_move = (piece, move) + new_node = Node(new_board, new_move, self, [], 0, self.depth - 1, 'white' if self.color == 'black' else 'black') + self.children.append(new_node) + if self.color == 'white': + alpha = max(alpha, new_node.create_and_evaluate_tree(depth - 1, alpha, beta)) + if beta <= alpha: + break + #To stop exploring if one king is taken + if abs(self.score) > 10**4: + break + else: + beta = min(beta, new_node.create_and_evaluate_tree(depth - 1, alpha, beta)) + if beta <= alpha: + break + if abs(self.score) > 10**4: + break + if not self.children: + self.score = leaf_eval(self.board) + else: + self.score = max(child.score for child in self.children if child.score is not None) if self.color == 'white' else min(child.score for child in self.children if child.score is not None) + return self.score + + + def get_best_move(self): + self.evalute_tree() + maximazing = self.color == 'white' + if maximazing: + maxi = max(self.children, key=lambda x: x.score) + print(f"maxi: {maxi.score}") + return maxi.move + else: + mini = min(self.children, key=lambda x: x.score) + print(f"mini: {mini.score}") + return mini.move + + def pretty(self): + string = f"Node: {self.move} {self.score} {self.depth} {self.color} \n" + for child in self.children: + string += "| " + child.pretty() + "\n" + return string + + def __repr__(self): + return self.pretty() + + +get_deeper = 0 + +def get_best_move(board, color, depth): + global count, get_deeper + get_game_phase(board) + print(f"End game: {game_phase}") + depth += get_deeper + print(f"curent depth: {depth}") + a = time.time() + root = Node(board, None, None, [], 0, 0, color) + root.create_and_evaluate_tree(depth) + best_move = root.get_best_move() + exec_time = time.time() - a + print(f"Tree evaluated in {time.time() - a} seconds") + print(f"Number of leafs: {count}") + + if exec_time < 1.5: + get_deeper += 1 + print("Getting deeper, current depth: ", depth) + elif exec_time > 20: + get_deeper += -1 + print("Getting shallower, current depth: ", depth) + + print(f"Mean number of children : {count**(1/depth)}") + count = 0 + + + return best_move + +if __name__ == '__main__': + board = init_board() + b = get_best_move(board, 'white', 2) + print(b) + + + + + + + + + + + + + + + + + + + + + + diff --git a/chess_interface.py b/chess_interface.py new file mode 100644 index 0000000..97495c1 --- /dev/null +++ b/chess_interface.py @@ -0,0 +1,182 @@ +from tkinter import * +from chess_tools import * +from chess_ai import get_best_move +from PIL import Image, ImageTk, ImageDraw +import time + +pieces = {"P": "♙", "R": "♖", "N": "♘", "B": "♗", "Q": "♕", "K": "♔", "p": "♟", "r": "♜", "n": "♞", "b": "♝", "q": "♛", "k": "♚", '':'', '.':'', '\x00': ' '} + + +MODE = "HUMAN VS AI" +COLOR = "BLACK" + + +def create_circle(x, y, r, canvas, fill="yellow"): #center coordinates, radius + x0 = x - r + y0 = y - r + x1 = x + r + y1 = y + r + return canvas.create_oval(x0, y0, x1, y1, fill=fill) + + +class ChessInterface: + + __slots__ = 'root', 'canvas', 'size', 'square_size', 'selected', 'chess_board', 'count' + def __init__(self, chess_board): + self.root = Tk() + self.root.title("Chess") + self.canvas = Canvas(self.root, width=600, height=600, bg="white") + self.size = 600 // 8 * 8 + self.square_size = self.size // 8 + self.selected = None + self.chess_board = chess_board + self.count = 0 + + def ai_turn(color_ai): + print(f"AI turn {color_ai}d") + move = get_best_move(self.chess_board, color_ai, 3) + print(f"AI move: {move}") + self.chess_board = move_piece(move[0], move[1], self.chess_board) + self.canvas.delete("all") + self.draw_all(self.chess_board) + self.count += 1 + self.selected = None + + + def on_click_human_vs_ai_black(event): + """AI play black""" + global color + x = event.x // self.square_size + y = event.y // self.square_size + self.canvas.delete("all") + + if self.selected is None: + piece = self.chess_board[7-y, x] + print(piece) + color = "white" if piece.isupper() else "black" + self.selected = (7-y, x) + else: + color_turn = "white" if self.count % 2 == 0 else "black" + print(color, color_turn) + if color == color_turn == "white": + print("Player turn") + if (7-y, x) in get_possible_moves(self.selected, self.chess_board): + self.count += 1 + move = (self.selected, (7-y, x)) + print(f"Player move: {move}") + self.chess_board = move_piece(move[0], move[1], self.chess_board) + self.draw_all(self.chess_board) + self.root.update() + + # AI turn + ai_turn("black") + + self.selected = None + self.draw_all(self.chess_board) + + first_round = True + def on_click_human_vs_ai_white(event): + """AI play white""" + global color + nonlocal first_round + + if first_round: + # AI turn + print("AI turn") + move = get_best_move(self.chess_board, "white", 3) + self.canvas.delete("all") + self.draw_all(self.chess_board) + print(f"AI move: {move}") + self.chess_board = move_piece(move[0], move[1], self.chess_board) + self.count += 1 + self.selected = None + first_round = False + + x = event.x // self.square_size + y = event.y // self.square_size + self.canvas.delete("all") + + if self.selected is None: + piece = self.chess_board[7-y, x] + print(piece) + color = "white" if piece.isupper() else "black" + self.selected = (7-y, x) + else: + color_turn = "white" if self.count % 2 == 0 else "black" + print(color, color_turn) + if color == color_turn == "black": + print("Player turn") + if (7-y, x) in get_possible_moves(self.selected, self.chess_board): + self.count += 1 + move = (self.selected, (7-y, x)) + print(f"Player move: {move}") + self.chess_board = move_piece(move[0], move[1], self.chess_board) + self.draw_all(self.chess_board) + self.root.update() + + # AI turn + ai_turn("white") + + self.selected = None + self.draw_all(self.chess_board) + + def on_click_ai_vs_ai(event): + """start AI vs AI when one click on the board""" + while True: + ai_turn("white") + self.root.update() + time.sleep(0.5) + ai_turn("black") + self.root.update() + time.sleep(0.5) + self.canvas.bind("", on_click_ai_vs_ai) + + game_modes = [on_click_human_vs_ai_black, on_click_human_vs_ai_white, on_click_ai_vs_ai] + self.canvas.bind("", game_modes[1]) + self.canvas.pack() + self.run() + + + def draw_board(self): + for i in range(8): + for j in range(8): + if (i + j) % 2 == 0: + color = "white" + else: + color = "grey" + self.canvas.create_rectangle(i * self.square_size, j * self.square_size, (i + 1) * self.square_size, (j + 1) * self.square_size, fill=color) + + + def draw_pieces(self, chess_board): + for i in range(8): + for j in range(8): + if chess_board[i, j] != " ": + self.canvas.create_text((j + 0.5) * self.square_size, (7-i + 0.5) * self.square_size, text=pieces[chess_board[i, j]], font=("Arial", 60)) + + + def draw_possible_moves(self, selected, chess_board): + moves = get_possible_moves(selected, chess_board) + if moves: + for move in moves: + #circle + create_circle(move[1] * self.square_size + self.square_size // 2, (7-move[0]) * self.square_size + self.square_size // 2, self.square_size // 8, self.canvas) + + def draw_all(self, chess_board): + self.draw_board() + self.draw_pieces(chess_board) + if self.selected is not None: + self.draw_possible_moves(self.selected, chess_board) + self.canvas.pack() + + def run(self): + self.draw_all(self.chess_board) + self.root.mainloop() + +if __name__ == "__main__": + chess_board = init_board() + print(chess_board) + ChessInterface(chess_board) + + + + diff --git a/chess_tools.py b/chess_tools.py new file mode 100644 index 0000000..de94f28 --- /dev/null +++ b/chess_tools.py @@ -0,0 +1,668 @@ +import typing +from typing import Tuple, List, Iterable, Union +import numpy as np +from numpy import ndarray +import time +from array import array + +# The chess board is an 8x8 array of 1-character strings. uppercase = white, lowercase = black +#chess_board = np.empty((8, 8), dtype='U1') + +def is_in_board(pos: Tuple[int, int]) -> bool: + # Check if a position is in the board. + row, col = pos + return 0 <= row < 8 and 0 <= col < 8 + + +class CharArray2D: + """Fast 2D array of characters. Works like a numpy array of chr, but is much faster and memory efficient with pypy""" + __slots__ = ('width', 'height', 'array') + + def __init__(self, width, height): + self.width = width + self.height = height + self.array = bytearray(width * height) + + def __getitem__(self, index): + y, x = index + return chr(self.array[y * self.width + x]) + + def __setitem__(self, index, value): + y, x = index + self.array[y * self.width + x] = ord(value) + + def copy(self): + new_array = CharArray2D(self.width, self.height) + new_array.array = self.array.copy() + return new_array + + @classmethod + def from_ndarray(cls, array): + self = cls(array.shape[1], array.shape[0]) + for y in range(self.height): + for x in range(self.width): + self[y, x] = array[y, x] + return self + + def __str__(self): + return str(self.array) + + + +def init_board(): + # Initialize the board with the starting positions of the pieces. + chess_board = np.array([['R', 'N', 'B', 'Q', 'K', 'B', 'N', 'R'], + ['P', 'P', 'P', 'P', 'P', 'P', 'P', 'P'], + ['.', '.', '.', '.', '.', '.', '.', '.'], + ['.', '.', '.', '.', '.', '.', '.', '.'], + ['.', '.', '.', '.', '.', '.', '.', '.'], + ['.', '.', '.', '.', '.', '.', '.', '.'], + ['p', 'p', 'p', 'p', 'p', 'p', 'p', 'p'], + ['r', 'n', 'b', 'q', 'k', 'b', 'n', 'r']]) + chess_board = CharArray2D.from_ndarray(np.array(chess_board)) + return chess_board + +def move_piece(start: Tuple[int, int], end: Tuple[int, int], chess_board: ndarray) -> ndarray: + # Move a piece from start to end. + chess_board = chess_board.copy() + + + if chess_board[start] not in 'KkPp': + chess_board[end] = chess_board[start] + chess_board[start] = '.' + return chess_board + + # Check for pawn + if chess_board[start] == "P": + if end[0] == 7: + chess_board[end] = "Q" + chess_board[start] = '.' + return chess_board + #Check for en passant + if start[1] != end[1] and chess_board[end] == '.': + chess_board[end[0], end[1]] = 'P' + chess_board[start[0], start[1]] = '.' + chess_board[end[0] - 1, end[1]] = '.' + return chess_board + + chess_board[end] = chess_board[start] + chess_board[start] = '.' + return chess_board + + elif chess_board[start] == "p": + if end[0] == 0: + chess_board[end] = "q" + chess_board[start] = '.' + return chess_board + #Check for en passant + if start[1] != end[1] and chess_board[end] == '.': + chess_board[end[0], end[1]] = 'p' + chess_board[start[0], start[1]] = '.' + chess_board[end[0] + 1, end[1]] = '.' + return chess_board + chess_board[end] = chess_board[start] + chess_board[start] = '.' + return chess_board + + + # Check for castling. + if chess_board[start] == "K": + if start == (0, 4) and end == (0, 0): + chess_board[0, 2] = "K" + chess_board[0, 3] = "R" + chess_board[0, 0] = '.' + chess_board[0, 1] = '.' + chess_board[0, 4] = '.' + + elif start == (0, 4) and end == (0, 7): + chess_board[0, 5] = "R" + chess_board[0, 6] = "K" + chess_board[0, 7] = '.' + chess_board[0, 4] = '.' + + else: + chess_board[end] = chess_board[start] + chess_board[start] = '.' + return chess_board + + elif chess_board[start] == "k": + if start == (7, 4) and end == (7, 0): + chess_board[7, 2] = "k" + chess_board[7, 3] = "r" + chess_board[7, 0] = '.' + chess_board[7, 1] = '.' + chess_board[7, 4] = '.' + + elif start == (7, 4) and end == (7, 7): + chess_board[7, 4] = '.' + chess_board[7, 5] = "r" + chess_board[7, 6] = "k" + chess_board[7, 7] = '.' + + + else: + chess_board[end] = chess_board[start] + chess_board[start] = '.' + return chess_board + + + return chess_board + + + +def get_moves_pawn_white(start: Tuple[int, int], chess_board: ndarray) -> List[Tuple[int, int]]: + # Get all possible moves for a white pawn at start. + first_move = (start[0] == 1) + moves = [] + + # Move forward if not blocked + if chess_board[start[0] + 1, start[1]] == '.': + moves.append((start[0] + 1, start[1])) + if first_move and chess_board[start[0] + 2, start[1]] == '.': + moves.append((start[0] + 2, start[1])) + + # Capture diagonally. + if start[1] > 0 and chess_board[start[0] + 1, start[1] - 1].islower(): + moves.append((start[0] + 1, start[1] - 1)) + if start[1] < 7 and chess_board[start[0] + 1, start[1] + 1].islower(): + moves.append((start[0] + 1, start[1] + 1)) + + # Check for en passant + if start[0] == 4: + if start[1] > 0 and chess_board[start[0], start[1] - 1] == 'p': + moves.append((start[0] + 1, start[1] - 1)) + if start[1] < 7 and chess_board[start[0], start[1] + 1] == 'p': + moves.append((start[0] + 1, start[1] + 1)) + + return moves + +def get_moves_pawn_black(start: Tuple[int, int], chess_board: ndarray) -> List[Tuple[int, int]]: + # Get all possible moves for a black pawn at start. + first_move = (start[0] == 6) + moves = [] + + # Move forward if not blocked. + if chess_board[start[0] - 1, start[1]] == '.': + moves.append((start[0] - 1, start[1])) + if first_move and chess_board[start[0] - 2, start[1]] == '.': + moves.append((start[0] - 2, start[1])) + + # Capture diagonally. + if start[1] > 0 and chess_board[start[0] - 1, start[1] - 1].isupper(): + moves.append((start[0] - 1, start[1] - 1)) + if start[1] < 7 and chess_board[start[0] - 1, start[1] + 1].isupper(): + moves.append((start[0] - 1, start[1] + 1)) + + # Check for en passant + if start[0] == 3: + if start[1] > 0 and chess_board[start[0], start[1] - 1] == 'P': + moves.append((start[0] - 1, start[1] - 1)) + if start[1] < 7 and chess_board[start[0], start[1] + 1] == 'P': + moves.append((start[0] - 1, start[1] + 1)) + + return moves + +def get_moves_rook_white(start: Tuple[int, int], chess_board: ndarray) -> List[Tuple[int, int]]: + # Get all possible moves for a white rook at start. + moves = [] + + # Move up. + for i in range(start[0] + 1, 8): + if chess_board[i, start[1]] == '.': + moves.append((i, start[1])) + elif chess_board[i, start[1]].islower(): + moves.append((i, start[1])) + break + else: + break + + # Move down. + for i in range(start[0] - 1, -1, -1): + if chess_board[i, start[1]] == '.': + moves.append((i, start[1])) + elif chess_board[i, start[1]].islower(): + moves.append((i, start[1])) + break + else: + break + + # Move right. + for i in range(start[1] + 1, 8): + if chess_board[start[0], i] == '.': + moves.append((start[0], i)) + elif chess_board[start[0], i].islower(): + moves.append((start[0], i)) + break + else: + break + + # Move left. + for i in range(start[1] - 1, -1, -1): + if chess_board[start[0], i] == '.': + moves.append((start[0], i)) + elif chess_board[start[0], i].islower(): + moves.append((start[0], i)) + break + else: + break + + return moves + +def get_moves_rook_black(start: Tuple[int, int], chess_board: ndarray) -> List[Tuple[int, int]]: + # Get all possible moves for a black rook at start. + moves = [] + + # Move up. + for i in range(start[0] + 1, 8): + if chess_board[i, start[1]] == '.': + moves.append((i, start[1])) + elif chess_board[i, start[1]].isupper(): + moves.append((i, start[1])) + break + else: + break + + # Move down. + for i in range(start[0] - 1, -1, -1): + if chess_board[i, start[1]] == '.': + moves.append((i, start[1])) + elif chess_board[i, start[1]].isupper(): + moves.append((i, start[1])) + break + else: + break + + # Move right. + for i in range(start[1] + 1, 8): + if chess_board[start[0], i] == '.': + moves.append((start[0], i)) + elif chess_board[start[0], i].isupper(): + moves.append((start[0], i)) + break + else: + break + + # Move left. + for i in range(start[1] - 1, -1, -1): + if chess_board[start[0], i] == '.': + moves.append((start[0], i)) + elif chess_board[start[0], i].isupper(): + moves.append((start[0], i)) + break + else: + break + + return moves + +def get_moves_knight_white(start: Tuple[int, int], chess_board: ndarray) -> List[Tuple[int, int]]: + # Get all possible moves for a white knight at start. + moves = [] + verify = lambda move: is_in_board(move) and (chess_board[move].islower() or chess_board[move] == '.') + + # Move up right. + move = (start[0] + 2, start[1] + 1) + if verify(move): + moves.append(move) + + # Move up left. + move = (start[0] + 2, start[1] - 1) + if verify(move): + moves.append(move) + + # Move down right. + move = (start[0] - 2, start[1] + 1) + if verify(move): + moves.append(move) + + # Move down left. + move = (start[0] - 2, start[1] - 1) + if verify(move): + moves.append(move) + + # Move right up. + move = (start[0] + 1, start[1] + 2) + if verify(move): + moves.append(move) + + # Move right down. + move = (start[0] - 1, start[1] + 2) + if verify(move): + moves.append(move) + + # Move left up. + move = (start[0] + 1, start[1] - 2) + if verify(move): + moves.append(move) + + # Move left down. + move = (start[0] - 1, start[1] - 2) + if verify(move): + moves.append(move) + + return moves + +def get_moves_knight_black(start: Tuple[int, int], chess_board: ndarray) -> List[Tuple[int, int]]: + # Get all possible moves for a black knight at start. + moves = [] + verify = lambda move: is_in_board(move) and (chess_board[move].isupper() or chess_board[move] == '.') + + # Move up right. + move = (start[0] + 2, start[1] + 1) + if verify(move): + moves.append(move) + + # Move up left. + move = (start[0] + 2, start[1] - 1) + if verify(move): + moves.append(move) + + # Move down right. + move = (start[0] - 2, start[1] + 1) + if verify(move): + moves.append(move) + + # Move down left. + move = (start[0] - 2, start[1] - 1) + if verify(move): + moves.append(move) + + # Move right up. + move = (start[0] + 1, start[1] + 2) + if verify(move): + moves.append(move) + + # Move right down. + move = (start[0] - 1, start[1] + 2) + if verify(move): + moves.append(move) + + # Move left up. + move = (start[0] + 1, start[1] - 2) + if verify(move): + moves.append(move) + + # Move left down. + move = (start[0] - 1, start[1] - 2) + if verify(move): + moves.append(move) + + return moves + + +def get_moves_bishop_white(start: Tuple[int, int], chess_board: ndarray) -> List[Tuple[int, int]]: + # Get all possible moves for a white bishop at start. + moves = [] + + # Move up and right. + for i in range(1, 8): + if start[0] + i > 7 or start[1] + i > 7: + break + if chess_board[start[0] + i, start[1] + i] == '.': + moves.append((start[0] + i, start[1] + i)) + elif chess_board[start[0] + i, start[1] + i].islower(): + moves.append((start[0] + i, start[1] + i)) + break + else: + break + + # Move up and left. + for i in range(1, 8): + if start[0] + i > 7 or start[1] - i < 0: + break + if chess_board[start[0] + i, start[1] - i] == '.': + moves.append((start[0] + i, start[1] - i)) + elif chess_board[start[0] + i, start[1] - i].islower(): + moves.append((start[0] + i, start[1] - i)) + break + else: + break + + # Move down and right. + for i in range(1, 8): + if start[0] - i < 0 or start[1] + i > 7: + break + if chess_board[start[0] - i, start[1] + i] == '.': + moves.append((start[0] - i, start[1] + i)) + elif chess_board[start[0] - i, start[1] + i].islower(): + moves.append((start[0] - i, start[1] + i)) + break + else: + break + + # Move down and left. + for i in range(1, 8): + if start[0] - i < 0 or start[1] - i < 0: + break + if chess_board[start[0] - i, start[1] - i] == '.': + moves.append((start[0] - i, start[1] - i)) + elif chess_board[start[0] - i, start[1] - i].islower(): + moves.append((start[0] - i, start[1] - i)) + break + else: + break + + return moves + +def get_moves_bishop_black(start: Tuple[int, int], chess_board: ndarray) -> List[Tuple[int, int]]: + # Get all possible moves for a black bishop at start. + moves = [] + + # Move up and right. + for i in range(1, 8): + if start[0] + i > 7 or start[1] + i > 7: + break + if chess_board[start[0] + i, start[1] + i] == '.': + moves.append((start[0] + i, start[1] + i)) + elif chess_board[start[0] + i, start[1] + i].isupper(): + moves.append((start[0] + i, start[1] + i)) + break + else: + break + + # Move up and left. + for i in range(1, 8): + if start[0] + i > 7 or start[1] - i < 0: + break + if chess_board[start[0] + i, start[1] - i] == '.': + moves.append((start[0] + i, start[1] - i)) + elif chess_board[start[0] + i, start[1] - i].isupper(): + moves.append((start[0] + i, start[1] - i)) + break + else: + break + + # Move down and right. + for i in range(1, 8): + if start[0] - i < 0 or start[1] + i > 7: + break + if chess_board[start[0] - i, start[1] + i] == '.': + moves.append((start[0] - i, start[1] + i)) + elif chess_board[start[0] - i, start[1] + i].isupper(): + moves.append((start[0] - i, start[1] + i)) + break + else: + break + + # Move down and left. + for i in range(1, 8): + if start[0] - i < 0 or start[1] - i < 0: + break + if chess_board[start[0] - i, start[1] - i] == '.': + moves.append((start[0] - i, start[1] - i)) + elif chess_board[start[0] - i, start[1] - i].isupper(): + moves.append((start[0] - i, start[1] - i)) + break + else: + break + + return moves + +def get_moves_queen_white(start: Tuple[int, int], chess_board: ndarray) -> List[Tuple[int, int]]: + # Get all possible moves for a white queen at start. + return get_moves_bishop_white(start, chess_board) + get_moves_rook_white(start, chess_board) + +def get_moves_queen_black(start: Tuple[int, int], chess_board: ndarray) -> List[Tuple[int, int]]: + # Get all possible moves for a black queen at start. + return get_moves_bishop_black(start, chess_board) + get_moves_rook_black(start, chess_board) + +def get_moves_king_white(start: Tuple[int, int], chess_board: ndarray) -> List[Tuple[int, int]]: + # Get all possible moves for a black king at start. + moves = [] + # Move up if possible. + if start[0] < 7: + if chess_board[start[0] + 1, start[1]] == '.' or chess_board[start[0] + 1, start[1]].islower(): + moves.append((start[0] + 1, start[1])) + + # Move down. + if start[0] > 0: + if chess_board[start[0] - 1, start[1]] == '.' or chess_board[start[0] - 1, start[1]].islower(): + moves.append((start[0] - 1, start[1])) + + # Move right. + if start[1] < 7: + if chess_board[start[0], start[1] + 1] == '.' or chess_board[start[0], start[1] + 1].islower(): + moves.append((start[0], start[1] + 1)) + + # Move left. + if start[1] > 0: + if chess_board[start[0], start[1] - 1] == '.' or chess_board[start[0], start[1] - 1].islower(): + moves.append((start[0], start[1] - 1)) + + # Move up and right. + if start[0] < 7: + if start[1] < 7: + if chess_board[start[0] + 1, start[1] + 1] == '.' or chess_board[start[0] + 1, start[1] + 1].islower(): + moves.append((start[0] + 1, start[1] + 1)) + + # Move up and left. + if start[0] < 7: + if start[1] > 0: + if chess_board[start[0] + 1, start[1] - 1] == '.' or chess_board[start[0] + 1, start[1] - 1].islower(): + moves.append((start[0] + 1, start[1] - 1)) + + # Move down and right. + if start[0] > 0: + if start[1] < 7: + if chess_board[start[0] - 1, start[1] + 1] == '.' or chess_board[start[0] - 1, start[1] + 1].islower(): + moves.append((start[0] - 1, start[1] + 1)) + + # Move down and left. + if start[0] > 0: + if start[1] > 0: + if chess_board[start[0] - 1, start[1] - 1] == '.' or chess_board[start[0] - 1, start[1] - 1].islower(): + moves.append((start[0] - 1, start[1] - 1)) + + # Castling. + if start == (0, 4): + # White king side. + if chess_board[0, 5] == '.' and chess_board[0, 6] == '.' and chess_board[0, 7] == 'R': + moves.append((0, 7)) + + # White queen side. + if chess_board[0, 3] == '.' and chess_board[0, 2] == '.' and chess_board[0, 1] == '.' and chess_board[0, 0] == 'R': + moves.append((0, 0)) + + + return moves + +def get_moves_king_black(start: Tuple[int, int], chess_board: ndarray) -> List[Tuple[int, int]]: + # Get all possible moves for a black king at start. + moves = [] + # Move up if possible. + if start[0] < 7: + if chess_board[start[0] + 1, start[1]] == '.' or chess_board[start[0] + 1, start[1]].isupper(): + moves.append((start[0] + 1, start[1])) + + # Move down. + if start[0] > 0: + if chess_board[start[0] - 1, start[1]] == '.' or chess_board[start[0] - 1, start[1]].isupper(): + moves.append((start[0] - 1, start[1])) + + # Move right. + if start[1] < 7: + if chess_board[start[0], start[1] + 1] == '.' or chess_board[start[0], start[1] + 1].isupper(): + moves.append((start[0], start[1] + 1)) + + # Move left. + if start[1] > 0: + if chess_board[start[0], start[1] - 1] == '.' or chess_board[start[0], start[1] - 1].isupper(): + moves.append((start[0], start[1] - 1)) + + # Move up and right. + if start[0] < 7: + if start[1] < 7: + if chess_board[start[0] + 1, start[1] + 1] == '.' or chess_board[start[0] + 1, start[1] + 1].isupper(): + moves.append((start[0] + 1, start[1] + 1)) + + # Move up and left. + if start[0] < 7: + if start[1] > 0: + if chess_board[start[0] + 1, start[1] - 1] == '.' or chess_board[start[0] + 1, start[1] - 1].isupper(): + moves.append((start[0] + 1, start[1] - 1)) + + # Move down and right. + if start[0] > 0: + if start[1] < 7: + if chess_board[start[0] - 1, start[1] + 1] == '.' or chess_board[start[0] - 1, start[1] + 1].isupper(): + moves.append((start[0] - 1, start[1] + 1)) + + # Move down and left. + if start[0] > 0: + if start[1] > 0: + if chess_board[start[0] - 1, start[1] - 1] == '.' or chess_board[start[0] - 1, start[1] - 1].isupper(): + moves.append((start[0] - 1, start[1] - 1)) + + # Castling. + if start == (7, 4): + # Castling kingside. + if chess_board[7, 5] == '.' and chess_board[7, 6] == '.' and chess_board[7, 7] == 'r': + moves.append((7, 7)) + # Castling queenside. + if chess_board[7, 3] == '.' and chess_board[7, 2] == '.' and chess_board[7, 1] == '.' and chess_board[7, 0] == 'r': + moves.append((7, 0)) + + + return moves + +def get_possible_moves(start: Tuple[int, int], chess_board: ndarray) -> List[Tuple[int, int]]: + # Get all possible moves for a piece at start. + piece = chess_board[start] + + if piece == 'P': + return get_moves_pawn_white(start, chess_board) + elif piece == 'p': + return get_moves_pawn_black(start, chess_board) + elif piece == 'R': + return get_moves_rook_white(start, chess_board) + elif piece == 'r': + return get_moves_rook_black(start, chess_board) + elif piece == 'N': + return get_moves_knight_white(start, chess_board) + elif piece == 'n': + return get_moves_knight_black(start, chess_board) + elif piece == 'B': + return get_moves_bishop_white(start, chess_board) + elif piece == 'b': + return get_moves_bishop_black(start, chess_board) + elif piece == 'Q': + return get_moves_queen_white(start, chess_board) + elif piece == 'q': + return get_moves_queen_black(start, chess_board) + elif piece == 'K': + return get_moves_king_white(start, chess_board) + elif piece == 'k': + return get_moves_king_black(start, chess_board) + + return [] + + + +def get_time(func): + def wrapper(*args, **kwargs): + start = time.perf_counter() + result = func(*args, **kwargs) + print(f"{func.__name__} took {time.perf_counter() - start} seconds") + return result + + return wrapper +