Add ink integration UI and media playback
This commit is contained in:
+36
-23
@@ -21,6 +21,12 @@ import { Server as SocketIOServer } from 'socket.io';
|
||||
import * as dotenv from 'dotenv';
|
||||
import { existsSync, mkdirSync, copyFileSync } from 'fs';
|
||||
import { ZorkLlmEngine, ZorkTurnResult } from './engine/zork-llm-engine';
|
||||
import {
|
||||
clientGameConfig,
|
||||
ensureConfiguredAssetDirectories,
|
||||
loadGameConfig,
|
||||
projectPath,
|
||||
} from './config/game-config';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
@@ -30,8 +36,12 @@ const io = new SocketIOServer(server);
|
||||
|
||||
const DEFAULT_PORT = 3002;
|
||||
const PORT = process.env.PORT ? parseInt(process.env.PORT, 10) : DEFAULT_PORT;
|
||||
const PORT_RANGE = 10;
|
||||
const PORT_RANGE = 300;
|
||||
const DEBUG_ENABLED = /^(1|true|yes|on)$/i.test(process.env.ZORK_DEBUG ?? '');
|
||||
const engineConfig = loadGameConfig(
|
||||
process.env.ZORK_CONFIG_FILE || './config/engines/zork.json',
|
||||
'zork',
|
||||
);
|
||||
|
||||
function debugLog(message: string, details?: unknown): void {
|
||||
if (!DEBUG_ENABLED) return;
|
||||
@@ -58,23 +68,20 @@ app.use(
|
||||
}),
|
||||
);
|
||||
|
||||
app.get('/api/game-config', (_req, res) => {
|
||||
res.json(clientGameConfig(engineConfig));
|
||||
});
|
||||
|
||||
// One engine instance per connected socket
|
||||
const sessions = new Map<string, ZorkLlmEngine>();
|
||||
// Save-game slot maps: socketId → Map<slotNumber, serialisedJson>
|
||||
const saveSlots = new Map<string, Map<number, string>>();
|
||||
|
||||
function toLegacyNarrative(turn: ZorkTurnResult): {
|
||||
text: string;
|
||||
gameState: { currentRoomId?: string; statusLine?: string };
|
||||
} {
|
||||
const text = (turn.paragraphs ?? [])
|
||||
.map((p) => String(p?.text ?? '').trim())
|
||||
.filter(Boolean)
|
||||
.join('\n\n');
|
||||
|
||||
function toClientTurn(turn: ZorkTurnResult): ZorkTurnResult {
|
||||
return {
|
||||
text,
|
||||
...turn,
|
||||
gameState: {
|
||||
...turn.gameState,
|
||||
currentRoomId: turn.gameState?.statusLine,
|
||||
statusLine: turn.gameState?.statusLine,
|
||||
},
|
||||
@@ -89,7 +96,10 @@ function normalizeSaveSlot(slot: unknown): number {
|
||||
function getOrCreateEngine(socketId: string): ZorkLlmEngine {
|
||||
let engine = sessions.get(socketId);
|
||||
if (!engine) {
|
||||
engine = new ZorkLlmEngine();
|
||||
engine = new ZorkLlmEngine({
|
||||
storyPath: projectPath(process.env.ZORK_STORY_FILE || engineConfig.paths.mainGameFile),
|
||||
promptDir: projectPath(engineConfig.paths.promptDir || 'data/zork-prompts'),
|
||||
});
|
||||
sessions.set(socketId, engine);
|
||||
}
|
||||
return engine;
|
||||
@@ -119,7 +129,7 @@ async function handleGameApi(
|
||||
case 'newGame()': {
|
||||
const engine = getOrCreateEngine(socket.id);
|
||||
const turn = await engine.newGame();
|
||||
socket.emit('narrativeResponse', toLegacyNarrative(turn));
|
||||
socket.emit('narrativeResponse', toClientTurn(turn));
|
||||
return {
|
||||
success: true,
|
||||
result: true,
|
||||
@@ -136,7 +146,7 @@ async function handleGameApi(
|
||||
}
|
||||
const engine = getOrCreateEngine(socket.id);
|
||||
const turn = await engine.loadGame(slots.get(slot)!);
|
||||
socket.emit('narrativeResponse', toLegacyNarrative(turn));
|
||||
socket.emit('narrativeResponse', toClientTurn(turn));
|
||||
socket.emit('gameLoaded', { slot });
|
||||
return { success: true, result: true, running: true, slot };
|
||||
}
|
||||
@@ -180,10 +190,8 @@ async function handleGameApi(
|
||||
}
|
||||
|
||||
function checkRuntimeConfiguration(): void {
|
||||
const storyPath = path.resolve(
|
||||
process.env.ZORK_STORY_FILE ?? './data/z-code/zork1.bin',
|
||||
);
|
||||
const promptDir = path.resolve('./data/zork-prompts');
|
||||
const storyPath = projectPath(process.env.ZORK_STORY_FILE ?? engineConfig.paths.mainGameFile);
|
||||
const promptDir = projectPath(engineConfig.paths.promptDir || 'data/zork-prompts');
|
||||
const promptFiles = [
|
||||
'character-generation.yml',
|
||||
'text-rewriter.yml',
|
||||
@@ -223,6 +231,7 @@ function checkRuntimeConfiguration(): void {
|
||||
|
||||
io.on('connection', (socket) => {
|
||||
console.log(`[zork] Client connected: ${socket.id}`);
|
||||
socket.emit('gameConfig', clientGameConfig(engineConfig));
|
||||
|
||||
socket.on(
|
||||
'gameApi',
|
||||
@@ -272,7 +281,7 @@ io.on('connection', (socket) => {
|
||||
paragraphs: turn.paragraphs.length,
|
||||
statusLine: turn.gameState?.statusLine,
|
||||
});
|
||||
socket.emit('narrativeResponse', toLegacyNarrative(turn));
|
||||
socket.emit('narrativeResponse', toClientTurn(turn));
|
||||
} catch (error) {
|
||||
console.error('[zork] playerCommand error:', error);
|
||||
socket.emit('error', {
|
||||
@@ -309,6 +318,7 @@ function ensureDirectories(): void {
|
||||
for (const dir of dirs) {
|
||||
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
ensureConfiguredAssetDirectories(engineConfig);
|
||||
}
|
||||
|
||||
function ensureKokoroJs(): void {
|
||||
@@ -326,15 +336,17 @@ async function startServer(initialPort: number, range: number): Promise<void> {
|
||||
while (port < initialPort + range) {
|
||||
try {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
server.listen(port, () => {
|
||||
server.removeAllListeners('error');
|
||||
server.removeAllListeners('listening');
|
||||
server.once('listening', () => {
|
||||
console.log(
|
||||
`[zork] Zork Narrator server running on http://localhost:${port}`,
|
||||
);
|
||||
resolve();
|
||||
});
|
||||
server.on('error', (err: NodeJS.ErrnoException) => {
|
||||
if (err.code === 'EADDRINUSE') {
|
||||
console.log(`Port ${port} in use, trying ${port + 1}…`);
|
||||
server.once('error', (err: NodeJS.ErrnoException) => {
|
||||
if (err.code === 'EADDRINUSE' || err.code === 'EACCES') {
|
||||
console.log(`Port ${port} unavailable (${err.code}), trying ${port + 1}...`);
|
||||
server.close();
|
||||
port++;
|
||||
reject();
|
||||
@@ -342,6 +354,7 @@ async function startServer(initialPort: number, range: number): Promise<void> {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
server.listen(port);
|
||||
});
|
||||
return;
|
||||
} catch {
|
||||
|
||||
Reference in New Issue
Block a user