ChessBuilder/Assets/ChessEngines/fairy-chess-server/index.js
2025-03-18 09:55:17 -05:00

408 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';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import os from 'os';
function getLogPath() {
if (process.platform === 'linux') {
// Use ~/.local/share/ChessBuilder/logs
return path.join(os.homedir(), '.local', 'share', 'ChessBuilder', 'logs');
} else if (process.platform === 'win32') {
// Use %APPDATA%\ChessBuilder\logs
return path.join(os.homedir(), 'AppData', 'Roaming', 'ChessBuilder', 'logs');
} else {
// macOS: ~/Library/Logs/ChessBuilder
return path.join(os.homedir(), 'Library', 'ChessBuilder', 'logs');
}
}
// Ensure log directory exists
const logDir = getLogPath();
fs.mkdirSync(logDir, { recursive: true });
const logFile = path.join(logDir, 'chess-server.log');
console.log(`Logging to: ${logFile}`);
// Create a write stream for logging
const logStream = fs.createWriteStream(logFile, { flags: 'a' });
// Redirect console.log and console.error to both console and file
const originalConsoleLog = console.log;
const originalConsoleError = console.error;
console.log = (...args) => {
const message = args.map(arg =>
typeof arg === 'object' ? JSON.stringify(arg) : arg
).join(' ') + '\n';
logStream.write(`[${new Date().toISOString()}] ${message}`);
originalConsoleLog.apply(console, args);
};
console.error = (...args) => {
const message = args.map(arg =>
typeof arg === 'object' ? JSON.stringify(arg) : arg
).join(' ') + '\n';
logStream.write(`[${new Date().toISOString()}] ERROR: ${message}`);
originalConsoleError.apply(console, args);
};
try {
fs.writeFileSync(logFile, `Server started at ${new Date().toISOString()}\n`, { flag: 'a' });
fs.writeFileSync(logFile, `Process ID: ${process.pid}\n`, { flag: 'a' });
fs.writeFileSync(logFile, `Working directory: ${process.cwd()}\n`, { flag: 'a' });
fs.writeFileSync(logFile, `Node version: ${process.version}\n`, { flag: 'a' });
} catch (error) {
console.error('Failed to write startup log:', error);
}
const app = express();
const port = 27531;
let engine = null;
let isReady = false;
let lastResponse = null
const SERVER_WAIT_THRESHOLD = 1 * 60 * 1000;
const CHECK_INTERVAL = 5000;
// Initialize ffish and engine
ffish.onRuntimeInitialized = async () => {
try {
isReady = true;
console.log('Fairy-Chess library ready asd');
// Get and verify engine path
const enginePath = getEnginePath();
console.log('ENGINE PATH', enginePath);
// Initialize Stockfish
fs.chmodSync(enginePath, '755');
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');
engine.sendCommand('uci');
}
} catch (error) {
console.log('Initialization error:', error);
}
};
app.use(express.json());
// Health check endpoint
app.get('/health', (req, res) => {
console.log("@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@")
console.log("@@@@@@@@@@@@@@@@@HEALTH CHECK@@@@@@@@@@@@@@@@@@@@@@")
console.log("@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@")
console.log(JSON.stringify(ffish.variants()))
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 {
const tempBoard = new ffish.Board(variant);
const isValid = tempBoard.setFen(fen);
tempBoard.delete();
res.json({
status: 'ok',
isValid,
startingFen: ffish.startingFen(variant)
});
} catch (error) {
res.status(400).json({ error: 'Invalid FEN or variant' });
}
});
// New game endpoint
app.post('/new', async (req, res) => {
lastResponse = new Date().getTime()
const { variant = 'chess' } = req.body;
try {
const engineAnalysis = await engine.createNewGame();
res.json({
status: 'ok',
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Set position endpoint
app.post('/position', async(req, res) => {
lastResponse = new Date().getTime()
const { fen, variant = 'chess', start } = req.body;
try {
//we have a lot of funky rules lets not validate
// const isValid = ffish.validateFen(fen) == 1
if(start){
await engine.startPos()
engine.sendCommand('d');
}else if(fen){
await engine.setBoardPosition(fen)
}
res.json({
status: 'ok',
msg: "Set Position",
});
} catch (error) {
res.status(500).json({ error: error.message, poserr: true });
}
});
// Make move endpoint
app.post('/move', (req, res) => {
lastResponse = new Date().getTime()
const { move, notation = 'uci', variant = 'chess' } = req.body;
if (!move) {
return res.status(400).json({ error: 'Move required' });
}
try {
const response = {
status: 'ok',
};
res.json(response);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// State endpoint
app.get('/state', (req, res) => {
lastResponse = new Date().getTime()
try {
res.json({
status: 'ok',
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});
/*
createNewGame
*/
app.post('/newgame', async (req, res) => {
lastResponse = new Date().getTime()
try {
const response = {
status: 'ok',
};
// If engine is available, get engine analysis
if (engine && engine.isReady) {
const engineAnalysis = await engine.createNewGame();
}
res.json(response);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Analysis endpoint
app.post('/analyze', async (req, res) => {
lastResponse = new Date().getTime()
try {
const { depth = 15, movetime = 1000 } = req.body;
// Get basic position analysis
const positionAnalysis = {
status: 'ok',
};
// If engine is available, get engine analysis
if (engine && engine.isReady) {
const engineAnalysis = await engine.getAnalysis("", {
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 (!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,
fen
} = req.body;
try {
const analysis = await engine.getAnalysis(fen, {
depth,
movetime,
nodes
});
res.json({
status: 'ok',
move: analysis.bestMove,
analysis: analysis,
});
} 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()
logStream.end();
});
process.on('SIGINT', () => {
console.log('Received SIGINT, shutting down...');
closeServer()
logStream.end();
});
app.listen(port, () => {
lastResponse = new Date().getTime()
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...`);
console.log(currentTime, lastResponse, timeSinceLastResponse, SERVER_WAIT_THRESHOLD)
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 (engine) engine.quit();
process.exit(0);
}