Consolidate engine docs and naming

This commit is contained in:
2026-05-19 11:09:37 +02:00
parent 121b174f2c
commit dbcb8f4284
47 changed files with 826 additions and 1992 deletions
@@ -1,19 +1,19 @@
/**
* Zork LLM Engine
* Z-code LLM Engine
*
* Runs Zork I (or any Z-machine story file) as a headless subprocess via the
* Runs a Z-machine story file as a headless subprocess via the
* `ifvms` CLI, and wraps every I/O exchange with OpenRouter LLM calls that
* translate free natural-language player input into parser commands and
* re-voice the Z-machine's raw output as polished narrative prose.
*
* Configuration (environment variables):
* ZORK_STORY_FILE path to the .z5/.z8/.bin story file (default: ./data/z-code/zork1.bin)
* ZORK_MAX_RETRIES maximum command retry attempts per turn (default: 3)
* ZORK_HISTORY_SIZE player-facing outputs stored per room (default: 5)
* OPENROUTER_API_KEY, OPENROUTER_MODEL required
* ZCODE_STORY_FILE - path to the .z5/.z8/.bin story file (default: ./data/z-code/zork1.bin)
* ZCODE_MAX_RETRIES - maximum command retry attempts per turn (default: 3)
* ZCODE_HISTORY_SIZE - player-facing outputs stored per room (default: 5)
* OPENROUTER_API_KEY, OPENROUTER_MODEL - required
*/
import { TurnResult } from '../interfaces/turn-result';
export interface ZorkSession {
export interface ZcodeSession {
characterDescription: string;
notes: string[];
recentParagraphs: string[];
@@ -22,14 +22,14 @@ export interface ZorkSession {
timeOfDay: string;
weather: string;
virtualInventory: string[];
/** roomName last N player-facing output strings */
/** roomName -> last N player-facing output strings */
roomHistory: Record<string, string[]>;
currentRoom: string;
running: boolean;
}
export type ZorkTurnResult = TurnResult;
export declare class ZorkLlmEngine {
private zork;
export type ZcodeTurnResult = TurnResult;
export declare class ZcodeLlmEngine {
private zmachine;
private session;
private prompts;
private llm;
@@ -49,14 +49,14 @@ export declare class ZorkLlmEngine {
private resolveFallbackModel;
isRunning(): boolean;
/**
* Start a new game: launch Zork, generate the player character, rewrite the
* Start a new game: launch the Z-machine story, generate the player character, rewrite the
* intro text, and return the first TurnResult for the client.
*/
newGame(): Promise<ZorkTurnResult>;
newGame(): Promise<ZcodeTurnResult>;
/**
* Process player free-text input. Returns the next TurnResult.
*/
processInput(userInput: string): Promise<ZorkTurnResult>;
processInput(userInput: string): Promise<ZcodeTurnResult>;
private runCommandPlan;
/**
* Save the current game state. Returns a JSON string suitable for storing
@@ -66,7 +66,7 @@ export declare class ZorkLlmEngine {
/**
* Load a previously saved game. Returns the first TurnResult after restore.
*/
loadGame(savedJson: string): Promise<ZorkTurnResult>;
loadGame(savedJson: string): Promise<ZcodeTurnResult>;
private runSingleCommandLoop;
private generateCharacter;
private rewriteText;
@@ -1,17 +1,17 @@
"use strict";
/**
* Zork LLM Engine
* Z-code LLM Engine
*
* Runs Zork I (or any Z-machine story file) as a headless subprocess via the
* Runs a Z-machine story file as a headless subprocess via the
* `ifvms` CLI, and wraps every I/O exchange with OpenRouter LLM calls that
* translate free natural-language player input into parser commands and
* re-voice the Z-machine's raw output as polished narrative prose.
*
* Configuration (environment variables):
* ZORK_STORY_FILE path to the .z5/.z8/.bin story file (default: ./data/z-code/zork1.bin)
* ZORK_MAX_RETRIES maximum command retry attempts per turn (default: 3)
* ZORK_HISTORY_SIZE player-facing outputs stored per room (default: 5)
* OPENROUTER_API_KEY, OPENROUTER_MODEL required
* ZCODE_STORY_FILE - path to the .z5/.z8/.bin story file (default: ./data/z-code/zork1.bin)
* ZCODE_MAX_RETRIES - maximum command retry attempts per turn (default: 3)
* ZCODE_HISTORY_SIZE - player-facing outputs stored per room (default: 5)
* OPENROUTER_API_KEY, OPENROUTER_MODEL - required
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
@@ -50,7 +50,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ZorkLlmEngine = void 0;
exports.ZcodeLlmEngine = void 0;
const child_process_1 = require("child_process");
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
@@ -60,15 +60,15 @@ const axios_1 = __importDefault(require("axios"));
const dotenv = __importStar(require("dotenv"));
const turn_result_1 = require("../interfaces/turn-result");
dotenv.config();
const DEBUG_ENABLED = /^(1|true|yes|on)$/i.test(process.env.ZORK_DEBUG ?? '');
const DEBUG_ENABLED = /^(1|true|yes|on)$/i.test(process.env.ZCODE_DEBUG ?? '');
function debugLog(message, details) {
if (!DEBUG_ENABLED)
return;
if (typeof details === 'undefined') {
console.log(`[ZorkLlm:debug] ${message}`);
console.log(`[ZcodeLlm:debug] ${message}`);
return;
}
console.log(`[ZorkLlm:debug] ${message}`, details);
console.log(`[ZcodeLlm:debug] ${message}`, details);
}
function compactText(text, maxLength = 12000) {
if (text.length <= maxLength)
@@ -152,10 +152,10 @@ function isParserComplaint(output) {
"there is no",
].some(fragment => text.includes(fragment));
}
function formatExactReadOutput(command, zorkOutput) {
function formatExactReadOutput(command, zcodeOutput) {
const object = command.replace(/^READ\s+/i, '').trim().toLowerCase();
const label = object ? `the ${object}` : 'it';
const cleanedOutput = zorkOutput
const cleanedOutput = zcodeOutput
.split('\n')
.filter((line, index) => index !== 0 || line.trim().toUpperCase() !== command.trim().toUpperCase())
.join('\n')
@@ -198,9 +198,9 @@ function evolveWeather(previous, turnCount) {
return transitions[Math.floor(turnCount / 9) % transitions.length];
}
// ---------------------------------------------------------------------------
// ZorkProcess manages the ifvms zvm child process
// ZcodeProcess manages the ifvms zvm child process
// ---------------------------------------------------------------------------
class ZorkProcess {
class ZcodeProcess {
constructor() {
this.proc = null;
this.outputBuffer = '';
@@ -279,7 +279,7 @@ class ZorkProcess {
this.scheduleResolve();
});
}
/** Debounced check: resolve when the buffer ends with Zork's '>' prompt. */
/** Debounced check: resolve when the buffer ends with a Z-machine prompt. */
scheduleResolve() {
if (!/\n>\s*$/.test(this.outputBuffer))
return;
@@ -334,23 +334,23 @@ function renderTemplate(template, vars) {
function logLlmError(scope, err) {
if (axios_1.default.isAxiosError(err)) {
const ax = err;
console.error(`[ZorkLlm] ${scope} failed: ${ax.message}`);
console.error(`[ZcodeLlm] ${scope} failed: ${ax.message}`);
if (ax.response) {
console.error(`[ZorkLlm] ${scope} status=${ax.response.status} data=`, ax.response.data);
console.error(`[ZcodeLlm] ${scope} status=${ax.response.status} data=`, ax.response.data);
if (ax.response.status === 404) {
console.error('[ZorkLlm] Hint: OPENROUTER_MODEL is likely invalid or unavailable for your API key.');
console.error('[ZcodeLlm] Hint: OPENROUTER_MODEL is likely invalid or unavailable for your API key.');
}
}
return;
}
console.error(`[ZorkLlm] ${scope} failed:`, err);
console.error(`[ZcodeLlm] ${scope} failed:`, err);
}
// ---------------------------------------------------------------------------
// ZorkLlmEngine
// ZcodeLlmEngine
// ---------------------------------------------------------------------------
class ZorkLlmEngine {
class ZcodeLlmEngine {
constructor(options = {}) {
this.zork = new ZorkProcess();
this.zmachine = new ZcodeProcess();
this.session = null;
this.resolvedFallbackModel = null;
this.llmCallCounter = 0;
@@ -360,10 +360,10 @@ class ZorkLlmEngine {
if (!apiKey || !model) {
throw new Error('Missing required environment variables: OPENROUTER_API_KEY and OPENROUTER_MODEL');
}
const replacement = ZorkLlmEngine.DEPRECATED_MODEL_REPLACEMENTS[model] ?? null;
const replacement = ZcodeLlmEngine.DEPRECATED_MODEL_REPLACEMENTS[model] ?? null;
if (replacement) {
this.model = replacement;
console.warn(`[ZorkLlm] Replacing deprecated model '${model}' with '${replacement}'.`);
console.warn(`[ZcodeLlm] Replacing deprecated model '${model}' with '${replacement}'.`);
}
else {
this.model = model;
@@ -372,10 +372,10 @@ class ZorkLlmEngine {
requestedModel: model,
activeModel: this.model,
});
this.maxRetries = parseInt(process.env.ZORK_MAX_RETRIES ?? '3', 10);
this.historySize = parseInt(process.env.ZORK_HISTORY_SIZE ?? '5', 10);
this.storyPath = path.resolve(options.storyPath ?? process.env.ZORK_STORY_FILE ?? './data/z-code/zork1.bin');
const promptDir = path.resolve(options.promptDir ?? './data/zork-prompts');
this.maxRetries = parseInt(process.env.ZCODE_MAX_RETRIES ?? '3', 10);
this.historySize = parseInt(process.env.ZCODE_HISTORY_SIZE ?? '5', 10);
this.storyPath = path.resolve(options.storyPath ?? process.env.ZCODE_STORY_FILE ?? './data/z-code/zork1.bin');
const promptDir = path.resolve(options.promptDir ?? './data/zcode-prompts');
this.prompts = loadPrompts(promptDir);
this.llm = axios_1.default.create({
baseURL: 'https://openrouter.ai/api/v1',
@@ -408,7 +408,7 @@ class ZorkLlmEngine {
if (axios_1.default.isAxiosError(err) && err.response?.status === 404) {
const fallbackModel = await this.resolveFallbackModel();
this.model = fallbackModel;
console.warn(`[ZorkLlm] Switching active model to '${fallbackModel}'.`);
console.warn(`[ZcodeLlm] Switching active model to '${fallbackModel}'.`);
const withFallbackModel = {
...withReasoningDefaults(payload, fallbackModel),
model: fallbackModel,
@@ -478,23 +478,23 @@ class ZorkLlmEngine {
}
// ---- Public API -----------------------------------------------------------
isRunning() {
return this.session?.running === true && this.zork.isAlive();
return this.session?.running === true && this.zmachine.isAlive();
}
/**
* Start a new game: launch Zork, generate the player character, rewrite the
* Start a new game: launch the Z-machine story, generate the player character, rewrite the
* intro text, and return the first TurnResult for the client.
*/
async newGame() {
// Kill any existing game
if (this.zork.isAlive())
this.zork.kill();
if (this.zmachine.isAlive())
this.zmachine.kill();
this.nextTurnId = 1;
if (!fs.existsSync(this.storyPath)) {
throw new Error(`Story file not found: ${this.storyPath}\n` +
'Place zork1.bin in ./data/z-code/ (see README in that folder).');
}
debugLog('launching Z-machine', { storyPath: this.storyPath });
const rawIntro = await this.zork.launch(this.storyPath);
const rawIntro = await this.zmachine.launch(this.storyPath);
debugLog('Z-machine intro output', compactText(rawIntro));
// Generate the player character before showing any text
const characterDescription = await this.generateCharacter();
@@ -555,7 +555,7 @@ class ZorkLlmEngine {
for (const tool of cmdResponse.tools) {
this.executeTool(tool);
}
// If the translator also supplied a Zork command, continue to game loop
// If the translator also supplied a Z-machine command, continue to game loop
if (!cmdResponse.command && !cmdResponse.commands?.length) {
// Pure tool action — generate a brief acknowledgement via the rewriter
const ack = await this.rewriteText(`(The narrator pauses. ${userInput})`);
@@ -593,16 +593,16 @@ class ZorkLlmEngine {
async saveGame() {
if (!this.session)
throw new Error('No active session to save');
const tmpFile = path.join(os.tmpdir(), `zork-save-${Date.now()}.qzl`);
const tmpFile = path.join(os.tmpdir(), `zcode-save-${Date.now()}.qzl`);
try {
// Ask Zork to save, supply the temp file path, and discard the output
await this.zork.sendLine('SAVE');
await this.zork.sendLine(tmpFile);
let zorkSave = '';
// Ask the Z-machine to save, supply the temp file path, and discard the output
await this.zmachine.sendLine('SAVE');
await this.zmachine.sendLine(tmpFile);
let zcodeSave = '';
if (fs.existsSync(tmpFile)) {
zorkSave = fs.readFileSync(tmpFile).toString('base64');
zcodeSave = fs.readFileSync(tmpFile).toString('base64');
}
return JSON.stringify({ session: this.session, zorkSave });
return JSON.stringify({ session: this.session, zcodeSave });
}
finally {
if (fs.existsSync(tmpFile))
@@ -614,15 +614,15 @@ class ZorkLlmEngine {
*/
async loadGame(savedJson) {
var _a, _b, _c, _d, _e, _f;
const { session, zorkSave } = JSON.parse(savedJson);
if (this.zork.isAlive())
this.zork.kill();
const tmpFile = path.join(os.tmpdir(), `zork-restore-${Date.now()}.qzl`);
const { session, zcodeSave } = JSON.parse(savedJson);
if (this.zmachine.isAlive())
this.zmachine.kill();
const tmpFile = path.join(os.tmpdir(), `zcode-restore-${Date.now()}.qzl`);
try {
fs.writeFileSync(tmpFile, Buffer.from(zorkSave, 'base64'));
await this.zork.launch(this.storyPath);
await this.zork.sendLine('RESTORE');
const restoreOutput = await this.zork.sendLine(tmpFile);
fs.writeFileSync(tmpFile, Buffer.from(zcodeSave, 'base64'));
await this.zmachine.launch(this.storyPath);
await this.zmachine.sendLine('RESTORE');
const restoreOutput = await this.zmachine.sendLine(tmpFile);
this.session = { ...session, running: true };
(_a = this.session).rawTranscript ?? (_a.rawTranscript = []);
(_b = this.session).recentParagraphs ?? (_b.recentParagraphs = []);
@@ -650,7 +650,7 @@ class ZorkLlmEngine {
attempt,
maxRetries: this.maxRetries,
});
const rawOutput = await this.zork.sendLine(command);
const rawOutput = await this.zmachine.sendLine(command);
lastOutput = rawOutput;
this.appendRawTranscript(command, rawOutput);
debugLog('received Z-machine output', {
@@ -714,10 +714,10 @@ class ZorkLlmEngine {
return 'You are a wary but curious explorer, driven more by persistence than bravery. You have come to the old house seeking answers, carrying a notebook of unfinished questions and a habit of checking every corner twice.';
}
}
async rewriteText(zorkOutput) {
async rewriteText(zcodeOutput) {
const cfg = this.prompts.textRewriter;
const vars = this.buildCommonVars();
vars['zorkOutput'] = zorkOutput;
vars['zcodeOutput'] = zcodeOutput;
try {
const response = await this.createCompletion({
messages: [
@@ -731,7 +731,7 @@ class ZorkLlmEngine {
}
catch (err) {
logLlmError('rewriteText', err);
return zorkOutput;
return zcodeOutput;
}
}
async translateCommand(userInput) {
@@ -753,16 +753,16 @@ class ZorkLlmEngine {
}
catch (err) {
logLlmError('translateCommand', err);
// Fallback: pass input directly to Zork parser
// Fallback: pass input directly to Z-machine parser
return { type: 'command', command: userInput.toUpperCase() };
}
}
async evaluateOutput(userIntent, commandTried, zorkOutput, attempt) {
async evaluateOutput(userIntent, commandTried, zcodeOutput, attempt) {
const cfg = this.prompts.outputEvaluator;
const vars = this.buildCommonVars();
vars['userIntent'] = userIntent;
vars['commandTried'] = commandTried;
vars['zorkOutput'] = zorkOutput;
vars['zcodeOutput'] = zcodeOutput;
vars['attempt'] = String(attempt);
vars['maxAttempts'] = String(this.maxRetries);
try {
@@ -780,7 +780,7 @@ class ZorkLlmEngine {
catch (err) {
logLlmError('evaluateOutput', err);
// Fallback: accept the raw output as-is
return { decision: 'accept', text: zorkOutput };
return { decision: 'accept', text: zcodeOutput };
}
}
// ---- Session helpers -------------------------------------------------------
@@ -968,7 +968,7 @@ class ZorkLlmEngine {
};
}
buildTurnResult(text) {
const alive = this.zork.isAlive();
const alive = this.zmachine.isAlive();
if (!alive && this.session)
this.session.running = false;
const paragraphs = (0, turn_result_1.textToParagraphs)(text);
@@ -981,9 +981,9 @@ class ZorkLlmEngine {
};
}
}
exports.ZorkLlmEngine = ZorkLlmEngine;
ZorkLlmEngine.DEPRECATED_MODEL_REPLACEMENTS = {
exports.ZcodeLlmEngine = ZcodeLlmEngine;
ZcodeLlmEngine.DEPRECATED_MODEL_REPLACEMENTS = {
'anthropic/claude-3-opus-20240229': 'openai/gpt-5.5',
'openai/gpt-5.4-mini': 'openai/gpt-5.5',
};
//# sourceMappingURL=zork-llm-engine.js.map
//# sourceMappingURL=zcode-llm-engine.js.map
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long