Handle resignation coach response and add analysis option

This commit is contained in:
stefan-kp
2025-12-03 08:45:31 +01:00
parent 327f17ad39
commit f9a245742b
6 changed files with 366 additions and 48 deletions
+3 -1
View File
@@ -48,7 +48,9 @@ jest.mock("./GameAnalysisModal", () => ({
})); }));
jest.mock("./GameOverModal", () => ({ jest.mock("./GameOverModal", () => ({
GameOverModal: () => <div data-testid="game-over-modal">Game Over Modal Mock</div>, GameOverModal: ({ onAnalyze }: { onAnalyze: () => void }) => (
<div data-testid="game-over-modal" onClick={onAnalyze}>Game Over Modal Mock</div>
),
})); }));
jest.mock("./StartScreen", () => ({ jest.mock("./StartScreen", () => ({
+180 -8
View File
@@ -13,7 +13,7 @@ import { SupportedLanguage } from "@/lib/i18n/translations";
import { lookupOpening, lookupPossibleOpenings, extractMoveSequenceFromPGN, 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, Download } from "lucide-react"; import { Brain, ArrowLeft, Download, Flag } from "lucide-react";
import { CapturedPieces } from "./CapturedPieces"; import { CapturedPieces } from "./CapturedPieces";
import { detectMissedTactics, uciToSan, DetectedTactic } from "@/lib/tacticDetection"; import { detectMissedTactics, uciToSan, DetectedTactic } from "@/lib/tacticDetection";
import { upsertSavedGame } from "@/lib/savedGames"; import { upsertSavedGame } from "@/lib/savedGames";
@@ -68,7 +68,16 @@ export default function ChessGame({ gameId, initialFen, initialPgn, initialPerso
const [gameOverState, setGameOverState] = useState<{ result: string, winner: "White" | "Black" | "Draw" } | null>(null); const [gameOverState, setGameOverState] = useState<{ result: string, winner: "White" | "Black" | "Draw" } | null>(null);
const [moveHistory, setMoveHistory] = useState<MoveHistoryItem[]>([]); const [moveHistory, setMoveHistory] = useState<MoveHistoryItem[]>([]);
const [selectedPersonality, setSelectedPersonality] = useState<Personality>(initialPersonality); const [selectedPersonality, setSelectedPersonality] = useState<Personality>(initialPersonality);
const [resignationContext, setResignationContext] = useState<{
trigger: number;
fen: string;
evaluation: StockfishEvaluation | null;
history: MoveHistoryItem[];
result: string;
winner: "White" | "Black" | "Draw";
} | null>(null);
const messagesEndRef = useRef<HTMLDivElement>(null); const messagesEndRef = useRef<HTMLDivElement>(null);
const hasRebuiltHistoryRef = useRef(false);
// Sound Refs // Sound Refs
const moveSound = useRef<HTMLAudioElement | null>(null); const moveSound = useRef<HTMLAudioElement | null>(null);
@@ -110,12 +119,125 @@ export default function ChessGame({ gameId, initialFen, initialPgn, initialPerso
return () => sf.terminate(); return () => sf.terminate();
}, []); }, []);
useEffect(() => {
hasRebuiltHistoryRef.current = false;
}, [initialPgn]);
useEffect(() => { useEffect(() => {
if (typeof initialStockfishDepth === 'number') { if (typeof initialStockfishDepth === 'number') {
setStockfishDepth(initialStockfishDepth); setStockfishDepth(initialStockfishDepth);
} }
}, [initialStockfishDepth]); }, [initialStockfishDepth]);
useEffect(() => {
if (!initialPgn || !stockfish) return;
if (moveHistory.length > 0 || hasRebuiltHistoryRef.current) return;
let isCancelled = false;
hasRebuiltHistoryRef.current = true;
const rebuildHistoryFromPgn = async () => {
try {
const setupFen = initialFen || undefined;
const parsingGame = new Chess(setupFen);
parsingGame.loadPgn(initialPgn);
const verboseMoves = parsingGame.history({ verbose: true });
const replayGame = new Chess(setupFen);
const playerTurnColor = playerColor === 'white' ? 'w' : 'b';
const rebuiltHistory: MoveHistoryItem[] = [];
for (let i = 0; i < verboseMoves.length; i++) {
const move = verboseMoves[i];
// Play through opponent moves until it's the player's turn
if (move.color !== playerTurnColor) {
replayGame.move(move);
continue;
}
const moveNumber = Math.floor(i / 2) + 1;
const fenBeforePlayerMove = replayGame.fen();
const evalBeforePlayerMove = await stockfish.evaluate(fenBeforePlayerMove, stockfishDepth);
const playerMoveResult = replayGame.move(move);
if (!playerMoveResult) break;
const fenAfterPlayerMove = replayGame.fen();
const evalAfterPlayerMove = await stockfish.evaluate(fenAfterPlayerMove, stockfishDepth);
let computerMoveSan = '';
let fenAfterComputerMove = fenAfterPlayerMove;
let evalAfterComputerMove = evalAfterPlayerMove;
if (i + 1 < verboseMoves.length && verboseMoves[i + 1].color !== move.color) {
const computerMove = verboseMoves[i + 1];
const computerMoveResult = replayGame.move(computerMove);
if (computerMoveResult) {
computerMoveSan = computerMoveResult.san;
fenAfterComputerMove = replayGame.fen();
evalAfterComputerMove = await stockfish.evaluate(fenAfterComputerMove, stockfishDepth);
i++; // Skip the computer move we just processed
}
}
const isWhite = playerColor === 'white';
const evalBeforePerspective = isWhite ? evalBeforePlayerMove.score : -evalBeforePlayerMove.score;
const evalAfterPerspective = isWhite ? -evalAfterPlayerMove.score : evalAfterPlayerMove.score;
const cpLoss = evalBeforePerspective - evalAfterPerspective;
const bestMoveUci = evalBeforePlayerMove.bestMove;
const bestMoveSan = bestMoveUci ? uciToSan(fenBeforePlayerMove, bestMoveUci) : null;
const missedTactics = bestMoveUci ? detectMissedTactics({
fen: fenBeforePlayerMove,
playerColor,
playerMoveSan: playerMoveResult.san,
bestMoveUci,
cpLoss,
}) : undefined;
const currentPgn = replayGame.pgn();
const moveSequence = extractMoveSequenceFromPGN(currentPgn);
const possibleOpenings = lookupPossibleOpenings(moveSequence, 5);
rebuiltHistory.push({
moveNumber,
playerMove: playerMoveResult.san,
playerColor,
fenBeforePlayerMove,
evalBeforePlayerMove,
fenAfterPlayerMove,
evalAfterPlayerMove,
computerMove: computerMoveSan || '...',
fenAfterComputerMove,
evalAfterComputerMove,
opening: possibleOpenings.length > 0 ? possibleOpenings[0].name : undefined,
move: playerMoveResult.san,
evalBefore: evalBeforePlayerMove.score,
evalAfter: evalAfterPlayerMove.score,
bestMove: evalBeforePlayerMove.bestMove,
bestMoveSan,
cpLoss,
missedTactics,
});
}
if (!isCancelled) {
setMoveHistory(rebuiltHistory);
}
} catch (error) {
console.error('Failed to rebuild move history from PGN', error);
hasRebuiltHistoryRef.current = false;
}
};
rebuildHistoryFromPgn();
return () => {
isCancelled = true;
};
}, [initialPgn, stockfish, playerColor, stockfishDepth, initialFen, moveHistory.length]);
// Load Settings & Initial State // Load Settings & Initial State
useEffect(() => { useEffect(() => {
const storedKey = localStorage.getItem("gemini_api_key"); const storedKey = localStorage.getItem("gemini_api_key");
@@ -473,9 +595,43 @@ export default function ChessGame({ gameId, initialFen, initialPgn, initialPerso
setEvalP0(null); setEvalP0(null);
setEvalP2(null); setEvalP2(null);
setOpeningData([]); setOpeningData([]);
setResignationContext(null);
updateCapturedPieces(); updateCapturedPieces();
}; };
const handleResign = useCallback(async () => {
if (gameOverState) return;
setIsAnalyzing(false);
const currentFen = gameRef.current.fen();
let evaluation: StockfishEvaluation | null = null;
if (stockfish) {
try {
evaluation = await stockfish.evaluate(currentFen, stockfishDepth);
} catch (error) {
console.error("Failed to evaluate resignation position", error);
}
}
const result = t.game.resignation;
const winner = playerColor === 'white' ? 'Black' : 'White' as const;
setGameOverState({
result,
winner,
});
setResignationContext({
trigger: Date.now(),
fen: currentFen,
evaluation,
history: moveHistory,
result,
winner,
});
}, [gameOverState, moveHistory, playerColor, stockfish, stockfishDepth, t.game.resignation]);
const handleDownloadPGN = () => { const handleDownloadPGN = () => {
const pgn = gameRef.current.pgn(); const pgn = gameRef.current.pgn();
const blob = new Blob([pgn], { type: 'text/plain' }); const blob = new Blob([pgn], { type: 'text/plain' });
@@ -602,6 +758,7 @@ export default function ChessGame({ gameId, initialFen, initialPgn, initialPerso
)} )}
</div> </div>
<div className="flex items-center gap-3">
<button <button
onClick={() => { onClick={() => {
const game = gameRef.current; const game = gameRef.current;
@@ -615,10 +772,20 @@ export default function ChessGame({ gameId, initialFen, initialPgn, initialPerso
setOpeningData([]); 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-blue-600 dark:hover:text-blue-400 transition-colors"
disabled={!!gameOverState}
> >
<ArrowLeft size={12} /> Undo <ArrowLeft size={12} /> {t.game.undoMove}
</button> </button>
<button
onClick={handleResign}
className="flex items-center gap-1 text-red-600 dark:text-red-400 hover:text-red-700 dark:hover:text-red-300 transition-colors"
disabled={!!gameOverState}
>
<Flag size={12} /> {t.game.resign}
</button>
</div>
</div> </div>
</div> </div>
@@ -656,13 +823,14 @@ export default function ChessGame({ gameId, initialFen, initialPgn, initialPerso
language={language} language={language}
playerColor={playerColor} playerColor={playerColor}
onCheckComputerMove={checkAndMakeComputerMove} onCheckComputerMove={checkAndMakeComputerMove}
resignationContext={resignationContext}
/> />
</div> </div>
{/* 4. History (Col 1-3) - Full width at bottom */} {/* 4. History (Col 1-3) - Full width at bottom */}
<div className="md:col-span-3 bg-white dark:bg-gray-800 p-4 rounded-lg shadow-lg flex flex-col"> <div className="md:col-span-3 bg-white dark:bg-gray-800 p-4 rounded-lg shadow-lg flex flex-col">
<div className="flex items-center justify-between mb-2"> <div className="flex items-center justify-between mb-2">
<h3 className="text-sm font-medium text-gray-700 dark:text-gray-300">Game History</h3> <h3 className="text-sm font-medium text-gray-700 dark:text-gray-300">{t.game.gameHistory}</h3>
<div className="flex gap-2"> <div className="flex gap-2">
<button <button
onClick={() => setShowDownloadModal(true)} onClick={() => setShowDownloadModal(true)}
@@ -674,7 +842,7 @@ export default function ChessGame({ gameId, initialFen, initialPgn, initialPerso
onClick={() => setShowAnalysisModal(true)} onClick={() => setShowAnalysisModal(true)}
className="text-xs bg-purple-100 text-purple-700 px-2 py-1 rounded hover:bg-purple-200 dark:bg-purple-900 dark:text-purple-200 flex items-center gap-1" className="text-xs bg-purple-100 text-purple-700 px-2 py-1 rounded hover:bg-purple-200 dark:bg-purple-900 dark:text-purple-200 flex items-center gap-1"
> >
<Brain size={12} /> Analyze <Brain size={12} /> {t.game.analyze}
</button> </button>
</div> </div>
</div> </div>
@@ -683,8 +851,8 @@ export default function ChessGame({ gameId, initialFen, initialPgn, initialPerso
<thead> <thead>
<tr className="text-gray-500 dark:text-gray-400 border-b border-gray-200 dark:border-gray-700"> <tr className="text-gray-500 dark:text-gray-400 border-b border-gray-200 dark:border-gray-700">
<th className="py-1 px-2 w-12">#</th> <th className="py-1 px-2 w-12">#</th>
<th className="py-1 px-2">White</th> <th className="py-1 px-2">{t.game.white}</th>
<th className="py-1 px-2">Black</th> <th className="py-1 px-2">{t.game.black}</th>
<th className="py-1 px-2 text-center w-20">Eval Δ</th> <th className="py-1 px-2 text-center w-20">Eval Δ</th>
</tr> </tr>
</thead> </thead>
@@ -692,7 +860,7 @@ export default function ChessGame({ gameId, initialFen, initialPgn, initialPerso
{moveHistory.length === 0 ? ( {moveHistory.length === 0 ? (
<tr> <tr>
<td colSpan={4} className="py-4 text-center text-gray-500 italic"> <td colSpan={4} className="py-4 text-center text-gray-500 italic">
No moves yet. {t.game.noMovesYet}
</td> </td>
</tr> </tr>
) : ( ) : (
@@ -757,6 +925,10 @@ export default function ChessGame({ gameId, initialFen, initialPgn, initialPerso
language={language} language={language}
onClose={() => setGameOverState(null)} onClose={() => setGameOverState(null)}
onNewGame={handleNewGame} onNewGame={handleNewGame}
onAnalyze={() => {
setGameOverState(null);
setShowAnalysisModal(true);
}}
/> />
)} )}
+9 -1
View File
@@ -54,9 +54,10 @@ interface GameOverModalProps {
language: SupportedLanguage; language: SupportedLanguage;
onClose: () => void; onClose: () => void;
onNewGame: () => void; onNewGame: () => void;
onAnalyze: () => void;
} }
export function GameOverModal({ result, winner, history, apiKey, language, onClose, onNewGame }: GameOverModalProps) { export function GameOverModal({ result, winner, history, apiKey, language, onClose, onNewGame, onAnalyze }: GameOverModalProps) {
const [analysis, setAnalysis] = useState<string>(""); const [analysis, setAnalysis] = useState<string>("");
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [mistakes, setMistakes] = useState<MoveHistoryItem[]>([]); const [mistakes, setMistakes] = useState<MoveHistoryItem[]>([]);
@@ -329,6 +330,13 @@ Plain text paragraph (2-3 sentences).
> >
Close Close
</button> </button>
<button
onClick={onAnalyze}
className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 flex items-center gap-2 shadow-sm"
>
<Trophy size={16} />
Analyze Game
</button>
<button <button
onClick={onNewGame} onClick={onNewGame}
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 flex items-center gap-2 shadow-sm" className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 flex items-center gap-2 shadow-sm"
+55 -1
View File
@@ -15,6 +15,7 @@ import { useTranslation } from '@/lib/i18n/useTranslation';
import { SupportedLanguage } from '@/lib/i18n/translations'; import { SupportedLanguage } from '@/lib/i18n/translations';
import { DetectedTactic } from '@/lib/tacticDetection'; import { DetectedTactic } from '@/lib/tacticDetection';
import { useDebug } from '@/contexts/DebugContext'; import { useDebug } from '@/contexts/DebugContext';
import { MoveHistoryItem } from './GameOverModal';
interface TutorProps { interface TutorProps {
game: Chess; game: Chess;
@@ -32,6 +33,14 @@ interface TutorProps {
language: SupportedLanguage; language: SupportedLanguage;
playerColor: 'white' | 'black'; playerColor: 'white' | 'black';
onCheckComputerMove: () => void; onCheckComputerMove: () => void;
resignationContext?: {
trigger: number;
fen: string;
evaluation: StockfishEvaluation | null;
history: MoveHistoryItem[];
result: string;
winner: 'White' | 'Black' | 'Draw';
} | null;
} }
interface Message { interface Message {
@@ -40,7 +49,7 @@ interface Message {
timestamp: number; timestamp: number;
} }
export function Tutor({ game, currentFen, userMove, computerMove, stockfish, evalP0, evalP2, openingData, missedTactics, 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, resignationContext }: TutorProps) {
const [messages, setMessages] = useState<Message[]>([]); const [messages, setMessages] = useState<Message[]>([]);
const [input, setInput] = useState(""); const [input, setInput] = useState("");
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
@@ -460,6 +469,51 @@ INSTRUCTIONS:
}, 100); }, 100);
}; };
useEffect(() => {
const handleResignationMessage = async () => {
if (!resignationContext || !chatSession) return;
setIsLoading(true);
try {
let evaluation = resignationContext.evaluation;
if (!evaluation && stockfish) {
evaluation = await stockfish.evaluate(resignationContext.fen, 15);
}
const transcript = messages.map(msg => `${msg.role === "user" ? "User" : personality.name}: ${msg.text}`).join("\n");
const whiteEval = evaluation ? `${evaluation.score} cp${evaluation.mate ? ` (mate in ${evaluation.mate})` : ''}` : "N/A";
const blackEval = evaluation ? `${-evaluation.score} cp${evaluation.mate ? ` (mate in ${-evaluation.mate})` : ''}` : "N/A";
const prompt = `
[SYSTEM TRIGGER: resignation]
The user just resigned. Provide a final, in-character message that acknowledges the resignation and offers a brief next step.
RESULT: ${resignationContext.result} (${resignationContext.winner})
CURRENT POSITION FEN: ${resignationContext.fen}
ENGINE EVALUATION: White ${whiteEval}, Black ${blackEval}
RECENT CONVERSATION:
${transcript || 'No prior conversation.'}
INSTRUCTIONS:
- Respond in ${language.toUpperCase()} and stay true to your personality (${personality.name}).
- React naturally to the resignation (sarcastic, encouraging, etc. based on personality).
- Offer a quick suggestion: either invite a rematch or suggest analyzing the game.
- Keep it concise (2-3 sentences).
`;
await sendMessageToChat(prompt, true);
} catch (error) {
console.error("Failed to send resignation message", error);
} finally {
setIsLoading(false);
}
};
handleResignationMessage();
}, [chatSession, language, personality.name, resignationContext?.trigger, resignationContext?.evaluation, resignationContext?.fen, resignationContext?.result, resignationContext?.winner, stockfish]);
if (!apiKey) return null; if (!apiKey) return null;
return ( return (
+71 -1
View File
@@ -1,9 +1,10 @@
import { render, screen, fireEvent, act } from '@testing-library/react'; import { render, screen, fireEvent, act, waitFor } from '@testing-library/react';
import { Tutor } from '../Tutor'; import { Tutor } from '../Tutor';
import { Stockfish } from '@/lib/stockfish'; import { Stockfish } from '@/lib/stockfish';
import { Chess } from 'chess.js'; import { Chess } from 'chess.js';
import * as gemini from '@/lib/gemini'; import * as gemini from '@/lib/gemini';
import { DebugProvider } from '@/contexts/DebugContext';
jest.mock('@/lib/stockfish'); jest.mock('@/lib/stockfish');
jest.mock('@/lib/gemini'); jest.mock('@/lib/gemini');
@@ -36,6 +37,7 @@ describe('Tutor', () => {
}); });
render( render(
<DebugProvider>
<Tutor <Tutor
game={game} game={game}
currentFen={game.fen()} currentFen={game.fen()}
@@ -56,7 +58,9 @@ describe('Tutor', () => {
language="en" language="en"
playerColor="white" playerColor="white"
onCheckComputerMove={() => {}} onCheckComputerMove={() => {}}
resignationContext={null}
/> />
</DebugProvider>
); );
const hintButton = screen.getByText(/hint/i); const hintButton = screen.getByText(/hint/i);
@@ -67,4 +71,70 @@ describe('Tutor', () => {
expect(evaluateSpy).toHaveBeenCalledWith(game.fen(), 15); expect(evaluateSpy).toHaveBeenCalledWith(game.fen(), 15);
}); });
it('sends a resignation follow-up message when provided context', async () => {
const sendMessage = jest.fn().mockResolvedValue({
response: {
text: () => 'Resignation response',
},
});
(gemini.getGenAIModel as jest.Mock).mockReturnValue({
startChat: jest.fn().mockReturnValue({
sendMessage,
}),
});
const evaluation = {
bestMove: 'e2e4',
ponder: null,
score: 50,
mate: null,
depth: 12,
};
render(
<DebugProvider>
<Tutor
game={game}
currentFen={game.fen()}
userMove={null}
computerMove={null}
stockfish={stockfish}
evalP0={null}
evalP2={null}
openingData={null}
missedTactics={null}
onAnalysisComplete={() => {}}
apiKey="test-api-key"
personality={{
name: "Test Personality",
systemPrompt: "Test Prompt",
image: "🤖"
}}
language="en"
playerColor="white"
onCheckComputerMove={() => {}}
resignationContext={{
trigger: Date.now(),
fen: game.fen(),
evaluation,
history: [],
result: 'Resignation',
winner: 'Black',
}}
/>
</DebugProvider>
);
await waitFor(() => {
expect(sendMessage).toHaveBeenCalled();
});
const callsContainResignation = sendMessage.mock.calls.some((call: any[]) =>
String(call[0]).includes('[SYSTEM TRIGGER: resignation]')
);
expect(callsContainResignation).toBe(true);
});
}); });
+12
View File
@@ -62,6 +62,8 @@ export interface Translations {
stockfishStrength: string; stockfishStrength: string;
depth: string; depth: string;
undoMove: string; undoMove: string;
resign: string;
resignation: string;
gameHistory: string; gameHistory: string;
analyze: string; analyze: string;
noMovesYet: string; noMovesYet: string;
@@ -235,6 +237,8 @@ const en: Translations = {
stockfishStrength: 'Stockfish Strength', stockfishStrength: 'Stockfish Strength',
depth: 'Depth', depth: 'Depth',
undoMove: 'Undo Last Move', undoMove: 'Undo Last Move',
resign: 'Resign',
resignation: 'You resigned.',
gameHistory: 'Game History', gameHistory: 'Game History',
analyze: 'Analyze', analyze: 'Analyze',
noMovesYet: 'No moves yet.', noMovesYet: 'No moves yet.',
@@ -403,6 +407,8 @@ const de: Translations = {
stockfishStrength: 'Stockfish-Stärke', stockfishStrength: 'Stockfish-Stärke',
depth: 'Tiefe', depth: 'Tiefe',
undoMove: 'Letzten Zug rückgängig', undoMove: 'Letzten Zug rückgängig',
resign: 'Aufgeben',
resignation: 'Du hast aufgegeben.',
gameHistory: 'Spielverlauf', gameHistory: 'Spielverlauf',
analyze: 'Analysieren', analyze: 'Analysieren',
noMovesYet: 'Noch keine Züge.', noMovesYet: 'Noch keine Züge.',
@@ -571,6 +577,8 @@ const fr: Translations = {
stockfishStrength: 'Force de Stockfish', stockfishStrength: 'Force de Stockfish',
depth: 'Profondeur', depth: 'Profondeur',
undoMove: 'Annuler le dernier coup', undoMove: 'Annuler le dernier coup',
resign: 'Abandonner',
resignation: 'Vous avez abandonné.',
gameHistory: 'Historique de la partie', gameHistory: 'Historique de la partie',
analyze: 'Analyser', analyze: 'Analyser',
noMovesYet: 'Aucun coup pour le moment.', noMovesYet: 'Aucun coup pour le moment.',
@@ -739,6 +747,8 @@ const it: Translations = {
stockfishStrength: 'Forza di Stockfish', stockfishStrength: 'Forza di Stockfish',
depth: 'Profondità', depth: 'Profondità',
undoMove: 'Annulla ultima mossa', undoMove: 'Annulla ultima mossa',
resign: 'Abbandona',
resignation: 'Hai abbandonato.',
gameHistory: 'Cronologia partita', gameHistory: 'Cronologia partita',
analyze: 'Analizza', analyze: 'Analizza',
noMovesYet: 'Nessuna mossa ancora.', noMovesYet: 'Nessuna mossa ancora.',
@@ -907,6 +917,8 @@ const pl: Translations = {
stockfishStrength: 'Siła Stockfish', stockfishStrength: 'Siła Stockfish',
depth: 'Głębokość', depth: 'Głębokość',
undoMove: 'Cofnij ruch', undoMove: 'Cofnij ruch',
resign: 'Poddaj partię',
resignation: 'Poddano partię.',
gameHistory: 'Historia partii', gameHistory: 'Historia partii',
analyze: 'Analizuj', analyze: 'Analizuj',
noMovesYet: 'Brak ruchów.', noMovesYet: 'Brak ruchów.',