Add Stockfish evaluation API endpoint
This commit is contained in:
@@ -0,0 +1,23 @@
|
|||||||
|
# Stockfish API (v1)
|
||||||
|
|
||||||
|
## Welche Informationen brauchen wir?
|
||||||
|
|
||||||
|
Der bestehende Code nutzt Stockfish überall dort, wo Spielzüge validiert, bewertet oder nachträglich analysiert werden. Die zentralen Datenpunkte sind:
|
||||||
|
|
||||||
|
- **Best move & ponder**: Wird verwendet, um die Computerzüge auszuführen und verpasste Taktiken zu erkennen. Beispiel: Im laufenden Spiel wird der aus `bestMove` abgeleitete Zug sofort gespielt und für Taktikvergleiche gespeichert.【F:src/components/ChessGame.tsx†L448-L517】【F:src/lib/stockfish.ts†L1-L58】
|
||||||
|
- **Stellungsbewertung (score in Centipawns) & Mattdistanz**: Dient zur Bewertungsanzeige, Berechnung des CP-Verlusts und zum Speichern der Analysehistorie.【F:src/components/ChessGame.tsx†L486-L517】【F:src/app/analysis/page.tsx†L82-L113】
|
||||||
|
- **Suchtiefe**: Wird gespeichert, um die Qualität der Bewertung anzuzeigen und später wiederzugeben (z.B. in der Historie und bei Analysevergleichen).【F:src/lib/stockfish.ts†L1-L58】【F:src/components/ChessGame.tsx†L290-L305】
|
||||||
|
|
||||||
|
Damit lassen sich alle bestehenden Features abdecken: Live-Zugempfehlungen, Move-History mit CP-Loss, Taktik-Erkennung und die schrittweise Analyse importierter Partien.
|
||||||
|
|
||||||
|
## API-Endpoint
|
||||||
|
|
||||||
|
- **POST `/api/v1/stockfish`**
|
||||||
|
- **Body**: `{ fen: string, depth?: number, multiPV?: number }`
|
||||||
|
- `fen` – obligatorisch, aktuelle Stellung.
|
||||||
|
- `depth` – optional (Standard: 15), Suchtiefe für Stockfish.
|
||||||
|
- `multiPV` – optional (Standard: 1), Anzahl der Varianten; aktuell wird die Hauptvariante zurückgegeben.
|
||||||
|
- **Response**: `{ evaluation: { bestMove: string, ponder: string | null, score: number, mate: number | null, depth: number } }`
|
||||||
|
- `score` und `mate` sind aus Weiß-Perspektive normalisiert, passend zum bestehenden Frontend-Verhalten.【F:src/lib/server/stockfishEngine.ts†L60-L97】
|
||||||
|
|
||||||
|
Der Endpunkt kapselt den Stockfish-Worker serverseitig und liefert exakt die Daten, die das Frontend heute schon für Züge, Taktikerkennung und Analyse benötigt. Damit kann die React-Native-App die gleiche Engine-Funktionalität per HTTP nutzen.
|
||||||
@@ -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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user