Try to debug and fix this persistent attributes/object error:

Traceback (most recent call last):
File "C:\Users\hp\Downloads\AIPes.py", line 745, in
play_game()
File "C:\Users\hp\Downloads\AIPes.py", line 717, in play_game
move = ai.get_best_move(board, max_depth=3, time_limit=2.0)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\Users\hp\Downloads\AIPes.py", line 703, in get_best_move
return self.iterative_deepening_search(board, max_depth, time_limit)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\Users\hp\Downloads\AIPes.py", line 575, in iterative_deepening_search
score, move, reasoning = self.negamax_pvs(board, depth, -math.inf, math.inf, start_time, time_limit + start_time, current_depth=depth)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\Users\hp\Downloads\AIPes.py", line 631, in negamax_pvs
score, _, reasoning = self.negamax_pvs(board, reduced_depth, -beta, -alpha, start_time, time_limit, current_depth + 1)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\Users\hp\Downloads\AIPes.py", line 607, in negamax_pvs
eval_score, reasoning = self.evaluator.quiescence_search_with_reason(board, alpha, beta, start_time, time_limit)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
AttributeError: 'AdvancedSuperpositionEvaluator' object has no attribute 'quiescence_search_with_reason'

```
import chess
import chess.polyglot
import time
import math
from collections import defaultdict
from typing import List, Tuple, Optional
import g4f
import re
import json
import os


# ================== TranspositionTableEntry Class ==================
class TranspositionTableEntry:
    def __init__(self, depth: int, score: float, flag: str, move: Optional[chess.Move]):
        self.depth = depth
        self.score = score
        self.flag = flag  # 'EXACT', 'LOWERBOUND', 'UPPERBOUND'
        self.move = move


# ================== AdvancedSuperpositionEvaluator Class ==================
class AdvancedSuperpositionEvaluator:
    def __init__(self, learning_file: str = "learning_data.json"):
        # Initial weights and piece values
        self.weights = {
            'material': 1.0,
            'pawn_structure': 0.5,
            'king_safety': 1.0,
            'mobility': 0.3,
            'center_control': 0.7,
            'piece_activity': 0.5,
            'gpt_analysis': 0.2
        }

        self.piece_values = {
            chess.PAWN: 100,
            chess.KNIGHT: 320,
            chess.BISHOP: 330,
            chess.ROOK: 500,
            chess.QUEEN: 900,
            chess.KING: 20000
        }

        self.center_squares = [chess.D4, chess.E4, chess.D5, chess.E5]
        self.learning_rate = 0.01
        self.history: List[Tuple[dict, float]] = []
        self.learning_file = learning_file

        # Load learning data if available
        self.load_learning_data()

        # Initialize GPT4Free client
        # Note: We'll use g4f.ChatCompletion.create() directly in the method, so no need to initialize here
        # self.gpt4_client = g4f.ChatCompletion.create(...) # Removed to fix the error

    def load_learning_data(self):
        """
        Load weights and piece values from a JSON file to persist learning across sessions.
        """
        if os.path.exists(self.learning_file):
            try:
                with open(self.learning_file, 'r') as f:
                    data = json.load(f)
                    self.weights.update(data.get('weights', {}))
                    # Convert piece values keys from string to int
                    piece_values = data.get('piece_values', {})
                    self.piece_values = {int(k): v for k, v in piece_values.items()}
                print("Learning data loaded successfully.")
            except Exception as e:
                print(f"Error loading learning data: {e}")
        else:
            print("No existing learning data found. Starting fresh.")

    def save_learning_data(self):
        """
        Save weights and piece values to a JSON file to persist learning across sessions.
        """
        try:
            data = {
                'weights': self.weights,
                'piece_values': {str(k): v for k, v in self.piece_values.items()}
            }
            with open(self.learning_file, 'w') as f:
                json.dump(data, f, indent=4)
            print("Learning data saved successfully.")
        except Exception as e:
            print(f"Error saving learning data: {e}")

    def evaluate_with_reason(self, board: chess.Board) -> Tuple[float, str]:
        """
        Evaluate the board and provide reasoning.
        Returns a tuple of (evaluation score, reasoning string).
        """
        eval_score, reasoning = self.evaluate(board)
        return eval_score, reasoning

    def evaluate(self, board: chess.Board) -> Tuple[float, str]:
        # Basic evaluation (checking for terminal states)
        if board.is_checkmate():
            eval_score = -math.inf if board.turn else math.inf
            reasoning = "Checkmate detected."
            return (eval_score, reasoning)
        if board.is_stalemate() or board.is_insufficient_material():
            eval_score = 0
            reasoning = "Stalemate or insufficient material detected."
            return (eval_score, reasoning)

        evaluation = {}
        reasoning_steps = []

        evaluation['material'], material_reason = self._evaluate_material(board)
        reasoning_steps.append(f"Material Evaluation: {material_reason}")

        evaluation['pawn_structure'], pawn_structure_reason = self._evaluate_pawn_structure(board)
        reasoning_steps.append(f"Pawn Structure Evaluation: {pawn_structure_reason}")

        evaluation['king_safety'], king_safety_reason = self._evaluate_king_safety(board)
        reasoning_steps.append(f"King Safety Evaluation: {king_safety_reason}")

        evaluation['mobility'], mobility_reason = self._evaluate_mobility(board)
        reasoning_steps.append(f"Mobility Evaluation: {mobility_reason}")

        evaluation['center_control'], center_control_reason = self._evaluate_center_control(board)
        reasoning_steps.append(f"Center Control Evaluation: {center_control_reason}")

        evaluation['piece_activity'], piece_activity_reason = self._evaluate_piece_activity(board)
        reasoning_steps.append(f"Piece Activity Evaluation: {piece_activity_reason}")

        # Incorporate GPT4Free analysis
        gpt_evaluation, gpt_reason = self._gpt4_evaluate(board)
        evaluation['gpt_analysis'] = gpt_evaluation
        reasoning_steps.append(f"GPT-4 Analysis: {gpt_reason}")

        total_eval = sum(self.weights[key] * value for key, value in evaluation.items())
        reasoning_steps.append(f"Total Evaluation Score: {total_eval}")

        self.history.append((evaluation, total_eval))

        reasoning = "\n".join(reasoning_steps)

        return (total_eval if board.turn else -total_eval, reasoning)

    def _gpt4_evaluate(self, board: chess.Board) -> Tuple[float, str]:
        """
        Use GPT4Free to analyze the position and provide additional evaluation.
        Returns a tuple of (GPT evaluation score, reasoning string).
        """
        fen = board.fen()
        prompt = (f"Analyze the following chess position (FEN): {fen}. "
                  f"Provide a numerical evaluation between -1 and 1, where positive values favor White and negative values favor Black. "
                  f"Additionally, explain the reasoning behind the evaluation.")

        try:
            response = g4f.ChatCompletion.create(
                model="gpt-4",
                provider=g4f.Provider.OpenaiChat,
                messages=[
                    {"role": "system", "content": "You are a helpful chess AI assistant."},
                    {"role": "user", "content": prompt}
                ]
            )
            # Extract numerical value using regex
            match = re.search(r'(-?\d+(\.\d+)?)', response)
            if match:
                gpt_eval = float(match.group(1))
                gpt_eval_scaled = max(min(gpt_eval, 1), -1) * 100  # Scale to -100 to 100
                # Extract reasoning (everything after the number)
                reasoning = response[match.end():].strip()
                return (gpt_eval_scaled, reasoning)
            else:
                return (0, "GPT-4 failed to provide a numerical evaluation.")
        except Exception as e:
            print(f"GPT4Free evaluation error: {e}")
            return (0, f"GPT-4 evaluation error: {e}")

    def _evaluate_material(self, board: chess.Board) -> Tuple[float, str]:
        """
        Evaluate material balance.
        Returns a tuple of (material score, reasoning string).
        """
        material = 0
        reasoning = []
        for piece_type, value in self.piece_values.items():
            white_pieces = len(board.pieces(piece_type, chess.WHITE))
            black_pieces = len(board.pieces(piece_type, chess.BLACK))
            piece_score = (white_pieces - black_pieces) * value
            material += piece_score
            reasoning.append(f"{chess.piece_name(piece_type).capitalize()}: {white_pieces} White vs {black_pieces} Black, Score: {piece_score}")
        return material, "; ".join(reasoning)

    def _evaluate_pawn_structure(self, board: chess.Board) -> Tuple[float, str]:
        """
        Evaluate pawn structure (doubled, isolated, and passed pawns).
        Returns a tuple of (pawn structure score, reasoning string).
        """
        white_pawns = board.pieces(chess.PAWN, chess.WHITE)
        black_pawns = board.pieces(chess.PAWN, chess.BLACK)

        white_doubled = self._count_doubled_pawns(white_pawns)
        black_doubled = self._count_doubled_pawns(black_pawns)

        white_isolated = self._count_isolated_pawns(white_pawns, board)
        black_isolated = self._count_isolated_pawns(black_pawns, board)

        white_passed = self._count_passed_pawns(white_pawns, black_pawns, board)
        black_passed = self._count_passed_pawns(black_pawns, white_pawns, board)

        white_score = (white_doubled * -0.5) + (white_isolated * -0.5) + (white_passed * 0.5)
        black_score = (black_doubled * -0.5) + (black_isolated * -0.5) + (black_passed * 0.5)

        total_pawn_score = white_score - black_score

        reasoning = (f"White: Doubled Pawns = {white_doubled}, Isolated Pawns = {white_isolated}, "
                     f"Passed Pawns = {white_passed}; "
                     f"Black: Doubled Pawns = {black_doubled}, Isolated Pawns = {black_isolated}, "
                     f"Passed Pawns = {black_passed}; "
                     f"Total Pawn Structure Score: {total_pawn_score}")

        return total_pawn_score, reasoning

    def _evaluate_king_safety(self, board: chess.Board) -> Tuple[float, str]:
        """
        Evaluate king safety based on pawn shield and open files near the king.
        Returns a tuple of (king safety score, reasoning string).
        """
        white_king_square = board.king(chess.WHITE)
        black_king_square = board.king(chess.BLACK)

        white_safety = self._king_pawn_shield(board, white_king_square, chess.WHITE)
        black_safety = self._king_pawn_shield(board, black_king_square, chess.BLACK)

        white_open_files = self._count_open_files_near_square(board, white_king_square)
        black_open_files = self._count_open_files_near_square(board, black_king_square)

        white_total = white_safety - white_open_files
        black_total = black_safety - black_open_files

        total_king_safety = white_total - black_total

        reasoning = (f"White: Pawn Shield = {white_safety}, Open Files Near King = {white_open_files}, "
                     f"Score = {white_total}; "
                     f"Black: Pawn Shield = {black_safety}, Open Files Near King = {black_open_files}, "
                     f"Score = {black_total}; "
                     f"Total King Safety Score: {total_king_safety}")

        return total_king_safety, reasoning

    def _evaluate_mobility(self, board: chess.Board) -> Tuple[float, str]:
        """
        Evaluate mobility based on the number of available moves for both sides.
        Returns a tuple of (mobility score, reasoning string).
        """
        # Current player's mobility
        current_player_moves = len(list(board.legal_moves))

        # Switch turn to evaluate opponent's mobility
        board.push(chess.Move.null())  # Push a null move to switch turns
        opponent_moves = len(list(board.legal_moves))
        board.pop()  # Pop the null move

        mobility_score = current_player_moves - opponent_moves
        reasoning = f"Current Player Moves: {current_player_moves}, Opponent Moves: {opponent_moves}, Score: {mobility_score}"
        return mobility_score, reasoning

    def _evaluate_center_control(self, board: chess.Board) -> Tuple[float, str]:
        """
        Evaluate control of the center squares.
        Returns a tuple of (center control score, reasoning string).
        """
        white_control = sum(1 for square in self.center_squares if board.piece_at(square) and board.piece_at(square).color == chess.WHITE)
        black_control = sum(1 for square in self.center_squares if board.piece_at(square) and board.piece_at(square).color == chess.BLACK)

        center_score = white_control - black_control
        reasoning = f"White controls {white_control} center squares; Black controls {black_control} center squares; Score: {center_score}"
        return center_score, reasoning

    def _evaluate_piece_activity(self, board: chess.Board) -> Tuple[float, str]:
        """
        Evaluate piece activity based on the number of active pieces and their mobility.
        Returns a tuple of (piece activity score, reasoning string).
        """
        white_activity = 0
        black_activity = 0

        for piece_type in [chess.KNIGHT, chess.BISHOP, chess.ROOK, chess.QUEEN]:
            white_pieces = board.pieces(piece_type, chess.WHITE)
            black_pieces = board.pieces(piece_type, chess.BLACK)
            white_activity += len(white_pieces)
            black_activity += len(black_pieces)

        # Additional activity based on mobility (number of squares each piece attacks)
        for piece in board.pieces(chess.KNIGHT, chess.WHITE) | board.pieces(chess.BISHOP, chess.WHITE) | \
                     board.pieces(chess.ROOK, chess.WHITE) | board.pieces(chess.QUEEN, chess.WHITE):
            piece_square = piece
            piece_attacks = len(list(board.attacks(piece_square)))
            white_activity += piece_attacks * 0.1

        for piece in board.pieces(chess.KNIGHT, chess.BLACK) | board.pieces(chess.BISHOP, chess.BLACK) | \
                     board.pieces(chess.ROOK, chess.BLACK) | board.pieces(chess.QUEEN, chess.BLACK):
            piece_square = piece
            piece_attacks = len(list(board.attacks(piece_square)))
            black_activity += piece_attacks * 0.1

        activity_score = white_activity - black_activity
        reasoning = f"White Active Pieces: {white_activity}, Black Active Pieces: {black_activity}, Score: {activity_score}"
        return activity_score, reasoning

    def _count_doubled_pawns(self, pawns: List[chess.Square]) -> int:
        """
        Count the number of doubled pawns.
        """
        doubled_pawns = 0
        pawn_files = defaultdict(int)
        for pawn in pawns:
            file = chess.square_file(pawn)
            pawn_files[file] += 1
        for count in pawn_files.values():
            if count > 1:
                doubled_pawns += count - 1
        return doubled_pawns

    def _count_isolated_pawns(self, pawns: List[chess.Square], board: chess.Board) -> int:
        """
        Count the number of isolated pawns.
        """
        isolated_pawns = 0
        pawn_files = set(chess.square_file(pawn) for pawn in pawns)
        for pawn in pawns:
            file = chess.square_file(pawn)
            if (file - 1 not in pawn_files) and (file + 1 not in pawn_files):
                isolated_pawns += 1
        return isolated_pawns

    def _count_passed_pawns(self, pawns: List[chess.Square], opponent_pawns: List[chess.Square], board: chess.Board) -> int:
        """
        Count the number of passed pawns.
        """
        passed_pawns = 0
        for pawn in pawns:
            color = board.piece_at(pawn).color
            direction = 1 if color == chess.WHITE else -1
            pawn_rank = chess.square_rank(pawn)
            pawn_file = chess.square_file(pawn)
            is_passed = True
            for rank in range(pawn_rank + direction, 8 if direction == 1 else -1, direction):
                for df in [-1, 0, 1]:
                    file = pawn_file + df
                    if 0 <= file < 8:
                        sq = chess.square(file, rank)
                        if sq in opponent_pawns:
                            is_passed = False
                            break
                if not is_passed:
                    break
            if is_passed:
                passed_pawns += 1
        return passed_pawns

    def _king_pawn_shield(self, board: chess.Board, king_square: chess.Square, color: chess.Color) -> float:
        """
        Evaluate the pawn shield around the king.
        Returns the number of pawns protecting the king.
        """
        shield = 0
        king_file = chess.square_file(king_square)
        king_rank = chess.square_rank(king_square)
        # Consider squares directly in front of the king (orthogonal and diagonal)
        for df in [-1, 0, 1]:
            file = king_file + df
            rank = king_rank + (1 if color == chess.WHITE else -1)
            if 0 <= file < 8 and 0 <= rank < 8:
                square = chess.square(file, rank)
                piece = board.piece_at(square)
                if piece and piece.piece_type == chess.PAWN and piece.color == color:
                    shield += 1
        return shield

    def _count_open_files_near_square(self, board: chess.Board, square: chess.Square) -> int:
        """
        Count the number of open files near a given square (within one file distance).
        """
        open_files = 0
        file = chess.square_file(square)
        adjacent_files = [file - 1, file, file + 1]
        for f in adjacent_files:
            if 0 <= f < 8:
                # A file is open if it has no pawns
                pawns = board.pieces(chess.PAWN, chess.WHITE) | board.pieces(chess.PAWN, chess.BLACK)
                file_pawns = [pawn for pawn in pawns if chess.square_file(pawn) == f]
                if not file_pawns:
                    open_files += 1
        return open_files

    def _evaluate_move_ordering(self, board: chess.Board, moves: List[chess.Move]) -> List[chess.Move]:
        """
        Simple move ordering based on MVV-LVA for captures.
        Non-captures are considered less valuable.
        """
        def move_score(move):
            if board.is_capture(move):
                return self.mvv_lva_score(board, move)
            return 0  # Non-captures have lower priority

        sorted_moves = sorted(moves, key=lambda move: move_score(move), reverse=True)
        return sorted_moves

    def order_moves(self, board: chess.Board, moves: List[chess.Move], transposition_move: Optional[chess.Move] = None, depth: int = 0) -> List[chess.Move]:
        """
        Order moves based on Transposition Table move, MVV-LVA for captures, Killer Moves, and History Heuristic.
        """
        ordered_moves = []
        for move in moves:
            score = 0
            if transposition_move and move == transposition_move:
                score += 10000  # Highest priority
            # MVV-LVA
            score += self.mvv_lva_score(board, move)
            # History Heuristic
            score += self.history_table[move] * 0.001
            # Killer Move
            if move in self.killer_moves[depth]:
                score += 500
            ordered_moves.append((move, score))
        ordered_moves.sort(key=lambda x: x[1], reverse=True)
        return [move for move, score in ordered_moves]

    def tune_weights(self):
        """
        Tune the weights based on the history of evaluations and actual outcomes.
        """
        for evaluation, total_eval in self.history:
            for key, value in evaluation.items():
                self.weights[key] += self.learning_rate * (total_eval - value)

        # Adaptive learning: Adjust learning rate based on performance
        if len(self.history) > 10:
            recent_performance = sum(abs(eval_hist[1]) for eval_hist in self.history[-10:]) / 10
            self.learning_rate = max(0.001, min(0.1, self.learning_rate * (1 + recent_performance)))

    def superposition_evaluate(self, board: chess.Board) -> float:
        """
        Perform a superposition evaluation by considering multiple potential moves.
        """
        current_eval, _ = self.evaluate_with_reason(board)
        potential_moves = list(board.legal_moves)[:5]  # Consider top 5 moves
        superposition_eval = current_eval

        for move in potential_moves:
            board.push(move)
            move_eval, _ = self.evaluate_with_reason(board)
            board.pop()
            superposition_eval += move_eval * 0.1  # Weight future positions less

        return superposition_eval / (1 + len(potential_moves) * 0.1)

    def adaptive_learn(self, board: chess.Board, actual_outcome: float):
        """
        Adapt the evaluation based on the actual game outcome.
        """
        predicted_outcome = self.superposition_evaluate(board)
        error = actual_outcome - predicted_outcome

        # Update weights based on error
        for key in self.weights:
            self.weights[key] += self.learning_rate * error * self.history[-1][0].get(key, 0)

        # Update piece values
        for piece_type in self.piece_values:
            self.piece_values[piece_type] += self.learning_rate * error * (len(board.pieces(piece_type, chess.WHITE)) - len(board.pieces(piece_type, chess.BLACK)))

        # Update GPT4Free prompt based on performance
        if abs(error) > 50:
            self._update_gpt4_prompt()

        # Save learning data
        self.save_learning_data()

    def _update_gpt4_prompt(self):
        """
        Update the GPT4Free prompt to improve its analysis based on recent performance.
        """
        if not self.history:
            return

        recent_games = self.history[-10:]
        avg_error = sum(abs(game[1]) for game in recent_games) / len(recent_games)

        prompt = f"The chess evaluation model has an average error of {avg_error:.2f}. Suggest improvements to the evaluation criteria to reduce this error."

        try:
            response = g4f.ChatCompletion.create(
                model="gpt-4",
                provider=g4f.Provider.OpenaiChat,
                messages=[
                    {"role": "system", "content": "You are a helpful chess AI assistant."},
                    {"role": "user", "content": prompt}
                ]
            )
            print(f"GPT-4 Prompt Update Suggestion: {response}")
        except Exception as e:
            print(f"GPT4Free prompt update error: {e}")

    def mvv_lva_score(self, board: chess.Board, move: chess.Move) -> int:
        """
        Compute MVV-LVA (Most Valuable Victim - Least Valuable Aggressor) score for captures.
        """
        if not board.is_capture(move):
            return 0
        victim_piece = board.piece_at(move.to_square)
        attacker_piece = board.piece_at(move.from_square)
        if victim_piece and attacker_piece:
            return self.piece_values[victim_piece.piece_type] * 10 - self.piece_values[attacker_piece.piece_type]
        return 0


# ================== ChessAI Class ==================
class ChessAI:
    def __init__(self):
        self.transposition_table: dict[int, TranspositionTableEntry] = {}
        self.killer_moves: defaultdict = defaultdict(list)  # depth -> list of killer moves
        self.history_table: defaultdict = defaultdict(int)  # move -> history score
        self.evaluator = AdvancedSuperpositionEvaluator()
        self.FUTILITY_MARGIN = 50
        self.LMR_THRESHOLD = 3
        self.MAX_DEPTH = 5
        self.TIME_LIMIT = 5.0  # seconds

    def store_transposition(self, key: int, entry: TranspositionTableEntry):
        self.transposition_table[key] = entry

    def probe_transposition(self, key: int) -> Optional[TranspositionTableEntry]:
        return self.transposition_table.get(key, None)

    def mvv_lva_score(self, board: chess.Board, move: chess.Move) -> int:
        """
        Compute MVV-LVA (Most Valuable Victim - Least Valuable Aggressor) score for captures.
        """
        if not board.is_capture(move):
            return 0
        victim_piece = board.piece_at(move.to_square)
        attacker_piece = board.piece_at(move.from_square)
        if victim_piece and attacker_piece:
            evaluator = self.evaluator
            return evaluator.piece_values[victim_piece.piece_type] * 10 - evaluator.piece_values[attacker_piece.piece_type]
        return 0

    def order_moves(self, board: chess.Board, moves: List[chess.Move], transposition_move: Optional[chess.Move] = None, depth: int = 0) -> List[chess.Move]:
        """
        Order moves based on Transposition Table move, MVV-LVA for captures, Killer Moves, and History Heuristic.
        """
        ordered_moves = []
        for move in moves:
            score = 0
            if transposition_move and move == transposition_move:
                score += 10000  # Highest priority
            # MVV-LVA
            score += self.mvv_lva_score(board, move)
            # History Heuristic
            score += self.history_table[move] * 0.001
            # Killer Move
            if move in self.killer_moves[depth]:
                score += 500
            ordered_moves.append((move, score))
        ordered_moves.sort(key=lambda x: x[1], reverse=True)
        return [move for move, score in ordered_moves]

    def iterative_deepening_search(self, board: chess.Board, max_depth: int, time_limit: float) -> Optional[chess.Move]:
        """
        Perform iterative deepening search up to max_depth within time_limit seconds.
        """
        start_time = time.time()
        best_move = None
        for depth in range(1, max_depth + 1):
            score, move, reasoning = self.negamax_pvs(board, depth, -math.inf, math.inf, start_time, time_limit + start_time, current_depth=depth)
            if time.time() > start_time + time_limit:
                print("Time limit reached.")
                break
            if move is not None:
                best_move = move
            # Print intermediate results with reasoning
            print(f"Depth {depth}: Best Move = {board.san(move) if move else 'None'} Score = {score}")
            print(f"Reasoning:\n{reasoning}\n{'-'*50}\n")
        return best_move

    def negamax_pvs(self, board: chess.Board, depth: int, alpha: float, beta: float, start_time: float, time_limit: float, current_depth: int) -> Tuple[float, Optional[chess.Move], str]:
        """
        Negamax implementation with Principal Variation Search (PVS).
        Returns a tuple of (score, best_move, reasoning).
        """
        if time.time() > time_limit:
            return 0, None, "Time exceeded."

        key = chess.polyglot.zobrist_hash(board)
        tt_entry = self.probe_transposition(key)
        if tt_entry and tt_entry.depth >= depth:
            if tt_entry.flag == 'EXACT':
                return tt_entry.score, tt_entry.move, "Exact score from transposition table."
            elif tt_entry.flag == 'LOWERBOUND':
                alpha = max(alpha, tt_entry.score)
            elif tt_entry.flag == 'UPPERBOUND':
                beta = min(beta, tt_entry.score)
            if alpha >= beta:
                return tt_entry.score, tt_entry.move, "Alpha-beta cutoff from transposition table."

        if depth == 0:
            eval_score, reasoning = self.evaluator.quiescence_search_with_reason(board, alpha, beta, start_time, time_limit)
            return eval_score, None, reasoning

        max_score = -math.inf
        best_move = None
        reasoning_total = ""

        moves = list(board.legal_moves)
        trans_move = tt_entry.move if tt_entry else None
        ordered_moves = self.order_moves(board, moves, trans_move, depth)

        for idx, move in enumerate(ordered_moves):
            if time.time() > time_limit:
                reasoning_total += "Time exceeded during search.\n"
                break  # Time exceeded

            # Late Move Reduction (LMR)
            if idx >= self.LMR_THRESHOLD and depth >= 3:
                reduced_depth = depth - 2
                reasoning_total += f"Late Move Reduction applied to move {board.san(move)}: reducing depth to {reduced_depth}.\n"
            else:
                reduced_depth = depth - 1

            board.push(move)
            score, _, reasoning = self.negamax_pvs(board, reduced_depth, -beta, -alpha, start_time, time_limit, current_depth + 1)
            board.pop()

            score = -score
            reasoning_total += f"Evaluated move {board.san(move)}: Score = {score}\n"
            reasoning_total += reasoning + "\n"

            if score > max_score:
                max_score = score
                best_move = move
            alpha = max(alpha, score)
            if alpha >= beta:
                # Beta cutoff
                # Update killer moves and history heuristic
                if move not in self.killer_moves[current_depth]:
                    if len(self.killer_moves[current_depth]) < 2:
                        self.killer_moves[current_depth].append(move)
                    else:
                        self.killer_moves[current_depth][0] = move  # Replace oldest
                self.history_table[move] += depth * depth
                reasoning_total += f"Beta cutoff triggered by move {board.san(move)}.\n"
                break

        # Store in Transposition Table
        if max_score <= alpha:
            flag = 'UPPERBOUND'
        elif max_score >= beta:
            flag = 'LOWERBOUND'
        else:
            flag = 'EXACT'
        self.store_transposition(key, TranspositionTableEntry(depth, max_score, flag, best_move))
        return max_score, best_move, reasoning_total

    def quiescence_search_with_reason(self, board: chess.Board, alpha: float, beta: float, start_time: float, time_limit: float) -> Tuple[float, str]:
        """
        Quiescence search to extend the search in volatile positions (e.g., captures).
        Returns a tuple of (evaluation score, reasoning string).
        """
        stand_pat, stand_pat_reason = self.evaluator.evaluate(board)
        if stand_pat >= beta:
            return beta, stand_pat_reason + "\nStand pat exceeds or equals beta."
        alpha = max(alpha, stand_pat)
        reasoning = f"Stand Pat Evaluation: {stand_pat_reason}\n"

        captures = list(board.generate_legal_captures())
        ordered_captures = self.evaluator._evaluate_move_ordering(board, captures)

        for move in ordered_captures:
            if time.time() > time_limit:
                reasoning += "Time exceeded during quiescence search.\n"
                break  # Time exceeded

            board.push(move)
            score, move_reasoning = self.evaluator.quiescence_search_with_reason(board, -beta, -alpha, start_time, time_limit)
            board.pop()

            score = -score
            reasoning += f"Capture Move {board.san(move)}: Score = {score}\n"
            reasoning += move_reasoning + "\n"

            if score >= beta:
                reasoning += "Beta cutoff in quiescence search.\n"
                return beta, reasoning
            if score > alpha:
                alpha = score

        return alpha, reasoning

    def get_best_move(self, board: chess.Board, max_depth: int = 5, time_limit: float = 5.0) -> Optional[chess.Move]:
        """
        Get the best move for the current position using iterative deepening.
        """
        return self.iterative_deepening_search(board, max_depth, time_limit)


# ================== Main Gameplay Function ==================
def play_game():
    ai = ChessAI()
    board = chess.Board()
    move_number = 1

    while not board.is_game_over():
        print(f"\nMove {move_number}: {'White' if board.turn == chess.WHITE else 'Black'} to move")
        print(board)
        print("\nFEN:", board.fen())

        move = ai.get_best_move(board, max_depth=3, time_limit=2.0)
        if move is None:
            print("No valid moves available. Game Over.")
            break
        print("Best Move:", board.san(move))
        board.push(move)
        move_number += 1

        print("-" * 50)

    print("\nGame Over")
    result = board.result()
    print(f"Result: {result}")

    # Optionally, tune weights based on the game outcome
    # For example, if White wins, adjust weights to favor White's evaluations
    # Here, we use a simple scoring: 1 for White win, -1 for Black win, 0 for draw
    if result == "1-0":
        actual_outcome = 100
    elif result == "0-1":
        actual_outcome = -100
    else:
        actual_outcome = 0

    ai.evaluator.adaptive_learn(board, actual_outcome)


if __name__ == "__main__":
    play_game()