UNPKG

@chessle/chess.js-extended

Version:

A powerful browser-only chess engine package that bundles Stockfish for position evaluation and analysis on top of chess.js

196 lines 8.85 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __exportStar = (this && this.__exportStar) || function(m, exports) { for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.ChessEngine = void 0; const chess_js_1 = require("chess.js"); const stockfish_bundle_1 = require("./stockfish-bundle"); __exportStar(require("chess.js"), exports); class ChessEngine extends chess_js_1.Chess { constructor() { super(); this.evaluation = 0; this.bestMove = undefined; this.suggestedLine = []; } async evaluatePosition(options = {}) { // Check if we're in a browser environment if (typeof window === "undefined" || typeof Worker === "undefined") { throw new Error("ChessEngine requires a browser environment with Web Worker support"); } return new Promise((resolve, reject) => { let stockfishWorkerUrl; // If a custom URL is provided, use it if (options.stockfishUrl) { stockfishWorkerUrl = options.stockfishUrl; } else { // Use bundled Stockfish files const wasmSupported = typeof WebAssembly === "object" && WebAssembly.validate(Uint8Array.of(0x0, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00)); const stockfishCode = wasmSupported ? (0, stockfish_bundle_1.getStockfishWasmJs)() : (0, stockfish_bundle_1.getStockfishJs)(); // Create a blob URL from the bundled code const blob = new Blob([stockfishCode], { type: "application/javascript", }); stockfishWorkerUrl = URL.createObjectURL(blob); } const stockfish = new Worker(stockfishWorkerUrl); // Declare timeout variable let timeoutId; const cleanup = () => { stockfish.terminate(); if (timeoutId) { clearTimeout(timeoutId); } // Clean up blob URL if we created one if (!options.stockfishUrl && stockfishWorkerUrl.startsWith("blob:")) { URL.revokeObjectURL(stockfishWorkerUrl); } }; // Add error handling for worker stockfish.onerror = (error) => { console.error("Stockfish worker error:", error); cleanup(); reject(new Error(`Stockfish worker failed to load. ${error.message || "Check browser console for details."}`)); }; // Set a timeout to prevent hanging (default 30 seconds) const timeoutMs = options.movetime || 30000; timeoutId = setTimeout(() => { console.warn(`Stockfish evaluation timed out after ${timeoutMs}ms, returning default evaluation`); cleanup(); // Return default evaluation instead of throwing an error const defaultResult = { evaluation: 0, bestMove: undefined, suggestedLine: [], }; this.suggestedLine = defaultResult.suggestedLine; this.evaluation = defaultResult.evaluation; this.bestMove = defaultResult.bestMove; resolve(defaultResult); }, timeoutMs); // Extract side to move from FEN const sideToMove = this.fen().split(" ")[1]; // Keep track of the latest evaluation results let result = { evaluation: 0, bestMove: undefined, suggestedLine: [], }; // Keep track of the latest line while analyzing let latestLine = []; stockfish.addEventListener("message", (e) => { const message = e.data; // Parse multipv lines const multipvMatch = message.match(/multipv\s+1.*score (cp|mate) (-?\d+).*pv\s+(.+)/); if (multipvMatch) { const evalType = multipvMatch[1]; const evalValue = parseInt(multipvMatch[2], 10); const pvLine = multipvMatch[3].trim().split(/\s+/); if (pvLine.length > 0) { // Just update the latest line, but don't set it as the final line yet latestLine = this.convertMovesToSAN(pvLine, this.fen()); // Calculate evaluation let adjustedEval; if (evalType === "cp") { adjustedEval = sideToMove === "b" ? -evalValue / 100 : evalValue / 100; } else { // Handle mate scores with proper notation const mateInMoves = Math.abs(evalValue); if (evalValue > 0) { // Mate for current side to move adjustedEval = sideToMove === "w" ? `M${mateInMoves}` : `M-${mateInMoves}`; } else { // Mate against current side to move adjustedEval = sideToMove === "w" ? `M-${mateInMoves}` : `M${mateInMoves}`; } } result.evaluation = adjustedEval; } } // Parse best move - this indicates Stockfish has finished analyzing const bestMove = message.match(/bestmove\s+(\S+)/)?.[1]; if (bestMove) { result.bestMove = bestMove; // Now that Stockfish is done, set the final line if (latestLine.length > 0) { result.suggestedLine = latestLine; } cleanup(); this.suggestedLine = result.suggestedLine; this.evaluation = result.evaluation; this.bestMove = result.bestMove; resolve(result); } }); // Send commands to stockfish try { stockfish.postMessage("uci"); stockfish.postMessage("setoption name MultiPV value 1"); stockfish.postMessage(`position fen ${this.fen()}`); // Build the go command with options let goCommand = "go"; if (options.depth !== undefined) { goCommand += ` depth ${options.depth}`; } else if (options.time !== undefined) { goCommand += ` wtime ${options.time} btime ${options.time}`; } else if (options.movetime !== undefined) { goCommand += ` movetime ${options.movetime}`; } else if (options.nodes !== undefined) { goCommand += ` nodes ${options.nodes}`; } else { // Default to depth 15 if no options provided goCommand += " depth 15"; } stockfish.postMessage(goCommand); } catch (error) { cleanup(); reject(new Error(`Failed to send commands to Stockfish: ${error}`)); } }); } convertMovesToSAN(moves, position) { const chess = new chess_js_1.Chess(position); const convertedMoves = []; for (const move of moves) { try { // Make the move and get its SAN notation const moveObj = chess.move(move); if (moveObj) { convertedMoves.push(moveObj.san); } } catch (error) { break; } } return convertedMoves; } } exports.ChessEngine = ChessEngine; //# sourceMappingURL=index.js.map