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:
@@ -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
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user