"use strict"; /** * AI Interactive Fiction - Web Server * Serves the web UI and handles WebSocket communication */ 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 __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.io = exports.server = exports.app = void 0; exports.startServer = startServer; const path_1 = __importDefault(require("path")); const express_1 = __importDefault(require("express")); const http_1 = __importDefault(require("http")); const socket_io_1 = require("socket.io"); const dotenv = __importStar(require("dotenv")); const game_runner_1 = require("./cli/game-runner"); const fs_1 = require("fs"); const turn_result_1 = require("./interfaces/turn-result"); const game_config_1 = require("./config/game-config"); // Load environment variables dotenv.config(); // Create Express application const app = (0, express_1.default)(); exports.app = app; const server = http_1.default.createServer(app); exports.server = server; const io = new socket_io_1.Server(server); exports.io = io; // Get port from environment variables or use default const DEFAULT_PORT = 3001; const PORT = process.env.PORT ? parseInt(process.env.PORT) : DEFAULT_PORT; const PORT_RANGE = 300; // Try enough ports to skip OS-excluded ranges. const engineConfig = (0, game_config_1.loadGameConfig)(process.env.YAML_CONFIG_FILE || './config/engines/yaml.json', 'yaml'); // Serve static files from the public directory. During local development the // browser must not keep stale ES modules, otherwise UI fixes appear to do // nothing until a hard cache clear. app.use(express_1.default.static(path_1.default.join(__dirname, '../public'), { etag: false, lastModified: false, setHeaders: (res) => { res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate'); res.setHeader('Pragma', 'no-cache'); res.setHeader('Expires', '0'); } })); app.get('/api/game-config', (_req, res) => { res.json((0, game_config_1.clientGameConfig)(engineConfig)); }); // Set up game sessions const gameSessions = new Map(); const nextTurnIds = new Map(); function nextTurnId(socketId) { const current = nextTurnIds.get(socketId) || 1; nextTurnIds.set(socketId, current + 1); return current; } function createTextTurn(socketId, text, gameState = {}, suggestions) { const paragraphs = (0, turn_result_1.textToParagraphs)(text); return { turnId: nextTurnId(socketId), paragraphs, choices: [], inputMode: 'text', gameState, suggestions, }; } function normalizeSaveSlot(slot) { const value = Number(slot); return Number.isInteger(value) && value > 0 ? value : 1; } async function startDemoGameForSocket(socket) { nextTurnIds.set(socket.id, 1); const gameRunner = new game_runner_1.GameRunner(); const worldFile = (0, game_config_1.projectPath)(process.env.DEFAULT_WORLD_FILE || engineConfig.paths.mainGameFile); await gameRunner.initialize(worldFile); gameSessions.set(socket.id, gameRunner); const gameState = gameRunner.getGameState(); const paragraphs = [ ...(0, turn_result_1.textToParagraphs)(gameState.world.introduction), ...(0, turn_result_1.textToParagraphs)(gameRunner.getCurrentRoomDescription()), ]; socket.emit('narrativeResponse', { turnId: nextTurnId(socket.id), paragraphs, choices: [], inputMode: 'text', gameState: { currentRoomId: gameState.currentRoomId, }, }); return gameRunner; } async function handleGameApi(socket, method, args = []) { const saveGames = socket.data.saveGames || new Map(); socket.data.saveGames = saveGames; switch (method) { case 'newGame': case 'newGame()': await startDemoGameForSocket(socket); return { success: true, result: true, running: true, canLoad: saveGames.size > 0 }; case 'loadGame': case 'loadGame()': { const slot = normalizeSaveSlot(args[0]); if (!saveGames.has(slot)) { return { success: false, error: 'missing_save', result: false }; } await startDemoGameForSocket(socket); socket.emit('gameLoaded', { slot }); return { success: true, result: true, running: true, slot }; } case 'saveGame': case 'saveGame()': { const gameRunner = gameSessions.get(socket.id); if (!gameRunner) { return { success: false, error: 'game_not_running', result: false }; } const slot = normalizeSaveSlot(args[0]); saveGames.set(slot, gameRunner.getGameState()); socket.emit('gameSaved', { slot }); return { success: true, result: true, slot }; } case 'hasSaveGame': case 'hasSaveGame()': { const slot = normalizeSaveSlot(args[0]); return { success: true, result: saveGames.has(slot), slot }; } case 'getSaveGames': case 'getSaveGames()': return { success: true, result: Array.from(saveGames.keys()).sort((a, b) => a - b) }; case 'isGameRunning': case 'isGameRunning()': return { success: true, result: gameSessions.has(socket.id) }; default: return { success: false, error: `unknown_method:${method}` }; } } // Handle socket connections io.on('connection', (socket) => { console.log(`New client connected: ${socket.id}`); socket.emit('gameConfig', (0, game_config_1.clientGameConfig)(engineConfig)); socket.data.saveGames = new Map(); socket.on('gameApi', async (request, respond) => { try { const response = await handleGameApi(socket, String(request?.method || ''), Array.isArray(request?.args) ? request.args : []); if (typeof respond === 'function') { respond(response); } } catch (error) { console.error('Game API error:', error); if (typeof respond === 'function') { respond({ success: false, error: error instanceof Error ? error.message : String(error) }); } } }); // Start a new game socket.on('startGame', async () => { try { await handleGameApi(socket, 'newGame', []); } catch (error) { console.error('Error starting game:', error); socket.emit('error', { message: 'Failed to start game. Please try again.' }); } }); // Process player command socket.on('playerCommand', async (data) => { try { const gameRunner = gameSessions.get(socket.id); if (!gameRunner) { socket.emit('error', { message: 'Game session not found. Please start a new game.' }); return; } const command = String(data?.command || '').trim(); // During typography and animation work, mirror the command back through // the real socket path so the UI pipeline can be tested end to end. socket.emit('narrativeResponse', createTextTurn(socket.id, command, { currentRoomId: gameRunner.getGameState().currentRoomId }, gameRunner.getSuggestions())); } catch (error) { console.error('Error processing command:', error); socket.emit('error', { message: 'Failed to process command. Please try again.' }); } }); // Save game state socket.on('saveGame', () => { try { const gameRunner = gameSessions.get(socket.id); if (!gameRunner) { socket.emit('error', { message: 'Game session not found. Please start a new game.' }); return; } socket.data.saveGames.set(1, gameRunner.getGameState()); socket.emit('gameSaved'); } catch (error) { console.error('Error saving game:', error); socket.emit('error', { message: 'Failed to save game. Please try again.' }); } }); // Load game state socket.on('loadGame', async () => { try { const gameRunner = gameSessions.get(socket.id); if (!gameRunner) { socket.emit('error', { message: 'Game session not found. Please start a new game.' }); return; } // Check if there's a saved game if (!socket.data.saveGames?.has(1)) { socket.emit('error', { message: 'No saved game found.' }); return; } await handleGameApi(socket, 'loadGame', [1]); } catch (error) { console.error('Error loading game:', error); socket.emit('error', { message: 'Failed to load game. Please try again.' }); } }); // Handle disconnection socket.on('disconnect', () => { console.log(`Client disconnected: ${socket.id}`); // Clean up game session if (gameSessions.has(socket.id)) { gameSessions.delete(socket.id); } nextTurnIds.delete(socket.id); }); }); // Ensure required asset folders exist function ensureDirectories() { const dirs = [ path_1.default.join(__dirname, '../public'), path_1.default.join(__dirname, '../public/js'), path_1.default.join(__dirname, '../public/css'), path_1.default.join(__dirname, '../public/images'), path_1.default.join(__dirname, '../public/music'), path_1.default.join(__dirname, '../public/sounds'), path_1.default.join(__dirname, '../public/fonts') ]; for (const dir of dirs) { if (!(0, fs_1.existsSync)(dir)) { (0, fs_1.mkdirSync)(dir, { recursive: true }); } } (0, game_config_1.ensureConfiguredAssetDirectories)(engineConfig); } // Copy kokoro-js library from node_modules if not already present function ensureKokoroJs() { const source = path_1.default.join(__dirname, '../node_modules/kokoro-js/dist/index.js'); const destination = path_1.default.join(__dirname, '../public/js/kokoro-js.js'); if ((0, fs_1.existsSync)(source) && !(0, fs_1.existsSync)(destination)) { (0, fs_1.copyFileSync)(source, destination); console.log(`Copied kokoro-js from ${source} to ${destination}`); } } // Start the server with port fallback async function startServer(initialPort, range) { let currentPort = initialPort; const maxPort = initialPort + range; // Try ports in the specified range while (currentPort < maxPort) { try { // Ensure directories exist ensureDirectories(); // Ensure kokoro-js is copied try { ensureKokoroJs(); } catch (error) { console.error('Error copying kokoro-js:', error); } // Try to start the server on the current port await new Promise((resolve, reject) => { server.removeAllListeners('error'); server.removeAllListeners('listening'); server.once('listening', () => { console.log(`AI Interactive Fiction web server running on http://localhost:${currentPort}`); resolve(); }); server.once('error', (error) => { // If port is in use, try next port if (error.code === 'EADDRINUSE' || error.code === 'EACCES') { console.log(`Port ${currentPort} is unavailable (${error.code}), trying next port...`); server.close(); currentPort++; reject(); } else { // For other errors, log and reject console.error('Server error:', error); reject(error); } }); server.listen(currentPort); }); // If we reach here, server started successfully return; } catch (error) { // If we reach the max port and still fail, throw an error if (currentPort >= maxPort - 1) { throw new Error(`Failed to start server on ports ${initialPort} to ${maxPort - 1}`); } // Otherwise try the next port // The loop continues as the rejection above increments currentPort } } } // Start the server when this module is run directly if (require.main === module) { startServer(PORT, PORT_RANGE).catch(error => { console.error('Failed to start server:', error); process.exit(1); }); } //# sourceMappingURL=server.js.map