fix: use child_process to spawn stockfish via stdin/stdout
This commit is contained in:
@@ -1,9 +1,12 @@
|
|||||||
|
import { spawn, ChildProcess } from "child_process";
|
||||||
|
import { createRequire } from "module";
|
||||||
|
|
||||||
export interface StockfishLevel {
|
export interface StockfishLevel {
|
||||||
level: number;
|
level: number;
|
||||||
name: string;
|
name: string;
|
||||||
skillLevel: number;
|
skillLevel: number;
|
||||||
depth: number;
|
depth: number;
|
||||||
moveTime: number; // ms
|
moveTime: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AI_LEVELS: StockfishLevel[] = [
|
export const AI_LEVELS: StockfishLevel[] = [
|
||||||
@@ -15,17 +18,12 @@ export const AI_LEVELS: StockfishLevel[] = [
|
|||||||
{ level: 6, name: "Meister", skillLevel: 20, depth: 20, moveTime: 3000 },
|
{ 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 {
|
export class StockfishEngine {
|
||||||
private engine: SFEngine | null = null;
|
private process: ChildProcess | null = null;
|
||||||
private ready = false;
|
private ready = false;
|
||||||
private resolvers: Array<(move: string) => void> = [];
|
private resolvers: Array<(move: string) => void> = [];
|
||||||
private initPromise: Promise<void>;
|
private initPromise: Promise<void>;
|
||||||
|
private buffer = "";
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.initPromise = this.init();
|
this.initPromise = this.init();
|
||||||
@@ -33,37 +31,45 @@ export class StockfishEngine {
|
|||||||
|
|
||||||
private async init() {
|
private async init() {
|
||||||
try {
|
try {
|
||||||
// Dynamic import of the stockfish npm package
|
const require = createRequire(import.meta.url);
|
||||||
const { default: Stockfish } = await import("stockfish") as { default: () => SFEngine };
|
const sfPath: string = require.resolve("stockfish");
|
||||||
this.engine = Stockfish();
|
|
||||||
|
this.process = spawn(process.execPath, [sfPath], {
|
||||||
|
stdio: ["pipe", "pipe", "pipe"],
|
||||||
|
});
|
||||||
|
|
||||||
await new Promise<void>((resolve) => {
|
await new Promise<void>((resolve) => {
|
||||||
let uciOk = false;
|
|
||||||
let readyOk = false;
|
let readyOk = false;
|
||||||
|
|
||||||
this.engine!.onmessage = (event: string | { data: string }) => {
|
this.process!.stdout!.on("data", (data: Buffer) => {
|
||||||
const line = typeof event === "string" ? event : event.data;
|
this.buffer += data.toString();
|
||||||
|
const lines = this.buffer.split("\n");
|
||||||
|
this.buffer = lines.pop() ?? "";
|
||||||
|
|
||||||
if (line === "uciok") {
|
for (const line of lines) {
|
||||||
uciOk = true;
|
const msg = line.trim();
|
||||||
this.engine!.postMessage("isready");
|
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(() => {
|
setTimeout(() => {
|
||||||
if (!readyOk) {
|
if (!readyOk) {
|
||||||
console.warn("Stockfish init timeout");
|
console.warn("Stockfish init timeout");
|
||||||
@@ -72,7 +78,7 @@ export class StockfishEngine {
|
|||||||
}, 5000);
|
}, 5000);
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("Stockfish engine ready");
|
if (this.ready) console.log("Stockfish engine ready");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Failed to initialize Stockfish:", err);
|
console.error("Failed to initialize Stockfish:", err);
|
||||||
}
|
}
|
||||||
@@ -81,7 +87,7 @@ export class StockfishEngine {
|
|||||||
async getBestMove(fen: string, level: StockfishLevel): Promise<string> {
|
async getBestMove(fen: string, level: StockfishLevel): Promise<string> {
|
||||||
await this.initPromise;
|
await this.initPromise;
|
||||||
|
|
||||||
if (!this.engine || !this.ready) {
|
if (!this.process || !this.ready) {
|
||||||
console.warn("Stockfish not ready");
|
console.warn("Stockfish not ready");
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
@@ -89,12 +95,11 @@ export class StockfishEngine {
|
|||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
this.resolvers.push(resolve);
|
this.resolvers.push(resolve);
|
||||||
|
|
||||||
this.engine!.postMessage("ucinewgame");
|
this.process!.stdin!.write("ucinewgame\n");
|
||||||
this.engine!.postMessage(`setoption name Skill Level value ${level.skillLevel}`);
|
this.process!.stdin!.write(`setoption name Skill Level value ${level.skillLevel}\n`);
|
||||||
this.engine!.postMessage(`position fen ${fen}`);
|
this.process!.stdin!.write(`position fen ${fen}\n`);
|
||||||
this.engine!.postMessage(`go depth ${level.depth} movetime ${level.moveTime}`);
|
this.process!.stdin!.write(`go depth ${level.depth} movetime ${level.moveTime}\n`);
|
||||||
|
|
||||||
// Safety timeout
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const idx = this.resolvers.indexOf(resolve);
|
const idx = this.resolvers.indexOf(resolve);
|
||||||
if (idx !== -1) {
|
if (idx !== -1) {
|
||||||
@@ -106,12 +111,11 @@ export class StockfishEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
this.engine?.terminate?.();
|
this.process?.kill();
|
||||||
this.engine = null;
|
this.process = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Singleton
|
|
||||||
let engineInstance: StockfishEngine | null = null;
|
let engineInstance: StockfishEngine | null = null;
|
||||||
|
|
||||||
export const getEngine = (): StockfishEngine => {
|
export const getEngine = (): StockfishEngine => {
|
||||||
|
|||||||
Reference in New Issue
Block a user