From 6b676d3015ddfae39c06c3482797fcd6f5d71bfc Mon Sep 17 00:00:00 2001 From: Michess Date: Mon, 13 Apr 2026 13:33:26 +0200 Subject: [PATCH] fix: use child_process to spawn stockfish via stdin/stdout --- .../src/controllers/stockfish.controller.ts | 86 ++++++++++--------- 1 file changed, 45 insertions(+), 41 deletions(-) diff --git a/server/src/controllers/stockfish.controller.ts b/server/src/controllers/stockfish.controller.ts index bfef8ef..7b7f777 100644 --- a/server/src/controllers/stockfish.controller.ts +++ b/server/src/controllers/stockfish.controller.ts @@ -1,9 +1,12 @@ +import { spawn, ChildProcess } from "child_process"; +import { createRequire } from "module"; + export interface StockfishLevel { level: number; name: string; skillLevel: number; depth: number; - moveTime: number; // ms + moveTime: number; } export const AI_LEVELS: StockfishLevel[] = [ @@ -15,17 +18,12 @@ export const AI_LEVELS: StockfishLevel[] = [ { level: 6, name: "Meister", skillLevel: 20, depth: 20, moveTime: 3000 }, ]; -type SFEngine = { - postMessage: (cmd: string) => void; - onmessage: ((line: string | { data: string }) => void) | null; - terminate?: () => void; -}; - export class StockfishEngine { - private engine: SFEngine | null = null; + private process: ChildProcess | null = null; private ready = false; private resolvers: Array<(move: string) => void> = []; private initPromise: Promise; + private buffer = ""; constructor() { this.initPromise = this.init(); @@ -33,37 +31,45 @@ export class StockfishEngine { private async init() { try { - // Dynamic import of the stockfish npm package - const { default: Stockfish } = await import("stockfish") as { default: () => SFEngine }; - this.engine = Stockfish(); + const require = createRequire(import.meta.url); + const sfPath: string = require.resolve("stockfish"); + + this.process = spawn(process.execPath, [sfPath], { + stdio: ["pipe", "pipe", "pipe"], + }); await new Promise((resolve) => { - let uciOk = false; let readyOk = false; - this.engine!.onmessage = (event: string | { data: string }) => { - const line = typeof event === "string" ? event : event.data; + this.process!.stdout!.on("data", (data: Buffer) => { + this.buffer += data.toString(); + const lines = this.buffer.split("\n"); + this.buffer = lines.pop() ?? ""; - if (line === "uciok") { - uciOk = true; - this.engine!.postMessage("isready"); + for (const line of lines) { + const msg = line.trim(); + if (msg === "uciok") { + this.process!.stdin!.write("isready\n"); + } + if (msg === "readyok") { + readyOk = true; + this.ready = true; + resolve(); + } + if (msg.startsWith("bestmove ")) { + const move = msg.split(" ")[1]; + const resolver = this.resolvers.shift(); + if (resolver) resolver(move === "(none)" ? "" : move); + } } - if (line === "readyok") { - readyOk = true; - this.ready = true; - resolve(); - } - if (line.startsWith("bestmove ")) { - const parts = line.split(" "); - const move = parts[1]; - const resolver = this.resolvers.shift(); - if (resolver) resolver(move === "(none)" ? "" : move); - } - }; + }); - this.engine!.postMessage("uci"); + this.process!.stderr!.on("data", (data: Buffer) => { + console.error("Stockfish stderr:", data.toString()); + }); + + this.process!.stdin!.write("uci\n"); - // Timeout after 5s if engine doesn't respond setTimeout(() => { if (!readyOk) { console.warn("Stockfish init timeout"); @@ -72,7 +78,7 @@ export class StockfishEngine { }, 5000); }); - console.log("Stockfish engine ready"); + if (this.ready) console.log("Stockfish engine ready"); } catch (err) { console.error("Failed to initialize Stockfish:", err); } @@ -81,7 +87,7 @@ export class StockfishEngine { async getBestMove(fen: string, level: StockfishLevel): Promise { await this.initPromise; - if (!this.engine || !this.ready) { + if (!this.process || !this.ready) { console.warn("Stockfish not ready"); return ""; } @@ -89,12 +95,11 @@ export class StockfishEngine { return new Promise((resolve) => { this.resolvers.push(resolve); - this.engine!.postMessage("ucinewgame"); - this.engine!.postMessage(`setoption name Skill Level value ${level.skillLevel}`); - this.engine!.postMessage(`position fen ${fen}`); - this.engine!.postMessage(`go depth ${level.depth} movetime ${level.moveTime}`); + this.process!.stdin!.write("ucinewgame\n"); + this.process!.stdin!.write(`setoption name Skill Level value ${level.skillLevel}\n`); + this.process!.stdin!.write(`position fen ${fen}\n`); + this.process!.stdin!.write(`go depth ${level.depth} movetime ${level.moveTime}\n`); - // Safety timeout setTimeout(() => { const idx = this.resolvers.indexOf(resolve); if (idx !== -1) { @@ -106,12 +111,11 @@ export class StockfishEngine { } destroy() { - this.engine?.terminate?.(); - this.engine = null; + this.process?.kill(); + this.process = null; } } -// Singleton let engineInstance: StockfishEngine | null = null; export const getEngine = (): StockfishEngine => {