Further improve this python chess engine 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()