113 lines
3.4 KiB
TypeScript
113 lines
3.4 KiB
TypeScript
import { useState, useRef, useCallback } from 'react';
|
|
import { ChessEngine, type GameResult } from '../engine';
|
|
import type { PieceType } from '../schema';
|
|
import { saveAutoSave } from '../persist/autosave.js';
|
|
import * as audio from '../audio';
|
|
|
|
export function useChessEngine() {
|
|
const engineRef = useRef<ChessEngine | null>(null);
|
|
const moveHistoryRef = useRef<Array<{ from: number, to: number, promoteTo: PieceType }>>([]);
|
|
|
|
// Force re-render state
|
|
const [tick, setTick] = useState(0);
|
|
|
|
// Initialize engine lazily
|
|
if (!engineRef.current) {
|
|
engineRef.current = new ChessEngine();
|
|
}
|
|
|
|
const engine = engineRef.current;
|
|
|
|
const loadEngine = useCallback((newEngine: ChessEngine) => {
|
|
engineRef.current = newEngine;
|
|
moveHistoryRef.current = [];
|
|
saveAutoSave(newEngine.session.allFacts());
|
|
setTick(t => t + 1);
|
|
}, []);
|
|
|
|
const getTurn = useCallback(() => {
|
|
return engine.getCurrentTurn();
|
|
}, [engine, tick]);
|
|
|
|
const getFacts = useCallback(() => {
|
|
return engine.session.allFacts();
|
|
}, [engine, tick]);
|
|
|
|
const getLegalMoves = useCallback(() => {
|
|
return engine.getAllLegalMoves();
|
|
}, [engine, tick]);
|
|
|
|
const getResult = useCallback(() => {
|
|
return engine.checkGameResult();
|
|
}, [engine, tick]);
|
|
|
|
const applyMove = useCallback((from: number, to: number, promoteTo: PieceType = 'queen'): GameResult | null => {
|
|
const move = engine.findMove(from, to, promoteTo);
|
|
if (move) {
|
|
moveHistoryRef.current.push({ from, to, promoteTo });
|
|
const result = engine.applyMove(move, promoteTo);
|
|
saveAutoSave(engine.session.allFacts());
|
|
setTick(t => t + 1); // trigger re-render
|
|
|
|
// Play appropriate sound
|
|
if (result === 'checkmate') {
|
|
audio.play('checkmate');
|
|
} else if (engine.session.allFacts().some(f => f.attr === 'InCheck' && f.value === true)) {
|
|
audio.play('check');
|
|
} else if (move.isCapture) {
|
|
audio.play('capture');
|
|
} else if (promoteTo && move.promoteTo) {
|
|
audio.play('promote');
|
|
} else {
|
|
audio.play('move'); // TODO: differentiate castle
|
|
}
|
|
|
|
return result;
|
|
}
|
|
return null;
|
|
}, [engine]);
|
|
|
|
const undo = useCallback(() => {
|
|
if (moveHistoryRef.current.length > 0) {
|
|
moveHistoryRef.current.pop();
|
|
const newEngine = new ChessEngine();
|
|
for (const historyMove of moveHistoryRef.current) {
|
|
const move = newEngine.findMove(historyMove.from, historyMove.to, historyMove.promoteTo);
|
|
if (move) {
|
|
newEngine.applyMove(move, historyMove.promoteTo);
|
|
}
|
|
}
|
|
engineRef.current = newEngine;
|
|
saveAutoSave(newEngine.session.allFacts());
|
|
setTick(t => t + 1);
|
|
}
|
|
}, []);
|
|
|
|
const lastMove = moveHistoryRef.current.length > 0
|
|
? moveHistoryRef.current[moveHistoryRef.current.length - 1]
|
|
: null;
|
|
|
|
/**
|
|
* Force the hook's derived state (facts, legalMoves, turn, result) to
|
|
* recompute. Call this after mutating external state that the engine
|
|
* reads but React can't see — for example, toggling PRESET_REGISTRY
|
|
* entries that change the set of legal moves without triggering a move.
|
|
*/
|
|
const refresh = useCallback(() => {
|
|
setTick(t => t + 1);
|
|
}, []);
|
|
|
|
return {
|
|
engine,
|
|
turn: getTurn(),
|
|
facts: getFacts(),
|
|
legalMoves: getLegalMoves(),
|
|
result: getResult(),
|
|
applyMove,
|
|
undo,
|
|
canUndo: moveHistoryRef.current.length > 0,
|
|
loadEngine,
|
|
refresh,
|
|
lastMove,
|
|
};
|
|
}
|