houserules/packages/chess/src/hooks/useChessEngine.ts

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,
};
}