game ready

This commit is contained in:
Stefan
2025-11-23 11:44:05 +01:00
parent 30f1e9e3a5
commit 8a6d6bd780
8 changed files with 5259 additions and 30 deletions
+106 -1
View File
@@ -1,4 +1,109 @@
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
# AI Chess Tutor
## The Story
I always wanted to implement an AI-based chess tutor because I like playing chess, although to be honest, I actually suck at it. I didn't find the existing tutors or big apps useful enough for my needs, so I decided to build my own approach.
This application was built using **Antigravity by Google**. I like to work with it, though sometimes it just runs away. Still, I found the end result to be quite fun to play, which is why I'm sharing it here.
## How It Works
**AI Chess Tutor** is an interactive chess learning application where you play against an AI opponent powered by Stockfish while receiving real-time coaching feedback from a Large Language Model (LLM).
### Typical Game Flow
1. **Choose Your Personality**: Select from three unique AI coaching personalities, each with their own teaching style and character
2. **Play Chess**: Make your moves on the board while the Stockfish engine plays against you
3. **Get Real-Time Feedback**: After each move exchange, your AI tutor analyzes the position and provides personalized feedback based on:
- Move quality and alternatives
- Position evaluation changes
- Opening theory (when applicable)
- Tactical and strategic considerations
4. **Chat with Your Tutor**: Ask questions anytime using the integrated chat feature - your tutor will answer in character
5. **Post-Game Analysis**: When the game ends, review a comprehensive analysis showing your mistakes and learning opportunities
![Gameplay Screenshot](screenshots/gameplay.png)
*Screenshot placeholder: Main game interface with board, evaluation bar, and chat*
## Features
### Core Features
- **Stockfish Engine**: Powerful chess engine for move analysis and opponent play
- **Opening Database**: Comprehensive database of chess openings with metadata and theory
- **Real-Time Evaluation**: Live position evaluation with visual evaluation bar
- **Move Analysis**: Detailed feedback on every move you make
- **Interactive Chat**: Ask your AI tutor questions and get personalized answers
- **Post-Game Analysis**: Review all your mistakes and missed opportunities after each game
- **Multi-Language Support**: Available in English, German, French, and Italian
### AI Personalities
Choose from three distinct coaching personalities, each offering a unique learning experience:
#### 🥃 Drunk Russian GM
*"Ach... life is pain, my boy"*
A bitter, fatalistic, but brilliant Grandmaster who has seen it all. Expect brutally honest feedback delivered with dark humor and existential commentary. This personality combines deep chess knowledge with a Dostoevsky-like atmosphere.
![Drunk Russian GM Screenshot](screenshots/personality-russian-gm.png)
*Screenshot placeholder: Game with Drunk Russian GM personality*
#### 🎧 Hype Streamer
*"BRO! THAT MOVE WAS INSANE!"*
An energetic, loud, and entertaining chess streamer who makes every game exciting. Expect dramatic reactions, Gen-Z slang, and over-the-top commentary that keeps you engaged and motivated.
![Hype Streamer Screenshot](screenshots/personality-hype-streamer.png)
*Screenshot placeholder: Game with Hype Streamer personality*
#### 👨‍🏫 Professional Coach
*"Let's analyze the structure of this position"*
A strict, analytical, and straightforward chess coach focused on your improvement. Expect objective analysis, clear explanations based on chess principles, and professional teaching methods.
![Professional Coach Screenshot](screenshots/personality-professional-coach.png)
*Screenshot placeholder: Game with Professional Coach personality*
## Setup
### Prerequisites
- Node.js 18+ installed
- A free Google Gemini API key
### Getting Your Gemini API Key
1. Visit [Google AI Studio](https://aistudio.google.com/app/apikey)
2. Sign in with your Google account
3. Click "Create API Key"
4. Copy your API key
### API Key Configuration
You have two options for providing your Gemini API key:
#### Option 1: Browser Storage (Recommended for trying it out)
1. Run the application
2. When prompted, enter your API key in the modal dialog
3. The key will be stored in your browser's localStorage
![API Key Input Screenshot](screenshots/api-key-input.png)
*Screenshot placeholder: API key input modal*
#### Option 2: Environment Variable (Recommended for local development)
1. Create a `.env` file in the project root
2. Add your API key:
```
NEXT_PUBLIC_GEMINI_API_KEY=your_api_key_here
```
3. The application will automatically use this key
> [!NOTE]
> You can update your API key anytime by clicking the key icon in the bottom-right corner of the application.
## Credits
- Opening collection originally by [ragizaki/ChessOpeningsRecommender](https://github.com/ragizaki/ChessOpeningsRecommender)
- Chess engine: [Stockfish](https://stockfishchess.org/)
- LLM: [Google Gemini](https://ai.google.dev/)
## Getting Started
+4721 -1
View File
File diff suppressed because it is too large Load Diff
+20 -4
View File
@@ -3,24 +3,40 @@
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"dev": "next dev -p 3050",
"build": "next build",
"start": "next start",
"lint": "eslint"
"start": "next start -p 3050",
"lint": "eslint",
"test": "jest"
},
"dependencies": {
"@google/generative-ai": "^0.24.1",
"chess.js": "^1.4.0",
"clsx": "^2.1.1",
"lucide-react": "^0.554.0",
"next": "16.0.3",
"react": "19.2.0",
"react-dom": "19.2.0"
"react-chessboard": "^5.8.4",
"react-dom": "19.2.0",
"stockfish.js": "^10.0.2",
"tailwind-merge": "^3.4.0"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.0",
"@types/jest": "^30.0.0",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "16.0.3",
"jest": "^30.2.0",
"jest-environment-jsdom": "^30.2.0",
"tailwindcss": "^4",
"ts-jest": "^29.4.5",
"ts-node": "^10.9.2",
"typescript": "^5"
}
}
+18
View File
@@ -0,0 +1,18 @@
# Screenshot Placeholders
This directory contains screenshots for the README documentation.
## Required Screenshots
1. **gameplay.png** - Main game interface showing the chessboard, evaluation bar, and chat panel
2. **personality-russian-gm.png** - Game interface with Drunk Russian GM personality selected
3. **personality-hype-streamer.png** - Game interface with Hype Streamer personality selected
4. **personality-professional-coach.png** - Game interface with Professional Coach personality selected
5. **api-key-input.png** - Modal dialog for entering the Gemini API key
## Screenshot Guidelines
- Use a consistent window size for all screenshots
- Capture actual gameplay to show real examples
- Include both light and dark mode versions if applicable
- Ensure text is readable at typical display sizes
+13 -6
View File
@@ -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", () => {
+80 -3
View File
@@ -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>
);
}
+164
View File
@@ -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
View File
@@ -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."
- "Arent 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 ("Ive 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: "📘"
}
];