Add Stockfish evaluation API endpoint

This commit is contained in:
stefan-kp
2025-12-03 21:30:39 +01:00
parent 3787696aa8
commit cab02cfefd
3 changed files with 179 additions and 0 deletions
+33
View File
@@ -0,0 +1,33 @@
import { NextRequest, NextResponse } from "next/server";
import { evaluateStockfish } from "@/lib/server/stockfishEngine";
import { StockfishEvaluation } from "@/lib/stockfish";
export const runtime = "nodejs";
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const { fen, depth = 15, multiPV = 1 } = body ?? {};
if (!fen || typeof fen !== "string") {
return NextResponse.json({ error: "Missing or invalid FEN" }, { status: 400 });
}
const parsedDepth = Number(depth);
const parsedMultiPV = Number(multiPV);
if (!Number.isFinite(parsedDepth) || parsedDepth <= 0) {
return NextResponse.json({ error: "Depth must be a positive number" }, { status: 400 });
}
if (!Number.isFinite(parsedMultiPV) || parsedMultiPV <= 0) {
return NextResponse.json({ error: "multiPV must be a positive number" }, { status: 400 });
}
const evaluation: StockfishEvaluation = await evaluateStockfish(fen, parsedDepth, parsedMultiPV);
return NextResponse.json({ evaluation });
} catch (error) {
console.error("Stockfish API error", error);
return NextResponse.json({ error: "Failed to evaluate position" }, { status: 500 });
}
}
+123
View File
@@ -0,0 +1,123 @@
import path from "path";
import { Worker } from "worker_threads";
import { StockfishEvaluation } from "../stockfish";
const WORKER_CODE = `
const { parentPort, workerData } = require('worker_threads');
const { enginePath } = workerData;
const emit = (msg) => parentPort.postMessage(msg);
global.postMessage = emit;
global.self = global;
global.window = global;
global.document = {};
global.close = () => parentPort.close();
let handler = null;
Object.defineProperty(global, 'onmessage', {
get() { return handler; },
set(fn) { handler = fn; }
});
require(enginePath);
parentPort.on('message', (data) => {
if (typeof handler === 'function') {
handler({ data });
}
});
`;
const ENGINE_PATH = path.resolve(process.cwd(), "node_modules/stockfish.js/stockfish.js");
const DEFAULT_TIMEOUT_MS = 15_000;
export async function evaluateStockfish(
fen: string,
depth: number = 15,
multiPV: number = 1,
timeoutMs: number = DEFAULT_TIMEOUT_MS
): Promise<StockfishEvaluation> {
return new Promise((resolve, reject) => {
const worker = new Worker(WORKER_CODE, {
eval: true,
workerData: { enginePath: ENGINE_PATH },
});
let lastScore = 0;
let lastMate: number | null = null;
let lastDepth = 0;
let resolved = false;
let timeout: NodeJS.Timeout | null = null;
const cleanup = () => {
if (timeout) clearTimeout(timeout);
worker.removeAllListeners();
worker.terminate().catch(() => undefined);
};
const onMessage = (msg: unknown) => {
if (typeof msg !== "string") return;
if (msg.startsWith("info depth")) {
const depthMatch = msg.match(/depth (\d+)/);
const scoreMatch = msg.match(/score cp (-?\d+)/);
const mateMatch = msg.match(/score mate (-?\d+)/);
if (depthMatch) lastDepth = parseInt(depthMatch[1], 10);
if (scoreMatch) {
lastScore = parseInt(scoreMatch[1], 10);
lastMate = null;
}
if (mateMatch) {
lastMate = parseInt(mateMatch[1], 10);
lastScore = 0;
}
}
if (msg.startsWith("bestmove")) {
const parts = msg.split(" ");
const bestMove = parts[1];
let ponder: string | null = null;
if (parts.length > 3 && parts[2] === "ponder") {
ponder = parts[3];
}
const evaluation: StockfishEvaluation = {
bestMove,
ponder,
score: lastScore,
mate: lastMate,
depth: lastDepth,
};
const sideToMove = fen.split(" ")[1];
if (sideToMove === "b") {
if (evaluation.score !== 0) evaluation.score = -evaluation.score;
if (evaluation.mate !== null && evaluation.mate !== 0) {
evaluation.mate = -evaluation.mate;
}
}
resolved = true;
cleanup();
resolve(evaluation);
}
};
worker.on("message", onMessage);
worker.on("error", (err) => {
if (resolved) return;
resolved = true;
cleanup();
reject(err);
});
timeout = setTimeout(() => {
if (resolved) return;
resolved = true;
cleanup();
reject(new Error("Stockfish evaluation timed out"));
}, timeoutMs);
worker.postMessage("uci");
worker.postMessage("setoption name MultiPV value " + multiPV);
worker.postMessage("isready");
worker.postMessage(`position fen ${fen}`);
worker.postMessage(`go depth ${depth}`);
});
}