Implement tactical detection module
This commit is contained in:
@@ -15,6 +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";
|
||||
|
||||
interface ChessGameProps {
|
||||
initialFen?: string;
|
||||
@@ -328,6 +329,19 @@ export default function ChessGame({ initialFen, initialPgn, initialPersonality,
|
||||
|
||||
// 5. Complete the history item with computer's move data (only if we have evalP0)
|
||||
if (partialHistoryItem && evalP0) {
|
||||
const isWhite = playerColor === 'white';
|
||||
const evalBefore = isWhite ? evalP0.score : -evalP0.score;
|
||||
const evalAfterPlayerMove = isWhite ? -p1Eval.score : p1Eval.score;
|
||||
const cpLoss = evalBefore - evalAfterPlayerMove;
|
||||
const bestMoveSan = uciToSan(fenP0, evalP0.bestMove);
|
||||
const missedTactics = detectMissedTactics({
|
||||
fen: fenP0,
|
||||
playerColor,
|
||||
playerMoveSan: moveResult.result.san,
|
||||
bestMoveUci: evalP0.bestMove,
|
||||
cpLoss,
|
||||
});
|
||||
|
||||
const completeHistoryItem: MoveHistoryItem = {
|
||||
...partialHistoryItem,
|
||||
computerMove: compResult.result.san,
|
||||
@@ -339,6 +353,9 @@ export default function ChessGame({ initialFen, initialPgn, initialPersonality,
|
||||
evalBefore: evalP0.score,
|
||||
evalAfter: p1Eval.score,
|
||||
bestMove: evalP0.bestMove,
|
||||
bestMoveSan,
|
||||
cpLoss,
|
||||
missedTactics,
|
||||
};
|
||||
setMoveHistory(prev => [...prev, completeHistoryItem]);
|
||||
} else {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useState, useEffect, useRef } from "react";
|
||||
import { getGenAIModel } from "@/lib/gemini";
|
||||
import { Loader2, X, Trophy, AlertTriangle, RefreshCw } from "lucide-react";
|
||||
import { StockfishEvaluation } from "@/lib/stockfish";
|
||||
import { DetectedTactic } from "@/lib/tacticDetection";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
|
||||
export interface MoveHistoryItem {
|
||||
@@ -29,6 +30,10 @@ export interface MoveHistoryItem {
|
||||
category?: 'inaccuracy' | 'mistake' | 'blunder';
|
||||
cpLoss?: number;
|
||||
|
||||
// Missed tactical opportunities on the engine's best move
|
||||
missedTactics?: DetectedTactic[];
|
||||
bestMoveSan?: string | null;
|
||||
|
||||
// Legacy fields for backward compatibility (deprecated)
|
||||
/** @deprecated Use playerMove instead */
|
||||
move?: string;
|
||||
@@ -77,6 +82,9 @@ export function GameOverModal({ result, winner, history, apiKey, language, onClo
|
||||
let evalAfter: number;
|
||||
let playerMove: string;
|
||||
let bestMove: string | undefined;
|
||||
let bestMoveSan: string | null | undefined;
|
||||
let missedTactics = item.missedTactics;
|
||||
let cpLoss: number | undefined = item.cpLoss;
|
||||
|
||||
if (item.evalBeforePlayerMove && item.evalAfterPlayerMove) {
|
||||
// New enhanced format
|
||||
@@ -91,6 +99,7 @@ export function GameOverModal({ result, winner, history, apiKey, language, onClo
|
||||
|
||||
playerMove = item.playerMove;
|
||||
bestMove = item.evalBeforePlayerMove.bestMove;
|
||||
bestMoveSan = item.bestMoveSan;
|
||||
} else {
|
||||
// Legacy format (backward compatibility)
|
||||
evalBefore = item.evalBefore || 0;
|
||||
@@ -102,21 +111,24 @@ export function GameOverModal({ result, winner, history, apiKey, language, onClo
|
||||
// Calculate centipawn loss
|
||||
// Positive delta = position got worse for player
|
||||
const delta = evalBefore - evalAfter;
|
||||
const cpLossValue = cpLoss ?? delta;
|
||||
let category: 'inaccuracy' | 'mistake' | 'blunder' | null = null;
|
||||
|
||||
if (delta >= 300) category = 'blunder';
|
||||
else if (delta >= 100) category = 'mistake';
|
||||
else if (delta >= 50) category = 'inaccuracy';
|
||||
if (cpLossValue >= 300) category = 'blunder';
|
||||
else if (cpLossValue >= 100) category = 'mistake';
|
||||
else if (cpLossValue >= 50) category = 'inaccuracy';
|
||||
|
||||
return {
|
||||
...item,
|
||||
category,
|
||||
cpLoss: delta,
|
||||
cpLoss: cpLossValue,
|
||||
// Ensure legacy fields are populated for display
|
||||
move: playerMove,
|
||||
evalBefore: evalBefore,
|
||||
evalAfter: evalAfter,
|
||||
bestMove: bestMove,
|
||||
bestMoveSan,
|
||||
missedTactics,
|
||||
};
|
||||
}).filter(item => item.category !== null) as MoveHistoryItem[];
|
||||
|
||||
@@ -130,9 +142,23 @@ export function GameOverModal({ result, winner, history, apiKey, language, onClo
|
||||
const mistakes = detectedMistakes.filter(m => m.category === 'mistake');
|
||||
const inaccuracies = detectedMistakes.filter(m => m.category === 'inaccuracy');
|
||||
|
||||
const mistakesText = detectedMistakes.map(m =>
|
||||
`Move ${m.moveNumber}: ${m.move} (${m.category?.toUpperCase()}: -${Math.round(m.cpLoss || 0)}cp loss, eval ${Math.round(m.evalBefore || 0)} → ${Math.round(m.evalAfter || 0)}). Best was: ${m.bestMove}`
|
||||
).join("\n");
|
||||
const describeTactics = (tactics?: DetectedTactic[]) => {
|
||||
if (!tactics || tactics.length === 0) return "";
|
||||
const meaningful = tactics.filter(t => t.tactic_type !== 'none');
|
||||
if (meaningful.length === 0) return "";
|
||||
return meaningful.map(t => {
|
||||
const material = t.material_delta ? ` (~${t.material_delta}cp)` : '';
|
||||
const pieces = t.piece_roles ? ` [${t.piece_roles.join(', ')}]` : '';
|
||||
return `${t.tactic_type}${material}${pieces}`;
|
||||
}).join('; ');
|
||||
};
|
||||
|
||||
const mistakesText = detectedMistakes.map(m => {
|
||||
const tacticSummary = describeTactics(m.missedTactics);
|
||||
const bestMoveDisplay = m.bestMoveSan || m.bestMove || 'N/A';
|
||||
const tacticNote = tacticSummary ? ` Tactics missed: ${tacticSummary}.` : '';
|
||||
return `Move ${m.moveNumber}: ${m.move} (${m.category?.toUpperCase()}: -${Math.round(m.cpLoss || 0)}cp loss, eval ${Math.round(m.evalBefore || 0)} → ${Math.round(m.evalAfter || 0)}). Best was: ${bestMoveDisplay}.${tacticNote}`;
|
||||
}).join("\n");
|
||||
|
||||
// Build a complete game narrative for better LLM analysis
|
||||
const gameNarrative = history.map((item, idx) => {
|
||||
|
||||
Reference in New Issue
Block a user