Traceback (most recent call last):
  File "C:\Users\hp\Downloads\gambit2.py", line 1129, in <module>
    main()
  File "C:\Users\hp\Downloads\gambit2.py", line 1115, in main
    engine_move = engine.play_engine_move()
                  ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\hp\Downloads\gambit2.py", line 872, in play_engine_move
    move = self.iterated_super_deepening()
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\hp\Downloads\gambit2.py", line 341, in iterated_super_deepening
    best_move = future.result(timeout=self.time_limit)
                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\hp\AppData\Local\Programs\Python\Python312\Lib\concurrent\futures\_base.py", line 449, in result
    return self.__get_result()
           ^^^^^^^^^^^^^^^^^^^
  File "C:\Users\hp\AppData\Local\Programs\Python\Python312\Lib\concurrent\futures\_base.py", line 401, in __get_result
    raise self._exception
  File "C:\Users\hp\AppData\Local\Programs\Python\Python312\Lib\concurrent\futures\thread.py", line 58, in run
    result = self.fn(*self.args, **self.kwargs)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\hp\Downloads\gambit2.py", line 356, in iterative_super_deepening_search
    eval, move = self.super_deepening_pvs(tuned_depth, float('-inf'), float('inf'))
                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\hp\Downloads\gambit2.py", line 371, in super_deepening_pvs
    legal_moves = self.apply_tactical_pruning_filters(legal_moves)
                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\hp\Downloads\gambit2.py", line 408, in apply_tactical_pruning_filters
    tactical_score = self.evaluate_move_tactics(move)
                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\hp\Downloads\gambit2.py", line 426, in evaluate_move_tactics
    if self.detect_double_check(move):
       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\hp\Downloads\gambit2.py", line 1046, in detect_double_check
    is_double_check = self.board.is_double_check()
                      ^^^^^^^^^^^^^^^^^^^^^^^^^^
AttributeError: 'Board' object has no attribute 'is_double_check'

