import { EngineName } from "@/types/enums"; import { EvaluateGameParams, EvaluatePositionWithUpdateParams, GameEval, MoveEval, } from "@/types/eval"; import { parseEvaluationResults } from "./helpers/parseResults"; import { computeAccuracy } from "./helpers/accuracy"; import { getWhoIsCheckmated } from "../chess"; import { getLichessEval } from "../lichess"; import { getMovesClassification } from "./helpers/moveClassification"; export abstract class UciEngine { private worker: Worker; private ready = false; private engineName: EngineName; private multiPv = 3; constructor(engineName: EngineName, enginePath: string) { this.engineName = engineName; this.worker = new Worker(enginePath); console.log(`${engineName} created`); } public async init(): Promise { await this.sendCommands(["uci"], "uciok"); await this.setMultiPv(this.multiPv, true); this.ready = true; console.log(`${this.engineName} initialized`); } private async setMultiPv(multiPv: number, initCase = false) { if (!initCase) { if (multiPv === this.multiPv) return; this.throwErrorIfNotReady(); } if (multiPv < 1 || multiPv > 6) { throw new Error(`Invalid MultiPV value : ${multiPv}`); } await this.sendCommands( [`setoption name MultiPV value ${multiPv}`, "isready"], "readyok" ); this.multiPv = multiPv; } private throwErrorIfNotReady() { if (!this.ready) { throw new Error(`${this.engineName} is not ready`); } } public shutdown(): void { this.ready = false; this.worker.postMessage("quit"); this.worker.terminate(); console.log(`${this.engineName} shutdown`); } public isReady(): boolean { return this.ready; } private async stopSearch(): Promise { await this.sendCommands(["stop", "isready"], "readyok"); } private async sendCommands( commands: string[], finalMessage: string, onNewMessage?: (messages: string[]) => void ): Promise { return new Promise((resolve) => { const messages: string[] = []; this.worker.onmessage = (event) => { const messageData: string = event.data; messages.push(messageData); onNewMessage?.(messages); if (messageData.startsWith(finalMessage)) { resolve(messages); } }; for (const command of commands) { this.worker.postMessage(command); } }); } public async evaluateGame({ fens, uciMoves, depth = 16, multiPv = this.multiPv, }: EvaluateGameParams): Promise { this.throwErrorIfNotReady(); await this.setMultiPv(multiPv); this.ready = false; await this.sendCommands(["ucinewgame", "isready"], "readyok"); this.worker.postMessage("position startpos"); const moves: MoveEval[] = []; for (const fen of fens) { const whoIsCheckmated = getWhoIsCheckmated(fen); if (whoIsCheckmated) { moves.push({ lines: [ { pv: [], depth: 0, multiPv: 1, mate: whoIsCheckmated === "w" ? -1 : 1, }, ], }); continue; } const result = await this.evaluatePosition(fen, depth); moves.push(result); } const movesWithClassification = getMovesClassification( moves, uciMoves, fens ); const accuracy = computeAccuracy(moves); this.ready = true; return { moves: movesWithClassification, accuracy, settings: { engine: this.engineName, date: new Date().toISOString(), depth, multiPv, }, }; } private async evaluatePosition(fen: string, depth = 16): Promise { console.log(`Evaluating position: ${fen}`); const lichessEval = await getLichessEval(fen, this.multiPv); if ( lichessEval.lines.length >= this.multiPv && lichessEval.lines[0].depth >= depth ) { return lichessEval; } const results = await this.sendCommands( [`position fen ${fen}`, `go depth ${depth}`], "bestmove" ); const whiteToPlay = fen.split(" ")[1] === "w"; return parseEvaluationResults(results, whiteToPlay); } public async evaluatePositionWithUpdate({ fen, depth = 16, multiPv = this.multiPv, setPartialEval, }: EvaluatePositionWithUpdateParams): Promise { this.throwErrorIfNotReady(); const lichessEvalPromise = getLichessEval(fen, multiPv); await this.stopSearch(); await this.setMultiPv(multiPv); const whiteToPlay = fen.split(" ")[1] === "w"; const onNewMessage = (messages: string[]) => { const parsedResults = parseEvaluationResults(messages, whiteToPlay); setPartialEval(parsedResults); }; console.log(`Evaluating position: ${fen}`); const lichessEval = await lichessEvalPromise; if ( lichessEval.lines.length >= multiPv && lichessEval.lines[0].depth >= depth ) { setPartialEval(lichessEval); return; } await this.sendCommands( [`position fen ${fen}`, `go depth ${depth}`], "bestmove", onNewMessage ); } }