Further optimize this script, and add a more traditional ASCII chessboard:

```
import chess
import chess.pgn
import random
import math
import sys
import logging
from colorama import init, Fore, Style
import time
import hashlib
import numpy as np
import json

# Initialize colorama for colored terminal output
init(autoreset=True)

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

class TranspositionTable:
    """
    Transposition Table using Zobrist Hashing for caching evaluated positions.
    """
    def __init__(self, size=1000000):
        self.table = {}
        self.size = size  # Maximum number of entries

    def get(self, key):
        return self.table.get(key, None)

    def set(self, key, value):
        if len(self.table) > self.size:
            # Simple eviction policy: remove the first inserted item (FIFO)
            self.table.pop(next(iter(self.table)))
        self.table[key] = value

def zobrist_hash(board):
    """
    Generates a Zobrist hash for the current board state using SHA-256 on the FEN string.
    """
    return hashlib.sha256(board.fen().encode()).hexdigest()

class DrunkMagnusEngine:
    def __init__(self, config_path='config.json'):
        """
        Initializes the Drunk Magnus Engine with comprehensive parameters from a config file.

        :param config_path: Path to the JSON configuration file.
        """
        # Load configuration parameters
        try:
            with open(config_path, 'r') as f:
                config = json.load(f)
        except FileNotFoundError:
            print(Fore.RED + f"Configuration file '{config_path}' not found.")
            sys.exit(1)
        except json.JSONDecodeError:
            print(Fore.RED + f"Configuration file '{config_path}' contains invalid JSON.")
            sys.exit(1)
        
        self.board = chess.Board()
        self.initial_depth = config.get('depth', 6)  # Default search depth
        self.current_depth = self.initial_depth
        self.randomness_config = config.get('randomness_config', {
            'opening': 0.10,
            'middlegame': 0.25,
            'endgame': 0.15
        })
        self.blunder_rate = config.get('blunder_rate', 0.05)
        self.time_per_move = config.get('time_per_move', 5.0)  # Time per move in seconds
        self.transposition_table = TranspositionTable(size=config.get('transposition_table_size', 1000000))
        self.move_count = 0
        self.blunder_count = 0

        # Piece weights from configuration
        self.piece_weights = {
            chess.PAWN: config['piece_weights'].get('PAWN', 100),
            chess.KNIGHT: config['piece_weights'].get('KNIGHT', 320),
            chess.BISHOP: config['piece_weights'].get('BISHOP', 330),
            chess.ROOK: config['piece_weights'].get('ROOK', 500),
            chess.QUEEN: config['piece_weights'].get('QUEEN', 900),
            chess.KING: config['piece_weights'].get('KING', 20000)
        }

        # Enhanced piece-square tables using numpy arrays for efficiency
        self.piece_square_tables = self.init_piece_square_tables()

        # Move ordering heuristics
        self.killer_moves = {}
        self.history_heuristic = {}

    def init_piece_square_tables(self):
        """
        Initializes detailed piece-square tables for enhanced positional evaluations.

        :return: Dictionary containing piece-square tables for each piece type.
        """
        pst = {
            chess.PAWN: np.array([
                0, 0, 0, 0, 0, 0, 0, 0,
                50, 50, 50, 50, 50, 50, 50, 50,
                10, 10, 20, 30, 30, 20, 10, 10,
                5, 5, 10, 25, 25, 10, 5, 5,
                0, 0, 0, 20, 20, 0, 0, 0,
                5, -5, -10, 0, 0, -10, -5, 5,
                5, 10, 10, -20, -20, 10, 10, 5,
                0, 0, 0, 0, 0, 0, 0, 0
            ]),
            chess.KNIGHT: np.array([
                -50, -40, -30, -30, -30, -30, -40, -50,
                -40, -20, 0, 5, 5, 0, -20, -40,
                -30, 5, 10, 15, 15, 10, 5, -30,
                -30, 0, 15, 20, 20, 15, 0, -30,
                -30, 5, 15, 20, 20, 15, 5, -30,
                -30, 0, 10, 15, 15, 10, 0, -30,
                -40, -20, 0, 0, 0, 0, -20, -40,
                -50, -40, -30, -30, -30, -30, -40, -50
            ]),
            chess.BISHOP: np.array([
                -20, -10, -10, -10, -10, -10, -10, -20,
                -10, 5, 0, 0, 0, 0, 5, -10,
                -10, 10, 10, 10, 10, 10, 10, -10,
                -10, 0, 10, 10, 10, 10, 0, -10,
                -10, 5, 5, 10, 10, 5, 5, -10,
                -10, 0, 5, 10, 10, 5, 0, -10,
                -10, 0, 0, 0, 0, 0, 0, -10,
                -20, -10, -10, -10, -10, -10, -10, -20
            ]),
            chess.ROOK: np.array([
                0, 0, 0, 0, 0, 0, 0, 0,
                5, 10, 10, 10, 10, 10, 10, 5,
                -5, 0, 0, 0, 0, 0, 0, -5,
                -5, 0, 0, 0, 0, 0, 0, -5,
                -5, 0, 0, 0, 0, 0, 0, -5,
                -5, 0, 0, 0, 0, 0, 0, -5,
                -5, 0, 0, 0, 0, 0, 0, -5,
                0, 0, 0, 5, 5, 0, 0, 0
            ]),
            chess.QUEEN: np.array([
                -20, -10, -10, -5, -5, -10, -10, -20,
                -10, 0, 5, 0, 0, 0, 0, -10,
                -10, 5, 5, 5, 5, 5, 0, -10,
                0, 0, 5, 5, 5, 5, 0, -5,
                -5, 0, 5, 5, 5, 5, 0, -5,
                -10, 0, 5, 5, 5, 5, 0, -10,
                -10, 0, 0, 0, 0, 0, 0, -10,
                -20, -10, -10, -5, -5, -10, -10, -20
            ]),
            chess.KING: np.array([
                -30, -40, -40, -50, -50, -40, -40, -30,
                -30, -40, -40, -50, -50, -40, -40, -30,
                -30, -40, -40, -50, -50, -40, -40, -30,
                -30, -40, -40, -50, -50, -40, -40, -30,
                -20, -30, -30, -40, -40, -30, -30, -20,
                -10, -20, -20, -20, -20, -20, -20, -10,
                 20, 20, 0, 0, 0, 0, 20, 20,
                 20, 30, 10, 0, 0, 10, 30, 20
            ])
        }
        return pst

    def evaluate_board(self):
        """
        Comprehensive evaluation of the current board state from the AI's perspective.

        :return: Numerical score representing the board position.
        """
        if self.board.is_checkmate():
            if self.board.turn:
                return -math.inf  # AI is in checkmate
            else:
                return math.inf   # AI has delivered checkmate
        if self.board.is_stalemate() or self.board.is_insufficient_material():
            return 0

        score = 0
        # Material and positional evaluation
        for piece_type in self.piece_weights:
            score += self.count_piece_value(piece_type, chess.WHITE)
            score -= self.count_piece_value(piece_type, chess.BLACK)

        # Additional evaluation components
        score += self.evaluate_piece_mobility(chess.WHITE)
        score -= self.evaluate_piece_mobility(chess.BLACK)

        score += self.evaluate_king_safety(chess.WHITE)
        score -= self.evaluate_king_safety(chess.BLACK)

        score += self.evaluate_pawn_structure(chess.WHITE)
        score -= self.evaluate_pawn_structure(chess.BLACK)

        score += self.evaluate_control_of_center()

        score += self.evaluate_threats(chess.WHITE)
        score -= self.evaluate_threats(chess.BLACK)

        score += self.evaluate_pinned_pieces(chess.WHITE)
        score -= self.evaluate_pinned_pieces(chess.BLACK)

        # Endgame adjustments
        total_pieces = len(self.board.piece_map())
        if total_pieces <= 10:
            score += self.evaluate_endgame(chess.WHITE)
            score -= self.evaluate_endgame(chess.BLACK)

        logging.debug(f"Board evaluation: {score}")
        return score

    def count_piece_value(self, piece_type, color):
        """
        Counts the total value of a specific piece type for a given color, including positional bonuses.

        :param piece_type: Type of the piece (e.g., chess.PAWN).
        :param color: Color of the pieces (chess.WHITE or chess.BLACK).
        :return: Total value of the pieces.
        """
        total = 0
        for square in self.board.pieces(piece_type, color):
            total += self.piece_weights[piece_type] + self.piece_square_tables[piece_type][square]
        return total

    def evaluate_piece_mobility(self, color):
        """
        Evaluates mobility based on the number of legal moves available to the pieces.

        :param color: Color to evaluate (chess.WHITE or chess.BLACK).
        :return: Mobility score.
        """
        mobility = 0
        for piece_type in [chess.PAWN, chess.KNIGHT, chess.BISHOP, chess.ROOK, chess.QUEEN]:
            pieces = list(self.board.pieces(piece_type, color))
            mobility += len(pieces)
            for square in pieces:
                mobility += len(list(self.board.attacks(square)))
        mobility_score = 0.1 * mobility
        logging.debug(f"Mobility for {'White' if color else 'Black'}: {mobility_score}")
        return mobility_score

    def evaluate_king_safety(self, color):
        """
        Evaluates the safety of the king.

        :param color: Color of the king (chess.WHITE or chess.BLACK).
        :return: King safety score.
        """
        king_square = self.board.king(color)
        if king_square is None:
            return 0  # Game over conditions handled elsewhere

        # Define a safety zone around the king (radius 2 squares)
        safe_zone = self.get_safe_zone(king_square, radius=2)

        # Count enemy pieces attacking the safe zone
        enemy_color = not color
        attacks = 0
        for sq in safe_zone:
            attacks += len(list(self.board.attackers(enemy_color, sq)))

        # Penalize based on the number of attacks
        safety = -20 * attacks
        logging.debug(f"King safety for {'White' if color else 'Black'}: {safety}")
        return safety

    def get_safe_zone(self, square, radius=2):
        """
        Returns squares around the king that constitute its safety zone.

        :param square: Square of the king.
        :param radius: Radius around the king to define the safety zone.
        :return: Set of squares in the safe zone.
        """
        safe_zone = set()
        for dr in range(-radius, radius + 1):
            for df in range(-radius, radius + 1):
                if dr == 0 and df == 0:
                    safe_zone.add(square)
                else:
                    target_file = chess.square_file(square) + df
                    target_rank = chess.square_rank(square) + dr
                    if 0 <= target_file <= 7 and 0 <= target_rank <= 7:
                        target_square = chess.square(target_file, target_rank)
                        safe_zone.add(target_square)
        return safe_zone

    def evaluate_pawn_structure(self, color):
        """
        Evaluates pawn structure, penalizing doubled, isolated, and backward pawns.

        :param color: Color of the pawns (chess.WHITE or chess.BLACK).
        :return: Pawn structure score.
        """
        score = 0
        pawns = self.board.pieces(chess.PAWN, color)
        files = [chess.square_file(sq) for sq in pawns]
        file_counts = {}
        for f in files:
            file_counts[f] = file_counts.get(f, 0) + 1

        # Penalize doubled pawns
        for f, count in file_counts.items():
            if count > 1:
                penalty = -50 * (count - 1)
                score += penalty

        # Penalize isolated pawns
        for f in file_counts:
            if f - 1 not in file_counts and f + 1 not in file_counts:
                score += -20

        # Bonus for passed pawns
        for pawn in pawns:
            if self.is_passed_pawn(pawn, color):
                score += 25

        logging.debug(f"Pawn structure for {'White' if color else 'Black'}: {score}")
        return score

    def is_passed_pawn(self, pawn_square, color):
        """
        Determines if a pawn is a passed pawn.

        :param pawn_square: Square of the pawn.
        :param color: Color of the pawn.
        :return: Boolean indicating if the pawn is passed.
        """
        file = chess.square_file(pawn_square)
        rank = chess.square_rank(pawn_square)
        enemy_color = not color

        if color == chess.WHITE:
            advanced_squares = range(rank + 1, 8)
        else:
            advanced_squares = range(rank - 1, -1, -1)

        for r in advanced_squares:
            for f in [file - 1, file, file + 1]:
                if 0 <= f <= 7:
                    target_square = chess.square(f, r)
                    piece = self.board.piece_at(target_square)
                    if piece and piece.color == enemy_color and piece.piece_type == chess.PAWN:
                        return False
        return True

    def evaluate_control_of_center(self):
        """
        Evaluates control over the center squares.

        :return: Control of center score.
        """
        center_squares = [chess.D4, chess.E4, chess.D5, chess.E5]
        white_control = 0
        black_control = 0
        for square in center_squares:
            white_control += len(list(self.board.attackers(chess.WHITE, square)))
            black_control += len(list(self.board.attackers(chess.BLACK, square)))
        control_score = 10 * (white_control - black_control)
        logging.debug(f"Control of center: {control_score}")
        return control_score

    def evaluate_threats(self, color):
        """
        Evaluates immediate threats such as captures and possible checkmates.

        :param color: Color to evaluate threats for (chess.WHITE or chess.BLACK).
        :return: Threats score.
        """
        threats = 0
        enemy_color = not color
        for move in self.board.legal_moves:
            if self.board.piece_at(move.from_square).color != color:
                continue
            if self.board.is_capture(move):
                captured_piece = self.board.piece_at(move.to_square)
                if captured_piece:
                    value = self.piece_weights[captured_piece.piece_type]
                    threats += value
        logging.debug(f"Threats for {'White' if color else 'Black'}: {threats}")
        return threats

    def evaluate_pinned_pieces(self, color):
        """
        Evaluates and penalizes positions where pieces are pinned.

        :param color: Color to evaluate (chess.WHITE or chess.BLACK).
        :return: Pinned pieces score.
        """
        score = 0
        # Pinned pieces can be detected using chess.Board.is_pinned
        for piece_type in [chess.PAWN, chess.KNIGHT, chess.BISHOP, chess.ROOK, chess.QUEEN]:
            for square in self.board.pieces(piece_type, color):
                if self.board.is_pinned(color, square):
                    score -= self.piece_weights[piece_type] * 0.5
        logging.debug(f"Pinned pieces for {'White' if color else 'Black'}: {score}")
        return score

    def evaluate_endgame(self, color):
        """
        Evaluates endgame-specific factors such as king activity.

        :param color: Color to evaluate (chess.WHITE or chess.BLACK).
        :return: Endgame evaluation score.
        """
        king_square = self.board.king(color)
        if king_square is None:
            return 0  # Game over conditions handled elsewhere

        # Encouraging centralization: the closer the king is to the center, the better
        center_files = [chess.FILE_D, chess.FILE_E]
        center_ranks = [chess.RANK_4, chess.RANK_5]
        king_file = chess.square_file(king_square)
        king_rank = chess.square_rank(king_square)
        distance = 0
        if king_file not in center_files:
            distance += min([abs(king_file - f) for f in center_files])
        if king_rank not in center_ranks:
            distance += min([abs(king_rank - r) for r in center_ranks])
        endgame_score = -10 * distance  # Centralization is good
        logging.debug(f"Endgame evaluation for {'White' if color else 'Black'}: {endgame_score}")
        return endgame_score

    def minimax(self, depth, alpha, beta, maximizing, start_time, time_limit):
        """
        Minimax algorithm with alpha-beta pruning, quiescence search, and transposition tables.

        :param depth: Current depth in the search tree.
        :param alpha: Alpha value for pruning.
        :param beta: Beta value for pruning.
        :param maximizing: Boolean indicating if the current layer is maximizing.
        :param start_time: Time when the search started.
        :param time_limit: Time allotted for the search.
        :return: (score, best_move)
        """
        if time.time() - start_time > time_limit:
            self.stop_search = True
            return 0, None

        board_hash = zobrist_hash(self.board)
        tt_entry = self.transposition_table.get(board_hash)
        if tt_entry and tt_entry['depth'] >= depth:
            return tt_entry['score'], tt_entry['move']

        if depth == 0:
            score = self.quiescence_search(alpha, beta, maximizing, start_time, time_limit)
            return score, None

        if self.board.is_game_over():
            score = self.evaluate_board()
            return score, None

        legal_moves = list(self.board.legal_moves)
        ordered_moves = self.order_moves(legal_moves)

        best_move = None

        if maximizing:
            max_eval = -math.inf
            for move in ordered_moves:
                self.board.push(move)
                eval, _ = self.minimax(depth - 1, alpha, beta, False, start_time, time_limit)
                self.board.pop()
                if self.stop_search:
                    return 0, None
                if eval > max_eval:
                    max_eval = eval
                    best_move = move
                alpha = max(alpha, eval)
                if beta <= alpha:
                    break
            self.transposition_table.set(board_hash, {'score': max_eval, 'move': best_move, 'depth': depth})
            return max_eval, best_move
        else:
            min_eval = math.inf
            for move in ordered_moves:
                self.board.push(move)
                eval, _ = self.minimax(depth - 1, alpha, beta, True, start_time, time_limit)
                self.board.pop()
                if self.stop_search:
                    return 0, None
                if eval < min_eval:
                    min_eval = eval
                    best_move = move
                beta = min(beta, eval)
                if beta <= alpha:
                    break
            self.transposition_table.set(board_hash, {'score': min_eval, 'move': best_move, 'depth': depth})
            return min_eval, best_move

    def quiescence_search(self, alpha, beta, maximizing, start_time, time_limit):
        """
        Extends the search in volatile positions to avoid the horizon effect.

        :param alpha: Alpha value for pruning.
        :param beta: Beta value for pruning.
        :param maximizing: Boolean indicating if the current layer is maximizing.
        :param start_time: Time when the search started.
        :param time_limit: Time allotted for the search.
        :return: Evaluation score.
        """
        if time.time() - start_time > time_limit:
            self.stop_search = True
            return 0

        score = self.evaluate_board()
        if score >= beta:
            return beta
        if score > alpha:
            alpha = score

        # Include only capture moves in quiescence
        capture_moves = [move for move in self.board.legal_moves if self.board.is_capture(move)]
        ordered_captures = self.order_moves(capture_moves)

        for move in ordered_captures:
            self.board.push(move)
            eval = self.quiescence_search(alpha, beta, not maximizing, start_time, time_limit)
            self.board.pop()
            if self.stop_search:
                return 0
            if maximizing:
                if eval > score:
                    score = eval
                if score > alpha:
                    alpha = score
                if score >= beta:
                    return beta
            else:
                if eval < score:
                    score = eval
                if score < beta:
                    beta = score
                if score <= alpha:
                    return alpha
        return score

    def order_moves(self, moves):
        """
        Orders moves to improve Minimax efficiency using MVV-LVA and history heuristics.

        :param moves: Iterable of legal moves.
        :return: List of ordered moves.
        """
        def move_order(move):
            score = 0
            # Most Valuable Victim - Least Valuable Aggressor (MVV-LVA)
            if self.board.is_capture(move):
                captured_piece = self.board.piece_at(move.to_square)
                if captured_piece:
                    score += 10 * self.piece_weights[captured_piece.piece_type]
                aggressor_piece = self.board.piece_at(move.from_square)
                if aggressor_piece:
                    score += self.piece_weights[aggressor_piece.piece_type]
            # History heuristic
            score += self.history_heuristic.get(move, 0)
            # Killer moves
            if move in self.killer_moves.get(self.current_depth, []):
                score += 500
            return score

        return sorted(moves, key=move_order, reverse=True)

    def choose_move(self):
        """
        Chooses the best move based on minimax evaluation with iterative deepening, randomness, and blunder simulation.

        :return: Chosen move.
        """
        self.move_count += 1
        current_phase = self.get_game_phase()
        randomness = self.randomness_config.get(current_phase, 0.2)

        # Determine time allocation
        time_limit = self.time_per_move

        # Start iterative deepening
        best_move = None
        for depth in range(1, self.initial_depth + 1):
            self.current_depth = depth
            self.stop_search = False
            start_time = time.time()
            eval, move = self.minimax(depth, -math.inf, math.inf, self.board.turn, start_time, time_limit)
            if self.stop_search:
                break
            if move:
                best_move = move
            # Early stopping if game is likely over
            if abs(eval) == math.inf:
                break

        # Decide whether to make a blunder
        if random.random() < self.blunder_rate:
            blunder_move = self.get_blunder_move()
            if blunder_move:
                logging.info(f"Blunder made on move {self.move_count}: {self.board.san(blunder_move)}")
                self.blunder_count += 1
                return blunder_move

        # Decide whether to make a random suboptimal move based on phase
        if random.random() < randomness:
            suboptimal_move = self.get_suboptimal_move()
            if suboptimal_move:
                logging.info(f"Suboptimal move on move {self.move_count}: {self.board.san(suboptimal_move)}")
                return suboptimal_move

        # Make the optimal move
        if best_move:
            logging.info(f"Optimal move on move {self.move_count}: {self.board.san(best_move)}")
        return best_move

    def get_blunder_move(self):
        """
        Generates a blunder by selecting a move that significantly worsens the board.

        :return: Blunder move.
        """
        legal_moves = list(self.board.legal_moves)
        if not legal_moves:
            return None

        # Simulate making each move and evaluate
        evaluated_moves = []
        for move in legal_moves:
            self.board.push(move)
            eval_score = self.evaluate_board()
            self.board.pop()
            evaluated_moves.append((eval_score, move))

        # Choose the worst move for the current player
        if self.board.turn:
            # AI is White
            worst_eval, worst_move = min(evaluated_moves, key=lambda x: x[0])
        else:
            # AI is Black
            worst_eval, worst_move = max(evaluated_moves, key=lambda x: x[0])

        return worst_move

    def get_suboptimal_move(self):
        """
        Selects a suboptimal move by avoiding the top N moves.

        :return: Suboptimal move.
        """
        n = 2  # Avoid top N moves
        legal_moves = list(self.board.legal_moves)
        if not legal_moves:
            return None

        evaluated_moves = []
        for move in legal_moves:
            self.board.push(move)
            eval_score = self.evaluate_board()
            self.board.pop()
            evaluated_moves.append((eval_score, move))

        if self.board.turn:
            # Higher eval is better
            evaluated_moves.sort(key=lambda x: x[0], reverse=True)
        else:
            # Lower eval is better
            evaluated_moves.sort(key=lambda x: x[0])

        # Exclude top N moves
        suboptimal_choices = evaluated_moves[n:]
        if not suboptimal_choices:
            return None
        _, move = random.choice(suboptimal_choices)
        return move

    def get_game_phase(self):
        """
        Determines the current phase of the game based on the number of pieces on the board.

        :return: Game phase as a string (`'opening'`, `'middlegame'`, or `'endgame'`).
        """
        total_pieces = len(self.board.piece_map())
        if total_pieces > 24:
            return 'opening'
        elif 10 < total_pieces <= 24:
            return 'middlegame'
        else:
            return 'endgame'

    def make_move(self, move_uci):
        """
        Attempts to make a move on the board.

        :param move_uci: Move in UCI notation (e.g., e2e4).
        :return: Response message or None if successful.
        """
        try:
            move = self.board.parse_uci(move_uci)
            if move in self.board.legal_moves:
                if not self.board.is_legal(move):
                    logging.error(f"Illegal move attempted: {move}")
                    return None
                san_move = self.board.san(move)  # Generate SAN before pushing
                self.board.push(move)
                logging.info(f"Player move: {san_move}")
                return None
            else:
                return "Illegal move. Please try again."
        except ValueError:
            return "Invalid move format. Please use UCI notation (e.g., e2e4)."

    def play_engine_move(self):
        """
        Determines and makes the engine's move.

        :return: Engine's move in SAN notation.
        """
        move = self.choose_move()
        if move:
            self.board.push(move)
            if not self.board.is_legal(move):
                logging.error(f"Illegal move attempted: {move}")
                return None
            san_move = self.board.san(move)
            logging.info(f"Drunk Magnus move {self.move_count}: {san_move}")
            return san_move
        return None

    def display_board(self):
        """
        Returns a string representation of the current board with colors.

        :return: Colored board as a string.
        """
        board_str = self.board.unicode(borders=True)
        # Enhance board display with colors
        colored_board = ""
        for line in board_str.split('\n'):
            colored_line = ""
            for char in line:
                if char in ['♙', '♖', '♘', '♗', '♕', '♔']:
                    colored_line += Fore.GREEN + char + Style.RESET_ALL
                elif char in ['♟', '♜', '♞', '♝', '♛', '♚']:
                    colored_line += Fore.RED + char + Style.RESET_ALL
                else:
                    colored_line += char
            colored_board += colored_line + '\n'
        return colored_board

    def is_game_over(self):
        """
        Checks if the game is over.

        :return: Boolean indicating game over status.
        """
        return self.board.is_game_over()

    def get_game_result(self):
        """
        Determines the result of the game.

        :return: Result string.
        """
        if self.board.is_checkmate():
            if self.board.turn:
                return "You win by checkmate!"
            else:
                return "Drunk Magnus wins by checkmate!"
        elif self.board.is_stalemate():
            return "Draw by stalemate."
        elif self.board.is_insufficient_material():
            return "Draw due to insufficient material."
        elif self.board.can_claim_fifty_moves():
            return "Draw by fifty-move rule."
        elif self.board.can_claim_threefold_repetition():
            return "Draw by threefold repetition."
        else:
            return f"Game over: {self.board.result()}"

    def display_statistics(self):
        """
        Displays game statistics.
        """
        print(Fore.CYAN + "\nGame Statistics:")
        print(f"Total moves made: {self.move_count}")
        print(f"Blunders made by Drunk Magnus: {self.blunder_count}")
        blunder_percentage = (self.blunder_count / self.move_count) * 100 if self.move_count > 0 else 0
        print(f"Blunder rate: {blunder_percentage:.2f}%")
        print(f"Transposition Table Size: {len(self.transposition_table.table)}")
        print(Style.RESET_ALL)

def main():
    # Initialize the chess engine with configuration
    engine = DrunkMagnusEngine(config_path='config.json')
    print(Fore.MAGENTA + "Welcome to Advanced Drunk Magnus Carlsen Bot!")
    print("You are playing as White. Enter your moves in UCI notation (e.g., e2e4) or type 'quit' to exit.\n")

    while not engine.is_game_over():
        print(engine.display_board())
        
        if engine.board.turn == chess.WHITE:
            # Player's turn
            user_move = input(Fore.YELLOW + "Your move: " + Style.RESET_ALL).strip()
            if user_move.lower() == 'quit':
                print("Game terminated by user.")
                sys.exit()
            response = engine.make_move(user_move)
            if response:
                print(Fore.RED + response + Style.RESET_ALL)
                continue
            logging.info(f"Move count: {engine.move_count}")
            if engine.is_game_over():
                break
        else:
            # AI's turn
            if engine.board.turn == chess.BLACK:  # Ensure it is Black's turn
                engine_move = engine.play_engine_move()
            else:
                logging.error("Attempted to play Black's move on White's turn.")
                continue
            if engine_move:
                print(Fore.BLUE + f"Drunk Magnus moves: {engine_move}" + Style.RESET_ALL)

    print(Fore.MAGENTA + "\nGame over!")
    print(engine.display_board())
    print(Fore.CYAN + engine.get_game_result())
    engine.display_statistics()

if __name__ == "__main__":
    main()