Handle resignation coach response and add analysis option
This commit is contained in:
@@ -48,7 +48,9 @@ jest.mock("./GameAnalysisModal", () => ({
|
||||
}));
|
||||
|
||||
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", () => ({
|
||||
|
||||
+195
-23
@@ -13,7 +13,7 @@ import { SupportedLanguage } from "@/lib/i18n/translations";
|
||||
import { lookupOpening, lookupPossibleOpenings, extractMoveSequenceFromPGN, OpeningMetadata } from "@/lib/openings";
|
||||
import { GameAnalysisModal } from "./GameAnalysisModal";
|
||||
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 { detectMissedTactics, uciToSan, DetectedTactic } from "@/lib/tacticDetection";
|
||||
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 [moveHistory, setMoveHistory] = useState<MoveHistoryItem[]>([]);
|
||||
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 hasRebuiltHistoryRef = useRef(false);
|
||||
|
||||
// Sound Refs
|
||||
const moveSound = useRef<HTMLAudioElement | null>(null);
|
||||
@@ -110,12 +119,125 @@ export default function ChessGame({ gameId, initialFen, initialPgn, initialPerso
|
||||
return () => sf.terminate();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
hasRebuiltHistoryRef.current = false;
|
||||
}, [initialPgn]);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof initialStockfishDepth === 'number') {
|
||||
setStockfishDepth(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
|
||||
useEffect(() => {
|
||||
const storedKey = localStorage.getItem("gemini_api_key");
|
||||
@@ -473,9 +595,43 @@ export default function ChessGame({ gameId, initialFen, initialPgn, initialPerso
|
||||
setEvalP0(null);
|
||||
setEvalP2(null);
|
||||
setOpeningData([]);
|
||||
setResignationContext(null);
|
||||
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 pgn = gameRef.current.pgn();
|
||||
const blob = new Blob([pgn], { type: 'text/plain' });
|
||||
@@ -602,23 +758,34 @@ export default function ChessGame({ gameId, initialFen, initialPgn, initialPerso
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => {
|
||||
const game = gameRef.current;
|
||||
game.undo();
|
||||
game.undo();
|
||||
setFen(game.fen());
|
||||
setUserMove(null);
|
||||
setComputerMove(null);
|
||||
setEvalP0(null);
|
||||
setEvalP2(null);
|
||||
setOpeningData([]);
|
||||
updateCapturedPieces();
|
||||
}}
|
||||
className="flex items-center gap-1 hover:text-red-600 dark:hover:text-red-400 transition-colors"
|
||||
>
|
||||
<ArrowLeft size={12} /> Undo
|
||||
</button>
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
onClick={() => {
|
||||
const game = gameRef.current;
|
||||
game.undo();
|
||||
game.undo();
|
||||
setFen(game.fen());
|
||||
setUserMove(null);
|
||||
setComputerMove(null);
|
||||
setEvalP0(null);
|
||||
setEvalP2(null);
|
||||
setOpeningData([]);
|
||||
updateCapturedPieces();
|
||||
}}
|
||||
className="flex items-center gap-1 hover:text-blue-600 dark:hover:text-blue-400 transition-colors"
|
||||
disabled={!!gameOverState}
|
||||
>
|
||||
<ArrowLeft size={12} /> {t.game.undoMove}
|
||||
</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>
|
||||
|
||||
@@ -656,13 +823,14 @@ export default function ChessGame({ gameId, initialFen, initialPgn, initialPerso
|
||||
language={language}
|
||||
playerColor={playerColor}
|
||||
onCheckComputerMove={checkAndMakeComputerMove}
|
||||
resignationContext={resignationContext}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 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="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">
|
||||
<button
|
||||
onClick={() => setShowDownloadModal(true)}
|
||||
@@ -674,7 +842,7 @@ export default function ChessGame({ gameId, initialFen, initialPgn, initialPerso
|
||||
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"
|
||||
>
|
||||
<Brain size={12} /> Analyze
|
||||
<Brain size={12} /> {t.game.analyze}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -683,8 +851,8 @@ export default function ChessGame({ gameId, initialFen, initialPgn, initialPerso
|
||||
<thead>
|
||||
<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">White</th>
|
||||
<th className="py-1 px-2">Black</th>
|
||||
<th className="py-1 px-2">{t.game.white}</th>
|
||||
<th className="py-1 px-2">{t.game.black}</th>
|
||||
<th className="py-1 px-2 text-center w-20">Eval Δ</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -692,7 +860,7 @@ export default function ChessGame({ gameId, initialFen, initialPgn, initialPerso
|
||||
{moveHistory.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={4} className="py-4 text-center text-gray-500 italic">
|
||||
No moves yet.
|
||||
{t.game.noMovesYet}
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
@@ -757,6 +925,10 @@ export default function ChessGame({ gameId, initialFen, initialPgn, initialPerso
|
||||
language={language}
|
||||
onClose={() => setGameOverState(null)}
|
||||
onNewGame={handleNewGame}
|
||||
onAnalyze={() => {
|
||||
setGameOverState(null);
|
||||
setShowAnalysisModal(true);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -54,9 +54,10 @@ interface GameOverModalProps {
|
||||
language: SupportedLanguage;
|
||||
onClose: () => 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 [isLoading, setIsLoading] = useState(true);
|
||||
const [mistakes, setMistakes] = useState<MoveHistoryItem[]>([]);
|
||||
@@ -329,6 +330,13 @@ Plain text paragraph (2-3 sentences).
|
||||
>
|
||||
Close
|
||||
</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
|
||||
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"
|
||||
|
||||
@@ -15,6 +15,7 @@ import { useTranslation } from '@/lib/i18n/useTranslation';
|
||||
import { SupportedLanguage } from '@/lib/i18n/translations';
|
||||
import { DetectedTactic } from '@/lib/tacticDetection';
|
||||
import { useDebug } from '@/contexts/DebugContext';
|
||||
import { MoveHistoryItem } from './GameOverModal';
|
||||
|
||||
interface TutorProps {
|
||||
game: Chess;
|
||||
@@ -32,6 +33,14 @@ interface TutorProps {
|
||||
language: SupportedLanguage;
|
||||
playerColor: 'white' | 'black';
|
||||
onCheckComputerMove: () => void;
|
||||
resignationContext?: {
|
||||
trigger: number;
|
||||
fen: string;
|
||||
evaluation: StockfishEvaluation | null;
|
||||
history: MoveHistoryItem[];
|
||||
result: string;
|
||||
winner: 'White' | 'Black' | 'Draw';
|
||||
} | null;
|
||||
}
|
||||
|
||||
interface Message {
|
||||
@@ -40,7 +49,7 @@ interface Message {
|
||||
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 [input, setInput] = useState("");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
@@ -460,6 +469,51 @@ INSTRUCTIONS:
|
||||
}, 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;
|
||||
|
||||
return (
|
||||
|
||||
@@ -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 { Stockfish } from '@/lib/stockfish';
|
||||
import { Chess } from 'chess.js';
|
||||
import * as gemini from '@/lib/gemini';
|
||||
import { DebugProvider } from '@/contexts/DebugContext';
|
||||
|
||||
jest.mock('@/lib/stockfish');
|
||||
jest.mock('@/lib/gemini');
|
||||
@@ -36,27 +37,30 @@ describe('Tutor', () => {
|
||||
});
|
||||
|
||||
render(
|
||||
<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={() => {}}
|
||||
/>
|
||||
<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={null}
|
||||
/>
|
||||
</DebugProvider>
|
||||
);
|
||||
|
||||
const hintButton = screen.getByText(/hint/i);
|
||||
@@ -67,4 +71,70 @@ describe('Tutor', () => {
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -62,6 +62,8 @@ export interface Translations {
|
||||
stockfishStrength: string;
|
||||
depth: string;
|
||||
undoMove: string;
|
||||
resign: string;
|
||||
resignation: string;
|
||||
gameHistory: string;
|
||||
analyze: string;
|
||||
noMovesYet: string;
|
||||
@@ -235,6 +237,8 @@ const en: Translations = {
|
||||
stockfishStrength: 'Stockfish Strength',
|
||||
depth: 'Depth',
|
||||
undoMove: 'Undo Last Move',
|
||||
resign: 'Resign',
|
||||
resignation: 'You resigned.',
|
||||
gameHistory: 'Game History',
|
||||
analyze: 'Analyze',
|
||||
noMovesYet: 'No moves yet.',
|
||||
@@ -403,6 +407,8 @@ const de: Translations = {
|
||||
stockfishStrength: 'Stockfish-Stärke',
|
||||
depth: 'Tiefe',
|
||||
undoMove: 'Letzten Zug rückgängig',
|
||||
resign: 'Aufgeben',
|
||||
resignation: 'Du hast aufgegeben.',
|
||||
gameHistory: 'Spielverlauf',
|
||||
analyze: 'Analysieren',
|
||||
noMovesYet: 'Noch keine Züge.',
|
||||
@@ -571,6 +577,8 @@ const fr: Translations = {
|
||||
stockfishStrength: 'Force de Stockfish',
|
||||
depth: 'Profondeur',
|
||||
undoMove: 'Annuler le dernier coup',
|
||||
resign: 'Abandonner',
|
||||
resignation: 'Vous avez abandonné.',
|
||||
gameHistory: 'Historique de la partie',
|
||||
analyze: 'Analyser',
|
||||
noMovesYet: 'Aucun coup pour le moment.',
|
||||
@@ -739,6 +747,8 @@ const it: Translations = {
|
||||
stockfishStrength: 'Forza di Stockfish',
|
||||
depth: 'Profondità',
|
||||
undoMove: 'Annulla ultima mossa',
|
||||
resign: 'Abbandona',
|
||||
resignation: 'Hai abbandonato.',
|
||||
gameHistory: 'Cronologia partita',
|
||||
analyze: 'Analizza',
|
||||
noMovesYet: 'Nessuna mossa ancora.',
|
||||
@@ -907,6 +917,8 @@ const pl: Translations = {
|
||||
stockfishStrength: 'Siła Stockfish',
|
||||
depth: 'Głębokość',
|
||||
undoMove: 'Cofnij ruch',
|
||||
resign: 'Poddaj partię',
|
||||
resignation: 'Poddano partię.',
|
||||
gameHistory: 'Historia partii',
|
||||
analyze: 'Analizuj',
|
||||
noMovesYet: 'Brak ruchów.',
|
||||
|
||||
Reference in New Issue
Block a user