This commit is contained in:
Stefan
2025-11-24 17:53:48 +01:00
parent b923f68841
commit 1dd54e8937
4 changed files with 69 additions and 32 deletions
+3 -2
View File
@@ -199,12 +199,13 @@ export default function ChessGame({ initialFen, initialPgn, initialPersonality,
// Pre-Analysis (P0)
useEffect(() => {
if (stockfish && gameRef.current.turn() === 'w' && !isAnalyzing && !gameOverState) {
const playerTurn = playerColor === 'white' ? 'w' : 'b';
if (stockfish && gameRef.current.turn() === playerTurn && !isAnalyzing && !gameOverState) {
stockfish.evaluate(gameRef.current.fen(), stockfishDepth).then(evalResult => {
setEvalP0(evalResult);
}).catch(err => console.error("Pre-analysis failed:", err));
}
}, [fen, stockfish, stockfishDepth, isAnalyzing, gameOverState]);
}, [playerColor, fen, stockfish, stockfishDepth, isAnalyzing, gameOverState]);
const updateCapturedPieces = useCallback(() => {
const history = gameRef.current.history({ verbose: true });
+5 -2
View File
@@ -38,19 +38,22 @@ export function GameAnalysisModal({ fen, stockfish, apiKey, language, onClose }:
// 3. LLM Summary
if (apiKey && evalResult) {
const model = getGenAIModel(apiKey, "gemini-2.5-flash");
const evalInPawns = (evalResult.score / 100).toFixed(2);
const prompt = `
You are a Chess Grandmaster Analyst.
Analyze this position for the user.
DATA:
- FEN: ${fen}
- Evaluation: ${evalResult.score} cp (positive = White advantage, negative = Black advantage)
- Evaluation: ${evalInPawns} pawns (${evalResult.score} centipawns)
Note: Positive = White advantage, Negative = Black advantage
100 centipawns = 1 pawn
- Mate in: ${evalResult.mate ?? "N/A"}
- Best Move: ${evalResult.bestMove}
- Opening: ${openingData ? `${openingData.name} (${openingData.eco})` : "Unknown/Midgame"}
INSTRUCTIONS:
1. Summarize who is winning and why (based on score).
1. Summarize who is winning and why (based on score). Use the pawn value (e.g., "White is up 2.5 pawns" not "250 centipawns").
2. Identify the key strategic factors (space, piece activity, king safety).
3. Mention the opening if relevant.
4. Keep it concise (max 3-4 sentences).
+61 -27
View File
@@ -10,6 +10,8 @@ export interface MoveHistoryItem {
evalBefore: number; // cp
evalAfter: number; // cp
bestMove?: string;
category?: 'inaccuracy' | 'mistake' | 'blunder';
cpLoss?: number;
}
interface GameOverModalProps {
@@ -31,41 +33,56 @@ export function GameOverModal({ result, winner, history, apiKey, language, onClo
const analyzeGame = async () => {
setIsLoading(true);
try {
// 1. Identify Mistakes (Blunders)
// A blunder is roughly a drop of > 100cp (1 pawn) or missing a mate
const detectedMistakes = history.filter(item => {
const delta = item.evalAfter - item.evalBefore;
// Note: eval is from White's perspective.
// If White moves, eval should ideally go up or stay same.
// If eval drops significantly, it's a mistake.
return delta <= -100;
});
// 1. Identify Mistakes with proper categorization
// Standard chess analysis thresholds:
// - Inaccuracy: 50-100 centipawns loss
// - Mistake: 100-300 centipawns loss
// - Blunder: 300+ centipawns loss
const detectedMistakes = history.map(item => {
const delta = item.evalBefore - item.evalAfter; // Positive = eval got worse for player
let category: 'inaccuracy' | 'mistake' | 'blunder' | null = null;
if (delta >= 300) category = 'blunder';
else if (delta >= 100) category = 'mistake';
else if (delta >= 50) category = 'inaccuracy';
return { ...item, category, cpLoss: delta };
}).filter(item => item.category !== null) as MoveHistoryItem[];
setMistakes(detectedMistakes);
// 2. LLM Analysis
if (apiKey) {
const model = getGenAIModel(apiKey, "gemini-2.5-flash");
const blunders = detectedMistakes.filter(m => m.category === 'blunder');
const mistakes = detectedMistakes.filter(m => m.category === 'mistake');
const inaccuracies = detectedMistakes.filter(m => m.category === 'inaccuracy');
const mistakesText = detectedMistakes.map(m =>
`Move ${m.moveNumber}: Played ${m.move} (Eval dropped from ${m.evalBefore} to ${m.evalAfter}). Best move was likely ${m.bestMove}.`
`Move ${m.moveNumber}: ${m.move} (${m.category?.toUpperCase()}: -${m.cpLoss}cp, eval ${m.evalBefore} ${m.evalAfter}). Best: ${m.bestMove}`
).join("\n");
const prompt = `
You are a Chess Coach. The game is over.
Result: ${result} (${winner === "Draw" ? "Draw" : winner + " Won"}).
Here are the player's (White) key mistakes (Blunders):
${mistakesText || "No major blunders detected."}
Player's Performance Summary:
- Blunders (300+ cp loss): ${blunders.length}
- Mistakes (100-300 cp loss): ${mistakes.length}
- Inaccuracies (50-100 cp loss): ${inaccuracies.length}
${mistakesText ? `Detailed Mistakes:\n${mistakesText}` : "No significant mistakes detected - excellent play!"}
INSTRUCTIONS:
1. Briefly comment on the game result.
2. If there were mistakes, explain WHY they were bad and what the player should have looked for (tactics, hanging pieces, etc.).
3. If no mistakes, praise the solid play.
4. Be encouraging but educational.
2. If there were mistakes, explain WHY the worst ones were bad and what the player should have looked for (tactics, hanging pieces, positional errors, etc.).
3. If no mistakes, praise the solid play and suggest areas for improvement.
4. Be encouraging but educational. Focus on learning.
5. Respond in ${language.toUpperCase()}.
OUTPUT FORMAT:
Plain text paragraph.
Plain text paragraph (2-3 sentences).
`;
const resultGen = await model.generateContent(prompt);
@@ -112,18 +129,35 @@ Plain text paragraph.
Key Moments / Mistakes
</h3>
<div className="max-h-40 overflow-y-auto space-y-2 pr-2">
{mistakes.map((m, idx) => (
<div key={idx} className="p-3 bg-orange-50 dark:bg-orange-900/10 border border-orange-100 dark:border-orange-900/30 rounded-lg text-sm">
<span className="font-bold text-gray-900 dark:text-white">Move {m.moveNumber}: {m.move}</span>
<span className="mx-2 text-gray-400">|</span>
<span className="text-red-600 dark:text-red-400">Eval: {m.evalBefore} {m.evalAfter}</span>
{m.bestMove && (
<div className="text-gray-500 dark:text-gray-400 mt-1">
Best was likely: <span className="font-mono">{m.bestMove}</span>
{mistakes.map((m, idx) => {
const categoryColors = {
inaccuracy: 'bg-yellow-50 dark:bg-yellow-900/10 border-yellow-200 dark:border-yellow-900/30 text-yellow-700 dark:text-yellow-400',
mistake: 'bg-orange-50 dark:bg-orange-900/10 border-orange-200 dark:border-orange-900/30 text-orange-700 dark:text-orange-400',
blunder: 'bg-red-50 dark:bg-red-900/10 border-red-200 dark:border-red-900/30 text-red-700 dark:text-red-400'
};
const categoryColor = categoryColors[m.category || 'inaccuracy'];
return (
<div key={idx} className={`p-3 border rounded-lg text-sm ${categoryColor}`}>
<div className="flex items-center gap-2">
<span className="font-bold text-gray-900 dark:text-white">Move {m.moveNumber}: {m.move}</span>
<span className="px-2 py-0.5 rounded text-xs font-semibold uppercase bg-white/50 dark:bg-black/20">
{m.category}
</span>
</div>
)}
</div>
))}
<div className="mt-1 text-xs">
<span className="font-medium">Loss: -{m.cpLoss}cp</span>
<span className="mx-2 text-gray-400">|</span>
<span>Eval: {m.evalBefore} {m.evalAfter}</span>
</div>
{m.bestMove && (
<div className="text-gray-600 dark:text-gray-400 mt-1 text-xs">
Best: <span className="font-mono">{m.bestMove}</span>
</div>
)}
</div>
);
})}
</div>
</div>
)}