From a9a5366832d2c21dcb3eb85aa947133d4da028e9 Mon Sep 17 00:00:00 2001 From: Stefan Date: Thu, 27 Nov 2025 12:32:59 +0100 Subject: [PATCH] feat: Update play page to use multiple opening detection - Change openingData from single to array in ChessGame.tsx - Update all opening lookups to use lookupPossibleOpenings() - Extract move sequence from PGN for opening lookup - Update Tutor.tsx to accept array of openings - Add smart opening instructions based on count: - 1 opening: Confident identification with metadata - Multiple: List possibilities, suggest general principles - None: Focus on position without inventing names - Pass up to 5 possible openings to LLM in all prompts --- src/components/ChessGame.tsx | 26 ++++++++++++--------- src/components/Tutor.tsx | 45 ++++++++++++++++++++++++------------ 2 files changed, 45 insertions(+), 26 deletions(-) diff --git a/src/components/ChessGame.tsx b/src/components/ChessGame.tsx index 9772a7d..0932d4d 100644 --- a/src/components/ChessGame.tsx +++ b/src/components/ChessGame.tsx @@ -10,7 +10,7 @@ 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 { lookupOpening, lookupPossibleOpenings, extractMoveSequenceFromPGN, OpeningMetadata } from "@/lib/openings"; import { GameAnalysisModal } from "./GameAnalysisModal"; import { GameOverModal, MoveHistoryItem } from "./GameOverModal"; import { Brain, ArrowLeft } from "lucide-react"; @@ -46,7 +46,7 @@ export default function ChessGame({ gameId, initialFen, initialPgn, initialPerso const [evalP2, setEvalP2] = useState(null); // Opening Data - const [openingData, setOpeningData] = useState(null); + const [openingData, setOpeningData] = useState([]); // Tactical Analysis Data const [latestMissedTactics, setLatestMissedTactics] = useState(null); @@ -302,7 +302,7 @@ export default function ChessGame({ gameId, initialFen, initialPgn, initialPerso // Reset Computer State setComputerMove(null); setEvalP2(null); - setOpeningData(null); + setOpeningData([]); setIsAnalyzing(true); const { newFen: fenP1 } = moveResult; @@ -337,9 +337,11 @@ export default function ChessGame({ gameId, initialFen, initialPgn, initialPerso stockfish.evaluate(fenP2, stockfishDepth).then(p2Eval => { setEvalP2(p2Eval); - // 4. Opening Lookup - const opening = lookupOpening(fenP2); - setOpeningData(opening); + // 4. Opening Lookup - Get multiple possible openings + const currentPgn = gameRef.current.pgn(); + const moveSequence = extractMoveSequenceFromPGN(currentPgn); + const possibleOpenings = lookupPossibleOpenings(moveSequence, 5); + setOpeningData(possibleOpenings); // 5. Complete the history item with computer's move data (only if we have evalP0) if (partialHistoryItem && evalP0) { @@ -364,7 +366,7 @@ export default function ChessGame({ gameId, initialFen, initialPgn, initialPerso computerMove: compResult.result.san, fenAfterComputerMove: fenP2, evalAfterComputerMove: p2Eval, - opening: opening?.name, + opening: possibleOpenings.length > 0 ? possibleOpenings[0].name : undefined, // Legacy fields for backward compatibility move: moveResult.result.san, evalBefore: evalP0.score, @@ -424,8 +426,10 @@ export default function ChessGame({ gameId, initialFen, initialPgn, initialPerso // Evaluate the position after computer's move stockfish.evaluate(newFen, stockfishDepth).then(p2Eval => { setEvalP2(p2Eval); - const opening = lookupOpening(newFen); - setOpeningData(opening); + const currentPgn = gameRef.current.pgn(); + const moveSequence = extractMoveSequenceFromPGN(currentPgn); + const possibleOpenings = lookupPossibleOpenings(moveSequence, 5); + setOpeningData(possibleOpenings); setIsAnalyzing(false); }).catch(err => { console.error("Post-computer-move analysis failed:", err); @@ -453,7 +457,7 @@ export default function ChessGame({ gameId, initialFen, initialPgn, initialPerso setComputerMove(null); setEvalP0(null); setEvalP2(null); - setOpeningData(null); + setOpeningData([]); updateCapturedPieces(); }; @@ -565,7 +569,7 @@ export default function ChessGame({ gameId, initialFen, initialPgn, initialPerso setComputerMove(null); setEvalP0(null); setEvalP2(null); - setOpeningData(null); + setOpeningData([]); updateCapturedPieces(); }} className="flex items-center gap-1 hover:text-red-600 dark:hover:text-red-400 transition-colors" diff --git a/src/components/Tutor.tsx b/src/components/Tutor.tsx index 85b7fe8..3242a11 100644 --- a/src/components/Tutor.tsx +++ b/src/components/Tutor.tsx @@ -23,7 +23,7 @@ interface TutorProps { stockfish: Stockfish | null; evalP0: StockfishEvaluation | null; evalP2: StockfishEvaluation | null; - openingData: OpeningMetadata | null; + openingData: OpeningMetadata[]; missedTactics: DetectedTactic[] | null; onAnalysisComplete: () => void; apiKey: string | null; @@ -168,19 +168,34 @@ CRITICAL RULES: // Opening Instruction let openingInstruction = ""; - if (openingData) { - openingInstruction = ` -OPENING IDENTIFIED: ${openingData.name} (${openingData.eco}). -You MUST mention the opening name. + if (openingData && openingData.length > 0) { + if (openingData.length === 1) { + // Single opening identified + const opening = openingData[0]; + openingInstruction = ` +OPENING IDENTIFIED: ${opening.name} (${opening.eco}). +You can confidently reference this opening and its typical plans. You can use this metadata to explain the position: -- Strengths (White): ${openingData.meta?.strengths_white?.join(", ")} -- Weaknesses (White): ${openingData.meta?.weaknesses_white?.join(", ")} -- Strengths (Black): ${openingData.meta?.strengths_black?.join(", ")} -- Weaknesses (Black): ${openingData.meta?.weaknesses_black?.join(", ")} - `; +- Strengths (White): ${opening.meta?.strengths_white?.join(", ") || 'N/A'} +- Weaknesses (White): ${opening.meta?.weaknesses_white?.join(", ") || 'N/A'} +- Strengths (Black): ${opening.meta?.strengths_black?.join(", ") || 'N/A'} +- Weaknesses (Black): ${opening.meta?.weaknesses_black?.join(", ") || 'N/A'} + `; + } else { + // Multiple possible openings + const openingList = openingData.map(o => `- ${o.name} (${o.eco})`).join('\n'); + openingInstruction = ` +OPENING CONTEXT: +Multiple openings are possible from this position: +${openingList} + +INSTRUCTIONS: +- Do NOT claim a specific opening is being played yet +- You may mention "this could lead to..." or "typical of openings like..." +- Focus on general principles rather than specific opening theory + `; + } } else { - // openingInstruction = "NO opening identified. Do NOT invent an opening name. Do NOT mention openings."; - // Relaxed instruction to allow general commentary if no specific opening is found, but still forbid inventing names. openingInstruction = "NO specific opening identified from database. Do NOT invent an opening name. Focus on the position."; } @@ -299,7 +314,7 @@ Current Position Data: - Best Move: ${evaluation?.bestMove} - Evaluation: ${evaluation?.score ?? 'N/A'} centipawns ${evaluation?.score !== undefined ? (evaluation.score > 0 ? '(White is better)' : evaluation.score < 0 ? '(Black is better)' : '(Equal)') : ''} - Mate in: ${evaluation?.mate || 'None'} -- Opening: ${openingData ? `${openingData.name} (${openingData.eco})` : 'Unknown/Midgame'} +- Possible Openings: ${openingData && openingData.length > 0 ? openingData.map(o => `${o.name} (${o.eco})`).join(', ') : 'Unknown/Midgame'} INSTRUCTIONS: - Tell them the best move clearly (e.g., "The best move is e2-e4" or "You should play Nf3") @@ -323,7 +338,7 @@ Current Position Data: - Best Move: ${evaluation?.bestMove} - Evaluation: ${evaluation?.score ?? 'N/A'} centipawns ${evaluation?.score !== undefined ? (evaluation.score > 0 ? '(White is better)' : evaluation.score < 0 ? '(Black is better)' : '(Equal)') : ''} - Mate in: ${evaluation?.mate || 'None'} -- Opening: ${openingData ? `${openingData.name} (${openingData.eco})` : 'Unknown/Midgame'} +- Possible Openings: ${openingData && openingData.length > 0 ? openingData.map(o => `${o.name} (${o.eco})`).join(', ') : 'Unknown/Midgame'} INSTRUCTIONS: - Give a HELPFUL hint without revealing the exact move (unless they specifically ask for it) @@ -342,7 +357,7 @@ Current Position Context: - Evaluation: ${evaluation?.score ?? 'N/A'} centipawns ${evaluation?.score !== undefined ? (evaluation.score > 0 ? '(White is better)' : evaluation.score < 0 ? '(Black is better)' : '(Equal)') : ''} - Best Move: ${evaluation?.bestMove ?? 'N/A'} - Mate in: ${evaluation?.mate || 'None'} -- Opening: ${openingData ? `${openingData.name} (${openingData.eco})` : 'Unknown/Midgame'} +- Possible Openings: ${openingData && openingData.length > 0 ? openingData.map(o => `${o.name} (${o.eco})`).join(', ') : 'Unknown/Midgame'} INSTRUCTIONS: - Answer the user's question based on the CURRENT position data above