Traceback (most recent call last):
  File "C:\Users\hp\Downloads\fert.py", line 897, in <module>
    main()
  File "C:\Users\hp\Downloads\fert.py", line 868, in main
    engine.train(data, stockfish_path="stockfish", epochs=10, learning_rate=0.001)
  File "C:\Users\hp\Downloads\fert.py", line 632, in train
    dataloader = DataLoader(dataset, batch_size=64, shuffle=True, num_workers=2)
                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\hp\AppData\Local\Programs\Python\Python312\Lib\site-packages\torch\utils\data\dataloader.py", line 350, in __init__
    sampler = RandomSampler(dataset, generator=generator)  # type: ignore[arg-type]
              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\hp\AppData\Local\Programs\Python\Python312\Lib\site-packages\torch\utils\data\sampler.py", line 143, in __init__
    raise ValueError(f"num_samples should be a positive integer value, but got num_samples={self.num_samples}")
ValueError: num_samples should be a positive integer value, but got num_samples=0

```
"""
NeuroDepth Rook Chess Engine Full Complex Application
=====================================================

A deep learning-powered chess engine that dynamically adjusts its evaluation 
function based on opponent style recognition. Through neural network profiling, 
it learns from its own games and adapts its style in real-time, becoming either 
more defensive or aggressive depending on the opponent’s tendencies. It excels 
in closed positions where strategic long-term planning is required, with a 
focus on pawn structure recognition.

Unique Properties:
- Real-time Opponent Profiling
- ELO-Specific Optimization
- Endgame Synaptic Prediction

Special Features:
- Over-training against repetitive opponents
- Style Recognition Network
- Pawnscape AI
- ELO Dynamic Adaptation
- Endgame Neural Synapse

Enhanced with:
- Aesthetic Command-Line UI using Rich
- Advanced Animated Loading Screen
- User Choice of Playing Color
- Move Validation and Error Handling
- Move History Display
- Undo Move Feature
- Save and Load Games
- Real-Time Board Updates
- Enhanced AI Move Selection with Minimax
- Game Timer
- Improved Training Feedback
- Option to Quit During Gameplay
- **9K Enhanced Parameter Training**

Ensure required packages are installed:
pip install python-chess torch requests rich
"""

import requests
import chess
import chess.pgn
import chess.engine
import random
import io
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import Dataset, DataLoader
import numpy as np
import json
from collections import defaultdict
import os
import pickle
import time
from rich.console import Console
from rich.table import Table
from rich.progress import Progress, SpinnerColumn, BarColumn, TextColumn, TimeElapsedColumn
from rich.prompt import Prompt, Confirm
from rich.text import Text
from rich.live import Live
from rich.panel import Panel
from rich.align import Align
from rich.spinner import Spinner
from datetime import datetime

# Initialize Rich console
console = Console()

# -------------------------------
# Neural Network Evaluator (NNUE)
# -------------------------------

class NNUE(nn.Module):
    def __init__(self, input_size=64*12, hidden_size=512, hidden_layers=3, output_size=1, dropout=0.3):
        super(NNUE, self).__init__()
        layers = []
        layers.append(nn.Linear(input_size, hidden_size))
        layers.append(nn.BatchNorm1d(hidden_size))
        layers.append(nn.ReLU())
        layers.append(nn.Dropout(dropout))
        for _ in range(hidden_layers - 1):
            layers.append(nn.Linear(hidden_size, hidden_size))
            layers.append(nn.BatchNorm1d(hidden_size))
            layers.append(nn.ReLU())
            layers.append(nn.Dropout(dropout))
        layers.append(nn.Linear(hidden_size, output_size))
        self.network = nn.Sequential(*layers)

    def forward(self, x):
        return self.network(x)

# -------------------------------
# Dataset Class
# -------------------------------

class ChessDataset(Dataset):
    def __init__(self, positions, evaluations):
        self.positions = positions
        self.evaluations = evaluations

    def __len__(self):
        return len(self.positions)

    def __getitem__(self, idx):
        board = chess.Board(self.positions[idx])
        input_tensor = board_to_tensor(board)
        target_tensor = torch.tensor([self.evaluations[idx]], dtype=torch.float32)
        return input_tensor, target_tensor

# -------------------------------
# Opponent Profiling
# -------------------------------

class OpponentProfile:
    def __init__(self):
        self.move_history = []
        self.style_score = defaultdict(float)  # tracks different style metrics
        self.update_interval = 20  # number of moves to update profile

    def update_profile(self, move, board):
        """
        Updates the profile based on the opponent's move and the resulting board.
        """
        self.move_history.append((move, board.fen()))
        if len(self.move_history) >= self.update_interval:
            self.analyze_style()
            self.move_history = []

    def analyze_style(self):
        """
        Analyze the recent moves to determine the opponent's style.
        """
        aggressive_moves = 0
        defensive_moves = 0
        for move, fen in self.move_history:
            # Simplistic heuristic: captures and checks are aggressive
            board = chess.Board(fen)
            if board.is_capture(move) or board.gives_check(move):
                aggressive_moves += 1
            else:
                defensive_moves += 1
        total = aggressive_moves + defensive_moves
        if total == 0:
            return
        self.style_score['aggressive'] += aggressive_moves / total
        self.style_score['defensive'] += defensive_moves / total

    def get_style(self):
        """
        Returns the dominant style based on the accumulated scores.
        """
        if not self.style_score:
            return 'balanced'
        if self.style_score['aggressive'] > self.style_score['defensive']:
            return 'aggressive'
        elif self.style_score['defensive'] > self.style_score['aggressive']:
            return 'defensive'
        else:
            return 'balanced'

# -------------------------------
# Pawnscape AI
# -------------------------------

class PawnscapeAI:
    def evaluate_pawns(self, board):
        """
        Evaluates the pawn structure of the given board.
        Returns a score based on pawn islands, doubled pawns, isolated pawns, etc.
        """
        pawns = board.pieces(chess.PAWN, chess.WHITE) | board.pieces(chess.PAWN, chess.BLACK)
        score = 0
        pawn_structure = self.analyze_pawn_structure(board)
        # Assign weights to different pawn structure features
        score -= 0.5 * pawn_structure['doubled_pawns']
        score -= 0.3 * pawn_structure['isolated_pawns']
        score += 0.4 * pawn_structure['connected_pawns']
        score += 0.2 * pawn_structure['passed_pawns']
        return score

    def analyze_pawn_structure(self, board):
        """
        Analyzes the pawn structure and returns a dictionary with features.
        """
        pawn_structure = {
            'doubled_pawns': 0,
            'isolated_pawns': 0,
            'connected_pawns': 0,
            'passed_pawns': 0
        }
        for color in [chess.WHITE, chess.BLACK]:
            pawns = board.pieces(chess.PAWN, color)
            files = [chess.square_file(square) for square in pawns]
            file_counts = defaultdict(int)
            for f in files:
                file_counts[f] += 1
            for f in file_counts:
                if file_counts[f] > 1:
                    pawn_structure['doubled_pawns'] += file_counts[f] - 1
                # Check for isolated pawns
                if f == 0 or f == 7:
                    neighbors = [f + 1] if f == 0 else [f - 1]
                else:
                    neighbors = [f - 1, f + 1]
                if all(file_counts.get(neighbor, 0) == 0 for neighbor in neighbors):
                    pawn_structure['isolated_pawns'] += 1
                # Connected pawns
                if file_counts[f] > 1:
                    pawn_structure['connected_pawns'] += file_counts[f] - 1
            # Passed pawns
            for pawn in pawns:
                file = chess.square_file(pawn)
                rank = chess.square_rank(pawn)
                passed = True
                direction = 1 if color == chess.WHITE else -1
                for r in range(rank + direction, 8 if color == chess.WHITE else -1, direction):
                    for f in [file - 1, file, file + 1]:
                        if 0 <= f < 8:
                            piece = board.piece_at(chess.square(f, r))
                            if piece and piece.piece_type == chess.PAWN and piece.color != color:
                                passed = False
                                break
                    if not passed:
                        break
                if passed:
                    pawn_structure['passed_pawns'] += 1
        return pawn_structure

# -------------------------------
# Endgame Synaptic Prediction
# -------------------------------

class EndgameSynapse:
    def __init__(self, stockfish_path="stockfish"):
        # Initialize Stockfish engine for endgame evaluation
        try:
            self.engine = chess.engine.SimpleEngine.popen_uci(stockfish_path)
            console.print("[green]Endgame Synapse initialized with Stockfish.[/green]")
        except Exception as e:
            console.print(f"[red]Failed to initialize Stockfish: {e}[/red]")
            self.engine = None

    def evaluate_endgame(self, board):
        """
        Uses Stockfish to evaluate endgame positions with higher precision.
        """
        if board.is_game_over() or not self.engine:
            return 0
        if self.is_endgame(board):
            try:
                result = self.engine.analyse(board, chess.engine.Limit(depth=30))
                score = result['score'].white().score(mate_score=1000)
                if score is None:
                    score = 0
                return score / 100.0
            except Exception as e:
                console.print(f"[red]Error during endgame evaluation: {e}[/red]")
                return 0
        return None

    def is_endgame(self, board):
        """
        Determines if the current position is in the endgame phase.
        A simplistic approach: few pieces on the board.
        """
        piece_count = len(board.piece_map())
        return piece_count <= 10

    def close(self):
        if self.engine:
            self.engine.quit()
            console.print("[green]Stockfish engine closed.[/green]")

# -------------------------------
# ELO Dynamic Adaptation
# -------------------------------

class ELOAdaptor:
    def __init__(self):
        self.opponent_elo = 1500  # Starting estimation
        self.history = []
        self.adjust_factor = 10

    def update_elo(self, game_result):
        """
        Updates the estimated ELO based on game results.
        game_result: 1 (win), 0.5 (draw), 0 (loss)
        """
        # Simple ELO update based on result
        # This is a placeholder for a more sophisticated approach
        if len(self.history) < 10:
            self.history.append(game_result)
            self.opponent_elo = 1500 + sum(self.history) * 20  # Simplistic adjustment
        else:
            self.history.pop(0)
            self.history.append(game_result)
            self.opponent_elo = 1500 + sum(self.history) * 20

    def get_opponent_elo(self):
        return self.opponent_elo

# -------------------------------
# Movement Selector with Minimax
# -------------------------------

class MovementSelector:
    def __init__(self, evaluator, pawnscape_ai, endgame_synapse):
        self.evaluator = evaluator
        self.pawnscape_ai = pawnscape_ai
        self.endgame_synapse = endgame_synapse

    def select_move(self, board, style, depth=3):
        """
        Selects the best move based on the current evaluation and style using Minimax with alpha-beta pruning.
        """
        def minimax(node, depth, alpha, beta, maximizing_player):
            if depth == 0 or node.is_game_over():
                return self.evaluate_board(node, style)

            legal_moves = list(node.legal_moves)
            if maximizing_player:
                max_eval = -float('inf')
                for move in legal_moves:
                    node.push(move)
                    eval = minimax(node, depth - 1, alpha, beta, False)
                    node.pop()
                    max_eval = max(max_eval, eval)
                    alpha = max(alpha, eval)
                    if beta <= alpha:
                        break
                return max_eval
            else:
                min_eval = float('inf')
                for move in legal_moves:
                    node.push(move)
                    eval = minimax(node, depth - 1, alpha, beta, True)
                    node.pop()
                    min_eval = min(min_eval, eval)
                    beta = min(beta, eval)
                    if beta <= alpha:
                        break
                return min_eval

        best_move = None
        best_score = -float('inf') if board.turn == chess.WHITE else float('inf')

        for move in board.legal_moves:
            board.push(move)
            score = minimax(board, depth - 1, -float('inf'), float('inf'), board.turn != chess.WHITE)
            board.pop()

            if board.turn == chess.WHITE:
                if score > best_score:
                    best_score = score
                    best_move = move
            else:
                if score < best_score:
                    best_score = score
                    best_move = move

        return best_move

    def evaluate_board(self, board, style):
        """
        Evaluates the board using NNUE and additional modules.
        """
        tensor = board_to_tensor(board)
        tensor = tensor.unsqueeze(0)  # Add batch dimension
        with torch.no_grad():
            nnue_score = self.evaluator(tensor).item()

        # Adjust score based on style
        if style == 'aggressive':
            style_multiplier = 1.1
        elif style == 'defensive':
            style_multiplier = 0.9
        else:
            style_multiplier = 1.0

        # Incorporate pawn structure
        pawn_score = self.pawnscape_ai.evaluate_pawns(board)

        # Incorporate endgame evaluation
        endgame_score = self.endgame_synapse.evaluate_endgame(board)
        if endgame_score is not None:
            final_score = endgame_score * style_multiplier + pawn_score
        else:
            final_score = nnue_score * style_multiplier + pawn_score

        return final_score

# -------------------------------
# NeuroDepth Rook Chess Engine
# -------------------------------

class NeuroDepthEngine:
    def __init__(self, stockfish_path="stockfish"):
        self.evaluator = NNUE()
        self.load_model_with_loading_screen()
        self.opponent_profile = OpponentProfile()
        self.pawnscape_ai = PawnscapeAI()
        self.endgame_synapse = EndgameSynapse(stockfish_path=stockfish_path)
        self.elo_adaptor = ELOAdaptor()
        self.movement_selector = MovementSelector(self.evaluator, self.pawnscape_ai, self.endgame_synapse)
        self.move_history = []
        self.start_time = None

    def load_model_with_loading_screen(self, model_path='nnue_model.pth'):
        """
        Loads the NNUE model with an animated loading screen.
        """
        with Progress(
            SpinnerColumn(),
            TextColumn("[progress.description]{task.description}"),
            transient=True,
        ) as progress:
            task = progress.add_task("Loading NNUE model...", total=None)
            time.sleep(1)  # Simulate loading time
            if os.path.exists(model_path):
                self.evaluator.load_state_dict(torch.load(model_path))
                self.evaluator.eval()
                progress.update(task, description="Loaded trained NNUE model.")
                time.sleep(1)
            else:
                progress.update(task, description="No trained model found. Initializing a new model.")
                self.evaluator = NNUE()
                time.sleep(1)

    def save_model(self, model_path='nnue_model.pth'):
        """
        Saves the trained NNUE model.
        """
        torch.save(self.evaluator.state_dict(), model_path)
        console.print(f"[green]Saved trained model to {model_path}.[/green]")

    def board_to_tensor_custom(self, board):
        """
        Converts the board to a tensor for NNUE evaluation.
        """
        return board_to_tensor(board)

    def play_game(self, opponent="Random", max_moves=200, user_color='white'):
        """
        Plays a game against the specified opponent.
        opponent: "Random", "Stockfish", or "CheckmateOrCapture"
        user_color: 'white' or 'black'
        """
        board = chess.Board()
        game = chess.pgn.Game()
        node = game
        move_count = 0
        self.move_history = []
        self.start_time = datetime.now()

        # Initialize opponent based on type
        if opponent == "Random":
            opponent_player = RandomPlayer()
        elif opponent == "CheckmateOrCapture":
            opponent_player = CheckmateOrCapturePlayer()
        elif opponent == "Stockfish":
            opponent_player = StockfishPlayer()
        else:
            console.print("[red]Unknown opponent type.[/red]")
            return

        console.print(f"\n[bold cyan]Starting a game against {opponent} as {user_color.capitalize()}[/bold cyan]")

        try:
            while not board.is_game_over() and move_count < max_moves:
                display_final_board(board)
                if (board.turn == chess.WHITE and user_color == 'white') or (board.turn == chess.BLACK and user_color == 'black'):
                    # User's turn
                    move = self.get_user_move(board)
                    if move is None:
                        pass  # No move made (e.g., undo or save)
                    else:
                        board.push(move)
                        node = node.add_variation(move)
                        self.move_history.append(('User', board.san(move)))
                else:
                    # Opponent's turn
                    move = opponent_player.select_move(board)
                    if move is None:
                        pass  # No move made
                    else:
                        if opponent != "Random" and opponent != "CheckmateOrCapture":
                            # Update profile only for human-like opponents
                            self.opponent_profile.update_profile(move, board)
                        board.push(move)
                        node = node.add_variation(move)
                        self.move_history.append((opponent, board.san(move)))
                        console.print(f"[bold red]{opponent}:[/bold red] {board.san(move)}")
                move_count += 1

            # Display the final board
            display_final_board(board)

            # Display move history
            self.display_move_history()

            # Display game result
            console.print(f"\n[bold]Game Result:[/bold] {board.result()}")

            # Update ELO based on result
            if board.result() == '1-0' and user_color == 'white' or board.result() == '0-1' and user_color == 'black':
                self.elo_adaptor.update_elo(1)
                console.print("[green]You won![/green]")
            elif board.result() == '0-1' and user_color == 'white' or board.result() == '1-0' and user_color == 'black':
                self.elo_adaptor.update_elo(0)
                console.print("[red]You lost.[/red]")
            else:
                self.elo_adaptor.update_elo(0.5)
                console.print("[yellow]Game ended in a draw.[/yellow]")

            # Display estimated opponent ELO
            console.print(f"[blue]Estimated Opponent ELO:[/blue] {self.elo_adaptor.get_opponent_elo()}")

            # Optionally, save the game
            save_game = Confirm.ask("Would you like to save this game?")
            if save_game:
                self.save_game(game, board)

        except KeyboardInterrupt:
            console.print("\n[red]Game terminated by user.[/red]")
            return

    def get_user_move(self, board):
        """
        Prompts the user to enter a move and validates it.
        Allows special commands: undo, save, quit.
        """
        while True:
            user_input = Prompt.ask("\nEnter your move (or 'undo', 'save', 'quit')", default="").strip()
            if user_input.lower() == 'quit':
                confirm_quit = Confirm.ask("Are you sure you want to quit the game?")
                if confirm_quit:
                    console.print("[bold red]Quitting the game...[/bold red]")
                    raise KeyboardInterrupt
                else:
                    continue
            elif user_input.lower() == 'undo':
                if board.move_stack:
                    board.pop()
                    if board.move_stack:
                        board.pop()
                        self.move_history = self.move_history[:-2] if len(self.move_history) >=2 else []
                        console.print("[yellow]Last move undone.[/yellow]")
                        display_final_board(board)
                    else:
                        console.print("[yellow]No more moves to undo.[/yellow]")
                    return None
                else:
                    console.print("[yellow]No moves to undo.[/yellow]")
                    return None
            elif user_input.lower() == 'save':
                self.save_current_game(board)
                continue
            else:
                try:
                    move = board.parse_san(user_input)
                    if move in board.legal_moves:
                        return move
                    else:
                        console.print("[red]Illegal move. Please try again.[/red]")
                except ValueError:
                    console.print("[red]Invalid move format. Use standard algebraic notation (e.g., e2e4).[/red]")

    def save_current_game(self, board):
        """
        Saves the current game state to a PGN file.
        """
        game = chess.pgn.Game()
        node = game
        for move in board.move_stack:
            node = node.add_variation(move)
        game.headers["Event"] = "NeuroDepth Engine Game"
        game.headers["Site"] = "Local"
        game.headers["Date"] = datetime.now().strftime("%Y.%m.%d")
        game.headers["Round"] = "-"
        game.headers["White"] = "User" if board.turn == chess.BLACK else "NeuroDepthEngine"
        game.headers["Black"] = "NeuroDepthEngine" if board.turn == chess.BLACK else "User"
        game.headers["Result"] = board.result()

        filename = f"game_{datetime.now().strftime('%Y%m%d_%H%M%S')}.pgn"
        with open(filename, 'w') as f:
            f.write(str(game))
        console.print(f"[green]Game saved to {filename}.[/green]")

    def save_game(self, game, board):
        """
        Saves the completed game to a PGN file.
        """
        game.headers["Event"] = "NeuroDepth Engine Game"
        game.headers["Site"] = "Local"
        game.headers["Date"] = datetime.now().strftime("%Y.%m.%d")
        game.headers["Round"] = "-"
        game.headers["White"] = "User" if board.turn == chess.BLACK else "NeuroDepthEngine"
        game.headers["Black"] = "NeuroDepthEngine" if board.turn == chess.BLACK else "User"
        game.headers["Result"] = board.result()

        filename = f"game_{datetime.now().strftime('%Y%m%d_%H%M%S')}.pgn"
        with open(filename, 'w') as f:
            f.write(str(game))
        console.print(f"[green]Game saved to {filename}.[/green]")

    def display_move_history(self):
        """
        Displays the move history in a table format.
        """
        table = Table(title="Move History", show_header=True, header_style="bold magenta")
        table.add_column("Move Number", justify="right")
        table.add_column("White", justify="left")
        table.add_column("Black", justify="left")

        for i in range(0, len(self.move_history), 2):
            move_number = i//2 + 1
            white_move = self.move_history[i][1] if i < len(self.move_history) else "-"
            black_move = self.move_history[i+1][1] if (i+1) < len(self.move_history) else "-"
            table.add_row(str(move_number), white_move, black_move)

        console.print(table)

    def train(self, games, stockfish_path, epochs=10, learning_rate=0.001):
        """
        Trains the NNUE model with provided game data.
        """
        dataset = ChessDataset(games['positions'], games['evaluations'])
        dataloader = DataLoader(dataset, batch_size=64, shuffle=True, num_workers=2)

        # Define loss and optimizer
        criterion = nn.MSELoss()
        optimizer = optim.Adam(self.evaluator.parameters(), lr=learning_rate)

        scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=5, gamma=0.1)

        console.print("[blue]Starting training on 9,000 games...[/blue]")

        for epoch in range(epochs):
            epoch_loss = 0
            for batch_idx, (inputs, targets) in enumerate(dataloader):
                optimizer.zero_grad()
                outputs = self.evaluator(inputs)
                loss = criterion(outputs, targets)
                loss.backward()
                optimizer.step()
                epoch_loss += loss.item()
            scheduler.step()
            average_loss = epoch_loss / len(dataloader)
            console.print(f"[cyan]Epoch {epoch+1}/{epochs}, Average Loss: {average_loss:.6f}[/cyan]")

        console.print("[green]Training completed successfully.[/green]")
        self.save_model()

    def close(self):
        """
        Closes all engine components.
        """
        self.endgame_synapse.close()

# -------------------------------
# Additional Players
# -------------------------------

class RandomPlayer:
    def __init__(self):
        pass

    def select_move(self, board):
        return random.choice(list(board.legal_moves)) if board.legal_moves else None

class CheckmateOrCapturePlayer:
    def __init__(self):
        pass

    def select_move(self, board):
        if not board.legal_moves:
            return None

        legal_moves = list(board.legal_moves)
        captures = [move for move in legal_moves if board.is_capture(move)]
        checks = [move for move in captures if board.gives_check(move)]
        if checks:
            return random.choice(checks)
        elif captures:
            return random.choice(captures)
        else:
            return random.choice(legal_moves)

class StockfishPlayer:
    def __init__(self, stockfish_path="stockfish", depth=15):
        try:
            self.engine = chess.engine.SimpleEngine.popen_uci(stockfish_path)
            self.depth = depth
            console.print("[green]StockfishPlayer initialized.[/green]")
        except Exception as e:
            console.print(f"[red]Failed to initialize Stockfish: {e}[/red]")
            self.engine = None

    def select_move(self, board):
        if not board.legal_moves or not self.engine:
            return None
        try:
            result = self.engine.play(board, chess.engine.Limit(depth=self.depth))
            return result.move
        except Exception as e:
            console.print(f"[red]Error during Stockfish move selection: {e}[/red]")
            return None

    def close(self):
        if self.engine:
            self.engine.quit()
            console.print("[green]Stockfish engine closed.[/green]")

# -------------------------------
# Utility Functions
# -------------------------------

def download_lichess_games(username, max_games=9000):
    """
    Downloads games from Lichess for the given username.
    """
    console.print(f"[blue]Downloading up to {max_games} games for user '{username}' from Lichess...[/blue]")
    url = f"https://lichess.org/api/games/user/{username}?max={max_games}&pgnInJson=true"
    headers = {'Accept': 'application/x-ndjson'}
    try:
        response = requests.get(url, headers=headers, stream=True)
        if response.status_code != 200:
            raise Exception(f"Failed to fetch data from Lichess API: {response.status_code}")
        games = []
        for line in response.iter_lines():
            if line:
                games.append(json.loads(line))
        console.print(f"[green]Successfully downloaded {len(games)} games from Lichess.[/green]")
        return games
    except Exception as e:
        console.print(f"[red]{e}[/red]")
        return []

def extract_fen_positions_and_evaluations(games, stockfish_path, depth=15):
    """
    Extracts FEN positions and their evaluations using Stockfish.
    """
    console.print("[blue]Extracting FEN positions and evaluations using Stockfish...[/blue]")
    try:
        stockfish = chess.engine.SimpleEngine.popen_uci(stockfish_path)
    except Exception as e:
        console.print(f"[red]Failed to initialize Stockfish: {e}[/red]")
        return [], []

    positions = []
    evaluations = []
    for i, game_json in enumerate(games):
        pgn = game_json.get('pgn')
        if not pgn:
            console.print(f"[yellow]No PGN found for game {i + 1}. Skipping.[/yellow]")
            continue
        try:
            pgn_io = io.StringIO(pgn)
            game = chess.pgn.read_game(pgn_io)
            if game is None:
                console.print(f"[yellow]Failed to parse PGN for game {i + 1}. Skipping.[/yellow]")
                continue
            board = game.board()
            for move in game.mainline_moves():
                board.push(move)
                positions.append(board.fen())
                info = stockfish.analyse(board, chess.engine.Limit(depth=depth))
                score = info["score"].white().score(mate_score=1000)
                if score is None:
                    score = 0
                evaluations.append(score / 100.0)
        except Exception as e:
            console.print(f"[red]Error processing PGN for game {i + 1}: {e}[/red]")
            continue
    stockfish.quit()
    console.print(f"[green]Extracted {len(positions)} positions and evaluations.[/green]")
    return positions, evaluations

def board_to_tensor(board):
    """
    Converts a chess.Board to a PyTorch tensor.
    """
    pieces = ['p', 'n', 'b', 'r', 'q', 'k',
              'P', 'N', 'B', 'R', 'Q', 'K']
    board_tensor = torch.zeros(64 * 12)
    for square in chess.SQUARES:
        piece = board.piece_at(square)
        if piece:
            index = pieces.index(piece.symbol())
            board_tensor[square * 12 + index] = 1
    return board_tensor

def preprocess_data(positions, evaluations):
    """
    Preprocesses the positions and evaluations into tensor pairs.
    """
    return {'positions': positions, 'evaluations': evaluations}

# -------------------------------
# Main Execution
# -------------------------------

def main():
    console.print(Panel("[bold magenta]Welcome to NeuroDepth Rook Chess Engine[/bold magenta]", style="bold cyan"))

    engine = NeuroDepthEngine(stockfish_path=r"C:\Users\hp\Downloads\Stock\stockfish\Stocky.exe")  # Ensure Stockfish is in PATH or provide full path

    while True:
        console.print("\n[bold green]Main Menu:[/bold green]")
        console.print("1. [cyan]Play a game[/cyan]")
        console.print("2. [cyan]Train the model with Lichess games[/cyan]")
        console.print("3. [cyan]Load a saved game[/cyan]")
        console.print("4. [cyan]Exit[/cyan]")

        choice = Prompt.ask("\nChoose an option", choices=["1", "2", "3", "4"], default="1")

        if choice == "1":
            # Choose opponent and user color
            console.print("\n[bold blue]Choose an opponent:[/bold blue]")
            console.print("1. [green]Random Player[/green]")
            console.print("2. [green]CheckmateOrCapture Player[/green]")
            console.print("3. [green]Stockfish Player[/green]")

            opponent_choice = Prompt.ask("\nEnter opponent number", choices=["1", "2", "3"], default="1")
            opponent_mapping = {
                "1": "Random",
                "2": "CheckmateOrCapture",
                "3": "Stockfish"
            }
            opponent = opponent_mapping.get(opponent_choice, "Random")

            console.print("\n[bold blue]Choose your color:[/bold blue]")
            console.print("1. [green]White[/green]")
            console.print("2. [green]Black[/green]")

            # Modified input validation for color choice (optional as choices are restricted)
            color_choice = Prompt.ask("\nEnter color number", choices=["1", "2"], default="1")
            user_color = "white" if color_choice == "1" else "black"

            try:
                engine.play_game(opponent=opponent, max_moves=200, user_color=user_color)
            except KeyboardInterrupt:
                console.print("\n[red]Game terminated by user.[/red]")
                continue

        elif choice == "2":
            username = Prompt.ask("\nEnter Lichess username to download games", default="NoOneCanBeatMe4")
            # Replace regex-based input with manual validation
            while True:
                user_input = Prompt.ask("Enter number of games to download (max 9000)", default="9000")
                if user_input.isdigit():
                    max_games = int(user_input)
                    if 1 <= max_games <= 9000:
                        break
                console.print("[red]Invalid input. Please enter a number between 1 and 9000.[/red]")
            games_raw = download_lichess_games(username, max_games=max_games)
            if games_raw:
                # Extract positions and evaluations
                positions, evaluations = extract_fen_positions_and_evaluations(games_raw, stockfish_path="stockfish", depth=15)
                if len(positions) < 9000:
                    console.print(f"[yellow]Warning: Only {len(positions)} positions extracted. Expected 9000.[/yellow]")
                data = preprocess_data(positions, evaluations)
                # Train the model
                engine.train(data, stockfish_path="stockfish", epochs=10, learning_rate=0.001)

        elif choice == "3":
            filename = Prompt.ask("\nEnter the filename of the saved game to load", default="")
            if not filename.endswith(".pgn"):
                filename += ".pgn"
            if os.path.exists(filename):
                try:
                    with open(filename, 'r') as f:
                        game = chess.pgn.read_game(f)
                    board = game.board()
                    engine.move_history = []
                    for move in game.mainline_moves():
                        board.push(move)
                        engine.move_history.append(('Saved Game', board.san(move)))
                    display_final_board(board)
                    engine.display_move_history()
                    console.print(f"\n[bold]Loaded Game Result:[/bold] {game.headers.get('Result', 'Unknown')}")
                except Exception as e:
                    console.print(f"[red]Failed to load game: {e}[/red]")
            else:
                console.print(f"[red]File '{filename}' does not exist.[/red]")

        elif choice == "4":
            console.print("\n[bold red]Exiting NeuroDepth Rook Chess Engine. Goodbye![/bold red]")
            engine.close()
            break

if __name__ == "__main__":
    main()