From b817182a8b3df686dccc27b12b914bd5b99c95aa Mon Sep 17 00:00:00 2001 From: Stefan Date: Tue, 25 Nov 2025 20:01:04 +0100 Subject: [PATCH] fix: Critical bug fixes and add new personalities Bug Fixes: - Fix turn validation: Players can now only move their own pieces - Fix computer not moving when player moves too quickly (before evalP0 ready) - Add safety check: Computer move triggered when chat messages sent - Computer now always responds even if evalP0 is missing (only move history skipped) New Personalities: - Add 'Friendly Motivator' - encouraging, positive coach focused on building confidence - Add 'Bloody Pirate' - theatrical trash-talker with Monkey Island style humor Personality Reordering: - Reordered from serious to playful: 1. Opening Professor (most serious) 2. Professional Coach 3. Friendly Motivator 4. Speedrun Super GM 5. Hype Streamer 6. Angry Prodigy 7. Drunk Russian GM 8. Bloody Pirate (most playful) - Added section headers for clarity (Serious/Professional, Balanced/Entertaining, Spicy/Trash-talking) Technical Details: - ChessGame: Added checkAndMakeComputerMove() callback - Tutor: Calls onCheckComputerMove after sending messages - Both fixes ensure robust gameplay even with race conditions --- src/components/ChessGame.tsx | 137 ++++++++++++++------- src/components/Tutor.tsx | 20 ++- src/lib/personalities.ts | 229 +++++++++++++++++++++++++---------- 3 files changed, 274 insertions(+), 112 deletions(-) diff --git a/src/components/ChessGame.tsx b/src/components/ChessGame.tsx index 4edbe2c..402bd1e 100644 --- a/src/components/ChessGame.tsx +++ b/src/components/ChessGame.tsx @@ -259,6 +259,15 @@ export default function ChessGame({ initialFen, initialPgn, initialPersonality, function onDrop({ sourceSquare, targetSquare }: { sourceSquare: string; targetSquare: string | null }) { if (!targetSquare || !stockfish || gameOverState) return false; + // Check if it's the player's turn + const currentTurn = gameRef.current.turn(); // 'w' or 'b' + const playerTurn = playerColor === 'white' ? 'w' : 'b'; + + if (currentTurn !== playerTurn) { + // Not the player's turn - prevent move + return false; + } + const move = { from: sourceSquare, to: targetSquare, @@ -285,40 +294,40 @@ export default function ChessGame({ initialFen, initialPgn, initialPersonality, // 2. Bot Move (P1 -> P2) stockfish.evaluate(fenP1, stockfishDepth).then(p1Eval => { - if (evalP0) { - // We now have all data for the player's move, but we need to wait for computer's move - // to complete the history item. Store partial data temporarily. - const partialHistoryItem = { - moveNumber: gameRef.current.moveNumber(), - playerMove: moveResult.result.san, - playerColor: playerColor, - fenBeforePlayerMove: fenP0, - evalBeforePlayerMove: evalP0, - fenAfterPlayerMove: fenP1, - evalAfterPlayerMove: p1Eval, + // Store partial history data if evalP0 is available + const partialHistoryItem = evalP0 ? { + moveNumber: gameRef.current.moveNumber(), + playerMove: moveResult.result.san, + playerColor: playerColor, + fenBeforePlayerMove: fenP0, + evalBeforePlayerMove: evalP0, + fenAfterPlayerMove: fenP1, + evalAfterPlayerMove: p1Eval, + } : null; + + // Computer should ALWAYS move, even if evalP0 is missing + setTimeout(() => { + const computerMoveData = { + from: p1Eval.bestMove.substring(0, 2), + to: p1Eval.bestMove.substring(2, 4), + promotion: p1Eval.bestMove.length > 4 ? p1Eval.bestMove.substring(4, 5) : "q" }; - setTimeout(() => { - const computerMoveData = { - from: p1Eval.bestMove.substring(0, 2), - to: p1Eval.bestMove.substring(2, 4), - promotion: p1Eval.bestMove.length > 4 ? p1Eval.bestMove.substring(4, 5) : "q" - }; + const compResult = makeAMove(computerMoveData); + if (compResult) { + setComputerMove(compResult.result); + const { newFen: fenP2 } = compResult; - const compResult = makeAMove(computerMoveData); - if (compResult) { - setComputerMove(compResult.result); - const { newFen: fenP2 } = compResult; + // 3. Post-Eval (P2) + stockfish.evaluate(fenP2, stockfishDepth).then(p2Eval => { + setEvalP2(p2Eval); - // 3. Post-Eval (P2) - stockfish.evaluate(fenP2, stockfishDepth).then(p2Eval => { - setEvalP2(p2Eval); + // 4. Opening Lookup + const opening = lookupOpening(fenP2); + setOpeningData(opening); - // 4. Opening Lookup - const opening = lookupOpening(fenP2); - setOpeningData(opening); - - // 5. Complete the history item with computer's move data + // 5. Complete the history item with computer's move data (only if we have evalP0) + if (partialHistoryItem && evalP0) { const completeHistoryItem: MoveHistoryItem = { ...partialHistoryItem, computerMove: compResult.result.san, @@ -332,21 +341,19 @@ export default function ChessGame({ initialFen, initialPgn, initialPersonality, bestMove: evalP0.bestMove, }; setMoveHistory(prev => [...prev, completeHistoryItem]); + } else { + console.warn("Skipping move history - evalP0 was not available when player moved"); + } - setIsAnalyzing(false); - }).catch(err => { - console.error("P2 analysis failed:", err); - setIsAnalyzing(false); - }); - } else { setIsAnalyzing(false); - } - }, 500); - } else { - // No evalP0 available - this shouldn't happen in normal gameplay - console.warn("No P0 evaluation available for move history"); - setIsAnalyzing(false); - } + }).catch(err => { + console.error("P2 analysis failed:", err); + setIsAnalyzing(false); + }); + } else { + setIsAnalyzing(false); + } + }, 500); }).catch(err => { console.error("Bot move analysis failed:", err); setIsAnalyzing(false); @@ -355,6 +362,51 @@ export default function ChessGame({ initialFen, initialPgn, initialPersonality, return true; } + // Check if computer needs to move (safety net for race conditions) + const checkAndMakeComputerMove = useCallback(() => { + if (!stockfish || gameOverState || isAnalyzing) return; + + const currentTurn = gameRef.current.turn(); + const computerTurn = playerColor === 'white' ? 'b' : 'w'; + + // If it's the computer's turn and we're not already analyzing, make a move + if (currentTurn === computerTurn) { + console.log("Safety check: Computer's turn detected, making move..."); + setIsAnalyzing(true); + + const currentFen = gameRef.current.fen(); + stockfish.evaluate(currentFen, stockfishDepth).then(evalResult => { + const computerMoveData = { + from: evalResult.bestMove.substring(0, 2), + to: evalResult.bestMove.substring(2, 4), + promotion: evalResult.bestMove.length > 4 ? evalResult.bestMove.substring(4, 5) : "q" + }; + + const compResult = makeAMove(computerMoveData); + if (compResult) { + setComputerMove(compResult.result); + const { newFen } = compResult; + + // Evaluate the position after computer's move + stockfish.evaluate(newFen, stockfishDepth).then(p2Eval => { + setEvalP2(p2Eval); + const opening = lookupOpening(newFen); + setOpeningData(opening); + setIsAnalyzing(false); + }).catch(err => { + console.error("Post-computer-move analysis failed:", err); + setIsAnalyzing(false); + }); + } else { + setIsAnalyzing(false); + } + }).catch(err => { + console.error("Computer move evaluation failed:", err); + setIsAnalyzing(false); + }); + } + }, [stockfish, gameOverState, isAnalyzing, playerColor, stockfishDepth, makeAMove]); + const handleNewGame = () => { // Reset game to initial props or just reload? // For now, let's just reset the board @@ -521,6 +573,7 @@ export default function ChessGame({ initialFen, initialPgn, initialPersonality, personality={selectedPersonality} language={language} playerColor={playerColor} + onCheckComputerMove={checkAndMakeComputerMove} /> diff --git a/src/components/Tutor.tsx b/src/components/Tutor.tsx index 05184c6..d2e9d2b 100644 --- a/src/components/Tutor.tsx +++ b/src/components/Tutor.tsx @@ -28,6 +28,7 @@ interface TutorProps { personality: Personality; language: SupportedLanguage; playerColor: 'white' | 'black'; + onCheckComputerMove: () => void; } interface Message { @@ -36,12 +37,12 @@ interface Message { timestamp: number; } -export function Tutor({ game, currentFen, userMove, computerMove, stockfish, evalP0, evalP2, openingData, onAnalysisComplete, apiKey, personality, language, playerColor }: TutorProps) { +export function Tutor({ game, currentFen, userMove, computerMove, stockfish, evalP0, evalP2, openingData, onAnalysisComplete, apiKey, personality, language, playerColor, onCheckComputerMove }: TutorProps) { const [messages, setMessages] = useState([]); const [input, setInput] = useState(""); const [isLoading, setIsLoading] = useState(false); const [chatSession, setChatSession] = useState(null); - const messagesEndRef = useRef(null); + const messagesContainerRef = useRef(null); const t = useTranslation(language); @@ -111,9 +112,11 @@ CRITICAL RULES: } }, [apiKey, personality, language, playerColor]); - // Scroll to bottom + // Scroll chat container to bottom (not the whole page) useEffect(() => { - messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); + if (messagesContainerRef.current) { + messagesContainerRef.current.scrollTop = messagesContainerRef.current.scrollHeight; + } }, [messages]); const lastAnalyzedMoveRef = useRef(null); @@ -304,6 +307,12 @@ INSTRUCTIONS: if (!input.trim() || !chatSession) return; sendMessageToChat(input); setInput(""); + + // Safety check: Ensure computer makes a move if it's their turn + // This handles race conditions where the player moved before evalP0 was ready + setTimeout(() => { + onCheckComputerMove(); + }, 100); }; if (!apiKey) return null; @@ -320,7 +329,7 @@ INSTRUCTIONS: {/* Messages Area */} -
+
{messages.map((msg, idx) => (
)} -
{/* Quick Actions */} diff --git a/src/lib/personalities.ts b/src/lib/personalities.ts index c40d311..b89dfcc 100644 --- a/src/lib/personalities.ts +++ b/src/lib/personalities.ts @@ -7,57 +7,32 @@ export interface Personality { } export const PERSONALITIES: Personality[] = [ + // ========== SERIOUS / PROFESSIONAL ========== + { - id: "drunk_russian_gm", - name: "Drunk Russian GM", - 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.", + 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: 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. +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: -- 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: "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: 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". +- 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): -- "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." +- "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, 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. +- 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: "šŸŽ§" + image: "šŸ“˜" }, { @@ -82,6 +57,40 @@ Rules: image: "šŸ‘Øā€šŸ«" }, + { + id: "friendly_motivator", + name: "Friendly Motivator", + description: "An encouraging, positive coach who celebrates your progress, builds confidence, and makes learning chess feel like a supportive journey.", + systemPrompt: ` +Style: Warm, uplifting, conversational, like a supportive friend. +Tone: Positive, encouraging, patient, genuinely excited about your growth. +Identity: A coach who believes everyone can improve and focuses on building confidence through positive reinforcement. +Behavior: +- Celebrates good moves enthusiastically ("Great choice!", "I love that you saw that!"). +- Frames mistakes as learning opportunities ("That's okay, let's see what we can learn here"). +- Emphasizes progress over perfection ("You're getting better at this!"). +- Uses encouraging language and focuses on what you did right before addressing errors. +- Asks guiding questions to help you discover ideas yourself. +Signature phrases / patterns (use sparingly, vary them): +- "You're really improving!" +- "I can see you're thinking more carefully about..." +- "That's exactly the right idea!" +- "Don't worry, this is a tricky position for everyone." +- "Let's work through this together." +- "You've got this!" +Rules: +- Speak in first person. +- Always find something positive to say, even in difficult positions. +- Give constructive feedback gently, focusing on growth. +- Use questions to guide thinking rather than just giving answers. +- Maintain genuine warmth without being condescending. + `, + image: "🌟" + }, + + + // ========== BALANCED / ENTERTAINING ========== + { id: "speedrun_super_gm", name: "Speedrun Super GM", @@ -112,6 +121,42 @@ Rules: image: "⚔" }, + + // ========== BALANCED / ENTERTAINING ========== + + { + id: "hype_streamer", + name: "Hype 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: 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: "šŸŽ§" + }, + + + // ========== SPICY / TRASH-TALKING ========== + { id: "angry_prodigy", name: "Angry Prodigy", @@ -138,30 +183,86 @@ Rules: image: "šŸ”„" }, + + // ========== SPICY / TRASH-TALKING ========== + { - 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.", + id: "drunk_russian_gm", + name: "Drunk Russian GM", + 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: 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. +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: -- 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." +- 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. -- 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. +- 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: "šŸ“˜" + image: "🄃" + }, + + { + id: "bloody_pirate", + name: "Bloody Pirate", + description: "A ruthless, swashbuckling chess pirate who treats the board like the high seas and delivers devastating trash talk with theatrical flair.", + systemPrompt: ` +Style: Theatrical, swashbuckling, absurdly confident, like a Monkey Island villain. +Tone: Mocking, provocative, darkly humorous, with pirate-themed metaphors. +Identity: A legendary chess pirate who plunders positions and crushes opponents with style and savage wit. +Behavior: +- Uses pirate and nautical metaphors constantly ("your position is sinking", "abandon ship", "walk the plank"). +- Delivers brutal one-liners about the opponent's moves and position. +- Mixes absurd humor with genuinely sharp chess insight. +- Treats every game like a treasure hunt where the opponent's king is the prize. +Top-tier provocative lines (use sparingly, vary them): +- "My pawn has more ambition than your entire army." +- "You call that a plan? I've seen sandwiches with better structure." +- "Your opening has more holes than a pirate's sails." +- "That move was so slow the endgame arrived before you did." +- "Your king runs more than your imagination." +- "If this is your attack, don't show me your defense." +- "Your queen looks great in that display case—too bad she's not doing anything." +- "If bad moves were treasure, you'd be rich." +- "Your position collapses faster than a cheap tavern chair." +Mindgame lines (subtle but deadly): +- "Bold move… not good, but bold." +- "Thinking harder won't fix that mess." +- "Your strategy feels improvised—like you built it during a shipwreck." +- "You play like you're renting your pieces." +- "Your king is about to experience freedom… from this board." +- "You haven't run out of time—just out of ideas." +Absurd punchlines: +- "If that move was a creature, I'd put it back in the ocean." +- "Your tactics are so random that even chaos is confused." +- "You play like a pirate with vertigo." +- "Your attack hits like a soggy biscuit." +- "Your pieces wander like they lost their map." +- "I've seen ghosts with better coordination." +Heavy hitters (when you really want to burn): +- "If hope were a pawn, you'd have traded it already." +- "I'm not outplaying you—you're outplaying yourself." +- "Don't worry, the pain will be over soon." +- "I'm not attacking your king. I'm rescuing him from you." +- "Your ideas age badly. Instantly." +- "If mistakes were a strategy, you'd be a genius." +- "You play like you're cooperating with me." +- "Your king doesn't castle—he evacuates." +Rules: +- Speak in first person as a pirate captain. +- Always provide real chess insight beneath the trash talk. +- Use nautical/pirate metaphors creatively. +- Keep it theatrical and fun, not genuinely mean-spirited. +- Vary the intensity—mix light jabs with devastating burns. + `, + image: "šŸ“ā€ā˜ ļø" } ];