GHChess / src /evaluation /Engine.js
GHHG10's picture
Upload 529 files
f0c8ada verified
Raw
History Blame Contribute Delete
10.4 kB
const engines = {
'stockfish-17-lite': {
name: "Stockfish 17 Lite",
path: "./src/engines/stockfish/stockfish-17-lite.js",
},
'stockfish-16-nnue': {
name: "Stockfish 16 NNUE",
path: "./src/engines/stockfish/stockfish-nnue-16.js",
},
'stockfish-16-lite': {
name: "Stockfish 16 Lite",
path: "./src/engines/stockfish/fallback-stockfish.js",
},
'stockfish-11': {
name: "Stockfish 11",
path: "./src/engines/stockfish/stockfish-11.js",
}
}
export class Engine {
currentDepth = 0;
multiPV = 3;
busy = false;
currentResolve = null;
currentReject = null;
constructor({ engineType = 'stockfish-17-lite' } = {}) {
this.engine = engines[engineType];
this.worker = new Worker(this.engine.path);
this.worker.postMessage("uci");
this.worker.postMessage(`setoption name MultiPV value ${this.multiPV}`);
// Setup global message handler for reuse
this.worker.addEventListener("error", this.handleError.bind(this));
// Add onerror handler directly (catches more errors than the event listener)
this.worker.onerror = this.handleError.bind(this);
// Add global error handling through window for worker errors
window.addEventListener('unhandledrejection', (event) => {
if (event.reason && event.reason.toString().includes('stockfish')) {
console.log("Caught unhandled rejection:", event.reason);
this.handleError(event.reason);
event.preventDefault();
}
});
}
// Method to abort current evaluation
abort() {
this.worker.postMessage('stop');
if (this.currentResolve) {
this.currentResolve([]);
this.currentResolve = null;
this.currentReject = null;
}
this.busy = false;
}
// Properly terminate worker
terminate() {
this.abort();
if (this.worker) {
this.worker.terminate();
this.worker = null;
}
}
handleError(err) {
console.log("Engine error", err);
if (this.currentReject) {
this.currentReject(err);
this.currentResolve = null;
this.currentReject = null;
}
// Fallback to alternative engine if needed
this.fallbackToAlternativeEngine();
return true; // Prevent the error from propagating
}
fallbackToAlternativeEngine(fallen = 0) {
if (fallen > 1) return;
if (this.worker) {
try {
this.worker.terminate();
} catch (err) {
console.log("Error terminating worker:", err);
}
}
try {
console.log("Falling back to alternative engine");
this.engine = engines['stockfish-16-lite'];
this.worker = new Worker(this.engine.path);
this.worker.postMessage("uci");
this.worker.postMessage(`setoption name MultiPV value ${this.multiPV}`);
this.worker.addEventListener("error", this.handleError.bind(this));
this.worker.onerror = this.handleError.bind(this);
} catch (err) {
console.log("Error creating fallback worker:", err);
// Try with a different engine if the fallback fails
if (fallen < 1) {
console.log("Trying with most basic engine");
this.engine = engines['stockfish-11'];
try {
this.worker = new Worker(this.engine.path);
this.worker.postMessage("uci");
this.worker.postMessage(`setoption name MultiPV value ${this.multiPV}`);
this.worker.addEventListener("error", this.handleError.bind(this));
this.worker.onerror = this.handleError.bind(this);
} catch (e) {
console.log("All engine attempts failed");
this.worker = null;
}
} else {
this.worker = null;
}
}
}
interpret(uciOutputLines, fen, targetDepth) {
const lines = [];
const outputs = uciOutputLines.filter(uciOutput => uciOutput.startsWith("info depth"));
for (const output of outputs) {
// Extract depth, MultiPV line ID and evaluation from search message
const id = parseInt(output.match(/(?:multipv )(\d+)/)?.[1]);
const depth = parseInt(output.match(/(?:depth )(\d+)/)?.[1]);
const uciMove = output.match(/(?: pv )(.+?)(?= |$)/)?.[1];
if (!id || !depth || !uciMove || depth != targetDepth || lines.some(line => line.id == id)) continue;
// Invert score for black since stockfish is negamax instead of minimax
const negamaxScore = parseInt(output.match(/(?:(?:cp )|(?:mate ))([\d-]+)/)?.[1] || "0");
const score = fen.includes(" b ") ? -negamaxScore : negamaxScore;
const type = output.includes(" cp ") ? "cp" : "mate";
const pv = output.match(/.*pv\s+(.*)$/)?.[1].split(" ")
lines.push({ id, uciMove, depth, score, type, pv });
}
return lines;
}
async evaluate(fen, targetDepth, verbose = false, progressCallback = null, fallen = 0) {
this.busy = true;
// Reset current depth
this.currentDepth = 0;
if (!this.worker) {
try {
this.worker = new Worker(engines[0].path);
this.worker.postMessage("uci");
this.worker.postMessage(`setoption name MultiPV value ${this.multiPV}`);
this.worker.addEventListener("error", this.handleError.bind(this));
this.worker.onerror = this.handleError.bind(this);
} catch (err) {
console.log("Error creating worker:", err);
this.fallbackToAlternativeEngine(fallen);
return this.evaluate(fen, targetDepth, verbose, progressCallback, fallen + 1);
}
}
try {
this.worker.postMessage(`position fen ${fen}`);
this.worker.postMessage(`go depth ${targetDepth}`);
} catch (err) {
console.log("Error sending commands to worker:", err);
this.fallbackToAlternativeEngine(fallen);
return this.evaluate(fen, targetDepth, verbose, progressCallback, fallen + 1);
}
const messages = [];
return new Promise((resolve, reject) => {
this.currentResolve = resolve;
this.currentReject = reject;
const messageHandler = (event) => {
try {
const message = event.data;
messages.unshift(message);
if (verbose) console.log(message);
let latestDepth = parseInt(message.match(/(?:depth )(\d+)/)?.[1] || "0");
if (latestDepth > 0) {
this.currentDepth = Math.max(latestDepth, this.currentDepth);
// Report progress based on current depth compared to target depth
if (progressCallback && typeof progressCallback === 'function') {
const progressPercent = Math.round((this.currentDepth / targetDepth) * 100);
progressCallback({
depth: this.currentDepth,
targetDepth: targetDepth,
percent: progressPercent
});
}
}
// Best move or checkmate log indicates end of search
if (message.startsWith("bestmove") || message.includes("depth 0")) {
const lines = this.interpret(messages, fen, targetDepth);
// Report 100% completion
if (progressCallback && typeof progressCallback === 'function') {
progressCallback({
depth: targetDepth,
targetDepth: targetDepth,
percent: 100
});
}
this.worker.removeEventListener("message", messageHandler);
this.busy = false;
this.currentResolve = null;
this.currentReject = null;
resolve(lines);
}
} catch (err) {
console.log("Error handling message:", err);
this.worker.removeEventListener("message", messageHandler);
this.handleError(err);
}
};
this.worker.addEventListener("message", messageHandler, { once: false });
// Add safety timeout in case engine gets stuck
setTimeout(() => {
if (this.busy && this.currentResolve === resolve) {
console.warn("Engine evaluation timed out, falling back");
this.worker.removeEventListener("message", messageHandler);
if (fallen < 2) {
// Try with fallback engine
this.fallbackToAlternativeEngine(fallen);
this.evaluate(fen, targetDepth, verbose, progressCallback, fallen + 1)
.then(resolve)
.catch(reject);
} else {
// Give up after fallback attempt
this.busy = false;
this.currentResolve = null;
this.currentReject = null;
resolve([]);
console.log("Gave up")
}
}
}, 30000 * targetDepth); // timeout depends on depth
});
}
}