I’m working on a chess analysis tool that uses a StockfishWorker class to evaluate moves and provide engine suggestions. The code is designed to interface with a web worker running Stockfish, handle communication, and process multi-PV lines.
Here’s the complete code for the class:
import type { EngineLine, Evaluation, Lan } from '../types/Game';
import type GameReviewManager from './_GameReviewManager';
import { getCloudEvaluation } from '../api/lichessApiAccess';
class StockfishWorker {
private workerUrl: URL;
private stockfishWorker: Worker;
private verbose: boolean;
private multipv: number;
private targetDepth: number;
readonly game_review_manager: GameReviewManager;
private waitFor(response: string, errormsg = 'error') {
return new Promise((resolve, reject) => {
const listener = <K extends keyof WorkerEventMap>(
e: WorkerEventMap[K],
) => {
if (this.verbose) console.debug(e);
if (e instanceof MessageEvent && e.data.includes(response)) {
this.stockfishWorker.removeEventListener('message', listener);
resolve(true);
}
};
this.stockfishWorker.addEventListener('message', listener);
// Add a timeout for error handling (optional)
setTimeout(() => {
this.stockfishWorker.removeEventListener('message', listener);
reject(`delay time exceeded: ${errormsg}`);
}, 5000);
});
}
_init(restart = false) {
return new Promise(async (resolve) => {
this.stockfishWorker.onmessageerror = (e) => console.debug(e);
if (!restart) {
this.stockfishWorker.postMessage('uci');
await this.waitFor('uciok', 'uci setup error');
} else console.debug('Restarting engine...');
this.stockfishWorker.postMessage(`ucinewgame`);
this.stockfishWorker.postMessage('isready');
this.stockfishWorker.postMessage(
`setoption name MultiPV value ${this.multipv}`,
);
this.waitFor('readyok', 'this.stockfishWorker not ready after timeout')
.then(() => {
resolve(true);
})
.catch((err) => {
throw new Error(err);
});
});
}
evaluateMove(fen: string = 'startpos'): Promise<EngineLine[]> {
console.log('evaluate move');
!fen || fen == 'startpos'
? this.stockfishWorker.postMessage(`position startpos`)
: this.stockfishWorker.postMessage(`position fen ${fen}`);
this.stockfishWorker.postMessage(`go depth ${this.targetDepth}`);
let messages = [];
let lines: EngineLine[] = [];
return new Promise((resolve, reject) => {
const listener = <K extends keyof WorkerEventMap>(
e: WorkerEventMap[K],
) => {
if (e instanceof MessageEvent) {
messages.unshift(e.data);
if (e.data.includes('depth 0')) {
if (this.verbose) console.warn(`${e}`);
}
if (e.data.startsWith('bestmove') || e.data.includes('depth 0')) {
this.stockfishWorker.removeEventListener('message', listener);
let searchMessages = messages.filter((msg) =>
msg.startsWith('info depth'),
);
for (let searchMessage of searchMessages) {
// Extract depth, MultiPV line ID and evaluation from search message
let idString = searchMessage.match(/(?:multipv )(d+)/)?.[1];
let depthString = searchMessage.match(/(?:depth )(d+)/)?.[1];
let bestMove: Lan =
searchMessage.match(/(?: pv )(.+?)(?= |$)/)?.[1];
let evaluation: Evaluation = {
type: searchMessage.includes(' cp ') ? 'cp' : 'mate',
value: parseInt(
searchMessage.match(/(?:(?:cp )|(?:mate ))([d-]+)/)?.[1] ||
'0',
),
};
// Invert evaluation if black to play since scores are from black perspective
// and we want them always from the perspective of white
if (fen && fen.includes(' b ')) {
evaluation.value *= -1;
}
// If any piece of data from message is missing, discard message
if (!idString || !depthString || !bestMove) {
lines.push(
{
id: parseInt(idString),
depth: parseInt(depthString),
evaluation: { type: 'mate', value: 0 },
bestMove: 'a1a1',
},
{
id: parseInt(idString),
depth: parseInt(depthString),
evaluation: { type: 'mate', value: 0 },
bestMove: 'a1a1',
},
);
resolve(lines);
return;
}
let id = parseInt(idString);
let depth = parseInt(depthString);
// Discard if target depth not reached or lineID already present
if (
depth != this.targetDepth ||
lines.some((line) => line.id == id)
)
continue;
lines.push({
id,
depth,
evaluation,
bestMove,
});
}
clearTimeout(timeoutId); // Clear the timeout if message is received
console.debug(lines);
console.debug('cleared time out id');
resolve(lines);
}
}
};
this.stockfishWorker.addEventListener('message', listener);
const timeoutId = setTimeout(() => {
this.stockfishWorker.removeEventListener('message', listener);
reject(new Error('takes alot of time'));
}, 20000); // Adjust timeout as needed
});
}
private restartWorker(
ui_update_callback: () => void,
move: {
current_fen: string;
move_num: number;
sanmove: string;
},
) {
console.log('restarting worker....');
this.stockfishWorker.terminate();
this.stockfishWorker = new Worker(this.workerUrl, { type: 'classic' });
this._init().then(() => {
this.evaluateStuckMove(ui_update_callback, move).then((res) => {
if (res.success) {
console.warn('continue with work');
this.evaluatePosition(ui_update_callback);
} else {
console.error('restarting worker again');
this.restartWorker(ui_update_callback, move);
}
});
});
}
private async evaluateStuckMove(
ui_update_callback: () => void,
move: {
current_fen: string;
move_num: number;
sanmove: string;
},
) {
try {
console.debug('evaluating halt move', move);
const engineLines = await this.evaluateMove(move.current_fen);
ui_update_callback();
this.game_review_manager.add_enginelines(engineLines, move.move_num);
return { success: true };
} catch (error) {
return { success: false };
}
}
async evaluatePosition(ui_update_callback: () => void) {
console.log('evaluating position');
while (!this.game_review_manager.done_evaluating()) {
const currentMove = this.game_review_manager.get_next_move();
const { current_fen, move_num } = currentMove;
console.debug({ currentMove });
try {
const engineLines = await this.evaluateMove(current_fen);
const cloud_eval = await getCloudEvaluation(current_fen);
console.warn({ cloud_eval });
console.debug({ engineLines });
ui_update_callback();
this.game_review_manager.add_enginelines(engineLines, move_num);
} catch (error) {
console.error('error');
console.error('restarting');
this.restartWorker(ui_update_callback, currentMove);
}
}
console.log('terminate_worker');
this.stockfishWorker.terminate();
return { success: true };
}
/**
* @param startingpos : fen format
* @param multipv : number of lines to return
* @param verbose : testing
*/
constructor(
game_review_manager: GameReviewManager,
multipv = 2,
targetDepth = 15,
verbose = false,
) {
var wasmSupported =
typeof WebAssembly === 'object' &&
WebAssembly.validate(
Uint8Array.of(0x0, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00),
);
this.workerUrl = wasmSupported
? new URL('./stockfish/stockfish.js', import.meta.url)
: new URL('./stockfish/stockfish.wasm.js', import.meta.url);
this.stockfishWorker = new Worker(this.workerUrl, { type: 'classic' });
this.multipv = multipv;
//this.evaluateMove = this.evaluateMove.bind(this);
this.targetDepth = targetDepth;
this.verbose = verbose;
//this.waitFor = this.waitFor.bind(this);
this.game_review_manager = game_review_manager;
}
}
export default StockfishWorker;
Features:
Initialization: The _init method sets up the Stockfish engine and waits for it to be ready.
Move Evaluation: evaluateMove sends FEN positions to the engine and collects depth-based evaluations.
Timeout Handling: There are multiple timeout mechanisms to ensure the process doesn’t hang.
Worker Restarts: restartWorker attempts to recover the worker if it becomes unresponsive.
Cloud Integration: It also integrates with a cloud evaluation API for additional analysis.
problems:
the engine keeps halting and restarting, and the engine sometimes returns an empty result as the eval even if I know it exists one
when I increase the multiple the failing rises a lot to the point of it not working
My Concerns:
Timeouts and Asynchronous Handling: Are the timeout and retry mechanisms implemented correctly to ensure robust communication with the worker?
Message Filtering: Is the filtering and processing of engine output (info depth, bestmove, etc.) reliable for real-time chess analysis?
Redundancy: Certain parts, like pushing fallback lines with placeholder values (a1a1 moves), seem redundant. Should this be optimized?
Worker Performance: Does terminating and restarting the worker frequently affect performance, and is there a better way to handle “stuck” evaluations?