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
This commit is contained in:
Stefan
2025-11-27 12:32:59 +01:00
parent 3f8f623d5c
commit a9a5366832
2 changed files with 45 additions and 26 deletions
+15 -11
View File
@@ -10,7 +10,7 @@ import { Personality } from "@/lib/personalities";
import Header from "./Header"; import Header from "./Header";
import { useTranslation } from "@/lib/i18n/useTranslation"; import { useTranslation } from "@/lib/i18n/useTranslation";
import { SupportedLanguage } from "@/lib/i18n/translations"; 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 { GameAnalysisModal } from "./GameAnalysisModal";
import { GameOverModal, MoveHistoryItem } from "./GameOverModal"; import { GameOverModal, MoveHistoryItem } from "./GameOverModal";
import { Brain, ArrowLeft } from "lucide-react"; import { Brain, ArrowLeft } from "lucide-react";
@@ -46,7 +46,7 @@ export default function ChessGame({ gameId, initialFen, initialPgn, initialPerso
const [evalP2, setEvalP2] = useState<StockfishEvaluation | null>(null); const [evalP2, setEvalP2] = useState<StockfishEvaluation | null>(null);
// Opening Data // Opening Data
const [openingData, setOpeningData] = useState<OpeningMetadata | null>(null); const [openingData, setOpeningData] = useState<OpeningMetadata[]>([]);
// Tactical Analysis Data // Tactical Analysis Data
const [latestMissedTactics, setLatestMissedTactics] = useState<DetectedTactic[] | null>(null); const [latestMissedTactics, setLatestMissedTactics] = useState<DetectedTactic[] | null>(null);
@@ -302,7 +302,7 @@ export default function ChessGame({ gameId, initialFen, initialPgn, initialPerso
// Reset Computer State // Reset Computer State
setComputerMove(null); setComputerMove(null);
setEvalP2(null); setEvalP2(null);
setOpeningData(null); setOpeningData([]);
setIsAnalyzing(true); setIsAnalyzing(true);
const { newFen: fenP1 } = moveResult; const { newFen: fenP1 } = moveResult;
@@ -337,9 +337,11 @@ export default function ChessGame({ gameId, initialFen, initialPgn, initialPerso
stockfish.evaluate(fenP2, stockfishDepth).then(p2Eval => { stockfish.evaluate(fenP2, stockfishDepth).then(p2Eval => {
setEvalP2(p2Eval); setEvalP2(p2Eval);
// 4. Opening Lookup // 4. Opening Lookup - Get multiple possible openings
const opening = lookupOpening(fenP2); const currentPgn = gameRef.current.pgn();
setOpeningData(opening); 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) // 5. Complete the history item with computer's move data (only if we have evalP0)
if (partialHistoryItem && evalP0) { if (partialHistoryItem && evalP0) {
@@ -364,7 +366,7 @@ export default function ChessGame({ gameId, initialFen, initialPgn, initialPerso
computerMove: compResult.result.san, computerMove: compResult.result.san,
fenAfterComputerMove: fenP2, fenAfterComputerMove: fenP2,
evalAfterComputerMove: p2Eval, evalAfterComputerMove: p2Eval,
opening: opening?.name, opening: possibleOpenings.length > 0 ? possibleOpenings[0].name : undefined,
// Legacy fields for backward compatibility // Legacy fields for backward compatibility
move: moveResult.result.san, move: moveResult.result.san,
evalBefore: evalP0.score, evalBefore: evalP0.score,
@@ -424,8 +426,10 @@ export default function ChessGame({ gameId, initialFen, initialPgn, initialPerso
// Evaluate the position after computer's move // Evaluate the position after computer's move
stockfish.evaluate(newFen, stockfishDepth).then(p2Eval => { stockfish.evaluate(newFen, stockfishDepth).then(p2Eval => {
setEvalP2(p2Eval); setEvalP2(p2Eval);
const opening = lookupOpening(newFen); const currentPgn = gameRef.current.pgn();
setOpeningData(opening); const moveSequence = extractMoveSequenceFromPGN(currentPgn);
const possibleOpenings = lookupPossibleOpenings(moveSequence, 5);
setOpeningData(possibleOpenings);
setIsAnalyzing(false); setIsAnalyzing(false);
}).catch(err => { }).catch(err => {
console.error("Post-computer-move analysis failed:", err); console.error("Post-computer-move analysis failed:", err);
@@ -453,7 +457,7 @@ export default function ChessGame({ gameId, initialFen, initialPgn, initialPerso
setComputerMove(null); setComputerMove(null);
setEvalP0(null); setEvalP0(null);
setEvalP2(null); setEvalP2(null);
setOpeningData(null); setOpeningData([]);
updateCapturedPieces(); updateCapturedPieces();
}; };
@@ -565,7 +569,7 @@ export default function ChessGame({ gameId, initialFen, initialPgn, initialPerso
setComputerMove(null); setComputerMove(null);
setEvalP0(null); setEvalP0(null);
setEvalP2(null); setEvalP2(null);
setOpeningData(null); setOpeningData([]);
updateCapturedPieces(); updateCapturedPieces();
}} }}
className="flex items-center gap-1 hover:text-red-600 dark:hover:text-red-400 transition-colors" className="flex items-center gap-1 hover:text-red-600 dark:hover:text-red-400 transition-colors"
+30 -15
View File
@@ -23,7 +23,7 @@ interface TutorProps {
stockfish: Stockfish | null; stockfish: Stockfish | null;
evalP0: StockfishEvaluation | null; evalP0: StockfishEvaluation | null;
evalP2: StockfishEvaluation | null; evalP2: StockfishEvaluation | null;
openingData: OpeningMetadata | null; openingData: OpeningMetadata[];
missedTactics: DetectedTactic[] | null; missedTactics: DetectedTactic[] | null;
onAnalysisComplete: () => void; onAnalysisComplete: () => void;
apiKey: string | null; apiKey: string | null;
@@ -168,19 +168,34 @@ CRITICAL RULES:
// Opening Instruction // Opening Instruction
let openingInstruction = ""; let openingInstruction = "";
if (openingData) { if (openingData && openingData.length > 0) {
openingInstruction = ` if (openingData.length === 1) {
OPENING IDENTIFIED: ${openingData.name} (${openingData.eco}). // Single opening identified
You MUST mention the opening name. 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: You can use this metadata to explain the position:
- Strengths (White): ${openingData.meta?.strengths_white?.join(", ")} - Strengths (White): ${opening.meta?.strengths_white?.join(", ") || 'N/A'}
- Weaknesses (White): ${openingData.meta?.weaknesses_white?.join(", ")} - Weaknesses (White): ${opening.meta?.weaknesses_white?.join(", ") || 'N/A'}
- Strengths (Black): ${openingData.meta?.strengths_black?.join(", ")} - Strengths (Black): ${opening.meta?.strengths_black?.join(", ") || 'N/A'}
- Weaknesses (Black): ${openingData.meta?.weaknesses_black?.join(", ")} - 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 { } 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."; 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} - 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)') : ''} - 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'} - 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: INSTRUCTIONS:
- Tell them the best move clearly (e.g., "The best move is e2-e4" or "You should play Nf3") - 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} - 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)') : ''} - 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'} - 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: INSTRUCTIONS:
- Give a HELPFUL hint without revealing the exact move (unless they specifically ask for it) - 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)') : ''} - 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'} - Best Move: ${evaluation?.bestMove ?? 'N/A'}
- Mate in: ${evaluation?.mate || 'None'} - 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: INSTRUCTIONS:
- Answer the user's question based on the CURRENT position data above - Answer the user's question based on the CURRENT position data above