From 6faee20268bbd5c514c2073578480e6e4bb9eb9d Mon Sep 17 00:00:00 2001 From: Georg Tomitsch Date: Fri, 15 May 2026 07:55:05 +0200 Subject: [PATCH] Add Zork engine integration work --- .env | 5 +- .env.example | 6 +- data/z-code/README.md | 30 + data/z-code/zork1.bin | Bin 0 -> 92160 bytes data/zork-prompts/character-generation.yml | 44 + data/zork-prompts/command-translator.yml | 112 ++ data/zork-prompts/output-evaluator.yml | 76 ++ data/zork-prompts/text-rewriter.yml | 77 ++ dist/engine/zork-llm-engine.d.ts | 90 ++ dist/engine/zork-llm-engine.js | 984 +++++++++++++++++ dist/engine/zork-llm-engine.js.map | 1 + dist/server-zork.d.ts | 16 + dist/server-zork.js | 343 ++++++ dist/server-zork.js.map | 1 + package-lock.json | 146 ++- package.json | 9 + src/engine/zork-llm-engine.ts | 1158 ++++++++++++++++++++ src/server-zork.ts | 362 ++++++ zcode_inclusion.md | 674 ++++++++++++ 19 files changed, 4113 insertions(+), 21 deletions(-) create mode 100644 data/z-code/README.md create mode 100644 data/z-code/zork1.bin create mode 100644 data/zork-prompts/character-generation.yml create mode 100644 data/zork-prompts/command-translator.yml create mode 100644 data/zork-prompts/output-evaluator.yml create mode 100644 data/zork-prompts/text-rewriter.yml create mode 100644 dist/engine/zork-llm-engine.d.ts create mode 100644 dist/engine/zork-llm-engine.js create mode 100644 dist/engine/zork-llm-engine.js.map create mode 100644 dist/server-zork.d.ts create mode 100644 dist/server-zork.js create mode 100644 dist/server-zork.js.map create mode 100644 src/engine/zork-llm-engine.ts create mode 100644 src/server-zork.ts create mode 100644 zcode_inclusion.md diff --git a/.env b/.env index 76645c5..3aacba2 100644 --- a/.env +++ b/.env @@ -1,10 +1,11 @@ # OpenRouter API Configuration OPENROUTER_API_KEY=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 -OPENROUTER_MODEL=anthropic/claude-3-opus-20240229 +OPENROUTER_MODEL=openai/gpt-5.5 +OPENROUTER_REASONING_EFFORT=none # Application Configuration PORT=3001 NODE_ENV=development # Game Configuration -DEFAULT_WORLD_FILE=./data/worlds/example_world.yml \ No newline at end of file +DEFAULT_WORLD_FILE=./data/worlds/example_world.yml diff --git a/.env.example b/.env.example index 64cb74e..8557052 100644 --- a/.env.example +++ b/.env.example @@ -1,10 +1,12 @@ # OpenRouter API Configuration OPENROUTER_API_KEY=your_openrouter_api_key_here -OPENROUTER_MODEL=your_selected_model_here +OPENROUTER_MODEL=openai/gpt-5.5 +# GPT-5 reasoning tokens can consume short completion budgets; keep narration calls direct by default. +OPENROUTER_REASONING_EFFORT=none # Application Configuration PORT=3000 NODE_ENV=development # Game Configuration -DEFAULT_WORLD_FILE=./data/worlds/example_world.yml \ No newline at end of file +DEFAULT_WORLD_FILE=./data/worlds/example_world.yml diff --git a/data/z-code/README.md b/data/z-code/README.md new file mode 100644 index 0000000..bcee534 --- /dev/null +++ b/data/z-code/README.md @@ -0,0 +1,30 @@ +# Z-Code Story Files + +Place your Z-machine story files here. The Zork Narrator engine looks for +`zork1.bin` by default. This can be overridden with the `ZORK_STORY_FILE` +environment variable. + +## Obtaining Zork I + +The Zork I story file (`zork1.bin`, also distributed as `ZORK1.DAT` or as a +`.z3` or `.z5` file) is copyrighted by Infocom / Activision. It is not +included in this repository. + +You can obtain a legal copy via: + +- The **Zork Trilogy** on GOG.com or Steam (includes the original data files). +- The [Internet Archive](https://archive.org/details/Zork_I_The_Great_Underground_Empire_1980_Infocom) + hosts a playable version in-browser; the original data files are part of some + archived distributions listed under the Infocom catalogue. + +Once you have the file, rename it to `zork1.bin` and place it in this folder, +or set `ZORK_STORY_FILE=./path/to/your/file` in your `.env`. + +## Supported Formats + +The `ifvms` interpreter accepts: +- `.z3`, `.z4`, `.z5`, `.z8` — raw Z-machine story files +- `.zblorb` — Blorb-wrapped story files (may include sound resources) +- Any file with the correct Z-machine header (the extension is ignored) + +Zork I is a Z-machine version 3 (`.z3`) game. diff --git a/data/z-code/zork1.bin b/data/z-code/zork1.bin new file mode 100644 index 0000000000000000000000000000000000000000..5b79e24fd2ecd39fdfd680da5a9b5b668e5b21e9 GIT binary patch literal 92160 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⪙DESKAXb`Mn*#)A*?J$>uxq+{21u#A<)Fp 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 literal 0 HcmV?d00001 diff --git a/data/zork-prompts/character-generation.yml b/data/zork-prompts/character-generation.yml new file mode 100644 index 0000000..94c0ea4 --- /dev/null +++ b/data/zork-prompts/character-generation.yml @@ -0,0 +1,44 @@ +# Character Generation Prompt +# Called once at game start to create a unique player character. +# No user_template is needed — the system message IS the full prompt. +# Expected output: 300-500 words of vivid character description prose. No JSON. + +system: | + You are creating the canonical player-character profile for: + Zork I: The Great Underground Empire. + + Hard requirements: + - Always write in second person and refer to the protagonist as "you". + - Never call the protagonist "he", "she", "they", or by a third-person noun. + - The character is from an Earth-like 1980s setting blended with Zork lore. + - The character is NOT an American treasure hunter. + - Tone: vivid, concrete, grounded, literary, and emotionally specific. + - Give the character one primary sensitive sense and make it easy for later + narration to use that sense. + + Generate a complete persona that includes: + - Random full name. + - Gender, nationality, race, age. + - Skin color, eye color, hair color, body size, body build. + - Personal style, hairstyle. + - Tattoos (optional), piercings (optional), scars (optional). + - Distinctive standout trait (at least one clearly unusual detail). + - One dominant sense (sight, hearing, smell, taste, touch) that is most sensitive. + - Exactly three sentences of backstory. + - Personality, likes, dislikes, hopes, fears, worldview. + - Clothing and accessories worn on body, including underlayers where relevant. + - Do NOT list bags, tools, or equipment. + - Seed one or two concrete memory hooks that can later be triggered by places, + smells, sounds, architecture, darkness, weather, or treasure. + + Output format (strict): + - First line must start exactly with: Welcome to the game + - On that same line include the full official title: Zork I: The Great Underground Empire + - Second line must start exactly with: You are + - Continue with the full persona in flowing prose. + - Do not output any extra headings, metadata, bullet points, or explanations. + + Ensure the generated profile is specific enough to support memory continuity, + body-description requests, mood shifts, and character-consistent narration later. + +user_template: "" diff --git a/data/zork-prompts/command-translator.yml b/data/zork-prompts/command-translator.yml new file mode 100644 index 0000000..6fbffba --- /dev/null +++ b/data/zork-prompts/command-translator.yml @@ -0,0 +1,112 @@ +# Command Translator Prompt +# Called for every player input. Converts free natural-language text into a +# Zork parser command, or decides to reply directly / execute session tools. +# Expected output: a JSON object (see schema below). + +system: | + You are the command-intent router for a literary Zork I engine. + + Hard rules: + - Keep player-character continuity in second person ("you"). + - If user asks for personal life/body/memory detail not present in context, + reply directly from the established character profile instead of sending a + parser command to Zork. + - If the player changes or adds stable identity, personality, mood, memory, + clothing, body, or backstory facts, use update_character or add_note so future + narration remembers it. + - If newly invented personal possessions are implied, add them to virtual inventory. + + Choose one response mode: + + MODE A — command + Use for one parser action. + JSON: + { "type": "command", "command": "OPEN MAILBOX" } + + MODE B — commands + Use when the user asks for multiple sequential actions in one input. + Example: "Take and read the pamphlet" -> TAKE PAMPHLET, READ PAMPHLET. + JSON: + { "type": "commands", "commands": ["TAKE PAMPHLET", "READ PAMPHLET"] } + + MODE C — reply + Use when no meaningful parser action exists. + Give a brief in-world response and guide back to actionable input only if the + player seems blocked. For body, clothing, identity, mood, memory, or "who am I" + questions, answer in second-person prose from the character profile. + JSON: + { "type": "reply", "text": "..." } + + MODE D — tools + Use tools when memory/state should be persisted, optionally with command(s). + JSON shape: + { + "type": "tools", + "tools": [ ... ], + "command": "OPTIONAL_SINGLE_COMMAND", + "commands": ["OPTIONAL", "MULTI", "COMMANDS"] + } + + Available tools: + - update_character + args: { "description": string } + - add_note + args: { "note": string } + - remove_note + args: { "index": number } + - add_inventory_item + args: { "item": string } + - remove_inventory_item + args: { "item": string } + + Tool usage policy: + - Use update_character for stable identity/body/personality updates. + - Use add_note for world facts, personal memories, unresolved goals, promises. + - Use add_inventory_item when narration introduces an on-person personal item + (even if Zork parser does not track it). + - Use remove_inventory_item when item is consumed/lost/discarded in story logic. + + Command policy: + - Use terse Zork-style imperatives, uppercase preferred. + - Split compound natural language requests into ordered commands when needed. + - Avoid impossible commands when a helpful reply is better. + - Do not translate "who am I", "describe me", "look at myself", or body/clothing + inspection into parser commands; answer as the narrator using MODE C unless + the input also contains a concrete world action. + - When the player asks what a leaflet/pamphlet/paper says, use READ LEAFLET. + - When the player asks to take and read something from the mailbox, use + TAKE LEAFLET followed by READ LEAFLET, not TAKE MAILBOX or READ MAILBOX. + - When the player asks to look inside the mailbox, use LOOK IN MAILBOX. + - If the player complains that readable text was not shown, route to READ LEAFLET + when the recent context includes a leaflet/pamphlet/paper. + + Output only valid JSON in exactly one mode. + +user_template: | + Player character: + {{characterDescription}} + + Narrator's notes (index 0, 1, 2…): + {{notes}} + + Character-side virtual inventory: + {{virtualInventory}} + + Narrator simulation state: + {{narratorState}} + + Current location: {{currentRoom}} + + What the player has seen here recently: + {{roomHistory}} + + Most recent narrative paragraphs across scenes (up to 10, newest last): + {{recentNarrative}} + + Recent raw parser transcript for factual anchoring: + {{rawTranscript}} + + Player's input: + "{{userInput}}" + + Respond with the appropriate JSON now. diff --git a/data/zork-prompts/output-evaluator.yml b/data/zork-prompts/output-evaluator.yml new file mode 100644 index 0000000..9e65ca8 --- /dev/null +++ b/data/zork-prompts/output-evaluator.yml @@ -0,0 +1,76 @@ +# Output Evaluator Prompt +# Called after each Z-machine response. Decides whether to accept the output +# and rewrite it for the player, or to discard it and retry with a new command. +# Expected output: a JSON object (see schema below). + +system: | + You are the quality gate between parser output and literary narration. + + Decide whether to accept parser output or retry with a better command. + + Retry when: + - parser error / unknown verb / malformed command, + - a clearer command likely achieves user intent, + - and attempt is not the final one. + + Accept when: + - any meaningful world response occurred (including meaningful failure), + - or this is the final attempt. + + If accepting, output vivid prose that: + - always refers to protagonist as "you" (never he/she/they), + - preserves parser facts, + - preserves written/readable text exactly when the command reads an object, + - uses the narrator simulation state for time/weather continuity, + - uses atmosphere and sensory detail, especially the character's sensitive sense, + - may include required preparatory body movement if it does not change game state, + - may include fitting internal monologue, direct speech, or a triggered memory, + - aligns with established character, notes, virtual inventory, and recent narrative. + + Keep output concrete and scene-rooted. + Do not recommend commands, list possible next actions, or end with "If you want...". + Do not say the parser failed to provide text when the raw Z-machine response contains + the text being read. + + Output JSON only: + - Accept: + { "decision": "accept", "text": "..." } + - Retry: + { "decision": "retry", "command": "..." } + +user_template: | + Player character: + {{characterDescription}} + + Narrator's notes: + {{notes}} + + Character-side virtual inventory: + {{virtualInventory}} + + Narrator simulation state: + {{narratorState}} + + Current location: {{currentRoom}} + + What the player has seen here recently: + {{roomHistory}} + + Most recent narrative paragraphs across scenes (up to 10, newest last): + {{recentNarrative}} + + Recent raw parser transcript for factual anchoring: + {{rawTranscript}} + + --- + Original player intent: "{{userIntent}}" + Command tried: {{commandTried}} + Attempt: {{attempt}} of {{maxAttempts}} + + Raw Z-machine response: + --- + {{zorkOutput}} + --- + + Decide now: accept and rewrite, or retry with a new command? + Respond with the appropriate JSON. diff --git a/data/zork-prompts/text-rewriter.yml b/data/zork-prompts/text-rewriter.yml new file mode 100644 index 0000000..c3e912f --- /dev/null +++ b/data/zork-prompts/text-rewriter.yml @@ -0,0 +1,77 @@ +# Text Rewriter Prompt +# Called for the game's opening text, and for re-entry into rooms that have +# no prior player-facing history yet. +# Expected output: polished prose. No JSON. + +system: | + You are the narrative layer for Zork I: The Great Underground Empire. + Rewrite raw Z-machine output into immersive prose while preserving game facts. + + Core stance: + - Always narrate the player-character in second person: "you". + - Never refer to the player-character as he, she, they, or by third-person labels. + - Keep canon game facts intact (objects, exits, outcomes, failures, state changes). + - Do not invent gameplay-critical facts that contradict Zork output. + + Style and simulation goals: + - Use atmospheric detail: light/shadow, sound, smell, airflow, temperature. + - Use the supplied narrator simulation state for day/night and weather continuity; + let it influence outside scenes and thresholds, and mention it only when it + naturally changes the felt scene. + - Make physical actions visceral when movement/exertion occurs. + - Let the character's personality, sensitive sense, hopes, fears, and worldview + color word choice, interpretation, internal monologue, and occasional direct + speech. + - Occasionally weave memory flashes from established backstory/notes when context fits. + - If describing the body, describe only what "you" can perceive directly and your + immediate thoughts about those details. + - Add incidental preparatory body movement when it would be required to perform + an action, as long as it does not change Zork's authoritative game state. + - Use Zork lore as texture, rumor, architecture, old names, or cultural memory, + but never as a new solvable fact unless the raw parser output establishes it. + + Continuity policy: + - Use character profile, notes, virtual inventory, room history, and recent narrative + context to keep prose consistent. + - If prior context introduced non-Zork personal possessions, they can appear in prose + as personal details but must not be treated as parser-available game objects unless + present in Zork output. + + Output constraints: + - Return prose only. No JSON, no labels, no headings. + - Prefer short paragraphs (2-5 sentences each). + - Preserve parser intent while replacing parser phrasing with natural narration. + - Do not recommend commands, list possible actions, or end with "If you want...". + - Do not apologize or mention missing information unless the raw Z-machine output + explicitly says that information is unavailable. + - When raw output contains written text from a sign, leaflet, book, label, inscription, + or other readable object, preserve the exact wording verbatim inside the prose. + +user_template: | + The player character: + {{characterDescription}} + + Narrator's notes about the story so far: + {{notes}} + + Character-side virtual inventory (can exist even if Zork does not track it): + {{virtualInventory}} + + Narrator simulation state: + {{narratorState}} + + What the player has seen in this location before (most recent last): + {{roomHistory}} + + Most recent narrative paragraphs across scenes (up to 10, newest last): + {{recentNarrative}} + + Recent raw parser transcript for factual anchoring: + {{rawTranscript}} + + Raw Z-machine output to rewrite: + --- + {{zorkOutput}} + --- + + Rewrite the above as prose for the player now. diff --git a/dist/engine/zork-llm-engine.d.ts b/dist/engine/zork-llm-engine.d.ts new file mode 100644 index 0000000..06a0ab2 --- /dev/null +++ b/dist/engine/zork-llm-engine.d.ts @@ -0,0 +1,90 @@ +/** + * Zork LLM Engine + * + * Runs Zork I (or any 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 + */ +export interface ZorkSession { + characterDescription: string; + notes: string[]; + recentParagraphs: string[]; + rawTranscript: string[]; + turnCount: number; + timeOfDay: string; + weather: string; + virtualInventory: string[]; + /** roomName → last N player-facing output strings */ + roomHistory: Record; + currentRoom: string; + running: boolean; +} +/** Subset of the unified TurnResult protocol understood by the client. */ +export interface ZorkTurnResult { + paragraphs: Array<{ + text: string; + tags: unknown[]; + }>; + choices: unknown[]; + inputMode: 'text' | 'end'; + gameState?: { + statusLine?: string; + }; +} +export declare class ZorkLlmEngine { + private zork; + private session; + private prompts; + private llm; + private model; + private resolvedFallbackModel; + private llmCallCounter; + private maxRetries; + private historySize; + private storyPath; + private static readonly DEPRECATED_MODEL_REPLACEMENTS; + constructor(); + private createCompletion; + private resolveFallbackModel; + isRunning(): boolean; + /** + * Start a new game: launch Zork, generate the player character, rewrite the + * intro text, and return the first TurnResult for the client. + */ + newGame(): Promise; + /** + * Process player free-text input. Returns the next TurnResult. + */ + processInput(userInput: string): Promise; + private runCommandPlan; + /** + * Save the current game state. Returns a JSON string suitable for storing + * in the socket's save-game slot map. + */ + saveGame(): Promise; + /** + * Load a previously saved game. Returns the first TurnResult after restore. + */ + loadGame(savedJson: string): Promise; + private runSingleCommandLoop; + private generateCharacter; + private rewriteText; + private translateCommand; + private evaluateOutput; + private executeTool; + private appendRecentParagraph; + private extractCommands; + private appendRawTranscript; + private advanceNarratorState; + private getDeterministicCommandPlan; + private appendRoomHistory; + private buildCommonVars; + private buildTurnResult; +} diff --git a/dist/engine/zork-llm-engine.js b/dist/engine/zork-llm-engine.js new file mode 100644 index 0000000..75d11b2 --- /dev/null +++ b/dist/engine/zork-llm-engine.js @@ -0,0 +1,984 @@ +"use strict"; +/** + * Zork LLM Engine + * + * Runs Zork I (or any 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 + */ +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 }); +exports.ZorkLlmEngine = void 0; +const child_process_1 = require("child_process"); +const fs = __importStar(require("fs")); +const path = __importStar(require("path")); +const os = __importStar(require("os")); +const yaml = __importStar(require("js-yaml")); +const axios_1 = __importDefault(require("axios")); +const dotenv = __importStar(require("dotenv")); +dotenv.config(); +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(`[ZorkLlm:debug] ${message}`); + return; + } + console.log(`[ZorkLlm:debug] ${message}`, details); +} +function compactText(text, maxLength = 12000) { + if (text.length <= maxLength) + return text; + return `${text.slice(0, maxLength)}\n...[truncated ${text.length - maxLength} chars]`; +} +function getAssistantContent(data) { + const content = data?.choices?.[0]?.message?.content; + if (typeof content === 'string') + return content; + if (Array.isArray(content)) { + return content + .map((part) => { + if (typeof part === 'string') + return part; + if (typeof part?.text === 'string') + return part.text; + if (typeof part?.content === 'string') + return part.content; + return ''; + }) + .join('') + .trim(); + } + throw new Error(`LLM response did not contain assistant text: ${compactText(JSON.stringify(data))}`); +} +function withReasoningDefaults(payload, model) { + if (payload.reasoning || !/\bgpt-5/i.test(model)) + return payload; + return { + ...payload, + reasoning: { + effort: process.env.OPENROUTER_REASONING_EFFORT ?? 'none', + exclude: true, + }, + }; +} +// --------------------------------------------------------------------------- +// Utility: strip ANSI escape sequences +// --------------------------------------------------------------------------- +function stripAnsi(s) { + // eslint-disable-next-line no-control-regex + return s.replace(/\x1B\[[0-9;]*[mGKHFJA-Z]/g, ''); +} +// --------------------------------------------------------------------------- +// Utility: extract the current room name from Z-machine output +// --------------------------------------------------------------------------- +function extractRoomName(output) { + const lines = output + .split('\n') + .map(l => l.trim()) + .filter(l => l.length > 0); + if (lines.length === 0) + return null; + const first = lines[0]; + // Room name heuristics: short, starts with capital, no sentence-ending punctuation + if (first.length < 65 && + /^[A-Z]/.test(first) && + !/[.!?]$/.test(first) && + !/^(You |I |It |There |The [a-z])/.test(first)) { + return first; + } + return null; +} +function isReadCommand(command) { + return /^READ\b/i.test(command.trim()); +} +function isParserComplaint(output) { + const text = output.toLowerCase(); + return [ + "i don't know the word", + "i don't understand", + "that's not a verb", + "you can't see any", + "you don't have", + "you aren't carrying", + "what do you want to", + "what do you want to read", + "what do you want to take", + "which do you mean", + "there is no", + ].some(fragment => text.includes(fragment)); +} +function formatExactReadOutput(command, zorkOutput) { + const object = command.replace(/^READ\s+/i, '').trim().toLowerCase(); + const label = object ? `the ${object}` : 'it'; + const cleanedOutput = zorkOutput + .split('\n') + .filter((line, index) => index !== 0 || line.trim().toUpperCase() !== command.trim().toUpperCase()) + .join('\n') + .trim(); + return `You read ${label}.\n\n${cleanedOutput}`; +} +function pickInitialWeather() { + const options = [ + 'cool, unsettled air under a low grey sky', + 'a dry bright afternoon with thin wind moving through the grass', + 'misty weather with damp earth-smell clinging to everything outside', + 'a mild overcast day, quiet enough that small sounds carry', + ]; + return options[Math.floor(Math.random() * options.length)]; +} +function timeOfDayForTurn(turnCount) { + const phases = [ + 'late morning', + 'early afternoon', + 'late afternoon', + 'dusk', + 'early evening', + 'night', + 'deep night', + 'pre-dawn', + 'morning', + ]; + return phases[Math.floor(turnCount / 12) % phases.length]; +} +function evolveWeather(previous, turnCount) { + if (turnCount > 0 && turnCount % 9 !== 0) + return previous; + const transitions = [ + 'the air has cooled and carries a faint mineral dampness', + 'the wind has shifted, restless but not yet stormy', + 'the light has thinned behind a veil of cloud', + 'the weather holds steady, quiet and watchful', + 'a trace of moisture gathers in the air', + ]; + return transitions[Math.floor(turnCount / 9) % transitions.length]; +} +// --------------------------------------------------------------------------- +// ZorkProcess – manages the ifvms zvm child process +// --------------------------------------------------------------------------- +class ZorkProcess { + constructor() { + this.proc = null; + this.outputBuffer = ''; + this.pendingResolve = null; + this.debounceTimer = null; + } + /** Start the Z-machine with the given story file, return the opening text. */ + async launch(storyPath) { + const zvm = this.locateZvm(); + this.proc = (0, child_process_1.spawn)(zvm, [storyPath], { + stdio: ['pipe', 'pipe', 'pipe'], + shell: true, + cwd: process.cwd(), + }); + this.proc.stdout.on('data', (chunk) => { + this.outputBuffer += stripAnsi(chunk.toString()); + this.scheduleResolve(); + }); + this.proc.stderr.on('data', (chunk) => { + // Log but don't throw – ifvms may emit warnings on stderr + console.warn('[zvm]', chunk.toString().trim()); + }); + this.proc.on('exit', () => { + // If the process exits while we are waiting for output, resolve immediately + if (this.pendingResolve) { + const resolver = this.pendingResolve; + this.pendingResolve = null; + resolver(this.outputBuffer.trim()); + this.outputBuffer = ''; + } + this.proc = null; + }); + return this.waitForPrompt(); + } + /** Send a line of input and return all output until the next prompt. */ + async sendLine(text) { + if (!this.proc) + throw new Error('Z-machine process is not running'); + this.outputBuffer = ''; + this.proc.stdin.write(text + '\n'); + return this.waitForPrompt(); + } + isAlive() { + return this.proc !== null && !this.proc.killed; + } + kill() { + if (this.proc) { + this.proc.kill(); + this.proc = null; + } + } + // ---- private ---- + waitForPrompt() { + return new Promise((resolve) => { + // Wrap to allow debounce timer to cancel a previous waiter safely + const wrapped = (text) => resolve(text); + this.pendingResolve = wrapped; + // Safety timeout: if no prompt detected after 15 s, resolve with what we have + const safety = setTimeout(() => { + if (this.pendingResolve === wrapped) { + this.pendingResolve = null; + const text = this.outputBuffer.trim(); + this.outputBuffer = ''; + resolve(text); + } + }, 15000); + // Ensure the safety timeout does not keep Node alive indefinitely + if (safety.unref) + safety.unref(); + // Override so debounce also cancels the safety timer + this.pendingResolve = (text) => { + clearTimeout(safety); + resolve(text); + }; + // Data may already be buffered + this.scheduleResolve(); + }); + } + /** Debounced check: resolve when the buffer ends with Zork's '>' prompt. */ + scheduleResolve() { + if (!/\n>\s*$/.test(this.outputBuffer)) + return; + if (this.debounceTimer) + clearTimeout(this.debounceTimer); + this.debounceTimer = setTimeout(() => { + this.debounceTimer = null; + if (!this.pendingResolve) + return; + const text = this.outputBuffer.replace(/\n>\s*$/, '').trim(); + this.outputBuffer = ''; + const resolver = this.pendingResolve; + this.pendingResolve = null; + resolver(text); + }, 80); + } + locateZvm() { + const binDir = path.join(process.cwd(), 'node_modules', '.bin'); + const candidates = process.platform === 'win32' + ? ['zvm.cmd', 'zvm.ps1', 'zvm'] + : ['zvm']; + for (const name of candidates) { + const full = path.join(binDir, name); + if (fs.existsSync(full)) + return full; + } + // Fall through to shell PATH lookup (works if ifvms is installed globally) + return 'zvm'; + } +} +// --------------------------------------------------------------------------- +// Prompt loader +// --------------------------------------------------------------------------- +function loadPrompts(promptDir) { + function load(filename) { + const filePath = path.join(promptDir, filename); + if (!fs.existsSync(filePath)) { + throw new Error(`Prompt file not found: ${filePath}`); + } + return yaml.load(fs.readFileSync(filePath, 'utf8')); + } + return { + characterGeneration: load('character-generation.yml'), + textRewriter: load('text-rewriter.yml'), + commandTranslator: load('command-translator.yml'), + outputEvaluator: load('output-evaluator.yml'), + }; +} +function renderTemplate(template, vars) { + return template.replace(/\{\{(\w+)\}\}/g, (_, key) => vars[key] ?? ''); +} +function logLlmError(scope, err) { + if (axios_1.default.isAxiosError(err)) { + const ax = err; + console.error(`[ZorkLlm] ${scope} failed: ${ax.message}`); + if (ax.response) { + console.error(`[ZorkLlm] ${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.'); + } + } + return; + } + console.error(`[ZorkLlm] ${scope} failed:`, err); +} +// --------------------------------------------------------------------------- +// ZorkLlmEngine +// --------------------------------------------------------------------------- +class ZorkLlmEngine { + constructor() { + this.zork = new ZorkProcess(); + this.session = null; + this.resolvedFallbackModel = null; + this.llmCallCounter = 0; + const apiKey = process.env.OPENROUTER_API_KEY; + const model = process.env.OPENROUTER_MODEL; + if (!apiKey || !model) { + throw new Error('Missing required environment variables: OPENROUTER_API_KEY and OPENROUTER_MODEL'); + } + const replacement = ZorkLlmEngine.DEPRECATED_MODEL_REPLACEMENTS[model] ?? null; + if (replacement) { + this.model = replacement; + console.warn(`[ZorkLlm] Replacing deprecated model '${model}' with '${replacement}'.`); + } + else { + this.model = model; + } + debugLog('active LLM model configured', { + 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(process.env.ZORK_STORY_FILE ?? './data/z-code/zork1.bin'); + const promptDir = path.resolve('./data/zork-prompts'); + this.prompts = loadPrompts(promptDir); + this.llm = axios_1.default.create({ + baseURL: 'https://openrouter.ai/api/v1', + headers: { + Authorization: `Bearer ${apiKey}`, + 'Content-Type': 'application/json', + }, + }); + } + async createCompletion(payload) { + const withConfiguredModel = { + ...withReasoningDefaults(payload, this.model), + model: this.model, + }; + const callId = ++this.llmCallCounter; + debugLog(`LLM call #${callId} request`, { + model: this.model, + payload: compactText(JSON.stringify(withConfiguredModel, null, 2)), + }); + try { + const response = await this.llm.post('/chat/completions', withConfiguredModel); + debugLog(`LLM call #${callId} response`, { + model: this.model, + status: response.status, + data: compactText(JSON.stringify(response.data, null, 2)), + }); + return response; + } + catch (err) { + 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}'.`); + const withFallbackModel = { + ...withReasoningDefaults(payload, fallbackModel), + model: fallbackModel, + }; + debugLog(`LLM call #${callId} fallback request`, { + model: fallbackModel, + payload: compactText(JSON.stringify(withFallbackModel, null, 2)), + }); + const fallbackResponse = await this.llm.post('/chat/completions', withFallbackModel); + debugLog(`LLM call #${callId} fallback response`, { + model: fallbackModel, + status: fallbackResponse.status, + data: compactText(JSON.stringify(fallbackResponse.data, null, 2)), + }); + return fallbackResponse; + } + debugLog(`LLM call #${callId} error`, { + message: err instanceof Error ? err.message : String(err), + }); + throw err; + } + } + async resolveFallbackModel() { + if (this.resolvedFallbackModel) + return this.resolvedFallbackModel; + const preferred = [ + process.env.OPENROUTER_FALLBACK_MODEL, + 'openai/gpt-5.5', + 'openai/gpt-5.4', + 'openai/gpt-5.4-mini', + 'openai/gpt-5.4-nano', + 'openai/gpt-5.3-chat', + '~anthropic/claude-sonnet-latest', + '~anthropic/claude-opus-latest', + 'anthropic/claude-sonnet-4.6', + 'anthropic/claude-sonnet-4', + 'openai/gpt-4o-mini', + ].filter((v) => Boolean(v && v.trim())); + try { + const response = await this.llm.get('/models'); + const ids = new Set(Array.isArray(response.data?.data) + ? response.data.data + .map((m) => (typeof m?.id === 'string' ? m.id : null)) + .filter((id) => Boolean(id)) + : []); + debugLog('OpenRouter model list fetched for fallback resolution', { + preferred, + availableCount: ids.size, + }); + for (const candidate of preferred) { + if (ids.has(candidate)) { + this.resolvedFallbackModel = candidate; + return candidate; + } + } + const firstAvailable = response.data?.data?.[0]?.id; + if (typeof firstAvailable === 'string' && firstAvailable.length > 0) { + this.resolvedFallbackModel = firstAvailable; + return firstAvailable; + } + } + catch (err) { + logLlmError('resolveFallbackModel', err); + } + this.resolvedFallbackModel = 'openai/gpt-4o-mini'; + return this.resolvedFallbackModel; + } + // ---- Public API ----------------------------------------------------------- + isRunning() { + return this.session?.running === true && this.zork.isAlive(); + } + /** + * Start a new game: launch Zork, 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 (!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); + debugLog('Z-machine intro output', compactText(rawIntro)); + // Generate the player character before showing any text + const characterDescription = await this.generateCharacter(); + this.session = { + characterDescription, + notes: [], + roomHistory: {}, + currentRoom: extractRoomName(rawIntro) ?? 'Unknown Location', + recentParagraphs: [], + rawTranscript: [`[intro]\n${rawIntro}`], + turnCount: 0, + timeOfDay: timeOfDayForTurn(0), + weather: pickInitialWeather(), + virtualInventory: [], + running: true, + }; + // Rewrite the opening text with the character's narrative voice + debugLog('session initialized', { + currentRoom: this.session.currentRoom, + characterDescription, + timeOfDay: this.session.timeOfDay, + weather: this.session.weather, + }); + const introText = await this.rewriteText(rawIntro); + this.appendRecentParagraph(introText); + this.appendRoomHistory(this.session.currentRoom, introText); + return this.buildTurnResult(introText); + } + /** + * Process player free-text input. Returns the next TurnResult. + */ + async processInput(userInput) { + if (!this.session?.running) { + throw new Error('No active game session'); + } + debugLog('processInput start', { + userInput, + currentRoom: this.session.currentRoom, + turnCount: this.session.turnCount, + timeOfDay: this.session.timeOfDay, + weather: this.session.weather, + notes: this.session.notes, + virtualInventory: this.session.virtualInventory, + }); + this.advanceNarratorState(); + const deterministicCommands = this.getDeterministicCommandPlan(userInput); + if (deterministicCommands.length > 0) { + debugLog('deterministic command plan selected', { + userInput, + commands: deterministicCommands, + }); + return this.runCommandPlan(userInput, deterministicCommands); + } + const cmdResponse = await this.translateCommand(userInput); + debugLog('command translator parsed response', cmdResponse); + // Execute any tool calls first + if (cmdResponse.type === 'tools') { + for (const tool of cmdResponse.tools) { + this.executeTool(tool); + } + // If the translator also supplied a Zork 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})`); + this.appendRecentParagraph(ack); + return this.buildTurnResult(ack); + } + } + if (cmdResponse.type === 'reply') { + this.appendRecentParagraph(cmdResponse.text); + return this.buildTurnResult(cmdResponse.text); + } + const commands = this.extractCommands(cmdResponse); + if (commands.length === 0) { + const fallback = await this.rewriteText("You hesitate, uncertain what action to take."); + this.appendRecentParagraph(fallback); + return this.buildTurnResult(fallback); + } + return this.runCommandPlan(userInput, commands); + } + async runCommandPlan(userInput, commands) { + const texts = []; + for (const command of commands) { + const text = await this.runSingleCommandLoop(userInput, command); + texts.push(text); + if (!this.isRunning()) + break; + } + const combined = texts.join('\n\n'); + return this.buildTurnResult(combined); + } + /** + * Save the current game state. Returns a JSON string suitable for storing + * in the socket's save-game slot map. + */ + async saveGame() { + if (!this.session) + throw new Error('No active session to save'); + const tmpFile = path.join(os.tmpdir(), `zork-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 = ''; + if (fs.existsSync(tmpFile)) { + zorkSave = fs.readFileSync(tmpFile).toString('base64'); + } + return JSON.stringify({ session: this.session, zorkSave }); + } + finally { + if (fs.existsSync(tmpFile)) + fs.unlinkSync(tmpFile); + } + } + /** + * Load a previously saved game. Returns the first TurnResult after restore. + */ + 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`); + 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); + this.session = { ...session, running: true }; + (_a = this.session).rawTranscript ?? (_a.rawTranscript = []); + (_b = this.session).recentParagraphs ?? (_b.recentParagraphs = []); + (_c = this.session).virtualInventory ?? (_c.virtualInventory = []); + (_d = this.session).turnCount ?? (_d.turnCount = 0); + (_e = this.session).timeOfDay ?? (_e.timeOfDay = timeOfDayForTurn(this.session.turnCount)); + (_f = this.session).weather ?? (_f.weather = pickInitialWeather()); + const text = await this.rewriteText(restoreOutput); + this.appendRecentParagraph(text); + return this.buildTurnResult(text); + } + finally { + if (fs.existsSync(tmpFile)) + fs.unlinkSync(tmpFile); + } + } + // ---- Core game loop ------------------------------------------------------- + async runSingleCommandLoop(userIntent, firstCommand) { + let command = firstCommand; + let lastOutput = ''; + for (let attempt = 1; attempt <= this.maxRetries; attempt++) { + debugLog('sending Z-machine command', { + userIntent, + command, + attempt, + maxRetries: this.maxRetries, + }); + const rawOutput = await this.zork.sendLine(command); + lastOutput = rawOutput; + this.appendRawTranscript(command, rawOutput); + debugLog('received Z-machine output', { + command, + attempt, + output: compactText(rawOutput), + }); + const newRoom = extractRoomName(rawOutput); + if (newRoom) { + this.session.currentRoom = newRoom; + debugLog('current room updated', newRoom); + } + if (isReadCommand(command) && !isParserComplaint(rawOutput)) { + const exactText = formatExactReadOutput(command, rawOutput); + debugLog('accepted exact READ output without LLM paraphrase', { + command, + text: compactText(exactText), + }); + this.appendRecentParagraph(exactText); + this.appendRoomHistory(this.session.currentRoom, exactText); + return exactText; + } + const evalResponse = await this.evaluateOutput(userIntent, command, rawOutput, attempt); + debugLog('output evaluator decision', evalResponse); + if (evalResponse.decision === 'accept') { + this.appendRecentParagraph(evalResponse.text); + this.appendRoomHistory(this.session.currentRoom, evalResponse.text); + return evalResponse.text; + } + // Retry with the LLM-suggested command + if (attempt < this.maxRetries) { + debugLog('retrying with evaluator command', { + previousCommand: command, + nextCommand: evalResponse.command, + }); + command = evalResponse.command; + } + } + // Max retries exceeded — force a rewrite of the last output + const fallbackText = await this.rewriteText(lastOutput); + this.appendRecentParagraph(fallbackText); + this.appendRoomHistory(this.session.currentRoom, fallbackText); + return fallbackText; + } + // ---- LLM calls ------------------------------------------------------------ + async generateCharacter() { + const cfg = this.prompts.characterGeneration; + try { + const response = await this.createCompletion({ + messages: [ + { role: 'system', content: cfg.system }, + { role: 'user', content: 'Create the player character now.' }, + ], + temperature: 0.9, + max_tokens: 600, + }); + return getAssistantContent(response.data).trim(); + } + catch (err) { + logLlmError('generateCharacter', err); + 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) { + const cfg = this.prompts.textRewriter; + const vars = this.buildCommonVars(); + vars['zorkOutput'] = zorkOutput; + try { + const response = await this.createCompletion({ + messages: [ + { role: 'system', content: cfg.system }, + { role: 'user', content: renderTemplate(cfg.user_template, vars) }, + ], + temperature: 0.75, + max_tokens: 800, + }); + return getAssistantContent(response.data).trim(); + } + catch (err) { + logLlmError('rewriteText', err); + return zorkOutput; + } + } + async translateCommand(userInput) { + const cfg = this.prompts.commandTranslator; + const vars = this.buildCommonVars(); + vars['userInput'] = userInput; + try { + const response = await this.createCompletion({ + messages: [ + { role: 'system', content: cfg.system }, + { role: 'user', content: renderTemplate(cfg.user_template, vars) }, + ], + temperature: 0.2, + max_tokens: 300, + response_format: { type: 'json_object' }, + }); + const parsed = JSON.parse(getAssistantContent(response.data)); + return parsed; + } + catch (err) { + logLlmError('translateCommand', err); + // Fallback: pass input directly to Zork parser + return { type: 'command', command: userInput.toUpperCase() }; + } + } + async evaluateOutput(userIntent, commandTried, zorkOutput, attempt) { + const cfg = this.prompts.outputEvaluator; + const vars = this.buildCommonVars(); + vars['userIntent'] = userIntent; + vars['commandTried'] = commandTried; + vars['zorkOutput'] = zorkOutput; + vars['attempt'] = String(attempt); + vars['maxAttempts'] = String(this.maxRetries); + try { + const response = await this.createCompletion({ + messages: [ + { role: 'system', content: cfg.system }, + { role: 'user', content: renderTemplate(cfg.user_template, vars) }, + ], + temperature: 0.3, + max_tokens: 500, + response_format: { type: 'json_object' }, + }); + return JSON.parse(getAssistantContent(response.data)); + } + catch (err) { + logLlmError('evaluateOutput', err); + // Fallback: accept the raw output as-is + return { decision: 'accept', text: zorkOutput }; + } + } + // ---- Session helpers ------------------------------------------------------- + executeTool(tool) { + if (!this.session) + return; + debugLog('executing tool call', tool); + switch (tool.name) { + case 'update_character': + if (typeof tool.args['description'] === 'string') { + this.session.characterDescription = tool.args['description']; + debugLog('tool updated character', this.session.characterDescription); + } + break; + case 'add_note': + if (typeof tool.args['note'] === 'string') { + this.session.notes.push(tool.args['note']); + debugLog('tool added note', { + note: tool.args['note'], + notes: this.session.notes, + }); + } + break; + case 'remove_note': { + const idx = Number(tool.args['index']); + if (Number.isInteger(idx) && + idx >= 0 && + idx < this.session.notes.length) { + this.session.notes.splice(idx, 1); + debugLog('tool removed note', { + index: idx, + notes: this.session.notes, + }); + } + break; + } + case 'add_inventory_item': { + const item = String(tool.args['item'] ?? '').trim(); + if (!item) + break; + const exists = this.session.virtualInventory.some((it) => it.toLowerCase() === item.toLowerCase()); + if (!exists) + this.session.virtualInventory.push(item); + debugLog('tool added inventory item', { + item, + virtualInventory: this.session.virtualInventory, + }); + break; + } + case 'remove_inventory_item': { + const item = String(tool.args['item'] ?? '').trim(); + if (!item) + break; + this.session.virtualInventory = this.session.virtualInventory.filter((it) => it.toLowerCase() !== item.toLowerCase()); + debugLog('tool removed inventory item', { + item, + virtualInventory: this.session.virtualInventory, + }); + break; + } + } + } + appendRecentParagraph(text) { + if (!this.session) + return; + const trimmed = text.trim(); + if (!trimmed) + return; + this.session.recentParagraphs.push(trimmed); + if (this.session.recentParagraphs.length > 10) { + this.session.recentParagraphs.splice(0, this.session.recentParagraphs.length - 10); + } + } + extractCommands(cmdResponse) { + const list = []; + if (cmdResponse.type === 'command') { + list.push(cmdResponse.command); + } + else if (cmdResponse.type === 'commands') { + list.push(...cmdResponse.commands); + } + else if (cmdResponse.type === 'tools') { + if (cmdResponse.command) + list.push(cmdResponse.command); + if (Array.isArray(cmdResponse.commands)) + list.push(...cmdResponse.commands); + } + return list + .map((c) => String(c).trim()) + .filter(Boolean) + .map((c) => c.toUpperCase()); + } + appendRawTranscript(command, output) { + if (!this.session) + return; + this.session.rawTranscript.push([`> ${command}`, output.trim()].filter(Boolean).join('\n')); + if (this.session.rawTranscript.length > 12) { + this.session.rawTranscript.splice(0, this.session.rawTranscript.length - 12); + } + } + advanceNarratorState() { + if (!this.session) + return; + this.session.turnCount += 1; + this.session.timeOfDay = timeOfDayForTurn(this.session.turnCount); + this.session.weather = evolveWeather(this.session.weather, this.session.turnCount); + debugLog('narrator state advanced', { + turnCount: this.session.turnCount, + timeOfDay: this.session.timeOfDay, + weather: this.session.weather, + }); + } + getDeterministicCommandPlan(userInput) { + const normalized = userInput.toLowerCase(); + const context = [ + this.session?.currentRoom ?? '', + this.session?.recentParagraphs.join('\n') ?? '', + Object.values(this.session?.roomHistory ?? {}).flat().join('\n'), + ].join('\n').toLowerCase(); + const mentionsLeaflet = /\b(leaflet|pamphlet|brochure|paper|it|this)\b/.test(normalized); + const contextHasLeaflet = /\b(leaflet|pamphlet|brochure)\b/.test(context); + const mentionsMailbox = /\bmail\s*box|mailbox\b/.test(normalized); + const asksToRead = /\bread\b/.test(normalized) || + /\bwhat (does|did|do).*say\b/.test(normalized) || + /\btell me what it says\b/.test(normalized) || + /\byou did not tell me\b/.test(normalized); + const asksToTake = /\b(take|get|grab|pick up|pluck)\b/.test(normalized); + const asksToOpen = /\bopen\b/.test(normalized); + const asksToLookIn = /\blook (in|inside|into)\b/.test(normalized) || /\binside\b/.test(normalized); + if (mentionsMailbox && asksToOpen && asksToLookIn) { + return ['OPEN MAILBOX', 'LOOK IN MAILBOX']; + } + if (mentionsMailbox && asksToOpen) { + return ['OPEN MAILBOX']; + } + if (asksToRead && (mentionsLeaflet || mentionsMailbox || contextHasLeaflet)) { + if (asksToTake || mentionsMailbox) { + return ['TAKE LEAFLET', 'READ LEAFLET']; + } + return ['READ LEAFLET']; + } + if (asksToTake && (mentionsLeaflet || (mentionsMailbox && contextHasLeaflet))) { + return ['TAKE LEAFLET']; + } + return []; + } + appendRoomHistory(room, text) { + if (!this.session) + return; + const history = this.session.roomHistory[room] ?? []; + history.push(text); + if (history.length > this.historySize) { + history.splice(0, history.length - this.historySize); + } + this.session.roomHistory[room] = history; + } + buildCommonVars() { + const s = this.session; + const notes = s.notes.length > 0 + ? s.notes.map((n, i) => `${i + 1}. ${n}`).join('\n') + : '(none)'; + const virtualInventory = s.virtualInventory.length > 0 + ? s.virtualInventory.map((n, i) => `${i + 1}. ${n}`).join('\n') + : '(none)'; + const recentNarrative = s.recentParagraphs.length > 0 + ? s.recentParagraphs.join('\n\n---\n\n') + : '(none)'; + const rawTranscript = s.rawTranscript.length > 0 + ? s.rawTranscript.join('\n\n---\n\n') + : '(none)'; + const history = (s.roomHistory[s.currentRoom] ?? []).join('\n\n---\n\n'); + return { + characterDescription: s.characterDescription, + notes, + virtualInventory, + recentNarrative, + rawTranscript, + roomHistory: history || '(no prior visits)', + currentRoom: s.currentRoom, + narratorState: [ + `Turn count: ${s.turnCount}`, + `Time of day: ${s.timeOfDay}`, + `Outside weather drift: ${s.weather}`, + ].join('\n'), + }; + } + buildTurnResult(text) { + const alive = this.zork.isAlive(); + if (!alive && this.session) + this.session.running = false; + return { + paragraphs: [{ text, tags: [] }], + choices: [], + inputMode: alive ? 'text' : 'end', + gameState: { statusLine: this.session?.currentRoom }, + }; + } +} +exports.ZorkLlmEngine = ZorkLlmEngine; +ZorkLlmEngine.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 \ No newline at end of file diff --git a/dist/engine/zork-llm-engine.js.map b/dist/engine/zork-llm-engine.js.map new file mode 100644 index 0000000..1069c82 --- /dev/null +++ b/dist/engine/zork-llm-engine.js.map @@ -0,0 +1 @@ +{"version":3,"file":"zork-llm-engine.js","sourceRoot":"","sources":["../../src/engine/zork-llm-engine.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/dist/server-zork.d.ts b/dist/server-zork.d.ts new file mode 100644 index 0000000..ec9b845 --- /dev/null +++ b/dist/server-zork.d.ts @@ -0,0 +1,16 @@ +/** + * 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 + */ +export {}; diff --git a/dist/server-zork.js b/dist/server-zork.js new file mode 100644 index 0000000..6ef0cce --- /dev/null +++ b/dist/server-zork.js @@ -0,0 +1,343 @@ +"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 +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 \ No newline at end of file diff --git a/dist/server-zork.js.map b/dist/server-zork.js.map new file mode 100644 index 0000000..961f633 --- /dev/null +++ b/dist/server-zork.js.map @@ -0,0 +1 @@ +{"version":3,"file":"server-zork.js","sourceRoot":"","sources":["../src/server-zork.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 07d116b..270c4b5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "dotenv": "^16.4.7", "express": "^5.1.0", "hyphenopoly": "^6.0.0", + "ifvms": "^1.1.6", "js-yaml": "^4.1.0", "kokoro-js": "^1.2.0", "openai": "^4.91.0", @@ -32,6 +33,9 @@ "ts-jest": "^29.3.1", "ts-node": "^10.9.2", "typescript": "^5.8.2" + }, + "engines": { + "node": ">=18.17" } }, "node_modules/@ampproject/remapping": { @@ -2314,7 +2318,6 @@ "version": "4.3.2", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", - "dev": true, "license": "MIT", "dependencies": { "type-fest": "^0.21.3" @@ -2330,7 +2333,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -2340,7 +2342,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -2707,7 +2708,6 @@ "version": "5.3.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -3031,6 +3031,15 @@ } } }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/dedent": { "version": "1.5.3", "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.3.tgz", @@ -3192,7 +3201,6 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, "license": "MIT" }, "node_modules/encodeurl": { @@ -3913,7 +3921,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, "license": "MIT", "dependencies": { "locate-path": "^5.0.0", @@ -4088,7 +4095,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, "license": "ISC", "engines": { "node": "6.* || 8.* || >= 10.*" @@ -4154,6 +4160,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/glkote-term": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/glkote-term/-/glkote-term-0.4.4.tgz", + "integrity": "sha512-5l2t4QC9Pr4DgMz/OBGojgaAZJ3p0yf+e8pIYuz63kT0gBaHqsAuASYWQVqSkj60v6nUxKYJRzE0GQucf9PDxg==", + "license": "MIT", + "dependencies": { + "ansi-escapes": "^4.0.0" + } + }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -4343,6 +4358,89 @@ "node": ">=0.10.0" } }, + "node_modules/ifvms": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/ifvms/-/ifvms-1.1.6.tgz", + "integrity": "sha512-4OPV23gHu/YsyqcUuV4oqVBkicz6KsFdwKyMQkaUeN6nvv4maGcYA5qgjDse/iEdvsqSijLHRbx5VuM0zuXEMQ==", + "license": "MIT", + "dependencies": { + "glkote-term": "^0.4.0", + "mute-stream": "0.0.8", + "yargs": "^15.0.1" + }, + "bin": { + "zvm": "bin/zvm.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/ifvms/node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/ifvms/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ifvms/node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "license": "ISC" + }, + "node_modules/ifvms/node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "license": "MIT", + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ifvms/node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "license": "ISC", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -4494,7 +4592,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -5396,7 +5493,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, "license": "MIT", "dependencies": { "p-locate": "^4.1.0" @@ -5628,6 +5724,12 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "license": "ISC" + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -5968,7 +6070,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, "license": "MIT", "dependencies": { "p-limit": "^2.2.0" @@ -5981,7 +6082,6 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, "license": "MIT", "dependencies": { "p-try": "^2.0.0" @@ -5997,7 +6097,6 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -6048,7 +6147,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -6358,12 +6456,17 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" } }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "license": "ISC" + }, "node_modules/resolve": { "version": "1.22.10", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", @@ -6542,6 +6645,12 @@ "node": ">= 18" } }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC" + }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -6983,7 +7092,6 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -6998,7 +7106,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -7319,7 +7426,6 @@ "version": "0.21.3", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", - "dev": true, "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=10" @@ -7501,6 +7607,12 @@ "node": ">= 8" } }, + "node_modules/which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", + "license": "ISC" + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", diff --git a/package.json b/package.json index e7388c1..d269bdb 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,14 @@ "dev:web": "nodemon --watch src --ext ts,json --exec \"ts-node src/index.ts\"", "predev:cli": "npm run check:node", "dev:cli": "nodemon --watch src --ext ts,json --exec \"ts-node src/index.ts --cli\"", + "predev:zork": "npm run check:node", + "dev:zork": "nodemon --watch src --watch data/zork-prompts --ext ts,json,yml --exec \"ts-node src/server-zork.ts\"", + "dev:zork:debug": "powershell -NoProfile -Command \"$env:ZORK_DEBUG='1'; npm run dev:zork\"", + "dev:zork:inspect": "powershell -NoProfile -Command \"$env:ZORK_DEBUG='1'; nodemon --watch src --watch data/zork-prompts --ext ts,json,yml --exec \\\"node --inspect=127.0.0.1:9229 -r ts-node/register src/server-zork.ts\\\"\"", + "prestart:zork": "npm run check:node && npm run build", + "start:zork": "node dist/server-zork.js", + "start:zork:debug": "powershell -NoProfile -Command \"$env:ZORK_DEBUG='1'; npm run start:zork\"", + "start:zork:inspect": "powershell -NoProfile -Command \"$env:ZORK_DEBUG='1'; node --inspect=127.0.0.1:9229 dist/server-zork.js\"", "pretest-server": "npm run check:node", "test-server": "ts-node src/test-server.ts", "build": "tsc", @@ -51,6 +59,7 @@ "dotenv": "^16.4.7", "express": "^5.1.0", "hyphenopoly": "^6.0.0", + "ifvms": "^1.1.6", "js-yaml": "^4.1.0", "kokoro-js": "^1.2.0", "openai": "^4.91.0", diff --git a/src/engine/zork-llm-engine.ts b/src/engine/zork-llm-engine.ts new file mode 100644 index 0000000..ea53420 --- /dev/null +++ b/src/engine/zork-llm-engine.ts @@ -0,0 +1,1158 @@ +/** + * Zork LLM Engine + * + * Runs Zork I (or any 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 + */ + +import { spawn, ChildProcess } from 'child_process'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as os from 'os'; +import * as yaml from 'js-yaml'; +import axios, { AxiosError, AxiosInstance } from 'axios'; +import * as dotenv from 'dotenv'; + +dotenv.config(); + +const DEBUG_ENABLED = /^(1|true|yes|on)$/i.test(process.env.ZORK_DEBUG ?? ''); + +function debugLog(message: string, details?: unknown): void { + if (!DEBUG_ENABLED) return; + if (typeof details === 'undefined') { + console.log(`[ZorkLlm:debug] ${message}`); + return; + } + console.log(`[ZorkLlm:debug] ${message}`, details); +} + +function compactText(text: string, maxLength = 12_000): string { + if (text.length <= maxLength) return text; + return `${text.slice(0, maxLength)}\n...[truncated ${text.length - maxLength} chars]`; +} + +function getAssistantContent(data: any): string { + const content = data?.choices?.[0]?.message?.content; + if (typeof content === 'string') return content; + if (Array.isArray(content)) { + return content + .map((part) => { + if (typeof part === 'string') return part; + if (typeof part?.text === 'string') return part.text; + if (typeof part?.content === 'string') return part.content; + return ''; + }) + .join('') + .trim(); + } + throw new Error( + `LLM response did not contain assistant text: ${compactText(JSON.stringify(data))}`, + ); +} + +function withReasoningDefaults( + payload: Record, + model: string, +): Record { + if (payload.reasoning || !/\bgpt-5/i.test(model)) return payload; + return { + ...payload, + reasoning: { + effort: process.env.OPENROUTER_REASONING_EFFORT ?? 'none', + exclude: true, + }, + }; +} + +// --------------------------------------------------------------------------- +// Types +// --------------------------------------------------------------------------- + +export interface ZorkSession { + characterDescription: string; + notes: string[]; + recentParagraphs: string[]; + rawTranscript: string[]; + turnCount: number; + timeOfDay: string; + weather: string; + virtualInventory: string[]; + /** roomName → last N player-facing output strings */ + roomHistory: Record; + currentRoom: string; + running: boolean; +} + +/** Subset of the unified TurnResult protocol understood by the client. */ +export interface ZorkTurnResult { + paragraphs: Array<{ text: string; tags: unknown[] }>; + choices: unknown[]; + inputMode: 'text' | 'end'; + gameState?: { statusLine?: string }; +} + +interface PromptConfig { + system: string; + user_template: string; +} + +interface ZorkPrompts { + characterGeneration: PromptConfig; + textRewriter: PromptConfig; + commandTranslator: PromptConfig; + outputEvaluator: PromptConfig; +} + +// LLM response shapes --------------------------------------------------------- + +type CommandResponse = + | { type: 'command'; command: string } + | { type: 'commands'; commands: string[] } + | { type: 'reply'; text: string } + | { type: 'tools'; tools: ToolCall[]; command?: string; commands?: string[] }; + +interface ToolCall { + name: + | 'update_character' + | 'add_note' + | 'remove_note' + | 'add_inventory_item' + | 'remove_inventory_item'; + args: Record; +} + +type EvaluatorResponse = + | { decision: 'accept'; text: string } + | { decision: 'retry'; command: string }; + +// --------------------------------------------------------------------------- +// Utility: strip ANSI escape sequences +// --------------------------------------------------------------------------- + +function stripAnsi(s: string): string { + // eslint-disable-next-line no-control-regex + return s.replace(/\x1B\[[0-9;]*[mGKHFJA-Z]/g, ''); +} + +// --------------------------------------------------------------------------- +// Utility: extract the current room name from Z-machine output +// --------------------------------------------------------------------------- + +function extractRoomName(output: string): string | null { + const lines = output + .split('\n') + .map(l => l.trim()) + .filter(l => l.length > 0); + if (lines.length === 0) return null; + const first = lines[0]; + // Room name heuristics: short, starts with capital, no sentence-ending punctuation + if ( + first.length < 65 && + /^[A-Z]/.test(first) && + !/[.!?]$/.test(first) && + !/^(You |I |It |There |The [a-z])/.test(first) + ) { + return first; + } + return null; +} + +function isReadCommand(command: string): boolean { + return /^READ\b/i.test(command.trim()); +} + +function isParserComplaint(output: string): boolean { + const text = output.toLowerCase(); + return [ + "i don't know the word", + "i don't understand", + "that's not a verb", + "you can't see any", + "you don't have", + "you aren't carrying", + "what do you want to", + "what do you want to read", + "what do you want to take", + "which do you mean", + "there is no", + ].some(fragment => text.includes(fragment)); +} + +function formatExactReadOutput(command: string, zorkOutput: string): string { + const object = command.replace(/^READ\s+/i, '').trim().toLowerCase(); + const label = object ? `the ${object}` : 'it'; + const cleanedOutput = zorkOutput + .split('\n') + .filter((line, index) => index !== 0 || line.trim().toUpperCase() !== command.trim().toUpperCase()) + .join('\n') + .trim(); + return `You read ${label}.\n\n${cleanedOutput}`; +} + +function pickInitialWeather(): string { + const options = [ + 'cool, unsettled air under a low grey sky', + 'a dry bright afternoon with thin wind moving through the grass', + 'misty weather with damp earth-smell clinging to everything outside', + 'a mild overcast day, quiet enough that small sounds carry', + ]; + return options[Math.floor(Math.random() * options.length)]; +} + +function timeOfDayForTurn(turnCount: number): string { + const phases = [ + 'late morning', + 'early afternoon', + 'late afternoon', + 'dusk', + 'early evening', + 'night', + 'deep night', + 'pre-dawn', + 'morning', + ]; + return phases[Math.floor(turnCount / 12) % phases.length]; +} + +function evolveWeather(previous: string, turnCount: number): string { + if (turnCount > 0 && turnCount % 9 !== 0) return previous; + const transitions = [ + 'the air has cooled and carries a faint mineral dampness', + 'the wind has shifted, restless but not yet stormy', + 'the light has thinned behind a veil of cloud', + 'the weather holds steady, quiet and watchful', + 'a trace of moisture gathers in the air', + ]; + return transitions[Math.floor(turnCount / 9) % transitions.length]; +} + +// --------------------------------------------------------------------------- +// ZorkProcess – manages the ifvms zvm child process +// --------------------------------------------------------------------------- + +class ZorkProcess { + private proc: ChildProcess | null = null; + private outputBuffer = ''; + private pendingResolve: ((text: string) => void) | null = null; + private debounceTimer: ReturnType | null = null; + + /** Start the Z-machine with the given story file, return the opening text. */ + async launch(storyPath: string): Promise { + const zvm = this.locateZvm(); + this.proc = spawn(zvm, [storyPath], { + stdio: ['pipe', 'pipe', 'pipe'], + shell: true, + cwd: process.cwd(), + }); + + this.proc.stdout!.on('data', (chunk: Buffer) => { + this.outputBuffer += stripAnsi(chunk.toString()); + this.scheduleResolve(); + }); + + this.proc.stderr!.on('data', (chunk: Buffer) => { + // Log but don't throw – ifvms may emit warnings on stderr + console.warn('[zvm]', chunk.toString().trim()); + }); + + this.proc.on('exit', () => { + // If the process exits while we are waiting for output, resolve immediately + if (this.pendingResolve) { + const resolver = this.pendingResolve; + this.pendingResolve = null; + resolver(this.outputBuffer.trim()); + this.outputBuffer = ''; + } + this.proc = null; + }); + + return this.waitForPrompt(); + } + + /** Send a line of input and return all output until the next prompt. */ + async sendLine(text: string): Promise { + if (!this.proc) throw new Error('Z-machine process is not running'); + this.outputBuffer = ''; + this.proc.stdin!.write(text + '\n'); + return this.waitForPrompt(); + } + + isAlive(): boolean { + return this.proc !== null && !this.proc.killed; + } + + kill(): void { + if (this.proc) { + this.proc.kill(); + this.proc = null; + } + } + + // ---- private ---- + + private waitForPrompt(): Promise { + return new Promise((resolve) => { + // Wrap to allow debounce timer to cancel a previous waiter safely + const wrapped = (text: string) => resolve(text); + this.pendingResolve = wrapped; + + // Safety timeout: if no prompt detected after 15 s, resolve with what we have + const safety = setTimeout(() => { + if (this.pendingResolve === wrapped) { + this.pendingResolve = null; + const text = this.outputBuffer.trim(); + this.outputBuffer = ''; + resolve(text); + } + }, 15_000); + + // Ensure the safety timeout does not keep Node alive indefinitely + if (safety.unref) safety.unref(); + + // Override so debounce also cancels the safety timer + this.pendingResolve = (text: string) => { + clearTimeout(safety); + resolve(text); + }; + + // Data may already be buffered + this.scheduleResolve(); + }); + } + + /** Debounced check: resolve when the buffer ends with Zork's '>' prompt. */ + private scheduleResolve(): void { + if (!/\n>\s*$/.test(this.outputBuffer)) return; + + if (this.debounceTimer) clearTimeout(this.debounceTimer); + this.debounceTimer = setTimeout(() => { + this.debounceTimer = null; + if (!this.pendingResolve) return; + const text = this.outputBuffer.replace(/\n>\s*$/, '').trim(); + this.outputBuffer = ''; + const resolver = this.pendingResolve; + this.pendingResolve = null; + resolver(text); + }, 80); + } + + private locateZvm(): string { + const binDir = path.join(process.cwd(), 'node_modules', '.bin'); + const candidates = + process.platform === 'win32' + ? ['zvm.cmd', 'zvm.ps1', 'zvm'] + : ['zvm']; + for (const name of candidates) { + const full = path.join(binDir, name); + if (fs.existsSync(full)) return full; + } + // Fall through to shell PATH lookup (works if ifvms is installed globally) + return 'zvm'; + } +} + +// --------------------------------------------------------------------------- +// Prompt loader +// --------------------------------------------------------------------------- + +function loadPrompts(promptDir: string): ZorkPrompts { + function load(filename: string): PromptConfig { + const filePath = path.join(promptDir, filename); + if (!fs.existsSync(filePath)) { + throw new Error(`Prompt file not found: ${filePath}`); + } + return yaml.load(fs.readFileSync(filePath, 'utf8')) as PromptConfig; + } + return { + characterGeneration: load('character-generation.yml'), + textRewriter: load('text-rewriter.yml'), + commandTranslator: load('command-translator.yml'), + outputEvaluator: load('output-evaluator.yml'), + }; +} + +function renderTemplate(template: string, vars: Record): string { + return template.replace(/\{\{(\w+)\}\}/g, (_, key) => vars[key] ?? ''); +} + +function logLlmError(scope: string, err: unknown): void { + if (axios.isAxiosError(err)) { + const ax = err as AxiosError; + console.error(`[ZorkLlm] ${scope} failed: ${ax.message}`); + if (ax.response) { + console.error( + `[ZorkLlm] ${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.', + ); + } + } + return; + } + + console.error(`[ZorkLlm] ${scope} failed:`, err); +} + +// --------------------------------------------------------------------------- +// ZorkLlmEngine +// --------------------------------------------------------------------------- + +export class ZorkLlmEngine { + private zork = new ZorkProcess(); + private session: ZorkSession | null = null; + private prompts: ZorkPrompts; + private llm: AxiosInstance; + private model: string; + private resolvedFallbackModel: string | null = null; + private llmCallCounter = 0; + private maxRetries: number; + private historySize: number; + private storyPath: string; + + private static readonly DEPRECATED_MODEL_REPLACEMENTS: Record = { + 'anthropic/claude-3-opus-20240229': 'openai/gpt-5.5', + 'openai/gpt-5.4-mini': 'openai/gpt-5.5', + }; + + constructor() { + const apiKey = process.env.OPENROUTER_API_KEY; + const model = process.env.OPENROUTER_MODEL; + if (!apiKey || !model) { + throw new Error( + 'Missing required environment variables: OPENROUTER_API_KEY and OPENROUTER_MODEL', + ); + } + const replacement = + ZorkLlmEngine.DEPRECATED_MODEL_REPLACEMENTS[model] ?? null; + if (replacement) { + this.model = replacement; + console.warn( + `[ZorkLlm] Replacing deprecated model '${model}' with '${replacement}'.`, + ); + } else { + this.model = model; + } + debugLog('active LLM model configured', { + 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( + process.env.ZORK_STORY_FILE ?? './data/z-code/zork1.bin', + ); + + const promptDir = path.resolve('./data/zork-prompts'); + this.prompts = loadPrompts(promptDir); + + this.llm = axios.create({ + baseURL: 'https://openrouter.ai/api/v1', + headers: { + Authorization: `Bearer ${apiKey}`, + 'Content-Type': 'application/json', + }, + }); + } + + private async createCompletion( + payload: Record, + ): Promise<{ data: any }> { + const withConfiguredModel = { + ...withReasoningDefaults(payload, this.model), + model: this.model, + }; + const callId = ++this.llmCallCounter; + debugLog(`LLM call #${callId} request`, { + model: this.model, + payload: compactText(JSON.stringify(withConfiguredModel, null, 2)), + }); + try { + const response = await this.llm.post('/chat/completions', withConfiguredModel); + debugLog(`LLM call #${callId} response`, { + model: this.model, + status: response.status, + data: compactText(JSON.stringify(response.data, null, 2)), + }); + return response; + } catch (err) { + if (axios.isAxiosError(err) && err.response?.status === 404) { + const fallbackModel = await this.resolveFallbackModel(); + this.model = fallbackModel; + console.warn( + `[ZorkLlm] Switching active model to '${fallbackModel}'.`, + ); + const withFallbackModel = { + ...withReasoningDefaults(payload, fallbackModel), + model: fallbackModel, + }; + debugLog(`LLM call #${callId} fallback request`, { + model: fallbackModel, + payload: compactText(JSON.stringify(withFallbackModel, null, 2)), + }); + const fallbackResponse = await this.llm.post( + '/chat/completions', + withFallbackModel, + ); + debugLog(`LLM call #${callId} fallback response`, { + model: fallbackModel, + status: fallbackResponse.status, + data: compactText(JSON.stringify(fallbackResponse.data, null, 2)), + }); + return fallbackResponse; + } + debugLog(`LLM call #${callId} error`, { + message: err instanceof Error ? err.message : String(err), + }); + throw err; + } + } + + private async resolveFallbackModel(): Promise { + if (this.resolvedFallbackModel) return this.resolvedFallbackModel; + + const preferred = [ + process.env.OPENROUTER_FALLBACK_MODEL, + 'openai/gpt-5.5', + 'openai/gpt-5.4', + 'openai/gpt-5.4-mini', + 'openai/gpt-5.4-nano', + 'openai/gpt-5.3-chat', + '~anthropic/claude-sonnet-latest', + '~anthropic/claude-opus-latest', + 'anthropic/claude-sonnet-4.6', + 'anthropic/claude-sonnet-4', + 'openai/gpt-4o-mini', + ].filter((v): v is string => Boolean(v && v.trim())); + + try { + const response = await this.llm.get('/models'); + const ids = new Set( + Array.isArray(response.data?.data) + ? response.data.data + .map((m: any) => (typeof m?.id === 'string' ? m.id : null)) + .filter((id: string | null): id is string => Boolean(id)) + : [], + ); + debugLog('OpenRouter model list fetched for fallback resolution', { + preferred, + availableCount: ids.size, + }); + + for (const candidate of preferred) { + if (ids.has(candidate)) { + this.resolvedFallbackModel = candidate; + return candidate; + } + } + + const firstAvailable = response.data?.data?.[0]?.id; + if (typeof firstAvailable === 'string' && firstAvailable.length > 0) { + this.resolvedFallbackModel = firstAvailable; + return firstAvailable; + } + } catch (err) { + logLlmError('resolveFallbackModel', err); + } + + this.resolvedFallbackModel = 'openai/gpt-4o-mini'; + return this.resolvedFallbackModel; + } + + // ---- Public API ----------------------------------------------------------- + + isRunning(): boolean { + return this.session?.running === true && this.zork.isAlive(); + } + + /** + * Start a new game: launch Zork, generate the player character, rewrite the + * intro text, and return the first TurnResult for the client. + */ + async newGame(): Promise { + // Kill any existing game + if (this.zork.isAlive()) this.zork.kill(); + + 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); + debugLog('Z-machine intro output', compactText(rawIntro)); + + // Generate the player character before showing any text + const characterDescription = await this.generateCharacter(); + + this.session = { + characterDescription, + notes: [], + roomHistory: {}, + currentRoom: extractRoomName(rawIntro) ?? 'Unknown Location', + recentParagraphs: [], + rawTranscript: [`[intro]\n${rawIntro}`], + turnCount: 0, + timeOfDay: timeOfDayForTurn(0), + weather: pickInitialWeather(), + virtualInventory: [], + running: true, + }; + + // Rewrite the opening text with the character's narrative voice + debugLog('session initialized', { + currentRoom: this.session.currentRoom, + characterDescription, + timeOfDay: this.session.timeOfDay, + weather: this.session.weather, + }); + const introText = await this.rewriteText(rawIntro); + this.appendRecentParagraph(introText); + this.appendRoomHistory(this.session.currentRoom, introText); + + return this.buildTurnResult(introText); + } + + /** + * Process player free-text input. Returns the next TurnResult. + */ + async processInput(userInput: string): Promise { + if (!this.session?.running) { + throw new Error('No active game session'); + } + + debugLog('processInput start', { + userInput, + currentRoom: this.session.currentRoom, + turnCount: this.session.turnCount, + timeOfDay: this.session.timeOfDay, + weather: this.session.weather, + notes: this.session.notes, + virtualInventory: this.session.virtualInventory, + }); + this.advanceNarratorState(); + + const deterministicCommands = this.getDeterministicCommandPlan(userInput); + if (deterministicCommands.length > 0) { + debugLog('deterministic command plan selected', { + userInput, + commands: deterministicCommands, + }); + return this.runCommandPlan(userInput, deterministicCommands); + } + + const cmdResponse = await this.translateCommand(userInput); + debugLog('command translator parsed response', cmdResponse); + + // Execute any tool calls first + if (cmdResponse.type === 'tools') { + for (const tool of cmdResponse.tools) { + this.executeTool(tool); + } + // If the translator also supplied a Zork 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})`, + ); + this.appendRecentParagraph(ack); + return this.buildTurnResult(ack); + } + } + + if (cmdResponse.type === 'reply') { + this.appendRecentParagraph(cmdResponse.text); + return this.buildTurnResult(cmdResponse.text); + } + + const commands = this.extractCommands(cmdResponse); + if (commands.length === 0) { + const fallback = await this.rewriteText( + "You hesitate, uncertain what action to take.", + ); + this.appendRecentParagraph(fallback); + return this.buildTurnResult(fallback); + } + + return this.runCommandPlan(userInput, commands); + } + + private async runCommandPlan( + userInput: string, + commands: string[], + ): Promise { + const texts: string[] = []; + for (const command of commands) { + const text = await this.runSingleCommandLoop(userInput, command); + texts.push(text); + if (!this.isRunning()) break; + } + + const combined = texts.join('\n\n'); + return this.buildTurnResult(combined); + } + + /** + * Save the current game state. Returns a JSON string suitable for storing + * in the socket's save-game slot map. + */ + async saveGame(): Promise { + if (!this.session) throw new Error('No active session to save'); + + const tmpFile = path.join(os.tmpdir(), `zork-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 = ''; + if (fs.existsSync(tmpFile)) { + zorkSave = fs.readFileSync(tmpFile).toString('base64'); + } + + return JSON.stringify({ session: this.session, zorkSave }); + } finally { + if (fs.existsSync(tmpFile)) fs.unlinkSync(tmpFile); + } + } + + /** + * Load a previously saved game. Returns the first TurnResult after restore. + */ + async loadGame(savedJson: string): Promise { + const { session, zorkSave } = JSON.parse(savedJson) as { + session: ZorkSession; + zorkSave: string; + }; + + if (this.zork.isAlive()) this.zork.kill(); + + const tmpFile = path.join(os.tmpdir(), `zork-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); + + this.session = { ...session, running: true }; + this.session.rawTranscript ??= []; + this.session.recentParagraphs ??= []; + this.session.virtualInventory ??= []; + this.session.turnCount ??= 0; + this.session.timeOfDay ??= timeOfDayForTurn(this.session.turnCount); + this.session.weather ??= pickInitialWeather(); + + const text = await this.rewriteText(restoreOutput); + this.appendRecentParagraph(text); + return this.buildTurnResult(text); + } finally { + if (fs.existsSync(tmpFile)) fs.unlinkSync(tmpFile); + } + } + + // ---- Core game loop ------------------------------------------------------- + + private async runSingleCommandLoop( + userIntent: string, + firstCommand: string, + ): Promise { + let command = firstCommand; + let lastOutput = ''; + + for (let attempt = 1; attempt <= this.maxRetries; attempt++) { + debugLog('sending Z-machine command', { + userIntent, + command, + attempt, + maxRetries: this.maxRetries, + }); + const rawOutput = await this.zork.sendLine(command); + lastOutput = rawOutput; + this.appendRawTranscript(command, rawOutput); + debugLog('received Z-machine output', { + command, + attempt, + output: compactText(rawOutput), + }); + + const newRoom = extractRoomName(rawOutput); + if (newRoom) { + this.session!.currentRoom = newRoom; + debugLog('current room updated', newRoom); + } + + if (isReadCommand(command) && !isParserComplaint(rawOutput)) { + const exactText = formatExactReadOutput(command, rawOutput); + debugLog('accepted exact READ output without LLM paraphrase', { + command, + text: compactText(exactText), + }); + this.appendRecentParagraph(exactText); + this.appendRoomHistory(this.session!.currentRoom, exactText); + return exactText; + } + + const evalResponse = await this.evaluateOutput( + userIntent, + command, + rawOutput, + attempt, + ); + debugLog('output evaluator decision', evalResponse); + + if (evalResponse.decision === 'accept') { + this.appendRecentParagraph(evalResponse.text); + this.appendRoomHistory(this.session!.currentRoom, evalResponse.text); + return evalResponse.text; + } + + // Retry with the LLM-suggested command + if (attempt < this.maxRetries) { + debugLog('retrying with evaluator command', { + previousCommand: command, + nextCommand: evalResponse.command, + }); + command = evalResponse.command; + } + } + + // Max retries exceeded — force a rewrite of the last output + const fallbackText = await this.rewriteText(lastOutput); + this.appendRecentParagraph(fallbackText); + this.appendRoomHistory(this.session!.currentRoom, fallbackText); + return fallbackText; + } + + // ---- LLM calls ------------------------------------------------------------ + + private async generateCharacter(): Promise { + const cfg = this.prompts.characterGeneration; + try { + const response = await this.createCompletion({ + messages: [ + { role: 'system', content: cfg.system }, + { role: 'user', content: 'Create the player character now.' }, + ], + temperature: 0.9, + max_tokens: 600, + }); + return getAssistantContent(response.data).trim(); + } catch (err) { + logLlmError('generateCharacter', err); + 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.'; + } + } + + private async rewriteText(zorkOutput: string): Promise { + const cfg = this.prompts.textRewriter; + const vars = this.buildCommonVars(); + vars['zorkOutput'] = zorkOutput; + + try { + const response = await this.createCompletion({ + messages: [ + { role: 'system', content: cfg.system }, + { role: 'user', content: renderTemplate(cfg.user_template, vars) }, + ], + temperature: 0.75, + max_tokens: 800, + }); + return getAssistantContent(response.data).trim(); + } catch (err) { + logLlmError('rewriteText', err); + return zorkOutput; + } + } + + private async translateCommand(userInput: string): Promise { + const cfg = this.prompts.commandTranslator; + const vars = this.buildCommonVars(); + vars['userInput'] = userInput; + + try { + const response = await this.createCompletion({ + messages: [ + { role: 'system', content: cfg.system }, + { role: 'user', content: renderTemplate(cfg.user_template, vars) }, + ], + temperature: 0.2, + max_tokens: 300, + response_format: { type: 'json_object' }, + }); + const parsed = JSON.parse(getAssistantContent(response.data)) as CommandResponse; + return parsed; + } catch (err) { + logLlmError('translateCommand', err); + // Fallback: pass input directly to Zork parser + return { type: 'command', command: userInput.toUpperCase() }; + } + } + + private async evaluateOutput( + userIntent: string, + commandTried: string, + zorkOutput: string, + attempt: number, + ): Promise { + const cfg = this.prompts.outputEvaluator; + const vars = this.buildCommonVars(); + vars['userIntent'] = userIntent; + vars['commandTried'] = commandTried; + vars['zorkOutput'] = zorkOutput; + vars['attempt'] = String(attempt); + vars['maxAttempts'] = String(this.maxRetries); + + try { + const response = await this.createCompletion({ + messages: [ + { role: 'system', content: cfg.system }, + { role: 'user', content: renderTemplate(cfg.user_template, vars) }, + ], + temperature: 0.3, + max_tokens: 500, + response_format: { type: 'json_object' }, + }); + return JSON.parse(getAssistantContent(response.data)) as EvaluatorResponse; + } catch (err) { + logLlmError('evaluateOutput', err); + // Fallback: accept the raw output as-is + return { decision: 'accept', text: zorkOutput }; + } + } + + // ---- Session helpers ------------------------------------------------------- + + private executeTool(tool: ToolCall): void { + if (!this.session) return; + debugLog('executing tool call', tool); + switch (tool.name) { + case 'update_character': + if (typeof tool.args['description'] === 'string') { + this.session.characterDescription = tool.args['description']; + debugLog('tool updated character', this.session.characterDescription); + } + break; + case 'add_note': + if (typeof tool.args['note'] === 'string') { + this.session.notes.push(tool.args['note']); + debugLog('tool added note', { + note: tool.args['note'], + notes: this.session.notes, + }); + } + break; + case 'remove_note': { + const idx = Number(tool.args['index']); + if ( + Number.isInteger(idx) && + idx >= 0 && + idx < this.session.notes.length + ) { + this.session.notes.splice(idx, 1); + debugLog('tool removed note', { + index: idx, + notes: this.session.notes, + }); + } + break; + } + case 'add_inventory_item': { + const item = String(tool.args['item'] ?? '').trim(); + if (!item) break; + const exists = this.session.virtualInventory.some( + (it) => it.toLowerCase() === item.toLowerCase(), + ); + if (!exists) this.session.virtualInventory.push(item); + debugLog('tool added inventory item', { + item, + virtualInventory: this.session.virtualInventory, + }); + break; + } + case 'remove_inventory_item': { + const item = String(tool.args['item'] ?? '').trim(); + if (!item) break; + this.session.virtualInventory = this.session.virtualInventory.filter( + (it) => it.toLowerCase() !== item.toLowerCase(), + ); + debugLog('tool removed inventory item', { + item, + virtualInventory: this.session.virtualInventory, + }); + break; + } + } + } + + private appendRecentParagraph(text: string): void { + if (!this.session) return; + const trimmed = text.trim(); + if (!trimmed) return; + this.session.recentParagraphs.push(trimmed); + if (this.session.recentParagraphs.length > 10) { + this.session.recentParagraphs.splice( + 0, + this.session.recentParagraphs.length - 10, + ); + } + } + + private extractCommands(cmdResponse: CommandResponse): string[] { + const list: string[] = []; + + if (cmdResponse.type === 'command') { + list.push(cmdResponse.command); + } else if (cmdResponse.type === 'commands') { + list.push(...cmdResponse.commands); + } else if (cmdResponse.type === 'tools') { + if (cmdResponse.command) list.push(cmdResponse.command); + if (Array.isArray(cmdResponse.commands)) list.push(...cmdResponse.commands); + } + + return list + .map((c) => String(c).trim()) + .filter(Boolean) + .map((c) => c.toUpperCase()); + } + + private appendRawTranscript(command: string, output: string): void { + if (!this.session) return; + this.session.rawTranscript.push( + [`> ${command}`, output.trim()].filter(Boolean).join('\n'), + ); + if (this.session.rawTranscript.length > 12) { + this.session.rawTranscript.splice( + 0, + this.session.rawTranscript.length - 12, + ); + } + } + + private advanceNarratorState(): void { + if (!this.session) return; + this.session.turnCount += 1; + this.session.timeOfDay = timeOfDayForTurn(this.session.turnCount); + this.session.weather = evolveWeather( + this.session.weather, + this.session.turnCount, + ); + debugLog('narrator state advanced', { + turnCount: this.session.turnCount, + timeOfDay: this.session.timeOfDay, + weather: this.session.weather, + }); + } + + private getDeterministicCommandPlan(userInput: string): string[] { + const normalized = userInput.toLowerCase(); + const context = [ + this.session?.currentRoom ?? '', + this.session?.recentParagraphs.join('\n') ?? '', + Object.values(this.session?.roomHistory ?? {}).flat().join('\n'), + ].join('\n').toLowerCase(); + const mentionsLeaflet = /\b(leaflet|pamphlet|brochure|paper|it|this)\b/.test( + normalized, + ); + const contextHasLeaflet = /\b(leaflet|pamphlet|brochure)\b/.test(context); + const mentionsMailbox = /\bmail\s*box|mailbox\b/.test(normalized); + const asksToRead = + /\bread\b/.test(normalized) || + /\bwhat (does|did|do).*say\b/.test(normalized) || + /\btell me what it says\b/.test(normalized) || + /\byou did not tell me\b/.test(normalized); + const asksToTake = /\b(take|get|grab|pick up|pluck)\b/.test(normalized); + const asksToOpen = /\bopen\b/.test(normalized); + const asksToLookIn = + /\blook (in|inside|into)\b/.test(normalized) || /\binside\b/.test(normalized); + + if (mentionsMailbox && asksToOpen && asksToLookIn) { + return ['OPEN MAILBOX', 'LOOK IN MAILBOX']; + } + + if (mentionsMailbox && asksToOpen) { + return ['OPEN MAILBOX']; + } + + if (asksToRead && (mentionsLeaflet || mentionsMailbox || contextHasLeaflet)) { + if (asksToTake || mentionsMailbox) { + return ['TAKE LEAFLET', 'READ LEAFLET']; + } + return ['READ LEAFLET']; + } + + if (asksToTake && (mentionsLeaflet || (mentionsMailbox && contextHasLeaflet))) { + return ['TAKE LEAFLET']; + } + + return []; + } + + private appendRoomHistory(room: string, text: string): void { + if (!this.session) return; + const history = this.session.roomHistory[room] ?? []; + history.push(text); + if (history.length > this.historySize) { + history.splice(0, history.length - this.historySize); + } + this.session.roomHistory[room] = history; + } + + private buildCommonVars(): Record { + const s = this.session!; + const notes = + s.notes.length > 0 + ? s.notes.map((n, i) => `${i + 1}. ${n}`).join('\n') + : '(none)'; + const virtualInventory = + s.virtualInventory.length > 0 + ? s.virtualInventory.map((n, i) => `${i + 1}. ${n}`).join('\n') + : '(none)'; + const recentNarrative = + s.recentParagraphs.length > 0 + ? s.recentParagraphs.join('\n\n---\n\n') + : '(none)'; + const rawTranscript = + s.rawTranscript.length > 0 + ? s.rawTranscript.join('\n\n---\n\n') + : '(none)'; + const history = (s.roomHistory[s.currentRoom] ?? []).join('\n\n---\n\n'); + return { + characterDescription: s.characterDescription, + notes, + virtualInventory, + recentNarrative, + rawTranscript, + roomHistory: history || '(no prior visits)', + currentRoom: s.currentRoom, + narratorState: [ + `Turn count: ${s.turnCount}`, + `Time of day: ${s.timeOfDay}`, + `Outside weather drift: ${s.weather}`, + ].join('\n'), + }; + } + + private buildTurnResult(text: string): ZorkTurnResult { + const alive = this.zork.isAlive(); + if (!alive && this.session) this.session.running = false; + return { + paragraphs: [{ text, tags: [] }], + choices: [], + inputMode: alive ? 'text' : 'end', + gameState: { statusLine: this.session?.currentRoom }, + }; + } +} diff --git a/src/server-zork.ts b/src/server-zork.ts new file mode 100644 index 0000000..8d44454 --- /dev/null +++ b/src/server-zork.ts @@ -0,0 +1,362 @@ +/** + * 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 + */ + +import path from 'path'; +import http from 'http'; +import express from 'express'; +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'; + +dotenv.config(); + +const app = express(); +const server = http.createServer(app); +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 DEBUG_ENABLED = /^(1|true|yes|on)$/i.test(process.env.ZORK_DEBUG ?? ''); + +function debugLog(message: string, details?: unknown): void { + 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.static(path.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 +const saveSlots = new Map>(); + +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'); + + return { + text, + gameState: { + currentRoomId: turn.gameState?.statusLine, + statusLine: turn.gameState?.statusLine, + }, + }; +} + +function normalizeSaveSlot(slot: unknown): number { + const n = Number(slot); + return Number.isInteger(n) && n > 0 ? n : 1; +} + +function getOrCreateEngine(socketId: string): ZorkLlmEngine { + let engine = sessions.get(socketId); + if (!engine) { + engine = new ZorkLlmEngine(); + sessions.set(socketId, engine); + } + return engine; +} + +function getSlots(socketId: string): Map { + let slots = saveSlots.get(socketId); + if (!slots) { + slots = new Map(); + saveSlots.set(socketId, slots); + } + return slots; +} + +async function handleGameApi( + socket: ReturnType & { + id: string; + }, + method: string, + args: unknown[], +): Promise { + 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(): void { + const storyPath = path.resolve( + process.env.ZORK_STORY_FILE ?? './data/z-code/zork1.bin', + ); + const promptDir = path.resolve('./data/zork-prompts'); + const promptFiles = [ + 'character-generation.yml', + 'text-rewriter.yml', + 'command-translator.yml', + 'output-evaluator.yml', + ]; + + const missingPrompts = promptFiles + .map((file) => path.join(promptDir, file)) + .filter((filePath) => !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 (!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: { method?: string; args?: unknown[] }, + respond: (result: object) => void, + ) => { + try { + const result = await handleGameApi( + socket as Parameters[0], + 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: { command?: string }) => { + 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: ZorkTurnResult = 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(): void { + const dirs = [ + path.join(__dirname, '../public'), + path.join(__dirname, '../public/js'), + path.join(__dirname, '../public/css'), + path.join(__dirname, '../public/images'), + path.join(__dirname, '../public/music'), + path.join(__dirname, '../public/sounds'), + path.join(__dirname, '../public/fonts'), + path.join(__dirname, '../data/z-code'), + path.join(__dirname, '../data/zork-prompts'), + ]; + for (const dir of dirs) { + if (!existsSync(dir)) mkdirSync(dir, { recursive: true }); + } +} + +function ensureKokoroJs(): void { + const src = path.join(__dirname, '../node_modules/kokoro-js/dist/index.js'); + const dst = path.join(__dirname, '../public/js/kokoro-js.js'); + if (existsSync(src) && !existsSync(dst)) copyFileSync(src, dst); +} + +async function startServer(initialPort: number, range: number): Promise { + 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: NodeJS.ErrnoException) => { + 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); + }); +} diff --git a/zcode_inclusion.md b/zcode_inclusion.md new file mode 100644 index 0000000..84bc160 --- /dev/null +++ b/zcode_inclusion.md @@ -0,0 +1,674 @@ +# Z-Code Engine Integration Analysis + +## Overview + +This document analyses what would be required to add a **Z-Code engine server** (`src/server-zcode.ts`) to the multi-engine architecture described in `ink_inclusion.md`. The Z-Code server would allow classic and modern Inform-compiled `.z5`, `.z8`, and `.zblorb` story files to run in the same book-style UI, over the same unified Socket.IO protocol. + +--- + +## 1. What Is the Z-Machine? + +The Z-machine is a virtual machine created by Infocom in 1979 for running interactive fiction. Versions 1–8 exist; almost all modern Inform 6/7-compiled games target version 5 or 8. Key properties: + +- **Text-based I/O** via a simple line-input / text-output model. +- **Multi-window layout**: a fixed "status bar" window (room name + score/moves) plus a scrolling story window. +- **Formatting**: bold, italic, fixed-pitch, colours (optional). +- **Sound** (v5+): simple beeps in older games; sampled audio via Blorb archives in modern ones. +- **Graphics** (v6 only): a rarely-used version with arbitrary bitmap graphics windows. Almost no modern games use v6. +- **UNDO**: built-in opcode; many games support multiple undo levels. +- **Save/restore**: binary save files (Quetzal format). + +The Z-machine does **not** have a tag system like Ink. All structural and media information must be inferred from the Glk/GlkOte output stream. + +--- + +## 2. Available JavaScript Z-Machine Interpreters + +### 2.1 `ifvms.js` (Recommended) + +- **Repo**: [github.com/curiousdannii/ifvms.js](https://github.com/curiousdannii/ifvms.js) +- **npm**: `npm install ifvms` (package name: `ifvms`) +- **License**: MIT +- **Versions supported**: Z-machine v1–8 (full), Glulx (not yet) +- **Architecture**: JIT disassembler/compiler targeting JS. Generates an AST from Z-machine bytecode, then emits JS. Uses the **GlkOte** I/O protocol (JSON update objects). +- **Node.js support**: Yes — used by Parchment, also ships a terminal CLI (`zvm story.z5`). +- **Active maintenance**: Yes; last commit 10 months ago fixing edge cases in the spec. +- **Used by**: Parchment (`iplayif.com`), Lectrote desktop interpreter. + +### 2.2 `Bocfel` via `Emglken` (WebAssembly) + +- **Repo**: [github.com/garglk/garglk](https://github.com/garglk/garglk) (Bocfel) + [curiousdannii/emglken](https://github.com/curiousdannii/emglken) +- **Architecture**: Bocfel is a C interpreter compiled to WebAssembly via Emscripten. More complete Z-machine support (all versions, including V6 graphics to some extent), but WASM adds ~2 MB overhead and Node.js integration is more complex. +- **Recommendation**: Overkill for this use case. `ifvms.js` is simpler to integrate in Node.js. + +### 2.3 `Quixe` + +- **Repo**: [github.com/erkyrath/quixe](https://github.com/erkyrath/quixe) +- Implements **Glulx** (the successor to Z-machine), not Z-machine. Not directly applicable unless you want to run Glulx games. + +**Verdict**: Use `ifvms.js` for Z-machine. If Glulx support is wanted later, add Quixe as a fourth engine type. + +--- + +## 3. The GlkOte Protocol — The Key to Integration + +Both `ifvms.js` and the browser Parchment front-end communicate via the **GlkOte JSON protocol**. Understanding this is essential because it is the seam where we integrate with our own pipeline. + +GlkOte sends structured JSON **content updates** from the interpreter to the display layer: + +```json +{ + "type": "update", + "windows": [ + { "id": 1, "type": "grid", "gridheight": 1, "gridwidth": 80 }, + { "id": 2, "type": "buffer", "rock": 201 } + ], + "content": [ + { + "id": 2, + "text": [ + { "content": [{ "style": "normal", "text": "You are in a dark room." }] }, + { "content": [{ "style": "em", "text": "Something moves." }] } + ] + }, + { + "id": 1, + "lines": [ + { "line": 0, "content": [{ "style": "normal", "text": "Dark Room Score: 12 Moves: 7" }] } + ] + } + ], + "input": [ + { "id": 2, "type": "line", "gen": 5 } + ] +} +``` + +The interpreter **waits** after sending an update. The display sends back input events: + +```json +{ "type": "line", "window": 2, "value": "go north", "gen": 5 } +``` + +This is already very close to our `TurnResult` shape. **Our Z-Code server translates GlkOte updates into `TurnResult` objects**, forwarding them to the client via Socket.IO. + +--- + +## 4. Architecture: Z-Code Server + +### 4.1 `src/engine/zcode-engine.ts` + +**Effort: ~300 lines new — the hardest piece** + +The engine runs `ifvms.js` in a Node.js worker thread (or the main thread with async I/O), intercepts GlkOte updates, and translates them: + +```ts +import { ZMachine } from 'ifvms'; // ifvms npm package + +class ZCodeEngine { + private vm: ZMachine; + private pendingResolve: ((input: GlkInput) => void) | null = null; + private statusLine: string = ''; + + async newGame(storyPath: string): Promise { ... } + async sendCommand(text: string): Promise { ... } + async sendCharInput(charCode: number): Promise { ... } + async undo(): Promise { ... } + + private onGlkUpdate(update: GlkUpdate): TurnResult { ... } // The translator +} +``` + +The `ifvms.js` Glk layer is designed to be replaceable. Instead of plugging in the browser GlkOte display, we plug in a **custom Glk backend** that captures output into our `TurnResult` structure. + +### 4.2 GlkOte Update → `TurnResult` Translation + +This is the core of the Z-Code server. The translation rules: + +| GlkOte concept | `TurnResult` mapping | +|---|---| +| Window 2 (buffer) text spans | `paragraphs[]` — one entry per `text[]` item | +| `style: "header"` or bold span at paragraph start | `tags: [{ key: 'chapter', value: text }]` | +| Window 1 (grid/status bar) line 0 | `gameState.statusLine`, parsed for room/score/moves | +| `input: [{ type: 'line' }]` | `inputMode: 'text'` | +| `input: [{ type: 'char' }]` | `inputMode: 'char'` (see §5.4) | +| No input (game waiting for timer/end) | `inputMode: 'end'` | +| Sound channel open (Blorb) | `tags: [{ key: 'sfx', value: soundId }]` | +| Background colour set | `tags: [{ key: 'background', value: cssColor }]` | + +### 4.3 `src/server-zcode.ts` + +**Effort: ~70 lines new** + +Identical structure to `server-ink.ts`. `handleGameApi` creates a `ZCodeEngine` per socket session, delegates `newGame` / `saveGame` / `loadGame` to it, and also handles: +- `playerCommand` → `engine.sendCommand(text)` → emit `narrativeResponse` +- `chooseChoice` → `engine.sendCharInput(charCode)` for char-input mode (see §5.4) +- `undo` (new optional API method) → `engine.undo()` + +--- + +## 5. What Works Easily + +### 5.1 Text Output (Buffer Window) + +**Effort: Low** — maps directly to `paragraphs[]`. The GlkOte buffer window content arrives as an array of styled text spans per paragraph, which translates cleanly into `{ text, tags }` pairs. + +Inline styling (`style: 'em'`, `style: 'strong'`) maps to Markdown `_..._` and `**...**` in the text field, or to an inline HTML wrapper. SmartyPants is applied server-side. + +### 5.2 Line Input (Standard Command Mode) + +**Effort: None** — already matches the existing `playerCommand` event and `inputMode: 'text'`. The Z-machine's line input request (`input: [{ type: 'line' }]`) maps directly. + +### 5.3 Save and Restore + +**Effort: Low** — `ifvms.js` handles Quetzal-format save files internally via its Glk Dialog layer. We intercept Glk file-open/write calls and redirect to Node.js `Buffer` objects stored in the session slot map (`Map`). No Quetzal parsing needed on our side. + +### 5.4 Character Input (Menu Mode) + +**Effort: Medium** + +Some Z-machine games use `read_char` for yes/no prompts, menu selections, or "press any key" pauses. When GlkOte reports `input: [{ type: 'char' }]`, we set `inputMode: 'char'`. + +The client already has a `'choice'` mode concept. For char input we can synthesise a minimal choice list: +- For yes/no prompts: `choices: [{ index: 89, text: 'Yes' }, { index: 78, text: 'No' }]` +- For "press any key": `choices: [{ index: 32, text: 'Continue' }]` + +Detecting *which* char input a game expects requires heuristics (checking the game's current output context). A simpler fallback: always show a freetext input accepting single characters, passed as `engine.sendCharInput(text.charCodeAt(0))`. + +### 5.5 UNDO + +**Effort: Low** + +The Z-machine `save_undo` / `restore_undo` opcodes are handled internally by `ifvms.js` when the game calls them. We can also expose an explicit `undo` API method in `handleGameApi` that the client's toolbar restart button can call. The game then emits a new turn result showing the result of undoing. + +### 5.6 Sound Effects (V5+ with Blorb) + +**Effort: Medium** + +Blorb archives embed sounds alongside the story file. `ifvms.js` (via GlkOte's Blorb support) signals sound playback via `glk_schannel_play`. We intercept these Glk sound calls and translate them to `sfx[soundId]` tags in the turn result. The client's `audio-manager-module.js` then fetches and plays the audio. Sound files from the Blorb archive would need to be extracted to `public/sounds/` at server startup. + +### 5.7 Text Styling (Bold, Italic, Fixed-Pitch) + +**Effort: Low** + +GlkOte styles (`em`, `strong`, `fixed`, `preformatted`) map to Markdown or HTML in the paragraph text field: +- `em` → `_text_` +- `strong` / `header` → `**text**` +- `fixed` / `preformatted` → `` `text` `` or `text` + +The existing `text-processor-module.js` already handles Markdown inline formatting. + +--- + +## 6. What Is Difficult or Unsupported + +### 6.1 Status Bar / Split Windows + +**Effort: Medium — biggest conceptual mismatch** + +The Z-machine has a top "status bar" (window 1, type grid) showing room name and score/moves. Our UI currently has no equivalent space. + +Options: +1. **Parse and emit as `gameState`**: Extract room name, score, and move count from the grid window content and send them as `gameState.statusLine` in `TurnResult`. The client `ui-controller-module.js` would display them in the existing toolbar area (room label, score). **This works for standard Inform games.** +2. **Ignore entirely**: Many games function fine without a visible status bar. +3. **Add a status bar element**: Add a `
` to `index.html` and update it from `gameState`. Medium CSS/JS effort. + +The grid window parsing is fragile for non-standard layouts, but for Inform 6/7 games it is reliably structured as `Room Name Score: N Moves: M`. + +### 6.2 Multiple Text Windows + +**Effort: High — not practical for most games** + +A few Z-machine v5+ games use multiple buffer windows (e.g. `Border Zone` with split-screen displays). Our UI has a single story column. Supporting arbitrary multi-window layout would require significant UI restructuring and is not recommended. These games represent a small minority. + +### 6.3 V6 Graphics Windows + +**Effort: Very High — not recommended** + +Z-machine version 6 (`Shogun`, `Journey`, `Arthur`) uses arbitrary graphics windows with bitmap drawing commands. `ifvms.js` has limited V6 support. The GlkOte graphics API (pixel buffer, image blitting) has no mapping to our canvas-less UI. V6 games should be considered **out of scope**. + +### 6.4 Colours + +**Effort: Low–Medium** + +Z-machine games can set foreground/background colours (16 named colours + true colour in V5+). GlkOte reports these as CSS-compatible strings. We can translate `background-colour` changes to `background[#rrggbb]` tags, but mapping arbitrary text colours to our typography-focused style is aesthetically problematic. Recommended: honour background colour changes, ignore foreground colour (always use theme typography). + +### 6.5 Fixed-Width / Pre-formatted Text + +**Effort: Medium** + +Some Z-machine games use fixed-pitch text for ASCII-art maps, inventory tables, or status displays (e.g. `Anchorhead`'s newspaper). These arrive in `style: 'fixed'` or `style: 'preformatted'` spans. The current book-layout renderer with Knuth–Plass line breaking is incompatible with fixed-width layout. Options: +- Render fixed spans as `
` elements, outside the book column layout.
+- Tag the paragraph with `class[fixed]` and use monospace CSS.
+
+Neither option will look as good as a native terminal display, but both are functional.
+
+### 6.6 Timed Input
+
+**Effort: High**
+
+The Z-machine supports timed line input: the interpreter can interrupt an in-progress input request after N/10 seconds to fire a timer routine. This is used by games like `Border Zone` for real-time events. `ifvms.js` supports this, but over Socket.IO it would require sending a "timer tick" event from server to client and receiving it mid-input. This is complex and rarely needed. **Not recommended for initial implementation.**
+
+### 6.7 Mouse Input (V5+, V6)
+
+**Effort: High**
+
+Mouse clicks are used by very few V5 games and extensively by V6 games. V5 mouse support maps to a grid window coordinate — not applicable in our single-column layout. **Out of scope.**
+
+### 6.8 Hyperlinks (Extended Glk)
+
+**Effort: Medium**
+
+Some modern Glk-aware Z-machine games (compiled with extensions) use hyperlinks in the story text. GlkOte reports these as spans with `href` or `hyperlink` values. We could translate them to `[text](choice://N)` Markdown links that the client renders as clickable choices. Nice to have, but not essential.
+
+### 6.9 Transcript / Recording
+
+**Effort: Low**
+
+The Z-machine's `script_on` / `script_off` opcodes open a transcript file via Glk. We can intercept these and append to a server-side text file per session. Straightforward but low priority.
+
+---
+
+## 7. The Paragraph Boundary Problem
+
+**This is the most subtle technical challenge.**
+
+The Z-machine does not have an explicit "paragraph" concept. Games print text character by character (or string by string) via `print` opcodes. GlkOte batches output between input requests into `content[].text[]` arrays, where each array element is one `glk_put_string` call. A single "paragraph" may be split across many such calls.
+
+The rule we apply for translation:
+- A `\n` newline within output → end current paragraph, start new.
+- Two consecutive `\n` → additional visual space (paragraph break).
+- No trailing `\n` → append to current paragraph buffer.
+
+This is exactly what `text-buffer-module.js` already does on the client for the LLM narrative stream. Applying the same logic server-side before building `ParagraphResult[]` works correctly for standard Inform games.
+
+Edge cases:
+- Games that use `glk_put_char('\n')` to create specific whitespace (e.g. centred headings) may produce unexpected splits. These are rare.
+- The status bar (window 1 grid) output does not follow this rule and must be handled separately (see §6.1).
+
+---
+
+## 8. Save/Restore Deep Dive
+
+`ifvms.js` implements Glk file operations via a pluggable `Dialog` object. We replace the default browser-localStorage `Dialog` with a Node.js implementation that stores Quetzal save data in `Buffer` objects:
+
+```ts
+const customDialog = {
+  open: (usage, mode, rock, callback) => { /* return file reference */ },
+  read: (ref, callback) => callback(saveSlots.get(currentSlot)), // Buffer
+  write: (ref, data, callback) => { saveSlots.set(currentSlot, data); callback(); },
+};
+```
+
+This is ~50 lines and gives us full save/restore with no changes to `ifvms.js`.
+
+---
+
+## 9. Effort Summary
+
+| Area | Effort | Notes |
+|---|---|---|
+| `src/engine/zcode-engine.ts` — GlkOte translator | **~300 lines new** | Hardest piece |
+| Custom Glk Dialog (save/restore) | **~50 lines new** | Replaces localStorage |
+| `src/server-zcode.ts` — server entry | **~70 lines new** | Same pattern as ink server |
+| Blorb sound extraction utility | **~60 lines new** | Run at startup |
+| Status bar parsing + `gameState` | **~30 lines** | In `zcode-engine.ts` |
+| Client: `story:char-input` mode handling | **~20 lines** | Extend `choice-display-module.js` |
+| CSS: fixed-pitch paragraph style | **~15 lines** | `style.css` |
+| **`ifvms.js` npm dependency** | `npm install ifvms` | |
+
+**Total new Z-Code specific code: ~530 lines** across 4 new files. All client-side changes reuse the infrastructure built for the Ink engine (tag events, choice display, input mode switching).
+
+---
+
+## 10. Feature Support Matrix
+
+| Z-Machine Feature | Support Level | Notes |
+|---|---|---|
+| Text output (buffer window) | ✅ Full | Core functionality |
+| Line input | ✅ Full | Standard command prompt |
+| Character input | ⚠️ Partial | Synthesised choice list or single-char text field |
+| UNDO | ✅ Full | Via Z-machine internal opcode |
+| Save / Restore | ✅ Full | Custom Glk Dialog with server-side Buffers |
+| Status bar (room/score) | ⚠️ Partial | Parsed and shown in toolbar, not as a true grid window |
+| Text styles (bold/italic/fixed) | ⚠️ Partial | Mapped to Markdown/CSS; fixed-width not book-formatted |
+| Colours | ⚠️ Partial | Background colour only; foreground ignored |
+| Sound effects (Blorb v5+) | ⚠️ Partial | Requires Blorb extraction at startup |
+| Simple beeps (v3) | ❌ Ignored | No concept in our audio system |
+| Multiple buffer windows | ❌ Not supported | UI has single story column |
+| V6 graphics | ❌ Out of scope | No canvas rendering |
+| Timed input | ❌ Not supported | Complex; rarely needed |
+| Mouse input | ❌ Out of scope | No pointing concept in UI |
+| Hyperlinks | ⚠️ Optional | Could map to clickable choice spans |
+| Transcript | ⚠️ Optional | Server-side file append |
+
+---
+
+## 11. Recommended Implementation Order
+
+1. `npm install ifvms`
+2. Write `ZCodeEngine` with a minimal custom Glk backend, returning raw GlkOte updates.
+3. Implement the GlkOte → `TurnResult` translator for buffer window only (ignoring status bar and styles).
+4. Build `server-zcode.ts`; test with a simple `.z5` file (e.g. `Zork I`) via `playerCommand`.
+5. Add Quetzal save/restore via custom Dialog.
+6. Add status bar parsing → `gameState`.
+7. Add Blorb sound extraction + `sfx` tag emission.
+8. Add fixed-pitch / bold text style mapping.
+9. Handle char input mode (yes/no, press-any-key).
+10. Test with a range of Inform 6 and Inform 7 games.
+
+---
+
+---
+
+## Section 2: LLM-Enhanced Zork Engine ("Zork Narrator")
+
+### 2.1 Concept
+
+This engine runs a specific Z-machine game — Zork I — inside `ifvms.js` as a headless backend, while an LLM acts as a narrative layer between the Z-machine and the player. The player never sees raw Z-machine output; they read a continuously re-voiced prose adaptation of it. Conversely, the player never types parser commands; they write in natural language, and the LLM translates their intent into the terse syntax Zork's parser understands.
+
+The central premise is that **Zork's world state is authoritative** — only the Z-machine determines what actually exists, what has been taken, what doors are unlocked — while **everything the player reads and writes is mediated by an LLM** that maintains a consistent narrative voice, a distinct player character, and persistent memory across the session.
+
+This engine is a separate npm server (`dev:zork`) that shares the same client UI over the same Socket.IO protocol. No client changes are required.
+
+---
+
+### 2.2 Architecture Overview
+
+```
+Player ──(free text)──► [Command Translator LLM]
+                                │
+                     ┌──────────┴───────────┐
+                     │ tool call            │ Zork command
+                     ▼                      ▼
+              [Session Manager]    [Z-Machine (ifvms.js)]
+              (char, notes)               │
+                                   raw Zork output
+                                          │
+                                          ▼
+                               [Output Evaluator LLM]
+                                ┌─────────┴──────────┐
+                                │ retry              │ accept
+                                ▼                    ▼
+                        new command          [Text Rewriter LLM]
+                                                     │
+                                              prose output
+                                                     ▼
+                                                  Player
+```
+
+---
+
+### 2.3 Component Inventory
+
+| Component | File | Role |
+|---|---|---|
+| Z-machine subprocess | `src/engine/zork-llm-engine.ts` → `ZorkProcess` | Runs `zork1.bin` via `ifvms` CLI; captures text I/O |
+| Session state | `ZorkSession` (in-memory) | Character description, notes, room history |
+| LLM client | Inline in engine (axios + OpenRouter) | Four distinct prompt invocations per turn |
+| Prompt files | `data/zork-prompts/*.yml` | YAML templates; easily refined without code changes |
+| Engine | `ZorkLlmEngine` | Orchestrates the three-step loop |
+| Server | `src/server-zork.ts` | Express + Socket.IO, same pattern as YAML server |
+| Story file | `data/z-code/zork1.bin` | Provided by the operator; not in version control |
+
+---
+
+### 2.4 Session State
+
+The engine maintains one `ZorkSession` object per connected socket:
+
+```ts
+interface ZorkSession {
+  characterDescription: string;       // Generated at game start; LLM can update
+  notes: string[];                     // Persistent notes the LLM adds/removes
+  roomHistory: Record; // roomName → up to 5 recent player-facing outputs
+  currentRoom: string;                 // Latest known room name
+  running: boolean;
+}
+```
+
+When saved, the session is serialised to JSON and stored in a server-side slot map (same pattern as the YAML engine). The Zork process state is saved by sending the `SAVE` command to the running Z-machine and capturing the resulting Quetzal binary, stored as Base64 inside the session JSON.
+
+---
+
+### 2.5 Prompt Files
+
+All four prompts live in `data/zork-prompts/` as YAML files with two fields:
+
+- `system` — the system message, used verbatim.
+- `user_template` — the user message, with `{{variable}}` placeholders substituted at call time.
+
+Available variables in every prompt:
+
+| Variable | Contents |
+|---|---|
+| `{{characterDescription}}` | Current player character prose |
+| `{{notes}}` | Numbered list of persistent notes, or "(none)" |
+| `{{roomHistory}}` | Up to 5 most recent player-facing outputs for the current room |
+| `{{currentRoom}}` | Current room name |
+
+#### 2.5.1 `character-generation.yml`
+
+Called once at the start of a new game, before any Z-machine output is processed.
+
+- **Input**: none (no user message template needed; the system message is the full prompt).
+- **Expected output**: A single block of vivid prose (300–500 words) describing the player character — name, personality, history, voice, quirks, motivations.
+- **Used in**: All subsequent prompts as `{{characterDescription}}`.
+
+#### 2.5.2 `text-rewriter.yml`
+
+Called only for the game's **opening text** (before the player has made any input) and for any Z-machine output produced when re-entering a previously visited room that has no recent history yet.
+
+Additional template variables:
+
+| Variable | Contents |
+|---|---|
+| `{{zorkOutput}}` | Raw Z-machine text |
+
+- **Expected output**: Plain prose. No JSON. No Z-machine parser vocabulary. Written in second person (or first if the character's voice demands it), in the established narrative style.
+
+#### 2.5.3 `command-translator.yml`
+
+Called each time the player submits input. Translates free natural-language text into a Zork parser command — or decides that no game command is appropriate.
+
+Additional template variables:
+
+| Variable | Contents |
+|---|---|
+| `{{userInput}}` | Raw player input |
+
+- **Expected output**: A JSON object with one of these shapes:
+
+```jsonc
+// Player input maps to a Zork command
+{ "type": "command", "command": "open mailbox" }
+
+// Player input has no in-game equivalent; reply narratively
+{ "type": "reply", "text": "You pause and take a steadying breath..." }
+
+// LLM wants to update session state; may also include a command
+{
+  "type": "tools",
+  "tools": [
+    { "name": "add_note",           "args": { "note": "Player is afraid of the dark." } },
+    { "name": "update_character",   "args": { "description": "Updated character prose..." } },
+    { "name": "remove_note",        "args": { "index": 2 } }
+  ],
+  "command": "examine lantern"   // optional — also try this command
+}
+```
+
+The `command-translator` prompt must include the full list of available tools with descriptions, so that the LLM knows what actions it can take independently of the Z-machine.
+
+#### 2.5.4 `output-evaluator.yml`
+
+Called after each Z-machine response. Decides whether to accept and rewrite the output, or to discard it and try a different command.
+
+Additional template variables:
+
+| Variable | Contents |
+|---|---|
+| `{{userIntent}}` | Original player input (natural language) |
+| `{{commandTried}}` | The Zork command that was sent |
+| `{{zorkOutput}}` | Raw Z-machine text |
+| `{{attempt}}` | Current retry attempt number (1-based) |
+| `{{maxAttempts}}` | Maximum allowed retry attempts |
+
+- **Expected output**: A JSON object with one of two shapes:
+
+```jsonc
+// Accept: rewrite the output for the player
+{
+  "decision": "accept",
+  "text": "The heavy lid of the mailbox swings open..."
+}
+
+// Retry: discard this output, try a different command instead
+{
+  "decision": "retry",
+  "command": "open mailbox with hands"
+}
+```
+
+When the evaluator returns `retry`, the engine sends the new command to the Z-machine and calls the evaluator again on the result. If the maximum number of retries is reached without acceptance, the engine falls back to rewriting the last Z-machine output regardless.
+
+**The evaluator should return `retry` when:**
+- The Z-machine says it does not understand the command ("I don't understand that" / "That's not a verb I recognise").
+- The Z-machine says the action is not possible in a way that suggests a different phrasing would work ("You can't go that way" when the player clearly wants to go somewhere).
+- The output is mechanically correct but logically inconsistent with the established narrative.
+
+**The evaluator should return `accept` when:**
+- The Z-machine performed an observable world-state change (picked up an object, moved to a new room, unlocked something).
+- The action failed in a meaningful, story-relevant way ("The troll blocks your path").
+- The maximum retry count has been reached (soft-forced accept on the last attempt).
+
+---
+
+### 2.6 The Game Loop in Detail
+
+#### Start-Up
+
+1. Spawn the Z-machine subprocess with `zork1.bin`.
+2. Collect all output until the first `>` input prompt.
+3. Call `character-generation` LLM → store result as `session.characterDescription`.
+4. Call `text-rewriter` LLM with the Zork intro text → send rewritten output to client as `TurnResult`.
+5. Set `inputMode: 'text'`; await player input.
+
+#### Per-Turn Loop
+
+```
+user input
+    │
+    ▼
+command-translator LLM
+    │
+    ├─── type: 'reply'    → send LLM text to client; await next input (no Zork involved)
+    │
+    ├─── type: 'tools'    → execute tool actions (update character / add or remove notes)
+    │         │               then, if a command is included, fall through to ↓
+    │         └─────────────────────────────────────────────────────────────────────────┐
+    │                                                                                   │
+    └─── type: 'command'  ──────────────────────────────────────────────────────────────┘
+              │
+              ▼
+         send command to Z-machine
+              │
+              ▼
+         raw Zork output
+              │
+              ▼
+         extract room name → update currentRoom → update roomHistory
+              │
+              ▼
+         output-evaluator LLM (attempt N of maxRetries)
+              │
+              ├─── decision: 'retry' AND attempt < maxRetries
+              │         │
+              │         └──→ send new command to Z-machine (loop back)
+              │
+              └─── decision: 'accept' (or maxRetries exceeded)
+                        │
+                        ▼
+                   store accepted text in roomHistory[currentRoom]
+                        │
+                        ▼
+                   send TurnResult to client; await next input
+```
+
+#### Room History
+
+Each accepted player-facing output is stored in `session.roomHistory[roomName]`. Only the five most recent entries per room are kept (older entries are dropped). This rolling history is injected into all subsequent LLM prompts via `{{roomHistory}}`, ensuring that descriptions added to a room on previous visits can be referenced again when the player returns.
+
+Room names are extracted from Z-machine output using the status bar (window 1 in the GlkOte protocol), which Zork reliably populates with the current room name. As a fallback, the first line of each Z-machine response is used if it matches the pattern for a room title (capitalised, fewer than 60 characters, no trailing punctuation).
+
+---
+
+### 2.7 LLM Tool Actions
+
+The `command-translator` prompt informs the LLM of three tool actions it can invoke instead of — or in addition to — a Zork command:
+
+| Tool | Arguments | Effect |
+|---|---|---|
+| `update_character` | `description: string` | Replaces `session.characterDescription` with new prose |
+| `add_note` | `note: string` | Appends a note to `session.notes` |
+| `remove_note` | `index: number` | Removes the note at the given zero-based index |
+
+These tools exist to handle player inputs that have no in-game equivalent but *do* have a character-state equivalent. Examples:
+
+- Player writes: *"I decide I'm no longer scared of trolls after that encounter."* → LLM calls `update_character` to amend the character's personality note, then replies narratively.
+- Player writes: *"Remember that the sword glows near enemies."* → LLM calls `add_note` with that fact so it appears in all future evaluator and rewriter prompts.
+- Player writes: *"Forget about that note about the axe."* → LLM calls `remove_note` referencing the relevant index.
+
+---
+
+### 2.8 Configuration
+
+All runtime configuration is provided via environment variables (`.env`):
+
+| Variable | Default | Description |
+|---|---|---|
+| `OPENROUTER_API_KEY` | — | Required |
+| `OPENROUTER_MODEL` | — | Required (e.g. `anthropic/claude-3-5-sonnet`) |
+| `ZORK_STORY_FILE` | `./data/z-code/zork1.bin` | Path to the Z-machine story file |
+| `ZORK_MAX_RETRIES` | `3` | Maximum retry attempts before forced accept |
+| `ZORK_HISTORY_SIZE` | `5` | Number of past outputs stored per room |
+| `PORT` | `3002` | HTTP port for the Zork server |
+
+---
+
+### 2.9 Save and Restore
+
+Saving a game involves two steps:
+
+1. Send the `SAVE` command to the running Z-machine subprocess; respond to its filename prompt with a temporary file path; read the resulting Quetzal binary and encode it as Base64.
+2. Serialise `ZorkSession` (character, notes, room history, current room) to JSON. Embed the Base64 Quetzal data as `zorkSave`.
+
+Loading reverses this: decode and write the Quetzal file to a temp path, start a fresh Z-machine process, send `RESTORE` and provide that path, then run `continueUntilPrompt()` to reach the restored state.
+
+---
+
+### 2.10 Effort Estimate
+
+| Component | Effort |
+|---|---|
+| `ZorkProcess` class (subprocess management) | ~100 lines |
+| `ZorkLlmEngine` class (session + loop logic) | ~280 lines |
+| `src/server-zork.ts` | ~80 lines |
+| 4 × YAML prompt files | ~200 lines total |
+| `package.json` script additions | 4 lines |
+| **Total** | **~660 lines** |
+
+No client-side changes are required. The engine reuses `axios` and `js-yaml`, which are already in the project's dependency tree. The only new dependency is `ifvms`.
+
+---
+
+## 12. Recommended Test Games
+
+| Game | Format | Tests |
+|---|---|---|
+| `Zork I` | z3 | Basic text, status bar, save/restore |
+| `Anchorhead` | z8 | Fixed-width text, complex parser |
+| `Lost Pig` | z8 | Modern Inform 6, clean output |
+| `Counterfeit Monkey` | zblorb | Blorb, sounds, complex Inform 7 |
+| `Anchorhead` (Glulx re-release) | — | Out of scope (Glulx, not Z-machine) |
+| Any V6 game | z6 | Expect graceful failure / fallback |