"use client"; import { useState, useEffect, useRef } from "react"; import { useRouter, useParams } from "next/navigation"; import { Chess, Move, Square } from "chess.js"; import { Chessboard } from "react-chessboard"; import { ArrowLeft, CheckCircle, XCircle, RefreshCw, SkipForward } from "lucide-react"; import Header from "@/components/Header"; import { Tutor } from "@/components/Tutor"; import { useTranslation } from "@/lib/i18n/useTranslation"; import { SupportedLanguage } from "@/lib/i18n/translations"; import { Personality, PERSONALITIES } from "@/lib/personalities"; import { generateTacticExercise, TacticalPatternType, TacticExercise } from "@/lib/tacticalLibrary"; import pinFixtures from "../../../../../fixtures/tactics/pin.json"; type FeedbackState = 'none' | 'correct' | 'incorrect'; export default function TacticalPracticePage() { const router = useRouter(); const params = useParams(); const pattern = (params.pattern as string).toUpperCase() as TacticalPatternType; const [language, setLanguage] = useState('en'); const [mounted, setMounted] = useState(false); const [exercise, setExercise] = useState(null); const [fen, setFen] = useState(''); const [feedback, setFeedback] = useState('none'); const [selectedPersonality, setSelectedPersonality] = useState(PERSONALITIES[0]); const [apiKey, setApiKey] = useState(''); const [userMove, setUserMove] = useState(null); const [setupError, setSetupError] = useState(null); const [showSetupWarning, setShowSetupWarning] = useState(false); const [difficulty, setDifficulty] = useState<'easy' | 'medium' | 'hard'>('easy'); const [currentMoveIndex, setCurrentMoveIndex] = useState(0); // Track progress in move sequence // Statistics tracking const [stats, setStats] = useState({ totalCorrect: 0, totalIncorrect: 0, currentStreak: 0, bestStreak: 0, }); const gameRef = useRef(new Chess()); // Sound Refs const moveSound = useRef(null); const captureSound = useRef(null); const successSound = useRef(null); const errorSound = useRef(null); // Initialize sound effects useEffect(() => { moveSound.current = new Audio('/sounds/move.wav'); captureSound.current = new Audio('/sounds/capture.wav'); successSound.current = new Audio('/sounds/victory.wav'); errorSound.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)); } }; useEffect(() => { const storedLang = localStorage.getItem("chess_tutor_language"); if (storedLang) setLanguage(storedLang as SupportedLanguage); // Load API key from the correct localStorage key const storedApiKey = localStorage.getItem("gemini_api_key"); if (storedApiKey) setApiKey(storedApiKey); const storedPersonalityId = localStorage.getItem("chess_tutor_personality"); if (storedPersonalityId) { const personality = PERSONALITIES.find(p => p.id === storedPersonalityId); if (personality) setSelectedPersonality(personality); } // Check if high-quality puzzles have been set up by checking the fixture metadata // Lichess puzzles have source: "https://database.lichess.org/" const isLichessPuzzles = pinFixtures.source === "https://database.lichess.org/"; const warningDismissed = localStorage.getItem("tactical_puzzles_warning_dismissed"); if (!isLichessPuzzles && !warningDismissed) { setShowSetupWarning(true); } setMounted(true); }, []); useEffect(() => { if (mounted) { loadNewExercise(); } }, [mounted, pattern]); const loadNewExercise = () => { try { // Try to load a puzzle for either side (white or black) // The library will randomly pick from available puzzles const randomSide = Math.random() > 0.5 ? 'white' : 'black'; const newExercise = generateTacticExercise({ patternType: pattern, side: randomSide, difficulty: difficulty, // Use selected difficulty }); console.log('📚 Loaded new exercise:'); console.log(' FEN:', newExercise.startPosition.fen); console.log(' Solution move:', newExercise.solutionMove); console.log(' Move sequence:', newExercise.moves); console.log(' Rating:', newExercise.rating); setExercise(newExercise); setFen(newExercise.startPosition.fen); gameRef.current = new Chess(newExercise.startPosition.fen); setFeedback('none'); setCurrentMoveIndex(0); // Reset move sequence progress setSetupError(null); } catch (error) { console.error('Error loading exercise:', error); // Check if error is due to missing fixtures if (error instanceof Error && error.message.includes('No fixture available')) { setSetupError(`No ${difficulty} puzzles available for this pattern. Try a different difficulty.`); } else { setSetupError('Failed to load tactical exercise. Please try again.'); } } }; const t = useTranslation(language); // Determine which side is to move from the current position const getSideToMove = (): 'white' | 'black' => { if (!exercise) return 'white'; const chess = new Chess(exercise.startPosition.fen); return chess.turn() === 'w' ? 'white' : 'black'; }; const sideToMove = getSideToMove(); if (!mounted) return null; // Show setup error if puzzles are not configured if (setupError) { return (

Setup Required

{setupError}

Run this command in your terminal:

cd chess_tutor && python3 scripts/setup_tactical_puzzles.py

This one-time setup will download high-quality tactical puzzles from Lichess. It may take 5-10 minutes depending on your internet connection.

); } if (!exercise) return null; const getPatternName = (): string => { const mapping: Record = { 'PIN': 'pin', 'SKEWER': 'skewer', 'FORK': 'fork', 'DISCOVERED_CHECK': 'discoveredCheck', 'DOUBLE_ATTACK': 'doubleAttack', 'OVERLOADING': 'overloading', 'BACK_RANK_WEAKNESS': 'backRankWeakness', 'TRAPPED_PIECE': 'trappedPiece', }; return t.learning.patterns[mapping[pattern]]; }; const onDrop = ({ sourceSquare, targetSquare }: { sourceSquare: Square; targetSquare: Square | null }) => { if (!targetSquare || feedback !== 'none') return false; // Additional validation: Check if there's actually a piece on the source square const piece = gameRef.current.get(sourceSquare); if (!piece) { console.warn('⚠️ No piece found on source square:', sourceSquare); return false; } console.log('🎯 Attempting move:', { from: sourceSquare, to: targetSquare, piece: piece.type, color: piece.color }); const move = { from: sourceSquare, to: targetSquare, promotion: "q", }; try { const result = gameRef.current.move(move); if (!result) { console.warn('❌ Invalid move attempted:', move); return false; } // Track the user's move for the Tutor (store the full Move object) setUserMove(result); // Check if this is the correct move in the sequence const moves = exercise.moves || []; // If no move sequence (old format), fall back to single-move check if (moves.length === 0) { const isCorrect = result.from === exercise.solutionMove.from && result.to === exercise.solutionMove.to; if (isCorrect) { playMoveSound(!!result.captured); successSound.current?.play().catch(e => console.error("Audio play failed", e)); setFeedback('correct'); setFen(gameRef.current.fen()); // Update stats for correct answer setStats(prev => ({ totalCorrect: prev.totalCorrect + 1, totalIncorrect: prev.totalIncorrect, currentStreak: prev.currentStreak + 1, bestStreak: Math.max(prev.bestStreak, prev.currentStreak + 1), })); } else { errorSound.current?.play().catch(e => console.error("Audio play failed", e)); setFeedback('incorrect'); // Undo the wrong move instead of resetting to start position // This allows the user to try again from the same position (like chess.com) gameRef.current.undo(); setFen(gameRef.current.fen()); // Update stats for incorrect answer setStats(prev => ({ totalCorrect: prev.totalCorrect, totalIncorrect: prev.totalIncorrect + 1, currentStreak: 0, bestStreak: prev.bestStreak, })); } return true; } // Multi-move sequence handling const expectedMove = moves[currentMoveIndex]; if (!expectedMove || !expectedMove.player) { // This shouldn't happen - we should only be waiting for player moves console.error('Unexpected state: waiting for player move but none expected'); return false; } // Check if the move matches (compare UCI notation) const playerMoveUci = result.from + result.to + (result.promotion || ''); const isCorrect = playerMoveUci === expectedMove.uci; // Debug logging console.log('🔍 Move Validation Debug:'); console.log(' Player move UCI:', playerMoveUci); console.log(' Expected move UCI:', expectedMove.uci); console.log(' Current move index:', currentMoveIndex); console.log(' Is correct?', isCorrect); console.log(' Move result:', result); if (!isCorrect) { errorSound.current?.play().catch(e => console.error("Audio play failed", e)); setFeedback('incorrect'); // Undo the wrong move instead of resetting to start position // This allows the user to try again from the same position (like chess.com) // The currentMoveIndex stays the same since we're still waiting for the same move gameRef.current.undo(); setFen(gameRef.current.fen()); // Update stats for incorrect answer setStats(prev => ({ totalCorrect: prev.totalCorrect, totalIncorrect: prev.totalIncorrect + 1, currentStreak: 0, bestStreak: prev.bestStreak, })); return true; } // Correct move! Play sound and update the board playMoveSound(!!result.captured); setFen(gameRef.current.fen()); // Check if this was the last move in the sequence if (currentMoveIndex >= moves.length - 1) { // Puzzle complete! successSound.current?.play().catch(e => console.error("Audio play failed", e)); setFeedback('correct'); // Update stats for correct answer (only when puzzle is complete) setStats(prev => ({ totalCorrect: prev.totalCorrect + 1, totalIncorrect: prev.totalIncorrect, currentStreak: prev.currentStreak + 1, bestStreak: Math.max(prev.bestStreak, prev.currentStreak + 1), })); return true; } // More moves to go - make the opponent's response automatically const nextMoveIndex = currentMoveIndex + 1; const opponentMove = moves[nextMoveIndex]; if (opponentMove && !opponentMove.player) { // Make opponent's move after a short delay setTimeout(() => { try { const oppMove = gameRef.current.move(opponentMove.uci); if (oppMove) { // Play sound for opponent's move playMoveSound(!!oppMove.captured); setFen(gameRef.current.fen()); setCurrentMoveIndex(nextMoveIndex + 1); // Ready for next player move } } catch (e) { console.error('Failed to make opponent move:', e); } }, 500); // 500ms delay so user can see their move } else { // No opponent response - puzzle complete successSound.current?.play().catch(e => console.error("Audio play failed", e)); setFeedback('correct'); } return true; } catch (e) { return false; } }; const handleTryAgain = () => { // Since we now undo the wrong move immediately when it's made, // the board is already in the correct position (before the error). // We just need to reset the feedback state to allow the user to try again. setFeedback('none'); // Don't reset userMove - keep the chat context // The Tutor will know the user tried again because feedback changed to 'none' }; const handleSkipPuzzle = () => { // Skip counts as incorrect for stats setStats(prev => ({ totalCorrect: prev.totalCorrect, totalIncorrect: prev.totalIncorrect + 1, currentStreak: 0, bestStreak: prev.bestStreak, })); loadNewExercise(); }; const handleNextExercise = () => { loadNewExercise(); }; return ( <>
{/* Header */}
{/* Difficulty Selector */}
Difficulty:
{/* Setup Warning Banner */} {showSetupWarning && (

⚠️ Using basic tactical puzzles

For better quality puzzles from Lichess (5.6M verified puzzles), run:

cd chess_tutor && python3 scripts/setup_tactical_puzzles.py

One-time setup (~5-10 minutes). Current puzzles will work but may have quality issues.

)}

{getPatternName()}

{t.learning.practice.findTheMove} {getPatternName().toLowerCase()}

{/* Puzzle Rating Display */} {exercise.rating && (
Puzzle Rating
{exercise.rating}
{exercise.rating < 1400 ? 'Easy' : exercise.rating < 1800 ? 'Medium' : 'Hard'}
)}
{/* Statistics Display */}
Correct
{stats.totalCorrect}
Incorrect
{stats.totalIncorrect}
Current Streak
{stats.currentStreak}
Best Streak
{stats.bestStreak}
{/* Main Content Grid */}
{/* Chessboard */}
{ console.log('🎲 onPieceDrop called with:', { sourceSquare, targetSquare }); return onDrop({ sourceSquare: sourceSquare as Square, targetSquare: targetSquare as Square | null }); }, darkSquareStyle: { backgroundColor: '#779954' }, lightSquareStyle: { backgroundColor: '#e9edcc' }, animationDurationInMs: 200, boardOrientation: sideToMove }} />
{/* Coach Chat */}
{apiKey ? ( {}} apiKey={apiKey} personality={selectedPersonality} language={language} playerColor={sideToMove} onCheckComputerMove={() => {}} resignationContext={null} tacticalPracticeMode={{ patternName: getPatternName(), solutionMove: exercise.solutionMove, feedback: feedback, moves: exercise.moves || [], currentMoveIndex: currentMoveIndex, stats: stats, }} /> ) : (
🔑

API Key Required

To chat with your coach, please set up your Gemini API key in the settings.

)} {/* Action Buttons */}
{feedback === 'correct' && ( )} {feedback === 'incorrect' && ( <> )} {feedback === 'none' && ( )}
); }