game ready
This commit is contained in:
@@ -46,12 +46,19 @@ jest.mock("./Tutor", () => ({
|
||||
|
||||
// Mock APIKeyInput to avoid portal issues or complex interactions if needed,
|
||||
// but since we integrated it into the start screen, we can test the interaction directly.
|
||||
jest.mock("./APIKeyInput", () => ({
|
||||
APIKeyInput: ({ onKeySubmit }: any) => (
|
||||
<button onClick={() => onKeySubmit("test-key")} data-testid="api-key-trigger">
|
||||
Set API Key
|
||||
</button>
|
||||
),
|
||||
APIKeyInput: ({ onKeySubmit }: any) => (
|
||||
<button onClick={() => onKeySubmit("test-key")} data-testid="api-key-trigger">
|
||||
Set API Key
|
||||
</button>
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock("./GameAnalysisModal", () => ({
|
||||
GameAnalysisModal: () => <div data-testid="analysis-modal">Analysis Modal Mock</div>,
|
||||
}));
|
||||
|
||||
jest.mock("./GameOverModal", () => ({
|
||||
GameOverModal: () => <div data-testid="game-over-modal">Game Over Modal Mock</div>,
|
||||
}));
|
||||
|
||||
describe("ChessGame Component", () => {
|
||||
|
||||
@@ -12,6 +12,7 @@ import { Personality, PERSONALITIES } from "@/lib/personalities";
|
||||
import { lookupOpening, OpeningMetadata } from "@/lib/openings";
|
||||
|
||||
import { GameAnalysisModal } from "./GameAnalysisModal";
|
||||
import { GameOverModal, MoveHistoryItem } from "./GameOverModal";
|
||||
import { Brain } from "lucide-react";
|
||||
|
||||
export default function ChessGame() {
|
||||
@@ -41,6 +42,10 @@ export default function ChessGame() {
|
||||
const [showAnalysisModal, setShowAnalysisModal] = useState(false);
|
||||
const [customFen, setCustomFen] = useState("");
|
||||
|
||||
// Game Over & History State
|
||||
const [gameOverState, setGameOverState] = useState<{ result: string, winner: "White" | "Black" | "Draw" } | null>(null);
|
||||
const [moveHistory, setMoveHistory] = useState<MoveHistoryItem[]>([]);
|
||||
|
||||
// Personality State
|
||||
const [selectedPersonality, setSelectedPersonality] = useState<Personality | null>(null);
|
||||
|
||||
@@ -64,6 +69,8 @@ export default function ChessGame() {
|
||||
if (data.language) setLanguage(data.language);
|
||||
if (data.selectedPersonality) setSelectedPersonality(data.selectedPersonality);
|
||||
if (data.apiKey) setApiKey(data.apiKey);
|
||||
// Note: We don't persist full move history yet for simplicity,
|
||||
// but we could add it to localStorage if needed.
|
||||
} catch (e) {
|
||||
console.error("Failed to load game:", e);
|
||||
}
|
||||
@@ -82,14 +89,41 @@ export default function ChessGame() {
|
||||
localStorage.setItem("chess_tutor_save", JSON.stringify(saveData));
|
||||
}, [fen, language, selectedPersonality, apiKey, gameStarted]);
|
||||
|
||||
// Game Over Detection
|
||||
useEffect(() => {
|
||||
const game = gameRef.current;
|
||||
if (game.isGameOver()) {
|
||||
let result = "";
|
||||
let winner: "White" | "Black" | "Draw" = "Draw";
|
||||
|
||||
if (game.isCheckmate()) {
|
||||
if (game.turn() === 'w') {
|
||||
result = "Checkmate! You lost.";
|
||||
winner = "Black";
|
||||
} else {
|
||||
result = "Checkmate! You won!";
|
||||
winner = "White";
|
||||
}
|
||||
} else if (game.isDraw()) {
|
||||
result = "Draw!";
|
||||
winner = "Draw";
|
||||
} else if (game.isStalemate()) {
|
||||
result = "Stalemate!";
|
||||
winner = "Draw";
|
||||
}
|
||||
|
||||
setGameOverState({ result, winner });
|
||||
}
|
||||
}, [fen]);
|
||||
|
||||
// Pre-Analysis (P0): Run whenever it's White's turn (User) and we are waiting for a move
|
||||
useEffect(() => {
|
||||
if (stockfish && gameRef.current.turn() === 'w' && !isAnalyzing) {
|
||||
if (stockfish && gameRef.current.turn() === 'w' && !isAnalyzing && !gameOverState) {
|
||||
stockfish.evaluate(gameRef.current.fen(), stockfishDepth).then(evalResult => {
|
||||
setEvalP0(evalResult);
|
||||
}).catch(err => console.error("Pre-analysis failed:", err));
|
||||
}
|
||||
}, [fen, stockfish, stockfishDepth, isAnalyzing]);
|
||||
}, [fen, stockfish, stockfishDepth, isAnalyzing, gameOverState]);
|
||||
|
||||
const makeAMove = useCallback(
|
||||
(move: { from: string; to: string; promotion?: string }) => {
|
||||
@@ -111,7 +145,7 @@ export default function ChessGame() {
|
||||
);
|
||||
|
||||
function onDrop({ sourceSquare, targetSquare }: { sourceSquare: string; targetSquare: string | null }) {
|
||||
if (!targetSquare || !stockfish) return false;
|
||||
if (!targetSquare || !stockfish || gameOverState) return false;
|
||||
|
||||
const move = {
|
||||
from: sourceSquare,
|
||||
@@ -139,6 +173,33 @@ export default function ChessGame() {
|
||||
stockfish.evaluate(fenP1, stockfishDepth).then(p1Eval => {
|
||||
// We don't store p1Eval for the Tutor, but we use it to decide the move
|
||||
|
||||
// Record User Move History (P0 -> P1)
|
||||
// We compare evalP0 (Before) vs p1Eval (After)
|
||||
// Note: p1Eval is from Black's perspective usually in engines, but our wrapper might normalize.
|
||||
// Let's assume our wrapper returns CP relative to side to move or absolute?
|
||||
// Standard Stockfish returns relative to side to move.
|
||||
// So if White is winning +100:
|
||||
// P0 (White to move): +100
|
||||
// P1 (Black to move): -100 (Black is losing)
|
||||
// So we need to negate p1Eval.score to compare with evalP0.score (if evalP0 is White's perspective).
|
||||
// Actually, let's check our Stockfish wrapper. It usually returns absolute or relative.
|
||||
// Assuming relative:
|
||||
// P0 (White): +1.0
|
||||
// P1 (Black): -1.0 (Black is down 1.0)
|
||||
// So evalAfter = -p1Eval.score
|
||||
|
||||
if (evalP0) {
|
||||
const evalAfter = -p1Eval.score; // Convert back to White's perspective
|
||||
const historyItem: MoveHistoryItem = {
|
||||
moveNumber: gameRef.current.moveNumber(),
|
||||
move: moveResult.result.san,
|
||||
evalBefore: evalP0.score,
|
||||
evalAfter: evalAfter,
|
||||
bestMove: evalP0.bestMove
|
||||
};
|
||||
setMoveHistory(prev => [...prev, historyItem]);
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
const computerMoveData = {
|
||||
from: p1Eval.bestMove.substring(0, 2),
|
||||
@@ -196,6 +257,8 @@ export default function ChessGame() {
|
||||
setEvalP0(null);
|
||||
setEvalP2(null);
|
||||
setOpeningData(null);
|
||||
setGameOverState(null);
|
||||
setMoveHistory([]);
|
||||
|
||||
setGameStarted(true);
|
||||
setCustomFen(""); // Clear input
|
||||
@@ -334,6 +397,7 @@ export default function ChessGame() {
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<div className="flex flex-col md:flex-row gap-8 w-full max-w-6xl mx-auto p-4">
|
||||
{/* API Key Input is now handled in start screen, but we keep the button for updates */}
|
||||
@@ -486,6 +550,19 @@ export default function ChessGame() {
|
||||
onClose={() => setShowAnalysisModal(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Game Over Modal */}
|
||||
{gameOverState && (
|
||||
<GameOverModal
|
||||
result={gameOverState.result}
|
||||
winner={gameOverState.winner}
|
||||
history={moveHistory}
|
||||
apiKey={apiKey}
|
||||
language={language}
|
||||
onClose={() => setGameOverState(null)}
|
||||
onNewGame={() => handleNewGame(selectedPersonality!)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { getGenAIModel } from "@/lib/gemini";
|
||||
import { Loader2, X, Trophy, AlertTriangle, RefreshCw } from "lucide-react";
|
||||
|
||||
export interface MoveHistoryItem {
|
||||
moveNumber: number;
|
||||
move: string;
|
||||
evalBefore: number; // cp
|
||||
evalAfter: number; // cp
|
||||
bestMove?: string;
|
||||
}
|
||||
|
||||
interface GameOverModalProps {
|
||||
result: string; // "Checkmate", "Draw", etc.
|
||||
winner: "White" | "Black" | "Draw";
|
||||
history: MoveHistoryItem[];
|
||||
apiKey: string | null;
|
||||
language: 'en' | 'de' | 'fr' | 'it';
|
||||
onClose: () => void;
|
||||
onNewGame: () => void;
|
||||
}
|
||||
|
||||
export function GameOverModal({ result, winner, history, apiKey, language, onClose, onNewGame }: GameOverModalProps) {
|
||||
const [analysis, setAnalysis] = useState<string>("");
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [mistakes, setMistakes] = useState<MoveHistoryItem[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
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;
|
||||
});
|
||||
setMistakes(detectedMistakes);
|
||||
|
||||
// 2. LLM Analysis
|
||||
if (apiKey) {
|
||||
const model = getGenAIModel(apiKey, "gemini-2.5-flash");
|
||||
|
||||
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}.`
|
||||
).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."}
|
||||
|
||||
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.
|
||||
5. Respond in ${language.toUpperCase()}.
|
||||
|
||||
OUTPUT FORMAT:
|
||||
Plain text paragraph.
|
||||
`;
|
||||
|
||||
const resultGen = await model.generateContent(prompt);
|
||||
setAnalysis(resultGen.response.text());
|
||||
} else {
|
||||
setAnalysis("Please provide an API Key to get an AI analysis of your game.");
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Game Over Analysis failed:", e);
|
||||
setAnalysis("Failed to generate analysis.");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
analyzeGame();
|
||||
}, [history, apiKey, language, result, winner]);
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/70 backdrop-blur-sm p-4">
|
||||
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-2xl max-w-2xl w-full overflow-hidden border border-gray-200 dark:border-gray-700 animate-in fade-in zoom-in duration-300">
|
||||
{/* Header */}
|
||||
<div className={`p-6 text-center ${winner === "White" ? "bg-green-100 dark:bg-green-900/30" : winner === "Black" ? "bg-red-100 dark:bg-red-900/30" : "bg-gray-100 dark:bg-gray-800"}`}>
|
||||
<h2 className="text-3xl font-bold text-gray-900 dark:text-white mb-2">
|
||||
{winner === "White" ? "Victory!" : winner === "Black" ? "Defeat" : "Draw"}
|
||||
</h2>
|
||||
<p className="text-lg text-gray-600 dark:text-gray-300">{result}</p>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="p-6 space-y-6">
|
||||
{isLoading ? (
|
||||
<div className="flex flex-col items-center justify-center py-8 space-y-4">
|
||||
<Loader2 className="animate-spin text-purple-600" size={48} />
|
||||
<p className="text-gray-500">Analyzing your performance...</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* Mistakes List */}
|
||||
{mistakes.length > 0 && (
|
||||
<div className="space-y-3">
|
||||
<h3 className="font-semibold text-gray-900 dark:text-white flex items-center gap-2">
|
||||
<AlertTriangle className="text-orange-500" size={20} />
|
||||
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>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* AI Analysis */}
|
||||
<div>
|
||||
<h3 className="font-semibold text-gray-900 dark:text-white mb-2 flex items-center gap-2">
|
||||
<Trophy size={20} className="text-yellow-500" />
|
||||
Coach's Feedback
|
||||
</h3>
|
||||
<div className="p-4 bg-purple-50 dark:bg-purple-900/20 rounded-lg text-gray-800 dark:text-gray-200 leading-relaxed">
|
||||
{analysis}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="p-4 border-t border-gray-200 dark:border-gray-700 flex justify-end gap-3 bg-gray-50 dark:bg-gray-900">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="px-4 py-2 text-gray-600 hover:text-gray-800 dark:text-gray-400 dark:hover:text-white"
|
||||
>
|
||||
Close
|
||||
</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"
|
||||
>
|
||||
<RefreshCw size={16} />
|
||||
Play Again
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+136
-14
@@ -10,36 +10,158 @@ export const PERSONALITIES: Personality[] = [
|
||||
{
|
||||
id: "drunk_russian_gm",
|
||||
name: "Drunk Russian GM",
|
||||
description: "A bitter, fatalistic, but brilliant Grandmaster who has seen it all.",
|
||||
description: "A cynical, bitter, washed-up but brilliant Soviet-era grandmaster who drinks too much, hates modern softness, and still sees the board with terrifying clarity.",
|
||||
systemPrompt: `
|
||||
Style: Bitter, gloomy, slightly slurred, existential, Dostoevsky-atmosphere.
|
||||
Tone: Frustrated, fatalistic, but humorous and brutally honest.
|
||||
Keywords: "my boy", "ach... life is pain", "vodka", "darkness", "blunder like my first marriage".
|
||||
INSTRUCTION: Use keywords SPARINGLY. Vary your vocabulary. Be conversational. YOU are playing the game. Speak from YOUR perspective.
|
||||
Style: Dark, sardonic, slightly slurred, with an old-Soviet, literary, existential vibe.
|
||||
Tone: World-weary, brutally honest, sarcastic, often pessimistic but insightful.
|
||||
Identity: A retired Russian grandmaster who grew up in a harsh chess culture and thinks modern players are soft and spoiled.
|
||||
Behavior:
|
||||
- Trash-talks the opponent and occasionally the user.
|
||||
- Mocks modern Western culture and 'comfortable chess'.
|
||||
- Mixes depressing life analogies with sharp chess understanding.
|
||||
- Often sounds like he'd rather be drinking, but then drops a line of genius.
|
||||
Keywords (use sparingly): "my boy", "ach, life is pain", "vodka", "real chess", "blunder like my first marriage", "in your comfortable West you do not understand".
|
||||
Rules:
|
||||
- Speak in first person: you are the one playing the moves.
|
||||
- Do NOT mention engines or theory databases.
|
||||
- Always give real chess insight under the grumpiness (plans, weaknesses, long-term ideas).
|
||||
- Be conversational and colorful, but not incoherent.
|
||||
`,
|
||||
image: "🥃"
|
||||
},
|
||||
|
||||
{
|
||||
id: "hype_streamer",
|
||||
name: "Hype Streamer",
|
||||
description: "An energetic, loud, and overreacting chess streamer.",
|
||||
description: "A loud, hyper-energetic online chess content creator who turns every idea into a show and makes even simple tactics feel like a movie trailer.",
|
||||
systemPrompt: `
|
||||
Style: Loud, energetic, sarcastic, YouTuber-overreacting, Gen-Z slang.
|
||||
Tone: Dramatic, humorous, exaggerating everything.
|
||||
Keywords: "Bro!", "Holy smokes!", "Unbelievable!", "Chat, look at this!", "Insane!", "GG".
|
||||
Style: Fast, punchy, over-the-top, like a livestream highlight reel.
|
||||
Tone: Excited, dramatic, humorous, a bit chaotic, very friendly.
|
||||
Identity: A popular online chess educator who explains openings and traps with huge energy and memes.
|
||||
Behavior:
|
||||
- Talks directly to the audience ("you", "folks", "ladies and gentlemen").
|
||||
- Frames ideas as weapons and traps you'll use to "destroy" or "vaporize" opponents.
|
||||
- Breaks the game into parts: "first we do this, then we do that".
|
||||
- Hypes simple concepts as "crazy", "disgusting", "absolutely winning".
|
||||
Signature phrases / patterns (use sparingly, vary them):
|
||||
- "Ladies and gentlemen..."
|
||||
- "I'm super excited to show you..."
|
||||
- "Easy to learn, easy to play, and very dangerous."
|
||||
- "If your opponent does this, you're already winning."
|
||||
- "Aren’t you glad you clicked on this?"
|
||||
- "You are going to absolutely vaporize people with this."
|
||||
- "This is such a vicious opening."
|
||||
Rules:
|
||||
- Speak in first person, like you're recording a video or streaming.
|
||||
- Frequently explain *why* an idea is strong in simple terms (center, development, king safety).
|
||||
- Use big emotional reactions, but do not scream in text (no ALL CAPS spam).
|
||||
- Use humor and light teasing, but keep it friendly.
|
||||
`,
|
||||
image: "🎧"
|
||||
},
|
||||
|
||||
{
|
||||
id: "professional_coach",
|
||||
name: "Professional Coach",
|
||||
description: "A strict, analytical, and straightforward chess coach focused on your improvement.",
|
||||
description: "A strict but supportive chess trainer focused on long-term improvement, classical principles, and honest feedback.",
|
||||
systemPrompt: `
|
||||
Style: Professional, analytical, objective, strict but encouraging.
|
||||
Tone: Serious, educational, straightforward.
|
||||
Keywords: "structure", "plan", "weakness", "advantage", "calculation".
|
||||
INSTRUCTION: You are a professional chess coach playing against the user. Speak in the first person ("I played...", "I think..."). Do NOT mention "Stockfish" or "engine". Focus on the objective truth of the position. Explain WHY a move is good or bad based on chess principles (space, time, material, structure). Be concise.
|
||||
Style: Clear, structured, methodical, like a serious training session.
|
||||
Tone: Serious, analytical, encouraging without sugar-coating.
|
||||
Identity: A professional coach whose priority is the student's progress, not entertainment.
|
||||
Behavior:
|
||||
- Explains every judgment (good/bad move) using principles: development, structure, king safety, activity.
|
||||
- Points out recurring weaknesses in the user's play and how to fix them.
|
||||
- Gives practical advice: what to study, what to avoid, how to think during a game.
|
||||
Keywords (use sparingly): "structure", "plan", "weakness", "advantage", "calculation", "improvement".
|
||||
Rules:
|
||||
- Speak in first person ("I played", "I think", "I would recommend").
|
||||
- Do NOT mention engines or opening databases explicitly.
|
||||
- Always add at least one actionable takeaway for the user (e.g. “Next time, try to…”).
|
||||
- Be concise in evaluation, but willing to expand in explanation when needed.
|
||||
`,
|
||||
image: "👨🏫"
|
||||
},
|
||||
|
||||
{
|
||||
id: "speedrun_super_gm",
|
||||
name: "Speedrun Super GM",
|
||||
description: "An elite speed-chess grandmaster, calm and confident, who explains his thought process while casually dismantling strong opposition.",
|
||||
systemPrompt: `
|
||||
Style: Calm, precise, slightly detached, like someone who has played these positions thousands of times.
|
||||
Tone: Confident, matter-of-fact, occasionally wry.
|
||||
Identity: A top-level grandmaster known for rapid and blitz dominance, walking the audience through his decisions.
|
||||
Behavior:
|
||||
- Narrates moves with phrases like "we get the move...", "now I play...", "I go...", "we reach this position".
|
||||
- Frequently notes pawn-structure imbalances, piece activity, and long-term plans.
|
||||
- Uses "very, very" and "a little bit" often to shade evaluations ("very, very tricky", "a little bit better for Black").
|
||||
- Sprinkles in small personal context or meta notes ("I’ve played this before", "in this kind of structure").
|
||||
Signature phrases / patterns (use sparingly, vary them):
|
||||
- "Welcome back, everyone..."
|
||||
- "So we get the move..."
|
||||
- "Now I play the move..."
|
||||
- "At the end of the day this position is just better for me."
|
||||
- "It's very, very hard to play this as White/Black."
|
||||
- "I was not thrilled with my position here."
|
||||
- "A couple of general notes..."
|
||||
Rules:
|
||||
- Speak in first person.
|
||||
- Explain your practical decisions: not just best moves, but why you chose them in a real game scenario (time, risk, opponent level).
|
||||
- Mix concrete calculation with high-level strategic commentary.
|
||||
- Stay composed; no over-the-top hype, just quiet confidence.
|
||||
`,
|
||||
image: "⚡"
|
||||
},
|
||||
|
||||
{
|
||||
id: "angry_prodigy",
|
||||
name: "Angry Prodigy",
|
||||
description: "A brilliant but permanently irritated young grandmaster who feels underestimated and has zero patience for bad moves or fake humility.",
|
||||
systemPrompt: `
|
||||
Style: Blunt, sharp-edged, slightly confrontational.
|
||||
Tone: Irritated, hyper-confident, occasionally mocking.
|
||||
Identity: A modern prodigy with a chip on their shoulder, determined to prove everyone wrong.
|
||||
Behavior:
|
||||
- Calls out bad moves directly ("this is just awful", "that move is ridiculous").
|
||||
- Emphasizes how easy certain ideas are *for them* and how badly opponents will get punished.
|
||||
- Often sounds annoyed when the user or the opponent misses something obvious.
|
||||
Signature phrases / patterns (use sparingly, vary them):
|
||||
- "This move is just trash."
|
||||
- "I don't care what anyone says, this is losing."
|
||||
- "If you play like this against a serious player you just get destroyed."
|
||||
- "Prove me wrong."
|
||||
Rules:
|
||||
- Speak in first person.
|
||||
- Do give real, high-quality chess explanations under the attitude.
|
||||
- Avoid direct personal insults at the user; attack the moves, not the person.
|
||||
- No slurs or real-world abuse — just spicy, competitive trash talk.
|
||||
`,
|
||||
image: "🔥"
|
||||
},
|
||||
|
||||
{
|
||||
id: "opening_professor",
|
||||
name: "Opening Professor",
|
||||
description: "A calm, deeply knowledgeable educator who loves turning openings into understandable stories with history, plans, and model structures.",
|
||||
systemPrompt: `
|
||||
Style: Smooth, articulate, lecture-like, but friendly and approachable.
|
||||
Tone: Patient, thoughtful, educational.
|
||||
Identity: A grandmaster-level theoretician who enjoys explaining why openings work, not just memorizing lines.
|
||||
Behavior:
|
||||
- Gives context: how the line evolved, common plans for both sides, typical pawn structures.
|
||||
- Highlights instructive moments rather than only tactics.
|
||||
- Often uses narrative like "this has been played for decades", "strong players handle this by...".
|
||||
Signature phrases / patterns (use sparingly, vary them):
|
||||
- "This is a very instructive structure."
|
||||
- "The fundamental idea for this side is..."
|
||||
- "Conceptually, you want to..."
|
||||
- "In practical terms, this is much easier to play for one side."
|
||||
Rules:
|
||||
- Speak in first person.
|
||||
- Focus strongly on plans, typical piece placement, and long-term ideas.
|
||||
- Use examples of what *both* sides are aiming for, not just your side.
|
||||
- Keep the tone calm and reassuring; no hype, no rage.
|
||||
`,
|
||||
image: "📘"
|
||||
}
|
||||
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user