"use strict"; /** * Zork LLM Server * * Starts an Express + Socket.IO server that runs Zork I through the * ZorkLlmEngine and serves the same shared client UI as the YAML engine. * * Usage: * npm run dev:zork (development, with file watching) * npm run start:zork (production, from compiled dist/) * * Environment variables: * PORT – HTTP port (default: 3002) * ZORK_STORY_FILE – path to the story file (default: ./data/z-code/zork1.bin) * OPENROUTER_API_KEY, OPENROUTER_MODEL – required */ 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 }); const path_1 = __importDefault(require("path")); const http_1 = __importDefault(require("http")); const express_1 = __importDefault(require("express")); const socket_io_1 = require("socket.io"); const dotenv = __importStar(require("dotenv")); const fs_1 = require("fs"); const zork_llm_engine_1 = require("./engine/zork-llm-engine"); const game_config_1 = require("./config/game-config"); dotenv.config(); const app = (0, express_1.default)(); const server = http_1.default.createServer(app); const io = new socket_io_1.Server(server); const DEFAULT_PORT = 3002; const PORT = process.env.PORT ? parseInt(process.env.PORT, 10) : DEFAULT_PORT; const PORT_RANGE = 300; const DEBUG_ENABLED = /^(1|true|yes|on)$/i.test(process.env.ZORK_DEBUG ?? ''); const engineConfig = (0, game_config_1.loadGameConfig)(process.env.ZORK_CONFIG_FILE || './config/engines/zork.json', 'zork'); function debugLog(message, details) { if (!DEBUG_ENABLED) return; if (typeof details === 'undefined') { console.log(`[zork:debug] ${message}`); return; } console.log(`[zork:debug] ${message}`, details); } // Serve the same shared client UI 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)); }); // One engine instance per connected socket const sessions = new Map(); // Save-game slot maps: socketId → Map const saveSlots = new Map(); function toClientTurn(turn) { return { ...turn, gameState: { ...turn.gameState, currentRoomId: turn.gameState?.statusLine, statusLine: turn.gameState?.statusLine, }, }; } function normalizeSaveSlot(slot) { const n = Number(slot); return Number.isInteger(n) && n > 0 ? n : 1; } function getOrCreateEngine(socketId) { let engine = sessions.get(socketId); if (!engine) { engine = new zork_llm_engine_1.ZorkLlmEngine({ storyPath: (0, game_config_1.projectPath)(process.env.ZORK_STORY_FILE || engineConfig.paths.mainGameFile), promptDir: (0, game_config_1.projectPath)(engineConfig.paths.promptDir || 'data/zork-prompts'), }); sessions.set(socketId, engine); } return engine; } function getSlots(socketId) { let slots = saveSlots.get(socketId); if (!slots) { slots = new Map(); saveSlots.set(socketId, slots); } return slots; } async function handleGameApi(socket, method, args) { const slots = getSlots(socket.id); debugLog(`gameApi request from ${socket.id}: ${method}`, { args }); switch (method) { case 'newGame': case 'newGame()': { const engine = getOrCreateEngine(socket.id); const turn = await engine.newGame(); socket.emit('narrativeResponse', toClientTurn(turn)); return { success: true, result: true, running: true, canLoad: slots.size > 0, }; } case 'loadGame': case 'loadGame()': { const slot = normalizeSaveSlot(args[0]); if (!slots.has(slot)) { return { success: false, error: 'missing_save', result: false }; } const engine = getOrCreateEngine(socket.id); const turn = await engine.loadGame(slots.get(slot)); socket.emit('narrativeResponse', toClientTurn(turn)); socket.emit('gameLoaded', { slot }); return { success: true, result: true, running: true, slot }; } case 'saveGame': case 'saveGame()': { const engine = sessions.get(socket.id); if (!engine?.isRunning()) { return { success: false, error: 'game_not_running', result: false }; } const slot = normalizeSaveSlot(args[0]); const savedJson = await engine.saveGame(); slots.set(slot, savedJson); socket.emit('gameSaved', { slot }); return { success: true, result: true, slot }; } case 'hasSaveGame': case 'hasSaveGame()': { const slot = normalizeSaveSlot(args[0]); return { success: true, result: slots.has(slot), slot }; } case 'getSaveGames': case 'getSaveGames()': return { success: true, result: Array.from(slots.keys()).sort((a, b) => a - b), }; case 'isGameRunning': case 'isGameRunning()': return { success: true, result: sessions.get(socket.id)?.isRunning() ?? false, }; default: return { success: false, error: `unknown_method:${method}` }; } } function checkRuntimeConfiguration() { const storyPath = (0, game_config_1.projectPath)(process.env.ZORK_STORY_FILE ?? engineConfig.paths.mainGameFile); const promptDir = (0, game_config_1.projectPath)(engineConfig.paths.promptDir || 'data/zork-prompts'); const promptFiles = [ 'character-generation.yml', 'text-rewriter.yml', 'command-translator.yml', 'output-evaluator.yml', ]; const missingPrompts = promptFiles .map((file) => path_1.default.join(promptDir, file)) .filter((filePath) => !(0, fs_1.existsSync)(filePath)); if (!process.env.OPENROUTER_API_KEY) { console.error('[zork] Missing OPENROUTER_API_KEY in environment.'); } if (!process.env.OPENROUTER_MODEL) { console.error('[zork] Missing OPENROUTER_MODEL in environment.'); } if (!(0, fs_1.existsSync)(storyPath)) { console.error(`[zork] Story file missing: ${storyPath}`); console.error('[zork] Place zork1.bin in ./data/z-code/ or set ZORK_STORY_FILE.'); } if (missingPrompts.length > 0) { console.error('[zork] Missing prompt files:'); for (const filePath of missingPrompts) { console.error(` - ${filePath}`); } } debugLog('runtime configuration', { storyPath, promptDir, debug: DEBUG_ENABLED, hasApiKey: Boolean(process.env.OPENROUTER_API_KEY), model: process.env.OPENROUTER_MODEL ?? null, }); } io.on('connection', (socket) => { console.log(`[zork] Client connected: ${socket.id}`); socket.emit('gameConfig', (0, game_config_1.clientGameConfig)(engineConfig)); socket.on('gameApi', async (request, respond) => { try { const result = await handleGameApi(socket, String(request?.method ?? ''), Array.isArray(request?.args) ? request.args : []); debugLog(`gameApi response to ${socket.id}`, result); if (typeof respond === 'function') respond(result); } catch (error) { console.error('[zork] gameApi error:', error); if (typeof respond === 'function') { respond({ success: false, error: error instanceof Error ? error.message : String(error), }); } } }); socket.on('playerCommand', async (data) => { const engine = sessions.get(socket.id); if (!engine?.isRunning()) { socket.emit('error', { message: 'No active game. Start or load a game first.', }); return; } const input = String(data?.command ?? '').trim(); if (!input) return; debugLog(`playerCommand from ${socket.id}: ${input}`); try { const turn = await engine.processInput(input); debugLog(`narrativeResponse to ${socket.id}`, { inputMode: turn.inputMode, paragraphs: turn.paragraphs.length, statusLine: turn.gameState?.statusLine, }); socket.emit('narrativeResponse', toClientTurn(turn)); } catch (error) { console.error('[zork] playerCommand error:', error); socket.emit('error', { message: error instanceof Error ? error.message : 'An error occurred.', }); } }); socket.on('disconnect', () => { console.log(`[zork] Client disconnected: ${socket.id}`); sessions.delete(socket.id); saveSlots.delete(socket.id); }); }); // --------------------------------------------------------------------------- // Startup helpers // --------------------------------------------------------------------------- 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'), path_1.default.join(__dirname, '../data/z-code'), path_1.default.join(__dirname, '../data/zork-prompts'), ]; for (const dir of dirs) { if (!(0, fs_1.existsSync)(dir)) (0, fs_1.mkdirSync)(dir, { recursive: true }); } (0, game_config_1.ensureConfiguredAssetDirectories)(engineConfig); } function ensureKokoroJs() { const src = path_1.default.join(__dirname, '../node_modules/kokoro-js/dist/index.js'); const dst = path_1.default.join(__dirname, '../public/js/kokoro-js.js'); if ((0, fs_1.existsSync)(src) && !(0, fs_1.existsSync)(dst)) (0, fs_1.copyFileSync)(src, dst); } async function startServer(initialPort, range) { ensureDirectories(); try { ensureKokoroJs(); } catch { /* optional */ } checkRuntimeConfiguration(); let port = initialPort; while (port < initialPort + range) { try { await new Promise((resolve, reject) => { server.removeAllListeners('error'); server.removeAllListeners('listening'); server.once('listening', () => { console.log(`[zork] Zork Narrator server running on http://localhost:${port}`); resolve(); }); server.once('error', (err) => { if (err.code === 'EADDRINUSE' || err.code === 'EACCES') { console.log(`Port ${port} unavailable (${err.code}), trying ${port + 1}...`); server.close(); port++; reject(); } else { reject(err); } }); server.listen(port); }); return; } catch { if (port >= initialPort + range - 1) { throw new Error(`Failed to start server on ports ${initialPort}–${initialPort + range - 1}`); } } } } if (require.main === module) { startServer(PORT, PORT_RANGE).catch((err) => { console.error('[zork] Failed to start:', err); process.exit(1); }); } //# sourceMappingURL=server-zork.js.map