```
import chess
import chess.engine
import chess.syzygy
import chess.polyglot
import random
import time
import threading
import concurrent.futures
from typing import Optional, Tuple, List, Dict
import math
import hashlib
import logging
from collections import defaultdict
import copy
import os

import torch
import torch.nn as nn
import torch.nn.functional as F

# --- Initialize Logging ---
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(levelname)s - %(message)s',
    handlers=[
        logging.FileHandler("devilgambit_engine.log"),
        logging.StreamHandler()
    ]
)

# --- Define Piece Values ---
PIECE_VALUES = {
    chess.PAWN: 100,
    chess.KNIGHT: 320,
    chess.BISHOP: 330,
    chess.ROOK: 500,
    chess.QUEEN: 900,
    chess.KING: 20000
}

# --- Define Piece-Square Tables ---
# Simplified example tables; in practice, use more refined tables
PIECE_SQUARE_TABLES = {
    chess.PAWN: [
        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
    ],
    # ... (Other piece-square tables as previously defined)
}

# --- Neural Network Definition ---
class ResidualBlock(nn.Module):
    def __init__(self, features):
        super(ResidualBlock, self).__init__()
        self.conv1 = nn.Conv2d(features, features, kernel_size=3, padding=1)
        self.bn1 = nn.BatchNorm2d(features)
        self.conv2 = nn.Conv2d(features, features, kernel_size=3, padding=1)
        self.bn2 = nn.BatchNorm2d(features)

    def forward(self, x):
        residual = x
        out = F.relu(self.bn1(self.conv1(x)))
        out = self.bn2(self.conv2(out))
        out += residual
        out = F.relu(out)
        return out

class ChessNet(nn.Module):
    def __init__(self, num_residual_blocks=20):
        super(ChessNet, self).__init__()
        self.conv1 = nn.Conv2d(14, 256, kernel_size=3, padding=1)  # 14 feature planes
        self.bn1 = nn.BatchNorm2d(256)
        self.residual_blocks = nn.Sequential(
            *[ResidualBlock(256) for _ in range(num_residual_blocks)]
        )
        self.policy_head = nn.Conv2d(256, 2, kernel_size=1)
        self.policy_bn = nn.BatchNorm2d(2)
        self.policy_fc = nn.Linear(2 * 8 * 8, 1968)  # 1968 possible moves in chess
        self.value_head = nn.Conv2d(256, 1, kernel_size=1)
        self.value_bn = nn.BatchNorm2d(1)
        self.value_fc1 = nn.Linear(1 * 8 * 8, 256)
        self.value_fc2 = nn.Linear(256, 1)

    def forward(self, x):
        x = F.relu(self.bn1(self.conv1(x)))
        x = self.residual_blocks(x)
        
        # Policy Head
        p = F.relu(self.policy_bn(self.policy_head(x)))
        p = p.view(-1, 2 * 8 * 8)
        p = self.policy_fc(p)
        p = F.log_softmax(p, dim=1)
        
        # Value Head
        v = F.relu(self.value_bn(self.value_head(x)))
        v = v.view(-1, 1 * 8 * 8)
        v = F.relu(self.value_fc1(v))
        v = torch.tanh(self.value_fc2(v))
        
        return p, v

# --- Zobrist Hashing Implementation ---
random.seed(0)  # For reproducibility

# Total of 12 piece types (6 for white, 6 for black)
ZOBRIST_TABLE = [[[random.getrandbits(64) for _ in range(12)] for _ in range(64)] for _ in range(2)]  # [color][square][piece]

# Castling rights: K, Q, k, q -> 16 possibilities (each can be present or not)
ZOBRIST_CASTLING = [random.getrandbits(64) for _ in range(16)]

# En passant: 8 possible files
ZOBRIST_ENPASSANT = [random.getrandbits(64) for _ in range(8)]

ZOBRIST_BLACK_TO_MOVE = random.getrandbits(64)

def compute_zobrist_hash(board: chess.Board) -> int:
    h = 0
    for square in chess.SQUARES:
        piece = board.piece_at(square)
        if piece:
            piece_index = piece.piece_type - 1 + (6 if piece.color == chess.BLACK else 0)
            if piece_index < 12:
                h ^= ZOBRIST_TABLE[piece.color][square][piece_index % 12]
            else:
                logging.warning(f"Unexpected piece index: {piece_index}")
    # Castling rights: index based on presence of KQkq
    castling = (
        (1 if board.has_kingside_castling_rights(chess.WHITE) else 0) |
        (2 if board.has_queenside_castling_rights(chess.WHITE) else 0) |
        (4 if board.has_kingside_castling_rights(chess.BLACK) else 0) |
        (8 if board.has_queenside_castling_rights(chess.BLACK) else 0)
    )
    h ^= ZOBRIST_CASTLING[castling]
    # En passant
    if board.ep_square is not None:
        h ^= ZOBRIST_ENPASSANT[chess.square_file(board.ep_square)]
    # Black to move
    if board.turn == chess.BLACK:
        h ^= ZOBRIST_BLACK_TO_MOVE
    return h

# --- Transposition Table Entry ---
class TranspositionTableEntry:
    def __init__(self, depth: int, value: float, flag: str, move: Optional[chess.Move]):
        self.depth = depth
        self.value = value
        self.flag = flag  # "EXACT", "LOWERBOUND", "UPPERBOUND"
        self.move = move

# --- DevilGambit Chess Engine Implementation ---
class DevilGambitChessEngine:
    def __init__(self, 
                 max_depth: int = 5, 
                 iterative_depth_limit: int = 20, 
                 stockfish_path: Optional[str] = None,  # Ensure Stockfish is in PATH or provide full path
                 tablebase_path: Optional[str] = None,  # Update with actual path
                 opening_book_path: Optional[str] = None,  # Update with actual path
                 time_per_move: float = 5.0):
        self.board = chess.Board()
        self.max_depth = max_depth
        self.iterative_depth_limit = iterative_depth_limit
        self.stockfish_path = stockfish_path if stockfish_path else "stockfish"
        self.tablebase_path = tablebase_path
        self.opening_book_path = opening_book_path
        self.time_per_move = time_per_move
        self.gambit_moves = []
        self.passive_moves = []
        self.history = defaultdict(list)  # For Smooth Transposition
        self.transposition_table: Dict[int, TranspositionTableEntry] = {}
        self.move_order_history = defaultdict(int)  # For History Heuristic

        # Initialize Stockfish Engine if path is provided
        if self.stockfish_path and os.path.exists(self.stockfish_path):
            try:
                self.engine = chess.engine.SimpleEngine.popen_uci(self.stockfish_path)
                logging.info("Stockfish engine loaded successfully.")
            except Exception as e:
                logging.error(f"Failed to load Stockfish engine: {e}")
                self.engine = None
        else:
            logging.warning("Stockfish path not provided or invalid. Stockfish assistance will be unavailable.")
            self.engine = None

        # Initialize Syzygy Tablebase if path is provided
        if self.tablebase_path and os.path.exists(self.tablebase_path):
            try:
                self.tablebase = chess.syzygy.open_tablebase(self.tablebase_path)
                logging.info("Syzygy tablebases loaded successfully.")
            except Exception as e:
                logging.error(f"Failed to load Syzygy tablebases: {e}")
                self.tablebase = None
        else:
            logging.warning("Tablebase path not provided or invalid. Syzygy tablebase assistance will be unavailable.")
            self.tablebase = None

        # Initialize Opening Book if path is provided
        self.opening_book = None
        self.load_opening_book()

        # Load neural network
        self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
        self.model = ChessNet(num_residual_blocks=20).to(self.device)
        self.model.eval()
        model_path = 'path_to_pretrained_model.pth'
        if os.path.exists(model_path):
            try:
                self.model.load_state_dict(torch.load(model_path, map_location=self.device))
                logging.info("Neural network model loaded successfully.")
            except Exception as e:
                logging.error(f"Failed to load neural network model: {e}")
                self.model = None
        else:
            logging.warning("Neural network model path not found. Neural evaluations will be unavailable.")
            self.model = None

        # --- Initialize Iterative SUPER Deepening Components ---
        # Layered Selective Depth Tuning (LSDT) parameters
        self.lsd_temperature = 0.5  # Controls the sensitivity of depth adjustments
        self.lsd_threshold = 0.3    # Threshold for depth tuning based on evaluation

        # Tactical Heuristics parameters
        self.tactical_patterns = ['fork', 'pin', 'skewer', 'discovered_attack', 'double_check']

        # Tactical Pruning Filters (TPF) parameters
        self.tpf_threshold = 50  # Moves that do not meet this tactical score are pruned early

        # --- Initialize Iterative Depth Scaling by Phases (IDSP) Components ---
        # IDSP parameters
        self.idsp_opening_depth = 3  # Shallower depth in opening
        self.idsp_middlegame_depth = 5  # Aggressive depth in middlegame
        self.idsp_endgame_depth = 5  # Balanced depth in endgame
        self.idsp_depth_multiplier = 1  # Multiplier based on tactical complexity

    def load_opening_book(self):
        """Load an opening book from a file."""
        if not self.opening_book_path or not os.path.exists(self.opening_book_path):
            logging.warning("Opening book path not provided or invalid. Opening book assistance will be unavailable.")
            self.opening_book = None
            return
        try:
            self.opening_book = chess.polyglot.MemoryMappedReader(self.opening_book_path)
            logging.info("Opening book loaded successfully.")
        except Exception as e:
            logging.error(f"Failed to load opening book: {e}")
            self.opening_book = None

    def board_to_tensor(self, board: chess.Board) -> torch.Tensor:
        """
        Convert a chess.Board instance to a tensor suitable for the neural network.
        """
        board_tensor = torch.zeros(14, 8, 8)  # 14 feature planes

        # Piece planes
        piece_map = board.piece_map()
        for square, piece in piece_map.items():
            row = 7 - chess.square_rank(square)
            col = chess.square_file(square)
            piece_type = piece.piece_type
            color = 0 if piece.color == chess.WHITE else 1
            plane = (piece_type - 1) + (6 * color)  # 0-5 for white, 6-11 for black
            if plane < 12:
                board_tensor[plane, row, col] = 1
            else:
                logging.warning(f"Invalid plane index: {plane} for piece {piece} at {square}")

        # Additional planes can include castling rights, en passant, etc.
        # Plane 12: White kingside castling
        board_tensor[12, :, :] = int(board.has_kingside_castling_rights(chess.WHITE))
        # Plane 13: White queenside castling
        board_tensor[13, :, :] = int(board.has_queenside_castling_rights(chess.WHITE))
        # Similarly, you can encode other game state information

        return board_tensor.unsqueeze(0).to(self.device)  # Add batch dimension and move to device

    def evaluate_board_nn(self) -> float:
        """Evaluate the board using the neural network."""
        if self.model is None:
            logging.warning("Neural network model is unavailable. Falling back to material evaluation.")
            return self.calculate_material()
        with torch.no_grad():
            board_tensor = self.board_to_tensor(self.board)
            policy, value = self.model(board_tensor)
            eval_score = value.item() * 1000  # Scale appropriately
        return eval_score

    def evaluate_board(self) -> float:
        """Override the traditional evaluation with neural network evaluation."""
        if self.model:
            return self.evaluate_board_nn()
        else:
            return self.calculate_material()

    def calculate_material(self) -> float:
        """Calculate the material balance of the board."""
        material = 0
        for piece_type in PIECE_VALUES:
            material += len(self.board.pieces(piece_type, chess.WHITE)) * PIECE_VALUES[piece_type]
            material -= len(self.board.pieces(piece_type, chess.BLACK)) * PIECE_VALUES[piece_type]
        # Optionally add positional evaluation using PIECE_SQUARE_TABLES
        # This example includes basic material only
        return material

    def is_aggressive_move(self, move: chess.Move) -> bool:
        """Determine if a move is aggressive based on captures and checks."""
        return self.board.is_capture(move) or self.board.gives_check(move)

    def order_moves(self, moves: List[chess.Move]) -> List[chess.Move]:
        """Order moves to improve alpha-beta pruning efficiency using MVV-LVA and History Heuristic."""
        def move_score(move):
            score = 0
            if self.is_aggressive_move(move):
                score += 100
            # MVV-LVA heuristic
            if self.board.is_capture(move):
                captured_piece = self.board.piece_at(move.to_square)
                if captured_piece:
                    score += PIECE_VALUES[captured_piece.piece_type] * 10
                    score += 1 / PIECE_VALUES[self.board.piece_at(move.from_square).piece_type]
            # History heuristic
            score += self.move_order_history[move]
            return score

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

    # --- Iterative SUPER Deepening (ISD) Implementation ---
    def iterated_super_deepening(self) -> Optional[chess.Move]:
        """Iterative SUPER Deepening with Layered Selective Depth Tuning, Dynamic Move Selection, Tactical Heuristics, and Tactical Pruning Filters."""
        best_move = None
        self.start_time = time.time()
        self.time_limit = self.time_per_move

        with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
            future = executor.submit(self.iterative_super_deepening_search)
            try:
                best_move = future.result(timeout=self.time_limit)
            except concurrent.futures.TimeoutError:
                logging.info("Iterative SUPER Deepening search timed out.")
                # best_move remains None or last best_move
        return best_move

    def iterative_super_deepening_search(self) -> Optional[chess.Move]:
        """Perform iterated super deepening with advanced features."""
        best_move = None
        for depth in range(1, self.iterative_depth_limit + 1):
            logging.debug(f"ISD: Searching at depth {depth}")
            # Compute depth scaling based on game phase and tactical complexity
            depth_multiplier = self.compute_depth_scaling()
            tuned_depth = int(depth * depth_multiplier)
            logging.debug(f"ISD: Depth multiplier: {depth_multiplier}, Tuned depth: {tuned_depth}")
            eval, move = self.super_deepening_pvs(tuned_depth, float('-inf'), float('inf'))
            if move:
                best_move = move
                logging.debug(f"ISD: Best move at depth {depth} (tuned depth {tuned_depth}): {move} with eval {eval}")
            current_time = time.time()
            if current_time - self.start_time > self.time_limit:
                logging.debug("ISD: Time limit exceeded during search.")
                break
        return best_move

    def super_deepening_pvs(self, depth: int, alpha: float, beta: float) -> Tuple[float, Optional[chess.Move]]:
        """Principal Variation Search (PVS) with ISD and IDSP enhancements."""
        legal_moves = list(self.board.legal_moves)
        
        # Apply Tactical Pruning Filters (TPF)
        legal_moves = self.apply_tactical_pruning_filters(legal_moves)

        # Dynamic Move Selection (DMS): Reorder moves dynamically during search
        legal_moves = self.dynamic_move_selection(legal_moves)

        best_move = None

        for i, move in enumerate(legal_moves):
            self.board.push(move)

            # Layered Selective Depth Tuning (LSDT): Adjust depth based on position
            tuned_depth = self.layered_selective_depth_tuning(depth)

            if i == 0:
                score, _ = self.gambit_pruning(tuned_depth - 1, alpha, beta, False)
            else:
                score, _ = self.gambit_pruning(tuned_depth - 1, alpha, alpha + 1, False)
                if alpha < score < beta:
                    score, _ = self.gambit_pruning(tuned_depth - 1, score, beta, False)

            self.board.pop()

            # Tactical Heuristics: Adjust evaluation based on tactical patterns
            score += self.apply_tactical_heuristics(move)

            if score > alpha:
                alpha = score
                best_move = move
                if alpha >= beta:
                    break  # Beta cutoff

        return alpha, best_move

    def apply_tactical_pruning_filters(self, moves: List[chess.Move]) -> List[chess.Move]:
        """Apply Tactical Pruning Filters (TPF) to eliminate unlikely moves early."""
        filtered_moves = []
        for move in moves:
            tactical_score = self.evaluate_move_tactics(move)
            if tactical_score >= self.tpf_threshold:
                filtered_moves.append(move)
        logging.debug(f"TPF: Pruned {len(moves) - len(filtered_moves)} moves.")
        return filtered_moves

    def evaluate_move_tactics(self, move: chess.Move) -> float:
        """Evaluate the tactical potential of a move."""
        score = 0
        # Example: Assign points based on tactical motifs
        if self.detect_fork(move):
            score += 100
        if self.detect_pin(move):
            score += 80
        if self.detect_skewer(move):
            score += 80
        if self.detect_discovered_attack(move):
            score += 90
        if self.detect_double_check(move):
            score += 120
        return score

    def detect_fork(self, move: chess.Move) -> bool:
        """Detect if a move creates a fork."""
        # Implement actual fork detection logic
        self.board.push(move)
        piece = self.board.piece_at(move.to_square)
        fork = False
        if piece and piece.piece_type in [chess.KNIGHT, chess.BISHOP, chess.ROOK, chess.QUEEN]:
            targets = self.get_attacked_squares(piece, move.to_square)
            # Filter only opponent's pieces
            opponent_targets = [s for s in targets if self.board.piece_at(s) and self.board.piece_at(s).color != piece.color]
            # Consider only high-value targets (above pawn value)
            high_value_targets = [s for s in opponent_targets if PIECE_VALUES[self.board.piece_at(s).piece_type] > PIECE_VALUES[chess.PAWN]]
            if len(high_value_targets) >= 2:
                fork = True
        self.board.pop()
        return fork

    def get_attacked_squares(self, piece: chess.Piece, square: chess.Square) -> List[chess.Square]:
        """Get squares attacked by a specific piece on a given square."""
        temp_board = copy.deepcopy(self.board)
        temp_board.set_piece_at(square, piece)
        return list(temp_board.attacks(square))

    def detect_pin(self, move: chess.Move) -> bool:
        """Detect if a move creates a pin."""
        # Implement actual pin detection logic
        self.board.push(move)
        pin_found = False
        own_color = not self.board.turn

        # Iterate over own bishops, rooks, and queens to check for pins
        for square in self.board.pieces(chess.BISHOP, own_color) | \
                      self.board.pieces(chess.ROOK, own_color) | \
                      self.board.pieces(chess.QUEEN, own_color):
            if self.board.is_pinned(own_color, square):
                pin_found = True
                break

        self.board.pop()
        return pin_found

    def detect_skewer(self, move: chess.Move) -> bool:
        """Detect if a move creates a skewer."""
        # Placeholder for skewer detection logic
        # Implement actual skewer detection
        return False

    def detect_discovered_attack(self, move: chess.Move) -> bool:
        """Detect if a move creates a discovered attack."""
        # Placeholder for discovered attack detection logic
        # Implement actual discovered attack detection
        return False

    def detect_double_check(self, move: chess.Move) -> bool:
        """Detect if a move creates a double check."""
        # Detect double check by applying the move and checking after
        self.board.push(move)
        is_double_check = self.board.is_double_check()
        self.board.pop()
        return is_double_check

    def dynamic_move_selection(self, moves: List[chess.Move]) -> List[chess.Move]:
        """Reorder moves dynamically based on tactical and positional evaluations."""
        def move_score(move):
            score = 0
            # Apply tactical heuristics
            score += self.evaluate_move_tactics(move)
            # Apply positional heuristics
            if self.board.is_capture(move):
                captured_piece = self.board.piece_at(move.to_square)
                if captured_piece:
                    score += PIECE_VALUES[captured_piece.piece_type] * 10
                    score += 1 / PIECE_VALUES[self.board.piece_at(move.from_square).piece_type]
            # History heuristic
            score += self.move_order_history[move]
            return score

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

    def layered_selective_depth_tuning(self, depth: int) -> int:
        """Adjust depth based on position evaluation."""
        eval_score = self.evaluate_board()
        if abs(eval_score) > PIECE_VALUES[chess.QUEEN] and depth > 3:
            tuned_depth = depth + 1  # Increase depth for critical positions
            logging.debug(f"LSDT: Increased depth to {tuned_depth} based on evaluation {eval_score}")
            return tuned_depth
        elif abs(eval_score) < PIECE_VALUES[chess.PAWN] and depth > 3:
            tuned_depth = max(3, depth - 1)  # Decrease depth for less critical positions
            logging.debug(f"LSDT: Decreased depth to {tuned_depth} based on evaluation {eval_score}")
            return tuned_depth
        return depth  # No change

    def apply_tactical_heuristics(self, move: chess.Move) -> float:
        """Adjust evaluation score based on tactical heuristics."""
        score = 0
        # Example: Reward moves that result in tactical advantages
        if self.detect_fork(move):
            score += 50
        if self.detect_pin(move):
            score += 40
        if self.detect_skewer(move):
            score += 40
        if self.detect_discovered_attack(move):
            score += 45
        if self.detect_double_check(move):
            score += 60
        return score

    # --- Iterative Depth Scaling by Phases (IDSP) Implementation ---
    def compute_depth_scaling(self) -> float:
        """Compute depth scaling multiplier based on game phase and tactical complexity."""
        phase = self.detect_game_phase()
        tactical_complexity = self.assess_tactical_complexity()

        if phase == "opening":
            depth_multiplier = 0.8  # Shallower depth in opening
            logging.debug(f"IDSP: Game phase detected as Opening. Depth multiplier set to {depth_multiplier}.")
        elif phase == "middlegame":
            # Scale based on tactical complexity
            depth_multiplier = 1.0 + (self.idsp_depth_multiplier * tactical_complexity)
            logging.debug(f"IDSP: Game phase detected as Middlegame. Tactical complexity: {tactical_complexity}. Depth multiplier set to {depth_multiplier}.")
        elif phase == "endgame":
            # Balanced approach with possible slight aggression
            depth_multiplier = 1.0 + (0.5 * tactical_complexity)
            logging.debug(f"IDSP: Game phase detected as Endgame. Tactical complexity: {tactical_complexity}. Depth multiplier set to {depth_multiplier}.")
        else:
            depth_multiplier = 1.0  # Default multiplier
            logging.debug(f"IDSP: Unknown game phase detected. Depth multiplier set to {depth_multiplier}.")

        # Ensure the multiplier is within reasonable bounds
        depth_multiplier = max(0.5, min(depth_multiplier, 2.0))
        return depth_multiplier

    def detect_game_phase(self) -> str:
        """Detect the current game phase: opening, middlegame, or endgame."""
        total_material = self.calculate_material()
        if total_material > 20000:
            phase = "opening"
        elif 8000 < total_material <= 20000:
            phase = "middlegame"
        else:
            phase = "endgame"
        logging.debug(f"IDSP: Detected game phase: {phase} based on total material: {total_material}")
        return phase

    def assess_tactical_complexity(self) -> float:
        """Assess tactical complexity of the current position.

        Returns:
            float: A value between 0 and 1 indicating tactical complexity.
        """
        tactical_score = 0
        # Example factors contributing to tactical complexity
        # 1. Number of pieces that can participate in tactics
        active_pieces = len([p for p in self.board.piece_map().values() if p.piece_type in [chess.QUEEN, chess.ROOK, chess.BISHOP, chess.KNIGHT]])
        tactical_score += active_pieces / 10  # Assuming max 10 active pieces

        # 2. Number of possible captures
        captures = len([move for move in self.board.legal_moves if self.board.is_capture(move)])
        tactical_score += captures / 20  # Assuming max 20 captures

        # 3. King safety
        if self.is_king_in_danger():
            tactical_score += 0.2

        # 4. Open lines (ranks, files with few pawns)
        open_lines = self.count_open_lines()
        tactical_score += open_lines / 14  # 7 ranks + 7 files

        # Normalize the tactical score to be between 0 and 1
        tactical_score = min(tactical_score, 1.0)
        logging.debug(f"IDSP: Assessed tactical complexity: {tactical_score}")
        return tactical_score

    def is_king_in_danger(self) -> bool:
        """Determine if the king is under threat."""
        # Simple check: if the king is in check
        in_check = self.board.is_check()
        logging.debug(f"IDSP: King in danger: {in_check}")
        return in_check

    def count_open_lines(self) -> int:
        """Count the number of open files and ranks."""
        open_files = 0
        open_ranks = 0
        for file in range(8):
            squares_in_file = chess.BB_FILE_A << file
            pawns_in_file = len(self.board.pieces(chess.PAWN, chess.WHITE) & squares_in_file) + \
                            len(self.board.pieces(chess.PAWN, chess.BLACK) & squares_in_file)
            if pawns_in_file == 0:
                open_files += 1
        for rank in range(8):
            squares_in_rank = chess.BB_RANK_1 << (rank * 8)
            pawns_in_rank = len(self.board.pieces(chess.PAWN, chess.WHITE) & squares_in_rank) + \
                            len(self.board.pieces(chess.PAWN, chess.BLACK) & squares_in_rank)
            if pawns_in_rank == 0:
                open_ranks += 1
        total_open_lines = open_files + open_ranks
        logging.debug(f"IDSP: Counted {total_open_lines} open lines.")
        return total_open_lines

    # --- Iterative Depth Scaling by Phases Enhancements Continued ---

    def gambit_pruning(self, depth: int, alpha: float, beta: float, is_maximizing: bool) -> Tuple[float, Optional[chess.Move]]:
        """Minimax with alpha-beta pruning, quiescence search, transposition tables, and advanced pruning."""
        current_time = time.time()
        if current_time - self.start_time > self.time_limit:
            logging.debug("Time limit exceeded during search.")
            return self.evaluate_board(), None

        zobrist_hash = compute_zobrist_hash(self.board)
        if zobrist_hash in self.transposition_table:
            entry = self.transposition_table[zobrist_hash]
            if entry.depth >= depth:
                if entry.flag == "EXACT":
                    return entry.value, entry.move
                elif entry.flag == "LOWERBOUND":
                    alpha = max(alpha, entry.value)
                elif entry.flag == "UPPERBOUND":
                    beta = min(beta, entry.value)
                if alpha >= beta:
                    return entry.value, entry.move

        # Endgame tablebase check
        if self.is_endgame() and self.tablebase:
            tablebase_result = self.query_tablebase()
            if tablebase_result is not None:
                return tablebase_result, None

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

        if self.board.is_game_over():
            result = self.get_game_result()
            return result, None

        legal_moves = list(self.board.legal_moves)

        # Move ordering: prioritize aggressive moves using MVV-LVA and History Heuristic
        legal_moves = self.order_moves(legal_moves)

        best_move = None

        if is_maximizing:
            max_eval = float('-inf')
            for move in legal_moves:
                self.board.push(move)
                eval, _ = self.gambit_pruning(depth - 1, alpha, beta, False)
                self.board.pop()
                if eval > max_eval:
                    max_eval = eval
                    best_move = move
                alpha = max(alpha, eval)
                if beta <= alpha:
                    break  # Beta cutoff
            # Store in transposition table
            flag = "EXACT"
            if max_eval <= alpha:
                flag = "UPPERBOUND"
            elif max_eval >= beta:
                flag = "LOWERBOUND"
            self.transposition_table[zobrist_hash] = TranspositionTableEntry(depth, max_eval, flag, best_move)
            return max_eval, best_move
        else:
            min_eval = float('inf')
            for move in legal_moves:
                self.board.push(move)
                eval, _ = self.gambit_pruning(depth - 1, alpha, beta, True)
                self.board.pop()
                if eval < min_eval:
                    min_eval = eval
                    best_move = move
                beta = min(beta, eval)
                if beta <= alpha:
                    break  # Alpha cutoff
            # Store in transposition table
            flag = "EXACT"
            if min_eval <= alpha:
                flag = "UPPERBOUND"
            elif min_eval >= beta:
                flag = "LOWERBOUND"
            self.transposition_table[zobrist_hash] = TranspositionTableEntry(depth, min_eval, flag, best_move)
            return min_eval, best_move

    def quiescence_search(self, alpha: float, beta: float) -> float:
        """Extend search in volatile positions to avoid horizon effect."""
        stand_pat = self.evaluate_board()
        if stand_pat >= beta:
            return beta
        if stand_pat > alpha:
            alpha = stand_pat

        # Explore only aggressive moves (captures and checks)
        aggression_moves = [move for move in self.board.legal_moves if self.is_aggressive_move(move)]
        aggression_moves = self.order_moves(aggression_moves)

        for move in aggression_moves:
            self.board.push(move)
            score = -self.quiescence_search(-beta, -alpha)
            self.board.pop()

            if score >= beta:
                return beta
            if score > alpha:
                alpha = score
        return alpha

    def get_game_result(self) -> float:
        """Assign evaluation based on the game result."""
        if self.board.is_checkmate():
            if self.board.turn == chess.WHITE:
                return -math.inf  # Black wins
            else:
                return math.inf   # White wins
        elif self.board.is_stalemate() or self.board.is_insufficient_material() or \
             self.board.can_claim_fifty_moves() or self.board.can_claim_threefold_repetition():
            return 0  # Draw
        return 0  # Default

    def is_endgame(self) -> bool:
        """Determine if the current position is in the endgame."""
        # Simple criterion: total material below a threshold
        total_material = self.calculate_material()
        return abs(total_material) < 1500  # Adjust threshold as needed

    def query_tablebase(self) -> Optional[float]:
        """Query the endgame tablebase for a perfect evaluation."""
        if not self.tablebase:
            return None
        try:
            result = self.tablebase.probe_wdl(self.board)
            if result is not None:
                if result == chess.syzygy.WDL_WIN:
                    return math.inf if self.board.turn == chess.WHITE else -math.inf
                elif result == chess.syzygy.WDL_DRAW:
                    return 0
                elif result == chess.syzygy.WDL_LOSS:
                    return -math.inf if self.board.turn == chess.WHITE else math.inf
        except Exception as e:
            logging.error(f"Tablebase query failed: {e}")
        return None

    def make_gambit_move(self) -> Optional[chess.Move]:
        """Attempt to make a gambit move early in the game."""
        # Attempt to play from the opening book
        if self.opening_book:
            opening_move = self.play_from_opening_book()
            if opening_move:
                self.gambit_moves.append(opening_move)
                self.move_order_history[opening_move] += 1  # Update history heuristic
                logging.info(f"Played opening book move: {opening_move}")
                return opening_move

        # If not in opening book, proceed with gambit strategy
        legal_moves = list(self.board.legal_moves)
        gambit_moves = [move for move in legal_moves if self.is_aggressive_move(move)]

        if gambit_moves and self.board.fullmove_number < 10:
            move = random.choice(gambit_moves)
            self.gambit_moves.append(move)
            self.move_order_history[move] += 1  # Update history heuristic
            logging.info(f"Made gambit move: {move}")
            return move

        return None

    def play_from_opening_book(self) -> Optional[chess.Move]:
        """Attempt to select a move from the opening book."""
        try:
            entry = self.opening_book.choice(self.board)
            if entry and entry.move:
                return entry.move
        except Exception as e:
            logging.error(f"Opening book error: {e}")
        return None

    def gambit_variation(self) -> Optional[chess.Move]:
        """Implement specific gambit variations."""
        # Example: Always try to sacrifice a pawn for initiative
        sacrificial_moves = [move for move in self.board.legal_moves 
                             if self.board.is_capture(move) and 
                             self.board.piece_at(move.to_square) and 
                             self.board.piece_at(move.to_square).piece_type == chess.PAWN]
        if sacrificial_moves:
            move = random.choice(sacrificial_moves)
            self.gambit_moves.append(move)
            self.move_order_history[move] += 1  # Update history heuristic
            logging.info(f"Made sacrificial gambit move: {move}")
            return move
        return None

    def passive_tactic_play(self) -> Optional[chess.Move]:
        """Fallback strategy that emphasizes defense and solid play."""
        legal_moves = list(self.board.legal_moves)
        if not legal_moves:
            return None

        # Evaluate moves and choose one that minimizes opponent's opportunities
        best_move = max(legal_moves, key=lambda move: self.evaluate_move(move))
        self.passive_moves.append(best_move)
        self.move_order_history[best_move] += 1  # Update history heuristic
        logging.info(f"Made passive tactic move: {best_move}")
        return best_move

    def evaluate_move(self, move: chess.Move) -> float:
        """Evaluate a single move."""
        self.board.push(move)
        evaluation = self.evaluate_board()
        self.board.pop()
        return evaluation

    def stockfish_helper(self) -> Optional[chess.Move]:
        """Use Stockfish engine for assistance in complex positions."""
        if not self.engine:
            return None
        try:
            result = self.engine.play(self.board, chess.engine.Limit(time=0.1))
            if result:
                logging.info(f"Stockfish suggests move: {result.move}")
                return result.move
        except Exception as e:
            logging.error(f"Stockfish error: {e}")
        return None

    def play_engine_move(self, use_gambit: bool = True) -> Optional[chess.Move]:
        """Main engine move logic integrating all strategies."""
        move = None

        # Use Gambit move if conditions met
        if use_gambit and len(self.gambit_moves) < 5:
            move = self.make_gambit_move()

        # If not used gambit move, try gambit variation
        if move is None and self.board.fullmove_number < 10:
            move = self.gambit_variation()

        # If gambit move not made, try opening book
        if move is None and self.opening_book:
            move = self.play_from_opening_book()

        # If none of the above, proceed with ISD and IDSP
        if move is None:
            move = self.iterated_super_deepening()

        # If deep search fails or time runs out, use passive tactic play
        if move is None:
            move = self.passive_tactic_play()

        # If still no move, fallback to Stockfish
        if move is None and len(self.board.piece_map()) < 10:
            move = self.stockfish_helper()

        if move:
            self.board.push(move)
            self.smooth_transposition(move)
            # Update history heuristic for killer moves or commonly good moves
            self.move_order_history[move] += 1
            logging.info(f"Engine played move: {move}")
        return move

    def smooth_transposition(self, move: chess.Move):
        """Adjust move selection to avoid transposition repetition."""
        move_key = compute_zobrist_hash(self.board)
        self.history[move_key].append(move)
        # Potentially deprioritize moves that lead to repeated positions

    # --- Iterative Depth Scaling by Phases (IDSP) Implementation ---
    def compute_depth_scaling(self) -> float:
        """Compute depth scaling multiplier based on game phase and tactical complexity."""
        phase = self.detect_game_phase()
        tactical_complexity = self.assess_tactical_complexity()

        if phase == "opening":
            depth_multiplier = 0.8  # Shallower depth in opening
            logging.debug(f"IDSP: Game phase detected as Opening. Depth multiplier set to {depth_multiplier}.")
        elif phase == "middlegame":
            # Scale based on tactical complexity
            depth_multiplier = 1.0 + (self.idsp_depth_multiplier * tactical_complexity)
            logging.debug(f"IDSP: Game phase detected as Middlegame. Tactical complexity: {tactical_complexity}. Depth multiplier set to {depth_multiplier}.")
        elif phase == "endgame":
            # Balanced approach with possible slight aggression
            depth_multiplier = 1.0 + (0.5 * tactical_complexity)
            logging.debug(f"IDSP: Game phase detected as Endgame. Tactical complexity: {tactical_complexity}. Depth multiplier set to {depth_multiplier}.")
        else:
            depth_multiplier = 1.0  # Default multiplier
            logging.debug(f"IDSP: Unknown game phase detected. Depth multiplier set to {depth_multiplier}.")

        # Ensure the multiplier is within reasonable bounds
        depth_multiplier = max(0.5, min(depth_multiplier, 2.0))
        return depth_multiplier

    def detect_game_phase(self) -> str:
        """Detect the current game phase: opening, middlegame, or endgame."""
        total_material = self.calculate_material()
        if total_material > 20000:
            phase = "opening"
        elif 8000 < total_material <= 20000:
            phase = "middlegame"
        else:
            phase = "endgame"
        logging.debug(f"IDSP: Detected game phase: {phase} based on total material: {total_material}")
        return phase

    def assess_tactical_complexity(self) -> float:
        """Assess tactical complexity of the current position.

        Returns:
            float: A value between 0 and 1 indicating tactical complexity.
        """
        tactical_score = 0
        # Example factors contributing to tactical complexity
        # 1. Number of pieces that can participate in tactics
        active_pieces = len([p for p in self.board.piece_map().values() if p.piece_type in [chess.QUEEN, chess.ROOK, chess.BISHOP, chess.KNIGHT]])
        tactical_score += active_pieces / 10  # Assuming max 10 active pieces

        # 2. Number of possible captures
        captures = len([move for move in self.board.legal_moves if self.board.is_capture(move)])
        tactical_score += captures / 20  # Assuming max 20 captures

        # 3. King safety
        if self.is_king_in_danger():
            tactical_score += 0.2

        # 4. Open lines (ranks, files with few pawns)
        open_lines = self.count_open_lines()
        tactical_score += open_lines / 14  # 7 ranks + 7 files

        # Normalize the tactical score to be between 0 and 1
        tactical_score = min(tactical_score, 1.0)
        logging.debug(f"IDSP: Assessed tactical complexity: {tactical_score}")
        return tactical_score

    def is_king_in_danger(self) -> bool:
        """Determine if the king is under threat."""
        # Simple check: if the king is in check
        in_check = self.board.is_check()
        logging.debug(f"IDSP: King in danger: {in_check}")
        return in_check

    def count_open_lines(self) -> int:
        """Count the number of open files and ranks."""
        open_files = 0
        open_ranks = 0
        for file in range(8):
            squares_in_file = chess.BB_FILE_A << file
            pawns_in_file = len(self.board.pieces(chess.PAWN, chess.WHITE) & squares_in_file) + \
                            len(self.board.pieces(chess.PAWN, chess.BLACK) & squares_in_file)
            if pawns_in_file == 0:
                open_files += 1
        for rank in range(8):
            squares_in_rank = chess.BB_RANK_1 << (rank * 8)
            pawns_in_rank = len(self.board.pieces(chess.PAWN, chess.WHITE) & squares_in_rank) + \
                            len(self.board.pieces(chess.PAWN, chess.BLACK) & squares_in_rank)
            if pawns_in_rank == 0:
                open_ranks += 1
        total_open_lines = open_files + open_ranks
        logging.debug(f"IDSP: Counted {total_open_lines} open lines.")
        return total_open_lines

    # --- Iterative Depth Scaling by Phases Enhancements Continued ---

    # (All IDSP related methods are integrated above)

    # --- Sample Implementations for Tactical Pattern Detection ---
    # These are enhanced with basic logic but can be expanded with more sophisticated algorithms.

    def detect_fork(self, move: chess.Move) -> bool:
        """Detect if a move creates a fork."""
        self.board.push(move)
        piece = self.board.piece_at(move.to_square)
        fork = False
        if piece and piece.piece_type in [chess.KNIGHT, chess.BISHOP, chess.ROOK, chess.QUEEN]:
            targets = self.get_attacked_squares(piece, move.to_square)
            # Filter only opponent's pieces
            opponent_targets = [s for s in targets if self.board.piece_at(s) and self.board.piece_at(s).color != piece.color]
            # Consider only high-value targets (above pawn value)
            high_value_targets = [s for s in opponent_targets if PIECE_VALUES[self.board.piece_at(s).piece_type] > PIECE_VALUES[chess.PAWN]]
            if len(high_value_targets) >= 2:
                fork = True
        self.board.pop()
        return fork

    def detect_pin(self, move: chess.Move) -> bool:
        """Detect if a move creates a pin."""
        # Implement actual pin detection logic
        self.board.push(move)
        pin_found = False
        own_color = not self.board.turn

        # Iterate over own bishops, rooks, and queens to check for pins
        for square in self.board.pieces(chess.BISHOP, own_color) | \
                      self.board.pieces(chess.ROOK, own_color) | \
                      self.board.pieces(chess.QUEEN, own_color):
            if self.board.is_pinned(own_color, square):
                pin_found = True
                break

        self.board.pop()
        return pin_found

    def detect_skewer(self, move: chess.Move) -> bool:
        """Detect if a move creates a skewer."""
        # Placeholder for skewer detection logic
        # Implement actual skewer detection
        return False

    def detect_discovered_attack(self, move: chess.Move) -> bool:
        """Detect if a move creates a discovered attack."""
        # Placeholder for discovered attack detection logic
        # Implement actual discovered attack detection
        return False

    def detect_double_check(self, move: chess.Move) -> bool:
        """Detect if a move creates a double check."""
        # Detect double check by applying the move and checking after
        self.board.push(move)
        is_double_check = self.board.is_double_check()
        self.board.pop()
        return is_double_check

    # --- End of Tactical Pattern Detection Implementations ---

    # --- Iterated Depth Scaling by Phases Enhancements Continued ---

    def display_board(self) -> str:
        """Returns the current board state as a Unicode string."""
        return str(self.board)

    def close(self):
        """Close the Stockfish engine and tablebase to release resources."""
        if self.engine:
            self.engine.quit()
        if self.tablebase:
            self.tablebase.close()
        if self.opening_book:
            self.opening_book.close()

    def __del__(self):
        """Destructor to ensure resources are freed."""
        self.close()

# --- Main Function to Run the Engine ---
def main():
    # Initialize the engine with appropriate paths
    engine = DevilGambitChessEngine(
        max_depth=5, 
        iterative_depth_limit=20, 
        stockfish_path=r"C:\Users\hp\Downloads\Stock\stockfish\Stocky.exe",  # Update with actual path if required
        tablebase_path=r"path_to_syzygy_tablebases",  # Update with actual path if available
        opening_book_path=r"M11.2.bin",  # Update with actual path if available
        time_per_move=5.0
    )

    print("Welcome to The DEVIL's Gambit Chess Engine!")
    print("This advanced engine plays aggressively with gambit variations and passive tactics when needed.")
    print("It leverages a deep neural network for board evaluation, Stockfish's assistance, Syzygy tablebases for endgames, and an opening book.")
    print("Enter your moves in standard algebraic notation (e.g., e4, Nf3). Type 'quit' to exit.")

    while not engine.board.is_game_over():
        print("\nCurrent Board:")
        print(engine.display_board())
        print(f"FEN: {engine.board.fen()}")
        user_move = input("Your move: ").strip()
        if user_move.lower() == 'quit':
            break
        try:
            move = engine.board.parse_san(user_move)
            engine.board.push(move)
        except ValueError:
            try:
                move = chess.Move.from_uci(user_move)
                if move in engine.board.legal_moves:
                    engine.board.push(move)
                else:
                    print("Move is not legal. Please try again.")
                    continue
            except Exception:
                print("Invalid move format. Please try again.")
                continue

        if engine.board.is_game_over():
            break

        print("Engine is thinking...")
        start_time = time.time()
        engine_move = engine.play_engine_move()
        end_time = time.time()
        if engine_move:
            print(f"Engine move: {engine_move} (computed in {end_time - start_time:.2f} seconds)")
        else:
            print("Engine did not make a move.")

    print("\nGame over!")
    result = engine.board.result()
    print(f"Result: {result}")
    print(engine.display_board())
    engine.close()

if __name__ == "__main__":
    main()
```