Add this OTESM method to this chess engine;



### **Optimized Tactical Evaluation and Search Method (OTESM)**

1. **Selective Depth Extensions (Focus on Tactical Patterns):**
   - Extend the search depth **selectively** in positions where tactical motifs (forks, pins, skewers, checks) are present. Instead of evenly increasing search depth for all moves, target specific moves that involve captures, checks, and major threats.
   - For example, if a queen can deliver a check or if a knight is involved in a potential fork, extend the search deeper for these lines. 

2. **Pruning Techniques (Reducing Non-Tactical Paths):**
   - **Null Move Pruning:** Aggressively prune lines where a side moves without making a move (i.e., giving the opponent an extra turn). If a move is not immediately threatening, the search depth can be reduced. This is great for avoiding paths where there is no significant tactical activity.
   - **Late Move Reduction (LMR):** For moves that appear late in the move ordering (less promising moves), reduce the search depth. Moves that don't involve immediate tactical threats can be evaluated less deeply.

3. **Quiescence Search (Refining Search on Tactical Positions):**
   - After the main search, use a **quiescence search** to avoid the "horizon effect" (where the engine misses tactical threats just beyond the search depth). This focuses on evaluating only the "noisy" moves such as captures, checks, and threats. By allowing the engine to search beyond the standard depth only in tactical positions, it can better evaluate whether an immediate threat leads to a strong advantage or not.
   - **Tactical Focus:** Weight quiescence search heavily towards recaptures, checks, and any forced sequences.

4. **Killer Move Heuristic (Tactical Tuning):**
   - Use a **killer move table** to prioritize moves that have been highly effective in previous searches. Killer moves often lead to checkmates, significant material gain, or forcing continuations, and are more relevant for tactical play. This keeps the engine sharp and focused on potential knockout blows.
   - Increase the importance of tactical "killer moves" by maintaining a cache of such moves from similar positions encountered during the search.

5. **Tactical Pattern Recognition (Efficient CPU Usage):**
   - Use pre-coded **tactical pattern recognition** to quickly identify common tactics like discovered attacks, pins, forks, and back-rank threats. Once identified, increase the evaluation focus in those lines.
   - Instead of recalculating tactical motifs from scratch in every search iteration, apply fast pattern recognition to catch tactical opportunities early in the search.

6. **History Heuristic (Search Optimization):**
   - Use the **history heuristic** to bias the engine towards moves that have historically performed well in similar positions. Moves that have caused major material swings or resulted in tactical gains are given priority, speeding up the decision-making process and reducing unnecessary search space.

7. **Futility Pruning (Eliminate Useless Moves):**
   - If a move is not likely to change the evaluation by a significant amount (i.e., improve or worsen the position drastically), prune it from further consideration. For tactical optimization, moves that don't result in check, capture, or threats can often be skipped at shallower depths.

8. **Pawn Structure and Imbalances (Simplified for Tactics):**
   - While a highly tactical engine doesn't need to go deep into pawn structures, it can still recognize **imbalances** (e.g., a weak pawn that could be captured in a tactical sequence, or a passed pawn ready for promotion). However, limit the scope of positional evaluations to avoid wasting CPU resources on slower positional factors.

9. **Checkmate Patterns (Immediate Threats):**
   - Pre-load the engine with **known checkmate patterns** like back-rank mates, smothered mates, and basic mating nets. When one of these patterns arises, the engine can quickly identify them and give an immediate tactical evaluation boost, optimizing CPU cycles for quicker identification of winning tactics.

10. **Move Ordering Heuristics (Efficiency for CPU):**
   - Use advanced **move ordering** techniques to evaluate tactical moves first:
     - Captures (using the **MVV-LVA heuristic**: Most Valuable Victim - Least Valuable Attacker).
     - Checks.
     - Threats of major material gain (forks, skewers, etc.).
   - By evaluating tactical moves first, the engine avoids wasting time on less impactful moves and focuses on potential game-changing tactical shots.

11. **Parallel Search on Tactical Motifs (Multithreading for Average CPU):**
   - Even on an average CPU, optimize for **multithreaded searching**, where different threads can focus on different tactical motifs (e.g., one thread focuses on all checks, another on all captures). This allows the engine to explore multiple tactical avenues in parallel, increasing the chance of finding the best tactical shot in a given position.

Script:

