Files
ai.interactive.fiction/dist/server-zork.js
T

343 lines
13 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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");
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 = 10;
const DEBUG_ENABLED = /^(1|true|yes|on)$/i.test(process.env.ZORK_DEBUG ?? '');
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');
},
}));
// One engine instance per connected socket
const sessions = new Map();
// Save-game slot maps: socketId → Map<slotNumber, serialisedJson>
const saveSlots = new Map();
function toLegacyNarrative(turn) {
const text = (turn.paragraphs ?? [])
.map((p) => String(p?.text ?? '').trim())
.filter(Boolean)
.join('\n\n');
return {
text,
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();
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', toLegacyNarrative(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', toLegacyNarrative(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 = path_1.default.resolve(process.env.ZORK_STORY_FILE ?? './data/z-code/zork1.bin');
const promptDir = path_1.default.resolve('./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.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', toLegacyNarrative(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 });
}
}
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.listen(port, () => {
console.log(`[zork] Zork Narrator server running on http://localhost:${port}`);
resolve();
});
server.on('error', (err) => {
if (err.code === 'EADDRINUSE') {
console.log(`Port ${port} in use, trying ${port + 1}`);
server.close();
port++;
reject();
}
else {
reject(err);
}
});
});
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