import chess
import chess.engine
import numpy as np
from functools import lru_cache

class ChessBot:
    def __init__(self, depth=5, nn_evaluator=None):
        """
        Initialize the chess bot.

        Parameters:
        - depth: Search depth for alpha-beta pruning.
        - nn_evaluator: Lightweight neural network for evaluation (optional).
        """
        self.depth = depth
        self.nn_evaluator = nn_evaluator  # Neural network for evaluation

        # Piece-square tables for heuristic evaluation
        self.piece_square_tables = self._generate_piece_square_tables()

    def _generate_piece_square_tables(self):
        """
        Generate piece-square tables to guide positional evaluation.
        """
        return {
            chess.PAWN: np.array([
                0,  5,  5,  0,  5, 10, 50,  0,
                0, 10, -5,  0,  5, 10, 50,  0,
                0, 10, -10, 0, 10, 20, 50,  0,
                0,  0,   0, 20, 25, 30, 50,  0,
                0,  0,   0, 20, 25, 30, 50,  0,
                0, 10, -10, 0, 10, 20, 50,  0,
                0, 10, -5,  0,  5, 10, 50,  0,
                0,  5,  5,  0,  5, 10, 50,  0
            ]).reshape(8, 8),
            chess.KNIGHT: np.array([
                -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,   5, -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
            ]).reshape(8, 8),
            # Add tables for BISHOP, ROOK, QUEEN, KING...
        }

    def evaluate_board(self, board):
        """
        Evaluate the board position using heuristics and an optional NN evaluator.
        """
        if self.nn_evaluator:
            # Use the neural network for evaluation if available
            return self.nn_evaluator.predict(board)

        # Fall back to heuristic evaluation
        return self.heuristic_evaluation(board)

    def heuristic_evaluation(self, board):
        """
        Perform heuristic evaluation using material balance and piece-square tables.
        """
        score = 0
        for piece_type in self.piece_square_tables:
            for square in board.pieces(piece_type, chess.WHITE):
                score += self.piece_square_tables[piece_type].flat[square]
            for square in board.pieces(piece_type, chess.BLACK):
                score -= self.piece_square_tables[piece_type].flat[chess.square_mirror(square)]

        # Add material balance
        score += sum(
            (len(board.pieces(piece, chess.WHITE)) - len(board.pieces(piece, chess.BLACK))) * piece_value
            for piece, piece_value in zip(
                [chess.PAWN, chess.KNIGHT, chess.BISHOP, chess.ROOK, chess.QUEEN],
                [100, 320, 330, 500, 900]
            )
        )
        
        # Additional heuristics (e.g., king safety, pawn structure, etc.)
        score += self.king_safety(board)
        return score

    def king_safety(self, board):
        """
        Evaluate king safety as part of the heuristic.
        """
        king_safety_score = 0
        for color in [chess.WHITE, chess.BLACK]:
            king_square = board.king(color)
            if king_square:
                king_safety_score += (len(board.attackers(not color, king_square)) * -20)
        return king_safety_score

    @lru_cache(None)
    def alphabeta(self, board, depth, alpha, beta, is_maximizing):
        """
        Perform alpha-beta pruning to find the best move.
        """
        if depth == 0 or board.is_game_over():
            return self.evaluate_board(board)

        if is_maximizing:
            max_eval = -float("inf")
            for move in self.order_moves(board):
                board.push(move)
                eval = self.alphabeta(board, depth - 1, alpha, beta, False)
                board.pop()
                max_eval = max(max_eval, eval)
                alpha = max(alpha, eval)
                if beta <= alpha:
                    break
            return max_eval
        else:
            min_eval = float("inf")
            for move in self.order_moves(board):
                board.push(move)
                eval = self.alphabeta(board, depth - 1, alpha, beta, True)
                board.pop()
                min_eval = min(min_eval, eval)
                beta = min(beta, eval)
                if beta <= alpha:
                    break
            return min_eval

    def order_moves(self, board):
        """
        Order moves using heuristics (e.g., captures, checks first).
        """
        def move_priority(move):
            score = 0
            if board.is_capture(move):
                score += 10 * (board.piece_at(move.to_square).piece_type if board.piece_at(move.to_square) else 1)
            if board.gives_check(move):
                score += 5
            return score

        return sorted(board.legal_moves, key=move_priority, reverse=True)

    def get_best_move(self, board):
        """
        Find the best move for the current position.
        """
        best_move = None
        best_value = -float("inf")
        for move in self.order_moves(board):
            board.push(move)
            move_value = self.alphabeta(board, self.depth - 1, -float("inf"), float("inf"), False)
            board.pop()
            if move_value > best_value:
                best_value = move_value
                best_move = move
        return best_move

def chess_bot(obs):
    """
    Simple chess bot interface for making moves based on a FEN string.

    Args:
        obs: An object with a 'board' attribute representing the current board state as a FEN string.

    Returns:
        A string representing the chosen move in UCI notation (e.g., "e2e4")
    """
    board = chess.Board(obs['board'])
    bot = ChessBot(depth=5)  # Increased depth for stronger performance
    best_move = bot.get_best_move(board)
    return best_move.uci()

add tables for the other pieces. also make the alpha beta pruning algorithm more sophisticated by incorporating modern techniques so that the chess bot could defeat an elite playe while running on multiple cpu cores. also fix any possible issues with the code