Complete tactic recognition module with tests and real-time integration

- Add comprehensive test suite (20 tests) for tactic detection
- Fix pawn detection threshold bug (200 -> 100 centipawns)
- Add error handling to uciToSan for invalid moves
- Integrate tactical data into Tutor component for real-time feedback
- Pass missedTactics from ChessGame to Tutor via props
- Update LLM prompt to explain missed tactical opportunities
- Fix Jest configuration to handle react-markdown ESM issues
- All 50 tests passing
This commit is contained in:
Stefan
2025-11-26 16:51:38 +01:00
parent f0e95ae1a0
commit 49effedf61
8 changed files with 550 additions and 9 deletions
+8 -1
View File
@@ -15,7 +15,7 @@ import { GameAnalysisModal } from "./GameAnalysisModal";
import { GameOverModal, MoveHistoryItem } from "./GameOverModal";
import { Brain, ArrowLeft } from "lucide-react";
import { CapturedPieces } from "./CapturedPieces";
import { detectMissedTactics, uciToSan } from "@/lib/tacticDetection";
import { detectMissedTactics, uciToSan, DetectedTactic } from "@/lib/tacticDetection";
interface ChessGameProps {
initialFen?: string;
@@ -46,6 +46,9 @@ export default function ChessGame({ initialFen, initialPgn, initialPersonality,
// Opening Data
const [openingData, setOpeningData] = useState<OpeningMetadata | null>(null);
// Tactical Analysis Data
const [latestMissedTactics, setLatestMissedTactics] = useState<DetectedTactic[] | null>(null);
const [userMove, setUserMove] = useState<Move | null>(null);
const [computerMove, setComputerMove] = useState<Move | null>(null);
const [isAnalyzing, setIsAnalyzing] = useState(false);
@@ -342,6 +345,9 @@ export default function ChessGame({ initialFen, initialPgn, initialPersonality,
cpLoss,
});
// Store the latest tactics for the Tutor component
setLatestMissedTactics(missedTactics);
const completeHistoryItem: MoveHistoryItem = {
...partialHistoryItem,
computerMove: compResult.result.san,
@@ -585,6 +591,7 @@ export default function ChessGame({ initialFen, initialPgn, initialPersonality,
evalP0={evalP0}
evalP2={evalP2}
openingData={openingData}
missedTactics={latestMissedTactics}
onAnalysisComplete={() => { }}
apiKey={apiKey}
personality={selectedPersonality}
+45 -3
View File
@@ -13,6 +13,7 @@ import ReactMarkdown from "react-markdown";
import { useTranslation } from '@/lib/i18n/useTranslation';
import { SupportedLanguage } from '@/lib/i18n/translations';
import { DetectedTactic } from '@/lib/tacticDetection';
interface TutorProps {
game: Chess;
@@ -23,6 +24,7 @@ interface TutorProps {
evalP0: StockfishEvaluation | null;
evalP2: StockfishEvaluation | null;
openingData: OpeningMetadata | null;
missedTactics: DetectedTactic[] | null;
onAnalysisComplete: () => void;
apiKey: string | null;
personality: Personality;
@@ -37,7 +39,7 @@ interface Message {
timestamp: number;
}
export function Tutor({ game, currentFen, userMove, computerMove, stockfish, evalP0, evalP2, openingData, onAnalysisComplete, apiKey, personality, language, playerColor, onCheckComputerMove }: TutorProps) {
export function Tutor({ game, currentFen, userMove, computerMove, stockfish, evalP0, evalP2, openingData, missedTactics, onAnalysisComplete, apiKey, personality, language, playerColor, onCheckComputerMove }: TutorProps) {
const [messages, setMessages] = useState<Message[]>([]);
const [input, setInput] = useState("");
const [isLoading, setIsLoading] = useState(false);
@@ -182,6 +184,43 @@ You can use this metadata to explain the position:
openingInstruction = "NO specific opening identified from database. Do NOT invent an opening name. Focus on the position.";
}
// Tactical Analysis Instruction
let tacticalInstruction = "";
if (missedTactics && missedTactics.length > 0) {
const meaningfulTactics = missedTactics.filter(t => t.tactic_type !== 'none');
if (meaningfulTactics.length > 0) {
const tacticDescriptions = meaningfulTactics.map(t => {
let desc = `- ${t.tactic_type.toUpperCase()}`;
if (t.piece_roles && t.piece_roles.length > 0) {
desc += ` involving ${t.piece_roles.join(' and ')}`;
}
if (t.material_delta) {
desc += ` (worth ~${t.material_delta} centipawns)`;
}
if (t.affected_squares && t.affected_squares.length > 0) {
desc += ` on squares ${t.affected_squares.join(', ')}`;
}
return desc;
}).join('\n');
tacticalInstruction = `
TACTICAL OPPORTUNITY MISSED:
The User just played ${userMove.san}, but there was a better tactical opportunity available.
The analysis engine identified the following tactical themes that could have been exploited:
${tacticDescriptions}
IMPORTANT CONTEXT:
- This tactical data comes from analyzing what WOULD HAVE HAPPENED if the User had played the best move instead.
- You should explain this missed opportunity in your characteristic style.
- Point out what the User could have done (e.g., "You missed a fork with Nf3!" or "There was a pin available with Bb5!").
- Be educational but stay in character - if you're sarcastic, be sarcastic about the miss; if you're encouraging, be supportive.
- Do NOT mention "the engine" or "the computer" - present this as YOUR analysis as the opponent/tutor.
- Only mention this if the evaluation change was significant enough to warrant it.
`;
}
}
const prompt = `
[SYSTEM TRIGGER: move_exchange]
User (${playerColorName}) Move: ${userMove.san}
@@ -193,10 +232,13 @@ My Internal Thoughts (Data):
- Delta: ${delta} cp
(Note: Scores are from White's perspective. Positive = White advantage, Negative = Black advantage.)
${tacticalInstruction}
INSTRUCTIONS:
1. ${evalInstruction}
2. ${openingInstruction}
3. Respond in ${language}.
3. ${tacticalInstruction ? 'If tactical opportunities were missed (see above), explain them in your style.' : ''}
4. Respond in ${language}.
React to this exchange as the player.
`;
@@ -210,7 +252,7 @@ React to this exchange as the player.
}
};
analyzeExchange();
}, [computerMove, chatSession, evalP0, evalP2, userMove, onAnalysisComplete, openingData, language]);
}, [computerMove, chatSession, evalP0, evalP2, userMove, onAnalysisComplete, openingData, missedTactics, language]);
const evaluateCurrentPosition = async () => {
if (!stockfish) {
+2
View File
@@ -45,6 +45,7 @@ describe('Tutor', () => {
evalP0={null}
evalP2={null}
openingData={null}
missedTactics={null}
onAnalysisComplete={() => {}}
apiKey="test-api-key"
personality={{
@@ -54,6 +55,7 @@ describe('Tutor', () => {
}}
language="en"
playerColor="white"
onCheckComputerMove={() => {}}
/>
);