musical-chess / src /hooks /useChessGame.ts
Maximus Powers
final
3568151
import { useState, useCallback, useRef, useEffect } from 'react'
import { Chess, Square, Move, Color } from 'chess.js'
import { GameState, GameHistoryEntry, DraggedPiece } from '../types/chess'
import { evaluateGameState, isPotentialPromotion } from '../utils/chessUtils'
import { ChessAI } from '../engines/ChessAI'
export function useChessGame() {
const [gameState, setGameState] = useState<GameState>({
board: new Chess(),
gameActive: false,
playerColor: 'w',
selectedSquare: null,
legalMoves: [],
gameHistory: [],
gameOver: false,
gameResult: null,
promotionMove: null,
promotionDialogActive: false,
aiThinking: false,
aiModelLoaded: false,
aiModelLoading: false
})
const [selectedModel, setSelectedModel] = useState<string>('mlabonne/chesspythia-70m')
const [draggedPiece, setDraggedPiece] = useState<DraggedPiece | null>(null)
const aiTimeoutRef = useRef<NodeJS.Timeout>()
const chessAI = useRef<ChessAI | null>(null)
// init chess model
useEffect(() => {
const initializeAI = async () => {
setGameState(prev => ({ ...prev, aiModelLoading: true }))
chessAI.current = new ChessAI(selectedModel)
try {
await chessAI.current.initialize()
setGameState(prev => ({
...prev,
aiModelLoaded: true,
aiModelLoading: false
}))
console.log(`Chess AI model ${selectedModel} loaded successfully`)
} catch (error) {
console.error(`Failed to load Chess AI model ${selectedModel}:`, error)
setGameState(prev => ({
...prev,
aiModelLoaded: false,
aiModelLoading: false
}))
}
}
initializeAI()
return () => {
if (aiTimeoutRef.current) {
clearTimeout(aiTimeoutRef.current)
}
}
}, [selectedModel])
const startNewGame = useCallback(() => {
clearTimeout(aiTimeoutRef.current)
setGameState(prev => ({
...prev,
board: new Chess(),
gameActive: true,
gameOver: false,
gameHistory: [],
selectedSquare: null,
legalMoves: [],
gameResult: null,
promotionDialogActive: false,
promotionMove: null,
aiThinking: false
}))
if (gameState.playerColor === 'b') {
setGameState(prev => ({ ...prev, aiThinking: true }))
aiTimeoutRef.current = setTimeout(() => {
makeAIMove()
}, 500)
}
}, [gameState.playerColor])
const resignGame = useCallback(() => {
clearTimeout(aiTimeoutRef.current)
const winner = gameState.playerColor === 'w' ? 'b' : 'w'
const gameResult = {
isGameOver: true,
winner: winner as Color,
message: `Game Over! ${gameState.playerColor === 'w' ? 'White' : 'Black'} resigned - ${winner === 'w' ? 'White' : 'Black'} wins!`,
terminationReason: 'resignation',
details: `${gameState.playerColor === 'w' ? 'White' : 'Black'} player resigned the game`
}
setGameState(prev => ({
...prev,
gameActive: false,
gameOver: true,
gameResult,
aiThinking: false
}))
}, [gameState.playerColor])
const togglePlayerColor = useCallback(() => {
if (!gameState.gameActive) {
setGameState(prev => ({
...prev,
playerColor: prev.playerColor === 'w' ? 'b' : 'w'
}))
}
}, [gameState.gameActive])
const selectSquare = useCallback((square: Square) => {
if (!gameState.gameActive || gameState.board.turn() !== gameState.playerColor || gameState.aiThinking) {
return
}
const piece = gameState.board.get(square)
if (piece && piece.color === gameState.playerColor) {
const moves = gameState.board.moves({ square, verbose: true })
setGameState(prev => ({
...prev,
selectedSquare: square,
legalMoves: moves
}))
}
}, [gameState])
const attemptMove = useCallback((from: Square, to: Square) => {
console.log('attemptMove called:', from, 'to', to)
if (!gameState.gameActive || gameState.board.turn() !== gameState.playerColor) {
console.log('Move rejected: game not active or not player turn')
return false
}
if (isPotentialPromotion(gameState.board, from, to)) {
const possibleMoves = gameState.board.moves({ square: from, verbose: true })
const hasLegalPromotion = possibleMoves.some(move =>
move.to === to && move.promotion !== undefined
)
if (hasLegalPromotion) {
setGameState(prev => ({
...prev,
promotionMove: { from, to, promotion: 'q' } as Move,
promotionDialogActive: true,
selectedSquare: null,
legalMoves: []
}))
return true
}
}
try {
const testBoard = new Chess(gameState.board.fen())
const move = testBoard.move({ from, to })
if (move) {
const historyEntry: GameHistoryEntry = {
move: move.san,
moveData: move,
player: 'Human',
timestamp: new Date(),
capturedPiece: move.captured ? { type: move.captured, color: gameState.board.turn() === 'w' ? 'b' : 'w' } : undefined
}
setGameState(prev => ({
...prev,
board: testBoard,
gameHistory: [...prev.gameHistory, historyEntry],
selectedSquare: null,
legalMoves: []
}))
const gameResult = evaluateGameState(testBoard)
if (gameResult.isGameOver) {
setGameState(prev => ({
...prev,
gameActive: false,
gameOver: true,
gameResult
}))
} else {
// trigger AI move
setGameState(prev => ({ ...prev, aiThinking: true }))
aiTimeoutRef.current = setTimeout(() => {
makeAIMove()
}, 1000)
}
return true
} else {
console.log('Move returned null/false')
}
} catch (error) {
console.log('Invalid move attempted:', error)
}
return false
}, [gameState])
const makeAIMove = useCallback(async () => {
if (!chessAI.current) {
setGameState(prev => ({ ...prev, aiThinking: false }))
return
}
try {
setGameState(prev => {
const currentBoard = new Chess(prev.board.fen())
if (!prev.gameActive || currentBoard.turn() === prev.playerColor) {
return { ...prev, aiThinking: false }
}
const possibleMoves = currentBoard.moves({ verbose: true })
if (possibleMoves.length === 0) {
return { ...prev, aiThinking: false }
}
chessAI.current!.getMove(currentBoard, 10000).then(aiMove => {
if (!aiMove) {
setGameState(prev => ({ ...prev, aiThinking: false }))
return
}
const moveResult = currentBoard.move(aiMove)
if (!moveResult) {
setGameState(prev => ({ ...prev, aiThinking: false }))
return
}
const historyEntry: GameHistoryEntry = {
move: moveResult.san,
moveData: moveResult,
player: 'AI',
timestamp: new Date(),
capturedPiece: moveResult.captured ? { type: moveResult.captured, color: currentBoard.turn() === 'w' ? 'b' : 'w' } : undefined
}
const gameResult = evaluateGameState(currentBoard)
setGameState(prev => ({
...prev,
board: currentBoard,
gameHistory: [...prev.gameHistory, historyEntry],
aiThinking: false,
gameActive: !gameResult.isGameOver,
gameOver: gameResult.isGameOver,
gameResult: gameResult.isGameOver ? gameResult : null
}))
}).catch(error => {
console.error('Error in AI move generation:', error)
setGameState(prev => ({ ...prev, aiThinking: false }))
})
return prev
})
} catch (error) {
console.error('Error in AI move setup:', error)
setGameState(prev => ({ ...prev, aiThinking: false }))
}
}, [])
const completePromotion = useCallback((promotionPiece: 'q' | 'r' | 'b' | 'n') => {
if (!gameState.promotionMove) return
try {
const testBoard = new Chess(gameState.board.fen())
const move = testBoard.move({
from: gameState.promotionMove.from,
to: gameState.promotionMove.to,
promotion: promotionPiece
})
if (move) {
const historyEntry: GameHistoryEntry = {
move: move.san,
moveData: move,
player: 'Human',
timestamp: new Date(),
capturedPiece: move.captured ? { type: move.captured, color: gameState.board.turn() === 'w' ? 'b' : 'w' } : undefined
}
setGameState(prev => ({
...prev,
board: testBoard,
gameHistory: [...prev.gameHistory, historyEntry],
selectedSquare: null,
legalMoves: [],
promotionDialogActive: false,
promotionMove: null
}))
const gameResult = evaluateGameState(testBoard)
if (gameResult.isGameOver) {
setGameState(prev => ({
...prev,
gameActive: false,
gameOver: true,
gameResult
}))
} else {
setGameState(prev => ({ ...prev, aiThinking: true }))
aiTimeoutRef.current = setTimeout(() => {
makeAIMove()
}, 1000)
}
} else {
setGameState(prev => ({
...prev,
promotionDialogActive: false,
promotionMove: null
}))
}
} catch (error) {
console.error('Error during promotion:', error)
setGameState(prev => ({
...prev,
promotionDialogActive: false,
promotionMove: null
}))
}
}, [gameState.promotionMove, gameState.board, makeAIMove])
const startDrag = useCallback((square: Square) => {
if (!gameState.gameActive || gameState.board.turn() !== gameState.playerColor || gameState.aiThinking) {
return
}
const piece = gameState.board.get(square)
if (piece && piece.color === gameState.playerColor) {
setDraggedPiece({ piece, square })
const moves = gameState.board.moves({ square, verbose: true })
setGameState(prev => ({
...prev,
selectedSquare: square,
legalMoves: moves
}))
}
}, [gameState])
const endDrag = useCallback((targetSquare: Square | null) => {
console.log('Ending drag at:', targetSquare, 'from:', draggedPiece?.square)
let moveSuccessful = false
if (draggedPiece && targetSquare && targetSquare !== draggedPiece.square) {
moveSuccessful = attemptMove(draggedPiece.square, targetSquare)
}
setDraggedPiece(null)
if (!moveSuccessful) {
setGameState(prev => ({
...prev,
selectedSquare: null,
legalMoves: []
}))
}
}, [draggedPiece, attemptMove])
const getAIStatus = useCallback(() => {
if (!chessAI.current) return 'Not initialized'
return chessAI.current.getModelInfo()
}, [])
const changeModel = useCallback((modelId: string) => {
if (gameState.gameActive) return
setSelectedModel(modelId)
}, [gameState.gameActive])
useEffect(() => {
return () => {
if (aiTimeoutRef.current) {
clearTimeout(aiTimeoutRef.current)
}
}
}, [])
return {
gameState,
draggedPiece,
selectedModel,
startNewGame,
resignGame,
togglePlayerColor,
selectSquare,
attemptMove,
completePromotion,
startDrag,
endDrag,
getAIStatus,
changeModel
}
}