```
import chess
import chess.polyglot
import random
import time
from collections import defaultdict
import sys

# Constants for piece values
PIECE_VALUES = {
    chess.PAWN: 100,
    chess.KNIGHT: 320,
    chess.BISHOP: 330,
    chess.ROOK: 500,
    chess.QUEEN: 900,
    chess.KING: 0
}

# Weights for evaluation components
WEIGHTS = {
    'material': 1.0,
    'mobility': 0.8,
    'king_safety': 0.7,
    'center_control': 0.6,
    'piece_coordination': 0.5,
    'pawn_structure': 0.4,
    'strategic_imbalances': 0.3,
    'threats': 0.9
}

class TranspositionTableEntry:
    def __init__(self, depth, value, flag, best_move):
        self.depth = depth
        self.value = value
        self.flag = flag  # 'exact', 'lowerbound', 'upperbound'
        self.best_move = best_move

class UPPlusChessEngine:
    def __init__(self, time_limit=5.0):
        self.board = chess.Board()
        self.time_limit = time_limit  # Time limit per move in seconds
        self.start_time = None
        self.transposition_table = {}
        self.killer_moves = defaultdict(list)
        self.node_count = 0
        self.parameters = self.node_count_based_parameter_tuning()

    def node_count_based_parameter_tuning(self):
        # Placeholder for node count-based parameter tuning
        # In a real engine, this would dynamically adjust parameters based on node counts
        return {
            'max_depth': 5,
            'time_per_move': self.time_limit
        }

    def make_move(self, move_san):
        """Attempts to make a move on the board."""
        try:
            move = self.board.parse_san(move_san)
            if move in self.board.legal_moves:
                self.board.push(move)
                return True, move
            else:
                return False, "Illegal move."
        except ValueError:
            return False, "Invalid move format. Please use standard algebraic notation."

    def get_random_move(self):
        """Returns a random legal move from the current board state."""
        moves = list(self.board.legal_moves)
        return random.choice(moves) if moves else None

    def play_engine_move(self):
        """Determines the best move for the engine without making it."""
        self.start_time = time.time()
        self.node_count = 0
        best_move, best_value = self.iterative_deepening_search()
        return best_move if best_move else self.get_random_move()

    def iterative_deepening_search(self):
        """Performs iterative deepening search up to max_depth."""
        best_move = None
        best_value = -float('inf')
        max_depth = self.parameters['max_depth']
        for depth in range(1, max_depth + 1):
            value, move = self.alpha_beta_search(depth, -float('inf'), float('inf'), True)
            if time.time() - self.start_time > self.time_limit:
                break
            if move is not None:
                best_move = move
                best_value = value
        return best_move, best_value

    def alpha_beta_search(self, depth, alpha, beta, maximizing_player):
        """Alpha-beta pruning search."""
        if time.time() - self.start_time > self.time_limit:
            return 0, None
        self.node_count += 1

        key = (self.board.board_fen(), depth, maximizing_player)
        if key in self.transposition_table:
            entry = self.transposition_table[key]
            if entry.depth >= depth:
                if entry.flag == 'exact':
                    return entry.value, entry.best_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.best_move

        if depth == 0 or self.board.is_game_over():
            eval = self.evaluate_board()
            return eval, None

        legal_moves = list(self.board.legal_moves)
        # Move ordering: prioritize killer moves and captures
        legal_moves = self.order_moves(legal_moves, maximizing_player)

        best_move = None
        if maximizing_player:
            max_eval = -float('inf')
            for move in legal_moves:
                self.board.push(move)
                eval, _ = self.alpha_beta_search(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:
                    # Killer move heuristic
                    self.killer_moves[depth].append(move)
                    break
            # Transposition table storing exact value
            self.transposition_table[key] = TranspositionTableEntry(depth, max_eval, 'exact', best_move)
            return max_eval, best_move
        else:
            min_eval = float('inf')
            for move in legal_moves:
                self.board.push(move)
                eval, _ = self.alpha_beta_search(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:
                    # Killer move heuristic
                    self.killer_moves[depth].append(move)
                    break
            # Transposition table storing exact value
            self.transposition_table[key] = TranspositionTableEntry(depth, min_eval, 'exact', best_move)
            return min_eval, best_move

    def order_moves(self, moves, maximizing_player):
        """Orders moves based on heuristics like captures, killer moves, and history."""
        # Prioritize captures
        captures = [move for move in moves if self.board.is_capture(move)]
        non_captures = [move for move in moves if not self.board.is_capture(move)]

        # Sort captures based on Most Valuable Victim - Least Valuable Aggressor (MVV-LVA)
        captures.sort(key=lambda move: self.mvv_lva(move), reverse=True)

        # Prioritize killer moves
        killers = self.killer_moves.get(self.parameters['max_depth'], [])
        killer_moves = [move for move in non_captures if move in killers]
        others = [move for move in non_captures if move not in killers]

        return captures + killer_moves + others

    def mvv_lva(self, move):
        """Most Valuable Victim - Least Valuable Aggressor."""
        if self.board.is_capture(move):
            victim = self.board.piece_at(move.to_square)
            aggressor = self.board.piece_at(move.from_square)
            if victim and aggressor:
                return PIECE_VALUES[victim.piece_type] - PIECE_VALUES[aggressor.piece_type]
        return 0

    def evaluate_board(self):
        """Evaluates the board position based on various factors."""
        material = self.evaluate_material()
        mobility = self.evaluate_mobility()
        king_safety = self.evaluate_king_safety()
        center_control = self.evaluate_center_control()
        piece_coordination = self.evaluate_piece_coordination()
        pawn_structure = self.evaluate_pawn_structure()
        strategic_imbalances = self.evaluate_strategic_imbalances()
        threats = self.evaluate_threats()

        total_evaluation = (
            WEIGHTS['material'] * material +
            WEIGHTS['mobility'] * mobility +
            WEIGHTS['king_safety'] * king_safety +
            WEIGHTS['center_control'] * center_control +
            WEIGHTS['piece_coordination'] * piece_coordination +
            WEIGHTS['pawn_structure'] * pawn_structure +
            WEIGHTS['strategic_imbalances'] * strategic_imbalances +
            WEIGHTS['threats'] * threats
        )
        return total_evaluation if self.board.turn == chess.WHITE else -total_evaluation

    def evaluate_material(self):
        """Standard material balance."""
        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]
        return material

    def evaluate_mobility(self):
        """Piece mobility with traditional and custom weights."""
        mobility = len(list(self.board.legal_moves)) * 10  # Traditional mobility
        # Custom weight: favor having more mobility
        mobility += len(list(self.board.legal_moves)) * 2
        return mobility

    def evaluate_king_safety(self):
        """King safety with traditional and aggressive customization."""
        safety = 0
        # Traditional: How many squares around the king are controlled
        white_king = self.board.king(chess.WHITE)
        black_king = self.board.king(chess.BLACK)
        safety += self.count_controlled_squares(white_king, chess.WHITE) * 5
        safety -= self.count_controlled_squares(black_king, chess.BLACK) * 5
        # Aggressive customization: Penalize if king is exposed
        safety -= self.exposed_king(chess.WHITE) * 10
        safety += self.exposed_king(chess.BLACK) * 10
        return safety

    def count_controlled_squares(self, king_square, color):
        """Counts the number of squares around the king that are controlled."""
        controlled = 0
        for square in chess.SquareSet(chess.BB_KING_ATTACKS[king_square]):
            if self.board.is_attacked_by(color, square):
                controlled += 1
        return controlled

    def exposed_king(self, color):
        """Determines if the king is exposed."""
        king_square = self.board.king(color)
        attackers = self.board.attackers(not color, king_square)
        if attackers:
            return len(attackers)
        return 0

    def evaluate_center_control(self):
        """Weighted center control."""
        center_squares = [chess.D4, chess.D5, chess.E4, chess.E5]
        control = 0
        for square in center_squares:
            if self.board.is_attacked_by(chess.WHITE, square):
                control += 1.5
            if self.board.is_attacked_by(chess.BLACK, square):
                control -= 1.5
        return control

    def evaluate_piece_coordination(self):
        """Custom pairing bonus for piece coordination."""
        coordination = 0
        # Example: Bonus for having knights supporting each other
        white_knights = self.board.pieces(chess.KNIGHT, chess.WHITE)
        black_knights = self.board.pieces(chess.KNIGHT, chess.BLACK)
        coordination += self.pairs_bonus(white_knights)
        coordination -= self.pairs_bonus(black_knights)
        return coordination

    def pairs_bonus(self, pieces):
        """Calculates bonus for piece pairs."""
        bonus = 0
        pieces = list(pieces)
        for i in range(len(pieces)):
            for j in range(i + 1, len(pieces)):
                if chess.square_distance(pieces[i], pieces[j]) <= 2:
                    bonus += 10
        return bonus

    def evaluate_pawn_structure(self):
        """Advanced pawn structure customization."""
        pawn_structure = 0
        white_pawns = self.board.pieces(chess.PAWN, chess.WHITE)
        black_pawns = self.board.pieces(chess.PAWN, chess.BLACK)
        pawn_structure += self.pawn_isolated_bonus(white_pawns)
        pawn_structure -= self.pawn_isolated_bonus(black_pawns)
        pawn_structure += self.pawn_doubled_penalty(white_pawns)
        pawn_structure -= self.pawn_doubled_penalty(black_pawns)
        return pawn_structure

    def pawn_isolated_bonus(self, pawns):
        """Bonus for isolated pawns."""
        bonus = 0
        for pawn in pawns:
            file = chess.square_file(pawn)
            # A pawn is isolated if there are no pawns on adjacent files
            left_file = file - 1 if file > 0 else None
            right_file = file + 1 if file < 7 else None
            has_pawn = False
            if left_file is not None:
                has_pawn = has_pawn or bool(self.board.pieces(chess.PAWN, not self.board.color_at(pawn)) & chess.BB_FILES[left_file])
            if right_file is not None:
                has_pawn = has_pawn or bool(self.board.pieces(chess.PAWN, not self.board.color_at(pawn)) & chess.BB_FILES[right_file])
            if not has_pawn:
                bonus -= 5  # Penalize isolated pawns
        return bonus

    def pawn_doubled_penalty(self, pawns):
        """Penalty for doubled pawns."""
        files = defaultdict(int)
        for pawn in pawns:
            file = chess.square_file(pawn)
            files[file] += 1
        penalty = 0
        for count in files.values():
            if count > 1:
                penalty -= (count - 1) * 10
        return penalty

    def evaluate_strategic_imbalances(self):
        """Dynamic positional factors as strategic imbalances."""
        imbalances = 0
        # Example: More bishops vs more knights
        white_bishops = len(self.board.pieces(chess.BISHOP, chess.WHITE))
        white_knights = len(self.board.pieces(chess.KNIGHT, chess.WHITE))
        black_bishops = len(self.board.pieces(chess.BISHOP, chess.BLACK))
        black_knights = len(self.board.pieces(chess.KNIGHT, chess.BLACK))
        imbalances += (white_bishops - white_knights) * 15
        imbalances -= (black_bishops - black_knights) * 15
        return imbalances

    def evaluate_threats(self):
        """Custom evaluation for threats with aggressive style adaptation."""
        threats = 0
        # Example: Threats to the opponent's king
        opponent = not self.board.turn
        if self.board.turn == chess.WHITE:
            king_square = self.board.king(chess.BLACK)
        else:
            king_square = self.board.king(chess.WHITE)
        if king_square is not None:
            threats += len(self.board.attackers(self.board.turn, king_square)) * 20
            threats -= len(self.board.attackers(not self.board.turn, self.board.king(chess.WHITE if not self.board.turn else chess.BLACK))) * 20
        return threats

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

    def reset_engine(self):
        """Resets the engine to initial state."""
        self.board.reset()
        self.transposition_table.clear()
        self.killer_moves.clear()

    def print_statistics(self):
        """Prints engine statistics."""
        print(f"Nodes searched: {self.node_count}")
        print(f"Time taken: {time.time() - self.start_time:.2f} seconds")

def main():
    engine = UPPlusChessEngine(time_limit=5.0)  # 5 seconds per move
    print("Welcome to UP+ Chess Engine Full Application!")
    print("Enter your moves in standard algebraic notation (e.g., e4, Nf3). Type 'quit' to exit.")
    print(engine.display_board())

    while not engine.board.is_game_over():
        # User move
        user_move = input("Your move: ")
        if user_move.lower() == 'quit':
            break
        success, move_or_msg = engine.make_move(user_move)
        if not success:
            print(move_or_msg)
            continue
        print(engine.display_board())
        if engine.board.is_game_over():
            break
        # Engine move
        print("Engine is thinking...")
        engine_move = engine.play_engine_move()
        if engine_move is None:
            print("Engine has no legal moves. You win!")
            break
        # Get SAN before pushing the move
        try:
            san_move = engine.board.san(engine_move)
        except ValueError:
            # If san() fails, fallback to UCI
            san_move = engine_move.uci()
        # Push the move after getting SAN
        engine.board.push(engine_move)
        engine.print_statistics()
        print(f"Engine move: {san_move}")
        print(engine.display_board())

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

if __name__ == "__main__":
    main()