import express from 'express'; import ffish from 'ffish'; import ChessEngine from './engine.js'; import { getEnginePath, verifyEnginePath } from './engine-path.js'; const app = express(); const port = 27531; let board = null; let engine = null; let isReady = false; // Initialize ffish and engine ffish.onRuntimeInitialized = async () => { try { isReady = true; console.log('Fairy-Chess library ready'); // Get and verify engine path const enginePath = getEnginePath(); // Initialize Stockfish engine = new ChessEngine(enginePath); await engine.start(); console.log('Chess engine initialized at:', enginePath, engine.isReady); // Set initial engine options if (engine.isReady) { engine.sendCommand('setoption name Threads value 4'); engine.sendCommand('setoption name Hash value 128'); engine.sendCommand('setoption name MultiPV value 1'); } } catch (error) { console.error('Initialization error:', error); } }; app.use(express.json()); // Health check endpoint app.get('/health', (req, res) => { res.json({ status: 'ok', engineReady: isReady && engine && engine.isReady, enginePath: getEnginePath(), platform: process.platform, variants: ffish.variants() }); }); // Validate FEN endpoint app.post('/validate', (req, res) => { const { fen, variant = 'chess' } = req.body; if (!fen) { return res.status(400).json({ error: 'FEN string required' }); } try { // Create temporary board to validate FEN const tempBoard = new ffish.Board(variant); const isValid = tempBoard.setFen(fen); tempBoard.delete(); res.json({ isValid, startingFen: ffish.startingFen(variant) }); } catch (error) { res.status(400).json({ error: 'Invalid FEN or variant' }); } }); // New game endpoint app.post('/new', (req, res) => { const { variant = 'chess' } = req.body; try { if (board) { board.delete(); } board = new ffish.Board(variant); res.json({ status: 'ok', fen: board.fen(), legalMoves: board.legalMoves().split(' '), legalMovesSan: board.legalMovesSan().split(' '), isCheck: board.isCheck(), turn: board.turn(), moveStack: board.moveStack() }); } catch (error) { res.status(500).json({ error: error.message }); } }); // Set position endpoint app.post('/position', (req, res) => { const { fen, variant = 'chess' } = req.body; if (!fen) { return res.status(400).json({ error: 'FEN string required' }); } try { //we have a lot of funky rules lets not validate // const isValid = ffish.validateFen(fen) == 1 // if (!isValid) { // return res.status(400).json({ error: 'Invalid FEN string', // fen, // variant, // boardExists: !!board, reqBody: JSON.stringify(req.body) }); // }else { // // if (board) { // // board.delete(); // // } // board.setFen(fen); // } board.setFen(fen); res.json({ status: 'ok', msg: "Set Position", legalMoves: board.legalMoves().split(' '), legalMovesSan: board.legalMovesSan().split(' '), isCheck: board.isCheck(), isGameOver: board.isGameOver(), result: board.result(), turn: board.turn(), moveStack: board.moveStack() }); } catch (error) { res.status(500).json({ error: error.message }); } }); // Make move endpoint app.post('/move', (req, res) => { const { move, notation = 'uci', variant = 'chess' } = req.body; if (!board) { return res.status(400).json({ error: 'No active board' }); } if (!move) { return res.status(400).json({ error: 'Move required' }); } if (board) { board.delete(); } board = new ffish.Board(variant); try { if (notation === 'san') { board.pushSan(move); } else { board.push(move); } const response = { status: 'ok', fen: board.fen(), legalMoves: board.legalMoves().split(' '), legalMovesSan: board.legalMovesSan().split(' '), isCheck: board.isCheck(), isGameOver: board.isGameOver(), result: board.result(), turn: board.turn(), moveStack: board.moveStack(), fullmoveNumber: board.fullmoveNumber(), halfmoveClock: board.halfmoveClock() }; if (board.isGameOver()) { response.hasInsufficientMaterial = { white: board.hasInsufficientMaterial(true), // true for white black: board.hasInsufficientMaterial(false) // false for black }; response.gameResult = board.result(); } res.json(response); } catch (error) { res.status(500).json({ error: error.message }); } }); // State endpoint app.get('/state', (req, res) => { if (!board) { return res.status(400).json({ error: 'No active board' }); } try { res.json({ fen: board.fen(), legalMoves: board.legalMoves().split(' '), legalMovesSan: board.legalMovesSan().split(' '), isCheck: board.isCheck(), isGameOver: board.isGameOver(), result: board.result(), turn: board.turn(), moveStack: board.moveStack(), fullmoveNumber: board.fullmoveNumber(), halfmoveClock: board.halfmoveClock(), variant: board.variant(), is960: board.is960() }); } catch (error) { res.status(500).json({ error: error.message }); } }); // Analysis endpoint app.post('/analyze', async (req, res) => { if (!board) { return res.status(400).json({ error: 'No active board' }); } try { const { depth = 15, movetime = 1000 } = req.body; // Get basic position analysis const positionAnalysis = { fen: board.fen(), isCheck: board.isCheck(), isGameOver: board.isGameOver(), result: board.result(), hasInsufficientMaterial: { white: board.hasInsufficientMaterial(true), // true for white black: board.hasInsufficientMaterial(false) // false for black }, legalMoves: board.legalMoves().split(' '), legalMovesSan: board.legalMovesSan().split(' '), moveStack: board.moveStack() }; // If engine is available, get engine analysis if (engine && engine.isReady) { const engineAnalysis = await engine.getAnalysis(board.fen(), { depth, movetime }); positionAnalysis.engineAnalysis = engineAnalysis; } res.json(positionAnalysis); } catch (error) { res.status(500).json({ error: error.message }); } }); // Engine move endpoint app.post('/enginemove', async (req, res) => { if (!board) { return res.status(400).json({ error: 'No active board' }); } if (!engine || !engine.isReady) { return res.status(503).json({ error: 'Engine not available', details: 'Chess engine is not initialized or not ready' }); } const { depth = 15, movetime = 1000, nodes = null } = req.body; try { const fen = board.fen(); const analysis = await engine.getAnalysis(fen, { depth, movetime, nodes }); if (analysis.bestMove) { board.push(analysis.bestMove); } res.json({ status: 'ok', move: analysis.bestMove, analysis: analysis, fen: board.fen(), legalMoves: board.legalMoves().split(' '), isCheck: board.isCheck(), isGameOver: board.isGameOver(), turn: board.turn() }); } catch (error) { res.status(500).json({ error: 'Engine analysis failed', details: error.message }); } }); // Cleanup handling process.on('SIGTERM', () => { console.log('Shutting down...'); if (board) board.delete(); if (engine) engine.quit(); process.exit(0); }); process.on('SIGINT', () => { console.log('Received SIGINT, shutting down...'); if (board) board.delete(); if (engine) engine.quit(); process.exit(0); }); app.listen(port, () => { console.log(`Fairy-Chess server running on port ${port}`); }); app.post('/shutdown', (req, res) => { res.json({ status: 'shutting_down' }); // Give time for response to be sent setTimeout(() => { if (board) board.delete(); if (engine) engine.quit(); process.exit(0); }, 100); });