ChessBuilder/Assets/ChessEngines/fairy-chess-server/index.js

414 lines
No EOL
11 KiB
JavaScript

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;
let lastResponse = null
const SERVER_WAIT_THRESHOLD = 10 * 60 * 1000;
const CHECK_INTERVAL = 5000;
// 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');
engine.sendCommand('setoption name UCI_LimitStrength value true');
}
} catch (error) {
console.error('Initialization error:', error);
}
};
app.use(express.json());
// Health check endpoint
app.get('/health', (req, res) => {
lastResponse = new Date().getTime()
res.json({
status: 'ok',
engineReady: isReady && engine && engine.isReady,
enginePath: getEnginePath(),
platform: process.platform,
variants: ffish.variants()
});
});
// Set Options
app.post('/setoptions', (req, res) => {
lastResponse = new Date().getTime()
const { name, value, options } = req.body;
if(name && value !== null){
engine.sendCommand(`setoption name ${name} value ${value}`);
res.json({
status: 'ok',
success: name,
errs: null,
});
}else if(options && Array.isArray(options)){
let success = [];
let errs = [];
for(let x = 0; x < options.length; x++){
let option = options[x];
const {name, value} = option;
if(name && value !== null ){
engine.sendCommand(`setoption name ${name} value ${value}`);
success.push(name)
}else {
errs.push(name)
}
}
res.json({
status: 'ok',
success: success,
errs: errs,
});
}else {
return res.status(400).json({ error: 'No Params Provided' });
}
});
// Validate FEN endpoint
app.post('/validate', (req, res) => {
lastResponse = new Date().getTime()
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) => {
lastResponse = new Date().getTime()
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) => {
lastResponse = new Date().getTime()
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) => {
lastResponse = new Date().getTime()
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) => {
lastResponse = new Date().getTime()
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) => {
lastResponse = new Date().getTime()
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) => {
lastResponse = new Date().getTime()
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
});
}
});
app.post('/shutdown', (req, res) => {
lastResponse = new Date().getTime()
res.json({ status: 'shutting_down' });
// Give time for response to be sent
setTimeout(() => {
closeServer()
}, 100);
});
// Cleanup handling
process.on('SIGTERM', () => {
console.log('Shutting down...');
closeServer()
});
process.on('SIGINT', () => {
console.log('Received SIGINT, shutting down...');
closeServer()
});
app.listen(port, () => {
startIdleMonitor();
console.log(`Fairy-Chess server running on port ${port}`);
});
function startIdleMonitor() {
const checkIdle = () => {
const currentTime = new Date().getTime();
const timeSinceLastResponse = currentTime - lastResponse;
if (timeSinceLastResponse > SERVER_WAIT_THRESHOLD) {
console.log(`Server idle for ${timeSinceLastResponse/1000} seconds. Shutting down...`);
closeServer();
}
};
// Start the monitoring interval
const monitorInterval = setInterval(checkIdle, CHECK_INTERVAL);
// Clean up interval on server close
process.on('SIGTERM', () => {
clearInterval(monitorInterval);
closeServer();
});
process.on('SIGINT', () => {
clearInterval(monitorInterval);
closeServer();
});
}
function closeServer(){
if (board) board.delete();
if (engine) engine.quit();
process.exit(0);
}