"use client"; import { useState, useEffect, useCallback, useRef } from "react"; import { Chess, Move } from "chess.js"; import { Chessboard } from "react-chessboard"; import { Stockfish, StockfishEvaluation } from "@/lib/stockfish"; import { Tutor } from "./Tutor"; import { EvaluationBar } from "./EvaluationBar"; import { Personality } from "@/lib/personalities"; import Header from "./Header"; import { useTranslation } from "@/lib/i18n/useTranslation"; import { SupportedLanguage } from "@/lib/i18n/translations"; import { lookupOpening, OpeningMetadata } from "@/lib/openings"; import { GameAnalysisModal } from "./GameAnalysisModal"; import { GameOverModal, MoveHistoryItem } from "./GameOverModal"; import { Brain, ArrowLeft } from "lucide-react"; import { CapturedPieces } from "./CapturedPieces"; interface ChessGameProps { initialFen?: string; initialPgn?: string; initialPersonality: Personality; initialColor: 'white' | 'black'; onBack: () => void; } const PIECE_VALUES: Record = { 'p': 1, 'n': 3, 'b': 3, 'r': 5, 'q': 9, 'k': 0 }; export default function ChessGame({ initialFen, initialPgn, initialPersonality, initialColor, onBack }: ChessGameProps) { const gameRef = useRef(new Chess(initialFen || "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1")); const [fen, setFen] = useState(gameRef.current.fen()); const [stockfish, setStockfish] = useState(null); // Analysis States const [evalP0, setEvalP0] = useState(null); const [evalP2, setEvalP2] = useState(null); // Opening Data const [openingData, setOpeningData] = useState(null); const [userMove, setUserMove] = useState(null); const [computerMove, setComputerMove] = useState(null); const [isAnalyzing, setIsAnalyzing] = useState(false); const [apiKey, setApiKey] = useState(null); const [stockfishDepth, setStockfishDepth] = useState(15); // Settings const [language, setLanguage] = useState('en'); // Game State const [playerColor, setPlayerColor] = useState<'white' | 'black'>(initialColor); const [showAnalysisModal, setShowAnalysisModal] = useState(false); const [gameOverState, setGameOverState] = useState<{ result: string, winner: "White" | "Black" | "Draw" } | null>(null); const [moveHistory, setMoveHistory] = useState([]); const [selectedPersonality, setSelectedPersonality] = useState(initialPersonality); const messagesEndRef = useRef(null); // Sound Refs const moveSound = useRef(null); const captureSound = useRef(null); const checkSound = useRef(null); const victorySound = useRef(null); const defeatSound = useRef(null); useEffect(() => { moveSound.current = new Audio('/sounds/move.wav'); captureSound.current = new Audio('/sounds/capture.wav'); checkSound.current = new Audio('/sounds/check.wav'); victorySound.current = new Audio('/sounds/victory.wav'); defeatSound.current = new Audio('/sounds/defeat.wav'); }, []); const playMoveSound = (captured: boolean) => { if (captured) { captureSound.current?.play().catch(e => console.error("Audio play failed", e)); } else { moveSound.current?.play().catch(e => console.error("Audio play failed", e)); } }; // Scroll to bottom of move history useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); }, [moveHistory]); // Captured Pieces State const [capturedWhitePieces, setCapturedWhitePieces] = useState([]); const [capturedBlackPieces, setCapturedBlackPieces] = useState([]); const [materialScore, setMaterialScore] = useState<{ white: number, black: number }>({ white: 0, black: 0 }); const t = useTranslation(language); // Initialize Stockfish useEffect(() => { const sf = new Stockfish(); setStockfish(sf); return () => sf.terminate(); }, []); // Load Settings & Initial State useEffect(() => { const storedKey = localStorage.getItem("gemini_api_key"); const storedLang = localStorage.getItem("chess_tutor_language"); if (storedKey) setApiKey(storedKey); if (storedLang) setLanguage(storedLang as SupportedLanguage); // If initialFen is provided, ensure gameRef is synced if (initialFen && initialFen !== gameRef.current.fen()) { gameRef.current = new Chess(initialFen); setFen(initialFen); updateCapturedPieces(); // Update captured pieces for loaded game } // If initialPgn is provided, load it to restore history if (initialPgn) { try { gameRef.current.loadPgn(initialPgn); setFen(gameRef.current.fen()); updateCapturedPieces(); } catch (e) { console.error("Failed to load PGN:", e); } } // If computer is white (player is black) and it's the start of the game, make a move // But only if we are at the start position if (initialColor === 'black' && gameRef.current.fen() === "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1" && stockfish) { // Small delay to ensure stockfish is ready setTimeout(() => { stockfish.evaluate(gameRef.current.fen(), 10).then(evalResult => { const computerMoveData = { from: evalResult.bestMove.substring(0, 2), to: evalResult.bestMove.substring(2, 4), promotion: evalResult.bestMove.length > 4 ? evalResult.bestMove.substring(4, 5) : "q" }; makeAMove(computerMoveData); }); }, 1000); } }, [initialFen, initialColor, stockfish]); // Run when these change // Save Game State on Change useEffect(() => { const saveData = { fen, language, selectedPersonality, apiKey, playerColor, // Save player color too pgn: gameRef.current.pgn() }; localStorage.setItem("chess_tutor_save", JSON.stringify(saveData)); }, [fen, language, selectedPersonality, apiKey, playerColor]); // Game Over Detection useEffect(() => { const game = gameRef.current; if (game.isGameOver()) { let result = ""; let winner: "White" | "Black" | "Draw" = "Draw"; if (game.isCheckmate()) { if (game.turn() === 'w') { result = "Checkmate! You lost."; winner = "Black"; if (playerColor === 'white') defeatSound.current?.play().catch(e => console.error(e)); else victorySound.current?.play().catch(e => console.error(e)); } else { result = "Checkmate! You won!"; winner = "White"; if (playerColor === 'white') victorySound.current?.play().catch(e => console.error(e)); else defeatSound.current?.play().catch(e => console.error(e)); } } else if (game.isDraw()) { result = "Draw!"; winner = "Draw"; } else if (game.isStalemate()) { result = "Stalemate!"; winner = "Draw"; } else if (game.inCheck()) { checkSound.current?.play().catch(e => console.error(e)); } setGameOverState({ result, winner }); } }, [fen, playerColor]); // Pre-Analysis (P0) useEffect(() => { if (stockfish && gameRef.current.turn() === 'w' && !isAnalyzing && !gameOverState) { stockfish.evaluate(gameRef.current.fen(), stockfishDepth).then(evalResult => { setEvalP0(evalResult); }).catch(err => console.error("Pre-analysis failed:", err)); } }, [fen, stockfish, stockfishDepth, isAnalyzing, gameOverState]); const updateCapturedPieces = useCallback(() => { const history = gameRef.current.history({ verbose: true }); const whitePiecesLost: string[] = []; const blackPiecesLost: string[] = []; let whiteLostScore = 0; let blackLostScore = 0; history.forEach(move => { if (move.captured) { if (move.color === 'w') { // White moved, captured a black piece. So a black piece was lost. blackPiecesLost.push(move.captured); blackLostScore += PIECE_VALUES[move.captured] || 0; } else { // Black moved, captured a white piece. So a white piece was lost. whitePiecesLost.push(move.captured); whiteLostScore += PIECE_VALUES[move.captured] || 0; } } }); setCapturedWhitePieces(whitePiecesLost); setCapturedBlackPieces(blackPiecesLost); setMaterialScore({ white: whiteLostScore, black: blackLostScore }); }, []); const makeAMove = useCallback( (move: { from: string; to: string; promotion?: string }) => { try { const game = gameRef.current; const result = game.move(move); if (result) { const newFen = game.fen(); setFen(newFen); updateCapturedPieces(); playMoveSound(!!result.captured); // If it was computer's move, update state if (game.turn() === 'w') { // Computer just moved (assuming computer is Black? No, wait) // Logic below handles turns } return { result, newFen }; } } catch (e) { return null; } return null; }, [updateCapturedPieces] ); function onDrop({ sourceSquare, targetSquare }: { sourceSquare: string; targetSquare: string | null }) { if (!targetSquare || !stockfish || gameOverState) return false; const move = { from: sourceSquare, to: targetSquare, promotion: "q", }; // 1. User Move (P0 -> P1) const moveResult = makeAMove(move); if (!moveResult) return false; setUserMove(moveResult.result); // Reset Computer State setComputerMove(null); setEvalP2(null); setOpeningData(null); setIsAnalyzing(true); const { newFen: fenP1 } = moveResult; // 2. Bot Move (P1 -> P2) stockfish.evaluate(fenP1, stockfishDepth).then(p1Eval => { if (evalP0) { const evalAfter = -p1Eval.score; const historyItem: MoveHistoryItem = { moveNumber: gameRef.current.moveNumber(), move: moveResult.result.san, evalBefore: evalP0.score, evalAfter: evalAfter, bestMove: evalP0.bestMove }; setMoveHistory(prev => [...prev, historyItem]); } setTimeout(() => { const computerMoveData = { from: p1Eval.bestMove.substring(0, 2), to: p1Eval.bestMove.substring(2, 4), promotion: p1Eval.bestMove.length > 4 ? p1Eval.bestMove.substring(4, 5) : "q" }; const compResult = makeAMove(computerMoveData); if (compResult) { setComputerMove(compResult.result); const { newFen: fenP2 } = compResult; // 3. Post-Eval (P2) stockfish.evaluate(fenP2, stockfishDepth).then(p2Eval => { setEvalP2(p2Eval); // 4. Opening Lookup const opening = lookupOpening(fenP2); setOpeningData(opening); setIsAnalyzing(false); }).catch(err => { console.error("P2 analysis failed:", err); setIsAnalyzing(false); }); } else { setIsAnalyzing(false); } }, 500); }).catch(err => { console.error("Bot move analysis failed:", err); setIsAnalyzing(false); }); return true; } const handleNewGame = () => { // Reset game to initial props or just reload? // For now, let's just reset the board const newGame = new Chess(); gameRef.current = newGame; setFen(newGame.fen()); setGameOverState(null); setMoveHistory([]); setUserMove(null); setComputerMove(null); setEvalP0(null); setEvalP2(null); setOpeningData(null); updateCapturedPieces(); }; // Determine material advantage // If Black lost more value, White has advantage const whiteAdvantage = materialScore.black - materialScore.white; const blackAdvantage = materialScore.white - materialScore.black; const [showStrengthSlider, setShowStrengthSlider] = useState(false); return ( <>
{/* 1. Header (Col 1-3) */}
{t.game.playingAs} {playerColor === 'white' ? t.game.white : t.game.black} {t.game.vs} {selectedPersonality?.name}
{/* 2. Board Area (Col 1-2) */}
{/* Desktop Eval Bar (Vertical) */}
{/* Opponent's Captured Pieces (Top) */}
0 ? blackAdvantage : null) : (whiteAdvantage > 0 ? whiteAdvantage : null)} />
onDrop({ sourceSquare, targetSquare }), darkSquareStyle: { backgroundColor: '#779954' }, lightSquareStyle: { backgroundColor: '#e9edcc' }, animationDurationInMs: 200, boardOrientation: playerColor }} />
{/* Player's Captured Pieces (Bottom) */}
0 ? whiteAdvantage : null) : (blackAdvantage > 0 ? blackAdvantage : null)} />
{/* Board Footer: Controls */}
{showStrengthSlider && (
setStockfishDepth(parseInt(e.target.value))} className="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer dark:bg-gray-600" />
)}
{/* Mobile Eval Bar (Horizontal) */}
{/* 3. Tutor (Col 3) - Side by side with Board on Desktop */}
{/* Note: We rely on Tutor's internal height styling or pass a class. The Tutor component has 'h-[400px] md:h-full'. Since it's in a grid cell that might stretch, 'h-full' should work if the row has height. However, the Board Area defines the row height. */} { }} apiKey={apiKey} personality={selectedPersonality} language={language} playerColor={playerColor} />
{/* 4. History (Col 1-3) - Full width at bottom */}

Game History

{(() => { const history = gameRef.current.history(); const rows = []; for (let i = 0; i < history.length; i += 2) { rows.push( ); } if (rows.length === 0) { return ( ); } return rows; })()}
# White Black
{Math.floor(i / 2) + 1}. {history[i]} {history[i + 1] || ""}
No moves yet.
{showAnalysisModal && ( setShowAnalysisModal(false)} /> )} {gameOverState && ( setGameOverState(null)} onNewGame={handleNewGame} /> )} ); }