UNPKG

@chess-labs/terminal

Version:

A command-line chess game powered by Stockfish

215 lines 7.84 kB
import { spawn } from 'node:child_process'; import { EventEmitter } from 'node:events'; import chalk from 'chalk'; export class StockfishEngine extends EventEmitter { config; process = null; isReady = false; options = new Map(); isRestarting = false; constructor(config) { super(); this.config = config; } async start() { return new Promise((resolve, reject) => { try { // Start Stockfish process this.process = spawn(this.config.enginePath, [], { stdio: ['pipe', 'pipe', 'pipe'], }); if (!this.process.stdout || !this.process.stdin) { throw new Error('Failed to start engine process'); } // Handle engine output this.process.stdout.on('data', (data) => { const lines = data.toString().trim().split('\n'); for (const line of lines) { this.handleEngineOutput(line); } }); // Handle errors this.process.stderr?.on('data', (data) => { console.error('Engine error:', data.toString()); }); this.process.on('exit', (code) => { console.log(`Engine exited with code ${code}`); this.isReady = false; // Attempt to restart engine if it wasn't intentionally stopped if (!this.isRestarting && code !== 0) { console.log(chalk.yellow('🔄 Attempting to restart engine...')); this.attemptRestart(); } }); // Initialize UCI protocol this.once('ready', () => { this.configureEngine(); resolve(); }); this.sendCommand('uci'); // Timeout after 10 seconds setTimeout(() => { if (!this.isReady) { reject(new Error('Engine initialization timeout')); } }, 10000); } catch (error) { reject(error); } }); } handleEngineOutput(line) { const trimmed = line.trim(); if (trimmed.startsWith('id name')) { console.log(`Engine: ${trimmed.substring(8)}`); } else if (trimmed.startsWith('id author')) { console.log(`Author: ${trimmed.substring(10)}`); } else if (trimmed.startsWith('option name')) { this.parseOption(trimmed); } else if (trimmed === 'uciok') { this.sendCommand('isready'); } else if (trimmed === 'readyok') { this.isReady = true; this.emit('ready'); } else if (trimmed.startsWith('bestmove')) { const move = trimmed.split(' ')[1]; this.emit('bestmove', move); } else if (trimmed.startsWith('info')) { this.emit('info', trimmed); } } parseOption(line) { // Parse UCI option format: "option name <name> type <type> [default <value>] [min <value>] [max <value>] [var <value>]*" const parts = line.split(' '); let i = 2; // Skip 'option name' const option = {}; // Get option name const nameEnd = parts.findIndex((part, index) => index > i && part === 'type'); option.name = parts.slice(i, nameEnd).join(' '); i = nameEnd + 1; option.type = parts[i]; i++; // Parse additional parameters while (i < parts.length) { const param = parts[i]; if (param === 'default') { option.default = parts[++i]; } else if (param === 'min') { option.min = Number.parseInt(parts[++i]); } else if (param === 'max') { option.max = Number.parseInt(parts[++i]); } else if (param === 'var') { if (!option.var) option.var = []; option.var.push(parts[++i]); } i++; } if (option.name && option.type) { this.options.set(option.name, option); } } configureEngine() { // Set skill level if provided if (this.config.skillLevel !== undefined) { if (this.options.has('Skill Level')) { this.sendCommand(`setoption name Skill Level value ${this.config.skillLevel}`); } } // Set UCI_LimitStrength for weaker play if (this.config.skillLevel && this.config.skillLevel < 20) { if (this.options.has('UCI_LimitStrength')) { this.sendCommand('setoption name UCI_LimitStrength value true'); } } } async attemptRestart() { if (this.isRestarting) return; this.isRestarting = true; try { // Wait a bit before restarting await new Promise((resolve) => setTimeout(resolve, 1000)); // Clean up current process this.process = null; this.isReady = false; this.options.clear(); // Try to restart await this.start(); console.log(chalk.green('✅ Engine restarted successfully')); } catch (error) { console.error(chalk.red('❌ Failed to restart engine:'), error); this.emit('restart-failed', error); } finally { this.isRestarting = false; } } async getBestMove(position, timeLimit) { // If engine is not ready, try to restart once if (!this.isReady && !this.isRestarting) { console.log(chalk.yellow('⚠️ Engine not ready, attempting restart...')); try { await this.attemptRestart(); } catch (error) { throw new Error('Engine failed to restart and is not available'); } } // If still not ready after restart attempt, fail if (!this.isReady) { throw new Error('Engine not ready and restart failed'); } return new Promise((resolve, reject) => { const timeout = setTimeout(() => { reject(new Error('Move calculation timeout')); }, (timeLimit || this.config.timeLimit || 5000) + 1000); this.once('bestmove', (move) => { clearTimeout(timeout); resolve(move); }); // Send position and search command this.sendCommand(`position fen ${position}`); if (this.config.depth) { this.sendCommand(`go depth ${this.config.depth}`); } else if (timeLimit || this.config.timeLimit) { this.sendCommand(`go movetime ${timeLimit || this.config.timeLimit}`); } else { this.sendCommand('go depth 10'); // Default depth } }); } sendCommand(command) { if (!this.process?.stdin) { throw new Error('Engine process not available'); } console.log(`> ${command}`); this.process.stdin.write(`${command}\n`); } stop() { this.isRestarting = true; // Prevent auto-restart if (this.process) { this.sendCommand('quit'); this.process.kill(); this.process = null; this.isReady = false; } } getAvailableOptions() { return new Map(this.options); } } //# sourceMappingURL=engine.js.map