Traceback (most recent call last):
File "C:\Users\hp\Downloads\Tact\hss.py", line 1794, in
main()
File "C:\Users\hp\Downloads\Tact\hss.py", line 1786, in main
play_game(cte, opponent, mode='CLI')
File "C:\Users\hp\Downloads\Tact\hss.py", line 1750, in play_game
cli.start_game()
File "C:\Users\hp\Downloads\Tact\hss.py", line 1538, in start_game
self.move_history.append(('Player', self.board.san(move)))
^^^^^^^^^^^^^^^^^^^^
File "C:\Users\hp\AppData\Local\Programs\Python\Python312\Lib\site-packages\chess_init_.py", line 2866, in san
return self.algebraic(move)
^^^^^^^^^^^^^^^^^^^^^
File "C:\Users\hp\AppData\Local\Programs\Python\Python312\Lib\site-packages\chess_init.py", line 2879, in _algebraic
san = self.algebraic_and_push(move, long=long)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\Users\hp\AppData\Local\Programs\Python\Python312\Lib\site-packages\chess_init.py", line 2884, in _algebraic_and_push
san = self.algebraic_without_suffix(move, long=long)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\Users\hp\AppData\Local\Programs\Python\Python312\Lib\site-packages\chess_init.py", line 2920, in _algebraic_without_suffix
assert piece_type, f"san() and lan() expect move to be legal or null, but got {move} in {self.fen()}"
^^^^^^^^^^
AssertionError: san() and lan() expect move to be legal or null, but got e2e4 in rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1

