/// import * as React from 'react'; const { useState, useEffect } = React; import { getGameState, makeMove, getHint } from '../services/api'; import { playSound, preloadSounds } from '../services/soundService'; import ChessClock from './ChessClock'; interface ChessBoardProps { theme: 'brown' | 'grey'; } interface Piece { type: string; color: 'white' | 'black'; position: string; } interface HighlightedSquare { square: string; type: 'selected' | 'hint' | 'legal-move'; } const ChessBoard: React.FC = ({ theme }) => { const [fen, setFen] = useState('rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1'); const [pieces, setPieces] = useState([]); const [selectedPiece, setSelectedPiece] = useState(null); const [legalMoves, setLegalMoves] = useState([]); const [highlightedSquares, setHighlightedSquares] = useState([]); const [hintMove, setHintMove] = useState<{ from: string; to: string } | null>(null); const [isPlayerTurn, setIsPlayerTurn] = useState(true); const [playerColor, setPlayerColor] = useState<'white' | 'black'>('white'); const [lastMove, setLastMove] = useState<{ from: string; to: string } | null>(null); const [isLoading, setIsLoading] = useState(false); const [moveInProgress, setMoveInProgress] = useState(false); const [statusMessage, setStatusMessage] = useState(''); const [gameStarted, setGameStarted] = useState(false); const [lastPlayerMove, setLastPlayerMove] = useState<{ from: string; to: string } | null>(null); const [lastAiMove, setLastAiMove] = useState<{ from: string; to: string } | null>(null); // Initialize the board and preload sounds useEffect(() => { fetchGameState(); preloadSounds(); // Preload sound effects }, []); // Parse FEN and update pieces useEffect(() => { if (fen) { const parsedPieces = parseFen(fen); setPieces(parsedPieces); } }, [fen]); const fetchGameState = async () => { try { console.log('Fetching game state...'); const response = await getGameState(); console.log('Game state response:', response); setFen(response.board_state.fen); setLegalMoves(response.board_state.legal_moves); setIsPlayerTurn(response.board_state.turn === (response.player_color || 'white')); if (response.player_color) { setPlayerColor(response.player_color); } // Check if game has started (move count > 0 or legal moves available) setGameStarted(response.board_state.move_count > 0 || response.board_state.legal_moves.length > 0); } catch (error) { console.error('Error fetching game state:', error); console.error('Error details:', error.response?.data || error.message); setStatusMessage(`Connection error: ${error.response?.status || 'Server not responding'}`); } }; const parseFen = (fenString: string): Piece[] => { const pieces: Piece[] = []; const fenParts = fenString.split(' '); const ranks = fenParts[0].split('/'); ranks.forEach((rank, rankIndex) => { let fileIndex = 0; for (let i = 0; i < rank.length; i++) { const char = rank[i]; if (!isNaN(parseInt(char))) { fileIndex += parseInt(char); } else { const color = char === char.toUpperCase() ? 'white' : 'black'; const type = char.toLowerCase(); const position = String.fromCharCode(97 + fileIndex) + (8 - rankIndex); pieces.push({ type, color, position, }); fileIndex++; } } }); return pieces; }; const handleSquareClick = async (square: string) => { // Check if game has started if (!gameStarted) { setStatusMessage('Please start a new game first'); return; } // If a hint is active and the clicked square is the hint destination if (hintMove && square === hintMove.to) { await handleMove(hintMove.from, hintMove.to); setHintMove(null); return; } // Find if there's a piece on the clicked square const pieceOnSquare = pieces.find((p: Piece) => p.position === square); // If a piece is already selected if (selectedPiece) { // Check if the clicked square is a legal move const move = `${selectedPiece}${square}`; const isLegalMove = legalMoves.some((m: string) => m === move); if (isLegalMove) { await handleMove(selectedPiece, square); } // Clear selection setSelectedPiece(null); setHighlightedSquares([]); } // If clicking on a piece of the player's color else if (pieceOnSquare && pieceOnSquare.color === (isPlayerTurn ? playerColor : (playerColor === 'white' ? 'black' : 'white'))) { setSelectedPiece(square); // Highlight legal moves for this piece const legalMovesForPiece = legalMoves .filter((move: string) => move.startsWith(square)) .map((move: string) => move.substring(2)); const highlights: HighlightedSquare[] = [ { square, type: 'selected' }, ...legalMovesForPiece.map((move: string) => ({ square: move, type: 'legal-move' })), ]; setHighlightedSquares(highlights); } }; const handleMove = async (from: string, to: string) => { try { console.log(`Attempting to make move: ${from} to ${to}`); // IMMEDIATELY switch to AI's turn when player commits to a move // This stops the player's clock and starts the AI's clock setIsPlayerTurn(false); console.log('Player move committed - switched to AI turn'); // Set loading state and update status message setMoveInProgress(true); setStatusMessage('Processing move...'); // IMMEDIATELY update the UI for the player's move (optimistic update) // Find the piece being moved const movingPiece = pieces.find(p => p.position === from); const targetPiece = pieces.find(p => p.position === to); if (movingPiece) { // Create a new pieces array with the updated position const updatedPieces = pieces.map(p => { if (p.position === from) { return { ...p, position: to }; } // Remove the captured piece if there is one if (p.position === to && p !== movingPiece) { return null; } return p; }).filter(Boolean) as Piece[]; // Update the pieces state immediately for the player's move setPieces(updatedPieces); // Determine the type of move for sound effects const isCapture = targetPiece !== undefined; const isCastle = movingPiece.type === 'k' && ((from === 'e1' && (to === 'g1' || to === 'c1')) || (from === 'e8' && (to === 'g8' || to === 'c8'))); // Play appropriate sound if (isCastle) { playSound('castle'); } else if (isCapture) { playSound('capture'); } else { playSound('move'); } } // First highlight the squares for visual feedback setHighlightedSquares([ { square: from, type: 'selected' }, { square: to, type: 'selected' } ]); // Make the API call to process the move const moveNotation = `${from}${to}`; console.log(`Making API call with move: ${moveNotation}`); const response = await makeMove(moveNotation); console.log('API response:', response); if (response.status === 'success' || response.status === 'game_over') { // Check if the move puts the opponent in check const isCheck = response.board_state?.in_check; if (isCheck) { setTimeout(() => playSound('check'), 150); setStatusMessage('Check!'); } else { setStatusMessage(''); } // Check if game is over if (response.status === 'game_over') { setTimeout(() => playSound('game-end'), 300); setStatusMessage(response.result === 'draw' ? 'Game ended in a draw' : `${response.winner} wins!`); } // Store player's move first setLastPlayerMove({ from, to }); setLastMove({ from, to }); // Update the board state with the server response (but not the FEN yet if AI will move) setLegalMoves(response.board_state.legal_moves); // If AI made a move, handle the sequence properly if (response.ai_move) { const aiFrom = response.ai_move.substring(0, 2); const aiTo = response.ai_move.substring(2, 4); // AI's clock is already running since we switched turns at the start // Show AI move after a delay setTimeout(() => { // Store AI's last move setLastAiMove({ from: aiFrom, to: aiTo }); // Update the board with AI's move setFen(response.board_state.fen); // Play sound for AI move const aiMovingPiece = pieces.find(p => p.position === aiFrom); const aiTargetPiece = pieces.find(p => p.position === aiTo); if (aiMovingPiece) { const isAiCapture = aiTargetPiece !== undefined; const isAiCastle = aiMovingPiece.type === 'k' && ((aiFrom === 'e1' && (aiTo === 'g1' || aiTo === 'c1')) || (aiFrom === 'e8' && (aiTo === 'g8' || aiTo === 'c8'))); if (isAiCastle) { playSound('castle'); } else if (isAiCapture) { playSound('capture'); } else { playSound('move'); } } // After AI move is shown, switch back to player's turn setTimeout(() => { const finalPlayerTurn = response.board_state.turn === playerColor; setIsPlayerTurn(finalPlayerTurn); console.log(`Final turn update: ${finalPlayerTurn ? 'Player' : 'AI'} turn`); }, 200); }, 800); // Delay AI move display by 800ms } else { // No AI move, just update the board and turn setFen(response.board_state.fen); const newPlayerTurn = response.board_state.turn === playerColor; setIsPlayerTurn(newPlayerTurn); console.log(`Updated turn: board turn=${response.board_state.turn}, playerColor=${playerColor}, isPlayerTurn=${newPlayerTurn}`); } // Clear highlights after a short delay setTimeout(() => { setHighlightedSquares([]); }, 500); } else { // Clear highlights immediately if move failed setHighlightedSquares([]); setStatusMessage('Invalid move'); // Reset pieces to match the FEN since our optimistic update was wrong setPieces(parseFen(fen)); } } catch (error) { console.error('Error making move:', error); console.error('Error details:', error.response?.data || error.message); setHighlightedSquares([]); setStatusMessage(`Error making move: ${error.response?.data?.message || error.message}`); // Reset pieces to match the FEN since our optimistic update was wrong setPieces(parseFen(fen)); // Reset player turn on error setIsPlayerTurn(true); } finally { setMoveInProgress(false); } }; const handleRequestHint = async () => { try { const response = await getHint(); if (response.status === 'success') { const hint = response.hint; const from = hint.substring(0, 2); const to = hint.substring(2, 4); setHintMove({ from, to }); setHighlightedSquares([ { square: from, type: 'hint' }, { square: to, type: 'hint' }, ]); } } catch (error) { console.error('Error getting hint:', error); } }; const renderSquare = (square: string, isLight: boolean) => { const piece = pieces.find(p => p.position === square); const isHighlighted = highlightedSquares.find(h => h.square === square); const isLastMoveFrom = lastMove && lastMove.from === square; const isLastMoveTo = lastMove && lastMove.to === square; // Check for player and AI move highlights const isPlayerMoveFrom = lastPlayerMove && lastPlayerMove.from === square; const isPlayerMoveTo = lastPlayerMove && lastPlayerMove.to === square; const isAiMoveFrom = lastAiMove && lastAiMove.from === square; const isAiMoveTo = lastAiMove && lastAiMove.to === square; const squareColor = isLight ? 'light' : 'dark'; const boardColor = 'brown'; const colorInFilename = 'brown'; const imageUrl = `/assets/boards/${boardColor}/square ${colorInFilename} ${squareColor}_1x.png`; return ( handleSquareClick(square)} > {isHighlighted && isHighlighted.type === 'selected' && ( )} {isHighlighted && isHighlighted.type === 'hint' && ( )} {isHighlighted && isHighlighted.type === 'legal-move' && ( )} {/* Player move highlighting - light blue border */} {(isPlayerMoveFrom || isPlayerMoveTo) && ( )} {/* AI move highlighting - orange border */} {(isAiMoveFrom || isAiMoveTo) && ( )} {piece && ( )} ); }; const getPieceName = (type: string): string => { switch (type) { case 'p': return 'pawn'; case 'r': return 'rook'; case 'n': return 'knight'; case 'b': return 'bishop'; case 'q': return 'queen'; case 'k': return 'king'; default: return 'pawn'; } }; const renderBoard = () => { const squares = []; const files = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']; const ranks = ['8', '7', '6', '5', '4', '3', '2', '1']; // Flip the board if player is black const displayRanks = playerColor === 'white' ? ranks : [...ranks].reverse(); const displayFiles = playerColor === 'white' ? files : [...files].reverse(); for (let rankIndex = 0; rankIndex < 8; rankIndex++) { for (let fileIndex = 0; fileIndex < 8; fileIndex++) { const file = displayFiles[fileIndex]; const rank = displayRanks[rankIndex]; const square = file + rank; const isLight = (fileIndex + rankIndex) % 2 === 1; squares.push(renderSquare(square, isLight)); } } return squares; }; return ( {/* Chess Clock */} {/* Status message and turn indicator */} {!gameStarted ? "Click 'New Game' to start playing" : (isPlayerTurn ? "Your turn" : "AI is thinking...")} {statusMessage} {moveInProgress && ( Processing move... )} {/* Chess board with external rank and file labels */} {/* Rank labels (1-8) */} {playerColor === 'white' ? ['8', '7', '6', '5', '4', '3', '2', '1'].map(rank => ( {rank} )) : ['1', '2', '3', '4', '5', '6', '7', '8'].map(rank => ( {rank} )) } {/* Chess board with dark theme border */} {renderBoard()} {/* Loading overlay */} {isLoading && ( Loading... )} {/* File labels (a-h) */} {playerColor === 'white' ? ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'].map(file => ( {file} )) : ['h', 'g', 'f', 'e', 'd', 'c', 'b', 'a'].map(file => ( {file} )) } {/* Hint button with full width */} Get Hint ); }; export default ChessBoard;
Loading...