2024-09-30 21:18:32,465 [DEBUG] UCI Command Received: go
2024-09-30 21:18:32,466 [INFO] 'go' Command Parameters: {'depth': 3, 'movetime': None, 'wtime': None, 'btime': None, 'winc': 0, 'binc': 0, 'nodes': None, 'mate': None}
2024-09-30 21:18:32,467 [INFO] Remaining Time for Search: 5.00s
2024-09-30 21:18:32,469 [ERROR] Endgame navigator analysis failed: 'DeepMasterEngine' object has no attribute 'engine'
2024-09-30 21:18:32,470 [INFO] Alpha-Beta Pruning: Starting iterative deepening at depth 1 with time limit 5.00s.
2024-09-30 21:18:32,478 [ERROR] Error in UCI loop: 'Board' object has no attribute 'transposition_key'

```
import chess
import chess.engine
import chess.pgn
import random
import numpy as np
from functools import lru_cache
from time import perf_counter
from scipy.interpolate import interp1d
import math
import sys
import logging
import multiprocessing
from multiprocessing import Pool, cpu_count
import pickle
import os
import json
import threading
from collections import defaultdict, deque

# For Neural Network Integration
import tensorflow as tf
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense
from tensorflow.keras.optimizers import Adam

# ======================== Configuration and Initialization ========================

# Constants and Paths
STOCKFISH_PATH = r'C:\Users\hp\Downloads\Stock\stockfish\Stocky.exe'  # Update this to your Stockfish executable path
ENGINE_SAMPLER = 0.05  # Time in seconds per analysis
TOP_M = 3  # Number of top moves to consider from Stockfish
MCTS_ITERATIONS = 100  # Increased number of iterations for better exploration
MOVE_TIME_LIMIT = 5.0  # Default time limit for more complex decisions (to be adjusted dynamically)
TRANSPOSITION_TABLE_MAX_SIZE = 1000000  # Maximum number of entries in the transposition table

# Positional Memory and Transposition Table Files
MEMORY_FILE = "positional_memory.pkl"
TRANS_TABLE_FILE = "transposition_table.pkl"

# Neural Network Model File
NN_MODEL_FILE = "positional_evaluator.h5"

# Initialize Logging
logging.basicConfig(
    level=logging.DEBUG,  # Set to DEBUG for detailed logs
    format='%(asctime)s [%(levelname)s] %(message)s',
    handlers=[
        logging.FileHandler("deepmaster_v4.log"),
        logging.StreamHandler(sys.stdout)
    ]
)

# ======================== Utility Functions ========================

def approximate_table(x):
    """
    Utilizes cubic interpolation to approximate values based on predefined data points.
    This function simulates complex evaluations or parameter tuning within the engine.
    """
    x_points = np.array([1, 2, 3, 4, 5])
    y_points = np.array([0.714, 0.703, 0.704, 0.701, 0.700])
    interpolating_function = interp1d(x_points, y_points, kind='cubic', fill_value="extrapolate")
    return float(interpolating_function(x))

def piece_value(piece):
    """
    Assigns a value to each piece type for material evaluation.
    """
    values = {
        chess.PAWN: 100,
        chess.KNIGHT: 320,
        chess.BISHOP: 330,
        chess.ROOK: 500,
        chess.QUEEN: 900,
        chess.KING: 20000  # Essential piece
    }
    return values.get(piece.piece_type, 0)

def save_memory(memory_data, filename=MEMORY_FILE):
    """
    Saves positional memory to a file for persistence across sessions.
    """
    try:
        with open(filename, 'wb') as f:
            pickle.dump(memory_data, f)
        logging.info("Positional memory saved successfully.")
    except Exception as e:
        logging.error(f"Failed to save positional memory: {e}")

def load_memory(filename=MEMORY_FILE):
    """
    Loads positional memory from a file.
    """
    if os.path.exists(filename):
        try:
            with open(filename, 'rb') as f:
                memory_data = pickle.load(f)
            logging.info("Positional memory loaded successfully.")
            return memory_data
        except Exception as e:
            logging.error(f"Failed to load positional memory: {e}")
    return {}

def save_transposition_table(trans_table, filename=TRANS_TABLE_FILE):
    """
    Saves the transposition table to a file for persistence across sessions.
    """
    try:
        with open(filename, 'wb') as f:
            pickle.dump(trans_table, f)
        logging.info("Transposition table saved successfully.")
    except Exception as e:
        logging.error(f"Failed to save transposition table: {e}")

def load_transposition_table(filename=TRANS_TABLE_FILE):
    """
    Loads the transposition table from a file.
    """
    if os.path.exists(filename):
        try:
            with open(filename, 'rb') as f:
                trans_table = pickle.load(f)
            logging.info("Transposition table loaded successfully.")
            return trans_table
        except Exception as e:
            logging.error(f"Failed to load transposition table: {e}")
    return {}

def build_neural_network():
    """
    Constructs and compiles the neural network model for positional evaluation.
    """
    input_dim = (12 * 64) + (64 * 2) + 5  # Pieces (12 types) per square, move from and to squares, promotion
    model = Sequential()
    model.add(Dense(256, input_dim=input_dim, activation='relu'))
    model.add(Dense(128, activation='relu'))
    model.add(Dense(64, activation='relu'))
    model.add(Dense(1, activation='linear'))  # Output: Evaluation score
    model.compile(loss='mse', optimizer=Adam(learning_rate=0.001))
    if os.path.exists(NN_MODEL_FILE):
        try:
            model.load_weights(NN_MODEL_FILE)
            logging.info("Neural network model loaded successfully.")
        except Exception as e:
            logging.error(f"Failed to load neural network model: {e}")
    else:
        logging.info("Initialized new neural network model.")
    return model

# ======================== Positional Heuristics ========================

class PositionalHeuristics:
    """
    Evaluates various strategic elements of the chessboard to assess positional advantages using bitboards.
    """

    @staticmethod
    def evaluate(board: chess.Board):
        score = 0.0
        score += PositionalHeuristics.outpost_mastery(board)
        score += PositionalHeuristics.weak_square_exploitation(board)
        score += PositionalHeuristics.rook_on_seventh(board)
        score += PositionalHeuristics.minor_piece_dominance(board)
        score += PositionalHeuristics.pawn_structure(board)
        score += PositionalHeuristics.king_safety(board)
        score += PositionalHeuristics.space_control(board)
        score += PositionalHeuristics.piece_mobility(board)
        return score

    @staticmethod
    def outpost_mastery(board: chess.Board):
        """
        Rewards advanced positioning of knights and bishops on outposts using bitboards.
        """
        score = 0.0
        piece_types = [chess.KNIGHT, chess.BISHOP]
        for piece_type in piece_types:
            white_pieces = board.pieces(piece_type, chess.WHITE)
            black_pieces = board.pieces(piece_type, chess.BLACK)
            for square in white_pieces:
                if PositionalHeuristics.is_outpost(board, square, chess.WHITE):
                    score += 0.3  # Bonus for white outpost
            for square in black_pieces:
                if PositionalHeuristics.is_outpost(board, square, chess.BLACK):
                    score -= 0.3  # Bonus for black outpost
        return score

    @staticmethod
    def is_outpost(board: chess.Board, square, color):
        """
        Determines if a square is an outpost for the given color.
        """
        opponent_color = not color
        # Check if the square is attacked by opponent pawns; if so, it's not a safe outpost
        if PositionalHeuristics.is_attacked_by_pawns(board, square, opponent_color):
            return False
        # Check for pawn protection
        if PositionalHeuristics.is_square_protected(board, square, color):
            return True
        return False

    @staticmethod
    def is_attacked_by_pawns(board: chess.Board, square, color):
        """
        Checks if a square is attacked by the opponent's pawns.
        """
        pawn_attacks = chess.BB_EMPTY
        if color == chess.WHITE:
            # White pawns attack diagonally forward left and right
            if chess.square_file(square) > 0:
                pawn_attacks |= chess.BB_SQUARES[square - 7]
            if chess.square_file(square) < 7:
                pawn_attacks |= chess.BB_SQUARES[square - 9]
        else:
            # Black pawns attack diagonally forward left and right
            if chess.square_file(square) > 0:
                pawn_attacks |= chess.BB_SQUARES[square + 7]
            if chess.square_file(square) < 7:
                pawn_attacks |= chess.BB_SQUARES[square + 9]
        return bool(pawn_attacks & board.pawns(color))

    @staticmethod
    def is_square_protected(board: chess.Board, square, color):
        """
        Checks if a square is protected by at least one piece of the given color.
        """
        attackers = board.attackers(color, square)
        return len(attackers) > 0

    @staticmethod
    def weak_square_exploitation(board: chess.Board):
        """
        Rewards control over central weak squares while penalizing opponent control using bitboards.
        """
        score = 0.0
        # Central squares using bitboards
        weak_squares = [chess.D4, chess.E4, chess.D5, chess.E5]
        for square in weak_squares:
            white_attack = board.is_attacked_by(chess.WHITE, square)
            black_attack = board.is_attacked_by(chess.BLACK, square)
            if white_attack and not black_attack:
                score += 0.2  # Bonus for white controlling weak square
            if black_attack and not white_attack:
                score -= 0.2  # Bonus for black controlling weak square
        return score

    @staticmethod
    def rook_on_seventh(board: chess.Board):
        """
        Rewards rooks that have penetrated into the opponent's seventh rank using bitboards.
        """
        score = 0.0
        # White rooks on 7th rank (rank 6 in 0-index)
        white_rooks = board.pieces(chess.ROOK, chess.WHITE)
        black_rooks = board.pieces(chess.ROOK, chess.BLACK)
        for rook in white_rooks:
            if chess.square_rank(rook) == 6:
                score += 0.4
        for rook in black_rooks:
            if chess.square_rank(rook) == 1:
                score -= 0.4
        return score

    @staticmethod
    def minor_piece_dominance(board: chess.Board):
        """
        Incentivizes having a superior number of minor pieces (knights and bishops).
        """
        score = 0.0
        white_minors = len(board.pieces(chess.KNIGHT, chess.WHITE)) + len(board.pieces(chess.BISHOP, chess.WHITE))
        black_minors = len(board.pieces(chess.KNIGHT, chess.BLACK)) + len(board.pieces(chess.BISHOP, chess.BLACK))
        score += 0.1 * (white_minors - black_minors)
        return score

    @staticmethod
    def pawn_structure(board: chess.Board):
        """
        Evaluates the pawn structure, considering doubled pawns, isolated pawns, and passed pawns using bitboards.
        """
        score = 0.0
        for color in [chess.WHITE, chess.BLACK]:
            pawns = board.pieces(chess.PAWN, color)
            if not pawns:
                continue
            file_bitmask = 0
            for pawn in pawns:
                file_bitmask |= chess.BB_FILES[chess.square_file(pawn)]
            # Shift left and right to check adjacent files
            left_adjacent = (file_bitmask << 1) & chess.BB_FILES[chess.FILE_H]
            right_adjacent = (file_bitmask >> 1) & chess.BB_FILES[chess.FILE_A]
            adjacent_files = left_adjacent | right_adjacent

            # Doubled pawns
            pawn_counts = PositionalHeuristics.count_pawns_per_file(pawns)
            for file, count in pawn_counts.items():
                if count > 1:
                    penalty = (-0.2) * (count - 1) if color == chess.WHITE else 0.2 * (count - 1)
                    score += penalty

            # Isolated pawns
            isolated = PositionalHeuristics.count_isolated_pawns(pawns, adjacent_files)
            penalty = (-0.1) * isolated if color == chess.WHITE else 0.1 * isolated
            score += penalty

            # Passed pawns
            passed = PositionalHeuristics.count_passed_pawns(board, pawns, color)
            bonus = 0.3 * passed if color == chess.WHITE else -0.3 * passed
            score += bonus
        return score

    @staticmethod
    def count_pawns_per_file(pawns):
        """
        Counts the number of pawns per file.
        """
        pawn_counts = defaultdict(int)
        for pawn in pawns:
            file = chess.square_file(pawn)
            pawn_counts[file] += 1
        return pawn_counts

    @staticmethod
    def count_isolated_pawns(pawns, adjacent_files):
        """
        Counts the number of isolated pawns.
        """
        isolated = 0
        for pawn in pawns:
            file = chess.square_file(pawn)
            has_adjacent = False
            if file > 0 and (chess.BB_FILES[file - 1] & adjacent_files):
                has_adjacent = True
            if file < 7 and (chess.BB_FILES[file + 1] & adjacent_files):
                has_adjacent = True
            if not has_adjacent:
                isolated += 1
        return isolated

    @staticmethod
    def count_passed_pawns(board: chess.Board, pawns, color):
        """
        Counts the number of passed pawns for a given color.
        """
        passed = 0
        direction = 1 if color == chess.WHITE else -1
        opponent_pawns = board.pieces(chess.PAWN, not color)
        opponent_pawns_files = set(chess.square_file(p) for p in opponent_pawns)

        for pawn in pawns:
            file = chess.square_file(pawn)
            rank = chess.square_rank(pawn)
            is_passed = True
            # Check adjacent files for opponent pawns that can block
            for adj_file in [file - 1, file, file + 1]:
                if 0 <= adj_file <= 7:
                    for r in range(rank + direction, 8 if color == chess.WHITE else -1, direction):
                        sq = chess.square(adj_file, r)
                        if sq in opponent_pawns:
                            is_passed = False
                            break
                if not is_passed:
                    break
            if is_passed:
                passed += 1
        return passed

    @staticmethod
    def king_safety(board: chess.Board):
        """
        Assesses the safety of the king based on pawn shields and nearby friendly pieces using bitboards.
        """
        score = 0.0
        for color in [chess.WHITE, chess.BLACK]:
            king_square = board.king(color)
            if king_square is None:
                continue
            pawn_shield = PositionalHeuristics.count_pawn_shield(board, king_square, color)
            nearby_pieces = PositionalHeuristics.count_nearby_pieces(board, king_square, color)
            # Adjust scores based on pawn shield and nearby pieces
            score += 0.2 * pawn_shield if color == chess.WHITE else -0.2 * pawn_shield
            score += 0.1 * nearby_pieces if color == chess.WHITE else -0.1 * nearby_pieces
        return score

    @staticmethod
    def count_pawn_shield(board: chess.Board, king_square, color):
        """
        Counts the number of pawns in the immediate vicinity to shield the king using bitboards.
        """
        file = chess.square_file(king_square)
        rank = chess.square_rank(king_square)
        shield = 0
        for shift in [-1, 0, 1]:
            f = file + shift
            if 0 <= f <= 7:
                # Determine the pawn's rank based on color
                rank_shift = 1 if color == chess.WHITE else -1
                sq = chess.square(f, rank + rank_shift)
                if 0 <= chess.square_rank(sq) <= 7:
                    piece = board.piece_at(sq)
                    if piece and piece.piece_type == chess.PAWN and piece.color == color:
                        shield += 1
        return shield

    @staticmethod
    def count_nearby_pieces(board: chess.Board, king_square, color):
        """
        Counts the number of friendly pieces within two squares of the king using bitboards.
        """
        nearby = 0
        for square in chess.SQUARES:
            piece = board.piece_at(square)
            if piece and piece.color == color:
                dist = chess.square_distance(square, king_square)
                if dist <= 2 and square != king_square:
                    nearby += 1
        return nearby

    @staticmethod
    def space_control(board: chess.Board):
        """
        Rewards control over central and expansive squares using bitboards.
        """
        score = 0.0
        central_squares = [chess.D4, chess.E4, chess.D5, chess.E5]
        for square in central_squares:
            if board.is_attacked_by(chess.WHITE, square):
                score += 0.1
            if board.is_attacked_by(chess.BLACK, square):
                score -= 0.1
        return score

    @staticmethod
    def piece_mobility(board: chess.Board):
        """
        Rewards having more mobility (i.e., more legal moves) using bitboards.
        """
        white_mobility = len(list(board.generate_legal_moves(color=chess.WHITE)))
        black_mobility = len(list(board.generate_legal_moves(color=chess.BLACK)))
        mobility_score = 0.05 * (white_mobility - black_mobility)
        return mobility_score

# ======================== Positional Memory Learning ========================

class PositionalMemory:
    """
    Remembers and scores moves based on past outcomes to prioritize strong moves in similar positions.
    Utilizes persistent storage for memory across sessions.
    Implements a Lightweight Neural Network-based pattern recognition for advanced positional learning.
    """
    def __init__(self):
        self.memory = {}  # {fen: [move1, move2, ...]}
        self.move_scores = {}  # {fen: {move: score}}
        self.lock = threading.Lock()  # For thread-safe operations
        self.load_memory()
        self.neural_model = build_neural_network()
        logging.info("Positional memory initialized.")

    def load_memory(self):
        loaded_memory = load_memory()
        if loaded_memory:
            self.memory = loaded_memory.get('memory', {})
            self.move_scores = loaded_memory.get('move_scores', {})
        else:
            self.memory = {}
            self.move_scores = {}
        logging.info("Positional memory loaded.")

    def save_memory(self):
        with self.lock:
            save_memory({'memory': self.memory, 'move_scores': self.move_scores})

    def recall(self, board_fen):
        """
        Retrieves the list of remembered moves for a given board position.
        """
        return self.memory.get(board_fen, [])

    def remember(self, board_fen, move, outcome):
        """
        Stores a move and updates its score based on the game outcome.
        Also updates the neural network with the new data.
        """
        with self.lock:
            if board_fen not in self.memory:
                self.memory[board_fen] = []
                self.move_scores[board_fen] = {}
            self.memory[board_fen].append(move)
            if move not in self.move_scores[board_fen]:
                self.move_scores[board_fen][move] = 0
            # Update scores based on outcome
            if outcome == 'win':
                self.move_scores[board_fen][move] += 1
            elif outcome == 'loss':
                self.move_scores[board_fen][move] -= 1
            else:
                self.move_scores[board_fen][move] += 0.5  # For draws or neutral outcomes
            self.save_memory()
            # Update neural network asynchronously
            threading.Thread(target=self.update_neural_network, args=(board_fen, move, outcome), daemon=True).start()

    def prioritize_moves(self, board_fen, legal_moves):
        """
        Prioritizes moves based on historical performance and neural network evaluation.
        """
        with self.lock:
            remembered_moves = self.memory.get(board_fen, [])
            if not remembered_moves:
                return list(legal_moves)
            # Assign scores to moves based on historical data and neural evaluation
            ranked_moves = sorted(
                legal_moves,
                key=lambda move: self.move_scores.get(board_fen, {}).get(move.uci(), 0) + self.neural_evaluation(board_fen, move),
                reverse=True
            )
            return ranked_moves

    @staticmethod
    def fen_to_features(fen):
        """
        Converts FEN string to a feature vector for the neural network.
        """
        board = chess.Board(fen)
        features = []
        piece_map = board.piece_map()
        for square in chess.SQUARES:
            piece = piece_map.get(square)
            if piece:
                index = (piece.piece_type - 1) * 2 + (0 if piece.color == chess.WHITE else 1)
                features += PositionalMemory.one_hot_encoding(index, 12)
            else:
                features += PositionalMemory.one_hot_encoding(0, 12)
        return np.array(features)

    @staticmethod
    def move_to_features(move):
        """
        Converts a move to a feature vector for the neural network.
        """
        features = []
        from_square = move.from_square
        to_square = move.to_square
        features += PositionalMemory.one_hot_encoding(from_square, 64)
        features += PositionalMemory.one_hot_encoding(to_square, 64)
        promotion = move.promotion if move.promotion else 0
        features += PositionalMemory.one_hot_encoding(promotion, 5)
        return np.array(features)

    @staticmethod
    def one_hot_encoding(value, size):
        """
        Generates a one-hot encoded vector.
        """
        encoding = [0] * size
        if 0 <= value < size:
            encoding[value] = 1
        return encoding

    def neural_evaluation(self, board_fen, move):
        """
        Evaluates the move using the neural network to provide additional scoring.
        """
        try:
            # Convert FEN to input features
            features = self.fen_to_features(board_fen)
            # Append move representation
            move_features = self.move_to_features(move)
            input_features = np.concatenate((features, move_features))
            input_features = input_features.reshape(1, -1)
            # Predict using the neural network
            prediction = self.neural_model.predict(input_features, verbose=0)
            return prediction[0][0] * 0.1  # Scale prediction
        except Exception as e:
            logging.error(f"Neural evaluation failed: {e}")
        return 0

    def update_neural_network(self, board_fen, move, outcome):
        """
        Updates the neural network based on the outcome of a move.
        """
        try:
            # Prepare training data
            features = self.fen_to_features(board_fen)
            move_features = self.move_to_features(move)
            input_features = np.concatenate((features, move_features)).reshape(1, -1)
            target = self.calculate_target(outcome)
            # Train the model
            self.neural_model.fit(input_features, np.array([target]), epochs=1, verbose=0)
            # Save the model periodically
            self.neural_model.save(NN_MODEL_FILE)
            logging.debug("Neural network updated and saved.")
        except Exception as e:
            logging.error(f"Failed to update neural network: {e}")

    def calculate_target(self, outcome):
        """
        Determines the target value based on the game outcome.
        """
        if outcome == 'win':
            return 1.0
        elif outcome == 'loss':
            return -1.0
        else:
            return 0.0

# ======================== Deep Reinforcement Learning Simulation ========================

class DeepReinforcementLearning:
    """
    Simulates a deep reinforcement learning model's evaluation of board positions.
    Integrates with the neural network for advanced positional evaluations.
    """
    def __init__(self, positional_memory: PositionalMemory):
        self.positional_memory = positional_memory
        self.model = self.positional_memory.neural_model  # Use the same neural network

    def evaluate_position(self, board: chess.Board, move: chess.Move = None):
        """
        Combines material balance, mobility, and neural network evaluation to simulate neural network evaluation.
        """
        material = self.material_evaluation(board)
        mobility = len(list(board.legal_moves))
        positional_score = self.neural_evaluation(board, move) if move else 0
        return material + 0.1 * mobility + positional_score  # Weight mobility and positional score as factors

    def material_evaluation(self, board: chess.Board):
        """
        Calculates the material balance of the board.
        """
        value = 0
        piece_values_dict = {
            chess.PAWN: 100,
            chess.KNIGHT: 320,
            chess.BISHOP: 330,
            chess.ROOK: 500,
            chess.QUEEN: 900,
            chess.KING: 20000  # Essential piece
        }
        for piece in board.piece_map().values():
            v = piece_values_dict.get(piece.piece_type, 0)
            if piece.color == chess.WHITE:
                value += v
            else:
                value -= v
        return value / 100  # Normalize (e.g., +3.20 means White has an advantage equivalent to a Knight)

    def neural_evaluation(self, board: chess.Board, move: chess.Move):
        """
        Evaluates the move using the neural network for positional scoring.
        """
        try:
            board_fen = board.fen()
            return self.positional_memory.neural_evaluation(board_fen, move)
        except Exception as e:
            logging.error(f"Neural evaluation failed: {e}")
            return 0.0

# ======================== Transposition Table Management ========================

class TranspositionTableManager:
    """
    Manages the transposition table for caching evaluated positions to optimize Alpha-Beta pruning.
    Utilizes an LRU cache to limit memory usage.
    """
    def __init__(self, max_size=TRANSPOSITION_TABLE_MAX_SIZE):
        self.table = load_transposition_table()
        self.max_size = max_size
        self.access_queue = deque()  # To track access order for LRU
        self.lock = threading.Lock()
        if self.table and len(self.table) > self.max_size:
            self.trim_table()

    def lookup(self, transposition_key):
        with self.lock:
            entry = self.table.get(transposition_key, None)
            if entry:
                # Update access order for LRU
                if transposition_key in self.access_queue:
                    self.access_queue.remove(transposition_key)
                self.access_queue.append(transposition_key)
            return entry

    def store(self, transposition_key, value, depth):
        with self.lock:
            if transposition_key in self.table:
                self.table[transposition_key]['value'] = value
                self.table[transposition_key]['depth'] = depth
                # Update access order
                if transposition_key in self.access_queue:
                    self.access_queue.remove(transposition_key)
                self.access_queue.append(transposition_key)
            else:
                if len(self.table) >= self.max_size:
                    try:
                        # Remove the oldest accessed item
                        oldest_key = self.access_queue.popleft()
                        del self.table[oldest_key]
                        logging.debug(f"Transposition Table: Evicted {oldest_key}.")
                    except IndexError:
                        pass
                self.table[transposition_key] = {'value': value, 'depth': depth}
                self.access_queue.append(transposition_key)

    def save_transposition_table(self):
        with self.lock:
            save_transposition_table(self.table)
            logging.info("Transposition table saved.")

    def trim_table(self):
        """
        Trims the transposition table to ensure it doesn't exceed the maximum size.
        """
        with self.lock:
            while len(self.table) > self.max_size:
                try:
                    oldest_key = self.access_queue.popleft()
                    del self.table[oldest_key]
                    logging.debug(f"Transposition Table: Evicted {oldest_key} during trim.")
                except IndexError:
                    break

# ======================== Alpha-Beta Pruning with Transposition Tables ========================

class AlphaBetaPruning:
    """
    Implements the Alpha-Beta pruning algorithm with enhancements like transposition tables, iterative deepening, killer moves, and history heuristics.
    """
    def __init__(self, engine, positional_memory: PositionalMemory, sampler=ENGINE_SAMPLER):
        self.engine = engine  # Instance of chess.engine.SimpleEngine
        self.memory = positional_memory
        self.sampler = sampler
        self.lr = DeepReinforcementLearning(positional_memory)
        self.transposition_table = TranspositionTableManager()
        self.killer_moves = defaultdict(list)  # {depth: [move1, move2]}
        self.history_heuristic = defaultdict(int)  # {move_uci: score}
        logging.info("AlphaBetaPruning initialized.")

    def get_best_move(self, board: chess.Board, max_depth: int, remaining_time: float):
        """
        Determines the best move using enhanced Alpha-Beta pruning with iterative deepening.
        """
        self.best_move = None
        self.iterative_deepening(board, max_depth, remaining_time)
        logging.info(f"Alpha-Beta Pruning: Best move determined as {self.best_move.uci() if self.best_move else 'None'}.")
        return self.best_move

    def iterative_deepening(self, board: chess.Board, max_depth: int, remaining_time: float):
        """
        Implements iterative deepening to progressively deepen the search.
        Adjusts MOVE_TIME_LIMIT based on remaining_time.
        """
        self.start_time = perf_counter()
        for current_depth in range(1, max_depth + 1):
            elapsed_time = perf_counter() - self.start_time
            time_left = remaining_time - elapsed_time
            if time_left <= 0:
                logging.warning("Alpha-Beta Pruning: No remaining time for iterative deepening.")
                break
            current_time_limit = min(MOVE_TIME_LIMIT, time_left)
            logging.info(f"Alpha-Beta Pruning: Starting iterative deepening at depth {current_depth} with time limit {current_time_limit:.2f}s.")
            move, score = self.alphabeta(board, current_depth, -math.inf, math.inf, True, self.start_time, current_time_limit)
            if move:
                self.best_move = move
            else:
                logging.warning("Alpha-Beta Pruning: No move found in iterative deepening.")
            current_time = perf_counter()
            if current_time - self.start_time > remaining_time:
                logging.warning("Alpha-Beta Pruning: Time limit exceeded during iterative deepening.")
                break

    def alphabeta(self, board: chess.Board, depth: int, alpha: float, beta: float, maximizing_player: bool, start_time: float, time_limit: float):
        """
        Recursively applies the Alpha-Beta pruning algorithm with advanced move ordering, killer moves, and history heuristics.
        """
        current_time = perf_counter()
        if current_time - start_time > time_limit:
            logging.warning("Alpha-Beta Pruning: Time limit exceeded during recursive search.")
            return None, 0  # Neutral score on timeout

        board_key = board.transposition_key()
        tt_entry = self.transposition_table.lookup(board_key)
        if tt_entry and tt_entry['depth'] >= depth:
            return None, tt_entry['value']

        if depth == 0 or board.is_game_over():
            evaluation = self.evaluate(board)
            return None, evaluation

        legal_moves = list(board.legal_moves)
        if not legal_moves:
            # No legal moves, game over
            evaluation = self.evaluate(board)
            return None, evaluation

        prioritized_moves = self.memory.prioritize_moves(board.fen(), legal_moves)
        # Incorporate killer moves
        kill_moves = self.killer_moves[depth]
        # Prioritize killer moves first
        prioritized_moves = sorted(
            prioritized_moves,
            key=lambda move: (move in kill_moves, self.history_heuristic[move.uci()]),
            reverse=True
        )
        # Further sort based on history heuristic
        prioritized_moves = sorted(prioritized_moves, key=lambda move: self.move_order(move), reverse=True)

        best_move = None
        if maximizing_player:
            value = -math.inf
            for move in prioritized_moves:
                board.push(move)
                _, score = self.alphabeta(board, depth - 1, alpha, beta, False, start_time, time_limit)
                board.pop()
                if score > value:
                    value = score
                    best_move = move
                alpha = max(alpha, value)
                if alpha >= beta:
                    # Register killer move
                    if move not in self.killer_moves[depth]:
                        self.killer_moves[depth].append(move)
                        if len(self.killer_moves[depth]) > 2:  # Limit killer moves
                            self.killer_moves[depth].pop(0)
                        logging.debug(f"Killer Move: {move.uci()} at depth {depth}")
                    break  # Beta cut-off
            self.transposition_table.store(board_key, value, depth)
            return best_move, value
        else:
            value = math.inf
            for move in prioritized_moves:
                board.push(move)
                _, score = self.alphabeta(board, depth - 1, alpha, beta, True, start_time, time_limit)
                board.pop()
                if score < value:
                    value = score
                    best_move = move
                beta = min(beta, value)
                if beta <= alpha:
                    # Register killer move
                    if move not in self.killer_moves[depth]:
                        self.killer_moves[depth].append(move)
                        if len(self.killer_moves[depth]) > 2:
                            self.killer_moves[depth].pop(0)
                        logging.debug(f"Killer Move: {move.uci()} at depth {depth}")
                    break  # Alpha cut-off
            self.transposition_table.store(board_key, value, depth)
            return best_move, value

    def evaluate(self, board: chess.Board):
        """
        Combines engine analysis, positional heuristics, and simulated deep RL evaluation.
        """
        try:
            analysis = self.engine.analyse(board, chess.engine.Limit(time=self.sampler))
            engine_score = analysis["score"].white().score(mate_score=10000)
            if engine_score is None:
                # If no mate score, use centipawns
                engine_score = analysis["score"].white().score(mate_score=10000)
                if engine_score is None:
                    engine_score = 0
                else:
                    engine_score = engine_score / 100.0  # Normalize
            else:
                engine_score = engine_score / 100.0  # Normalize
        except Exception as e:
            logging.error(f"Engine analysis failed: {e}")
            engine_score = 0
        positional_score = PositionalHeuristics.evaluate(board)
        rl_score = self.lr.evaluate_position(board, None)  # Move is not used here
        total_score = engine_score + positional_score + rl_score
        logging.debug(f"Evaluation - Engine: {engine_score}, Positional: {positional_score}, RL: {rl_score}, Total: {total_score}")
        return total_score

    def move_order(self, move: chess.Move):
        """
        Determines the order of move evaluation based on history heuristics.
        """
        move_score = self.history_heuristic[move.uci()]
        return move_score

# ======================== Monte Carlo Tree Search (MCTS) ========================

class MCTSNode:
    """
    Represents a node in the Monte Carlo Tree Search.
    Utilizes bitboards for efficient state representation.
    """
    def __init__(self, board: chess.Board, parent=None, move=None):
        self.board = board.copy()
        self.parent = parent
        self.move = move
        self.children = {}
        self.visits = 0
        self.wins = 0
        self.untried_moves = list(board.legal_moves)

    def expand(self, memory: PositionalMemory):
        """
        Expands the node by adding a new child for an untried move.
        """
        if not self.untried_moves:
            return None
        move = random.choice(self.untried_moves)
        self.untried_moves.remove(move)
        new_board = self.board.copy()
        new_board.push(move)
        child_node = MCTSNode(new_board, parent=self, move=move)
        self.children[move] = child_node
        logging.debug(f"MCTS Node expanded with move {move.uci()}.")
        return child_node

    def is_fully_expanded(self):
        """
        Checks if all possible moves from this node have been tried.
        """
        return len(self.untried_moves) == 0

    def best_child(self, c_param=1.4):
        """
        Selects the best child node based on the UCB1 formula.
        """
        choices_weights = []
        for child in self.children.values():
            if child.visits > 0:
                exploit = child.wins / child.visits
                explore = math.sqrt((2 * math.log(self.visits)) / child.visits)
                choices_weights.append(exploit + c_param * explore)
            else:
                choices_weights.append(float('inf'))
        max_weight = max(choices_weights)
        best_children = [child for child, weight in zip(self.children.values(), choices_weights) if weight == max_weight]
        best = random.choice(best_children)  # Randomly select among equally good moves
        logging.debug(f"MCTS Best child selected with move {best.move.uci()}.")
        return best

class MonteCarloTreeSearch:
    """
    Implements the Monte Carlo Tree Search algorithm with statistical backing.
    Utilizes multiprocessing for parallel simulations.
    """
    def __init__(self, memory: PositionalMemory, iterations: int = MCTS_ITERATIONS):
        self.memory = memory
        self.iterations = iterations

    def search(self, board: chess.Board):
        """
        Performs MCTS to determine the best move from the current board position.
        """
        root = MCTSNode(board)
        logging.info(f"Starting MCTS with {self.iterations} iterations.")

        for i in range(self.iterations):
            node = root
            # Selection
            while node.is_fully_expanded() and node.children:
                node = node.best_child()
            # Expansion
            if not node.is_fully_expanded():
                node = node.expand(self.memory)
            # Simulation
            result = self.simulate_game(node.board)
            # Backpropagation
            self.backpropagate(node, result)
            # Progress logging
            if (i + 1) % max(1, self.iterations // 10) == 0:
                logging.debug(f"MCTS Progress: {((i + 1) / self.iterations) * 100:.1f}% completed.")

        # Determine the best move based on visit counts
        if root.children:
            best_move = max(root.children.values(), key=lambda c: c.visits).move
            logging.info(f"MCTS selected move: {best_move.uci()} after {self.iterations} iterations.")
            return best_move
        logging.warning("MCTS failed to select a best move.")
        return None

    def simulate_game(self, board: chess.Board):
        """
        Simulates a random playout from the current board position until the game ends.
        """
        simulation_board = board.copy()
        while not simulation_board.is_game_over():
            legal_moves = list(simulation_board.legal_moves)
            move = random.choice(legal_moves)
            simulation_board.push(move)
        result = simulation_board.result()
        logging.debug(f"Simulation completed with result: {result}")
        return result

    def backpropagate(self, node: MCTSNode, result: str):
        """
        Updates the nodes' visit and win counts based on the simulation result.
        """
        while node is not None:
            node.visits += 1
            if result == "1-0":
                node.wins += 1  # White wins
            elif result == "0-1":
                node.wins -= 1  # Black wins
            # Draws do not affect the score
            node = node.parent

# ======================== DeepMaster Engine ========================

class DeepMasterEngine:
    """
    Integrates all components to manage game state and select optimal moves.
    Implements full UCI protocol support for seamless integration with chess GUIs.
    """
    def __init__(self, engine: chess.engine.SimpleEngine, sampler=ENGINE_SAMPLER, top_m=TOP_M):
        self.board = chess.Board()
        self.memory = PositionalMemory()
        self.transposition_table_manager = TranspositionTableManager()
        self.alphabeta = AlphaBetaPruning(engine, self.memory, sampler)
        self.top_m = top_m
        self.mcts_iterations = MCTS_ITERATIONS
        self.lr = DeepReinforcementLearning(self.memory)
        self.mcts = MonteCarloTreeSearch(self.memory, self.mcts_iterations)
        logging.info("DeepMasterEngine initialized.")

    def make_move(self, move_uci):
        """
        Applies a move to the board if it's legal.
        """
        try:
            move = chess.Move.from_uci(move_uci)
            if move in self.board.legal_moves:
                self.board.push(move)
                logging.debug(f"User move applied: {move.uci()}")
                return True, ""
            else:
                logging.warning(f"Attempted illegal move: {move.uci()}")
                return False, "Illegal move."
        except ValueError:
            logging.warning(f"Invalid move format attempted: {move_uci}")
            return False, "Invalid move format."

    def get_deepmaster_move(self, remaining_time: float = MOVE_TIME_LIMIT):
        """
        Determines and applies DeepMaster's move using integrated components.
        Adjusts search depth based on remaining_time.
        """
        # Check for endgame and use Endgame Navigator if applicable
        end_move = self.endgame_navigator()
        if end_move and end_move in self.board.legal_moves:
            self.board.push(end_move)
            outcome = 'loss' if self.board.turn == chess.BLACK else 'win'  # Adjust based on turn
            self.memory.remember(self.board.fen(), end_move.uci(), outcome)
            logging.info(f"DeepMaster (Endgame) execution: {end_move.uci()}")
            return end_move

        # Dynamic Depth Control based on remaining time
        depth = self.dynamic_depth_control(remaining_time)

        # Alpha-Beta Pruning to determine best move
        move = self.alphabeta.get_best_move(self.board, depth, remaining_time)
        if move:
            self.board.push(move)
            outcome = 'win'  # Assuming the engine's move is beneficial
            self.memory.remember(self.board.fen(), move.uci(), outcome)
            logging.info(f"DeepMaster (Alpha-Beta) move selected: {move.uci()}")
            return move

        # If Alpha-Beta fails or no move found, fallback to MCTS
        mcts_move = self.mcts.search(self.board)
        if mcts_move:
            if mcts_move in self.board.legal_moves:
                self.board.push(mcts_move)
                outcome = 'win'  # Assuming the engine's move is beneficial
                self.memory.remember(self.board.fen(), mcts_move.uci(), outcome)
                logging.info(f"DeepMaster (MCTS) move selected: {mcts_move.uci()}")
                return mcts_move

        # Fallback to random move
        if self.board.legal_moves:
            move = random.choice(list(self.board.legal_moves))
            self.board.push(move)
            outcome = 'draw'
            self.memory.remember(self.board.fen(), move.uci(), outcome)
            logging.info(f"DeepMaster (Random) move selected: {move.uci()}")
            return move
        else:
            logging.info("DeepMaster has no legal moves and resigns.")
            return None

    def play_engine_move(self, remaining_time: float = MOVE_TIME_LIMIT):
        """
        Wrapper for selecting and executing DeepMaster's move.
        """
        move = self.get_deepmaster_move(remaining_time)
        return move

    def display_board(self):
        """
        Returns a string representation of the current board state.
        """
        return self.board.__str__()

    def is_game_over(self):
        """
        Checks if the game has concluded.
        """
        return self.board.is_game_over()

    def get_result(self):
        """
        Retrieves the result of the game.
        """
        return self.board.result()

    def save_state(self):
        """
        Saves the current state of the engine, including transposition tables and memory.
        """
        # Save transposition tables
        self.transposition_table_manager.save_transposition_table()
        # Memory is already saved within PositionalMemory
        logging.info("DeepMasterEngine state saved successfully.")

    def load_state(self):
        """
        Loads the saved state of the engine, including transposition tables and memory.
        """
        self.transposition_table_manager = TranspositionTableManager()
        self.memory.load_memory()
        logging.info("DeepMasterEngine state loaded successfully.")

    def dynamic_depth_control(self, remaining_time: float):
        """
        Determines search depth dynamically based on board complexity and time constraints.
        A simple heuristic based on remaining_time.
        """
        if remaining_time > 10:
            return 4
        elif remaining_time > 5:
            return 3
        else:
            return 2

    def endgame_navigator(self):
        """
        Determines if the current position is in the endgame and selects moves accordingly.
        Placeholder implementation; needs proper endgame detection.
        """
        # Example: If total material is below a threshold, consider it endgame
        total_material = self.calculate_total_material()
        if total_material <= 1000:  # Arbitrary threshold for endgame
            # Implement specific endgame strategies or use Stockfish's suggestions
            try:
                result = self.engine.analyse(self.board, chess.engine.Limit(time=self.sampler))
                best_move = result.get('pv', [None])[0]
                if best_move:
                    return best_move
            except Exception as e:
                logging.error(f"Endgame navigator analysis failed: {e}")
        return None

    def calculate_total_material(self):
        """
        Calculates the total material on the board for both sides.
        """
        piece_values_dict = {
            chess.PAWN: 100,
            chess.KNIGHT: 320,
            chess.BISHOP: 330,
            chess.ROOK: 500,
            chess.QUEEN: 900,
            chess.KING: 20000  # Essential piece
        }
        total = 0
        for piece in self.board.piece_map().values():
            total += piece_values_dict.get(piece.piece_type, 0) if piece.color == chess.WHITE else -piece_values_dict.get(piece.piece_type, 0)
        return abs(total)

# ======================== UCI Protocol Implementation ========================

class UCIEngine:
    """
    Implements the UCI protocol for communication with chess GUIs.
    """
    def __init__(self, engine_instance: DeepMasterEngine):
        self.engine = engine_instance
        self.isready = False
        self.quit_flag = False
        self.uci_commands = {
            "uci": self.handle_uci,
            "isready": self.handle_isready,
            "ucinewgame": self.handle_ucinewgame,
            "position": self.handle_position,
            "go": self.handle_go,
            "quit": self.handle_quit,
            "debug": self.handle_debug  # Added debug command
        }

    def start(self):
        """
        Starts the UCI engine loop.
        """
        while not self.quit_flag:
            try:
                line = sys.stdin.readline()
                if not line:
                    break
                line = line.strip()
                if line == '':
                    continue
                logging.debug(f"UCI Command Received: {line}")
                self.handle_command(line)
            except Exception as e:
                logging.error(f"Error in UCI loop: {e}")

    def handle_command(self, command: str):
        """
        Parses and executes UCI commands.
        """
        parts = command.split()
        if not parts:
            return
        cmd = parts[0].lower()
        if cmd in self.uci_commands:
            self.uci_commands[cmd](parts[1:])
        else:
            logging.warning(f"Unknown UCI command: {cmd}")

    def handle_uci(self, args):
        """
        Handles the 'uci' command.
        """
        print("id name DeepMaster V.4")
        print("id author Your Name")
        # Implement custom options here if needed
        print("uciok")

    def handle_isready(self, args):
        """
        Handles the 'isready' command.
        """
        print("readyok")

    def handle_ucinewgame(self, args):
        """
        Handles the 'ucinewgame' command.
        """
        self.engine.board.reset()
        self.engine.memory = PositionalMemory()
        self.engine.transposition_table_manager = TranspositionTableManager()
        logging.info("New game started.")
        print("")

    def handle_position(self, args):
        """
        Handles the 'position' command to set up the board.
        """
        try:
            if len(args) == 0:
                return
            if args[0] == 'startpos':
                self.engine.board.reset()
                moves = args[2:] if len(args) > 2 and args[1] == 'moves' else []
            elif args[0] == 'fen':
                if len(args) < 7:
                    logging.warning("Incomplete FEN provided.")
                    return
                fen = ' '.join(args[1:7])
                self.engine.board.set_fen(fen)
                moves = args[7:] if len(args) > 7 and args[6] == 'moves' else []
            else:
                logging.warning(f"Unknown position specifier: {args[0]}")
                return
            for move_uci in moves:
                try:
                    move = chess.Move.from_uci(move_uci)
                    if move in self.engine.board.legal_moves:
                        self.engine.board.push(move)
                    else:
                        logging.warning(f"Ignoring illegal move in UCI position setup: {move_uci}")
                except ValueError:
                    logging.warning(f"Ignoring invalid move format in UCI position setup: {move_uci}")
            logging.info(f"Board position set. Current FEN: {self.engine.board.fen()}")
            print("")
        except Exception as e:
            logging.error(f"Error handling position command: {e}")

    def handle_go(self, args):
        """
        Handles the 'go' command to start search.
        Supports various UCI parameters like depth, movetime, wtime, btime, winc, binc.
        """
        # Default settings
        search_params = {
            'depth': 3,
            'movetime': None,
            'wtime': None,
            'btime': None,
            'winc': 0,
            'binc': 0,
            'nodes': None,
            'mate': None
        }

        # Parse arguments
        it = iter(args)
        for arg in it:
            if arg == 'depth' and len(args) > 0:
                try:
                    search_params['depth'] = int(next(it))
                except (StopIteration, ValueError):
                    logging.warning("Invalid depth parameter in 'go' command.")
            elif arg == 'movetime' and len(args) > 0:
                try:
                    search_params['movetime'] = float(next(it)) / 1000.0  # Convert ms to seconds
                except (StopIteration, ValueError):
                    logging.warning("Invalid movetime parameter in 'go' command.")
            elif arg == 'wtime' and len(args) > 0:
                try:
                    search_params['wtime'] = float(next(it)) / 1000.0  # Convert ms to seconds
                except (StopIteration, ValueError):
                    logging.warning("Invalid wtime parameter in 'go' command.")
            elif arg == 'btime' and len(args) > 0:
                try:
                    search_params['btime'] = float(next(it)) / 1000.0  # Convert ms to seconds
                except (StopIteration, ValueError):
                    logging.warning("Invalid btime parameter in 'go' command.")
            elif arg == 'winc' and len(args) > 0:
                try:
                    search_params['winc'] = float(next(it)) / 1000.0  # Convert ms to seconds
                except (StopIteration, ValueError):
                    logging.warning("Invalid winc parameter in 'go' command.")
            elif arg == 'binc' and len(args) > 0:
                try:
                    search_params['binc'] = float(next(it)) / 1000.0  # Convert ms to seconds
                except (StopIteration, ValueError):
                    logging.warning("Invalid binc parameter in 'go' command.")
            elif arg == 'nodes' and len(args) > 0:
                try:
                    search_params['nodes'] = int(next(it))
                except (StopIteration, ValueError):
                    logging.warning("Invalid nodes parameter in 'go' command.")
            elif arg == 'mate' and len(args) > 0:
                try:
                    search_params['mate'] = int(next(it))
                except (StopIteration, ValueError):
                    logging.warning("Invalid mate parameter in 'go' command.")
            else:
                logging.warning(f"Unknown parameter '{arg}' in 'go' command.")

        # Determine remaining time based on the player's time and increment
        remaining_time = MOVE_TIME_LIMIT  # Default time limit
        if self.engine.board.turn == chess.WHITE and search_params['wtime'] is not None:
            remaining_time = max(search_params['wtime'] / 100.0, 1.0)  # Ensure at least 1 second
        elif self.engine.board.turn == chess.BLACK and search_params['btime'] is not None:
            remaining_time = max(search_params['btime'] / 100.0, 1.0)  # Ensure at least 1 second

        # Override with movetime if specified
        if search_params['movetime'] is not None:
            remaining_time = search_params['movetime']

        # Log the search parameters
        logging.info(f"'go' Command Parameters: {search_params}")
        logging.info(f"Remaining Time for Search: {remaining_time:.2f}s")

        # Start searching for the best move
        move = self.engine.get_deepmaster_move(remaining_time)
        if move:
            print(f"bestmove {move.uci()}")
        else:
            print("bestmove 0000")  # Resign or no move

    def handle_quit(self, args):
        """
        Handles the 'quit' command.
        """
        self.quit_flag = True
        self.engine.save_state()
        try:
            self.engine.alphabeta.transposition_table.save_transposition_table()
            self.engine.alphabeta.transposition_table = None
        except Exception as e:
            logging.error(f"Failed to save transposition table during quit: {e}")
        try:
            self.engine.transposition_table_manager.save_transposition_table()
        except Exception as e:
            logging.error(f"Failed to save transposition table during quit: {e}")
        try:
            self.engine.memory.neural_model.save(NN_MODEL_FILE)
            logging.info("Neural network model saved successfully.")
        except Exception as e:
            logging.error(f"Failed to save neural network model during quit: {e}")
        try:
            self.engine.engine.quit()
            logging.info("Stockfish engine process terminated.")
        except Exception as e:
            logging.error(f"Failed to quit Stockfish engine gracefully: {e}")
        logging.info("Engine quitting upon user request.")
        print("")

    def handle_debug(self, args):
        """
        Handles the 'debug' command for additional debugging information.
        Example usage: 'debug true' or 'debug false'
        """
        if len(args) == 0:
            print("debug invalid parameters")
            return
        if args[0].lower() == 'true':
            logging.getLogger().setLevel(logging.DEBUG)
            print("debugging enabled")
        elif args[0].lower() == 'false':
            logging.getLogger().setLevel(logging.INFO)
            print("debugging disabled")
        else:
            print("debug invalid parameters")

# ======================== Master Function with Debugging and Logging ========================

def master_func():
    """
    Manages the engine initialization and starts the UCI protocol loop.
    """
    try:
        # Initialize Stockfish Engine
        if not os.path.exists(STOCKFISH_PATH):
            logging.critical(f"Stockfish engine not found at {STOCKFISH_PATH}. Please check the path.")
            print(f"Error: Stockfish engine not found at {STOCKFISH_PATH}. Please check the path.")
            sys.exit(1)
        stockfish_engine = chess.engine.SimpleEngine.popen_uci(STOCKFISH_PATH)
        logging.info("Stockfish engine successfully initialized.")

        # Initialize DeepMasterEngine
        deepmaster = DeepMasterEngine(stockfish_engine)
        deepmaster.load_state()

        # Initialize UCI Engine
        uci_engine = UCIEngine(deepmaster)
        logging.info("DeepMaster Chess Engine V.4 Initialized.")
        print("DeepMaster Chess Engine V.4 (Bitboard + UCI) Ready.")
        print("Awaiting UCI commands...")
        uci_engine.start()
    except KeyboardInterrupt:
        logging.info("Engine interrupted by user.")
    except Exception as e:
        logging.exception(f"An unexpected error occurred: {e}")
    finally:
        try:
            if 'deepmaster' in locals():
                deepmaster.save_state()
        except Exception as e:
            logging.error(f"Error during state saving: {e}")
        try:
            if 'stockfish_engine' in locals():
                stockfish_engine.quit()
                logging.info("Stockfish engine process terminated.")
        except Exception as e:
            logging.error(f"Error during Stockfish engine quit: {e}")
        print("Goodbye!")

# ======================== Entry Point ========================

if __name__ == "__main__":
    master_func()