```
# advanced_cte.py

import chess
import chess.pgn
import random
import math
import copy
import sys
import time
from collections import defaultdict, Counter
from threading import Thread, Lock
from queue import Queue
import pickle
import os
import logging
from functools import lru_cache
from concurrent.futures import ThreadPoolExecutor, as_completed

# Optional: For machine learning integration
import numpy as np
import tensorflow as tf
from tensorflow.keras.models import load_model
from sklearn.ensemble import RandomForestClassifier

# Optional: For GUI implementation
import tkinter as tk
from tkinter import messagebox

# Configure logging
logging.basicConfig(level=logging.INFO, filename='cte_log.log',
                    format='%(asctime)s - %(levelname)s - %(message)s')

# ============================================
# Configuration Management
# ============================================
class Config:
    """
    Configuration settings for the Cognitive Tactical Engine.
    """
    def __init__(self):
        self.load_defaults()

    def load_defaults(self):
        self.zobrist_seed = 0
        self.nn_evaluator_model_path = 'nnue_evaluator.h5'
        self.opening_book_path = 'opening_book.pgn'
        self.endgame_tablebase_path = 'endgame_tablebases/'
        self.ml_predictor_model_path = 'move_predictor.pkl'
        self.deception_rate = 0.15
        self.deeper_patterns = True  # Enable more complex pattern recognition
        self.log_level = logging.INFO
        # Add more configurations as needed

    def load_from_file(self, config_file):
        """
        Load configurations from a file.
        """
        # Implementation can be added to parse a config file
        pass

# Initialize configuration
config = Config()

# ============================================
# Zobrist Hashing for Transposition Tables
# ============================================
class ZobristHashing:
    """
    Implements Zobrist Hashing for unique board position identification.
    """
    def __init__(self, seed=config.zobrist_seed):
        self.zobrist_table = {}
        self.seed = seed
        self.initialize_zobrist()
    
    def initialize_zobrist(self):
        """
        Initializes the Zobrist hashing table with random bitstrings.
        """
        random.seed(self.seed)  # For reproducibility
        pieces = [chess.KING, chess.QUEEN, chess.ROOK, chess.BISHOP, chess.KNIGHT, chess.PAWN]
        colors = [chess.WHITE, chess.BLACK]
        for piece in pieces:
            for color in colors:
                for square in chess.SQUARES:
                    self.zobrist_table[(piece, color, square)] = random.getrandbits(64)
        # Castling rights
        self.zobrist_table['K'] = random.getrandbits(64)
        self.zobrist_table['Q'] = random.getrandbits(64)
        self.zobrist_table['k'] = random.getrandbits(64)
        self.zobrist_table['q'] = random.getrandbits(64)
        # En passant
        self.zobrist_table['en_passant'] = random.getrandbits(64)
        # Side to move
        self.zobrist_table['side_to_move'] = random.getrandbits(64)
        logging.info("Zobrist hashing table initialized.")
    
    def compute_hash(self, board):
        """
        Computes the Zobrist hash for the given board state.
        """
        h = 0
        for square in chess.SQUARES:
            piece = board.piece_at(square)
            if piece:
                h ^= self.zobrist_table[(piece.piece_type, piece.color, square)]
        # Castling rights
        if board.has_kingside_castling_rights(chess.WHITE):
            h ^= self.zobrist_table['K']
        if board.has_queenside_castling_rights(chess.WHITE):
            h ^= self.zobrist_table['Q']
        if board.has_kingside_castling_rights(chess.BLACK):
            h ^= self.zobrist_table['k']
        if board.has_queenside_castling_rights(chess.BLACK):
            h ^= self.zobrist_table['q']
        # En passant
        if board.ep_square:
            h ^= self.zobrist_table['en_passant']
        # Side to move
        if board.turn:
            h ^= self.zobrist_table['side_to_move']
        return h

# ============================================
# Transposition Table
# ============================================
class TranspositionTable:
    """
    Caches evaluations of board positions to avoid redundant computations.
    """
    def __init__(self):
        self.table = {}
        self.lock = Lock()
    
    def store(self, zobrist_hash, depth, score, flag, best_move):
        """
        Stores an entry in the transposition table.
        """
        with self.lock:
            self.table[zobrist_hash] = {'depth': depth, 'score': score, 'flag': flag, 'best_move': best_move}
    
    def lookup(self, zobrist_hash, depth):
        """
        Looks up an entry in the transposition table.
        Returns the cached score and best_move if available and sufficient depth.
        """
        with self.lock:
            entry = self.table.get(zobrist_hash, None)
            if entry and entry['depth'] >= depth:
                return entry['score'], entry['best_move']
            return None, None

# ============================================
# Neural Network Evaluator
# ============================================
class NeuralNetworkEvaluator:
    """
    Evaluates chess board positions using a neural network.
    """
    def __init__(self, model_path=config.nn_evaluator_model_path):
        self.model_path = model_path
        if os.path.exists(self.model_path):
            try:
                self.model = load_model(self.model_path)
                logging.info(f"Neural Network model loaded successfully from {self.model_path}.")
            except Exception as e:
                logging.error(f"Failed to load Neural Network model: {e}")
                self.model = self.build_default_model()
        else:
            self.model = self.build_default_model()
            # Note: In practice, train the model with extensive chess data
            self.model.save(self.model_path)
            logging.warning(f"Default Neural Network model created and saved to {self.model_path}. Consider training a sophisticated model for better performance.")
    
    def build_default_model(self):
        """
        Builds and compiles a default neural network model.
        """
        model = tf.keras.Sequential([
            tf.keras.layers.Dense(128, activation='relu', input_shape=(64,)),
            tf.keras.layers.Dense(128, activation='relu'),
            tf.keras.layers.Dense(1, activation='tanh')  # Output between -1 (Black advantage) to 1 (White advantage)
        ])
        model.compile(optimizer='adam', loss='mean_squared_error')
        logging.info("Default Neural Network model built and compiled.")
        return model
    
    def board_to_input(self, board):
        """
        Converts the chess board to a 64-length input vector for the neural network.
        Positive values represent White pieces, negative values represent Black pieces.
        """
        piece_map = board.piece_map()
        input_vector = np.zeros(64)
        for square in chess.SQUARES:
            piece = piece_map.get(square)
            if piece:
                index = square
                # Encode pieces: Pawn=1, Knight=2, Bishop=3, Rook=4, Queen=5, King=6
                piece_value = {chess.PAWN:1, chess.KNIGHT:2, chess.BISHOP:3,
                              chess.ROOK:4, chess.QUEEN:5, chess.KING:6}.get(piece.piece_type, 0)
                input_vector[index] = piece_value if piece.color == chess.WHITE else -piece_value
        return input_vector.reshape(1, 64)
    
    def evaluate(self, board):
        """
        Evaluates the board position and returns a score.
        """
        try:
            input_vector = self.board_to_input(board)
            score = self.model.predict(input_vector, verbose=0)[0][0]
            logging.debug(f"NeuralNetworkEvaluator score: {score}")
            return score
        except Exception as e:
            logging.error(f"Error in NeuralNetworkEvaluator.evaluate: {e}")
            # Fallback to a simple material evaluation
            return self.simple_evaluate(board)
    
    def simple_evaluate(self, board):
        """
        Simple fallback evaluation based on material count.
        """
        material = 0
        piece_values = {
            chess.PAWN: 1,
            chess.KNIGHT: 3,
            chess.BISHOP: 3,
            chess.ROOK: 5,
            chess.QUEEN: 9,
            chess.KING: 0  # Kings are invaluable; not counted
        }
        for piece_type, value in piece_values.items():
            material += len(board.pieces(piece_type, chess.WHITE)) * value
            material -= len(board.pieces(piece_type, chess.BLACK)) * value
        logging.debug(f"Simple evaluation score: {material}")
        return material

# ============================================
# Psychological Profiler
# ============================================
class PsychologicalProfiler:
    """
    Profiles opponent's playing style based on move history.
    """
    def __init__(self):
        self.opponent_move_history = []
        self.move_frequencies = defaultdict(int)
        self.move_counter = 0
        self.opponent_style = "neutral"  # Possible styles: 'aggressive', 'defensive', 'positional'
        self.move_sequences = []
        self.sequence_length = 3  # For sequence-based prediction
    
    def record_move(self, move, board):
        """
        Records an opponent's move and updates profiling.
        """
        self.opponent_move_history.append(move)
        self.move_frequencies[move.uci()] += 1
        self.move_counter += 1
        self.update_move_sequences()
        self.update_opponent_style(move, board)
    
    def get_common_moves(self):
        """
        Retrieves the most frequently used moves by the opponent.
        """
        if not self.move_frequencies:
            return []
        max_freq = max(self.move_frequencies.values())
        common_moves = [move for move, freq in self.move_frequencies.items() if freq == max_freq]
        return common_moves
    
    def predict_opponent_move(self):
        """
        Predicts the opponent's next move based on common moves and move sequences.
        """
        # Sequence-based prediction
        if len(self.opponent_move_history) >= self.sequence_length:
            recent_sequence = tuple(self.opponent_move_history[-self.sequence_length:])
            possible_moves = [seq[-1] for seq in self.move_sequences if seq[:-1] == recent_sequence[:-1]]
            if possible_moves:
                move_counts = Counter(possible_moves)
                most_common = move_counts.most_common()
                highest_freq = most_common[0][1]
                common_moves = [move for move, freq in most_common if freq == highest_freq]
                return [chess.Move.from_uci(move) for move in common_moves]
        
        # Fallback to frequency-based prediction
        return [chess.Move.from_uci(move) for move in self.get_common_moves()]
    
    def update_move_sequences(self):
        """
        Updates the list of move sequences for sequence-based prediction.
        """
        if len(self.opponent_move_history) >= self.sequence_length:
            sequence = tuple(self.opponent_move_history[-self.sequence_length:])
            self.move_sequences.append(sequence)
    
    def update_opponent_style(self, move, board):
        """
        Determines the opponent's playing style based on recent moves.
        """
        if self.move_counter < 10:
            self.opponent_style = "neutral"
            return
        aggressive_moves = sum(1 for m in self.opponent_move_history if self.is_aggressive_move(m, board))
        defensive_moves = sum(1 for m in self.opponent_move_history if self.is_defensive_move(m, board))
        positional_moves = self.move_counter - aggressive_moves - defensive_moves

        styles = {
            'aggressive': aggressive_moves,
            'defensive': defensive_moves,
            'positional': positional_moves
        }

        # Determine the predominant style
        self.opponent_style = max(styles, key=styles.get)
        logging.info(f"Opponent style updated to: {self.opponent_style}")
    
    def is_aggressive_move(self, move, board):
        """
        Identifies if a move is aggressive (e.g., captures, checks).
        """
        return board.is_capture(move) or board.gives_check(move)
    
    def is_defensive_move(self, move, board):
        """
        Identifies if a move is defensive (e.g., protecting king, blocking).
        """
        # Protecting the king if in check
        if board.is_check() and not board.is_checkmate():
            return not board.gives_check(move) and not board.is_capture(move)
        # Blocking a threat
        return False  # Simplistic approach; can be enhanced with deeper analysis

# ============================================
# Tactical Pattern Recognizer
# ============================================
class TacticalPatternRecognizer:
    """
    Recognizes tactical patterns to exploit opportunities.
    """
    def __init__(self, enable_deeper_patterns=config.deeper_patterns):
        self.patterns = [
            self.fork_pattern,
            self.pin_pattern,
            self.skewer_pattern,
            self.discovered_attack_pattern,
            self.check_pattern
        ]
        self.enable_deeper_patterns = enable_deeper_patterns
    
    def recognize_patterns(self, board):
        """
        Checks for any recognized tactical patterns.
        """
        for pattern in self.patterns:
            if pattern(board):
                logging.info(f"Tactical pattern recognized: {pattern.__name__}")
                return True
        return False
    
    def fork_pattern(self, board):
        """
        Detects if the current player can create a fork.
        """
        try:
            for move in board.legal_moves:
                board.push(move)
                attackers = len(list(board.attackers(not board.turn, board.king(not board.turn))))
                board.pop()
                if attackers >= 2:
                    return True
        except:
            pass
        return False
    
    def pin_pattern(self, board):
        """
        Detects if any piece is pinned.
        """
        try:
            for square in chess.SQUARES:
                pin_info = board.is_pinned(chess.BLACK, square) if board.turn == chess.WHITE else board.is_pinned(chess.WHITE, square)
                if pin_info:
                    return True
        except:
            pass
        return False
    
    def skewer_pattern(self, board):
        """
        Detects skewers.
        A skewer is similar to a pin but the more valuable piece is in front.
        """
        try:
            # Iterate through all squares looking for sliding pieces
            for piece_type in [chess.QUEEN, chess.BISHOP, chess.ROOK]:
                for square in board.pieces(piece_type, not board.turn):
                    # Get all attackable squares by this piece
                    attacks = board.attacks(square)
                    for target_square in attacks:
                        target_piece = board.piece_at(target_square)
                        if target_piece and target_piece.color != (not board.turn):
                            # Determine direction vector
                            dx = chess.square_file(target_square) - chess.square_file(square)
                            dy = chess.square_rank(target_square) - chess.square_rank(square)
                            if dx != 0:
                                dx = dx // abs(dx)
                            if dy != 0:
                                dy = dy // abs(dy)
                            # Look for the next square in the same direction
                            current_square = target_square
                            while True:
                                file = chess.square_file(current_square) + dx
                                rank = chess.square_rank(current_square) + dy
                                if file not in range(8) or rank not in range(8):
                                    break
                                new_square = chess.square(file, rank)
                                piece = board.piece_at(new_square)
                                if piece:
                                    if piece.color != target_piece.color:
                                        # Found an opponent's piece behind the target
                                        # Check if target_piece is more valuable than the piece behind
                                        piece_value = {chess.PAWN:1, chess.KNIGHT:3, chess.BISHOP:3,
                                                      chess.ROOK:5, chess.QUEEN:9, chess.KING:0}.get(piece.piece_type, 0)
                                        target_value = {chess.PAWN:1, chess.KNIGHT:3, chess.BISHOP:3,
                                                        chess.ROOK:5, chess.QUEEN:9, chess.KING:0}.get(target_piece.piece_type, 0)
                                        if target_value > piece_value:
                                            return True
                                    break  # Stop after finding the first piece in that direction
            return False
        except Exception as e:
            logging.error(f"Error in skewer_pattern: {e}")
            return False
    
    def discovered_attack_pattern(self, board):
        """
        Detects discovered attacks.
        A discovered attack occurs when moving a piece reveals an attack by another piece.
        """
        try:
            # Clone the board to track attacks before and after the move
            for move in board.legal_moves:
                board.push(move)
                # Find all current player's pieces
                for piece_type in [chess.QUEEN, chess.BISHOP, chess.ROOK, chess.KNIGHT, chess.PAWN]:
                    for square in board.pieces(piece_type, board.turn):
                        attacks = board.attackers(board.turn, square)
                        if len(attacks) > 1:
                            board.pop()
                            return True
                board.pop()
            return False
        except Exception as e:
            logging.error(f"Error in discovered_attack_pattern: {e}")
            return False
    
    def check_pattern(self, board):
        """
        Detects if delivering check creates a tactical advantage.
        """
        try:
            for move in board.legal_moves:
                board.push(move)
                if board.is_check():
                    board.pop()
                    return True
                board.pop()
        except:
            pass
        return False

# ============================================
# Strategic Deception
# ============================================
class StrategicDeception:
    """
    Implements strategic deception by occasionally making suboptimal moves.
    """
    def __init__(self, deception_rate=config.deception_rate):
        """
        deception_rate: Probability of making a deceptive move.
        """
        self.deception_rate = deception_rate
    
    def maybe_deceive(self, board, best_move):
        """
        Decides whether to make a deceptive move instead of the best move.
        """
        try:
            if random.random() < self.deception_rate and len(list(board.legal_moves)) > 1:
                # Select a deceptive move: non-capturing, not giving check, not promoting
                deceptive_moves = [move for move in board.legal_moves if not board.is_capture(move)
                                   and not board.gives_check(move) and not move.promotion]
                if deceptive_moves:
                    deceived_move = random.choice(deceptive_moves)
                    logging.info(f"StrategicDeception applied: {board.san(deceived_move)} instead of {board.san(best_move)}")
                    return deceived_move
        except Exception as e:
            logging.error(f"Error in StrategicDeception.maybe_deceive: {e}")
        return best_move

# ============================================
# Scenario Simulator
# ============================================
class ScenarioSimulator:
    """
    Simulates various scenarios in multiple threads to predict opponent responses.
    """
    def __init__(self, evaluator, depth=2, num_threads=4):
        self.depth = depth
        self.evaluator = evaluator
        self.num_threads = num_threads
        self.lock = Lock()
        self.executor = ThreadPoolExecutor(max_workers=self.num_threads)
    
    def simulate_move(self, board, move, queue):
        """
        Simulates a single move and evaluates the resulting position.
        """
        try:
            simulated_board = board.copy(stack=False)
            simulated_board.push(move)
            score = self.evaluator.evaluate(simulated_board)
            queue.put((move, score))
        except Exception as e:
            logging.error(f"Error in ScenarioSimulator.simulate_move: {e}")
    
    def simulate(self, board, moves):
        """
        Simulates multiple moves concurrently and returns their evaluations.
        """
        queue = Queue()
        futures = []
        for move in moves:
            futures.append(self.executor.submit(self.simulate_move, board, move, queue))
        for future in as_completed(futures):
            pass  # Wait for all simulations to complete
        results = []
        while not queue.empty():
            results.append(queue.get())
        return results

# ============================================
# Machine Learning Predictor
# ============================================
class MachineLearningPredictor:
    """
    Predicts opponent's moves using a machine learning model.
    """
    def __init__(self, model_path=config.ml_predictor_model_path):
        self.model_path = model_path
        if os.path.exists(self.model_path):
            try:
                with open(self.model_path, 'rb') as f:
                    self.model = pickle.load(f)
                self.trained = True
                logging.info(f"Machine Learning model loaded successfully from {self.model_path}.")
            except Exception as e:
                logging.error(f"Failed to load Machine Learning model: {e}")
                self.model = RandomForestClassifier(n_estimators=100)
                self.trained = False
        else:
            self.model = RandomForestClassifier(n_estimators=100)
            self.trained = False
            logging.warning(f"Machine Learning model not found at '{self.model_path}'. Consider training it with PGN data.")
    
    def prepare_training_data(self, games_pgn_path):
        """
        Prepares and trains the machine learning model using PGN game data.
        """
        X = []
        y = []
        try:
            with open(games_pgn_path, 'r') as pgn_file:
                while True:
                    game = chess.pgn.read_game(pgn_file)
                    if game is None:
                        break
                    board = game.board()
                    for move in game.mainline_moves():
                        features = self.extract_features(board)
                        X.append(features)
                        y.append(move.uci())
                        board.push(move)
            if X and y:
                self.train(X, y)
                logging.info("Machine Learning model trained with PGN data.")
            else:
                logging.warning("No data found in PGN file for training.")
        except Exception as e:
            logging.error(f"Error in MachineLearningPredictor.prepare_training_data: {e}")
    
    def train(self, X, y):
        """
        Trains the machine learning model.
        """
        try:
            self.model.fit(X, y)
            with open(self.model_path, 'wb') as f:
                pickle.dump(self.model, f)
            self.trained = True
            logging.info("Machine Learning model trained and saved successfully.")
        except Exception as e:
            logging.error(f"Error in MachineLearningPredictor.train: {e}")
    
    def extract_features(self, board):
        """
        Extracts features from the board for the machine learning model.
        Currently uses material balance, piece activity, and control of the center.
        """
        material = 0
        piece_values = {
            chess.PAWN:1,
            chess.KNIGHT:3,
            chess.BISHOP:3,
            chess.ROOK:5,
            chess.QUEEN:9,
            chess.KING:0  # Kings are invaluable; not counted
        }
        for piece_type, value in piece_values.items():
            material += len(board.pieces(piece_type, chess.WHITE)) * value
            material -= len(board.pieces(piece_type, chess.BLACK)) * value
        
        # Piece Activity: Number of legal moves for each side
        white_activity = len(list(board.legal_moves)) if board.turn else len(list(board.legal_moves))
        black_activity = len(list(board.legal_moves)) if not board.turn else len(list(board.legal_moves))
        if board.turn:
            activity = white_activity
        else:
            activity = black_activity
        
        # Control of the Center: Number of pieces controlling central squares
        center_squares = [chess.E4, chess.D4, chess.E5, chess.D5]
        white_control = sum(1 for sq in center_squares if len(list(board.attackers(chess.WHITE, sq))) > 0)
        black_control = sum(1 for sq in center_squares if len(list(board.attackers(chess.BLACK, sq))) > 0)
        center_control = white_control - black_control
        
        return [material, activity, center_control]
    
    def predict(self, board):
        """
        Predicts the opponent's next move based on the current board state.
        """
        if not self.trained:
            logging.warning("Machine Learning model is not trained. Skipping prediction.")
            return None
        try:
            features = self.extract_features(board)
            predicted_move_uci = self.model.predict([features])[0]
            move = chess.Move.from_uci(predicted_move_uci)
            if move in board.legal_moves:
                logging.info(f"Machine Learning Predictor suggests move: {board.san(move)}")
                return move
            else:
                logging.warning(f"Predicted move {move} is illegal.")
        except Exception as e:
            logging.error(f"Error in MachineLearningPredictor.predict: {e}")
        return None

# ============================================
# Endgame Tablebase
# ============================================
class EndgameTablebase:
    """
    Utilizes precomputed endgame tablebases for perfect play in simplified positions.
    """
    def __init__(self, tablebase_path=config.endgame_tablebase_path):
        self.tablebase = {}
        self.lock = Lock()
        if os.path.exists(tablebase_path):
            self.load_tablebases(tablebase_path)
            logging.info("Endgame tablebases loaded successfully.")
        else:
            logging.warning(f"Endgame tablebase directory '{tablebase_path}' not found. Proceeding without it.")
    
    def load_tablebases(self, tablebase_path):
        """
        Loads endgame tablebases from pickled files.
        """
        try:
            for filename in os.listdir(tablebase_path):
                if filename.endswith('.pkl'):
                    with open(os.path.join(tablebase_path, filename), 'rb') as f:
                        data = pickle.load(f)
                        with self.lock:
                            self.tablebase.update(data)
            logging.info(f"Total endgame positions loaded: {len(self.tablebase)}")
        except Exception as e:
            logging.error(f"Error loading endgame tablebases: {e}")
    
    def get_endgame_move(self, board):
        """
        Retrieves an optimal endgame move if the current board state is in the tablebase.
        """
        try:
            fen = self.normalize_fen(board.fen())
            with self.lock:
                move_uci = self.tablebase.get(fen, None)
            if move_uci:
                move = chess.Move.from_uci(move_uci)
                if move in board.legal_moves:
                    logging.info(f"EndgameTablebase suggests move: {board.san(move)}")
                    return move
        except Exception as e:
            logging.error(f"Error in EndgameTablebase.get_endgame_move: {e}")
        return None
    
    def normalize_fen(self, fen):
        """
        Normalizes FEN to handle symmetrical positions if applicable.
        """
        # Placeholder for normalization logic
        return fen

# ============================================
# Opening Book
# ============================================
class OpeningBook:
    """
    Manages opening book moves loaded from a PGN file.
    """
    def __init__(self, book_path=config.opening_book_path):
        self.book = defaultdict(list)
        if os.path.exists(book_path):
            self.load_book(book_path)
            logging.info("Opening book loaded successfully.")
        else:
            logging.warning(f"Opening book file '{book_path}' not found. Proceeding without it.")
    
    def load_book(self, book_path):
        """
        Loads the opening book from a PGN file.
        """
        try:
            with open(book_path, 'r') as pgn_file:
                while True:
                    game = chess.pgn.read_game(pgn_file)
                    if game is None:
                        break
                    board = game.board()
                    for move in game.mainline_moves():
                        board.push(move)
                        fen = board.fen()
                        self.book[fen].append(move.uci())
            logging.info(f"Total openings loaded: {len(self.book)}")
        except Exception as e:
            logging.error(f"Error loading opening book: {e}")
    
    def get_opening_move(self, board):
        """
        Retrieves an opening move if available for the current board state.
        """
        try:
            fen = board.fen()
            moves = self.book.get(fen, [])
            if moves:
                move_uci = random.choice(moves)
                move = chess.Move.from_uci(move_uci)
                if move in board.legal_moves:
                    logging.info(f"OpeningBook suggests move: {board.san(move)}")
                    return move
        except Exception as e:
            logging.error(f"Error in OpeningBook.get_opening_move: {e}")
        return None

# ============================================
# Enhanced Decision Tree with Quiescence Search
# ============================================
class EnhancedDecisionTree:
    """
    Implements the Minimax algorithm with alpha-beta pruning, transposition tables,
    iterative deepening, and quiescence search.
    """
    def __init__(self, evaluator, depth=4, use_transposition=True):
        self.depth = depth
        self.evaluator = evaluator
        self.transposition_table = TranspositionTable() if use_transposition else None
        self.zobrist = ZobristHashing()
    
    def minimax(self, board, depth, alpha, beta, maximizing_player):
        """
        Performs the Minimax search with alpha-beta pruning and quiescence search.
        """
        zobrist_hash = self.zobrist.compute_hash(board)
        if self.transposition_table:
            cached = self.transposition_table.lookup(zobrist_hash, depth)
            if cached:
                cached_score, cached_move = cached
                return cached_score, cached_move

        if depth == 0:
            return self.quiescence_search(board, alpha, beta, maximizing_player), None

        if board.is_game_over():
            return self.evaluate_game_over(board), None

        legal_moves = list(board.legal_moves)
        # Move Ordering Heuristics: Prioritize captures, checks, promotions
        legal_moves = self.order_moves(board, legal_moves)

        best_move = None
        if maximizing_player:
            max_eval = -math.inf
            for move in legal_moves:
                board.push(move)
                eval, _ = self.minimax(board, depth - 1, alpha, beta, False)
                board.pop()
                if eval > max_eval:
                    max_eval = eval
                    best_move = move
                alpha = max(alpha, eval)
                if beta <= alpha:
                    break
            if self.transposition_table and best_move:
                self.transposition_table.store(zobrist_hash, depth, max_eval, 'exact', best_move.uci())
            return max_eval, best_move
        else:
            min_eval = math.inf
            for move in legal_moves:
                board.push(move)
                eval, _ = self.minimax(board, depth - 1, alpha, beta, True)
                board.pop()
                if eval < min_eval:
                    min_eval = eval
                    best_move = move
                beta = min(beta, eval)
                if beta <= alpha:
                    break
            if self.transposition_table and best_move:
                self.transposition_table.store(zobrist_hash, depth, min_eval, 'exact', best_move.uci())
            return min_eval, best_move
    
    def quiescence_search(self, board, alpha, beta, maximizing_player):
        """
        Extends the search in non-quiet positions to avoid the horizon effect.
        Only captures and checks are considered in quiescence search.
        """
        score = self.evaluate_board(board)
        if maximizing_player:
            alpha = max(alpha, score)
            if alpha >= beta:
                return beta
        else:
            beta = min(beta, score)
            if beta <= alpha:
                return alpha

        # Generate "noisy" moves: captures, checks
        noisy_moves = [move for move in board.legal_moves if board.is_capture(move) or board.gives_check(move)]
        # Move ordering for quiescence search
        noisy_moves = self.order_moves(board, noisy_moves)

        for move in noisy_moves:
            board.push(move)
            eval = self.quiescence_search(board, alpha, beta, not maximizing_player)
            board.pop()
            if maximizing_player:
                alpha = max(alpha, eval)
                if alpha >= beta:
                    return beta
            else:
                beta = min(beta, eval)
                if beta <= alpha:
                    return alpha
        return score
    
    def evaluate_game_over(self, board):
        """
        Evaluates the board when the game is over.
        """
        result = board.result()
        if result == '1-0':
            return math.inf  # White wins
        elif result == '0-1':
            return -math.inf  # Black wins
        elif result == '1/2-1/2':
            return 0  # Draw
        else:
            return 0  # Game still ongoing (should not reach here)
    
    def order_moves(self, board, moves):
        """
        Orders moves based on heuristics to improve alpha-beta pruning efficiency.
        Prioritizes captures, checks, promotions.
        """
        move_scores = []
        for move in moves:
            score = 0
            if board.is_capture(move):
                captured_piece = board.piece_at(move.to_square)
                piece_value = {chess.PAWN:100, chess.KNIGHT:320, chess.BISHOP:330,
                              chess.ROOK:500, chess.QUEEN:900}.get(captured_piece.piece_type, 0)
                score += piece_value
            if board.gives_check(move):
                score += 50
            if move.promotion:
                score += 1000  # Prioritize promotions
            move_scores.append((score, move))
        # Sort moves descending by score
        sorted_moves = [move for score, move in sorted(move_scores, key=lambda x: x[0], reverse=True)]
        return sorted_moves
    
    def evaluate_board(self, board):
        """
        Evaluates the board using the evaluator.
        """
        return self.evaluator.evaluate(board)
    
    def select_move(self, board):
        """
        Selects the best move using iterative deepening and quiescence search.
        """
        best_move = None
        best_score = -math.inf
        try:
            for depth in range(1, self.depth + 1):
                score, move = self.minimax(board, depth, -math.inf, math.inf, board.turn)
                if move:
                    best_move = move
                    best_score = score
                    logging.info(f"Depth {depth}: Selected move {board.san(move)} with score {score}")
                # Optional: Implement time constraints here
        except Exception as e:
            logging.error(f"Error during select_move: {e}")
        return best_move

# ============================================
# Cognitive Tactical Engine (CTE)
# ============================================
class CognitiveTacticalEngine:
    """
    Integrates all components to form the Cognitive Tactical Engine (CTE).
    """
    def __init__(self):
        self.zobrist = ZobristHashing()
        self.nn_evaluator = NeuralNetworkEvaluator()
        self.transposition_table = TranspositionTable()
        self.profiler = PsychologicalProfiler()
        self.opening_book = OpeningBook()
        self.endgame_tablebase = EndgameTablebase()
        self.decision_tree = EnhancedDecisionTree(self.nn_evaluator, depth=4, use_transposition=True)
        self.deception = StrategicDeception()
        self.pattern_recognizer = TacticalPatternRecognizer()
        self.simulator = ScenarioSimulator(self.decision_tree, depth=3, num_threads=8)
        self.ml_predictor = MachineLearningPredictor()
        self.ml_predictor_loaded = False
        self.load_or_train_ml_model()
        self.color = chess.BLACK  # CTE always plays as Black
    
    def load_or_train_ml_model(self):
        """
        Loads the machine learning predictor if trained; otherwise, initializes it.
        """
        if self.ml_predictor.trained:
            self.ml_predictor_loaded = True
            logging.info("Machine Learning Predictor is ready.")
        else:
            # Placeholder: Train model if possible
            # To train, call self.ml_predictor.prepare_training_data with a valid PGN file
            logging.warning("Machine Learning Predictor is not trained with actual data.")
    
    def select_move(self, board):
        """
        Selects the best move by integrating various strategies and components.
        """
        try:
            # 1. Check if in opening book
            opening_move = self.opening_book.get_opening_move(board)
            if opening_move:
                return opening_move

            # 2. Check endgame tablebase
            endgame_move = self.endgame_tablebase.get_endgame_move(board)
            if endgame_move:
                return endgame_move

            # 3. Use Machine Learning Predictor
            ml_move = self.ml_predictor.predict(board)
            if ml_move and ml_move in board.legal_moves:
                return ml_move

            # 4. Use Decision Tree to select best move
            best_move = self.decision_tree.select_move(board)

            # 5. Apply Strategic Deception
            move = self.deception.maybe_deceive(board, best_move)

            # 6. Tactical Pattern Recognition
            if self.pattern_recognizer.recognize_patterns(board):
                capturing_moves = [m for m in board.legal_moves if board.is_capture(m)]
                if capturing_moves:
                    move = random.choice(capturing_moves)
                    logging.info(f"TacticalPatternRecognizer adjusted move to: {board.san(move)}")

            # Record CTE's move for profiling if needed
            return move
        except Exception as e:
            logging.error(f"Error in CognitiveTacticalEngine.select_move: {e}")
            # Fallback to a random legal move
            return self.fallback_move(board)
    
    def fallback_move(self, board):
        """
        Selects a random legal move as a fallback strategy.
        """
        try:
            move = random.choice(list(board.legal_moves))
            logging.info(f"Fallback move selected: {board.san(move)}")
            return move
        except Exception as e:
            logging.error(f"Error in fallback_move: {e}")
            return None
    
    def play_move(self, board):
        """
        Executes the selected move on the board.
        """
        move = self.select_move(board)
        if move:
            board.push(move)
            logging.info(f"CTE played move: {board.san(move)}")
            return move
        logging.warning("CTE could not make a move.")
        return None

# ============================================
# Opponent Players
# ============================================
class RandomPlayer:
    """
    Opponent that selects random legal moves.
    """
    def __init__(self):
        pass
    
    def select_move(self, board):
        """
        Selects a random legal move.
        """
        try:
            move = random.choice(list(board.legal_moves)) if board.legal_moves else None
            if move:
                logging.info(f"RandomPlayer selected move: {board.san(move)}")
            else:
                logging.info("RandomPlayer has no legal moves.")
            return move
        except Exception as e:
            logging.error(f"Error in RandomPlayer.select_move: {e}")
            return None

class CheckmateOrCapturePlayer:
    """
    Opponent that prioritizes captures and checkmates.
    """
    def __init__(self):
        pass
    
    def select_move(self, board):
        """
        Selects a capture move if available; otherwise, selects a move leading to checkmate.
        If neither, selects a random move.
        """
        try:
            if not board.legal_moves:
                logging.info("CheckmateOrCapturePlayer has no legal moves.")
                return None

            legal_moves = list(board.legal_moves)
            capturing_moves = [move for move in legal_moves if board.is_capture(move)]
            if capturing_moves:
                # Prioritize captures that gain the most material
                capturing_moves.sort(key=lambda move: self.capture_value(board, move), reverse=True)
                move = capturing_moves[0]
                logging.info(f"CheckmateOrCapturePlayer selected capture move: {board.san(move)}")
                return move

            # Try to find a move that results in checkmate
            for move in legal_moves:
                board.push(move)
                if board.is_checkmate():
                    board.pop()
                    logging.info(f"CheckmateOrCapturePlayer found checkmate move: {board.san(move)}")
                    return move
                board.pop()

            # Otherwise, pick a random move
            move = random.choice(legal_moves)
            logging.info(f"CheckmateOrCapturePlayer selected random move: {board.san(move)}")
            return move
        except Exception as e:
            logging.error(f"Error in CheckmateOrCapturePlayer.select_move: {e}")
            return None
    
    def capture_value(self, board, move):
        """
        Assigns a value to a capture move based on the piece being captured.
        Higher-valued captures are prioritized.
        """
        try:
            captured_piece = board.piece_at(move.to_square)
            if captured_piece:
                piece_value = {chess.PAWN:100, chess.KNIGHT:320, chess.BISHOP:330,
                              chess.ROOK:500, chess.QUEEN:900}.get(captured_piece.piece_type, 0)
                return piece_value
            return 0
        except:
            return 0

# ============================================
# Command Line Interface (CLI)
# ============================================
class ChessEngineCLI:
    """
    Advanced Command Line Interface for interacting with the CTE.
    """
    def __init__(self, engine, opponent):
        self.engine = engine
        self.opponent = opponent
        self.board = chess.Board()
        self.game = chess.pgn.Game()
        self.node = self.game
        self.move_history = []
        self.cte_color = engine.color  # CTE is always Black in this configuration

    def start_game(self):
        """
        Starts a game between the CTE and the opponent with an advanced CLI.
        """
        print("Welcome to the Advanced Cognitive Tactical Engine (CTE) CLI!\n")
        print("**Configuration:**")
        print("CTE is playing as Black.")
        print("You are playing as White.\n")
        print("Type 'help' to see available commands.\n")
        
        while not self.board.is_game_over():
            print(self.display_board())
            print("\nMove History:")
            self.print_move_history()
            print("\nEnter your move (e.g., e4, Nf3) or a command ('help' for commands):")
            user_input = input("> ").strip()

            if user_input.lower() == 'help':
                self.print_help()
                continue
            elif user_input.lower() == 'undo':
                self.undo_move()
                continue
            elif user_input.lower() == 'save':
                self.save_game()
                continue
            elif user_input.lower() == 'load':
                self.load_game()
                continue
            elif user_input.lower() == 'history':
                self.print_move_history()
                continue
            elif user_input.lower() == 'quit':
                self.quit_game()
                return
            else:
                # Attempt to parse the move
                try:
                    move = self.board.parse_san(user_input)
                    if move not in self.board.legal_moves:
                        print("Illegal move. Please try again.")
                        continue
                    self.board.push(move)
                    self.move_history.append(('Player', self.board.san(move)))
                    self.node = self.node.add_variation(move)
                    logging.info(f"Player moved: {self.board.san(move)}")
                except ValueError:
                    print("Invalid move format. Type 'help' for a list of commands.")
                    continue

            # Check if the game is over after player's move
            if self.board.is_game_over():
                self.end_game()
                break

            # Opponent's turn
            print("\nCTE is thinking...")
            opponent_move = self.engine.play_move(self.board)
            if opponent_move:
                self.move_history.append(('CTE', self.board.san(opponent_move)))
                self.node = self.node.add_variation(opponent_move)
                logging.info(f"CTE played move: {self.board.san(opponent_move)}")
                print(f"CTE played: {self.board.san(opponent_move)}")
            else:
                print("CTE has no legal moves.")

        # Game over
        self.end_game()
    
    def display_board(self):
        """
        Returns a string representation of the current board state with move numbers.
        """
        # To display move numbers, we'll create an annotated display
        # Since chess.Board() str doesn't include move numbers, we'll manually format it
        board_str = str(self.board)
        lines = board_str.split('\n')
        annotated_board = ""
        for i, line in enumerate(lines, start=8):
            annotated_board += f"{i} {line}\n"
        annotated_board += "  a b c d e f g h"
        return annotated_board
    
    def print_move_history(self):
        """
        Prints the move history in a clear format.
        """
        if not self.move_history:
            print("No moves made yet.")
            return
        for idx in range(0, len(self.move_history), 2):
            move_num = (idx // 2) + 1
            player_move = self.move_history[idx][1]
            cte_move = self.move_history[idx + 1][1] if (idx + 1) < len(self.move_history) else "-"
            print(f"{move_num}. {player_move} {cte_move}")
    
    def print_help(self):
        """
        Displays the list of available commands and their descriptions.
        """
        help_text = """
Available Commands:
- help      : Show this help message.
- undo      : Undo the last two moves (player and opponent).
- save      : Save the current game to a PGN file.
- load      : Load a game from a PGN file.
- history   : Display the move history.
- quit      : Quit the game.

To make a move, enter it using standard algebraic notation (e.g., e4, Nf3, O-O).
"""
        print(help_text)
    
    def undo_move(self):
        """
        Undoes the last two moves (opponent and user) to revert to the previous state.
        """
        if len(self.move_history) < 2:
            print("No moves to undo.")
            return
        try:
            # Undo opponent's move
            last_player, last_move = self.move_history.pop()
            self.board.pop()
            self.node = self.node.parent
            # Undo user's move
            last_player, last_move = self.move_history.pop()
            self.board.pop()
            self.node = self.node.parent
            print("Last two moves have been undone.")
            logging.info("Performed undo move operation.")
        except Exception as e:
            print("Error undoing move.")
            logging.error(f"Error in undo_move: {e}")
    
    def save_game(self):
        """
        Saves the current game to a PGN file.
        """
        try:
            pgn_filename = input("Enter the filename to save the game (e.g., game1.pgn): ").strip()
            if not pgn_filename.endswith('.pgn'):
                pgn_filename += '.pgn'
            with open(pgn_filename, "w") as pgn_file:
                exporter = chess.pgn.FileExporter(pgn_file)
                self.game.accept(exporter)
            print(f"Game successfully saved to '{pgn_filename}'.")
            logging.info(f"Game saved to '{pgn_filename}'.")
        except Exception as e:
            print("Failed to save the game.")
            logging.error(f"Error in save_game: {e}")
    
    def load_game(self):
        """
        Loads a game from a PGN file.
        """
        try:
            pgn_filename = input("Enter the filename to load the game from (e.g., game1.pgn): ").strip()
            if not os.path.exists(pgn_filename):
                print(f"File '{pgn_filename}' does not exist.")
                return
            with open(pgn_filename, 'r') as pgn_file:
                game = chess.pgn.read_game(pgn_file)
                if game is None:
                    print("No game found in the PGN file.")
                    return
                board = game.board()
                self.board = board.copy()
                self.game = game
                self.node = self.game
                self.move_history.clear()
                for move in game.mainline_moves():
                    san_move = self.board.san(move)
                    player = "CTE" if self.board.turn == chess.BLACK else "Player"
                    self.move_history.append((player, san_move))
                    self.board.push(move)
            print(f"Game loaded successfully from '{pgn_filename}'.")
            logging.info(f"Game loaded from '{pgn_filename}'.")
        except Exception as e:
            print("Failed to load the game.")
            logging.error(f"Error in load_game: {e}")
    
    def quit_game(self):
        """
        Exits the game after confirmation.
        """
        confirmation = input("Are you sure you want to quit? (y/n): ").strip().lower()
        if confirmation == 'y':
            print("Quitting the game.")
            logging.info("Game terminated by the user.")
            sys.exit()
        else:
            print("Continuing the game.")
    
    def end_game(self):
        """
        Handles the end of the game by displaying the result.
        """
        print("\nGame Over!")
        print(self.display_board())
        result = self.board.result()
        if result == "1-0":
            print("Result: White (Player) wins!")
        elif result == "0-1":
            print("Result: Black (CTE) wins!")
        elif result == "1/2-1/2":
            print("Result: It's a draw!")
        else:
            print("Result: Game terminated unexpectedly.")
        logging.info(f"Game ended with result: {result}")
        # Optionally, prompt to save the game
        save_confirmation = input("Would you like to save the game? (y/n): ").strip().lower()
        if save_confirmation == 'y':
            self.save_game()

# ============================================
# Graphical User Interface (GUI)
# ============================================
class ChessGUI:
    """
    Graphical User Interface for interacting with the CTE.
    """
    def __init__(self, root, engine, opponent):
        self.root = root
        self.engine = engine
        self.opponent = opponent
        self.board = chess.Board()
        self.canvas = tk.Canvas(root, width=480, height=480)
        self.canvas.pack()
        self.draw_board()
        self.canvas.bind("<Button-1>", self.on_click)
        self.selected_square = None
        self.update_board()
        self.game_over = False
        self.square_size = 60
        self.piece_images = self.load_piece_images()
        logging.info("GUI initialized.")
    
    def load_piece_images(self):
        """
        Loads Unicode characters for chess pieces.
        """
        unicode_pieces = {
            'P': '♙',
            'N': '♘',
            'B': '♗',
            'R': '♖',
            'Q': '♕',
            'K': '♔',
            'p': '♟︎',
            'n': '♞',
            'b': '♝',
            'r': '♜',
            'q': '♛',
            'k': '♚'
        }
        return unicode_pieces
    
    def draw_board(self):
        """
        Draws the chessboard squares.
        """
        colors = ["#F0D9B5", "#B58863"]  # Light and dark squares
        for i in range(8):
            for j in range(8):
                color = colors[(i + j) % 2]
                self.canvas.create_rectangle(j*self.square_size, i*self.square_size,
                                             (j+1)*self.square_size, (i+1)*self.square_size,
                                             fill=color, tags="square")
        logging.info("GUI chessboard drawn.")
    
    def update_board(self):
        """
        Updates the GUI to reflect the current board state.
        """
        self.canvas.delete("piece")
        for square in chess.SQUARES:
            piece = self.board.piece_at(square)
            if piece:
                x = (square % 8) * self.square_size + self.square_size / 2
                y = (7 - square // 8) * self.square_size + self.square_size / 2
                piece_unicode = self.piece_images.get(piece.symbol(), '')
                self.canvas.create_text(x, y, text=piece_unicode,
                                        font=("Arial", 32), tags="piece")
        self.root.update_idletasks()
        logging.debug("GUI board updated.")
    
    def on_click(self, event):
        """
        Handles user clicks on the chessboard.
        """
        if self.game_over:
            return
        
        col = event.x // self.square_size
        row = 7 - (event.y // self.square_size)
        square = chess.square(col, row)

        if self.selected_square is None:
            if self.board.piece_at(square) and self.board.turn == chess.WHITE:
                self.selected_square = square
                logging.info(f"Player selected square: {chess.square_name(square)}")
        else:
            move = chess.Move(self.selected_square, square)
            if move in self.board.legal_moves:
                self.board.push(move)
                logging.info(f"Player moved: {self.board.san(move)}")
                self.update_board()
                self.root.update()
                
                self.check_game_over()
                if self.game_over:
                    return
                
                # Opponent's turn
                opponent_move = self.engine.play_move(self.board)
                if opponent_move:
                    self.board.push(opponent_move)
                    logging.info(f"Opponent moved: {self.board.san(opponent_move)}")
                    self.update_board()
                    self.root.update()
            
                    self.check_game_over()
            else:
                logging.warning(f"Invalid move attempted: {move}")
            self.selected_square = None
    
    def check_game_over(self):
        """
        Checks if the game is over and handles accordingly.
        """
        if self.board.is_game_over():
            self.game_over = True
            result = self.board.result()
            if result == "1-0":
                message = "Game Over! White (Player) wins."
            elif result == "0-1":
                message = "Game Over! Black (Opponent) wins."
            elif result == "1/2-1/2":
                message = "Game Over! It's a draw."
            else:
                message = "Game Over!"
            messagebox.showinfo("Game Over", message)
            logging.info(f"Game Over: {message}")

# ============================================
# Command Line Interface (CLI) with Advanced Checker
# ============================================
class ChessEngineCLI:
    """
    Advanced Command Line Interface for interacting with the CTE.
    """
    def __init__(self, engine, opponent):
        self.engine = engine
        self.opponent = opponent
        self.board = chess.Board()
        self.game = chess.pgn.Game()
        self.node = self.game
        self.move_history = []
        self.cte_color = engine.color  # CTE is always Black in this configuration

        # Assert that CTE is playing Black
        if self.cte_color != chess.BLACK:
            logging.error("CTE is not configured to play as Black.")
            print("Error: CTE must be configured to play as Black.")
            sys.exit(1)

    def start_game(self):
        """
        Starts a game between the CTE and the opponent with an advanced CLI.
        """
        print("Welcome to the Advanced Cognitive Tactical Engine (CTE) CLI!\n")
        print("**Configuration:**")
        print("CTE is playing as Black.")
        print("You are playing as White.\n")
        print("Type 'help' to see available commands.\n")
        
        while not self.board.is_game_over():
            print(self.display_board())
            print("\nMove History:")
            self.print_move_history()
            print("\nEnter your move (e.g., e4, Nf3) or a command ('help' for commands):")
            user_input = input("> ").strip()

            if user_input.lower() == 'help':
                self.print_help()
                continue
            elif user_input.lower() == 'undo':
                self.undo_move()
                continue
            elif user_input.lower() == 'save':
                self.save_game()
                continue
            elif user_input.lower() == 'load':
                self.load_game()
                continue
            elif user_input.lower() == 'history':
                self.print_move_history()
                continue
            elif user_input.lower() == 'quit':
                self.quit_game()
                return
            else:
                # Attempt to parse the move
                try:
                    move = self.board.parse_san(user_input)
                    if move not in self.board.legal_moves:
                        print("Illegal move. Please try again.")
                        logging.warning(f"Player attempted illegal move: {user_input}")
                        continue
                    self.board.push(move)
                    self.move_history.append(('Player', self.board.san(move)))
                    self.node = self.node.add_variation(move)
                    logging.info(f"Player moved: {self.board.san(move)}")
                except ValueError:
                    print("Invalid move format. Type 'help' for a list of commands.")
                    logging.warning(f"Player entered invalid move format: {user_input}")
                    continue

            # Check if the game is over after player's move
            if self.board.is_game_over():
                self.end_game()
                break

            # Opponent's turn
            print("\nCTE is thinking...")
            opponent_move = self.engine.play_move(self.board)
            if opponent_move:
                self.move_history.append(('CTE', self.board.san(opponent_move)))
                self.node = self.node.add_variation(opponent_move)
                logging.info(f"CTE played move: {self.board.san(opponent_move)}")
                print(f"CTE played: {self.board.san(opponent_move)}")
            else:
                print("CTE has no legal moves.")

        # Game over
        self.end_game()
    
    def display_board(self):
        """
        Returns a string representation of the current board state with move numbers.
        """
        # To display move numbers, we'll create an annotated display
        # Since chess.Board() str doesn't include move numbers, we'll manually format it
        board_str = str(self.board)
        lines = board_str.split('\n')
        annotated_board = ""
        for i, line in enumerate(lines, start=8):
            annotated_board += f"{i} {line}\n"
        annotated_board += "  a b c d e f g h"
        return annotated_board
    
    def print_move_history(self):
        """
        Prints the move history in a clear format.
        """
        if not self.move_history:
            print("No moves made yet.")
            return
        for idx in range(0, len(self.move_history), 2):
            move_num = (idx // 2) + 1
            player_move = self.move_history[idx][1]
            cte_move = self.move_history[idx + 1][1] if (idx + 1) < len(self.move_history) else "-"
            print(f"{move_num}. {player_move} {cte_move}")
    
    def print_help(self):
        """
        Displays the list of available commands and their descriptions.
        """
        help_text = """
Available Commands:
- help      : Show this help message.
- undo      : Undo the last two moves (player and opponent).
- save      : Save the current game to a PGN file.
- load      : Load a game from a PGN file.
- history   : Display the move history.
- quit      : Quit the game.

To make a move, enter it using standard algebraic notation (e.g., e4, Nf3, O-O).
"""
        print(help_text)
    
    def undo_move(self):
        """
        Undoes the last two moves (opponent and user) to revert to the previous state.
        """
        if len(self.move_history) < 2:
            print("No moves to undo.")
            return
        try:
            # Undo opponent's move
            last_player, last_move = self.move_history.pop()
            self.board.pop()
            self.node = self.node.parent
            # Undo user's move
            last_player, last_move = self.move_history.pop()
            self.board.pop()
            self.node = self.node.parent
            print("Last two moves have been undone.")
            logging.info("Performed undo move operation.")
        except Exception as e:
            print("Error undoing move.")
            logging.error(f"Error in undo_move: {e}")
    
    def save_game(self):
        """
        Saves the current game to a PGN file.
        """
        try:
            pgn_filename = input("Enter the filename to save the game (e.g., game1.pgn): ").strip()
            if not pgn_filename.endswith('.pgn'):
                pgn_filename += '.pgn'
            with open(pgn_filename, "w") as pgn_file:
                exporter = chess.pgn.FileExporter(pgn_file)
                self.game.accept(exporter)
            print(f"Game successfully saved to '{pgn_filename}'.")
            logging.info(f"Game saved to '{pgn_filename}'.")
        except Exception as e:
            print("Failed to save the game.")
            logging.error(f"Error in save_game: {e}")
    
    def load_game(self):
        """
        Loads a game from a PGN file.
        """
        try:
            pgn_filename = input("Enter the filename to load the game from (e.g., game1.pgn): ").strip()
            if not os.path.exists(pgn_filename):
                print(f"File '{pgn_filename}' does not exist.")
                return
            with open(pgn_filename, 'r') as pgn_file:
                game = chess.pgn.read_game(pgn_file)
                if game is None:
                    print("No game found in the PGN file.")
                    return
                board = game.board()
                self.board = board.copy(stack=False)
                self.game = game
                self.node = self.game
                self.move_history.clear()
                for move in game.mainline_moves():
                    san_move = self.board.san(move)
                    player = "CTE" if self.board.turn == chess.BLACK else "Player"
                    self.move_history.append((player, san_move))
                    self.board.push(move)
            # Validate that CTE is playing Black in the loaded game
            if not self.validate_loaded_game():
                print("Loaded game does not have CTE playing as Black. Please load a valid game.")
                logging.warning("Loaded game does not have CTE playing as Black.")
                self.board.reset()
                self.game = chess.pgn.Game()
                self.node = self.game
                self.move_history.clear()
                return
            print(f"Game loaded successfully from '{pgn_filename}'.")
            logging.info(f"Game loaded from '{pgn_filename}'.")
        except Exception as e:
            print("Failed to load the game.")
            logging.error(f"Error in load_game: {e}")
    
    def validate_loaded_game(self):
        """
        Validates that CTE is playing as Black in the loaded game.
        """
        try:
            # Ensure that the first move was made by White
            if not self.move_history:
                return False
            first_move_player = self.move_history[0][0]
            if first_move_player != 'Player':
                return False
            # Ensure that even-indexed moves are by the Player (White) and odd-indexed by CTE (Black)
            for idx, (player, move) in enumerate(self.move_history):
                expected_player = 'Player' if idx % 2 == 0 else 'CTE'
                if player != expected_player:
                    return False
            return True
        except Exception as e:
            logging.error(f"Error in validate_loaded_game: {e}")
            return False
    
    def quit_game(self):
        """
        Exits the game after confirmation.
        """
        confirmation = input("Are you sure you want to quit? (y/n): ").strip().lower()
        if confirmation == 'y':
            print("Quitting the game.")
            logging.info("Game terminated by the user.")
            sys.exit()
        else:
            print("Continuing the game.")
    
    def end_game(self):
        """
        Handles the end of the game by displaying the result.
        """
        print("\nGame Over!")
        print(self.display_board())
        result = self.board.result()
        if result == "1-0":
            print("Result: White (Player) wins!")
        elif result == "0-1":
            print("Result: Black (CTE) wins!")
        elif result == "1/2-1/2":
            print("Result: It's a draw!")
        else:
            print("Result: Game terminated unexpectedly.")
        logging.info(f"Game ended with result: {result}")
        # Optionally, prompt to save the game
        save_confirmation = input("Would you like to save the game? (y/n): ").strip().lower()
        if save_confirmation == 'y':
            self.save_game()

# ============================================
# Main Function
# ============================================
def play_game(engine, opponent, mode='CLI'):
    """
    Facilitates the game between the engine and the opponent based on the selected mode.
    """
    if mode == 'CLI':
        cli = ChessEngineCLI(engine, opponent)
        cli.start_game()
    elif mode == 'GUI':
        root = tk.Tk()
        root.title("Cognitive Tactical Engine (CTE) vs. Opponent")
        gui = ChessGUI(root, engine, opponent)
        root.mainloop()
    else:
        logging.error("Invalid game mode selected.")
        print("Invalid game mode selected.")

def main():
    """
    Main function to initialize components and start the game.
    """
    print("Welcome to the Advanced Cognitive Tactical Engine (CTE)!\n")
    print("Select opponent type:")
    print("1. Random Player")
    print("2. Checkmate or Capture Player")
    opponent_choice = input("Enter choice (1 or 2): ")
    
    if opponent_choice == '1':
        opponent = RandomPlayer()
    elif opponent_choice == '2':
        opponent = CheckmateOrCapturePlayer()
    else:
        print("Invalid choice. Defaulting to Random Player.")
        opponent = RandomPlayer()
    
    cte = CognitiveTacticalEngine()
    
    print("\nSelect mode:")
    print("1. Command Line Interface (CLI)")
    print("2. Graphical User Interface (GUI)")
    mode_choice = input("Enter choice (1 or 2): ")
    
    if mode_choice == '1':
        play_game(cte, opponent, mode='CLI')
    elif mode_choice == '2':
        play_game(cte, opponent, mode='GUI')
    else:
        print("Invalid mode selected.")
        logging.error("Invalid mode selected by user.")

if __name__ == "__main__":
    main()