Add ink integration UI and media playback

This commit is contained in:
2026-05-15 21:23:46 +02:00
parent 44dc64f830
commit f2e786d5bc
89 changed files with 6561 additions and 556 deletions
+36 -23
View File
@@ -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 {