From 90f81ee1b7744b5a0093d01b7a69e292ecc646aa Mon Sep 17 00:00:00 2001 From: Georg Tomitsch Date: Tue, 19 May 2026 15:25:23 +0200 Subject: [PATCH] Prepare Ink Coolify release branch --- .claude/settings.local.json | 8 - .devcontainer/Dockerfile | 19 - .devcontainer/devcontainer.json | 25 - .eslintrc.json | 23 - MARKUP_GUIDELINES.md | 92 - README.md | 264 +-- SPECIFICATION.md | 248 --- THIRD_PARTY_AUDIT.md | 47 - TODO.md | 129 -- ai.interactive.fiction.code-workspace | 7 - config/engines/yaml.json | 18 - config/engines/zcode.json | 19 - data/ink-src/kaiserpunk.ink | 1632 ------------------ data/ink-src/story.ink | 1686 ------------------- data/ink/kaiserpunk.ink.json | 1 - data/ink/story.ink.json | 1 - data/worlds/example_world.yml | 706 -------- data/z-code/README.md | 30 - data/z-code/zork1.bin | Bin 92160 -> 0 bytes data/zcode-prompts/character-generation.yml | 44 - data/zcode-prompts/command-translator.yml | 112 -- data/zcode-prompts/output-evaluator.yml | 76 - data/zcode-prompts/text-rewriter.yml | 77 - dist/cli/game-runner.d.ts | 64 - dist/cli/game-runner.js | 262 --- dist/cli/game-runner.js.map | 1 - dist/config/game-config.d.ts | 39 - dist/config/game-config.js | 96 -- dist/config/game-config.js.map | 1 - dist/engine/game-engine.d.ts | 77 - dist/engine/game-engine.js | 607 ------- dist/engine/game-engine.js.map | 1 - dist/engine/ink-engine.d.ts | 30 - dist/engine/ink-engine.js | 292 ---- dist/engine/ink-engine.js.map | 1 - dist/engine/zcode-llm-engine.d.ts | 84 - dist/engine/zcode-llm-engine.js | 989 ----------- dist/engine/zcode-llm-engine.js.map | 1 - dist/index.d.ts | 4 - dist/index.js | 112 -- dist/index.js.map | 1 - dist/interfaces/engine.d.ts | 39 - dist/interfaces/engine.js | 6 - dist/interfaces/engine.js.map | 1 - dist/interfaces/llm.d.ts | 46 - dist/interfaces/llm.js | 6 - dist/interfaces/llm.js.map | 1 - dist/interfaces/turn-result.d.ts | 36 - dist/interfaces/turn-result.js | 36 - dist/interfaces/turn-result.js.map | 1 - dist/interfaces/world-model.d.ts | 61 - dist/interfaces/world-model.js | 6 - dist/interfaces/world-model.js.map | 1 - dist/llm/openrouter-provider.d.ts | 36 - dist/llm/openrouter-provider.js | 192 --- dist/llm/openrouter-provider.js.map | 1 - dist/server-ink.d.ts | 13 - dist/server-ink.js | 293 ---- dist/server-ink.js.map | 1 - dist/server-yaml.d.ts | 11 - dist/server-yaml.js | 308 ---- dist/server-yaml.js.map | 1 - dist/server-zcode.d.ts | 16 - dist/server-zcode.js | 353 ---- dist/server-zcode.js.map | 1 - dist/test-server-yaml.d.ts | 10 - dist/test-server-yaml.js | 282 ---- dist/test-server-yaml.js.map | 1 - dist/utils/tag-parser.d.ts | 4 - dist/utils/tag-parser.js | 53 - dist/utils/tag-parser.js.map | 1 - dist/world-model/yaml-parser.d.ts | 71 - dist/world-model/yaml-parser.js | 399 ----- dist/world-model/yaml-parser.js.map | 1 - jest.config.js | 12 - package.json | 45 +- scripts/run-engine.js | 51 - src/cli/game-runner.ts | 265 --- src/config/game-config.ts | 11 +- src/engine/game-engine.ts | 661 -------- src/engine/zcode-llm-engine.ts | 1160 ------------- src/index.ts | 88 - src/interfaces/engine.ts | 56 - src/interfaces/llm.ts | 52 - src/interfaces/world-model.ts | 68 - src/llm/openrouter-provider.ts | 212 --- src/server-yaml.ts | 321 ---- src/server-zcode.ts | 375 ----- src/test-server-yaml.ts | 263 --- src/world-model/yaml-parser.ts | 429 ----- 90 files changed, 68 insertions(+), 14215 deletions(-) delete mode 100644 .claude/settings.local.json delete mode 100644 .devcontainer/Dockerfile delete mode 100644 .devcontainer/devcontainer.json delete mode 100644 .eslintrc.json delete mode 100644 MARKUP_GUIDELINES.md delete mode 100644 SPECIFICATION.md delete mode 100644 THIRD_PARTY_AUDIT.md delete mode 100644 TODO.md delete mode 100644 ai.interactive.fiction.code-workspace delete mode 100644 config/engines/yaml.json delete mode 100644 config/engines/zcode.json delete mode 100644 data/ink-src/kaiserpunk.ink delete mode 100644 data/ink-src/story.ink delete mode 100644 data/ink/kaiserpunk.ink.json delete mode 100644 data/ink/story.ink.json delete mode 100644 data/worlds/example_world.yml delete mode 100644 data/z-code/README.md delete mode 100644 data/z-code/zork1.bin delete mode 100644 data/zcode-prompts/character-generation.yml delete mode 100644 data/zcode-prompts/command-translator.yml delete mode 100644 data/zcode-prompts/output-evaluator.yml delete mode 100644 data/zcode-prompts/text-rewriter.yml delete mode 100644 dist/cli/game-runner.d.ts delete mode 100644 dist/cli/game-runner.js delete mode 100644 dist/cli/game-runner.js.map delete mode 100644 dist/config/game-config.d.ts delete mode 100644 dist/config/game-config.js delete mode 100644 dist/config/game-config.js.map delete mode 100644 dist/engine/game-engine.d.ts delete mode 100644 dist/engine/game-engine.js delete mode 100644 dist/engine/game-engine.js.map delete mode 100644 dist/engine/ink-engine.d.ts delete mode 100644 dist/engine/ink-engine.js delete mode 100644 dist/engine/ink-engine.js.map delete mode 100644 dist/engine/zcode-llm-engine.d.ts delete mode 100644 dist/engine/zcode-llm-engine.js delete mode 100644 dist/engine/zcode-llm-engine.js.map delete mode 100644 dist/index.d.ts delete mode 100644 dist/index.js delete mode 100644 dist/index.js.map delete mode 100644 dist/interfaces/engine.d.ts delete mode 100644 dist/interfaces/engine.js delete mode 100644 dist/interfaces/engine.js.map delete mode 100644 dist/interfaces/llm.d.ts delete mode 100644 dist/interfaces/llm.js delete mode 100644 dist/interfaces/llm.js.map delete mode 100644 dist/interfaces/turn-result.d.ts delete mode 100644 dist/interfaces/turn-result.js delete mode 100644 dist/interfaces/turn-result.js.map delete mode 100644 dist/interfaces/world-model.d.ts delete mode 100644 dist/interfaces/world-model.js delete mode 100644 dist/interfaces/world-model.js.map delete mode 100644 dist/llm/openrouter-provider.d.ts delete mode 100644 dist/llm/openrouter-provider.js delete mode 100644 dist/llm/openrouter-provider.js.map delete mode 100644 dist/server-ink.d.ts delete mode 100644 dist/server-ink.js delete mode 100644 dist/server-ink.js.map delete mode 100644 dist/server-yaml.d.ts delete mode 100644 dist/server-yaml.js delete mode 100644 dist/server-yaml.js.map delete mode 100644 dist/server-zcode.d.ts delete mode 100644 dist/server-zcode.js delete mode 100644 dist/server-zcode.js.map delete mode 100644 dist/test-server-yaml.d.ts delete mode 100644 dist/test-server-yaml.js delete mode 100644 dist/test-server-yaml.js.map delete mode 100644 dist/utils/tag-parser.d.ts delete mode 100644 dist/utils/tag-parser.js delete mode 100644 dist/utils/tag-parser.js.map delete mode 100644 dist/world-model/yaml-parser.d.ts delete mode 100644 dist/world-model/yaml-parser.js delete mode 100644 dist/world-model/yaml-parser.js.map delete mode 100644 jest.config.js delete mode 100644 scripts/run-engine.js delete mode 100644 src/cli/game-runner.ts delete mode 100644 src/engine/game-engine.ts delete mode 100644 src/engine/zcode-llm-engine.ts delete mode 100644 src/index.ts delete mode 100644 src/interfaces/engine.ts delete mode 100644 src/interfaces/llm.ts delete mode 100644 src/interfaces/world-model.ts delete mode 100644 src/llm/openrouter-provider.ts delete mode 100644 src/server-yaml.ts delete mode 100644 src/server-zcode.ts delete mode 100644 src/test-server-yaml.ts delete mode 100644 src/world-model/yaml-parser.ts diff --git a/.claude/settings.local.json b/.claude/settings.local.json deleted file mode 100644 index ea1f951..0000000 --- a/.claude/settings.local.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "permissions": { - "allow": [ - "Bash(wc:*)", - "Bash(git -C /workspaces/ai.interactive.fiction log --oneline -15)" - ] - } -} diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile deleted file mode 100644 index bb89aeb..0000000 --- a/.devcontainer/Dockerfile +++ /dev/null @@ -1,19 +0,0 @@ -FROM node:18 - -# Install basic development tools -RUN apt update && apt install -y less git procps - -# Install Kokoro JS dependencies if needed -RUN apt install -y build-essential python3 - -# Ensure default `node` user has access to `sudo` -ARG USERNAME=node -RUN apt-get install -y sudo \ - && echo $USERNAME ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/$USERNAME \ - && chmod 0440 /etc/sudoers.d/$USERNAME - -# Set the default user -USER node - -# Set working directory -WORKDIR /workspace \ No newline at end of file diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json deleted file mode 100644 index 48aa7c2..0000000 --- a/.devcontainer/devcontainer.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "name": "Node.js Development", - "build": { - "dockerfile": "Dockerfile" - }, - "customizations": { - "vscode": { - "extensions": [ - "dbaeumer.vscode-eslint", - "esbenp.prettier-vscode" - ], - "settings": { - "terminal.integrated.defaultProfile.linux": "bash", - "terminal.integrated.profiles.linux": { - "bash": { - "path": "/bin/bash" - } - } - } - } - }, - "forwardPorts": [3001], - "postCreateCommand": "npm install", - "remoteUser": "node" -} \ No newline at end of file diff --git a/.eslintrc.json b/.eslintrc.json deleted file mode 100644 index c3a9a16..0000000 --- a/.eslintrc.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "parser": "@typescript-eslint/parser", - "extends": [ - "eslint:recommended", - "plugin:@typescript-eslint/recommended" - ], - "parserOptions": { - "ecmaVersion": 2020, - "sourceType": "module" - }, - "plugins": [ - "@typescript-eslint" - ], - "rules": { - "@typescript-eslint/no-explicit-any": "warn", - "@typescript-eslint/explicit-module-boundary-types": "off", - "no-console": "off" - }, - "env": { - "node": true, - "jest": true - } -} \ No newline at end of file diff --git a/MARKUP_GUIDELINES.md b/MARKUP_GUIDELINES.md deleted file mode 100644 index b5fcbce..0000000 --- a/MARKUP_GUIDELINES.md +++ /dev/null @@ -1,92 +0,0 @@ -# Markup Guidelines - -This file documents author-facing Ink tag conventions. The active parser normalizes tags into structured `StoryTag` objects before they reach the UI. - -## Implemented Tag Forms - -Use bracket tags for titles, filenames, and longer text: - -```ink -#chapter[Eibenreith] -#image[statue.png](square) -#music[Kaiserpunk Waltz.mp3](crossfade, loop, lead=8) -#sfx[church-bells.ogg](max=8, fade) -#score[You reached an ending.] -#achievement[First Steps] -#alert[Try examining the room.] -``` - -Use colon tags for short identifiers, categories, and choice keys: - -```ink -#action:movement -#key:l -#sort:last -#gated:noble -``` - -Bare flags are accepted as tags with no value: - -```ink -#optional -``` - -## Right-Page Glossary Notes - -Glossary notes are story tags scoped to the paragraph/block they belong to. They affect only the right-page story rendering, never choice text or command history. - -```ink -The conductor points toward Eibenreith. -#gloss[Eibenreith](A fictional alpine town in the Kaiserpunk setting.) -``` - -The bracket value is the visible term to find. The parenthesized value is the note shown on hover/focus. The renderer marks every matching instance of the term in the same right-page block. The tag is not displayed and is not sent to TTS. Avoid raw Ink control characters in the explanation; `|`, `{`, and `}` must be escaped in Ink as `\|`, `\{`, and `\}` if they are needed literally. - -## Choice Metadata - -Choice tags are placed on the Ink choice they belong to: - -```ink -* [__Schaue__: Aus dem Fenster.] - #action:orientation - #key:l -``` - -Implemented choice metadata: - -- `#key:x`: reserves keyboard key `X` for the choice. -- `#letter[x]`: older equivalent for reserving keyboard key `X`. -- `#action:group` or `#action[group]`: stores a category/template hint. - -The current UI renders all choices in one list. Explicit keys are assigned first; choices without explicit keys receive `1` through `0`, then `A` through `Z` in visible order while skipping explicit keys. `#optional` choices are displayed italic. Grouping columns, stable shuffling, `#gated[...]`, and `#sort[...]` are documented authoring conventions or future metadata, not fully implemented UI behavior yet. - -## Popup And End-State Tags - -These tags may appear as Ink global tags, paragraph tags, or empty tag-only lines. They are dispatched through the same tag channel as media tags. - -```ink -#score[You reached the quiet ending.] -#error[The story ended unexpectedly.] -#achievement[First Steps] -#alert[Try examining objects before using them.] -``` - -- `#score[...]`: intended ending. When the turn reaches `inputMode: end`, the UI shows a localized ending popup with the tag value as the optional message. -- `#error[...]`: unrecoverable ending. The UI shows a localized error popup with the tag value as the optional message. The Ink engine emits this automatically if Ink runs out of content without an explicit `#score[...]` or `#error[...]`. -- `#achievement[...]`: queued localized achievement popup while the game continues. -- `#alert[...]`: queued localized player hint/tutorial popup while the game continues. - -## Existing Media And Structure Tags - -```ink -#chapter[Title] -#section -#textblock -#image[filename.png](landscape) -#image[filename.png](portrait pause=2) -#image[filename.png](square lead=1.5) -#music[track.mp3](crossfade, loop, lead=4) -#sfx[file.ogg](max=8 fade fade-duration=2) -``` - -Asset filenames resolve relative to the configured image, music, and sound folders. diff --git a/README.md b/README.md index 39cd413..79e91fd 100644 --- a/README.md +++ b/README.md @@ -1,257 +1,103 @@ -# AI Interactive Fiction +# AI Interactive Fiction - Ink Coolify Release -AI Interactive Fiction is a web and CLI text adventure prototype that combines a deterministic world model with LLM-assisted command interpretation and narrative output. The web client presents the story as an animated, novel-like book page with synchronized text animation, optional TTS, music, and sound effects. +This branch is the deployable Ink edition of the AI Interactive Fiction client/server. It contains the browser UI, the Ink server, the Eibenreith Ink source, compiled Ink output, media assets, fonts, locale files, and Docker/Coolify configuration. -## Quick Start +The full multi-engine development tree lives on `main`. The historical prototype is intentionally not part of this branch; it is preserved on `codex/archive-prototype` and tag `prototype-archive-2026-05-19`. -Use Node.js 22 LTS for development. The project accepts Node >= 18.17, but current development has been done on Node 22. +## Local Ink Development + +Use Node.js 22 LTS. ```powershell nvm install 22 nvm use 22 npm install -npm run build npm run dev ``` -`npm run dev` and `npm run start` use `DEFAULT_GAME_ENGINE` from `.env` to choose the active engine. Supported values are `ink`, `yaml`, and `zcode`. The engine-specific scripts remain available when you want to bypass the default. +`npm run dev` starts the Ink server through `ts-node` and watches `src/`, `data/ink-src/`, and `config/engines/ink.json`. The server compiles the configured Ink source when it starts. -Set `PORT` to choose a port; the server will try the next few ports if the requested one is already in use. Current engine defaults are YAML `3001`, Z-code `3002`, and Ink `3003` before port fallback. - -## Commands +Useful commands: ```powershell -npm run dev # Start the web UI through ts-node/nodemon -npm run start # Build/run the configured default engine from dist/ -npm run dev:ink # Start the Ink engine server, watch ink source, compile on restart -npm run dev:yaml # Start the YAML engine server -npm run dev:zcode # Start the Z-code engine server -npm run start:ink # Build and run the compiled Ink engine server -npm run build # Compile TypeScript -npm run test # Run Jest tests -npm run lint # Run ESLint on src/ -npm run start:cli # Run the CLI interface -npm run dev:cli # Run the CLI interface through ts-node/nodemon +npm run build # Compile TypeScript to dist/ +npm run start # Run the compiled Ink server +npm run dev:debug # Development server with Ink debug logging +npm run dev:inspect # Development server with Node inspector on 0.0.0.0:9231 +npm run start:debug # Compiled server with Ink debug logging +npm run start:inspect # Compiled server with Node inspector on 0.0.0.0:9231 ``` -Each game engine also has `:debug` and `:inspect` variants. `:debug` enables engine-specific diagnostic logging. `:inspect` starts Node with the inspector and currently also enables that engine's debug flag, so it is the combined debug-plus-inspector mode. +Set `PORT` to choose the server port. The Docker image defaults to `3000`. -## Docker / Coolify Ink Deployment +## Coolify 4 Deployment -The included `Dockerfile` builds and serves the Ink engine only. Coolify can use the repository Dockerfile directly. +Configure Coolify to deploy this branch with the repository `Dockerfile`. -Set the Coolify environment variables from `coolify.env.example`; at minimum: +Recommended environment: ```text NODE_ENV=production -DEFAULT_GAME_ENGINE=ink PORT=3000 INK_CONFIG_FILE=./config/engines/ink.json ``` -The container compiles TypeScript during image build and compiles the configured Ink source to JSON when the server starts. +Coolify can watch `release/coolify-ink` and redeploy on webhook pushes. The intended flow is: -## Configuration +1. Write Ink locally in `data/ink-src/`. +2. Test locally with `npm run dev`. +3. Commit to the development branch. +4. Merge or cherry-pick the wanted deployment state into `release/coolify-ink`. +5. Push `release/coolify-ink` to the Git remote watched by Coolify. -Environment variables are loaded from `.env`. +The container builds TypeScript during image build and compiles the configured Ink source at server startup. -- `PORT`: preferred web server port. -- `DEFAULT_GAME_ENGINE`: engine used by `npm run dev` and `npm run start`; one of `ink`, `yaml`, or `zcode`. -- `DEFAULT_WORLD_FILE`: YAML world file to load. Defaults to `./data/worlds/example_world.yml`. -- `OPENROUTER_API_KEY`: API key for LLM command interpretation. -- `OPENROUTER_MODEL`: OpenRouter model name. +## Ink Configuration -TTS provider settings are configured in the browser options menu and persisted in browser storage. Providers currently include `none`, browser speech synthesis, Kokoro, ElevenLabs, and OpenAI. Production should not assume a universal TTS default; the game or player state selects the active mode, and `none` is the safe fallback. +The active game is configured in `config/engines/ink.json`. -## Starting A Game +Important paths: -The web client no longer starts the game automatically. Browsers require a user gesture before audio playback, so the right page initially shows a start prompt and the command input is hidden. Use `new game` or `load` in the top bar to start. +- `paths.inkSource`: main Ink source file. +- `paths.inkCompiled`: compiled Ink JSON target. +- `paths.mainGameFile`: compiled Ink JSON loaded by the server. +- `paths.music`: background music directory. +- `paths.sfx`: sound effect directory. +- `paths.images`: image directory. -The placeholder server API supports: +Game metadata and language are sent to the client before game start. The client uses game language for hyphenation and TTS language hints; UI locale can still be overridden by the player. -- `newGame()` -- `loadGame(slot)` -- `saveGame(slot)` -- `hasSaveGame(slot)` -- `getSaveGames()` -- `isGameRunning()` +## Browser Client -Save slots are positive integers. Save behavior is engine-specific: the Ink client/server path persists Ink state, client history, choices, media state, and playback position for browser save/load; YAML and Z-code persistence still need regression testing and cleanup. +The client lives in `public/` and is served as native browser modules. It renders structured `TurnResult` output from the server, including paragraphs, headings, choices, media events, alerts, score messages, achievements, and errors. -## Web Client +TTS provider settings, volume controls, savegames, TTS cache, and rendered story history are stored in browser storage. Ink server state is also sent back to the browser save data so a client can recover after reload or server restart without server-side per-player sessions. -The browser app is built from native ES modules in `public/js/`. The loader dynamically imports modules, applies a cache-busting query string during development, resolves declared dependencies, and awaits module initialization in dependency order before the UI becomes usable. +## Story Tags -Major modules: +Ink tags are parsed server-side into structured output objects. The client consumes structured turn data only. -- `module-registry.js`, `base-module.js`, `loader.js`: module lifecycle, dependency graph, progress overlay, state reporting. -- `text-processor-module.js`, `paragraph-layout-module.js`, `layout-renderer-module.js`: SmartyPants, language-aware hyphenation, Knuth-Plass line breaking, DOM rendering. -- `markup-parser-module.js`: story markup fallback for chapters, sections, Markdown emphasis, right-page glossary notes, images, SFX, and music. -- `sentence-queue-module.js`, `playback-coordinator-module.js`, `animation-queue-module.js`: sentence preparation, synchronized playback, timing, fast-forward. -- `tts-factory-module.js` plus provider modules: TTS provider selection, voice settings, speed mapping, caching, and playback. -- `audio-manager-module.js`: master, speech, music, and sound effect volume, music playback, sound effects, and music ducking. -- `ui-controller-module.js`, `ui-display-handler-module.js`, `ui-input-handler-module.js`, `options-ui-module.js`: book UI, command input, options, top-bar controls, and game API calls. -- `choice-display-module.js`: choice-mode UI, click selection, keyboard-letter assignment, and future choice-template routing. - -The static server sends no-cache headers for local development so stale ES modules do not mask changes. If the browser console shows `onpage-dialog.preload.js:121 Uncaught ReferenceError: browser is not defined`, ignore it; that comes from the installed ad blocker, not this project. - -## Story Markup - -Plain paragraphs are rendered paragraph by paragraph. Normal following paragraphs are horizontally indented and do not get a blank line between them. Special block markers change the treatment of the next paragraph. - -Inline Markdown emphasis: - -```text -*italic* or _italic_ -**bold** or __bold__ -***bold italic*** or ___bold italic___ -``` - -Right-page glossary notes: - -```text -The train stops at Eibenreith. -#gloss[Eibenreith](A fictional alpine town in the Kaiserpunk setting.) -``` - -Glossary markup is a normal story tag scoped to the paragraph/block it is attached to. The UI finds every matching visible instance of the term in that right-page block and adds a hover/focus note. The tag itself is not displayed, is not sent to TTS, and is ignored by choices and command history. Avoid raw Ink control characters in the explanation; `|`, `{`, and `}` must be escaped in Ink as `\|`, `\{`, and `\}` if they are needed literally. - -Canonical block/media/control tags use Ink-style `#` syntax. In Ink these are real Ink tags. In YAML and Z-code narrative output, leading `#...` lines are parsed by the server into the same structured `StoryTag` objects before reaching the client. The browser only consumes structured `TurnResult` objects. - -Tag format: - -```text -#key -#key[value] -#key[value](options) -#key:value -``` - -For Ink choices, put choice-local tags under the choice they belong to. Explicit keyboard letters are supported with `# letter[x]`, `#letter[x]`, or the colon form `#key:x`; the client reserves those keys first, then assigns the remaining visible choices from `1` through `0`, then `A` through `Z` in order. `#optional` renders the choice in italic. `# action[name]` or `#action:name` is parsed as a category/template hint for future choice layouts, although the current UI displays all choices in one list. - -Chapter: - -```text -#chapter[The Mysterious Mansion] - -The first paragraph uses a drop cap and no first-line indent. - -Following paragraphs use the normal paragraph indent. -``` - -The heading is centered, italic, and uses the same text face as the body. The first paragraph after a chapter marker is unindented and receives the drop cap treatment. - -Section or text block: +Common tags: ```text +#chapter[Title] #section - -The first paragraph starts a separated block without horizontal indent. - -The following paragraph returns to the normal indent. +#image[file.png](landscape|portrait|square, pause=2) +#music[file.mp3](crossfade|queue|cut, loop=true, lead=5) +#sfx[file.ogg](duration=4, fade=true) +#gloss[Term](Explanation shown on hover.) +#score[Optional score text] +#achievement[Optional achievement text] +#alert[Optional player hint] +#error[Optional error text] ``` -`#textblock` is treated the same way. The first paragraph after the marker is separated from previous content by one line of vertical space. - -Images are story blocks: +Choice-local tags: ```text -#image[mansion-rain.jpg](landscape) -#image[portrait-letter.jpg](portrait pause=2) -#image[seal.png](square lead=1.5) +#key:x +#optional +#action[name] ``` -Image file names are relative to `public/images/`. `landscape`/`widescreen` and `square` images are centered, near full page width, and line-snapped. `portrait` images sit beside prose at half page width. Image pauses (`pause=`, `delay=`, `lead=`, or a bare `2s`) are skippable and do not block background TTS preparation. - -Sound effects are story tags: - -```text -#sfx[squeaky-door.ogg] -#sfx[church-bells.ogg](max=8 fade fade-duration=2) -The door opens and the hall exhales. -``` - -The tag is parsed by the server into a `StoryTag` object. Sound effect paths are relative to `public/sounds/`. Optional parameters can limit playback (`max=`, `duration=`, `stop-after=`, `fade-after=`), choose the end mode (`fade` or `stop`/`cut`), and set `fade-duration=`. - -Music can be placed as a block: - -```text -#music[rain-theme.ogg](crossfade, loop, lead=4) -``` - -Music paths are relative to `public/music/`. Supported modes are `queue`, `crossfade`, and `cut`. Use `loop` or `once` to control repetition. `lead=` delays the following text/TTS paragraph so the music can play alone before narration continues. To place that pause between a chapter heading and the dropcapped first paragraph, put the music tag after the chapter tag and before the first prose paragraph; TTS generation for the next spoken paragraph continues during the lead pause. - -Game-state and player-message tags: - -```text -#score[You found the quiet ending.] -#error[Ink story ended without an explicit ending tag.] -#achievement[First Steps] -#alert[Try examining objects before using them.] -``` - -`#score[...]` marks an intended ending and opens a localized ending popup when the turn reaches `inputMode: end`. `#error[...]` marks an unrecoverable ending and opens an error popup. If an Ink story runs out of content without an explicit `#score[...]` or `#error[...]`, the Ink engine emits an `#error[...]` tag. `#achievement[...]` and `#alert[...]` open localized queued popups while the game continues. - -## Architecture Documentation - -`SPECIFICATION.md` is the canonical architecture and implementation specification. `TODO.md` is the canonical progress and remaining-work list. The former loose Ink and Z-code inclusion notes have been folded into those two files. - -## Assets - -- `public/sounds/`: sound effects referenced by `#sfx[file]` tags. -- `public/music/`: background music referenced by `#music[file](...)` tags. -- `public/images/`: story images referenced by `#image[file](...)`. -- `public/fonts/`: font assets used by the book UI. - -Keep third-party assets licensed for local redistribution, and document source and license in the folder README or alongside the file. - -## Typography And Playback Behavior - -The renderer is designed to behave like a scaled static book page. The page keeps its aspect ratio, and text sizes and word positions scale relative to the page instead of reflowing unpredictably at small browser sizes. - -Text processing order: - -1. Parse story markup and remove non-display media markers. -2. Apply Markdown emphasis spans and right-page glossary annotations. -3. Run SmartyPants for typographic punctuation. -4. Apply Hyphenopoly for the selected language. -5. Calculate line breaks with the Knuth-Plass algorithm. -6. Render absolutely positioned word spans and animate them in sync with audio or estimated duration. - -When real TTS audio is available, animation duration is driven by measured audio length. With TTS disabled or unavailable, duration is estimated from text length and the persisted speed setting. - -Fast-forwarding by page click or space completes the active animation and fades/stops current TTS playback so queued content can proceed. - -The right page history is line-addressed rather than natively scrolled. The page has a fixed line count, all block heights snap to whole lines, and the custom scrollbar represents virtual history line position. The DOM keeps a moving window of history blocks around the active line instead of paginating the story. - -## Changelog - -### 2026-05-17 - -- Added Ink engine support with source compilation, engine config files, game metadata, locale-driven UI text, choice mode, keyboard choice letters, and one-list choice rendering. -- Added line-addressed right-page history, save/load reconstruction, image restoration, custom scrollbar plumbing, and virtual block-window rendering. -- Added story image rendering for landscape, portrait, and square images, including line-snapped sizing and portrait text exclusion. -- Added localized popups for endings, errors, achievements, and alerts through the tag channel. -- Added credits and third-party license UI. -- Added per-volume mute toggles and configurable music ducking amount. -- Added German typography handling for dialogue guillemets based on game metadata language. - -### 2026-05-14 - -- Consolidated usage, markup, and architecture documentation into `README.md` and `TODO.md`. -- Added no-cache static serving and module URL cache busting so browser reloads pick up JS changes reliably during development. -- Fixed module loader dependency ordering so modules are initialized only after their declared dependencies are ready. -- Added the placeholder game API for `newGame`, `loadGame`, `saveGame`, `hasSaveGame`, `getSaveGames`, and `isGameRunning`. -- Changed the web UI to require a manual game start before showing the command input, which keeps browser audio autoplay restrictions manageable. -- Implemented story markup for chapters, text blocks, Markdown emphasis, image placeholders, sound effects, and music cues. -- Added music block parameters for playback mode, loop/once behavior, and lead-in delay. -- Added sound and music asset folders and playback plumbing for sound effects and background music. -- Added music ducking while TTS is active. -- Reworked book typography around Knuth-Plass line breaking, Hyphenopoly hyphenation, SmartyPants, paragraph indentation rules, drop caps, and responsive page scaling. -- Reworked TTS provider behavior, speed mapping, persistence, caching keys, top-bar/options synchronization, and OpenAI voice validation. -- Added development notes for ignoring the unrelated ad-blocker console error. - -### Earlier Prototype Work - -- Established the original animated fiction prototype with inkjs, SmartyPants, Hyphenopoly, Knuth-Plass line breaking, custom animation scheduling, save/load concepts, and media tags. -- Split the client from a monolithic prototype into focused modules for text processing, layout, animation, audio, persistence, TTS, and UI control. +Explicit choice keys are reserved first. Remaining choices receive keys from `1` through `0`, then `a` through `z`. diff --git a/SPECIFICATION.md b/SPECIFICATION.md deleted file mode 100644 index 0f9372a..0000000 --- a/SPECIFICATION.md +++ /dev/null @@ -1,248 +0,0 @@ -# AI Interactive Fiction Specification - -This is the single architecture and behavior specification for the project. Usage and changelog live in `README.md`; actionable work items live in `TODO.md`; authoring conventions live in `MARKUP_GUIDELINES.md`. - -## Product Goal - -AI Interactive Fiction is a shared book-style web client plus interchangeable game engine servers. The client renders interactive fiction as animated, carefully typeset illustrated prose with optional speech, music, sound effects, images, choices, and command input. Game engines own game state and emit a shared structured protocol. - -The production client must tolerate speech being unavailable. The safe TTS provider default is `none`; a game or player preference may select another provider. - -## Repository Layout - -- `public/`: shared browser UI, assets, fonts, client modules, third-party browser libraries. -- `src/`: TypeScript servers, shared protocol types, engine implementations, YAML world model, CLI support. -- `config/engines/`: per-engine configuration files. -- `data/ink-src/`: Ink source files. -- `data/ink/`: compiled Ink JSON output. -- `data/worlds/`: YAML world files. -- `data/z-code/`: Z-machine story files such as `zork1.bin`. -- `data/zcode-prompts/`: prompt templates used by the current LLM-mediated Z-code narrator. -- `scripts/`: project utility scripts. Currently used: `check-node-version.js` and `run-engine.js`. -- `templates/`: not present in the current repository and not used. - -## Engine Selection And Commands - -`DEFAULT_GAME_ENGINE` in `.env` selects the engine used by: - -```text -npm run dev -npm run start -``` - -Supported values are `ink`, `yaml`, and `zcode`. - -Engine-specific commands bypass the default: - -```text -npm run dev:ink -npm run dev:yaml -npm run dev:zcode -npm run start:ink -npm run start:yaml -npm run start:zcode -``` - -`dev:*` runs TypeScript through `ts-node` and `nodemon`. `start:*` runs compiled JavaScript from `dist/` and builds first through `prestart:*`. `*:debug` enables the engine's debug environment flag. `*:inspect` starts Node inspector and currently also enables debug for that engine. - -The CLI path is YAML-only and uses `src/index.ts --cli`. It is useful for testing the YAML `GameRunner` without the browser UI. The old `test-server-yaml.ts` is a legacy static/YAML harness and should be removed once no workflow depends on it. - -## Shared Server Protocol - -All engines communicate with the browser through Socket.IO and the same game API: - -```text -newGame() -loadGame(slot) -saveGame(slot) -hasSaveGame(slot) -getSaveGames() -isGameRunning() -chooseChoice(index) -``` - -The Ink engine additionally supports browser-owned session recovery: - -```text -resumeGame(savedInkState) -exportGameState() -``` - -`exportGameState()` returns the current Ink state without creating a server-side save slot. The client stores that state with story history, choices, input mode, and media state in IndexedDB. `resumeGame(savedInkState)` rehydrates a fresh server-side InkEngine after a socket reconnect or browser reload without emitting duplicate narrative. This keeps durable player-specific state client-side for hosted multi-client Ink deployments. - -Line-input engines also use `playerCommand` for free text. - -Every engine emits `TurnResult` objects: - -```ts -interface TurnResult { - turnId: number; - paragraphs: Array<{ text: string; tags?: StoryTag[] }>; - choices: ChoiceResult[]; - inputMode: 'text' | 'choice' | 'end' | 'none'; - globalTags?: StoryTag[]; - gameState?: { - score?: number; - endState?: { type: 'intended' | 'error'; message?: string }; - }; - suggestions?: string[]; -} -``` - -The browser consumes structured `TurnResult` data only. YAML and Z-code servers must parse or synthesize the same tag objects that Ink exposes through native tags. - -## Game Engines - -### YAML Engine - -- Config: `config/engines/yaml.json` -- Server: `src/server-yaml.ts` -- World model: `data/worlds/*.yml` -- CLI entry: `src/index.ts --cli` - -The YAML engine is no longer the architectural default; it is one engine beside Ink and Z-code. It uses `GameRunner`, `GameEngine`, and `YamlWorldParser`, emits `inputMode: 'text'`, and remains the best test bed for deterministic world-model plus LLM command interpretation. - -### Ink Engine - -- Config: `config/engines/ink.json` -- Server: `src/server-ink.ts` -- Engine: `src/engine/ink-engine.ts` -- Source: `data/ink-src/eibenreith.ink` plus included chapter files. -- Compiled output: `data/ink/eibenreith.ink.json` - -The Ink server compiles source at startup using `inkjs/full`, then runs the compiled story with `inkjs`. Ink choices become `ChoiceResult` objects. Ink tags become shared `StoryTag` objects. Choice preview tags support `#key`, `#letter`, `#optional`, `#action`, `#gated`, and `#sort`. - -The server keeps only ephemeral per-socket InkEngine instances. Browser IndexedDB owns durable Ink saves and the current autosave. If the socket reconnects or the page reloads, the browser sends the autosaved Ink state to `resumeGame()` and restores rendered history locally. - -Ink does not provide arbitrary string input as a native async primitive comparable to choices. Future text-input turns should be implemented through a tag such as `#input[name](prompt)`: the server returns `inputMode: 'text'`, the UI shows command input for one round, then the server stores the submitted string into an Ink variable and continues. - -### Z-code Engine - -- Config: `config/engines/zcode.json` -- Server: `src/server-zcode.ts` -- Engine: `src/engine/zcode-llm-engine.ts` -- Story file: `data/z-code/zork1.bin` by default. -- Prompt templates: `data/zcode-prompts/*.yml` - -The engine name is Z-code. Zork I is only the current game file and prompt target. The current implementation runs a Z-machine story through `ifvms`, keeps Z-machine state authoritative, and uses an LLM to translate natural-language input into parser commands and rewrite raw Z-machine output into prose. - -Future work should separate Z-code-generic logic from Zork-specific prompt content more clearly. - -## Client Module System - -The browser client uses native ES modules, no bundler. The loader imports modules, analyzes dependency declarations, initializes modules in dependency order, tracks state/progress, and hides the loading overlay only when initialization and progress exit animations are complete. - -Rules: - -- Every app module extends `BaseModule`. -- Every app module registers with `moduleRegistry`. -- Required dependencies must be listed in `dependencies`. -- Modules should use authoritative dependencies instead of local fallbacks. -- Do not add fallback paths to hide bad dependency declarations or ordering bugs. -- `setTimeout` must not paper over initialization races. It is acceptable for animation, debounce, throttle, and browser rendering timing when locally justified. - -Core modules: - -- `loader.js`: module script loading, progress UI, dependency diagnostics. -- `module-registry.js`: registration and readiness promises. -- `base-module.js`: lifecycle, progress, state, event cleanup. - -Primary client responsibilities: - -- Text and typography: `text-processor`, `paragraph-layout`, `layout-renderer`. -- Markup: `markup-parser`. -- Queue/playback: `text-buffer`, `sentence-queue`, `playback-coordinator`, `animation-queue`. -- Audio/TTS: `audio-manager`, `tts-factory`, provider modules. -- UI: `ui-controller`, `ui-display-handler`, `ui-input-handler`, `choice-display`, `options-ui`, `ui-effects`. -- Persistence/history: `persistence-manager`, `story-history`. -- Networking: `socket-client`. - -Known cleanup candidates: `debug-utils-module.js` is not loaded; `game-loop-module.js` still contains high-level glue from older architecture and should be audited before removal. - -## Text Pipeline - -Processing order: - -1. Receive structured blocks and tags from a game engine. -2. Parse inline story markup and remove media markers from display/TTS text. -3. Apply Markdown emphasis. -4. Apply locale-aware SmartyPants typography. -5. Apply Hyphenopoly for the game metadata language. -6. Measure text using the exact page font settings. -7. Run Knuth-Plass line breaking. -8. Render absolutely positioned words into the page line-coordinate model. -9. Animate words in sync with measured TTS duration or estimated duration. - -The external Knuth-Plass library should not be locally modified. Adaptation belongs in our modules. - -## Right Page Layout And History - -The right page is a virtual line-addressed content pane: - -- `#page_right` does not use native scrolling. -- Page height is divided into `PAGE_LINE_COUNT = 25`. -- All block heights, margins, image spacing, and chapter/section spacing are exact line multiples. -- Stored block positions are line coordinates, not pixels. -- Window resize recalculates pixels from line coordinates. -- New content appends at the live bottom. -- Manual scrolling moves the active line and keeps a window of nearby blocks loaded. -- The custom scrollbar represents virtual line history, not DOM scroll state. - -Portrait images may overlap line ranges with text next to them, but edges must still land on line boundaries. - -## Markup And Tags - -Canonical tag syntax: - -```text -#key -#key[value] -#key[value](options) -#key:value -``` - -Supported story tags include: - -- `#chapter[Title]` -- `#section` / `#textblock` -- `#image[file](landscape|portrait|square pause=2)` -- `#sfx[file](max=8 fade fade-duration=2)` -- `#music[file](crossfade loop lead=4)` -- `#gloss[term](definition)` -- `#score[...]` -- `#error[...]` -- `#achievement[...]` -- `#alert[...]` - -Choice tags: - -- `#key:x` or `#key[x]` -- `#letter[x]` -- `#optional` -- `#action[name]` - -The active choice UI is one list. Explicit keys are reserved first, then remaining choices receive `1` through `0`, then `A` through `Z`. - -Markdown emphasis: - -```text -*italic* or _italic_ -**bold** or __bold__ -***bold italic*** or ___bold italic___ -``` - -## Audio, TTS, And Media - -TTS providers currently include `none`, Browser Speech, Kokoro, ElevenLabs, and OpenAI. Provider modules exist, but Browser Speech and Kokoro need focused validation before being considered production-ready. - -TTS cache keys include provider, voice, provider speed value, language, and exact normalized TTS string. Fast-forward must accelerate visible animation and fade/stop active TTS without cancelling background generations unless the foreground block has been waiting long enough. - -Music and sound effects are preloaded when requested. Music can queue, crossfade, cut, loop, play once, and lead into following text. Music ducks by a persisted percentage during TTS playback. - -## Documentation Source Of Truth - -- `README.md`: usage, commands, changelog, concise feature summary. -- `SPECIFICATION.md`: architecture and behavior. -- `TODO.md`: active status and backlog. -- `MARKUP_GUIDELINES.md`: writing/authoring rules for story files. -- `THIRD_PARTY_NOTICES.md` and `public/THIRD_PARTY_NOTICES.md`: license/credits material. diff --git a/THIRD_PARTY_AUDIT.md b/THIRD_PARTY_AUDIT.md deleted file mode 100644 index e83c84a..0000000 --- a/THIRD_PARTY_AUDIT.md +++ /dev/null @@ -1,47 +0,0 @@ -# Third-Party Library Audit - -Date: 2026-05-17 - -## Summary - -The project currently uses the expected browser-side typography/story libraries plus additional runtime packages: - -- inkjs -- SmartyPants.js -- Hyphenopoly -- Knuth-Plass line breaking support (`knuth-and-plass.js`, `linebreak.js`, `linked-list.js`) -- Kokoro JS browser bundle -- Server/runtime npm packages: Express, Socket.IO, OpenAI SDK, Axios, cors, dotenv, js-yaml, ifvms -- EB Garamond font files - -## Browser-vendored files - -| Component | Files | Upstream/latest check | Local status | -| --- | --- | --- | --- | -| SmartyPants.js | `public/js/smartypants.js` | Local header says `smartypants.js 0.0.6`; npm `smartypants` latest is `0.2.2`. The old `smartypants.js` package name is unpublished from npm. | Not byte-identical to npm `smartypants` 0.0.5, 0.0.9, or 0.2.2. Treat as modified/older vendor code. | -| Hyphenopoly browser files | `public/js/Hyphenopoly.js`, `public/js/Hyphenopoly_Loader.js`, `public/js/hyphenopoly.module.js`, `public/js/patterns/*.wasm` | Browser header says `5.2.0-beta.1`; npm dependency is `6.0.0`; npm latest is `6.1.0`. | `Hyphenopoly.js` is effectively 5.2.0-beta.1 after line-ending normalization. `Hyphenopoly_Loader.js` has a small local/prototype difference in `H.hide`. Browser copy is older than package/latest. | -| Knuth-Plass adapter | `public/js/knuth-and-plass.js` | No authoritative upstream identified from headers or npm metadata. | Modified from the prototype copy and currently application-owned adapter code. | -| Line breaking support | `public/js/linebreak.js`, `public/js/linked-list.js` | No authoritative upstream identified from headers. Not the npm `linebreak` package 1.1.0. | Identical to prototype copies. `linked-list.js` still has a suspicious `get last() { return this.last; }` accessor inherited from the prototype. | -| Kokoro JS browser bundle | `public/js/kokoro-js.js` | npm `kokoro-js` latest is `1.2.1`; installed is `1.2.0`. | Byte-identical to `kokoro-js@1.2.0/dist/kokoro.web.js`; not latest. | - -## Direct runtime npm packages - -| Package | Installed | Latest checked | License | Status | -| --- | --- | --- | --- | --- | -| `inkjs` | 2.4.0 | 2.4.0 | MIT | Current. | -| `hyphenopoly` | 6.0.0 | 6.1.0 | MIT | Not latest. Browser vendored files are older than this dependency. | -| `kokoro-js` | 1.2.0 | 1.2.1 | Apache-2.0 | Not latest. | -| `ifvms` | 1.1.6 | 1.1.6 | MIT | Current. | -| `openai` | 4.91.0 | 6.38.0 | Apache-2.0 | Not latest major. | -| `socket.io` | 4.8.1 | 4.8.3 | MIT | Not latest patch. | -| `express` | 5.1.0 | 5.2.1 | MIT | Not latest patch. | -| `axios` | 1.8.4 | 1.16.1 | MIT | Not latest. | -| `cors` | 2.8.5 | 2.8.6 | MIT | Not latest patch. | -| `dotenv` | 16.4.7 | 17.4.2 | BSD-2-Clause | Not latest major. | -| `js-yaml` | 4.1.0 | 4.1.1 | MIT | Not latest patch. | - -## Notices - -The UI-readable license and credit notice is `public/THIRD_PARTY_NOTICES.md`. - -The root `THIRD_PARTY_NOTICES.md` points to that served file so the repository has an obvious project-level notice entry. diff --git a/TODO.md b/TODO.md deleted file mode 100644 index f1bdc78..0000000 --- a/TODO.md +++ /dev/null @@ -1,129 +0,0 @@ -# TODO And Progress - -This is the active implementation checklist. Architecture lives in `SPECIFICATION.md`; usage lives in `README.md`; authoring conventions live in `MARKUP_GUIDELINES.md`. - -## Current Status - -- The shared client is feature-rich enough for Ink gameplay: line-based book layout, animated text, TTS, music, sound effects, images, choices, glossary notes, save/load restoration, and localized UI are implemented. -- The Ink engine is the current primary development engine. -- The YAML engine and Z-code engine need regression testing after the Ink-heavy client changes. -- Browser TTS and Kokoro provider modules exist but are not yet proven reliable. -- The codebase still contains logging noise and older architecture fragments that need cleanup. - -## Shared Client - -### Completed - -- [x] Native ES module loader, dependency graph, progress overlay, and ordered initialization. -- [x] Responsive book layout that scales page, font sizes, and word positions relative to page size. -- [x] SmartyPants, German guillemet normalization, Hyphenopoly, and Knuth-Plass layout. -- [x] Paragraph/chapter/section/drop-cap rules. -- [x] Markdown emphasis with `*` and `_` syntax. -- [x] Right-page `#gloss[term](definition)` hover/focus notes. -- [x] Image rendering for landscape, square, and portrait cases, with history/save restoration. -- [x] Sound effect and music playback, including music lead-in, loop/once, and ducking. -- [x] TTS `none`, OpenAI, ElevenLabs, Browser Speech, and Kokoro provider modules. -- [x] TTS cache keys include provider, voice, speed, language, and exact normalized string. -- [x] Persisted speech enable state, provider, voice, speed, language, and volume preferences. -- [x] Fast-forward for text animation and active TTS fade/stop. -- [x] Choice UI, explicit keys, automatic key assignment, optional-choice styling, click and keyboard selection. -- [x] Localized popups for endings, errors, achievements, and alerts. -- [x] Credits/license dialog. -- [x] Line-addressed history scrolling model. -- [x] Choice-return turns continue to the choice point when autoplay is off. - -### In Progress - -- [ ] Polish custom scrollbar dragging so the thumb moves freely during drag and commits the scroll target only on release. -- [ ] Tighten automated checks around top-bar/options state initialization after reload. -- [ ] Improve automated visual regression coverage for page scaling, drop caps, image wrapping, and paragraph indentation. -- [ ] Improve automated audio tests for music ducking, sound effect timing, and fast-forward fadeout. -- [ ] Validate provider-specific speed conversion for all TTS providers against real API behavior. - -### Pending - -- [ ] Add a logging module with levels/categories to reduce console output and improve runtime performance. -- [ ] Show startup warnings/instructions when TTS APIs still need to be selected or configured. -- [ ] Put production-ready default option values into code/config. -- [ ] Get Browser TTS working reliably. -- [ ] Get Kokoro.js TTS working for English-language games. -- [ ] Get Kokoro.js TTS working for German-language games. -- [ ] Add a TTS module for self-hosted or local OpenAI-compatible servers. -- [ ] Test every documented `#tag` parameter and effect against parser, server, client rendering, playback, and save/load behavior. -- [ ] Remove local file paths and diff-comments from third-party license markdown, refresh included third-party licenses/material, update external libraries where possible, and move any local modifications into our code. -- [ ] Improve credits page layout with more window height, a larger notices markdown pane, and a Hollywood-style title scroll for creative credits. -- [ ] Clean up unused modules, obsolete functions, legacy comments, and vestigial fragments from older architectures. -- [ ] Add optical margin alignment/punctuation protrusion as typography polish if current hanging punctuation proves insufficient. - -## Shared Server Architecture - -### Completed - -- [x] Shared `TurnResult` protocol used by all engines. -- [x] Shared game API shape: `newGame`, `loadGame`, `saveGame`, `hasSaveGame`, `getSaveGames`, `isGameRunning`. -- [x] Per-engine config files with metadata, locale, main game file, and asset paths. -- [x] `.env` default engine selection for `npm run dev` and `npm run start`. -- [x] Engine-specific dev/start/debug/inspect scripts. -- [x] YAML server renamed to `server-yaml.ts` so it is no longer implied as the generic server. -- [x] Z-code server/config/scripts use `zcode` naming; Zork is only the current story/prompt target. - -### Pending - -- [ ] Extract duplicated Express/Socket.IO/static-file/port-fallback setup into a shared server base. -- [ ] Replace session-local placeholder saves with durable server-side or browser-coordinated saves where appropriate. -- [ ] Clean up start scripts and add a Dockerfile for hosting the selected engine on Coolify. -- [ ] Decide whether `src/index.ts` should remain as the YAML CLI entry or be replaced by clearer `cli-yaml.ts` and engine-specific launchers. -- [ ] Remove `test-server-yaml.ts` if no current workflow depends on it. -- [ ] Add logger configuration to scripts: `LOG_LEVEL`, `LOG_CATEGORIES`, and engine debug defaults. - -## Ink Engine - -### Completed - -- [x] Ink source compilation through `inkjs/full`. -- [x] Split Ink source files with a master include file. -- [x] Ink metadata handoff to client. -- [x] Ink choices converted to `ChoiceResult`. -- [x] Ink tags converted to shared `StoryTag`. -- [x] Choice preview tags for `#key`, `#letter`, `#optional`, and `#action`. -- [x] Save/load of Ink state plus client history state. -- [x] `#score`, `#error`, `#achievement`, and `#alert` tag behavior. -- [x] `#gloss[term](definition)` support on right-page text. - -### Pending - -- [ ] Add text-input turns to Ink games, switching the UI to command input for one round and returning to choices afterward. -- [ ] Add a full dynamic description of the created character to the score panel after the game intro. -- [ ] Continue authoring and testing Eibenreith content. -- [ ] Test all documented tag syntax inside real Ink source, including edge cases with includes and choice-local tags. - -## YAML Engine - -### Completed - -- [x] Deterministic YAML world model and `GameRunner`. -- [x] YAML CLI path for testing without browser UI. -- [x] YAML web server emits `TurnResult` objects. - -### Pending - -- [ ] Test/debug the YAML engine after Ink-driven client changes. -- [ ] Continue development of the YAML engine. -- [ ] Replace command mirroring with the full LLM/world-model command loop when typography/audio testing no longer needs mirroring. -- [ ] Validate YAML-generated `#` tags through the shared parser/protocol path. - -## Z-code Engine - -### Completed - -- [x] Z-code naming for engine scripts/config/server. -- [x] Current Zork I narrator implementation using `ifvms` plus OpenRouter prompt templates. -- [x] Z-code engine emits shared `TurnResult` objects. - -### Pending - -- [ ] Test/debug the Z-code engine after Ink-driven client changes. -- [ ] Finish the Z-code version: optimize prompt templates, choose the best LLM for the task, and test project memory behavior. -- [ ] Separate Z-code-generic logic from Zork-specific prompt assumptions. -- [ ] Validate save/restore of Z-machine state. -- [ ] Merge this branch with `master` after YAML and Z-code regression testing. diff --git a/ai.interactive.fiction.code-workspace b/ai.interactive.fiction.code-workspace deleted file mode 100644 index 362d7c2..0000000 --- a/ai.interactive.fiction.code-workspace +++ /dev/null @@ -1,7 +0,0 @@ -{ - "folders": [ - { - "path": "." - } - ] -} \ No newline at end of file diff --git a/config/engines/yaml.json b/config/engines/yaml.json deleted file mode 100644 index a865863..0000000 --- a/config/engines/yaml.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "engine": "yaml", - "locale": "en_US", - "paths": { - "mainGameFile": "data/worlds/example_world.yml", - "music": "public/music", - "sfx": "public/sounds", - "images": "public/images" - }, - "metadata": { - "title": "The Mysterious Mansion", - "author": "AI Interactive Fiction", - "subtitle": "An open-world text adventure", - "version": "1.0.0", - "language": "en_US", - "copyright": "Prototype content for local development." - } -} diff --git a/config/engines/zcode.json b/config/engines/zcode.json deleted file mode 100644 index 76ad465..0000000 --- a/config/engines/zcode.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "engine": "zcode", - "locale": "en_US", - "paths": { - "mainGameFile": "data/z-code/zork1.bin", - "promptDir": "data/zcode-prompts", - "music": "public/music", - "sfx": "public/sounds", - "images": "public/images" - }, - "metadata": { - "title": "Zork I", - "author": "Infocom", - "subtitle": "A narrated Z-code adventure", - "version": "1.0.0", - "language": "en_US", - "copyright": "Use only with a legally supplied Z-code story file." - } -} diff --git a/data/ink-src/kaiserpunk.ink b/data/ink-src/kaiserpunk.ink deleted file mode 100644 index e57d934..0000000 --- a/data/ink-src/kaiserpunk.ink +++ /dev/null @@ -1,1632 +0,0 @@ -// Eibenreith: A Kaiserpunk Adventure -// Ink intro draft -// Compile-safe revision: protagonist speech only appears through explicit choices. - -VAR birth_class = "unset" -VAR title_part = "" -VAR given_names = "" -VAR common_name = "" -VAR surname = "" - -VAR baggage_style = "unset" -VAR hair_detail = "unset" -VAR complexion_detail = "unset" -VAR face_detail = "unset" -VAR outfit_detail = "unset" - -VAR supernatural_belief = "unset" -VAR supernatural_senses = "unset" -VAR viktor_relation = "unset" - -VAR lover = 0 -VAR sapphic = 0 -VAR detective = 0 -VAR careless = 0 -VAR eccentric = 0 -VAR class_confidence = 0 -VAR medium_reputation = 0 -VAR court_loyalty = 0 -VAR viktor_trust = 0 -VAR viktor_suspicion = 0 -VAR supernatural_exposure = 0 - --> intro_train - -=== intro_train === - -The train has left Vienna behind, though Vienna has not yet left you. #chapter[Eibenreith] #music[Kaiserpunk Waltz.mp3](crossfade, loop, lead=8) - -It clings to the black gloss of your travelling boots, to the cut of your coat, to the stiff little prison of your gloves. It lives in the seal upon the letter folded inside your reticule, in the thin scent of coal smoke that has insinuated itself even into first-class upholstery, in the fact that Herr Viktor Nowak sits opposite you as if the carriage were a field office and not a compartment lined in velvet, polished wood, and brass. - -Outside the window, the last outskirts of the capital have broken apart into winter-browned fields and villages with church towers too small to compete with the engine's whistle. The rails take the land without asking permission. Embankments cut through orchards. Telegraph poles pass at regular intervals, each one vanishing behind you like a thought dismissed too quickly. #sfx[steam-whistle.ogg] - -You had expected the train to feel like a triumph of the age. - -Instead it feels like an argument. #image[suedbahn.png](landscape) - -The machine throws itself southward with a violence that polite society would never admit to admiring. The lamps tremble in their fittings. Your cup rattles against its saucer. Beyond the glass, the country begins to rise, first gently, then with a firmer will, until the line itself seems to negotiate with the mountains through stone arches, black tunnels, and viaducts thrown across ravines with all the confidence of imperial engineering. - -Viktor has not looked impressed once. - -His civilian clothes are correct enough to pass without comment: dark frock coat, sober waistcoat, gloves, collar immaculate, the posture of a man who has never truly sat at ease in his life. But no tailor can disguise discipline. It remains in his shoulders, in the economy of his movements, in the way his eyes measure doors, windows, luggage rack, corridor, your face, then the door again. - -On the paperwork he is your secretary and travelling companion. - -In truth, he is an officer lent to a delicate matter by channels that prefer not to be named. Rittmeister Viktor Alois Nowak, though no one at Jagdhaus Hohenreith is expected to call him that. Your hosts have asked for a medium. The Cabinet has sent them you. The military has sent him to make certain that you do not become a scandal before you become useful. - -He folds the newspaper, though you are quite certain he had not been reading it. - -"You have been very quiet, gnädiges Fräulein. For a lady on her first official journey, you show remarkable restraint." - -The form of address is technically correct if you are noble, excessive if you are not, and perfectly chosen because he does not yet know which part of you is useful, which part is costume, and which part is threat. - -You look around the compartment before you answer. The answer comes from somewhere older than the letter in your reticule. It comes from the place you began. - -* [The compartment seems built for people who never wonder whether they belong in it.] - #class:noble - ~ birth_class = "noble" - ~ class_confidence += 2 - ~ court_loyalty += 1 - The compartment seems built for people who never wonder whether they belong in it. - - It is not luxury that unsettles you. Luxury is only wood, cloth, brass, service, silence. What matters is whether the servants glance twice, whether the guard lowers his voice, whether another passenger weighs your gloves and decides not to ask your business. - - You were born among people who understood such things before they understood kindness. - - -> class_noble_background - -* [You count the cost of each detail before you can stop yourself.] - #class:middle - ~ birth_class = "middle" - ~ class_confidence += 1 - You count the cost of each detail before you can stop yourself. - - The upholstery, the lamps, the polished veneer, the quiet attendance at stations: none of it is magical. It is paid for. Accounted for. Itemised somewhere by someone with ink on his cuffs and a wife who knows how long candles may be burned before the household budget complains. - - You were not born to this compartment, but you were born close enough to study its rules. - - -> class_middle_background - -* [You notice first how clean everything is, and how carefully one must sit so as not to betray noticing.] - #class:working - ~ birth_class = "working" - ~ class_confidence -= 1 - You notice first how clean everything is, and how carefully one must sit so as not to betray noticing. - - The velvet looks soft enough to swallow fingerprints. The brass fittings have been polished by hands that will never sit here. The little curtain strap is worn where other travellers, all of them more certain than you, have touched it without gratitude. - - You were not born on this side of service. - - -> class_working_background - -=== class_noble_background === - -Viktor waits for the answer owed to his remark. The train jolts once, then settles again into its hard, confident rhythm. - -* ["Restraint is not a virtue, Herr Nowak. It is often only good breeding with its mouth shut."] - #route:eccentric - ~ eccentric += 1 - "Restraint is not a virtue, Herr Nowak. It is often only good breeding with its mouth shut." - - His brows move almost imperceptibly. - - "Then good breeding has military applications," he says. - - -> class_noble_followup_breeding - -* ["You need not test whether I can sit still, Herr Nowak. I was trained by people with less patience and sharper eyes."] - #route:detective - ~ detective += 1 - ~ viktor_trust += 1 - "You need not test whether I can sit still, Herr Nowak. I was trained by people with less patience and sharper eyes." - - "A family education, then." - - -> class_noble_followup_family - -* ["If this is remarkable restraint, Herr Nowak, I fear you have mostly escorted officers."] - #route:lover - ~ lover += 1 - ~ viktor_suspicion += 1 - "If this is remarkable restraint, Herr Nowak, I fear you have mostly escorted officers." - - The corner of his mouth changes almost too little to notice. - - "Officers are less easily bored." - - -> class_noble_followup_officers - -=== class_noble_followup_breeding === - -* ["Only when properly commanded."] - #route:lover - ~ lover += 1 - ~ viktor_suspicion += 1 - "Only when properly commanded." - - "You intend to command it yourself?" - - The answer remains in the angle of your glove and the calmness of your stare. - - -> class_noble_explanation - -* ["Only when men mistake silence for obedience."] - #route:sapphic - ~ sapphic += 1 - ~ viktor_suspicion += 1 - "Only when men mistake silence for obedience." - - "That is an ambitious distinction." - - The distinction has preserved many women from being understood too early. - - -> class_noble_explanation - -* ["I prefer any discipline that leaves a clean record."] - #route:detective - ~ detective += 1 - ~ viktor_trust += 1 - "I prefer any discipline that leaves a clean record." - - "A useful preference," he says. "If sincere." - - The conditional nature of sincerity remains, for the moment, his problem to investigate. - - -> class_noble_explanation - -=== class_noble_followup_family === - -* ["A family sentence, more often."] - #route:eccentric - ~ eccentric += 1 - "A family sentence, more often." - - "You speak as if birth were a prison." - - The polished furniture answers him better than an argument could. - - -> class_noble_explanation - -* ["An education in rooms where every chair has rank."] - ~ class_confidence += 1 - "An education in rooms where every chair has rank." - - "Then Hohenreith may not surprise you." - - The possibility that Hohenreith may possess better secrets than chairs is allowed to stand. - - -> class_noble_explanation - -=== class_noble_followup_officers === - -* ["Or less honest about it."] - #route:lover - ~ lover += 1 - "Or less honest about it." - - "You accuse the army of vanity." - - The accusation of consistency is the least deniable one. - - -> class_noble_explanation - -* ["Then I must try not to disappoint the army."] - #route:careless - ~ careless += 1 - "Then I must try not to disappoint the army." - - "That is precisely what I have been asked to prevent." - - -> class_noble_explanation - -=== class_noble_explanation === - -You learned young that every room contains a court, even when no emperor is present. A girl of your rank is trained to enter, to bow, to be introduced, to be placed, to speak only enough, to understand more than she admits, and to know that a family name can be both a key and a chain. - -Your own family possesses no grand seat, no army of retainers, no ancient right to command provinces. But your name opened drawing-room doors in Vienna, and once inside those rooms you learned to make people repeat stories they had meant only to hint at. You learned how widows speak when priests are absent, how officers lie when flattered, how old men confess when they believe themselves admired, and how a young woman may be underestimated so consistently that underestimation becomes a profession. - -Your reputation as a medium did not descend from heaven. It was assembled from half-lights, correct guesses, careful silences, and the willingness of better-born fools to mistake performance for revelation. - -Before the court could use you, society had first to invent you. - -Vienna knew you by the name society had made useful. - --> choose_name_noble - -=== class_middle_background === - -Viktor's remark lingers between you with the odour of a polite accusation. - -* ["Restraint is easier when one has learned that every mistake is remembered by someone better placed."] - #route:detective - ~ detective += 1 - ~ viktor_trust += 1 - "Restraint is easier when one has learned that every mistake is remembered by someone better placed." - - Viktor watches you more closely. - - "A bitter lesson." - - -> class_middle_followup_bitter - -* ["If I am quiet, Herr Nowak, it is because men explain themselves faster when they dislike the silence."] - #route:lover - ~ lover += 1 - ~ medium_reputation += 1 - "If I am quiet, Herr Nowak, it is because men explain themselves faster when they dislike the silence." - - "A method?" - - -> class_middle_followup_method - -* ["I was considering whether your concern is official, personal, or merely masculine."] - #route:eccentric - ~ eccentric += 1 - ~ viktor_suspicion += 1 - "I was considering whether your concern is official, personal, or merely masculine." - - His eyes harden by one exact degree. - - "Today it is official." - - -> class_middle_followup_official - -=== class_middle_followup_bitter === - -* ["A useful one. Bitterness is merely the taste left by instruction."] - #route:eccentric - ~ eccentric += 1 - "A useful one. Bitterness is merely the taste left by instruction." - - "You collect phrases like weapons." - - The phrase lies lightly enough that he must decide whether it is ornament or weapon. - - -> class_middle_explanation - -* ["A common one. Some people only notice injustice when it reaches their own floor."] - #route:sapphic - ~ sapphic += 1 - "A common one. Some people only notice injustice when it reaches their own floor." - - "You have made a study of floors?" - - Thresholds, you have learned, are more honest than floors; they admit that passage is a privilege. - - -> class_middle_explanation - -=== class_middle_followup_method === - -* ["A courtesy. I let them begin with their favorite subject."] - #route:lover - ~ lover += 1 - "A courtesy. I let them begin with their favorite subject." - - "Themselves." - - His own answer completes the principle neatly enough that further instruction would be vanity. - - -> class_middle_explanation - -* ["An experiment. It has produced reliable results."] - #route:detective - ~ detective += 1 - "An experiment. It has produced reliable results." - - "Then I am part of your experiment." - - He has sat opposite you long enough to become evidence. - - -> class_middle_explanation - -=== class_middle_followup_official === - -* ["How convenient. The other two may deny responsibility."] - #route:eccentric - ~ eccentric += 1 - "How convenient. The other two may deny responsibility." - - "I advise you not to make wit your first instrument at Hohenreith." - - The demotion of wit to second instrument remains sufficiently theoretical to be safe. - - -> class_middle_explanation - -* ["Then I shall treat it with the respect due to paper."] - #route:detective - ~ detective += 1 - "Then I shall treat it with the respect due to paper." - - "Paper has moved armies." - - Paper has also buried mistakes, but not all corrections need to be spoken aloud. - - -> class_middle_explanation - -=== class_middle_explanation === - -You were born in that broad, anxious territory between deference and ambition. Your family had books, invoices, respectability, perhaps a piano no one played well enough, perhaps a father with an office, a mother with callers, brothers who were expected to advance, and daughters who were expected not to make advancement look hungry. - -You learned accounts before etiquette, etiquette before French, and French before you learned how easily a woman with a calm voice could make men explain themselves. You rose because you listened. You rose because you understood that fraud, faith, medicine, gossip, politics, and grief all use the same doors into the human mind. - -The court does not like to admit that it needs middle-class competence. It prefers to borrow it, dress it properly, and call it discretion. - -Your reputation as a medium gave them a word that sounded less dangerous than investigator. - -The salons that first laughed at you, then invited you back, learned your name before they learned what it cost. - --> choose_name_middle - -=== class_working_background === - -Viktor's courtesy is smooth enough to be handled without fingerprints. You hear, beneath it, the question of how much this compartment has improved you. - -* ["Restraint is what people praise when they prefer not to see the effort."] - #route:detective - ~ detective += 1 - ~ viktor_trust += 1 - "Restraint is what people praise when they prefer not to see the effort." - - The newspaper in Viktor's hand creases once. - - "You object to being praised?" - - -> class_working_followup_praise - -* ["I am quiet because people often prefer women of my origin either grateful or invisible."] - #route:eccentric - ~ eccentric += 1 - ~ viktor_suspicion += 1 - "I am quiet because people often prefer women of my origin either grateful or invisible." - - "I did not ask you to be grateful." - - -> class_working_followup_grateful - -* ["I was trying not to touch the upholstery as though it might accuse me."] - #route:careless - ~ careless += 1 - ~ viktor_relation = "dependence" - "I was trying not to touch the upholstery as though it might accuse me." - - Something like concern crosses his face, disguised too late as irritation. - - "The upholstery has survived ministers. It will survive you." - - -> class_working_followup_upholstery - -=== class_working_followup_praise === - -* ["Only cheaply."] - #route:eccentric - ~ eccentric += 1 - "Only cheaply." - - "That may be difficult to avoid." - - Hohenreith, if it intends to praise you cheaply, will have to discover the economy of disappointment. - - -> class_working_explanation - -* ["Only when it hides the person doing the work."] - #route:sapphic - ~ sapphic += 1 - "Only when it hides the person doing the work." - - He studies you as if the answer has come from farther down the train than first class. - - -> class_working_explanation - -=== class_working_followup_grateful === - -* ["No. You asked me to be manageable."] - #route:eccentric - ~ eccentric += 1 - "No. You asked me to be manageable." - - "I asked you nothing of the kind." - - Rank, unlike men, rarely needs to ask directly. The furniture asks for it. - - -> class_working_explanation - -* ["Then I shall postpone gratitude until you deserve it."] - #route:lover - ~ lover += 1 - "Then I shall postpone gratitude until you deserve it." - - A pause. Then, very dryly: "A generous arrangement." - - -> class_working_explanation - -=== class_working_followup_upholstery === - -* ["Then I am already braver than I was."] - #route:careless - ~ careless += 1 - "Then I am already braver than I was." - - "Courage measured against upholstery is not a military standard." - - -> class_working_explanation - -* ["I shall take that as reassurance, though you delivered it like a reprimand."] - #route:lover - ~ lover += 1 - "I shall take that as reassurance, though you delivered it like a reprimand." - - "I have had practice with both." - - -> class_working_explanation - -=== class_working_explanation === - -You were born among people who owned little but obligations. Work had a sound before it had a meaning: water, broom, bootsteps, breath, the clatter of dishes, the cough of men coming in from cold yards, women counting coins under their breath. You learned early that the high-born are not more observant than others. They are merely less often required to observe. - -That was your first advantage. - -A servant knows which door matters because she uses the others. A seamstress learns bodies because she measures them. A maid learns secrets because fine people leave their souls lying about like gloves, certain that no one beneath them has hands. - -You rose by talent, patronage, imitation, nerve, and the terrible convenience of being believed harmless. By the time Vienna began whispering that you saw more than respectable people saw, you had already spent years seeing what respectable people missed. - -The court has placed you in first class because it needs what birth did not give you. - -The name you carried upward was altered perhaps in pronunciation, never quite cleansed of where it began. - --> choose_name_working - -=== choose_name_noble === - -* [Valerie Eleonore Josepha] - ~ given_names = "Valerie Eleonore Josepha" - ~ common_name = "Valerie" - -> choose_surname_noble -* [Helene Cäcilie Franziska] - ~ given_names = "Helene Cäcilie Franziska" - ~ common_name = "Helene" - -> choose_surname_noble -* [Clara Theresia Leopoldine] - ~ given_names = "Clara Theresia Leopoldine" - ~ common_name = "Clara" - -> choose_surname_noble -* [Sophie Eleonore Auguste] - ~ given_names = "Sophie Eleonore Auguste" - ~ common_name = "Sophie" - -> choose_surname_noble -* [Mathilde Josepha Henriette] - ~ given_names = "Mathilde Josepha Henriette" - ~ common_name = "Mathilde" - -> choose_surname_noble -* [Therese Valerie Franziska] - ~ given_names = "Therese Valerie Franziska" - ~ common_name = "Therese" - -> choose_surname_noble -* [Ilona Theresia Eleonore] - ~ given_names = "Ilona Theresia Eleonore" - ~ common_name = "Ilona" - -> choose_surname_noble -* [Zdenka Eleonore Josepha] - ~ given_names = "Zdenka Eleonore Josepha" - ~ common_name = "Zdenka" - -> choose_surname_noble - -=== choose_surname_noble === - -Your title is fixed by birth and by the careful modesty of your family: not countess, not princess, not one of the brilliant names that gather ambassadors and creditors like dust. - -A Freiin. Baronial. Usable. Admitted, but not enthroned. - -* [Freiin von Rauhenfels] - ~ title_part = "Freiin von" - ~ surname = "Rauhenfels" - -> assemble_full_name -* [Freiin von Traunegg] - ~ title_part = "Freiin von" - ~ surname = "Traunegg" - -> assemble_full_name -* [Freiin von Ebenwald] - ~ title_part = "Freiin von" - ~ surname = "Ebenwald" - -> assemble_full_name -* [Freiin von Arnsberg] - ~ title_part = "Freiin von" - ~ surname = "Arnsberg" - -> assemble_full_name -* [Freiin von Reichenau] - ~ title_part = "Freiin von" - ~ surname = "Reichenau" - -> assemble_full_name -* [Freiin von Waldstätten] - ~ title_part = "Freiin von" - ~ surname = "Waldstätten" - -> assemble_full_name - -=== choose_name_middle === - -* [Clara Eleonore] - ~ given_names = "Clara Eleonore" - ~ common_name = "Clara" - -> choose_surname_middle -* [Anna Katharina] - ~ given_names = "Anna Katharina" - ~ common_name = "Anna" - -> choose_surname_middle -* [Helene Theresia] - ~ given_names = "Helene Theresia" - ~ common_name = "Helene" - -> choose_surname_middle -* [Rosa Franziska] - ~ given_names = "Rosa Franziska" - ~ common_name = "Rosa" - -> choose_surname_middle -* [Johanna Elise] - ~ given_names = "Johanna Elise" - ~ common_name = "Johanna" - -> choose_surname_middle -* [Katharina Sophie] - ~ given_names = "Katharina Sophie" - ~ common_name = "Katharina" - -> choose_surname_middle -* [Therese Leopoldine] - ~ given_names = "Therese Leopoldine" - ~ common_name = "Therese" - -> choose_surname_middle -* [Magdalena Cäcilie] - ~ given_names = "Magdalena Cäcilie" - ~ common_name = "Magdalena" - -> choose_surname_middle - -=== choose_surname_middle === - -Your family name contains no particle to soften the ascent. It must stand upright by itself. - -* [Leitner] - ~ title_part = "Fräulein" - ~ surname = "Leitner" - -> assemble_full_name -* [Wagner] - ~ title_part = "Fräulein" - ~ surname = "Wagner" - -> assemble_full_name -* [Kellner] - ~ title_part = "Fräulein" - ~ surname = "Kellner" - -> assemble_full_name -* [Baumgartner] - ~ title_part = "Fräulein" - ~ surname = "Baumgartner" - -> assemble_full_name -* [Fischer] - ~ title_part = "Fräulein" - ~ surname = "Fischer" - -> assemble_full_name -* [Schmid] - ~ title_part = "Fräulein" - ~ surname = "Schmid" - -> assemble_full_name -* [Pichler] - ~ title_part = "Fräulein" - ~ surname = "Pichler" - -> assemble_full_name -* [Rosenfeld] - ~ title_part = "Fräulein" - ~ surname = "Rosenfeld" - -> assemble_full_name - -=== choose_name_working === - -* [Anna] - ~ given_names = "Anna" - ~ common_name = "Anna" - -> choose_surname_working -* [Klara] - ~ given_names = "Klara" - ~ common_name = "Klara" - -> choose_surname_working -* [Agnes] - ~ given_names = "Agnes" - ~ common_name = "Agnes" - -> choose_surname_working -* [Leni] - ~ given_names = "Leni" - ~ common_name = "Leni" - -> choose_surname_working -* [Rosa] - ~ given_names = "Rosa" - ~ common_name = "Rosa" - -> choose_surname_working -* [Gertrud] - ~ given_names = "Gertrud" - ~ common_name = "Gertrud" - -> choose_surname_working -* [Elisabeth] - ~ given_names = "Elisabeth" - ~ common_name = "Elisabeth" - -> choose_surname_working -* [Franziska] - ~ given_names = "Franziska" - ~ common_name = "Franziska" - -> choose_surname_working - -=== choose_surname_working === - -A simple name can be a burden in Vienna. It tells people how little they must pretend to respect you before you have spoken. - -* [Pichler] - ~ title_part = "Fräulein" - ~ surname = "Pichler" - -> assemble_full_name -* [Huber] - ~ title_part = "Fräulein" - ~ surname = "Huber" - -> assemble_full_name -* [Maier] - ~ title_part = "Fräulein" - ~ surname = "Maier" - -> assemble_full_name -* [Gruber] - ~ title_part = "Fräulein" - ~ surname = "Gruber" - -> assemble_full_name -* [Schuster] - ~ title_part = "Fräulein" - ~ surname = "Schuster" - -> assemble_full_name -* [Krenn] - ~ title_part = "Fräulein" - ~ surname = "Krenn" - -> assemble_full_name -* [Wolf] - ~ title_part = "Fräulein" - ~ surname = "Wolf" - -> assemble_full_name -* [Moser] - ~ title_part = "Fräulein" - ~ surname = "Moser" - -> assemble_full_name - -=== assemble_full_name === - -{birth_class == "noble": - On visiting cards, in letters, in the cautious mouths of servants, you are {given_names} {title_part} {surname}. -- else: - On railway documents, hotel ledgers, and the tongues of people who have not yet decided how much respect you deserve, you are {title_part} {given_names} {surname}. -} - -But in the private chamber where a name is first answered before it is performed, you are {common_name}. - -Viktor has waited through your silence with a soldier's patience and a jailer's courtesy. The train enters another tunnel. For several seconds the compartment window gives you back only your own reflection: your hat, your pale face above the dark collar, your eyes too steady or not steady enough. - -When the mountains return, they seem closer. - --> mirror_definition - -=== mirror_definition === - -The black glass gives you the woman who will arrive in Eibenreith before any rumour has time to improve her. - -The window catches one thing before the rest. - -* [Dark ash-brown hair pinned with almost severe care.] - ~ hair_detail = "dark ash-brown" - The reflection has dark ash-brown hair, almost black where the tunnel has not quite left it, pinned with the severity of a woman who knows that disarray is forgiven less easily in the young. - -> mirror_complexion - -* [Chestnut hair arranged to look softer than the mind beneath it.] - ~ hair_detail = "chestnut" - The reflection has chestnut hair, warm where the lamp touches it, arranged with enough softness to flatter and enough control to warn the attentive. - -> mirror_complexion - -* [Cool fair hair made austere by dark travelling clothes.] - ~ hair_detail = "cool fair" - The reflection has cool fair hair, not golden enough for sentimental painters, but pale enough that the dark travelling clothes make your face appear more deliberate than gentle. - -> mirror_complexion - -* [Black-brown hair with a few escaped wisps already refusing discipline.] - ~ hair_detail = "black-brown" - The reflection has black-brown hair, glossy in the lamp's weak tremor, with two escaped wisps at the temple already committing small treasons against the pins. - -> mirror_complexion - -=== mirror_complexion === - -The window deepens, and with it the face. - -* [A fair, cool complexion that looks almost bloodless in railway light.] - ~ complexion_detail = "fair and cool" - Your complexion is fair and cool, made paler by smoke, glass, and the faint greenish cast of mountain light. - -> mirror_face - -* [A clear complexion that still remembers the city more than the sun.] - ~ complexion_detail = "clear and sheltered" - Your complexion has the clarity of rooms, gloves, and shaded streets; not sickly, but visibly protected from the labour that browns other lives. - -> mirror_face - -* [A face that has learned to look fragile when fragility is useful.] - ~ complexion_detail = "delicately pale" - Your complexion is delicately pale, the sort physicians and foolish men read too eagerly, and which you have never felt obliged to correct every time. - -> mirror_face - -* [A slightly warmer complexion that makes the severity of the outfit less forgiving.] - ~ complexion_detail = "warm fair" - Your complexion is warmer than Vienna's winter light would prefer, and that warmth makes the severe coat and collar seem chosen rather than imposed. - -> mirror_face - -=== mirror_face === - -The window keeps you just long enough to make judgement impolite. - -* [A long oval face, observant before it is beautiful.] - ~ face_detail = "long oval and observant" - The face is long and oval, with watchful eyes, a straight nose, and a mouth that knows how often women are punished for amusement before men call them clever. - -> mirror_outfit - -* [A composed face that seems trained equally for salons and interrogations.] - ~ face_detail = "composed and trained" - The face is composed rather than soft, with brows dark enough to sharpen silence and a mouth whose politeness has not yet promised mercy. - -> mirror_outfit - -* [A pretty face made less harmless by the steadiness of the eyes.] - ~ face_detail = "pretty but steady" - The face might be called pretty by people who do not like to work harder for adjectives, but the eyes disturb the compliment by appearing to have heard it before and found it insufficient. - -> mirror_outfit - -* [A severe face that becomes almost vulnerable only when caught unprepared.] - ~ face_detail = "severe and guarded" - The face is severe at first glance, guarded at the second, and only after that does it betray how young a woman may still be while carrying herself like a sealed document. - -> mirror_outfit - -=== mirror_outfit === - -The rest of the reflection is costume, armour, and evidence. - -* [Dark charcoal-plum travelling wool, restrained but expensive.] - ~ outfit_detail = "charcoal-plum travelling wool" - You wear a tailored travelling ensemble of dark charcoal wool with a plum undertone, high at the throat, close at the waist, expensive in the way that avoids asking to be admired. - -> supernatural_stance - -* [A severe black-brown coat and skirt, softened only by ivory at throat and cuffs.] - ~ outfit_detail = "black-brown travelling suit" - You wear a black-brown travelling suit and long coat, softened only by ivory at throat and cuffs, the lace narrow enough to seem like restraint rather than decoration. - -> supernatural_stance - -* [Bottle-green details hidden in an otherwise sombre travelling suit.] - ~ outfit_detail = "sombre suit with bottle-green details" - You wear a sombre travelling suit with bottle-green details so discreet that only close attention discovers them, which is nearly the point. - -> supernatural_stance - -* [A more fashionable ensemble, dark and narrow, calculated to be remembered.] - ~ outfit_detail = "fashionable dark travelling ensemble" - You wear a dark, narrow, more fashionable travelling ensemble, the hat and veil chosen with just enough theatrical instinct to make sceptical people use the word instinctive. - -> supernatural_stance - -=== supernatural_stance === - -The letter of commission in your reticule does not call you an investigator. - -It calls you, in prose dry enough to pass through any number of offices, a woman whose unusual spiritual reputation has recommended her to a delicate household matter. The phrasing is exquisite. It neither affirms nor denies. It permits everyone involved to believe afterward that they had believed nothing improper. - -The comital family at Jagdhaus Hohenreith has asked for discretion. Vienna has answered with a sealed letter, a woman reputed to speak with what is hidden, and a man opposite her who has orders of his own. - -Before this journey, before this train, before the mountains began taking the sky piece by piece, belief had already taken its position in you. - -* [The dead are not silent. The living are merely poor listeners.] - #supernatural:believer - ~ supernatural_belief = "believer" - ~ medium_reputation += 1 - ~ supernatural_exposure += 1 - The dead are not silent. The living are merely poor listeners. - - You have always thought disbelief a provincial arrogance of the educated. There are pressures in rooms where grief has been. There are words people speak before they know they have spoken. There are dreams that arrive with mud on their hems. - - Perhaps the world is not haunted. Perhaps it is simply crowded. - - -> spiritual_senses - -* [The supernatural is usually pain, fraud, fever, inheritance, or bad ventilation.] - #supernatural:sceptic - #route:detective - ~ supernatural_belief = "sceptic" - ~ detective += 1 - The supernatural is usually pain, fraud, fever, inheritance, or bad ventilation. - - The word spirit covers too much and explains too little. You have watched respectable people call an echo a message, a coincidence a sign, a trembling hand an angelic visitation. Men of science can be fools, but fools with candles and planchettes are no improvement. - - If Hohenreith has ghosts, you expect them to keep accounts, write letters, leave footprints, and benefit someone. - - -> spiritual_senses - -* [Belief is a costume. You wear it because men insist on dressing you in it.] - #supernatural:performer - ~ supernatural_belief = "performer" - ~ medium_reputation += 2 - Belief is a costume. You wear it because men insist on dressing you in it. - - You discovered early that men who distrust a woman's mind will sometimes worship her nerves. A conclusion from evidence irritates them. A vision, sighed through lowered lashes, makes them lean closer. - - Very well. Let them lean. - - -> spiritual_senses - -* [You have learned not to decide too early.] - #supernatural:undecided - ~ supernatural_belief = "undecided" - You have learned not to decide too early. - - There are things you can explain, things you cannot yet explain, and things that explanation damages before it helps. You have made a profession of standing at thresholds with a face composed enough for both sides to continue speaking. - - Hohenreith will have to show you what kind of case it is. - - -> spiritual_senses - -=== spiritual_senses === - -Belief is one matter. Experience is another. - -People call a woman sensitive when they want her perceptions to sound like an illness. They call her hysterical when those perceptions inconvenience them. They call her inspired when they need her, and unstable when they do not. - -Beneath reputation and performance, memory has its own testimony. - -* [There have been moments you cannot explain away.] - #powers:genuine - ~ supernatural_senses = "genuine" - ~ supernatural_exposure += 2 - There have been moments you cannot explain away. - - Once, as a child, you knew before the telegram came. Once, in a crowded room, a stranger's grief entered you with such force that your own knees failed. Once, in a mirror, you saw a door behind you that was not in the room when you turned. - - You learned caution after that. It is unwise for a woman to know things before a man has asked her opinion. - - -> viktor_first_exchange - -* [Everything you do can be explained by observation, timing, and nerve.] - #powers:faked - #route:detective - ~ supernatural_senses = "faked" - ~ detective += 1 - Everything you do can be explained by observation, timing, and nerve. - - You notice rings removed too recently, mourning gloves worn too carefully, letters folded and refolded until the crease gives away the reader's obsession. You hear servants misname guests, mothers pause before daughters' rooms, officers lie by becoming too exact. - - The dead have never told you anything. The living cannot stop telling you everything. - - -> viktor_first_exchange - -* [Something happens, but never when summoned.] - #powers:ambiguous - ~ supernatural_senses = "ambiguous" - ~ supernatural_exposure += 1 - Something happens, but never when summoned. - - Your reputation depends upon command. The truth, if truth it is, has no respect for appointments. - - Sometimes a room changes pressure around you. Sometimes a face acquires an old expression no living person taught it. Sometimes names arrive before introductions. But the harder you reach, the more ordinary the world becomes. - - -> viktor_first_exchange - -* [You buried the first signs so thoroughly that even you do not know what remains.] - #powers:repressed - #route:eccentric - ~ supernatural_senses = "repressed" - ~ eccentric += 1 - You buried the first signs so thoroughly that even you do not know what remains. - - There are childhood memories sealed behind politeness: a nursery mirror turned to the wall, a nurse dismissed without reference, your mother's hand tightening around your wrist until the bones complained. - - You became strange afterward in ways society found easier to admire than understand. - - -> viktor_first_exchange - -=== viktor_first_exchange === - -The train emerges from the tunnel into a pale afternoon cut by dark firs and white rock. Far below, water shows itself only in flashes. The valley is no longer a view from a salon painting. It has depth enough to hide things. - -Viktor opens a leather folder and removes a memorandum. He does not hand it to you at once. - -"When we leave the railway," he says, "we will be met by a coach from Hohenreith. From that moment, appearances matter. Your hosts have been told that I assist with correspondence, travel, and practical arrangements. They need not be troubled with military definitions." - -* ["And the villagers?"] - "And the villagers?" - - "The villagers need not be troubled with anything." - - There it is: the empire in miniature. A man, a folder, a locked sentence. - - -> viktor_restraint_advice - -* ["How merciful. The empire has spared them vocabulary."] - #route:eccentric - ~ eccentric += 1 - ~ viktor_suspicion += 1 - "How merciful. The empire has spared them vocabulary." - - "The empire has spared them alarm," Viktor says. - - The empire's habit of confusing ignorance with calm does not require stating to remain present. - - -> viktor_restraint_advice - -* ["You mean they are not to know whether I am guest, tool, or warning."] - #route:detective - ~ detective += 1 - ~ viktor_trust += 1 - "You mean they are not to know whether I am guest, tool, or warning." - - "I mean they are to know only what steadies the situation." - - The answer steadies nothing; that is not a contradiction. - - -> viktor_restraint_advice - -=== viktor_restraint_advice === - -"You will be addressed according to the station you present," he continues. "The Graf's household will observe rank. Servants will observe what the household observes. Villagers may observe less and remember more. I advise restraint." - -The advice is sound. That makes it no less irritating. - -* ["If gentlemen were less easily led, Herr Nowak, ladies would require fewer methods."] - #route:lover - ~ lover += 1 - ~ viktor_relation = "provocation" - ~ viktor_trust -= 1 - ~ viktor_suspicion += 1 - "If gentlemen were less easily led, Herr Nowak, ladies would require fewer methods." - - For the first time, amusement almost reaches his mouth. - - "A dangerous doctrine." - - -> viktor_lover_followup - -* ["If you wish me to pass as harmless, you must stop warning me like a gaoler."] - #route:sapphic - ~ sapphic += 1 - ~ viktor_relation = "tension" - ~ viktor_suspicion += 1 - "If you wish me to pass as harmless, you must stop warning me like a gaoler." - - His gaze sharpens. - - "I am not your gaoler." - - -> viktor_sapphic_followup - -* ["Then let us be exact. What do they know, what do they suspect, and what am I permitted to verify?"] - #route:detective - ~ detective += 1 - ~ viktor_relation = "professional" - ~ viktor_trust += 1 - "Then let us be exact. What do they know, what do they suspect, and what am I permitted to verify?" - - He gives the smallest nod, as if you have chosen the only answer fit for adults. - - "They know that you come recommended. They suspect that you may be able to settle disturbances without police, priest, or press. You are permitted to verify fraud, coercion, threat to public order, or credible phenomena not presently classifiable." - - -> viktor_detective_followup - -* ["I shall do my best not to faint unless it is useful."] - #route:careless - ~ careless += 1 - ~ viktor_relation = "dependence" - ~ viktor_trust -= 1 - "I shall do my best not to faint unless it is useful." - - Something in his expression tightens; not contempt exactly, but readiness. - - "I would prefer you did not faint at all." - - -> viktor_careless_followup - -* ["Restraint is what timid people call obedience after they have forgotten who trained them."] - #route:eccentric - ~ eccentric += 1 - ~ viktor_relation = "challenge" - ~ viktor_suspicion += 2 - "Restraint is what timid people call obedience after they have forgotten who trained them." - - Viktor studies you as he might study an unfamiliar weapon found in luggage. - - "You enjoy making enemies." - - -> viktor_eccentric_followup - -=== viktor_lover_followup === - -* ["A practical one."] - "A practical one." - - "You intend to practice it at Hohenreith?" - - -> viktor_lover_second - -* ["Dangerous doctrines travel best in good gloves."] - #route:eccentric - ~ eccentric += 1 - "Dangerous doctrines travel best in good gloves." - - "You intend to charm Hohenreith into confession?" - - If Hohenreith insists on being charmed, it will hardly be your fault. - - -> viktor_explains_orders - -=== viktor_lover_second === - -* ["Only where patriotism requires sacrifice."] - #route:lover - ~ lover += 1 - "Only where patriotism requires sacrifice." - - He looks down at the memorandum, but not quickly enough to conceal that he is reassessing you. - - -> viktor_explains_orders - -* ["Only where men mistake desire for judgement."] - #route:lover - ~ lover += 1 - "Only where men mistake desire for judgement." - - "That may include more territory than the maps admit." - - -> viktor_explains_orders - -=== viktor_sapphic_followup === - -* ["No. A gaoler is at least honest about the key."] - "No. A gaoler is at least honest about the key." - - The words surprise you by leaving a mark. Not on him, perhaps. On yourself. The closer the train carries you to Amalia's world, though you do not yet know her face, the more intolerable it seems that every female life there might be guarded by men who call the guarding concern. - - Viktor folds the memorandum once, precisely. - - -> viktor_explains_orders - -* ["Then do not stand between me and every locked door before I have touched the handle."] - ~ viktor_trust += 1 - "Then do not stand between me and every locked door before I have touched the handle." - - "Some doors are locked for cause." - - Any cause worthy of the lock should survive the indignity of being examined. - - Viktor folds the memorandum once, precisely. - - -> viktor_explains_orders - -=== viktor_detective_followup === - -* ["Credible phenomena not presently classifiable."] - "Credible phenomena not presently classifiable." - - "That is the phrase." - - The phrase files itself in your mind as a bureaucratic ghost. - - "The safest kind," he says. - - -> viktor_explains_orders - -* ["And if the phenomena become classifiable?"] - "And if the phenomena become classifiable?" - - "Then we classify them before others do." - - The sentence has the chill of a report written quickly over a grave. - - -> viktor_explains_orders - -=== viktor_careless_followup === - -* ["How ungallant."] - "How ungallant." - - "How practical." - - The burden of practicality passes toward him as gracefully as a fainting couch dragged into a field hospital. - - His answer is delayed by half a breath. - - "That, gnädiges Fräulein, is precisely what concerns me." - - -> viktor_explains_orders - -* ["Then you must remain close enough to catch me."] - #route:lover - ~ lover += 1 - "Then you must remain close enough to catch me." - - "My orders did not specify theatrical collapses." - - The omission does not flatter the thoroughness of his superiors. - - -> viktor_explains_orders - -=== viktor_eccentric_followup === - -* ["No. I dislike the laziness of letting fools remain undecided."] - "No. I dislike the laziness of letting fools remain undecided." - - "At Hohenreith, that dislike may become expensive." - - If the Graf wanted docility, he could have invited someone cheaper. - - -> viktor_explains_orders - -* ["Enemies are merely people honest enough to stand in the right place."] - "Enemies are merely people honest enough to stand in the right place." - - "You speak as though conflict were housekeeping." - - Conflict has always been a form of housekeeping; one discovers what belongs where. - - -> viktor_explains_orders - -=== viktor_explains_orders === - -The wheels strike a curve. The compartment leans. For a moment the two of you are held in the same narrow imbalance. - -Viktor gives you the memorandum at last. - -The document is not long. That is part of its menace. Long documents invite argument; short ones carry authority. - -A comital household. A hunting residence in Upper Styria, not the family's principal seat. Reports of disturbances among servants and villagers. No police action requested. No public ecclesiastical inquiry desired. No press. No correspondence beyond approved channels. Your presence to be explained as a discreet consultation requested by the family. Herr Nowak to assist in practical matters. - -No one has written the word ghost. - -No one has written the word fraud. - -No one has written the word daughter. - -Yet the omissions arrange themselves around the page like furniture around a corpse. - -* ["There is another instruction."] - "There is another instruction." - - Viktor does not ask how you know. - - "There is always another instruction," he says. - - -> viktor_second_instruction - -* ["Your version is shorter than your silence. That means there is another instruction."] - #route:detective - ~ detective += 1 - ~ viktor_trust += 1 - "Your version is shorter than your silence. That means there is another instruction." - - Viktor does not ask how you know. - - "There is always another instruction," he says. - - -> viktor_second_instruction - -* ["How touching. Vienna trusts us both so little it had to divide the distrust."] - #route:eccentric - ~ eccentric += 1 - ~ viktor_suspicion += 1 - "How touching. Vienna trusts us both so little it had to divide the distrust." - - Viktor does not ask how you know. - - "There is always another instruction," he says. - - -> viktor_second_instruction - -=== viktor_second_instruction === - -* ["For you."] - "For you." - - "Yes." - - -> viktor_instruction_concerns - -* ["Concerning me."] - "Concerning me." - - "Partly." - - -> train_slows - -* ["Concerning whether I am fraud, fool, or useful animal."] - ~ viktor_suspicion += 1 - "Concerning whether I am fraud, fool, or useful animal." - - "Partly," he says, and this time the honesty has a blade in it. - - -> train_slows - -=== viktor_instruction_concerns === - -* ["Concerning me."] - "Concerning me." - - "Partly." - - -> train_slows - -* ["Concerning whether I am fraud, fool, or useful animal."] - ~ viktor_suspicion += 1 - "Concerning whether I am fraud, fool, or useful animal." - - "Partly," he says, and this time the honesty has a blade in it. - - -> train_slows - -=== train_slows === - -The train begins to slow. The rhythm changes first in the floor, then in the window, then in the body. Houses gather beside the line. A station roof appears between drifting smoke and the dark combs of forested slopes. #sfx[steam-whistle.ogg] - -* ["Then I shall try to be worth the ink."] - "Then I shall try to be worth the ink." - - "I sincerely hope so." - - -> train_slows_end - -* ["Then I shall disappoint the instruction as creatively as circumstances permit."] - #route:eccentric - ~ eccentric += 1 - "Then I shall disappoint the instruction as creatively as circumstances permit." - - "I sincerely hope you do not." - - -> train_slows_end - -* ["Then keep your second instruction, Herr Nowak. I prefer primary sources."] - #route:detective - ~ detective += 1 - "Then keep your second instruction, Herr Nowak. I prefer primary sources." - - "A preference not always granted in imperial service." - - -> train_slows_end - -=== train_slows_end === - -You cannot decide whether his answer is an insult, a prayer, or his first honest sentence. - --> railway_station - -=== railway_station === - -The station is small enough that the train seems briefly embarrassed to stop there. #chapter[The Station] #image[muerzzuschlag.png](portrait) - -A porter in a cap too large for him hurries along the platform. A woman with a basket steps back from the steam as if from an animal. Somewhere beyond the station building, a cart horse stamps at frozen mud. The signboard gives the place a name you have seen in the timetable but will not remember with affection. - -Your luggage descends in stages. - -* [A disciplined official set: trunk, dispatch case, hatbox, and black séance case.] - ~ baggage_style = "official" - ~ detective += 1 - First comes a sober travelling trunk with brass corners dulled by use, then a dispatch case, then a hatbox, then the narrow black case whose contents would embarrass both a priest and a conjurer if either searched it without imagination. - -> station_luggage_common - -* [An elegant noblewoman's luggage: too correct to be accidental.] - ~ baggage_style = "elegant" - ~ class_confidence += 1 - First comes a large trunk in dark leather, then a second smaller one for linen, then a round hatbox, a fitted toilette case, and a reticule kept too close to your hand for any porter to misunderstand its importance. - -> station_luggage_common - -* [A performer's luggage: harmless on top, less harmless beneath.] - ~ baggage_style = "performer" - ~ medium_reputation += 1 - First comes a respectable trunk, then a hatbox, then a travelling case of gloves, veils, ribbons, calling cards, and the small objects by which a room may be persuaded to believe in forces already present. - -> station_luggage_common - -* [A practical assortment that betrays too much preparation.] - ~ baggage_style = "practical" - ~ detective += 1 - First comes a battered trunk reinforced at the corners, then a leather case with notebooks, pencils, folded maps, spare gloves, a hand-lamp, and enough small necessities to offend anyone who prefers women decorative. - -> station_luggage_common - -* [An excessive pile that makes concealment impossible.] - ~ baggage_style = "excessive" - ~ careless += 1 - First comes one trunk, then another, then a hatbox, then a rug, then a dressing case, then the narrow black case, then a smaller parcel you had forgotten had survived packing. By the end even Viktor looks faintly outnumbered. - -> station_luggage_common - -=== station_luggage_common === - -Viktor oversees the transfer with clipped civility. He does not carry like a servant. He directs like a man pretending not to command. - -The coach from Hohenreith waits beyond the station yard: dark green paint, black wheels, the comital crest discreetly worn on the door, and two horses already restless beneath harness. The driver removes his hat when he sees you. Not too deeply. Deep enough for rank, not deep enough for reverence. #sfx[horse-neigh.ogg] - -"Gnädiges Fräulein? Herr Sekretär?" - -{birth_class == "noble": - He has been told enough to place you. That is a courtesy. It is also a warning. -- else: - He hesitates over you by the smallest measure. The hesitation is not rudeness. It is calculation. First-class carriage, court letter, no title beyond Fräulein, and a man beside you who looks like he has arrested people for less than staring. -} - -Viktor answers before you can. - -"From Jagdhaus Hohenreith?" - -"Jawohl, Herr Sekretär. The road is passable. If the mist holds, we should reach Eibenreith before dark." - -The word enters the air without ceremony. - -Eibenreith. - -Not Hohenreith, the name printed on the invitation in a clean hand. Eibenreith: the village below it. A smaller name. Older in the mouth. A name with roots rather than stationery. - --> coach_journey - -=== coach_journey === - -The coach leaves the station behind and with it the last easy evidence of empire. #chapter[The Graben] #music[Kaiserpunk Jodler.mp3](crossfade, loop, lead=4) - -At first the road follows a valley where telegraph wire still keeps company with it and the river moves in a pale, stony bed. Sawmills, fenced meadows, and farmhouses appear and vanish behind stands of spruce. The mountains do not rise all at once. They advance by jurisdiction. A wooded slope claims the left-hand sky, then a grey wall of limestone closes the north, then another ridge gathers to the east until even the clouds seem to have entered service. - -The driver names places when Viktor asks, but the names are local and practical, meant for men who know which bridge floods and which farm breeds stubborn horses. Somewhere beyond the visible ridges, he says, lies the great white back of the Hochschwab. Eastward, beyond forest and pass, the Hohe Veitsch keeps its own weather. He says this not as a guide would say it, but as a man explaining neighbours who may or may not be in a temper. - -The main valley narrows. - -The road turns from it into a side Graben, and the change is immediate. Sound alters. The wheels no longer ring against open distance but grind between banks, roots, and wet stone. The air smells of leaf mould, resin, and cold water. Yews appear among the firs in dark, improbable patience, their needles too black for the afternoon. - -"Eibenreither Graben," the driver says, and crosses himself so quickly that the gesture might have been meant for a rut in the road. - -Viktor notices. Of course he notices. - -"Bad road?" he asks. - -"Old road," the driver says. - -No one speaks for a while. - -You watch the trees. - -There are forests that invite stories because they are pretty, and forests that reject stories because whatever happened there did not require witnesses. This one belongs to the second kind. Its trunks stand close, not wildly, but with the air of a crowd making room for something carried through it long ago. The snow that remains in hollows is not clean. It has gathered needles, bark, and a yellowish stain where water has risen underneath. - -On a slope above the road, half swallowed by undergrowth, you glimpse stone. - -A shrine, perhaps. A boundary marker. A figure. The coach has passed before your eyes can persuade themselves of its shape. For one instant you are left with the impression of a woman's head inclined not in prayer, but in listening. #image[statue.png](square) - -{supernatural_senses == "genuine" or supernatural_senses == "ambiguous" or supernatural_senses == "repressed": - The back of your neck tightens. - - Not fear. Recognition would be worse. - - ~ supernatural_exposure += 1 -- else: - You tell yourself that old stone, seen through moving branches, will become whatever the mind is cowardly enough to supply. -} - -Viktor has turned slightly toward the same slope. - -"Did you see something?" - -* ["A woman in the wood, perhaps. Or a stone that wanted to be one."] - #route:eccentric - #statue_hint - ~ eccentric += 1 - ~ viktor_suspicion += 1 - "A woman in the wood, perhaps. Or a stone that wanted to be one." - - He studies the passing trees. - - "A local shrine?" - - -> statue_eccentric_followup - -* ["A marker. I would like to know where that path leads."] - #route:detective - #statue_hint - ~ detective += 1 - ~ viktor_trust += 1 - "A marker. I would like to know where that path leads." - - "You saw a path?" - - -> statue_detective_followup - -* ["Only trees. The sort that make one grateful for gentlemen with revolvers."] - #route:careless - ~ careless += 1 - ~ viktor_relation = "dependence" - "Only trees. The sort that make one grateful for gentlemen with revolvers." - - His expression darkens by one official degree. - - "A revolver is a poor instrument against trees." - - -> statue_careless_followup - -* ["Would you believe me if I said I had?"] - #route:lover - ~ lover += 1 - ~ viktor_suspicion += 1 - "Would you believe me if I said I had?" - - "That would depend on what advantage you expected from the answer." - - -> statue_lover_followup - -* ["No."] - #route:sapphic - ~ sapphic += 1 - "No." - - The denial is too quick, and you both hear it. - - You are not thinking of the stone now. You are thinking of the young woman waiting somewhere ahead: the Graf's daughter, the reason carefully not written into the memorandum, the stranger whose household has summoned you under a title both absurd and useful. - - -> statue_sapphic_followup - -=== statue_eccentric_followup === - -* ["If it is a shrine, it has not been loved recently."] - "If it is a shrine, it has not been loved recently." - - "You speak as if stones notice neglect." - - Soldiers, too, notice neglect. His silence admits enough. - - He does not answer. - - -> coach_nears_village - -* ["No. Shrines face the faithful. That thing was listening sideways."] - ~ supernatural_exposure += 1 - "No. Shrines face the faithful. That thing was listening sideways." - - Viktor's hand rests on the coach strap, still and ready. - - -> coach_nears_village - -=== statue_detective_followup === - -* ["Not clearly. Enough to ask later."] - "Not clearly. Enough to ask later." - - Viktor looks back through the small rear window. The bend has already erased the slope. - - "Ask carefully. Places people fail to mention are often more informative than those they recommend." - - -> coach_nears_village - -* ["Only a suggestion of one. If it exists, someone maintains the absence of it."] - #route:detective - ~ detective += 1 - "Only a suggestion of one. If it exists, someone maintains the absence of it." - - "You make absences sound expensive." - - They usually are; absence is expensive when someone maintains it. - - -> coach_nears_village - -=== statue_careless_followup === - -* ["Then I shall rely on your conversation to intimidate them."] - "Then I shall rely on your conversation to intimidate them." - - The driver pretends not to hear. His shoulders, however, hear everything. - - -> coach_nears_village - -* ["How unfortunate. You seemed so professionally reassuring."] - #route:lover - ~ lover += 1 - "How unfortunate. You seemed so professionally reassuring." - - "I prefer enemies that identify themselves." - - -> coach_nears_village - -=== statue_lover_followup === - -* ["Herr Nowak. You wound me."] - "Herr Nowak. You wound me." - - "Not yet." - - It is the first thing he has said all day that almost sounds like flirtation, though perhaps only because danger has a talent for borrowing warmer clothes. - - -> coach_nears_village - -* ["Then watch the slope, not my intentions. One of them may be useful."] - ~ viktor_trust += 1 - "Then watch the slope, not my intentions. One of them may be useful." - - He obeys without admitting that he has done so. - - -> coach_nears_village - -=== statue_sapphic_followup === - -* ["It was only shadow."] - "It was only shadow." - - If this place keeps women in stone, you think, what does it do to them in houses? - - -> coach_nears_village - -* ["Or if I did, I prefer not to have it explained before I understand why it matters."] - #route:detective - ~ detective += 1 - "Or if I did, I prefer not to have it explained before I understand why it matters." - - If this place keeps women in stone, you think, what does it do to them in houses? - - -> coach_nears_village - -=== coach_nears_village === - -The Graben opens reluctantly. - -First comes the smell of smoke. Then a roof, low and dark with weather. Then another. Then a church tower, not high, not graceful, but thick-shouldered and pale against the slope behind it. Its walls look older than the village around them and less certain of victory. The windows are small. The churchyard wall holds the road at a distance, as if the dead require fortification from the living, or the living from something else. #chapter[Eibenreith Village] #sfx[church-bells.ogg](max=8, fade) #image[eibenreith.png](landscape) - -Eibenreith appears not as a village in a picture appears, all at once and composed for admiration, but by fragments. - -A woman in a dark kerchief pauses with a pail in her hand. A boy stops driving geese and lets them complain around his boots. Two men outside a shed end their conversation at the same moment without looking at each other. Curtains stir in windows where no one admits to standing. A blacksmith's sign moves slightly in air you cannot feel. Water runs somewhere under boards, under stone, under the road itself, quick and cold and hidden. - -The houses are not poor, not exactly. Many are solid, whitewashed, shingled, kept with the stubborn decency of people who repair what they cannot replace. Yet something in their arrangement troubles the eye. They turn toward the church but not fully. They keep the road but lean from it. They leave, between yard and fence and woodpile, narrow passages where shadow gathers too early. - -The coach slows. - -No one runs to greet it. - -No one needs to. News has already entered the village by means faster than railway, telegraph, or imperial seal. - -You sit very straight as Eibenreith takes its first look at you. - -Beside you, Viktor lowers his voice. - -"Remember: at Hohenreith, every courtesy will mean something. Here, every silence will." - -* ["Then we are already being received."] - #route:detective - ~ detective += 1 - "Then we are already being received." - - "Yes," he says. "And examined." - - -> village_final_image - -* ["You make it sound as if the village outranks the Graf."] - #route:eccentric - ~ eccentric += 1 - "You make it sound as if the village outranks the Graf." - - "No," Viktor says. "Only as if it may have survived more than one." - - -> village_final_image - -* ["How fortunate that I packed several silences."] - #route:lover - ~ lover += 1 - "How fortunate that I packed several silences." - - His mouth almost moves. "Use the plainest one first." - - -> village_final_image - -* ["I dislike being watched by people who will not introduce themselves."] - #route:careless - ~ careless += 1 - "I dislike being watched by people who will not introduce themselves." - - "That," he says, "is unlikely to improve today." - - -> village_final_image - -* ["If Amalia has lived under this gaze all her life, I begin to understand why they sent for ghosts."] - #route:sapphic - ~ sapphic += 1 - "If Amalia has lived under this gaze all her life, I begin to understand why they sent for ghosts." - - Viktor glances at you, but whatever answer he considers, he keeps it behind his teeth. - - -> village_final_image - -=== village_final_image === - -The horses draw the coach past the churchyard wall. Above it, on the old plaster beside the gate, a faded painted woman looks down from beneath a flaking blue mantle. Her hands are folded in prayer. Her eyes, damaged by weather, no longer point in the same direction. - -For one breath, as the wheels pass over a buried runnel of water, the painted face seems less like the Holy Mother than like a mask put on something that had been waiting longer. - -Then the coach enters the village proper, and the road bends toward the unseen height where Jagdhaus Hohenreith stands above Eibenreith under its newer name - --> DONE \ No newline at end of file diff --git a/data/ink-src/story.ink b/data/ink-src/story.ink deleted file mode 100644 index 6d18457..0000000 --- a/data/ink-src/story.ink +++ /dev/null @@ -1,1686 +0,0 @@ -// Character variables. We track just two, using a +/- scale -VAR forceful = 0 -VAR evasive = 0 - - -// Inventory Items -VAR teacup = false -VAR gotcomponent = false - - -// Story states: these can be done using read counts of knots; or functions that collect up more complex logic; or variables -VAR drugged = false -VAR hooper_mentioned = false - -VAR losttemper = false -VAR admitblackmail = false - -// what kind of clue did we pass to Hooper? -CONST NONE = 0 -CONST STRAIGHT = 1 -CONST CHESS = 2 -CONST CROSSWORD = 3 -VAR hooperClueType = NONE - -VAR hooperConfessed = false - -CONST SHOE = 1 -CONST BUCKET = 2 -VAR smashingWindowItem = NONE - -VAR notraitor = false -VAR revealedhooperasculprit = false -VAR smashedglass = false -VAR muddyshoes = false - -VAR framedhooper = false - -// What did you do with the component? -VAR putcomponentintent = false -VAR throwncomponentaway = false -VAR piecereturned = false -VAR longgrasshooperframe = false - - -// DEBUG mode adds a few shortcuts - remember to set to false in release! -VAR DEBUG = false -{DEBUG: - IN DEBUG MODE! - * [Beginning...] -> start - * [Framing Hooper...] -> claim_hooper_took_component - * [In with Hooper...] -> inside_hoopers_hut -- else: - // First diversion: where do we begin? - -> start -} - - /*-------------------------------------------------------------------------------- - Wrap up character movement using functions, in case we want to develop this logic in future ---------------------------------------------------------------------------------*/ - - - === function lower(ref x) - ~ x = x - 1 - - === function raise(ref x) - ~ x = x + 1 - -/*-------------------------------------------------------------------------------- - - Start the story! - ---------------------------------------------------------------------------------*/ - -=== start === - -// Intro - - They are keeping me waiting. - * Hut 14[]. The door was locked after I sat down. - I don't even have a pen to do any work. There's a copy of the morning's intercept in my pocket, but staring at the jumbled letters will only drive me mad. - I am not a machine, whatever they say about me. - - - (opts) - {|I rattle my fingers on the field table.|} - * (think) [Think] - They suspect me to be a traitor. They think I stole the component from the calculating machine. They will be searching my bunk and cases. - When they don't find it, {plan:then} they'll come back and demand I talk. - -> opts - * (plan) [Plan] - {not think:What I am is|I am} a problem—solver. Good with figures, quick with crosswords, excellent at chess. - But in this scenario — in this trap — what is the winning play? - * * (cooperate) [Co—operate] - I must co—operate. My credibility is my main asset. To contradict myself, or another source, would be fatal. - I must simply hope they do not ask the questions I do not want to answer. - ~ lower(forceful) - * * [Dissemble] - Misinformation, then. Just as the war in Europe is one of plans and interceptions, not planes and bombs. - My best hope is a story they prefer to the truth. - ~ raise(forceful) - * * (delay) [Divert] - Avoidance and delay. The military machine never fights on a single front. If I move slowly enough, things will resolve themselves some other way, my reputation intact. - ~ raise(evasive) - * [Wait] - - -> waited - -= waited - - Half an hour goes by before Commander Harris returns. He closes the door behind him quickly, as though afraid a loose word might slip inside. - "Well, then," he begins, awkwardly. This is an unseemly situation. - * "Commander." - He nods. <> - * (tellme) {not start.delay} "Tell me what this is about." - He shakes his head. - "Now, don't let's pretend." - * [Wait] - I say nothing. - - He has brought two cups of tea in metal mugs: he sets them down on the tabletop between us. - * {tellme} [Deny] "I'm not pretending anything." - {cooperate:I'm lying already, despite my good intentions.} - Harris looks disapproving. -> pushes_cup - * (took) [Take one] - ~ teacup = true - I take a mug and warm my hands. It's <> - * (what2) {not tellme} "What's going on?" - "You know already." - -> pushes_cup - * [Wait] - I wait for him to speak. - - - (pushes_cup) He pushes one mug halfway towards me: <> - - a small gesture of friendship. - Enough to give me hope? - * (lift_up_cup) {not teacup} [Take it] - I {took:lift the mug|take the mug,} and blow away the steam. It is too hot to drink. - Harris picks his own up and just holds it. - ~ teacup = true - ~ lower(forceful) - * {not teacup} [Don't take it] - Just a cup of insipid canteen tea. I leave it where it is. - ~ raise(forceful) - - * {teacup} [Drink] - I raise the cup to my mouth but it's too hot to drink. - - * {teacup} [Wait] - I say nothing as -> lift_up_cup - -- "Quite a difficult situation," {lift_up_cup:he|Harris} begins{forceful <= 0:, sternly}. I've seen him adopt this stiff tone of voice before, but only when talking to the brass. "I'm sure you agree." - * [Agree] - "Awkward," I reply - * (disagree) [Disagree] - "I don't see why," I reply - ~ raise(forceful) - ~ raise(evasive) - * [Lie] -> disagree - * [Evade] - "I'm sure you've handled worse," I reply casually - ~ raise(evasive) - - { teacup: - ~ drugged = true - <>, sipping at my tea as though we were old friends - } - <>. - - - - * [Watch him] - His face is telling me nothing. I've seen Harris broad and full of laughter. Today he is tight, as much part of the military machine as the device in Hut 5. - - * [Wait] - I wait to see how he'll respond. - - * {not disagree} [Smile] - I try a weak smile. It is not returned. - ~ lower(forceful) - -// Why you're here - - - "We need that component," he says. - - - //"There's no alternative, of course," he continues. - {not missing_reel: - -> missing_reel -> harris_demands_component - } - - - * [Yes] - "Of course I do," I answer. - * (no) [No] - "No I don't. And I've got work to do..." - "Work that will be rather difficult for you to do, don't you think?" Harris interrupts. - - * [Evade] - -> here_at_bletchley_diversion - * [Lie] - -> no - - -> missing_reel -> harris_demands_component - -=== missing_reel === - * [The stolen component...] - * [Shrug] - I shrug. - ->-> - - The reel went missing from the Bombe this afternoon. The four of us were in the Hut, working on the latest German intercept. The results were garbage. It was Russell who found the gap in the plugboard. - - Any of us could have taken it; and no one else would have known its worth. - - * {forceful <= 0 }[Panic] They will pin it on me. They need a scapegoat so that the work can continue. I'm a likely target. Weaker than the rest. - ~ lower(forceful) - * [Calculate] My odds, then, are one in four. Not bad; although the stakes themselves are higher than I would like. - ~ raise(evasive) - * {evasive >= 0} [Deny] But this is still a mere formality. The work will not stop. A replacement component will be made and we will all be put back to work. We are too valuable to shoot. - ~ raise(forceful) - - ->-> - - -=== here_at_bletchley_diversion - "Here at Bletchley? Of course." - ~ raise(evasive) - ~ lower(forceful) - "Here, now," Harris corrects. "We are not talking to everyone. I can imagine you might feel pretty sore about that. I can imagine you feeling picked on. { forceful < 0:You're a sensitive soul.}" - - * (fine) "I'm fine[."]," I reply. "This is all some misunderstanding and the quicker we have it cleared up the better." - ~ lower(forceful) - "I couldn't agree more." And then he comes right out with it, with an accusation. - - * {forceful < 0} "What do you mean by that?" - - * (sore) { forceful >= 0 } "Damn right[."] I'm sore. Was it one of the others who put you up to this? Was it Hooper? He's always been jealous of me. He's..." - ~ raise(forceful) - ~ hooper_mentioned = true - The Commander moustache bristles as he purses his lips. "Has he now? Of your achievements, do you think?" - It's difficult not to shake the sense that he's { evasive > 1 :mocking|simply humouring} me. - "Or of your brain? Or something else?" - * * "Of my genius.["] Hooper simply can't stand that I'm cleverer than he is. We work so closely together, cooped up in that Hut all day. It drives him to distraction. To worse." - "You're suggesting Hooper would sabotage this country's future simply to spite you?" Harris chooses his words like the military man he is, each lining up to create a ring around me. - * * * [Yes] - "{ forceful > 0:He's petty enough, certainly|I wouldn't put it past him}. He's a creep." { teacup : I set the teacup down.|I wipe a hand across my forehead.} - ~ raise(forceful) - ~ teacup = false - * * * [No] - "No, { forceful >0:of course not|I suppose not}." { teacup :I put the teacup back down on the table|I push the teacup around on its base}. - ~ lower(forceful) - ~ teacup = false - * * * [Evade] - "I don't know what I'm suggesting. I don't understand what's going on." - ~ raise(evasive) - "But of course you do." Harris narrows his eyes. - -> done - - - - - (suggest_its_a_lie) "All I can say is, ever since I arrived here, he's been looking to ways to bring me down a peg. I wouldn't be surprised if he set this whole affair up just to have me court—martialled." - "We don't court—martial civilians," Harris replies. "Traitors are simply hung at her Majesty's pleasure." - * * * "Quite right[."]," I answer smartly. - * * * (iamnotraitor) "I'm no traitor[."]," I answer{forceful > 0 :smartly|, voice quivering. "For God's sake!"} - * * * [Lie] -> iamnotraitor - - - - He stares back at me. - - * * "Of my standing.["] My reputation." { forceful > 0:I'm aware of how arrogant I must sound but I plough on all the same.|I don't like to talk of myself like this, but I carry on all the same.} "Hooper simply can't bear knowing that, once all this is over, I'll be the one receiving the knighthood and he..." - "No—one will be getting a knighthood if the Germans make landfall," Harris answers sharply. He casts a quick eye to the door of the Hut to check the latch is still down, then continues in more of a murmur: "Not you and not Hooper. Now answer me." - For the first time since the door closed, I wonder what the threat might be if I do not. - - * * [Evade] - ~ teacup = false - ~ raise(forceful) - "How should I know?" I reply, defensively. { teacup :I set the teacup back on the table.} -> suggest_its_a_lie - - - * [Be honest] -> sore - * [Lie] -> fine - -- (done) -> harris_demands_component - - -=== harris_demands_component === - "{here_at_bletchley_diversion:Please|So}. Do you have it?" Harris is {forceful > 3:sweating slightly|wasting no time}: Bletchley is his watch. "Do you know where it is?" - * [Yes] - "I do." - -> admitted_to_something - * (nope) [No] "I have no idea." - -> silence - * [Lie] -> nope - * [Evade] - "The component?" - ~ raise(evasive) - ~ lower(forceful) - "Don't play stupid," he replies. "{ not missing_reel:The component that went missing this afternoon. }Where is it?" - - - { not missing_reel: - -> missing_reel -> - } - * [Co-operate] "I know where it is." - -> admitted_to_something - * (nothing) [Delay] "I know nothing about it." My voice shakes{ forceful > 0: with anger|; I'm unaccustomed to facing off against men with holstered guns}. - - * [Lie] -> nothing - * [Evade] - - "I don't know what gives you the right to pick on me. { forceful > 0:I demand a lawyer.|I want a lawyer.}" - - "This is time of war," Harris answers. "And by God, if I have to shoot you to recover the component, I will. Understand?" He points at the mug,-> drinkit - - - (silence) There's an icy silence. { forceful > 2:I've cracked him a little.|{ evasive > 2:He's tiring of my evasiveness.}} - - // Drink tea and talk - - (drinkit) "Now drink your tea and talk." - * { teacup } [Drink] -> drinkfromcup - * { teacup } [Put the cup down] - I set the cup carefully down on the table once more. - ~ teacup = false - ~ raise(forceful) - -> whatsinit - - * { not teacup } [Take the cup] - - - (drinkfromcup) I lift the cup { teacup :to my lips }and sip. He waits for me to swallow before speaking again. - ~ drugged = true - ~ teacup = true - * { not teacup } [Don't take it] - I leave the cup where it is. - ~ raise(forceful) - - - (whatsinit) "Why?" I ask coldly. "What's in it?" - - - "Lapsang Souchong," he {drinkfromcup:remarks|replies}, placing his own cup back on the table untouched. "Such a curious flavour. It might almost not be tea at all. You might say it hides a multitude of sins. As do you. Isn't that right?" - - * (suppose_i_have) [Agree] - // Regrets - "I suppose so," I reply. "I've done things I shouldn't have done." - ~ lower(forceful) - -> harris_presses_for_details - - * (nothing_ashamed_of) { not drugged } [Disagree] - "I've done nothing that I'm ashamed of." - -> harris_asks_for_theory - - * (cant_talk_right) { drugged } [Disagree] - I open my mouth to disagree, but the words I want won't come. It is like Harris has taken a screwdriver to the sides of my jaw. - -> admitted_to_something.ive_done_things - - * {drugged} [Lie] -> cant_talk_right - * {not drugged} [Lie] -> nothing_ashamed_of - * { drugged } [Evade] -> cant_talk_right - - * { not drugged } [Evade] - "None of us are blameless, Harris. { forceful > 1:But you're not my priest and I'm not yours|But I've done nothing to deserve this treatment}. Now, please. Let me go. I'll help you find this damn component, of course I will." - // Who do you blame? - He appears to consider the offer. - -> harris_asks_for_theory - - - -=== harris_presses_for_details -// Open to Blackmail - "You mean you've left yourself open," Harris answers. "To pressure. Is that what you're saying?" - * [Yes] -> admit_open_to_pressure - * { not drugged } [No] - "I'm not saying anything of the sort," I snap back. "What is this, Harris? You're accusing me of treachery but I don't see a shred of evidence for it! Why don't you put your cards on the table?" - ~ raise(forceful) - - - * {drugged} [No] - I shake my head violently, to say no, that's not it, but whatever is wrong with tongue is wrong with neck too. I look across at the table at Harris' face and realise with a start how sympathetic he is. Such a kind, generous man. How can I hold anything back from him? - ~ lower(forceful) - I take another mouthful of the bitter, strange—tasting tea before answering. - -> admit_open_to_pressure - - - * { not drugged } [Evade] - "You're the one applying pressure here," I answer { forceful > 1:smartly|somewhat miserably}. "I'm just waiting until you tell me what is really going on." - ~ raise(evasive) - * { drugged } [Evade] - "We're all under pressure here." - He looks at me with pity. -> harris_has_seen_it_before - - - "It's simple enough," Harris says. -> harris_has_seen_it_before - -= admit_open_to_pressure - "That's it," I reply. "There are some things... which a man shouldn't do." - ~ admitblackmail = true - Harris doesn't stiffen. Doesn't lean away, as though my condition might be infectious. I had thought they trained them in the army to shoot my kind on sight. - He offers no sympathy either. He nods, once. His understanding of me is a mere turning cog in his calculations, with no meaning to it. - -> harris_has_seen_it_before - - -=== admitted_to_something - // Admitting Something - { not drugged : - Harris stares back at me. { evasive == 0:He cannot have expected it to be so easy to break me.} - - else: - Harris smiles with satisfaction, as if your willingness to talk was somehow his doing. - } - "I see." - There's a long pause, like the delay between feeding a line of cypher into the Bombe and waiting for its valves to warm up enough to begin processing. - "You want to explain that?" - * [Explain] - I pause a moment, trying to choose my words. To just come out and say it, after a lifetime of hiding... that is a circle I cannot square. - * * [Explain] -> ive_done_things - * * {drugged} [Say nothing] -> say_nothing - * * {not drugged} [Lie] -> claim_hooper_took_component - - * { not drugged } [Don't explain] - "There's nothing to explain," I reply stiffly. -> i_know_where - - * { not drugged } [Lie] -> claim_hooper_took_component - * { not drugged } [Evade] - "Explain what you should be doing, do you mean, rather than bullying me? Certainly." I fold my arms. -> i_know_where - - * (say_nothing) { drugged } [Say nothing] - I fold my arms, intended firmly to say nothing. But somehow, watching Harris' face, I cannot bring myself to do it. I want to confess. I want to tell him everything I can, to explain myself to him, to earn his forgiveness. The sensation is so strong my will is powerless in the face of it. - Something is wrong with me, I am sure of it. There is a strange, bitter flavour on my tongue. I taste it as words start to form. - -> ive_done_things - -= i_know_where - "I know where your component is because it's obvious where your component is. That doesn't mean I took it, just because I can figure out a simple problem, any more than it means I'm a German spy because I can crack their codes." - -> harris_asks_for_theory - - -= ive_done_things - "I've done things," I begin{harris_demands_component.cant_talk_right: helplessly}. "Things I didn't want to do. I tried not to. But in the end, it felt like cutting off my own arm to resist." - -> harris_presses_for_details - - - - -=== harris_asks_for_theory -"Tell me, then," he asks. "What's your theory? You're a smart fellow — as smart as they come around here, and that's saying something. What's your opinion on the missing component? Accident, perhaps? Or do you blame one of the other men? { hooper_mentioned :Hooper?}" - * [Blame no—one] - -> an_accident - * [Blame someone] -> claim_hooper_took_component - -= an_accident - "An accident, naturally." I risk a smile. "That damned machine is made from spare parts and string. Even these Huts leak when it rains. It wouldn't take more than one fellow to trip over a cable to shake out a component. Have you tried looking under the thing?" - "Do you believe we haven't?" - In a sudden moment I understand that his reply is a threat. - "Now," he continues. "Are you sure there isn't anything you want to tell me?" - - * [Co-operate] - "All right." With a sigh, your defiance collapses. "If you're searched my things then I suppose you've found { evasive > 1: what you need|my letters. Haven't you? In fact, if you haven't, don't tell me}. - ~ admitblackmail = true - Harris nods once. - <> -> harris_has_seen_it_before - - * {evasive > 0} [Evade] "Only that you're being unreasonable, and behaving like a swine." - // Loses temper - "You imbecile," Harris replies, with sudden force. He is half out of his chair. "You know the situation as well as I do. Why the fencing? The Hun are poised like rats, ready to run all over this country. They'll destroy everything. You understand that, don't you? You're not so locked up inside your crossword puzzles that you don't see that, are you? This machine we have here — you men — you are the best and only hope this country has. God help her." - ~ losttemper = true - I sit back, startled by the force of his outburst. His carefully sculpted expression has curled to angry disgust. He really does hate me, I think. He'll have my blood for the taste of it. - * * [Placate] - "Now steady on," I reply, gesturing for him to be calm. - - * * [Mock] - "I can imagine how being surrounded by clever men is pretty threatening for you, Commander," I reply with a sneer. "They don't train you to think in the Armed Forces." - ~ raise(forceful) - - * * [Dismiss] - "Then I'll be going, on and getting on with my job of saving her, shall I?" I even rise half to my feet, before he slams the tabletop. - - - - "Talk," Harris demands. "Talk now. Tell me where you've hidden it or who you passed it to. Or God help me, I'll take your wretched pansy body to pieces looking for it." - -> harris_demands_you_speak - - - - -=== harris_has_seen_it_before - "I've seen it before. A young man like you — clever, removed. The kind that doesn't go to parties. Who takes himself too seriously. Who takes things too far." - He slides his thumb between two fingers. - "Now they own you." - - * [Agree] - "What could I do?" I'm shaking now. The night is cold and the heat—lamp in the Hut has been removed. "{ forceful > 2:I won't|I don't want to} go to prison." - "Smart man," he replies. "You wouldn't last. - - * [Disagree] - "I can still fix this." - Harris shakes his head. "You'll do nothing. This is beyond you now. You may go to prison or may go to firing squad - or we can change your name and move you somewhere where your indiscretions can't hurt you. But right now, none of that matters. What happens to you doesn't matter. All that matters is where that component is. - - * { not drugged } [Lie] - "I wanted to tell you," I tell him. "I thought I could find out who they were. Lead you to them." - Harris looks at me with contempt. "You wretch. You'll pay for what you've done to this country today. If a single man loses his life because of your pride and your perversions then God help your soul. - - * {drugged} {forceful < 0} [Apologise] - "Harris, I..." - ~lower(forceful) - "Stop it," he interrupts. "There's no jury here to sway. And there's no time. - -- (tell_me_now) <> So why don't you tell me, right now. Where is it?" - -> harris_demands_you_speak - - - - -=== harris_demands_you_speak - His eyes bear down like carbonised drill—bits. - * [Confess] - { forceful > 1 : - "You want me to tell you what happened? You'll be disgusted." - -else: - "All right. I'll tell you what happened." And never mind my shame. - } - "I can imagine how it starts," he replies. - - * { not drugged } [Dissemble] -> claim_hooper_took_component - * { drugged } [Dissemble] - My plan now is to blame Hooper, but I cannot seem to tell the story. Whatever they put in my tea, it rules my tongue. { forceful >1:I fight it as hard as I can but it does no good.|I am desperate to tell him everything. I am weeping with shame.} - - ~ lower(forceful) -- -> i_met_a_young_man - - - - -=== i_met_a_young_man - // Explain Story - * [Talk] - "There was a young man. I met him in the town. A few months ago now. We got to talking. Not about work. And I used my cover story, but he seemed to know it wasn't true. That got me wondering if he might be one of us." - - Harris is not letting me off any more. - "You seriously entertained that possibility?" - * [Yes] - "Yes, I considered it. <> - * [No] - "No. Not for more than a moment, of course. Everyone here is marked out by how little we would be willing to say about it." - "Only you told this young man more than a little, didn't you?" - I nod. "<> - * [Lie] - "I was quite certain, after a while. After we'd been talking. <> -- He seemed to know all about me. He... he was quite enchanted by my achievements." - The way Harris is staring I expect him to strike me, but he does not. He replies, "I can see how that must have been attractive to you," with such plain—spokeness that I think I must have misheard. - - * [Yes] "It's a lonely life in this place," I reply. "Lonely - and still one never gets a moment to oneself." - "That's how it is in the Service," Harris answers. - * * [Argue] "I'm not in the Service." - Harris shakes his head. "Yes, you are." - * * [Agree] "Perhaps. But I didn't choose this life." - Harris shakes his head. "No. And there's plenty of others who didn't who are suffering far worse." - - - Then he waves the thought aside. - - * (nope) { not drugged } [No] "The boy was a pretty simpleton. Quite inferior. His good opinion meant nothing to be. Harris, do not misunderstand. I was simply after his body." - ~ raise(evasive) - Harris, to his credit, doesn't flinch; but I can see he will have nightmares of this moment later tonight. I'm tempted to reach out and take his hand to worsen it for him. - - * { drugged } [No] - "It wasn't," I reply. "But I doubt you'd understand." - He simply nods. - * { not drugged } [Lie] -> nope - -- "Go on with your confession." -- (paused) - { not nope: - That gives me pause. I hadn't thought of it as such. But I suppose he's right. I am about to admit what I did. - } - "There's not much else to say. I took the part from Bombe computing device. You seem to know that already. I had to. He was going to expose me if I didn't." - // So blackmail? - "This young man was blackmailing you over your affair?" - - ~ temp harris_thinks_youre_drugged = drugged - - { drugged: - ~ drugged = false - As Harris speaks I find myself suddenly sharply aware, as if waking from a long sleep. The table, the corrugated walls of the hut, everything seems suddenly more tangible than a moment before. - Whatever it was they put in my drink is wearing off. - } - - * (yes) [Yes] - "Yes. I suppose he was their agent. I should have realised but I didn't. Then he threatened to tell you. I thought you would have me locked up: I couldn't bear the thought of it. I love working here. I've never been so happy, so successful, anywhere before. I didn't want to lose it." - "So what did you do with the component?" Harris talks urgently. He grips his gloves tightly in one hand, perhaps prepared to lift them and strike if it is required. "Have you passed it to this man already? Have you left it somewhere for him to find?" - * * (still_have) [I have it] - "I still have it. Not on me, of course. -> reveal_location_of_component - - * * (dont_have) [I don't have it] -> i_dont_have_it - * * [Lie] -> dont_have - * * [Tell the truth] -> still_have - - * (notright) [No] - "No, Harris. The young man wasn't blackmailing me." I take a deep breath. "It was Hooper." - { not hooper_mentioned: - "Hooper!" Harris exclaims, in surprise. {harris_thinks_youre_drugged:He does not doubt me for a moment.} - - else: - "Now look here," Harris interrupts. "Don't start that again." - } - "It's the truth, Harris. If I'm going to jail, so be it, but I won't hang at Traitor's Gate. Hooper was the one who told the boy about our work. Hooper put the boy on to me. { forceful < 2:I should have realised, of course. These things don't happen by chance. I was a fool to think they might.} And then, once he had me compromised, he demanded I steal the part from the machine." - ~ revealedhooperasculprit = true - "Which you did." Harris leans forward. "And then what? You still have it? You've stashed it somewhere?" - * * (didnt_have_long) [Yes] - "Yes. I only had a moment. -> reveal_location_of_component - - * * (passed_on) [No] -> passed_onto_hooper - * * [Lie] -> passed_on - * * [Evade] - "I can't remember." - He draws his gun and lays it lightly on the field table. - "I'm sorry to threaten you, friend. But His Majesty needs that brain of yours, and that brain alone. There are plenty of other parts to you that our country could do better without. Now I'll ask you again. Did you hide the component?" - * * * [Yes] -> didnt_have_long - * * * (nope_didnt_hide) [No] - "Very well then." I swallow nervously, to make it look more genuine. -> passed_onto_hooper - * * * [Lie] -> nope_didnt_hide - - * * * [Evade] -> i_dont_have_it - - * [Tell the truth] -> yes - * [Lie] -> notright - -= i_dont_have_it - "I don't have it any more. I passed it through the fence to my contact straight after taking it, before it was discovered to be missing. It would have been idiocy to do differently. It's long gone, I'm afraid." - "You fool, Manning," Harris curses, getting quickly to his feet. "You utter fool. Do you suppose you will be any better off living under Hitler? It's men like you who will get us all killed. Men too feeble, too weak in their hearts to stand up and take a man's responsibility for the world. You're happier to stay a child all your life and play with your little childish toys." - * [Answer back] - "Really, Commander," I reply. "It rather sounds like you want to spank me." - "For God's sake," he declares with thick disgust, then swoops away out of the room. - - * [Say nothing] - I say nothing. It's true, isn't it? I can't deny that I know there is a world out there, a complicated world of pain and suffering. And I can't deny that I don't think about it a moment longer than I have to. What use is thinking on a problem that cannot be solved? It is precisely our ability to avoid such endless spirals that makes us human and not machine. - "God have mercy on your soul," Harris says finally, as he gets to his feet and heads for the door. "I fear no—one else will." - - - -> left_alone - -= passed_onto_hooper - ~ hooper_mentioned = true - "No. I passed it on to Hooper." - "I see. And what did he do with it?" - * [Evade] - "I don't know." - "You can do better than that. Remember, there's a hangman's noose waiting for traitors." - * * [Theorise] - "Well, then," I answer, nervously. "What would he do? Either get rid of it straight away — or if that wasn't possible, which it probably wouldn't be, since he'd have to arrange things with his contacts — so most likely, he'd hide it somewhere and wait, until you had the rope around my neck and he could be sure he was safe." - -> claim_hooper_took_component.harris_being_convinced - - * * [Shrug] -> claim_hooper_took_component.its_your_problem - - * [Tell the truth] - "I don't think Hooper could have planned this in advance. So he'd need to get word to whoever he's working with, and that would take time. So I think he would have hidden it somewhere, and be waiting to make sure I soundly take the fall. That way, if anything goes wrong, he can arrange for the part to be conveniently re—found." - -> claim_hooper_took_component.harris_being_convinced - - * [Lie] - "I'm sure I saw him this evening, talking to someone by the fence on the woodland side of the compound. He's probably passed it on already. You'll have to ask him." - - -> claim_hooper_took_component.harrumphs - - -/*-------------------------------------------------------------------------------- - Trying to frame Hooper ---------------------------------------------------------------------------------*/ - - -=== claim_hooper_took_component -// Blame Hooper - "I saw Hooper take it." - ~ hooper_mentioned = true - { losttemper : - "Did you?" - The worst of his rage is passing; he is now moving into a kind of contemptuous despair. I can imagine him wrapping up our interview soon, leaving the hut, locking the door, and dropping the key down the well in the yard. - And why wouldn't he? With my name tarnished they will not let me back to work on the Bombe — if there is the slightest smell of treachery about my name I would be lucky not be locked up for the remainder of the war. - - else: - "I see." He is starting to lose his patience. I have seen Harris angry a few times, with lackeys and secretaries. But never with us. With the 'brains' he has always been cautious, treating us like children. - And now I see that, like a father, he wants to smack us when we disobey him. - } - "Just get to the truth, man. Every minute matters." - * { admitblackmail } [Persist with this] - "I know what you're thinking. If I've transgressed once then I must be guilty of everything else... But I'm not. We were close to cracking the 13th's intercept. We were getting correlations in the data. Then Hooper disappeared for a moment, and next minute the machine was down." - - * [Tell the truth] - "Very well. I see there's no point in covering up. You know everything anyway." - Harris nods, and waits for me to continue. - -> i_met_a_young_man - - * { not admitblackmail } [Persist with this] - "This is the truth." - - - I have become, somehow, an accustomed liar — the words roll easily off my tongue. Perhaps I am a traitor, I think, now that I dissemble as easily as one. - "Go on," Harris says, giving me no indication of whether he believes my tale. - * [Assert] "I saw him take it," I continue. "Collins was outside having a cigarette. Peterson was at the table. But I was at the front of the machine. I saw Hooper go around the side. He leant down and pulled something free. I even challenged him. I said, 'What's that? Someone put a nail somewhere they shouldn't have?' He didn't reply." - Harris watches me for a long moment. - - * [Imply] "At the moment the machine halted, Peterson was at the bench and Collins was outside having a smoke. I was checking the dip—switches. Hooper was the only one at the back of the Bombe. No—one else could have done it." - "That's not quite the same as seeing him do it," Harris remarks. - * * [Logical] - "When you have eliminated the impossible..." I begin, but Harris cuts me off. - - * * [Persuasive] - "You have to believe me." - "We don't have to believe anyone," Harris returns. "I will only be happy with the truth, and your story doesn't tie up. We know you've been leaving yourself open to pressure. We've been watching your activities for some time. But we thought you were endangering the reputation of this site; not risking the country herself. Perhaps I put too much trust in your intellectual pride." - He pauses for a moment, considering something. Then he continues: - "It might have been Hooper. It might have been you. -> we_wont_guess - - * * [Confident] - "Ask the others," I reply, leaning back. "They'll tell you. If they haven't already, that's only because they're protecting Hooper. Hoping he'll come to his senses and stop being an idiot. I hope he does too. And if you lock him up in a freezing hut like you've done me, I'm sure he will." - "We have," Harris replies simply. - It's all I can do not to gape. - -> hoopers_hut_3 - - - "We are left with two possibilities. You, or Hooper." The Commander pauses to smooth down his moustache. <> - - (hoopers_hut_3) "Hooper's in Hut 3 with the Captain, having a similar conversation." - - * "And the other men?["] Do we have a hut each? Are there enough senior officers to go round?" - "Collins was outside when it happened, and Peterson can't get round the machine in that chair of his," Harris replies. "That leaves you and Hooper. - * "Then you know I'm right.["] You knew all along. Why did you threaten me?" - "All we know is that we have a traitor, holding the fate of the country in his hands. - - (we_wont_guess) <> We're not in the business of guessing here at Bletchley. We are military intelligence. We get answers." Harris points a finger. "And if that component has left these grounds, then every minute is critical." - * [Co-operate] - "I'd be happy to help," I answer, leaning forwards. "I'm sure there's something I could do." - "Like what, exactly?" - * * "Put me in with Hooper." - -> putmein - * * "Tell Hooper I've confessed.["] Better yet. Let him see you marching me off in handcuffs. Then let him go, and see what he does. Ten to one he'll go straight to wherever he's hidden that component and his game will be up." - Harris nods slowly, chewing over the idea. It isn't a bad plan even — except, of course, Hooper has not hidden the component, and won't lead them anywhere. But that's a problem I might be able to solve once I'm out of this place; and once they're too busy dogging Hooper's steps from hut to hut. - "Interesting," the Commander muses. "But I'm not so sure he'd be that stupid. And if he's already passed the part on, the whole thing will only be a waste of time." - * * * "Trust me. He hasn't.["] If I know that man, and I do, he'll be wanting to keep his options open as long as possible. If the component's gone then he's in it up to his neck. He'll take a week at least to make sure he's escaped suspicion. Then he'll pass it on." - "And if we keep applying pressure to him, you think the component will eventually just turn up?" - * * * * "Yes.["] Probably under my bunk." - Harris smiles wryly. "We'll know that for a fake, then. We've looked there already. - * * * * "Or be thrown into the river." - "Hmm." Harris chews his moustache thoughtfully. "Well, that would put us in a spot, seeing as how we'd never know for certain. We'd have to be ready to change our whole approach just in case the part had got through to the Germans. - - - - - <> I don't mind telling you, this is a disaster, this whole thing. What I want is to find that little bit of mechanical trickery. I don't care where. In your luncheon box or under Hooper's pillow. Just somewhere, and within the grounds of this place." - * * * * "Then let him he think he's off the hook.["] Make a show of me. And then you'll get your man." - Somehow, I think. But that's the part I need to work. - -> harris_takes_you_to_hooper - - * * * * "Then you'd better get searching[."]," I reply, tiring of his complaining. A war is a war, you have to expect an enemy. -> its_your_problem - - * * * "You're right. Let me talk to him[."], then. As a colleague. Maybe I can get something useful out of him." - -> putmein - - * * * "You're right." -> shake_head - - * [Block] -> its_your_problem - - -= harris_being_convinced - "Makes sense," Harris agrees, cautiously. { evasive > 1:I can see he's still not entirely convinced by my tale, as well he might not be — I've hardly been entirely straight with him.|I can see he's still not certain whether he can trust me.} "Which means the question is, what can we do to rat him out?" - * [Offer to help] - "Maybe I can help with that." - "Oh, yes? And how, exactly?" - * * "I'll talk to him." - "What?" - "Put me in with Hooper with him. Maybe I can get something useful out of him." - -> putmein - * * "We'll fool him.["] He's waiting to be sure that I've been strung up for this, so let's give him what he wants. If he sees me taken away, clapped in irons — he'll go straight to that component and set about getting rid of it." - -> harris_takes_you_to_hooper - - * [Don't offer to help] - I lean back. -> its_your_problem - -= putmein - Harris shakes his head. - "He despises you. I don't see why he'd give himself up to you." - * [Insist] "Try me. Just me and him." - -> go_in_alone - * [Give in] "You're right." - -> shake_head - - -= shake_head - // Can't help - <> I shake my head. "You're right. I don't see how I can help you. So there's only one conclusion." - "Oh, yes? And what's that?" - -> its_your_problem - - -= its_your_problem -// Won't Help - "It's your problem. Your security breach. So much for your careful vetting process." - I lean back in my chair and fold my arms so the way they shake will not be visible. - "You'd better get on with solving it, instead of wasting your time in here with me." - -> harrumphs - -= harrumphs - Harris harrumphs. He's thinking it all over. - * { putmein } [Wait] - "All right," he declares, gruffly. "We'll try it. But if this doesn't work, I might just put the both of you in front of a firing squad and be done with these games. Worse things happen in time of war, you know." - "Alone," I add. - -> go_in_alone - - * { not putmein } [Wait] - "No," Harris declares, finally. "I think you're lying about Hooper. I think you're a clever, scheming young man — that's why we hired you — and you're looking for the only reasonable out this situation has to offer. But I'm not taking it. We know you were in the room with the machine, we know you're of a perverted persuasion, we know you have compromised yourself. There's nothing more to say here. Either you tell me what you've done with that component, or we will hang you and search just as hard. It's your choice." - -> harris_threatens_lynching - - -= go_in_alone - "Alone?" - "Alone." - Harris considers it. I watch his eyes, flicking backwards and forwards over mine, like a ribbon—reader loading its program. - * [Patient] "Well?" - * [Impatient] "For God's sake, man, what do you have to lose?" - ~ raise(forceful) - - "We'll be outside the door," Harris replies, seriously. "The first sign of any funny business and we'll have you both on the floor in minutes. You understand? The country needs your brain, but it's not too worried about your legs. Remember that." - Then he gets to his feet, and opens the door, and marches me out across the yard. The evening is drawing in and there's a chill in the air. My mind is racing. I have one opportunity here — a moment in which to put the fear of God into Hooper and make him do something foolish that places him in harm's way. But how to achieve it? - "You ready?" Harris demands. - * (yes) [Yes] - "Absolutely." - * [No] - "No." - "Too bad." - * [Lie] -> yes - - - -> inside_hoopers_hut - - -/*-------------------------------------------------------------------------------- - Quick visit to see Hooper ---------------------------------------------------------------------------------*/ - -=== harris_takes_you_to_hooper - // Past Hooper - Harris gets to his feet. "All right," he says. "I should no better than to trust a clever man, but we'll give it a go." - Then, he smiles, with all his teeth, like a wolf. - { claim_hooper_took_component.hoopers_hut_3: - "Especially since this is a plan that involves keeping you in handcuffs. I don't see what I have to lose." - - else: - "Hooper's in Hut 3 being debriefed by the Captain. Let's see if we can't get his attention somehow." - } - // Leading you past Hooper - He raps on the door for the guard and gives the man a quick instruction. He returns a moment later with a cool pair of iron cuffs. - "Put 'em up," Harris instructs, and I do so. The metal closes around my wrists like a trap. I stand and follow Harris willingly out through the door. - But whatever I'm doing with my body, my mind is scheming. Somehow, I'm thinking, I have to get away from these men long enough to get that component behind Hut 2 and put it somewhere Hooper will go. Or, otherwise, somehow get Hooper to go there himself... - Harris marches me over to Hut 3, and gestures for the guard to stand aside. Pushing me forward, he opens the door nice and wide. - // Hut 3 - "Captain. Manning talked. If you'd step out for a moment?" - * [Play the part, head down] - From where he's sitting, I know Hooper can see me, so I keep my head down and look guilty as sin. The bastard is probably smiling. - - - * [Look inside the hut] - I look in through the door and catch Hooper's expression. I had half expected him to be smiling be he isn't. He looks shocked, almost hurt. "Iain," he murmurs. "You couldn't..." - - * (shouted) [Call to Hooper] - I have a single moment to shout something to Hooper before the door closes. - "I'll get you Hooper, you'll see!" I cry. Then: - - * * "Queen to rook two, checkmate!"[] I call, then laugh viciously, as if I am damning him straight to hell. - ~ hooperClueType = CHESS - - - (only_catch) I only catch Hooper's reaction for a moment — his eyebrow lifts in surprise and alarm. Good. If he thinks it is a threat then he just might be careless enough to go looking for what it might mean. - - * * "Ask not for whom the bell tolls!" - He stares back at me, as if were a madman and perhaps for a split second I see him shudder. - - - * * "Two words: messy, without one missing!"[] I cry, laughing. It isn't the best clue, hardly worthy of The Times, but it will have to do. - ~ hooperClueType = CROSSWORD - -> only_catch - -- The Captain comes outside, pulling the door to. "What's this?" he asks. "A confession? Just like that?" - "No," the Commander admits, in a low voice. "I'm afraid not. Rather more a scheme. The idea is to let Hooper go and see what he does. If he believes we have Manning here in irons, he'll try to shift the component." - "If he has it." - "Indeed." - The Captain peers at me for a moment, like I was some kind of curious insect. - "Sometimes, I think you people are magicians," he remarks. "Other times you seem more like witches. Very well." - With that he opens the door to the Hut and goes back inside. The Commander uses the moment to hustle me roughly forward. - { shouted : - "And what was all that shouting about?" he hisses in my ear as we move towards the barracks. "Are you trying to pull something? Or just make me look incompetent?" - - else: - "This scheme of yours had better come off," he hisses in my ear. "Otherwise the Captain is going to start having men tailing me to see where I go on Saturdays." - } - * [Reassure] - { not shouted : - "It will. Hooper's running scared," I reply, hoping I sound more confident than I feel. - - else: - "Just adding to the drama," I tell him, confidently. "I'm sure you can understand that." - } - "I think we've had enough drama today already," Harris replies. "Let's hope for a clean kill." - - * [Dissuade] - { not shouted: - "The Captain thought it was a good scheme. You'll most likely get a promotion." - - else: - "I'm not trying to do anything except save my neck." - } - "Let's hope things work out," Harris agrees darkly. - - * [Evade] - "We're still in ear—shot if they let Hooper go. Best get us inside and then we can talk, if we must." - "I've had enough of your voice for one day," Harris replies grimly. <> - - * [Say nothing] - I let him have his rant. <> -- He hustles me up the steps of the barracks, keeping me firmly gripped as if I had any chance of giving him, a trained military man, the slip. It's all I can do not to fall into the room. - -> slam_door_shut_and_gone - - - - -=== inside_hoopers_hut - - Harris opens the door and pushes me inside. "Captain," he calls. "Could I have a moment?" - The Captain, looking puzzled, steps out. The door is closed. Hooper stares at me, open—mouthed, about to say something. I probably have less than a minute before the Captain storms back in and declares this plan to be bunkum. - * [Threaten] - "Listen to me, Hooper. We were the only men in that hut today, so we know what happened. But I want you to know this. I put the component inside a breeze—block in the foundations of Hut 2, wrapped in one of your shirts. They're going to find it eventually, and that's going to be what tips the balance. And there's nothing you can do to stop any of that from happening." - ~ hooperClueType = STRAIGHT - - His eyes bulge with terror. "What did I do, to you? What did I ever do?" - * * [Tell the truth] - "You treated me like vermin. Like something abhorrent." - "You are something abhorrent." - "I wasn't. Not when I came here. And I won't be, once you're gone." - - * * [Lie] - "Nothing," I reply. "You're just the other man in the room. One of us has to get the blame." - - * * [Evade] - "It doesn't matter. Just remember what I said. I've beaten you, Hooper. Remember that." - - - I get to my feet and open the door of the Hut. The Captain storms back inside and I'm quickly thrown out. -> hustled_out - - - * [Bargain] - "Hooper, I'll make a deal with you. We both know what happened in that hut this afternoon. I know because I did it, and you know because you know you didn't. But once this is done I'll be rich, and I'll split that with you. I'll let you have the results, too. Your name on the discovery of the Bombe. And it won't hurt the war effort — you know as well as me that the component on its own is worthless, it's the wiring of the Bombe, the usage, that's what's valuable. So how about it?" - Hooper looks back at me, appalled. "You're asking me to commit treason?" - * * [Yes] - "Yes, perhaps. But also to ensure your name goes down in the annals of mathematics. -> back_of_hut_2 - * * [No] - "No. It's not treason. It's a trade, plain and simple." - - * * (lie) [Lie] - "I'm suggesting you save your own skin. I've wrapped that component in one of your shirts, Hooper. They'll be searching this place top to bottom. They'll find it eventually, and when they do, that's the thing that will swing it against you. So take my advice now. Hut 2." - ~ hooperClueType = STRAIGHT - - * * [Evade] -> lie - - - -> no_chance - - * [Plead] - "Please, Hooper. You don't understand. They have information on me. I don't need to tell you what I've done, you know. Have a soul. And the component — it's nothing. It's not the secret of the Bombe. It's just a part. The German's think it's a weapon — a missile component. Let them have it. Please, man. Just help me." - "Help you?" Hooper stares. "Help you? You're a traitor. A snake in the grass. And you're queer." - * * [Deny] - "I'm no traitor. You know I'm not. How much work have I done here against the Germans? I've given my all. And you know as well as I do, if the Reich were to invade, I would be a dead man. Please, Hooper. I'm not doing any of this lightly." - - * * [Accept] - "I am what I am," I reply. "I'm the way I was made. But they'll hang me unless you help, Hooper. Don't let them hang me." - - * * [Evade] - "That's not important now. What matters is what you do, this evening." - - - - "Assuming I wanted to help you," he replies, carefully. "Which I don't. What would I do?" - "Nothing. Almost nothing. - -> back_of_hut_2 - -= back_of_hut_2 - <> All you have to do is go to the back of Hut 2. There's a breeze—block with a cavity. That's where I've put it. I'll be locked up overnight. But you can pick it up and pass it to my contact. He'll be at the south fence around two AM." - ~ hooperClueType = STRAIGHT - -> no_chance - -= no_chance - "If you think I'll do that then you're crazy," Hooper replies. - At that moment the door flies open and the Captain comes storming back inside. - -> hustled_out - -= hustled_out - // To Barracks - Harris hustles me over to the barracks. "I hope that's the end of it," he mutters. - "Just be sure to let him out," I reply. "And then see where he goes." - -> slam_door_shut_and_gone - - - -/*-------------------------------------------------------------------------------- - Left alone overnight ---------------------------------------------------------------------------------*/ - - -=== slam_door_shut_and_gone - Then they slam the door shut, and it locks. - { hooperClueType == NONE : - <> How am I supposed to manage anything from in here? - * [Try the door] -> try_the_door - * [Try the windows] -> try_the_windows - - - else: - I can only hope that Hooper bites. If he thinks I'm bitter enough to have framed him, and arrogant enough to have taunted him with {hooperClueType > STRAIGHT:a clue to} where the damning evidence is hidden... - If he hates me enough, and is paranoid enough, then he might {hooperClueType > STRAIGHT:unravel my little riddle and} go searching around Hut 2. - } - - * [Wait] -> night_falls - - -= try_the_door - I try the door. It's locked, of course. - -> from_outside_heard - -= from_outside_heard - From outside, I hear a voice. Hooper's. He's haranguing someone. - - (opts) - * (listened) [Listen at the keyhole] - I put my ear down to the keyhole, but there's nothing now. Probably still a guard outside, of course, but they're keeping mum. - -> opts - - * { not try_the_windows } [Try the window] -> try_the_windows - * { not try_the_door } {listened} [Try the door] -> try_the_door - * { try_the_windows } [Smash the window] -> try_to_smash_the_window - * { try_the_door && try_the_windows } [Wait] - It's useless. There's nothing I can do but hope. I sit down on one corner of the bunk to wait. - -> night_falls - -= try_the_windows - I go over to the window and try to jimmy it open. Not much luck, but in my struggling I notice this window only backs on the thin little brook that runs down the back of the compound. Which means, if I smashed it, I might get away with no—one seeing. - -> from_outside_heard - - -= try_to_smash_the_window - The window is my only way out of here. I just need a way to smash it. - * [Punch it] - I suppose my fist would do a good enough job. But I'd cut myself to ribbons, most likely. <> - - * (use_bucket) [Find something] - ~ smashingWindowItem = BUCKET - I cast around the small room. There's a bucket in one corner for emergencies — I suppose I could use that. I pick it up but it's not very easy to heft. <> - * [Use something you've got] - I pat down my pockets but all I'm carrying is the intercept, which is no good at all. - * * [Something you're wearing?] - Ah, but of course! I slip off one shoe and heft it by the toe. The heel will make a decent enough hammer, if I give it enough wallop. - ~ smashingWindowItem = SHOE - But I'll cut my hand to ribbons doing it. <> - * * [Look around] -> use_bucket - - And the noise would be terrible. There must be a way of making this easier. I'm supposed to be a thief now. What would a burglar do? - * [Work slowly] - Work carefully? It's difficult to work carefully when all one's has is { smashingWindowItem == BUCKET :a bucket. It's rather like the sledgehammer for the proverbial nut|{ smashingWindowItem == SHOE :a shoe|nothing but brute force}}. - * * [Just do it] -> time_to_move_now - * * [Look around for something] - * [Find something to help] - - -> find_something_to_smash_window - - -= time_to_move_now - Enough of this. There isn't any time to lose. Right now they'll be following Hooper as he goes to bed, and goes to sleep; and then that's it. The minute he closes his eyelids and drifts off that's the moment that this trap swings shut on me. - So I punch out the glass with my { smashingWindowItem == BUCKET :bucket|{ smashingWindowItem == SHOE :shoe|fist}} and it shatters with a terrific noise. Then I stop, and wait, to see if anyone will come in through the door. - Nothing. - * (pause) [Wait a little longer] - I pause for a moment longer. It doesn't do to be too careless... - * [Clear the frame of shards] - With my jacket wrapped round my arm, I sweep out the remaining shards of glass. It's not a big window, but I'm not a big man. If I was Harris, I'd be stuffed, but as it is... - - - Then the door locks turns. The door opens. Then Jeremy — one of the guards, rather — sticks his head through the door. "I thought I heard..." - He stops. Looks for a moment. { smashingWindowItem ==BUCKET :Sees the bucket in my hand.|Sees the broken window.} Then without a moment's further thought he blows his shrill whistles and hustles into the hut, grabbing me roughly by my arms. - { pause: - I'll never know if I hadn't have waited that extra moment — maybe I still could have got away. But, how far? - } - I'm hustled into one of the huts. Nowhere to sleep, but they're not interested in my comfort any longer. Harris comes in with the Captain. - "So," Harris remarks. "Looks like your little trap worked. Only it worked to show you out for what you are." - * [Tell the truth] - { i_met_a_young_man : - "Please, Harris. You can't understand the pressure they put me under. You can't understand what it's like, to be in love but be able to do nothing about it..." - - else: - "Harris. They were blackmailing me. They knew about... certain indiscretions. You can understand, can't you, Harris? I was in an impossible bind..." - } - * [Lie] - "I had to get out, Harris. I had to provoke Hooper into doing something that would incriminate himself fully. He's too clever, you see..." - - * [Evade] - "This proves nothing," I reply stubbornly. "You still don't have the component and without it, I don't see what you can hope to prove." - - - "Be quiet, man. We know all about your and your sordid affairs." The Captain curls his lip. "Don't you know there's a war on? Do you know the kind of place they would have sent you if it haven't had been for that brain of yours? Don't you think you owe it to your country to use it a little more?" - - Do I, I wonder? Do I owe this country anything, this country that has spurned who and what am I since the day I became a man? - * [Yes] - My anger deflates like a collapsing equation, all arguments cancelling each other out. The world, of course, owes me nothing; and I owe it everything. - - * (alone) [No] - Of course not. I am alone; that is what they wanted me to be, because of who and what I love. So I have no nation, no country. - - - * [Lie] -> alone - * [Evade] - But what is a country, after all? A country is not a concept, not an ideal. Every country falls, its borders shift and move, its language disappears to be replaced by another. Neither the Reich nor the British Empire will survive forever, so what use is my loyalty to either? - I may as well, therefore, look after myself. Something I have attempted, but failed miserably, to do. - - - // Tell us where - "I'm afraid we have only one option, Manning," Harris says. "Please, man. Tell us where the component is." - ~ notraitor = true - ~ losttemper = false - * [Tell them] - ~ revealedhooperasculprit = false - "All right." I am beaten, after all. "<>-> reveal_location_of_component - - * [Say nothing] -> my_lips_are_sealed - -= find_something_to_smash_window - Let me see. There's the bunk, { not smashingWindowItem == BUCKET :a bucket,} nothing else. I have my jacket but nothing in the pockets — no handkerchief, for instance. - - (opts) - * [The bunk] - The bunk has a solid metal frame, a blanket, a pillow, nothing more. - - - (bunk_opts) - * * [The frame] - The frame is heavy and solid. I couldn't lift it or shift it without help from another man. And it wouldn't do me any good here anyway. I can reach the window perfectly well. - -> bunk_opts - * * [The blanket] - The blanket. Perfect. I scoop it up off the bed and hold it in place over the window. -> smash_the_window - * * [The pillow] - The pillow is fat and fluffy. I could put it over the window and it would muffle the sound of breaking glass, certainly; but I wouldn't be able to break any glass through it either. - -> bunk_opts - - * * {bunk_opts > 1} [Something else] -> opts - - * [The jacket] - I slip off my jacket and hold it with one hand over the glass. -> smash_the_window - * { not smashingWindowItem == BUCKET } [The bucket] - The bucket? Hardly. The bucket might do some good if I wanted to sweep up the glass afterwards, but it won't help me smash the glass quietly. - -> opts - - -=== smash_the_window - // Smashing glass - Then I heft { smashingWindowItem == BUCKET :up the bucket — this really is quite a fiddly thing to be doing in cuffs — |{ smashingWindowItem == SHOE : my shoe by its toe, |back my arm, }} and take a strong swing, trying to imagine it's Harris' face on the other side. - ~ smashedglass = true - ~ smashingWindowItem = NONE - * [Smash!] - - The sound of the impact is muffled. With my arm still covered, I sweep out the remaining glass in the frame. - - I'm ready to escape. The only trouble is — when they look in on me in the morning, there will be no question what has happened. It won't help me one jot with shifting suspicion off my back. - * [Wait] - So perhaps I should wait it out, after all. Who knows? I might have a better opportunity later. - -> night_passes - * [Slip out] - Moving quickly and quietly, I hoist myself up onto the window—frame and worm my way outside into the freezing night air. Then I am away, slipping down the paths between the Huts, sticking to the shadows, on my way to Hut 2. - // Out at night - - - * [Go the shortest way] - There's no time to lose. Throwing caution to the wind I make my way quickly to Hut 2, and around the back. I don't think I've been seen but if I have it is too late. My actions are suspicious enough for the noose. I have no choice but to follow through. - * [Take a longer route] - In case I'm being followed, I divert around the perimeter of the compound. It's a much longer path, and it takes me across some terrain that's difficult to negotiate in the dark — muddy, and thick with thistles and nestles. - ~ muddyshoes = true - Still, I can be confident no—one is behind me. I crouch down behind the rear wall of Hut 2. <> - - The component is still there, wrapped in a tea—towel and shoved into a cavity in a breeze—block at the base of the Hut wall. - * [Take it] - Quickly, I pull it free, and slip it into the pocket of my jacket. - ~ gotcomponent = true - - * [Leave it] - Still there means no—one has found it, which means it is probably well—hidden. And short of skipping the compound now, I can afford to leave it hidden there a while longer. So I leave it in place. - - Where now? - * [Back to the barracks] -> return_to_room_after_excursion - * { gotcomponent } [Go to Hooper's dorm] -> go_to_hoopers_dorm - * [Escape the compound] - Enough of this place. Time for me to get moving. I can get to the train station on foot, catch the postal train to Scotland and be somewhere else before anyone realises that I'm gone. - - Of course, then they'll be looking for me in earnest. { not framedhooper :As a confirmed traitor.|Perhaps not as a traitor — they might take the idea that Hooper was involved with the theft — but certainly as a valuable mind, one containing valuable secrets and all too easily threatened. They will think I am running away because of my indiscretions. I suppose, in fairness, that I am.} - * * [Go] -> live_on_the_run - * * [Don't go] - It's no good. That's only half a solution. I couldn't be happy with that. - * * * [Back to the barracks] -> return_to_room_after_excursion - * * * { gotcomponent && not go_to_hoopers_dorm } [To Hooper's dorm] -> go_to_hoopers_dorm - - -/*-------------------------------------------------------------------------------- - Visit Hooper's dorm overnight ---------------------------------------------------------------------------------*/ - - -=== go_to_hoopers_dorm - // Hooper's Dorm - I creep around the outside of the huts towards Hooper's dorm. Time to wrap up this little game once and for all. A few guards patrol the area at night but not many — after all, very few know this place even exists. - Our quarters are arranged away from the main house; where we sleep is of less importance than where we work. We each have our own hut, through some are less permanent than others. Hooper's is a military issue tent: quite a large canopy, with two rooms inside and a short porch area where he insists people leave their shoes. It's all zipped up for the night and no light shines from inside. - I hang back for a moment. If Harris is keeping to the terms of our deal then someone will be watching this place. But I can see no—one. - * (outer_zip) [Open the outer zip] - I creep forward to the tent, intent on lifting the zip to the front porch area just a little — enough to slip the component inside, and without the risk of the noise waking Hooper from his snoring. - The work is careful, and more than little fiddly — Hooper has tied the zips down on the inside, the fastidious little bastard! — but after a little work I manage to make a hole large enough for my hand. - * * [Slip in the component] - I slide the component into the tent, work the zip closed, and move quickly away into the shadows. It takes a few minutes for my breath to slow, and my heart to stop hammering, but I see no other movement. If anyone is watching Hooper's tent, they are asleep at their posts. - ~ putcomponentintent = true - ~ gotcomponent = false - -> return_to_room_after_excursion - * * [No, some other way] - Then pause. This is too transparent. Too blatant. If I leave it here, like this, Hooper will never be seen to go looking for it: he will stumble over it in plain sight, and the men watching will wonder why it was not there when he went to bed. - No, I must try something else — or nothing at all. - * * * [On top of the tent] -> put_component_on_tent - * * * [Throw the component into the long grass] - From inspiration — or desperation, I am not certain — a simple approach occurs to me. -> toss_component_into_bushes - * * * [Give up] - There is nothing to be gained here. I have the component now; maybe it will be of some value tomorrow. - * * * * [Return to my barrack] -> return_to_room_after_excursion - * * * * [Escape the compound] -> live_on_the_run - - * (wide_circuit) [Look for another opening] - Making a wide circuit I creep around the tent. It has plenty of other flaps and openings, tied down with Gordian complexity. But nothing afford itself to slipping the component inside. - * * [Try the porch zip] -> outer_zip - * * [Try on top of the tent] -> put_component_on_tent - * * [Give up] - It's no good. Nothing I can do will be any less than obvious — something appearing where something was not there before. The men watching Hooper will know it is a deception and Hooper's protestations will be taken at face value. - If I can't find a way for Hooper to pick the component up, as if from a hiding place of his own devising, and be caught doing it, then I have no plan at all. - * * * [Return to my barrack] -> return_to_room_after_excursion - * * * [Escape the compound] -> live_on_the_run - * * * [Toss the component into the bushes] -> toss_component_into_bushes - - * [Hide the component somewhere] - If I leave the component here somewhere it should be somewhere I can rely on Hooper finding it, but no—one before Hooper. In particular. - * * [Behind the tent] -> wide_circuit - * * [Inside the porch section] -> outer_zip - * * [On top of the canvas] -> put_component_on_tent - - -= put_component_on_tent - A neat idea strikes me. If I could place it on top of the canvas, somewhere in the middle where it would bow the cloth inwards, then it would be invisible to anyone passing by. But to Hooper, it would be above him: a shadow staring him in the face as he awoke. What could be more natural than getting up, coming out, and looking to see what had fallen on him during the night? - - It's the work of a moment. I was once an excellent bowler for the second XI back at school. This time I throw underarm, of course, but I still land the vital missing component exactly where I want it to go. - ~ framedhooper = true - ~ gotcomponent = false - For a second I hold my breath, but nothing and no—one stirs. -> return_to_room_after_excursion - - -= toss_component_into_bushes - I toss the component away into the bushes behind Hooper's tent and return to my barrack, wishing myself a long sleep followed by a morning, free of this business. - ~ gotcomponent = false - ~ throwncomponentaway = true - -> return_to_room_after_excursion - -/*-------------------------------------------------------------------------------- - Ending: Run away from the camp ---------------------------------------------------------------------------------*/ - - -=== live_on_the_run - Better to live on the run than die on the spit. Creeping around the edge of the compound{ gotcomponent :, the Bombe component heavy in my pocket}, I make my way to the front gate. As always, it's manned by two guards, but I slip past their box by crawling on my belly. - And then I'm on the road. Walking, not running. Silent. Free. - // End - Run Away - For the moment, at least. - -> END - -/*-------------------------------------------------------------------------------- - Return to room after slipping out ---------------------------------------------------------------------------------*/ - - -=== return_to_room_after_excursion - { gotcomponent :The weight of the Bombe component safely in my jacket|Satisfied}, I return the short way up the paths between the huts to the barrack block and the broken window. - It's a little harder getting back through — the window is higher off the ground than the floor inside — but after a decent bit of jumping and hauling I manage to get my elbows up, and then one leg, and finally I collapse inside, quite winded and out breath. - * [Wait] -> night_passes - -/*-------------------------------------------------------------------------------- - Night passes ---------------------------------------------------------------------------------*/ - - -=== night_passes -// In room smashed glass - The rest of the night passes slowly. I sleep a little, dozing mostly. Then I'm woken by the rooster in the yard. The door opens, and Harris comes in. He takes one look at the broken window and frowns with puzzlement. - { putcomponentintent: -> put_component_inside_tent } - - "What happened there?" - * [Confess] - "I broke it," I reply. There doesn't seem any use in trying to lie. "I thought I could escape. But I couldn't get myself through." - The Commander laughs. -> glad_youre_here - - * (deny) [Deny] - "I'm not sure. I was asleep: I woke up when someone broke the window. I looked out to see who it was, but they were already gone." - Harris looks at me with puzzlement. "Someone came by to break the window, and then ran off? That's absurd. That's utterly absurd. Admit it, Manning. You tried to escape and you couldn't get through." - * * [Admit it] - "All right. {forceful>1:Damn you.} That's exactly it." - -> glad_youre_here - - * * { not framedhooper } [Deny it] - "If I wanted to escape, I would have made damn sure that I could," I tell him sternly. - -> harris_certain_is_you - - * * { framedhooper } [Deny it] - "I tell you, someone broke it. Someone wanted to threaten me, I think." - Harris shakes his head. "Well, we can look into that matter later. For now, you probably want to hear the more pressing news. -> found_missing_component - - * { gotcomponent } [Show him the component] -> someone_threw_component - -= put_component_inside_tent - He takes one look around, and sighs, a deep, wistful sigh. - "Things just get worse and worse for you, Manning," he remarks. "You are your own worst enemy." - * [Agree] - "I've thought so before." { admitblackmail :Certainly in the matter of getting blackmailed.} - "Let me tell you what happened this morning. <> - - * [Disagree] - "Right now, I think you take that role, Harris," I reply coolly. - - - (droll) "Very droll," he replies. "Let me tell you what happened this morning. It will take the smile off your face. <> - - * [Evade] - "I'm looking forward to having a wash and a change of clothes; which should make me a little less evil to be around." - -> droll - - - Our men watching Hooper's tent saw Hooper wake up, get dressed, clamber out of his tent and then step on something in at the entrance of his tent." - ~ piecereturned = true - * [Be interested] - "You mean he didn't even hide it? He put it in his shoe?" - - - (not_that) "No," Harris replies. "That isn't really what I mean. <> - - * [Be dismissive] - "So he's an idiot, and he hid it in his shoe." - -> not_that - - * [Say nothing] - I say quiet, listening, not sure how this will go. - "In case I'm not making myself clear," Harris continues, "<> - - - I mean, he managed to find it, by accident, somewhere where it wasn't the night before. And at the same time, you're sitting here with your window broken. So, I rather think you've played your last hand and lost. It's utterly implausible that Hooper stole that component and then left it lying around in the doorway of his tent. So I came to tell you that the game is up, for you." - He nods and gets to his feet. -> left_alone - - - -= someone_threw_component - "Someone threw this in through the window over night," I reply, and open my jacket to reveal the component from the Bombe. "I couldn't see who, it was too dark. But I know what it is." - He reaches out and takes it. "Well, I'll be damned," he murmurs. "That's it all right. And you didn't have it on you when we put you in here. But it can't have been Hooper — I had men watching him all night. And there's no—one else it could have been." - He turns the component over in his hands, bemused. - ~ piecereturned = true - * [Suggest something] - "Perhaps Hooper had an accomplice. Someone else who works on site." - Harris shakes his head, distractedly. "That doesn't make sense," he says. "Why go to all the trouble of stealing it only to give it back? And why like this?" - * * [Suggest something] - "Perhaps the accomplice thought it was Hooper being kept in here. Maybe they saw the guard..." - -> all_too_farfetched - * * [Suggest nothing] - * [Suggest nothing] - - I shrug, eloquently. - - -> all_too_farfetched - - -= glad_youre_here - "Shame," he remarks. "I should have left that window open and put a guard on you. Might have been interesting to see where you went. Anyway, I'm glad you're still here, even if you do smell like a dog." - - * { not framedhooper } [Be optimistic] - -> night_falls.morning_not_saved.optimism - * { not framedhooper } [Be pessimistic] - -> night_falls.morning_not_saved.pessimism - - * { framedhooper } [Be optimistic] - "I'm looking forward to having a bath." - // Framed Hooper - "Well, you should enjoy it. <> - - * { framedhooper } [Be pessimistic] - "I imagine I'll smell worse after another couple of days of this." - "That won't be necessary. <> - - -> found_missing_component - - -= found_missing_component - // Framed Hooper - We found the missing component. Or rather, Hooper found it for us. He snuck out and retrieved it from on top. Of all the damnest places — you would never have known it was there. He claimed ignorance when we jumped him, of course. But it's good enough for me." - * (devil) [Approve] - "I can't tell you enough, I'm glad to hear it. I've had a devil of a night." - His gaze flicks to the broken window, but only for a moment. I think he genuinely cannot believe I could have done it. - * [Disapprove] - "You should never have hired him. A below-average intelligence can't be expected to cope with the pressure of our work." - - Harris rolls his eyes, but he might almost be smiling. "You'd better get along, { devil :and work through your devils|Mr Intelligent}. There's a 24—hour—late message to be tackled and we're a genius short. So you'd better be ready to work twice as hard." - * [Thank him] - "I'll enjoy it. Thank you for helping me clear this up." - "Don't thank me yet. There's still a war to fight. Now get a move on." - I nod, and hurry out of the door. The air outside has never tasted fresher and more invigorating. <> - - * [Argue with him] - "I'll work as hard as I work." - "Get out," Harris growls. "Before I decide to arrest you as an accessory." - I do as he says. Outside the barrack, the air has never smelt sweeter. - - -> head_for_my_dorm_free - - -=== night_falls === -// Night falls - Night falls. The clockwork of the heavens keeps turning, whatever state I might be in. No—one can steal the components that make the sun go down and the stars come out. I watch it performing its operations. I can't sleep. - { hooperClueType > NONE : - Has Hooper taken my bait? - } - * [Look of out the window] - I peer out of the window, but it looks out onto the little brook at the back of the compound, with no view of the other huts or the House. Who knows if there are men up, searching the base of Hut 2, following one another with flashlights... - {inside_hoopers_hut.back_of_hut_2: - Perhaps Hooper is there, in the dark, trying to help me after all? - } - * [Listen at the door] - I put my ear to the keyhole but can make out nothing. Are there still guards posted? { hooperClueType > NONE :Perhaps, if Hooper has managed to incriminate himself, the guards have been removed?|Perhaps the component has been found and the crisis is over.} - Perhaps the door is unlocked and they left me to sleep? - * * [Try it] I try the handle. No such luck. - * * [Leave it] I don't touch it. I don't want anyone outside thinking I'm trying to escape. - - * [Wait] - There is nothing I can do to speed up time. - - - The night moves at its own pace. I suppose by morning I will know my fate. - * { hooperClueType > NONE } [Wait] - // Hooper now arrested - Morning comes. I'm woken by a rooster calling from the yard behind the House. I must have slept after all. I pull myself up from the bunk, shivering slightly. There is condensation on the inside of the window. I have probably given myself a chill. - Without knocking, Harris comes inside. "You're up," he remarks, and then, "You smell like an animal." - * * [Be friendly] - "I suppose I do rather." I laugh, but Harris does not. - "This damn business gets worse and worse," he says, talking as he goes over to unlock and throw open the window. <> - * * [Be cold] - "So would you," I reply tartly. Harris shrugs. - "I've been through worse than this," he replies matter—of—factly. "It's hardly my fault if you sleep in your clothes." - I glare back. He goes over to the window, unlocks it and throws it open, relishing the fresh air from outside. - - - "Hooper's confessed, you know." - * * [Be eager] - "He has? I knew he would. The worm." - "Steady now. Matters aren't over yet. <> - * * [Be cautious] - "Oh, yes?" - "Yes. For what that's worth. <> - - - (hooper_didnt_give_himself_up) There's still the issue of the component. It hasn't turned up. He didn't lead us to it. I guess he figured you must have had something on him. I don't know." - - He looks quite put out by the whole affair. He is not the kind of man to deal well with probabilities. - * * [Be interested] - "You mean he confessed of his own accord? You didn't catch him?" - - * * [Be disinterested] - "Well, I'm glad his conscience finally caught up with him," I reply dismissively. - - - "The Captain went back into that hut and he confessed immediately. We were so surprised we didn't let you go." He wrinkles his nose. "I'm rather sorry about that now. I suggest you have a wash." - And with that he gestures to the doorway. - * * [Go] - * * [Wait] - I hang back a moment. Something does not seem quite right. After all, Hooper did not steal the component. He has no reason to confess to anything. Perhaps this is another trap? - "Well?" Harris asks. "What are you waiting for? Please don't tell me you want to confess now as well, I don't think my head could stand it." - * * * [Confess] - After a chance like this? A chance — however real — to save my neck? To hand it over — what, to save Hooper's worthless skin? - * * * * [Confess] - I see. Perhaps you think I bullied the man into giving himself up. Perhaps he understood my little clue far enough to know it was a threat against him, but not well enough to understand where he should look to find it. So he took the easy route out and folded. Gave me the hand. - ~ hooperConfessed = true - Hardly sporting, of course. - * * * * * [Confess] - Well, then. I suppose this must be what it feels like to have a conscience. I suppose I had always wondered. - "Harris, sir. I don't know what Hooper's playing at, sir. But I can't let him do this." - "Do what?" - "Take the rope for this. I took it, sir. - ~ revealedhooperasculprit = false - ~ losttemper = false - -> reveal_location_of_component - * * * * * [Don't confess] - * * * * [Don't confess] - * * * [Don't confess] - - - - "I certainly don't. But still, I'm surprised. I had Hooper down for a full—blown double agent, a traitor. He knows he'll face the rope, doesn't he?" - "Don't ask me to explain why he did what he did," Harris sighs. "Just be grateful that he did, and you're now off the hook." - - - Curiouser and curiouser. I nod once to Harris and slip outside into the cold morning air. - { hooperClueType == NONE : - Hooper's confession only makes sense in one fashion{ hooperConfessed :, and that is his being dim—witted and slow| — if I successfully implied to him that I had him framed, but he did not unpack my little clue well enough to go looking for the component. Well, I had figured him for a more intelligent opponent, but a resignation from the game will suffice}. Or perhaps he knew he would be followed if he went to check, and decided he would be doomed either way. - - else: - Hooper's confession only makes sense in one way — and that's that he believed me. He reasoned that he would be followed. To try and uncover the component would have got him arrested, and to confess was the same. - He simply caved, and threw in his hand. - } - // Outside, possibly free - Of course, however, there is only one way to be certain that Harris is telling the truth, and that is to check the breeze—block at the back of Hut 2. - * * [Check] -> go_to_where_component_is_hidden - * * [Don't check] - But there will time for that later. If there is nothing there, then Hooper discovered the component after all and Harris' men will have swooped on him, and the story about his confession is just a ruse to test me out. - And if the component is still there — well. It will be just as valuable to my contact in a week's time, and his deadline of the 31st is not yet upon us. - -> head_for_my_dorm_free - - * { hooperClueType == NONE } [Wait] -> morning_not_saved - -= morning_not_saved - // Not saved - Morning comes with the call of a rooster from the yard of the House. I must have slept after all. I pull myself up off the bunk, shivering slightly. There is condensation on the inside of the window. I have probably given myself a chill. - It's not long after that Harris enters the hut. He closes the door behind him, careful as ever, then takes a chair across from me. - "You smell like a dog," he remarks. - * (optimism) [Be optimistic] - "I'm looking forward to a long bath," I reply. "And getting back to work." - * (pessimism) [Be pessimistic] - "So would you after the night I've had." - - - -> harris_certain_is_you - - -=== harris_certain_is_you - "Well, I'm afraid it is going to get worse for you," Harris replies soberly. "We followed Hooper, and he took himself neatly to bed and slept like a boy scout. Which puts us back to square one, and you firmly in the frame. And I'm afraid I don't have time for any more games. I want you to tell me where that component is, or we will hang you as a traitor." - ~ revealedhooperasculprit = false - ~ losttemper = false - -> harris_threatens_lynching - - - - -/*--------------------------------------------------------------- - Ending: they don't think it was you ----------------------------------------------------------------*/ - - -=== head_for_my_dorm_free -I head for my dorm, intent on a bath, breakfast, a glance at the crossword before the other men get to it, and then on with work. They should have replaced the component in the Bombe by now. We will only be a day behind. - { not framedhooper : - And then everything will proceed as before. The component will mean nothing to the Germans — this is the one fact I could never have explained to a man like Harris, even though the principle behind the Bombe is the same as the principle behind the army. The individual pieces — the men, the components — do not matter. They are identical. It is how they are arranged that counts. -} -I bump into Russell in the dorm hut. -"Did you hear?" he whispers. "Terrible news about Hooper. Absolutely terrible." - * [Yes] - "Quite terrible. I would never have guessed." - "Well." Russell harrumphs. - - - (quince) "Quince was saying this morning, apparently his grandfather was German. So perhaps it's to be expected. See you there?" - - * [No] - - "Heard what?" - - - (hooper_taken) "Hooper's been taken away. They caught him, uncovering that missing Bombe component from a hiding place somewhere, apparently about to take it to his contact." Russell harrumphs. -> quince - * [Lie] - "I don't know what you're talking about." - -> hooper_taken - * [Evade] - "If you'll excuse me, Russell. I was about to take a bath." - "Oh, of course. Worked all night, did you? Well, you'll hear soon enough. Can hardly hide the fact there'll only be three of us from now on." - -- I wave to him and move away, my thoughts turning to the young man in the village. My lover. My contact. My blackmailer. Hooper may have taken the fall for the missing component, but { not framedhooper :if he did recover it from Hut 2 then | its recovery does mean }I have nothing to sell to save my reputation{ i_met_a_young_man :, if I have any left}. - { not framedhooper : -If he didn't, of course, and Harris was telling the truth about his sudden confession, then I will be able to buy my freedom once and for all. -} - * { not framedhooper } [Get the component] -> go_to_where_component_is_hidden - * { not framedhooper } [Leave it] - I will have to leave that question for another day. To return there now, when they're probably watching my every step, would be suicide. After all, if Hooper { hooperClueType == STRAIGHT :followed|understood} my clue, he will have explained it to them to save his neck. They won't believe him — but they won't quite disbelieve him either. We're locked in a cycle now, him and me, of half—truth and probability. There's nothing either of us can do to put the other entirely into blame. - -> ending_return_to_normal - * [Act normal] - But there is nothing to be done about it. -> ending_return_to_normal - - - - -=== ending_return_to_normal -Nothing, that is, except to act as if there is no game being played. I'll have a bath, then start work as normal. I've got a week to find something to give my blackmailer{ i_met_a_young_man : — or give him nothing: it seems my superiors know about my indiscretions now already}. - * [Co-operate] - Something will turn up. It always does. An opportunity will present itself, and more easily now that Hooper is out of the way. - But for now, there's yesterday's intercept to be resolved. - - * [Dissemble] - Or perhaps I might hand my young blackmailer over my superiors instead for being the spy he is. - Perhaps that would be the moral thing to do, even, and not just the most smart. - But not today. Today, there's an intercept to resolve. - - * [Lie] - In a week's time, this whole affair will be in the past and quite forgotten. I'm quite sure of that. -> moreimportant - * (moreimportant) [Evade] I've more important problems to think about now. There's still yesterday's intercept to be resolved. -- The Bombe needs to be set up once more and set running. -It's time I tackled a problem I can solve. -// End - Scot Free --> END - - -=== go_to_where_component_is_hidden - It won't take a moment to settle the matter. I can justify a walk past Hut 2 as part of my morning stroll. It will be obvious in a moment if the component is still there. - On my way across the paddocks, between the huts and the House, I catch sight of young Miss Lyon, arriving for work on her bicycle. She giggles as she sees me and waves. - * [Wave back] - I wave cheerily back and she giggles, almost drops her bicycle, then dashes away inside the House. Judging by the clock on the front gable, she's running a little late this morning. - * [Ignore her] - I give no reaction. She sighs to herself, as if this kind of behaviour is normal, and trots away inside the House to begin her duties. - - I turn the corner of Hut 3 and walk down the short gravel path to Hut 2. It was a good spot to choose — Hut 2 is where the electricians work, and they're generally focussed on what they're doing. They don't often come outside to smoke a cigarette so it's easy to slip past the doorway unnoticed. - * [Check inside] - I hop up the steps and put my head inside all the same. Nobody about. Still too early in the AM for sparks, I suppose. <> - * [Go around the back] - - - I head on around the back of the hut. The breeze—block with the cavity is on the left side. - * (check) [Check] - No time to waste. I drop to my knees and check the breeze—block. Sure enough, there's nothing there. Hooper took the bait. - Suddenly, there's a movement behind me. I look up to see, first a snub pistol, and then, Harris. - - * [Look around] - I pause to glance around, and catch a glimpse of movement. Someone ducking around the corner of the hut. Or a canvas sheet flapping in the light breeze. Impossible to be sure. - * * [Check the breeze—block] -> check - * * [Check around the side of the hut] - But too important to guess. I move back around the side of the hut. - Harris is there, leaning in against the wall. He holds a stub pistol in his hand. - - - { hooperClueType > STRAIGHT : - "{ hooperClueType == CHESS:Queen to rook two|Messy without one missing whatever it was}," he declares. "I wouldn't have fathomed it but Hooper did. Explained it right after we sprung him doing what you're doing now. We weren't sure what to believe but now, you seem to have resolved that for us." - - else: - "Hooper said you'd told him where to look. I didn't believe him. Or, well. I wasn't sure what to believe. Now I rather think you've settled it." - } - * [Agree] - "I have, rather." I put my hands into my pockets. "I seem to have done exactly that." - "I'm afraid my little story about Hooper confessing wasn't true. I wanted to see if you'd go to retrieve the part." Harris gestures me to start walking. "You were close, Manning, I'll give you that. I wanted to believe you. But I'm glad I didn't." - -> done - * [Lie] - "I spoke to Russell. He said he saw Hooper doing something round here. I wanted to see what it was." - - * [Evade] - "Harris, you'd better watch out. He's planted a time—bomb here." - Harris stares at me for a moment, then laughs. "Oh, goodness. That's rich." - I almost wish I had a way to make the hut explode, but of course I don't. - - - "Enough." Harris gestures for me to start walking. "This story couldn't be simpler. You took it to cover your back. You hid it. You lied to get Hooper into trouble, and when you thought you'd won, you came to scoop your prize. A good hand but ultimately, { hooperClueType <= STRAIGHT :if it hadn't have been you who hid the component, then you wouldn't be here now|you told Hooper where to look with your little riddle}." - - - (done) - // End - Caught in AM - He leads me across the yard. Back towards Hut 5 to be decoded, and taken to pieces, once again. - -> END - - -/*--------------------------------------------------------------- - Ending: they think it was you ----------------------------------------------------------------*/ - -=== harris_threatens_lynching - { harris_certain_is_you:He passes a hand across his eyes with a long look of despair.|He gets to his feet, and gathers his gloves from the table top.} - "I'm going to go outside and organise a rope. That'll take about twelve minutes. That's how long you have to decide." - * [Protest] - "You can't do this!" I cry. "It's murder! I demand a trial, a lawyer; for God's sake, man, you can't just throw me overboard, we're not barbarians...!" - - - (too_clever) "You leave me no choice," Harris snaps back, eyes cold as gun—metal. "You and your damn cyphers. Your damn clever problems. If men like you didn't exist, if we could just all be straight with one another." He gets to his feet and heads for the door. "I fear for the future of this world, with men like you in. Reich or no Reich, Mr Manning, people like you simply complicate matters." - -> left_alone - * { not gotcomponent && not throwncomponentaway } [Confess] - I nod. "I don't need twelve minutes. -> reveal_location_of_component - * [Stay silent] -> my_lips_are_sealed - * { gotcomponent } [Show him the component] - "I don't need twelve minutes. Here it is." - I open my jacket and pull the Bombe component out of my pocket. Harris takes it from me, whistling, curious. - "Well, I'll be. That's it all right." - "That's it." - "But you didn't have it on you yesterday." - * * [Explain] - "I climbed out of the window overnight," I explain. "I went and got this from where it was hidden, and brought it back here." - * * [Don't explain] - "No. I didn't." - - -> all_too_farfetched - - * { throwncomponentaway } [Confess] - "I don't need twelve minutes. The component is in the long grass behind Hooper's tent. I threw it there hoping to somehow frame him, but now I see that won't be possible. I was naive, I suppose." - ~ piecereturned = true - -> reveal_location_of_component.harris_believes - - * { throwncomponentaway } [Frame Hooper] - "Look, I know where it is. The missing piece of the Bombe is in the long grasses behind Hooper's tent. I saw him throw it there right after we finished work. He knew you'd scour the camp but I suppose he thought you'd more obvious places first. I suppose he was right about that. Look there. That proves his guilt." - ~ longgrasshooperframe = true - ~ piecereturned = true - "That doesn't prove anything," Harris returns sharply. "But we'll check what you say, all the same." He gets to his feet and heads out of the door. - -> left_alone - - - -=== reveal_location_of_component - <> The missing component of the Bombe computer is hidden in a small cavity in a breeze—block supporting the left rear post of Hut 2. I put in there anticipating a search. I intended to { revealedhooperasculprit:pass it to Hooper|dispose of it} once the fuss had died down. I suppose I was foolish to think that it might." - ~ piecereturned = true - -> harris_believes -= harris_believes - { not night_falls.hooper_didnt_give_himself_up : - "Indeed. And Mr Manning: God help you if you're lying to me." - - else: - "I thought as much. I hadn't expected you to give it out so easily, however. You understand, Hooper has said nothing, of course. In fact, he went to Hut 2 directly after we released him and uncovered the component. But he told us you had instructed him where to go. Hence my little double bluff. Frankly, I'll be glad when I'm shot of the lot of you mathematicians." - } - Harris stands, and slips away smartly. -> left_alone - - - -=== my_lips_are_sealed - I say nothing, my lips tightly, firmly sealed. It's true I am a traitor, to the very laws of nature. The world has taught me that since a very early age. But not to my country — should the Reich win this war, I would hardly be treated as an honoured hero. I was doomed from the very start. - ~ notraitor = true - I explain none of this. How could a man like Harris understand? - The Commander takes one look back from the doorway as he pulls it to. - "It's been a pleasure working with you, Mr Manning," he declares. "You've done a great service to this country. If we come through, I'm sure they'll remember you name. I'm sorry it had to end this way and I'll do my best to keep it quiet. No—one need know what you did." - -> left_alone - - - - -=== all_too_farfetched - // Returned Component - "This is all too far—fetched," Harris says. "I'm glad to have this back, but I need to think." - Getting to his feet, he nods once. "You'll have to wait a little longer, I'm afraid, Manning." - Then he steps out of the door, muttering to himself. - -> make_your_peace - - - -=== left_alone - // Alone, about to die - { slam_door_shut_and_gone.time_to_move_now :The Commander holds the door for his superior, and follows him out.} Then the door closes. I am alone again, as I have been for most of my short life. - -> make_your_peace - - -=== make_your_peace - * [Make your peace] - - I am waiting again. I have no God to make my peace with. I find it difficult to believe in goodness of any kind, in a world such as this. - { not notraitor: - ~ notraitor = true - But I am no traitor. Not to my country. To my sex, perhaps. But how could I support the Reich? If the Nazis were to come to power, I would be worse off than ever. - } - { harris_threatens_lynching.too_clever: - In truth, it is men like Harris who are complex, not men like me. I live to make things ordered, systematic. I like my pencils sharpened and lined up in a row. I do not deal in difficult borders, or uncertainties, or alliances. If I could, I would reduce the world to something easier to understand, something finite. - But I cannot, not even here, in our little haven from the horrors of the war. - } - I have no place here. No way to fit. I am caught, in the middle, cryptic and understood only thinly, through my machines. - * I must seem very calm. - * Perhaps I should try to escape.[] But escape to where? I am already a prisoner. Jail would be a blessing. -> monastic - - <> I suppose I do not believe they will hang me. They will lock me up and continue to use my brain, if they can. I wonder what they will tell the world — perhaps that I have taken my own life. That would be simplest. The few who know me would believe it. - Well, then. Not a bad existence, in prison. Removed from temptation. - - (monastic) A monastic life, with plenty of problems to keep me going. - I wonder what else I might yet unravel before I'm done? - * The door is opening.[] Harris is returning. Our little calculation here is complete. { not piecereturned: I can only hope one of the others will be able to explain to him that the part I stole will mean nothing to the Germans.|We are just pieces in this machine; interchangeable and prone to wear.} - - That is the true secret of the calculating engine, and the source of its power. It is not the components that matter, they are quite repetitive. What matters is how they are wired; the diversity of the patterns and structures they can form. Much like people — it is how they connect that determines our victories and tragedies, and not their genius. - Which makes me wonder. Should I give { i_met_a_young_man :up my beautiful young man|the young man who put me in this spot} to them as well as myself? - * [Yes] - But of course I will. { forceful > 2:Perhaps I can persuade them to put him in my cell.|A little vengeance, disguised as doing something good.} - * [No] - No. What would be the use? He will be long gone, and the name he told me is no doubt hokum. No: I was alone before in guilt, and I am thus alone again. - * [Lie] - No. Why would I? He is no doubt an innocent himself, trapped by some dire circumstance. Forced to act the way he did. I have every sympathy for him. - Of course I do. - * [Evade] - It depends, perhaps, on what his name his worth. If it were to prove valuable, well; perhaps I can concoct a few more such lovers with which to ease my later days. - { hooper_mentioned: Hooper, perhaps. He wouldn't like that. } - - { not longgrasshooperframe : - Harris put the cuffs around my wrists. "I still have the intercept in my pocket," I remark. "Wherever we're going, could I have a pencil?" - - else: - "We recovered the part, just where you said it was," Harris reports, as he puts the cuffs around my wrists. "Of course, a couple of the men swear blind they searched there yesterday, so I'm afraid, what with the broken window... we've formed a perfectly good theory which doesn't bode well for you." - } - ~ piecereturned = true - { longgrasshooperframe : - "I see." It doesn't seem worth arguing any further. "I still have the intercept in my pocket," I remark. "Wherever we're going, could I have a pencil?" - } - He looks me in the eye. - { not losttemper : - "Of course. And one of your computing things, if I get my way. And when we're old, and smoking pipes together in The Rag like heroes, I'll explain to you the way that decent men have affairs. - - else: - "I'll give you a stone to chisel notches in the wall. And that's all the calculations you'll be doing. And as you sit there, pissing into a bucket and growing a beard down to your toes, you have a think about how a smart man would conduct his illicit affairs. With a bit of due decorum you could have learnt off any squaddie. - } - <> You scientists." - He drags me up to my feet. - "You think you have to re—invent everything." - With that, he hustles me out of the door and I can't help thinking that, with a little more strategy, I could still have won the day. But too late now, of course. - -> END diff --git a/data/ink/kaiserpunk.ink.json b/data/ink/kaiserpunk.ink.json deleted file mode 100644 index 83fe341..0000000 --- a/data/ink/kaiserpunk.ink.json +++ /dev/null @@ -1 +0,0 @@ -{"inkVersion":21,"root":[[{"->":"intro_train"},["done",{"#n":"g-0"}],null],"done",{"intro_train":[["^The train has left Vienna behind, though Vienna has not yet left you. ","#","^chapter[Eibenreith] ","/#","#","^music[Kaiserpunk Waltz.mp3](crossfade, loop, lead=8)","/#","\n","^It clings to the black gloss of your travelling boots, to the cut of your coat, to the stiff little prison of your gloves. It lives in the seal upon the letter folded inside your reticule, in the thin scent of coal smoke that has insinuated itself even into first-class upholstery, in the fact that Herr Viktor Nowak sits opposite you as if the carriage were a field office and not a compartment lined in velvet, polished wood, and brass.","\n","^Outside the window, the last outskirts of the capital have broken apart into winter-browned fields and villages with church towers too small to compete with the engine's whistle. The rails take the land without asking permission. Embankments cut through orchards. Telegraph poles pass at regular intervals, each one vanishing behind you like a thought dismissed too quickly. ","#","^sfx[steam-whistle.ogg]","/#","\n","^You had expected the train to feel like a triumph of the age.","\n","^Instead it feels like an argument. ","#","^image[suedbahn.png](landscape)","/#","\n","^The machine throws itself southward with a violence that polite society would never admit to admiring. The lamps tremble in their fittings. Your cup rattles against its saucer. Beyond the glass, the country begins to rise, first gently, then with a firmer will, until the line itself seems to negotiate with the mountains through stone arches, black tunnels, and viaducts thrown across ravines with all the confidence of imperial engineering.","\n","^Viktor has not looked impressed once.","\n","^His civilian clothes are correct enough to pass without comment: dark frock coat, sober waistcoat, gloves, collar immaculate, the posture of a man who has never truly sat at ease in his life. But no tailor can disguise discipline. It remains in his shoulders, in the economy of his movements, in the way his eyes measure doors, windows, luggage rack, corridor, your face, then the door again.","\n","^On the paperwork he is your secretary and travelling companion.","\n","^In truth, he is an officer lent to a delicate matter by channels that prefer not to be named. Rittmeister Viktor Alois Nowak, though no one at Jagdhaus Hohenreith is expected to call him that. Your hosts have asked for a medium. The Cabinet has sent them you. The military has sent him to make certain that you do not become a scandal before you become useful.","\n","^He folds the newspaper, though you are quite certain he had not been reading it.","\n","^\"You have been very quiet, gnädiges Fräulein. For a lady on her first official journey, you show remarkable restraint.\"","\n","^The form of address is technically correct if you are noble, excessive if you are not, and perfectly chosen because he does not yet know which part of you is useful, which part is costume, and which part is threat.","\n","^You look around the compartment before you answer. The answer comes from somewhere older than the letter in your reticule. It comes from the place you began.","\n","ev","str","^The compartment seems built for people who never wonder whether they belong in it.","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^You count the cost of each detail before you can stop yourself.","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^You notice first how clean everything is, and how carefully one must sit so as not to betray noticing.","/str","/ev",{"*":".^.c-2","flg":20},{"c-0":["\n","#","^class:noble","/#","ev","str","^noble","/str","/ev",{"VAR=":"birth_class","re":true},"ev",{"VAR?":"class_confidence"},2,"+",{"VAR=":"class_confidence","re":true},"/ev","ev",{"VAR?":"court_loyalty"},1,"+",{"VAR=":"court_loyalty","re":true},"/ev","^The compartment seems built for people who never wonder whether they belong in it.","\n","^It is not luxury that unsettles you. Luxury is only wood, cloth, brass, service, silence. What matters is whether the servants glance twice, whether the guard lowers his voice, whether another passenger weighs your gloves and decides not to ask your business.","\n","^You were born among people who understood such things before they understood kindness.","\n",{"->":"class_noble_background"},{"#f":5}],"c-1":["\n","#","^class:middle","/#","ev","str","^middle","/str","/ev",{"VAR=":"birth_class","re":true},"ev",{"VAR?":"class_confidence"},1,"+",{"VAR=":"class_confidence","re":true},"/ev","^You count the cost of each detail before you can stop yourself.","\n","^The upholstery, the lamps, the polished veneer, the quiet attendance at stations: none of it is magical. It is paid for. Accounted for. Itemised somewhere by someone with ink on his cuffs and a wife who knows how long candles may be burned before the household budget complains.","\n","^You were not born to this compartment, but you were born close enough to study its rules.","\n",{"->":"class_middle_background"},{"#f":5}],"c-2":["\n","#","^class:working","/#","ev","str","^working","/str","/ev",{"VAR=":"birth_class","re":true},"ev",{"VAR?":"class_confidence"},1,"-",{"VAR=":"class_confidence","re":true},"/ev","^You notice first how clean everything is, and how carefully one must sit so as not to betray noticing.","\n","^The velvet looks soft enough to swallow fingerprints. The brass fittings have been polished by hands that will never sit here. The little curtain strap is worn where other travellers, all of them more certain than you, have touched it without gratitude.","\n","^You were not born on this side of service.","\n",{"->":"class_working_background"},{"#f":5}]}],null],"class_noble_background":[["^Viktor waits for the answer owed to his remark. The train jolts once, then settles again into its hard, confident rhythm.","\n","ev","str","^\"Restraint is not a virtue, Herr Nowak. It is often only good breeding with its mouth shut.\"","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^\"You need not test whether I can sit still, Herr Nowak. I was trained by people with less patience and sharper eyes.\"","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^\"If this is remarkable restraint, Herr Nowak, I fear you have mostly escorted officers.\"","/str","/ev",{"*":".^.c-2","flg":20},{"c-0":["\n","#","^route:eccentric","/#","ev",{"VAR?":"eccentric"},1,"+",{"VAR=":"eccentric","re":true},"/ev","^\"Restraint is not a virtue, Herr Nowak. It is often only good breeding with its mouth shut.\"","\n","^His brows move almost imperceptibly.","\n","^\"Then good breeding has military applications,\" he says.","\n",{"->":"class_noble_followup_breeding"},{"#f":5}],"c-1":["\n","#","^route:detective","/#","ev",{"VAR?":"detective"},1,"+",{"VAR=":"detective","re":true},"/ev","ev",{"VAR?":"viktor_trust"},1,"+",{"VAR=":"viktor_trust","re":true},"/ev","^\"You need not test whether I can sit still, Herr Nowak. I was trained by people with less patience and sharper eyes.\"","\n","^\"A family education, then.\"","\n",{"->":"class_noble_followup_family"},{"#f":5}],"c-2":["\n","#","^route:lover","/#","ev",{"VAR?":"lover"},1,"+",{"VAR=":"lover","re":true},"/ev","ev",{"VAR?":"viktor_suspicion"},1,"+",{"VAR=":"viktor_suspicion","re":true},"/ev","^\"If this is remarkable restraint, Herr Nowak, I fear you have mostly escorted officers.\"","\n","^The corner of his mouth changes almost too little to notice.","\n","^\"Officers are less easily bored.\"","\n",{"->":"class_noble_followup_officers"},{"#f":5}]}],null],"class_noble_followup_breeding":[["ev","str","^\"Only when properly commanded.\"","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^\"Only when men mistake silence for obedience.\"","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^\"I prefer any discipline that leaves a clean record.\"","/str","/ev",{"*":".^.c-2","flg":20},{"c-0":["\n","#","^route:lover","/#","ev",{"VAR?":"lover"},1,"+",{"VAR=":"lover","re":true},"/ev","ev",{"VAR?":"viktor_suspicion"},1,"+",{"VAR=":"viktor_suspicion","re":true},"/ev","^\"Only when properly commanded.\"","\n","^\"You intend to command it yourself?\"","\n","^The answer remains in the angle of your glove and the calmness of your stare.","\n",{"->":"class_noble_explanation"},{"#f":5}],"c-1":["\n","#","^route:sapphic","/#","ev",{"VAR?":"sapphic"},1,"+",{"VAR=":"sapphic","re":true},"/ev","ev",{"VAR?":"viktor_suspicion"},1,"+",{"VAR=":"viktor_suspicion","re":true},"/ev","^\"Only when men mistake silence for obedience.\"","\n","^\"That is an ambitious distinction.\"","\n","^The distinction has preserved many women from being understood too early.","\n",{"->":"class_noble_explanation"},{"#f":5}],"c-2":["\n","#","^route:detective","/#","ev",{"VAR?":"detective"},1,"+",{"VAR=":"detective","re":true},"/ev","ev",{"VAR?":"viktor_trust"},1,"+",{"VAR=":"viktor_trust","re":true},"/ev","^\"I prefer any discipline that leaves a clean record.\"","\n","^\"A useful preference,\" he says. \"If sincere.\"","\n","^The conditional nature of sincerity remains, for the moment, his problem to investigate.","\n",{"->":"class_noble_explanation"},{"#f":5}]}],null],"class_noble_followup_family":[["ev","str","^\"A family sentence, more often.\"","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^\"An education in rooms where every chair has rank.\"","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["\n","#","^route:eccentric","/#","ev",{"VAR?":"eccentric"},1,"+",{"VAR=":"eccentric","re":true},"/ev","^\"A family sentence, more often.\"","\n","^\"You speak as if birth were a prison.\"","\n","^The polished furniture answers him better than an argument could.","\n",{"->":"class_noble_explanation"},{"#f":5}],"c-1":["\n","ev",{"VAR?":"class_confidence"},1,"+",{"VAR=":"class_confidence","re":true},"/ev","^\"An education in rooms where every chair has rank.\"","\n","^\"Then Hohenreith may not surprise you.\"","\n","^The possibility that Hohenreith may possess better secrets than chairs is allowed to stand.","\n",{"->":"class_noble_explanation"},{"#f":5}]}],null],"class_noble_followup_officers":[["ev","str","^\"Or less honest about it.\"","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^\"Then I must try not to disappoint the army.\"","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["\n","#","^route:lover","/#","ev",{"VAR?":"lover"},1,"+",{"VAR=":"lover","re":true},"/ev","^\"Or less honest about it.\"","\n","^\"You accuse the army of vanity.\"","\n","^The accusation of consistency is the least deniable one.","\n",{"->":"class_noble_explanation"},{"#f":5}],"c-1":["\n","#","^route:careless","/#","ev",{"VAR?":"careless"},1,"+",{"VAR=":"careless","re":true},"/ev","^\"Then I must try not to disappoint the army.\"","\n","^\"That is precisely what I have been asked to prevent.\"","\n",{"->":"class_noble_explanation"},{"#f":5}]}],null],"class_noble_explanation":["^You learned young that every room contains a court, even when no emperor is present. A girl of your rank is trained to enter, to bow, to be introduced, to be placed, to speak only enough, to understand more than she admits, and to know that a family name can be both a key and a chain.","\n","^Your own family possesses no grand seat, no army of retainers, no ancient right to command provinces. But your name opened drawing-room doors in Vienna, and once inside those rooms you learned to make people repeat stories they had meant only to hint at. You learned how widows speak when priests are absent, how officers lie when flattered, how old men confess when they believe themselves admired, and how a young woman may be underestimated so consistently that underestimation becomes a profession.","\n","^Your reputation as a medium did not descend from heaven. It was assembled from half-lights, correct guesses, careful silences, and the willingness of better-born fools to mistake performance for revelation.","\n","^Before the court could use you, society had first to invent you.","\n","^Vienna knew you by the name society had made useful.","\n",{"->":"choose_name_noble"},null],"class_middle_background":[["^Viktor's remark lingers between you with the odour of a polite accusation.","\n","ev","str","^\"Restraint is easier when one has learned that every mistake is remembered by someone better placed.\"","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^\"If I am quiet, Herr Nowak, it is because men explain themselves faster when they dislike the silence.\"","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^\"I was considering whether your concern is official, personal, or merely masculine.\"","/str","/ev",{"*":".^.c-2","flg":20},{"c-0":["\n","#","^route:detective","/#","ev",{"VAR?":"detective"},1,"+",{"VAR=":"detective","re":true},"/ev","ev",{"VAR?":"viktor_trust"},1,"+",{"VAR=":"viktor_trust","re":true},"/ev","^\"Restraint is easier when one has learned that every mistake is remembered by someone better placed.\"","\n","^Viktor watches you more closely.","\n","^\"A bitter lesson.\"","\n",{"->":"class_middle_followup_bitter"},{"#f":5}],"c-1":["\n","#","^route:lover","/#","ev",{"VAR?":"lover"},1,"+",{"VAR=":"lover","re":true},"/ev","ev",{"VAR?":"medium_reputation"},1,"+",{"VAR=":"medium_reputation","re":true},"/ev","^\"If I am quiet, Herr Nowak, it is because men explain themselves faster when they dislike the silence.\"","\n","^\"A method?\"","\n",{"->":"class_middle_followup_method"},{"#f":5}],"c-2":["\n","#","^route:eccentric","/#","ev",{"VAR?":"eccentric"},1,"+",{"VAR=":"eccentric","re":true},"/ev","ev",{"VAR?":"viktor_suspicion"},1,"+",{"VAR=":"viktor_suspicion","re":true},"/ev","^\"I was considering whether your concern is official, personal, or merely masculine.\"","\n","^His eyes harden by one exact degree.","\n","^\"Today it is official.\"","\n",{"->":"class_middle_followup_official"},{"#f":5}]}],null],"class_middle_followup_bitter":[["ev","str","^\"A useful one. Bitterness is merely the taste left by instruction.\"","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^\"A common one. Some people only notice injustice when it reaches their own floor.\"","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["\n","#","^route:eccentric","/#","ev",{"VAR?":"eccentric"},1,"+",{"VAR=":"eccentric","re":true},"/ev","^\"A useful one. Bitterness is merely the taste left by instruction.\"","\n","^\"You collect phrases like weapons.\"","\n","^The phrase lies lightly enough that he must decide whether it is ornament or weapon.","\n",{"->":"class_middle_explanation"},{"#f":5}],"c-1":["\n","#","^route:sapphic","/#","ev",{"VAR?":"sapphic"},1,"+",{"VAR=":"sapphic","re":true},"/ev","^\"A common one. Some people only notice injustice when it reaches their own floor.\"","\n","^\"You have made a study of floors?\"","\n","^Thresholds, you have learned, are more honest than floors; they admit that passage is a privilege.","\n",{"->":"class_middle_explanation"},{"#f":5}]}],null],"class_middle_followup_method":[["ev","str","^\"A courtesy. I let them begin with their favorite subject.\"","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^\"An experiment. It has produced reliable results.\"","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["\n","#","^route:lover","/#","ev",{"VAR?":"lover"},1,"+",{"VAR=":"lover","re":true},"/ev","^\"A courtesy. I let them begin with their favorite subject.\"","\n","^\"Themselves.\"","\n","^His own answer completes the principle neatly enough that further instruction would be vanity.","\n",{"->":"class_middle_explanation"},{"#f":5}],"c-1":["\n","#","^route:detective","/#","ev",{"VAR?":"detective"},1,"+",{"VAR=":"detective","re":true},"/ev","^\"An experiment. It has produced reliable results.\"","\n","^\"Then I am part of your experiment.\"","\n","^He has sat opposite you long enough to become evidence.","\n",{"->":"class_middle_explanation"},{"#f":5}]}],null],"class_middle_followup_official":[["ev","str","^\"How convenient. The other two may deny responsibility.\"","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^\"Then I shall treat it with the respect due to paper.\"","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["\n","#","^route:eccentric","/#","ev",{"VAR?":"eccentric"},1,"+",{"VAR=":"eccentric","re":true},"/ev","^\"How convenient. The other two may deny responsibility.\"","\n","^\"I advise you not to make wit your first instrument at Hohenreith.\"","\n","^The demotion of wit to second instrument remains sufficiently theoretical to be safe.","\n",{"->":"class_middle_explanation"},{"#f":5}],"c-1":["\n","#","^route:detective","/#","ev",{"VAR?":"detective"},1,"+",{"VAR=":"detective","re":true},"/ev","^\"Then I shall treat it with the respect due to paper.\"","\n","^\"Paper has moved armies.\"","\n","^Paper has also buried mistakes, but not all corrections need to be spoken aloud.","\n",{"->":"class_middle_explanation"},{"#f":5}]}],null],"class_middle_explanation":["^You were born in that broad, anxious territory between deference and ambition. Your family had books, invoices, respectability, perhaps a piano no one played well enough, perhaps a father with an office, a mother with callers, brothers who were expected to advance, and daughters who were expected not to make advancement look hungry.","\n","^You learned accounts before etiquette, etiquette before French, and French before you learned how easily a woman with a calm voice could make men explain themselves. You rose because you listened. You rose because you understood that fraud, faith, medicine, gossip, politics, and grief all use the same doors into the human mind.","\n","^The court does not like to admit that it needs middle-class competence. It prefers to borrow it, dress it properly, and call it discretion.","\n","^Your reputation as a medium gave them a word that sounded less dangerous than investigator.","\n","^The salons that first laughed at you, then invited you back, learned your name before they learned what it cost.","\n",{"->":"choose_name_middle"},null],"class_working_background":[["^Viktor's courtesy is smooth enough to be handled without fingerprints. You hear, beneath it, the question of how much this compartment has improved you.","\n","ev","str","^\"Restraint is what people praise when they prefer not to see the effort.\"","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^\"I am quiet because people often prefer women of my origin either grateful or invisible.\"","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^\"I was trying not to touch the upholstery as though it might accuse me.\"","/str","/ev",{"*":".^.c-2","flg":20},{"c-0":["\n","#","^route:detective","/#","ev",{"VAR?":"detective"},1,"+",{"VAR=":"detective","re":true},"/ev","ev",{"VAR?":"viktor_trust"},1,"+",{"VAR=":"viktor_trust","re":true},"/ev","^\"Restraint is what people praise when they prefer not to see the effort.\"","\n","^The newspaper in Viktor's hand creases once.","\n","^\"You object to being praised?\"","\n",{"->":"class_working_followup_praise"},{"#f":5}],"c-1":["\n","#","^route:eccentric","/#","ev",{"VAR?":"eccentric"},1,"+",{"VAR=":"eccentric","re":true},"/ev","ev",{"VAR?":"viktor_suspicion"},1,"+",{"VAR=":"viktor_suspicion","re":true},"/ev","^\"I am quiet because people often prefer women of my origin either grateful or invisible.\"","\n","^\"I did not ask you to be grateful.\"","\n",{"->":"class_working_followup_grateful"},{"#f":5}],"c-2":["\n","#","^route:careless","/#","ev",{"VAR?":"careless"},1,"+",{"VAR=":"careless","re":true},"/ev","ev","str","^dependence","/str","/ev",{"VAR=":"viktor_relation","re":true},"^\"I was trying not to touch the upholstery as though it might accuse me.\"","\n","^Something like concern crosses his face, disguised too late as irritation.","\n","^\"The upholstery has survived ministers. It will survive you.\"","\n",{"->":"class_working_followup_upholstery"},{"#f":5}]}],null],"class_working_followup_praise":[["ev","str","^\"Only cheaply.\"","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^\"Only when it hides the person doing the work.\"","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["\n","#","^route:eccentric","/#","ev",{"VAR?":"eccentric"},1,"+",{"VAR=":"eccentric","re":true},"/ev","^\"Only cheaply.\"","\n","^\"That may be difficult to avoid.\"","\n","^Hohenreith, if it intends to praise you cheaply, will have to discover the economy of disappointment.","\n",{"->":"class_working_explanation"},{"#f":5}],"c-1":["\n","#","^route:sapphic","/#","ev",{"VAR?":"sapphic"},1,"+",{"VAR=":"sapphic","re":true},"/ev","^\"Only when it hides the person doing the work.\"","\n","^He studies you as if the answer has come from farther down the train than first class.","\n",{"->":"class_working_explanation"},{"#f":5}]}],null],"class_working_followup_grateful":[["ev","str","^\"No. You asked me to be manageable.\"","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^\"Then I shall postpone gratitude until you deserve it.\"","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["\n","#","^route:eccentric","/#","ev",{"VAR?":"eccentric"},1,"+",{"VAR=":"eccentric","re":true},"/ev","^\"No. You asked me to be manageable.\"","\n","^\"I asked you nothing of the kind.\"","\n","^Rank, unlike men, rarely needs to ask directly. The furniture asks for it.","\n",{"->":"class_working_explanation"},{"#f":5}],"c-1":["\n","#","^route:lover","/#","ev",{"VAR?":"lover"},1,"+",{"VAR=":"lover","re":true},"/ev","^\"Then I shall postpone gratitude until you deserve it.\"","\n","^A pause. Then, very dryly: \"A generous arrangement.\"","\n",{"->":"class_working_explanation"},{"#f":5}]}],null],"class_working_followup_upholstery":[["ev","str","^\"Then I am already braver than I was.\"","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^\"I shall take that as reassurance, though you delivered it like a reprimand.\"","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["\n","#","^route:careless","/#","ev",{"VAR?":"careless"},1,"+",{"VAR=":"careless","re":true},"/ev","^\"Then I am already braver than I was.\"","\n","^\"Courage measured against upholstery is not a military standard.\"","\n",{"->":"class_working_explanation"},{"#f":5}],"c-1":["\n","#","^route:lover","/#","ev",{"VAR?":"lover"},1,"+",{"VAR=":"lover","re":true},"/ev","^\"I shall take that as reassurance, though you delivered it like a reprimand.\"","\n","^\"I have had practice with both.\"","\n",{"->":"class_working_explanation"},{"#f":5}]}],null],"class_working_explanation":["^You were born among people who owned little but obligations. Work had a sound before it had a meaning: water, broom, bootsteps, breath, the clatter of dishes, the cough of men coming in from cold yards, women counting coins under their breath. You learned early that the high-born are not more observant than others. They are merely less often required to observe.","\n","^That was your first advantage.","\n","^A servant knows which door matters because she uses the others. A seamstress learns bodies because she measures them. A maid learns secrets because fine people leave their souls lying about like gloves, certain that no one beneath them has hands.","\n","^You rose by talent, patronage, imitation, nerve, and the terrible convenience of being believed harmless. By the time Vienna began whispering that you saw more than respectable people saw, you had already spent years seeing what respectable people missed.","\n","^The court has placed you in first class because it needs what birth did not give you.","\n","^The name you carried upward was altered perhaps in pronunciation, never quite cleansed of where it began.","\n",{"->":"choose_name_working"},null],"choose_name_noble":[["ev","str","^Valerie Eleonore Josepha","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Helene Cäcilie Franziska","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Clara Theresia Leopoldine","/str","/ev",{"*":".^.c-2","flg":20},"ev","str","^Sophie Eleonore Auguste","/str","/ev",{"*":".^.c-3","flg":20},"ev","str","^Mathilde Josepha Henriette","/str","/ev",{"*":".^.c-4","flg":20},"ev","str","^Therese Valerie Franziska","/str","/ev",{"*":".^.c-5","flg":20},"ev","str","^Ilona Theresia Eleonore","/str","/ev",{"*":".^.c-6","flg":20},"ev","str","^Zdenka Eleonore Josepha","/str","/ev",{"*":".^.c-7","flg":20},{"c-0":["\n","ev","str","^Valerie Eleonore Josepha","/str","/ev",{"VAR=":"given_names","re":true},"ev","str","^Valerie","/str","/ev",{"VAR=":"common_name","re":true},{"->":"choose_surname_noble"},{"#f":5}],"c-1":["\n","ev","str","^Helene Cäcilie Franziska","/str","/ev",{"VAR=":"given_names","re":true},"ev","str","^Helene","/str","/ev",{"VAR=":"common_name","re":true},{"->":"choose_surname_noble"},{"#f":5}],"c-2":["\n","ev","str","^Clara Theresia Leopoldine","/str","/ev",{"VAR=":"given_names","re":true},"ev","str","^Clara","/str","/ev",{"VAR=":"common_name","re":true},{"->":"choose_surname_noble"},{"#f":5}],"c-3":["\n","ev","str","^Sophie Eleonore Auguste","/str","/ev",{"VAR=":"given_names","re":true},"ev","str","^Sophie","/str","/ev",{"VAR=":"common_name","re":true},{"->":"choose_surname_noble"},{"#f":5}],"c-4":["\n","ev","str","^Mathilde Josepha Henriette","/str","/ev",{"VAR=":"given_names","re":true},"ev","str","^Mathilde","/str","/ev",{"VAR=":"common_name","re":true},{"->":"choose_surname_noble"},{"#f":5}],"c-5":["\n","ev","str","^Therese Valerie Franziska","/str","/ev",{"VAR=":"given_names","re":true},"ev","str","^Therese","/str","/ev",{"VAR=":"common_name","re":true},{"->":"choose_surname_noble"},{"#f":5}],"c-6":["\n","ev","str","^Ilona Theresia Eleonore","/str","/ev",{"VAR=":"given_names","re":true},"ev","str","^Ilona","/str","/ev",{"VAR=":"common_name","re":true},{"->":"choose_surname_noble"},{"#f":5}],"c-7":["\n","ev","str","^Zdenka Eleonore Josepha","/str","/ev",{"VAR=":"given_names","re":true},"ev","str","^Zdenka","/str","/ev",{"VAR=":"common_name","re":true},{"->":"choose_surname_noble"},{"#f":5}]}],null],"choose_surname_noble":[["^Your title is fixed by birth and by the careful modesty of your family: not countess, not princess, not one of the brilliant names that gather ambassadors and creditors like dust.","\n","^A Freiin. Baronial. Usable. Admitted, but not enthroned.","\n","ev","str","^Freiin von Rauhenfels","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Freiin von Traunegg","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Freiin von Ebenwald","/str","/ev",{"*":".^.c-2","flg":20},"ev","str","^Freiin von Arnsberg","/str","/ev",{"*":".^.c-3","flg":20},"ev","str","^Freiin von Reichenau","/str","/ev",{"*":".^.c-4","flg":20},"ev","str","^Freiin von Waldstätten","/str","/ev",{"*":".^.c-5","flg":20},{"c-0":["\n","ev","str","^Freiin von","/str","/ev",{"VAR=":"title_part","re":true},"ev","str","^Rauhenfels","/str","/ev",{"VAR=":"surname","re":true},{"->":"assemble_full_name"},{"#f":5}],"c-1":["\n","ev","str","^Freiin von","/str","/ev",{"VAR=":"title_part","re":true},"ev","str","^Traunegg","/str","/ev",{"VAR=":"surname","re":true},{"->":"assemble_full_name"},{"#f":5}],"c-2":["\n","ev","str","^Freiin von","/str","/ev",{"VAR=":"title_part","re":true},"ev","str","^Ebenwald","/str","/ev",{"VAR=":"surname","re":true},{"->":"assemble_full_name"},{"#f":5}],"c-3":["\n","ev","str","^Freiin von","/str","/ev",{"VAR=":"title_part","re":true},"ev","str","^Arnsberg","/str","/ev",{"VAR=":"surname","re":true},{"->":"assemble_full_name"},{"#f":5}],"c-4":["\n","ev","str","^Freiin von","/str","/ev",{"VAR=":"title_part","re":true},"ev","str","^Reichenau","/str","/ev",{"VAR=":"surname","re":true},{"->":"assemble_full_name"},{"#f":5}],"c-5":["\n","ev","str","^Freiin von","/str","/ev",{"VAR=":"title_part","re":true},"ev","str","^Waldstätten","/str","/ev",{"VAR=":"surname","re":true},{"->":"assemble_full_name"},{"#f":5}]}],null],"choose_name_middle":[["ev","str","^Clara Eleonore","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Anna Katharina","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Helene Theresia","/str","/ev",{"*":".^.c-2","flg":20},"ev","str","^Rosa Franziska","/str","/ev",{"*":".^.c-3","flg":20},"ev","str","^Johanna Elise","/str","/ev",{"*":".^.c-4","flg":20},"ev","str","^Katharina Sophie","/str","/ev",{"*":".^.c-5","flg":20},"ev","str","^Therese Leopoldine","/str","/ev",{"*":".^.c-6","flg":20},"ev","str","^Magdalena Cäcilie","/str","/ev",{"*":".^.c-7","flg":20},{"c-0":["\n","ev","str","^Clara Eleonore","/str","/ev",{"VAR=":"given_names","re":true},"ev","str","^Clara","/str","/ev",{"VAR=":"common_name","re":true},{"->":"choose_surname_middle"},{"#f":5}],"c-1":["\n","ev","str","^Anna Katharina","/str","/ev",{"VAR=":"given_names","re":true},"ev","str","^Anna","/str","/ev",{"VAR=":"common_name","re":true},{"->":"choose_surname_middle"},{"#f":5}],"c-2":["\n","ev","str","^Helene Theresia","/str","/ev",{"VAR=":"given_names","re":true},"ev","str","^Helene","/str","/ev",{"VAR=":"common_name","re":true},{"->":"choose_surname_middle"},{"#f":5}],"c-3":["\n","ev","str","^Rosa Franziska","/str","/ev",{"VAR=":"given_names","re":true},"ev","str","^Rosa","/str","/ev",{"VAR=":"common_name","re":true},{"->":"choose_surname_middle"},{"#f":5}],"c-4":["\n","ev","str","^Johanna Elise","/str","/ev",{"VAR=":"given_names","re":true},"ev","str","^Johanna","/str","/ev",{"VAR=":"common_name","re":true},{"->":"choose_surname_middle"},{"#f":5}],"c-5":["\n","ev","str","^Katharina Sophie","/str","/ev",{"VAR=":"given_names","re":true},"ev","str","^Katharina","/str","/ev",{"VAR=":"common_name","re":true},{"->":"choose_surname_middle"},{"#f":5}],"c-6":["\n","ev","str","^Therese Leopoldine","/str","/ev",{"VAR=":"given_names","re":true},"ev","str","^Therese","/str","/ev",{"VAR=":"common_name","re":true},{"->":"choose_surname_middle"},{"#f":5}],"c-7":["\n","ev","str","^Magdalena Cäcilie","/str","/ev",{"VAR=":"given_names","re":true},"ev","str","^Magdalena","/str","/ev",{"VAR=":"common_name","re":true},{"->":"choose_surname_middle"},{"#f":5}]}],null],"choose_surname_middle":[["^Your family name contains no particle to soften the ascent. It must stand upright by itself.","\n","ev","str","^Leitner","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Wagner","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Kellner","/str","/ev",{"*":".^.c-2","flg":20},"ev","str","^Baumgartner","/str","/ev",{"*":".^.c-3","flg":20},"ev","str","^Fischer","/str","/ev",{"*":".^.c-4","flg":20},"ev","str","^Schmid","/str","/ev",{"*":".^.c-5","flg":20},"ev","str","^Pichler","/str","/ev",{"*":".^.c-6","flg":20},"ev","str","^Rosenfeld","/str","/ev",{"*":".^.c-7","flg":20},{"c-0":["\n","ev","str","^Fräulein","/str","/ev",{"VAR=":"title_part","re":true},"ev","str","^Leitner","/str","/ev",{"VAR=":"surname","re":true},{"->":"assemble_full_name"},{"#f":5}],"c-1":["\n","ev","str","^Fräulein","/str","/ev",{"VAR=":"title_part","re":true},"ev","str","^Wagner","/str","/ev",{"VAR=":"surname","re":true},{"->":"assemble_full_name"},{"#f":5}],"c-2":["\n","ev","str","^Fräulein","/str","/ev",{"VAR=":"title_part","re":true},"ev","str","^Kellner","/str","/ev",{"VAR=":"surname","re":true},{"->":"assemble_full_name"},{"#f":5}],"c-3":["\n","ev","str","^Fräulein","/str","/ev",{"VAR=":"title_part","re":true},"ev","str","^Baumgartner","/str","/ev",{"VAR=":"surname","re":true},{"->":"assemble_full_name"},{"#f":5}],"c-4":["\n","ev","str","^Fräulein","/str","/ev",{"VAR=":"title_part","re":true},"ev","str","^Fischer","/str","/ev",{"VAR=":"surname","re":true},{"->":"assemble_full_name"},{"#f":5}],"c-5":["\n","ev","str","^Fräulein","/str","/ev",{"VAR=":"title_part","re":true},"ev","str","^Schmid","/str","/ev",{"VAR=":"surname","re":true},{"->":"assemble_full_name"},{"#f":5}],"c-6":["\n","ev","str","^Fräulein","/str","/ev",{"VAR=":"title_part","re":true},"ev","str","^Pichler","/str","/ev",{"VAR=":"surname","re":true},{"->":"assemble_full_name"},{"#f":5}],"c-7":["\n","ev","str","^Fräulein","/str","/ev",{"VAR=":"title_part","re":true},"ev","str","^Rosenfeld","/str","/ev",{"VAR=":"surname","re":true},{"->":"assemble_full_name"},{"#f":5}]}],null],"choose_name_working":[["ev","str","^Anna","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Klara","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Agnes","/str","/ev",{"*":".^.c-2","flg":20},"ev","str","^Leni","/str","/ev",{"*":".^.c-3","flg":20},"ev","str","^Rosa","/str","/ev",{"*":".^.c-4","flg":20},"ev","str","^Gertrud","/str","/ev",{"*":".^.c-5","flg":20},"ev","str","^Elisabeth","/str","/ev",{"*":".^.c-6","flg":20},"ev","str","^Franziska","/str","/ev",{"*":".^.c-7","flg":20},{"c-0":["\n","ev","str","^Anna","/str","/ev",{"VAR=":"given_names","re":true},"ev","str","^Anna","/str","/ev",{"VAR=":"common_name","re":true},{"->":"choose_surname_working"},{"#f":5}],"c-1":["\n","ev","str","^Klara","/str","/ev",{"VAR=":"given_names","re":true},"ev","str","^Klara","/str","/ev",{"VAR=":"common_name","re":true},{"->":"choose_surname_working"},{"#f":5}],"c-2":["\n","ev","str","^Agnes","/str","/ev",{"VAR=":"given_names","re":true},"ev","str","^Agnes","/str","/ev",{"VAR=":"common_name","re":true},{"->":"choose_surname_working"},{"#f":5}],"c-3":["\n","ev","str","^Leni","/str","/ev",{"VAR=":"given_names","re":true},"ev","str","^Leni","/str","/ev",{"VAR=":"common_name","re":true},{"->":"choose_surname_working"},{"#f":5}],"c-4":["\n","ev","str","^Rosa","/str","/ev",{"VAR=":"given_names","re":true},"ev","str","^Rosa","/str","/ev",{"VAR=":"common_name","re":true},{"->":"choose_surname_working"},{"#f":5}],"c-5":["\n","ev","str","^Gertrud","/str","/ev",{"VAR=":"given_names","re":true},"ev","str","^Gertrud","/str","/ev",{"VAR=":"common_name","re":true},{"->":"choose_surname_working"},{"#f":5}],"c-6":["\n","ev","str","^Elisabeth","/str","/ev",{"VAR=":"given_names","re":true},"ev","str","^Elisabeth","/str","/ev",{"VAR=":"common_name","re":true},{"->":"choose_surname_working"},{"#f":5}],"c-7":["\n","ev","str","^Franziska","/str","/ev",{"VAR=":"given_names","re":true},"ev","str","^Franziska","/str","/ev",{"VAR=":"common_name","re":true},{"->":"choose_surname_working"},{"#f":5}]}],null],"choose_surname_working":[["^A simple name can be a burden in Vienna. It tells people how little they must pretend to respect you before you have spoken.","\n","ev","str","^Pichler","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Huber","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Maier","/str","/ev",{"*":".^.c-2","flg":20},"ev","str","^Gruber","/str","/ev",{"*":".^.c-3","flg":20},"ev","str","^Schuster","/str","/ev",{"*":".^.c-4","flg":20},"ev","str","^Krenn","/str","/ev",{"*":".^.c-5","flg":20},"ev","str","^Wolf","/str","/ev",{"*":".^.c-6","flg":20},"ev","str","^Moser","/str","/ev",{"*":".^.c-7","flg":20},{"c-0":["\n","ev","str","^Fräulein","/str","/ev",{"VAR=":"title_part","re":true},"ev","str","^Pichler","/str","/ev",{"VAR=":"surname","re":true},{"->":"assemble_full_name"},{"#f":5}],"c-1":["\n","ev","str","^Fräulein","/str","/ev",{"VAR=":"title_part","re":true},"ev","str","^Huber","/str","/ev",{"VAR=":"surname","re":true},{"->":"assemble_full_name"},{"#f":5}],"c-2":["\n","ev","str","^Fräulein","/str","/ev",{"VAR=":"title_part","re":true},"ev","str","^Maier","/str","/ev",{"VAR=":"surname","re":true},{"->":"assemble_full_name"},{"#f":5}],"c-3":["\n","ev","str","^Fräulein","/str","/ev",{"VAR=":"title_part","re":true},"ev","str","^Gruber","/str","/ev",{"VAR=":"surname","re":true},{"->":"assemble_full_name"},{"#f":5}],"c-4":["\n","ev","str","^Fräulein","/str","/ev",{"VAR=":"title_part","re":true},"ev","str","^Schuster","/str","/ev",{"VAR=":"surname","re":true},{"->":"assemble_full_name"},{"#f":5}],"c-5":["\n","ev","str","^Fräulein","/str","/ev",{"VAR=":"title_part","re":true},"ev","str","^Krenn","/str","/ev",{"VAR=":"surname","re":true},{"->":"assemble_full_name"},{"#f":5}],"c-6":["\n","ev","str","^Fräulein","/str","/ev",{"VAR=":"title_part","re":true},"ev","str","^Wolf","/str","/ev",{"VAR=":"surname","re":true},{"->":"assemble_full_name"},{"#f":5}],"c-7":["\n","ev","str","^Fräulein","/str","/ev",{"VAR=":"title_part","re":true},"ev","str","^Moser","/str","/ev",{"VAR=":"surname","re":true},{"->":"assemble_full_name"},{"#f":5}]}],null],"assemble_full_name":["ev",{"VAR?":"birth_class"},"str","^noble","/str","==","/ev",[{"->":".^.b","c":true},{"b":["\n","^On visiting cards, in letters, in the cautious mouths of servants, you are ","ev",{"VAR?":"given_names"},"out","/ev","^ ","ev",{"VAR?":"title_part"},"out","/ev","^ ","ev",{"VAR?":"surname"},"out","/ev","^.","\n",{"->":".^.^.^.9"},null]}],[{"->":".^.b"},{"b":["\n","^On railway documents, hotel ledgers, and the tongues of people who have not yet decided how much respect you deserve, you are ","ev",{"VAR?":"title_part"},"out","/ev","^ ","ev",{"VAR?":"given_names"},"out","/ev","^ ","ev",{"VAR?":"surname"},"out","/ev","^.","\n",{"->":".^.^.^.9"},null]}],"nop","\n","^But in the private chamber where a name is first answered before it is performed, you are ","ev",{"VAR?":"common_name"},"out","/ev","^.","\n","^Viktor has waited through your silence with a soldier's patience and a jailer's courtesy. The train enters another tunnel. For several seconds the compartment window gives you back only your own reflection: your hat, your pale face above the dark collar, your eyes too steady or not steady enough.","\n","^When the mountains return, they seem closer.","\n",{"->":"mirror_definition"},null],"mirror_definition":[["^The black glass gives you the woman who will arrive in Eibenreith before any rumour has time to improve her.","\n","^The window catches one thing before the rest.","\n","ev","str","^Dark ash-brown hair pinned with almost severe care.","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Chestnut hair arranged to look softer than the mind beneath it.","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Cool fair hair made austere by dark travelling clothes.","/str","/ev",{"*":".^.c-2","flg":20},"ev","str","^Black-brown hair with a few escaped wisps already refusing discipline.","/str","/ev",{"*":".^.c-3","flg":20},{"c-0":["\n","ev","str","^dark ash-brown","/str","/ev",{"VAR=":"hair_detail","re":true},"^The reflection has dark ash-brown hair, almost black where the tunnel has not quite left it, pinned with the severity of a woman who knows that disarray is forgiven less easily in the young.","\n",{"->":"mirror_complexion"},{"#f":5}],"c-1":["\n","ev","str","^chestnut","/str","/ev",{"VAR=":"hair_detail","re":true},"^The reflection has chestnut hair, warm where the lamp touches it, arranged with enough softness to flatter and enough control to warn the attentive.","\n",{"->":"mirror_complexion"},{"#f":5}],"c-2":["\n","ev","str","^cool fair","/str","/ev",{"VAR=":"hair_detail","re":true},"^The reflection has cool fair hair, not golden enough for sentimental painters, but pale enough that the dark travelling clothes make your face appear more deliberate than gentle.","\n",{"->":"mirror_complexion"},{"#f":5}],"c-3":["\n","ev","str","^black-brown","/str","/ev",{"VAR=":"hair_detail","re":true},"^The reflection has black-brown hair, glossy in the lamp's weak tremor, with two escaped wisps at the temple already committing small treasons against the pins.","\n",{"->":"mirror_complexion"},{"#f":5}]}],null],"mirror_complexion":[["^The window deepens, and with it the face.","\n","ev","str","^A fair, cool complexion that looks almost bloodless in railway light.","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^A clear complexion that still remembers the city more than the sun.","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^A face that has learned to look fragile when fragility is useful.","/str","/ev",{"*":".^.c-2","flg":20},"ev","str","^A slightly warmer complexion that makes the severity of the outfit less forgiving.","/str","/ev",{"*":".^.c-3","flg":20},{"c-0":["\n","ev","str","^fair and cool","/str","/ev",{"VAR=":"complexion_detail","re":true},"^Your complexion is fair and cool, made paler by smoke, glass, and the faint greenish cast of mountain light.","\n",{"->":"mirror_face"},{"#f":5}],"c-1":["\n","ev","str","^clear and sheltered","/str","/ev",{"VAR=":"complexion_detail","re":true},"^Your complexion has the clarity of rooms, gloves, and shaded streets; not sickly, but visibly protected from the labour that browns other lives.","\n",{"->":"mirror_face"},{"#f":5}],"c-2":["\n","ev","str","^delicately pale","/str","/ev",{"VAR=":"complexion_detail","re":true},"^Your complexion is delicately pale, the sort physicians and foolish men read too eagerly, and which you have never felt obliged to correct every time.","\n",{"->":"mirror_face"},{"#f":5}],"c-3":["\n","ev","str","^warm fair","/str","/ev",{"VAR=":"complexion_detail","re":true},"^Your complexion is warmer than Vienna's winter light would prefer, and that warmth makes the severe coat and collar seem chosen rather than imposed.","\n",{"->":"mirror_face"},{"#f":5}]}],null],"mirror_face":[["^The window keeps you just long enough to make judgement impolite.","\n","ev","str","^A long oval face, observant before it is beautiful.","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^A composed face that seems trained equally for salons and interrogations.","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^A pretty face made less harmless by the steadiness of the eyes.","/str","/ev",{"*":".^.c-2","flg":20},"ev","str","^A severe face that becomes almost vulnerable only when caught unprepared.","/str","/ev",{"*":".^.c-3","flg":20},{"c-0":["\n","ev","str","^long oval and observant","/str","/ev",{"VAR=":"face_detail","re":true},"^The face is long and oval, with watchful eyes, a straight nose, and a mouth that knows how often women are punished for amusement before men call them clever.","\n",{"->":"mirror_outfit"},{"#f":5}],"c-1":["\n","ev","str","^composed and trained","/str","/ev",{"VAR=":"face_detail","re":true},"^The face is composed rather than soft, with brows dark enough to sharpen silence and a mouth whose politeness has not yet promised mercy.","\n",{"->":"mirror_outfit"},{"#f":5}],"c-2":["\n","ev","str","^pretty but steady","/str","/ev",{"VAR=":"face_detail","re":true},"^The face might be called pretty by people who do not like to work harder for adjectives, but the eyes disturb the compliment by appearing to have heard it before and found it insufficient.","\n",{"->":"mirror_outfit"},{"#f":5}],"c-3":["\n","ev","str","^severe and guarded","/str","/ev",{"VAR=":"face_detail","re":true},"^The face is severe at first glance, guarded at the second, and only after that does it betray how young a woman may still be while carrying herself like a sealed document.","\n",{"->":"mirror_outfit"},{"#f":5}]}],null],"mirror_outfit":[["^The rest of the reflection is costume, armour, and evidence.","\n","ev","str","^Dark charcoal-plum travelling wool, restrained but expensive.","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^A severe black-brown coat and skirt, softened only by ivory at throat and cuffs.","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Bottle-green details hidden in an otherwise sombre travelling suit.","/str","/ev",{"*":".^.c-2","flg":20},"ev","str","^A more fashionable ensemble, dark and narrow, calculated to be remembered.","/str","/ev",{"*":".^.c-3","flg":20},{"c-0":["\n","ev","str","^charcoal-plum travelling wool","/str","/ev",{"VAR=":"outfit_detail","re":true},"^You wear a tailored travelling ensemble of dark charcoal wool with a plum undertone, high at the throat, close at the waist, expensive in the way that avoids asking to be admired.","\n",{"->":"supernatural_stance"},{"#f":5}],"c-1":["\n","ev","str","^black-brown travelling suit","/str","/ev",{"VAR=":"outfit_detail","re":true},"^You wear a black-brown travelling suit and long coat, softened only by ivory at throat and cuffs, the lace narrow enough to seem like restraint rather than decoration.","\n",{"->":"supernatural_stance"},{"#f":5}],"c-2":["\n","ev","str","^sombre suit with bottle-green details","/str","/ev",{"VAR=":"outfit_detail","re":true},"^You wear a sombre travelling suit with bottle-green details so discreet that only close attention discovers them, which is nearly the point.","\n",{"->":"supernatural_stance"},{"#f":5}],"c-3":["\n","ev","str","^fashionable dark travelling ensemble","/str","/ev",{"VAR=":"outfit_detail","re":true},"^You wear a dark, narrow, more fashionable travelling ensemble, the hat and veil chosen with just enough theatrical instinct to make sceptical people use the word instinctive.","\n",{"->":"supernatural_stance"},{"#f":5}]}],null],"supernatural_stance":[["^The letter of commission in your reticule does not call you an investigator.","\n","^It calls you, in prose dry enough to pass through any number of offices, a woman whose unusual spiritual reputation has recommended her to a delicate household matter. The phrasing is exquisite. It neither affirms nor denies. It permits everyone involved to believe afterward that they had believed nothing improper.","\n","^The comital family at Jagdhaus Hohenreith has asked for discretion. Vienna has answered with a sealed letter, a woman reputed to speak with what is hidden, and a man opposite her who has orders of his own.","\n","^Before this journey, before this train, before the mountains began taking the sky piece by piece, belief had already taken its position in you.","\n","ev","str","^The dead are not silent. The living are merely poor listeners.","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^The supernatural is usually pain, fraud, fever, inheritance, or bad ventilation.","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Belief is a costume. You wear it because men insist on dressing you in it.","/str","/ev",{"*":".^.c-2","flg":20},"ev","str","^You have learned not to decide too early.","/str","/ev",{"*":".^.c-3","flg":20},{"c-0":["\n","#","^supernatural:believer","/#","ev","str","^believer","/str","/ev",{"VAR=":"supernatural_belief","re":true},"ev",{"VAR?":"medium_reputation"},1,"+",{"VAR=":"medium_reputation","re":true},"/ev","ev",{"VAR?":"supernatural_exposure"},1,"+",{"VAR=":"supernatural_exposure","re":true},"/ev","^The dead are not silent. The living are merely poor listeners.","\n","^You have always thought disbelief a provincial arrogance of the educated. There are pressures in rooms where grief has been. There are words people speak before they know they have spoken. There are dreams that arrive with mud on their hems.","\n","^Perhaps the world is not haunted. Perhaps it is simply crowded.","\n",{"->":"spiritual_senses"},{"#f":5}],"c-1":["\n","#","^supernatural:sceptic","/#","#","^route:detective","/#","ev","str","^sceptic","/str","/ev",{"VAR=":"supernatural_belief","re":true},"ev",{"VAR?":"detective"},1,"+",{"VAR=":"detective","re":true},"/ev","^The supernatural is usually pain, fraud, fever, inheritance, or bad ventilation.","\n","^The word spirit covers too much and explains too little. You have watched respectable people call an echo a message, a coincidence a sign, a trembling hand an angelic visitation. Men of science can be fools, but fools with candles and planchettes are no improvement.","\n","^If Hohenreith has ghosts, you expect them to keep accounts, write letters, leave footprints, and benefit someone.","\n",{"->":"spiritual_senses"},{"#f":5}],"c-2":["\n","#","^supernatural:performer","/#","ev","str","^performer","/str","/ev",{"VAR=":"supernatural_belief","re":true},"ev",{"VAR?":"medium_reputation"},2,"+",{"VAR=":"medium_reputation","re":true},"/ev","^Belief is a costume. You wear it because men insist on dressing you in it.","\n","^You discovered early that men who distrust a woman's mind will sometimes worship her nerves. A conclusion from evidence irritates them. A vision, sighed through lowered lashes, makes them lean closer.","\n","^Very well. Let them lean.","\n",{"->":"spiritual_senses"},{"#f":5}],"c-3":["\n","#","^supernatural:undecided","/#","ev","str","^undecided","/str","/ev",{"VAR=":"supernatural_belief","re":true},"^You have learned not to decide too early.","\n","^There are things you can explain, things you cannot yet explain, and things that explanation damages before it helps. You have made a profession of standing at thresholds with a face composed enough for both sides to continue speaking.","\n","^Hohenreith will have to show you what kind of case it is.","\n",{"->":"spiritual_senses"},{"#f":5}]}],null],"spiritual_senses":[["^Belief is one matter. Experience is another.","\n","^People call a woman sensitive when they want her perceptions to sound like an illness. They call her hysterical when those perceptions inconvenience them. They call her inspired when they need her, and unstable when they do not.","\n","^Beneath reputation and performance, memory has its own testimony.","\n","ev","str","^There have been moments you cannot explain away.","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Everything you do can be explained by observation, timing, and nerve.","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Something happens, but never when summoned.","/str","/ev",{"*":".^.c-2","flg":20},"ev","str","^You buried the first signs so thoroughly that even you do not know what remains.","/str","/ev",{"*":".^.c-3","flg":20},{"c-0":["\n","#","^powers:genuine","/#","ev","str","^genuine","/str","/ev",{"VAR=":"supernatural_senses","re":true},"ev",{"VAR?":"supernatural_exposure"},2,"+",{"VAR=":"supernatural_exposure","re":true},"/ev","^There have been moments you cannot explain away.","\n","^Once, as a child, you knew before the telegram came. Once, in a crowded room, a stranger's grief entered you with such force that your own knees failed. Once, in a mirror, you saw a door behind you that was not in the room when you turned.","\n","^You learned caution after that. It is unwise for a woman to know things before a man has asked her opinion.","\n",{"->":"viktor_first_exchange"},{"#f":5}],"c-1":["\n","#","^powers:faked","/#","#","^route:detective","/#","ev","str","^faked","/str","/ev",{"VAR=":"supernatural_senses","re":true},"ev",{"VAR?":"detective"},1,"+",{"VAR=":"detective","re":true},"/ev","^Everything you do can be explained by observation, timing, and nerve.","\n","^You notice rings removed too recently, mourning gloves worn too carefully, letters folded and refolded until the crease gives away the reader's obsession. You hear servants misname guests, mothers pause before daughters' rooms, officers lie by becoming too exact.","\n","^The dead have never told you anything. The living cannot stop telling you everything.","\n",{"->":"viktor_first_exchange"},{"#f":5}],"c-2":["\n","#","^powers:ambiguous","/#","ev","str","^ambiguous","/str","/ev",{"VAR=":"supernatural_senses","re":true},"ev",{"VAR?":"supernatural_exposure"},1,"+",{"VAR=":"supernatural_exposure","re":true},"/ev","^Something happens, but never when summoned.","\n","^Your reputation depends upon command. The truth, if truth it is, has no respect for appointments.","\n","^Sometimes a room changes pressure around you. Sometimes a face acquires an old expression no living person taught it. Sometimes names arrive before introductions. But the harder you reach, the more ordinary the world becomes.","\n",{"->":"viktor_first_exchange"},{"#f":5}],"c-3":["\n","#","^powers:repressed","/#","#","^route:eccentric","/#","ev","str","^repressed","/str","/ev",{"VAR=":"supernatural_senses","re":true},"ev",{"VAR?":"eccentric"},1,"+",{"VAR=":"eccentric","re":true},"/ev","^You buried the first signs so thoroughly that even you do not know what remains.","\n","^There are childhood memories sealed behind politeness: a nursery mirror turned to the wall, a nurse dismissed without reference, your mother's hand tightening around your wrist until the bones complained.","\n","^You became strange afterward in ways society found easier to admire than understand.","\n",{"->":"viktor_first_exchange"},{"#f":5}]}],null],"viktor_first_exchange":[["^The train emerges from the tunnel into a pale afternoon cut by dark firs and white rock. Far below, water shows itself only in flashes. The valley is no longer a view from a salon painting. It has depth enough to hide things.","\n","^Viktor opens a leather folder and removes a memorandum. He does not hand it to you at once.","\n","^\"When we leave the railway,\" he says, \"we will be met by a coach from Hohenreith. From that moment, appearances matter. Your hosts have been told that I assist with correspondence, travel, and practical arrangements. They need not be troubled with military definitions.\"","\n","ev","str","^\"And the villagers?\"","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^\"How merciful. The empire has spared them vocabulary.\"","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^\"You mean they are not to know whether I am guest, tool, or warning.\"","/str","/ev",{"*":".^.c-2","flg":20},{"c-0":["\n","^\"And the villagers?\"","\n","^\"The villagers need not be troubled with anything.\"","\n","^There it is: the empire in miniature. A man, a folder, a locked sentence.","\n",{"->":"viktor_restraint_advice"},{"#f":5}],"c-1":["\n","#","^route:eccentric","/#","ev",{"VAR?":"eccentric"},1,"+",{"VAR=":"eccentric","re":true},"/ev","ev",{"VAR?":"viktor_suspicion"},1,"+",{"VAR=":"viktor_suspicion","re":true},"/ev","^\"How merciful. The empire has spared them vocabulary.\"","\n","^\"The empire has spared them alarm,\" Viktor says.","\n","^The empire's habit of confusing ignorance with calm does not require stating to remain present.","\n",{"->":"viktor_restraint_advice"},{"#f":5}],"c-2":["\n","#","^route:detective","/#","ev",{"VAR?":"detective"},1,"+",{"VAR=":"detective","re":true},"/ev","ev",{"VAR?":"viktor_trust"},1,"+",{"VAR=":"viktor_trust","re":true},"/ev","^\"You mean they are not to know whether I am guest, tool, or warning.\"","\n","^\"I mean they are to know only what steadies the situation.\"","\n","^The answer steadies nothing; that is not a contradiction.","\n",{"->":"viktor_restraint_advice"},{"#f":5}]}],null],"viktor_restraint_advice":[["^\"You will be addressed according to the station you present,\" he continues. \"The Graf's household will observe rank. Servants will observe what the household observes. Villagers may observe less and remember more. I advise restraint.\"","\n","^The advice is sound. That makes it no less irritating.","\n","ev","str","^\"If gentlemen were less easily led, Herr Nowak, ladies would require fewer methods.\"","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^\"If you wish me to pass as harmless, you must stop warning me like a gaoler.\"","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^\"Then let us be exact. What do they know, what do they suspect, and what am I permitted to verify?\"","/str","/ev",{"*":".^.c-2","flg":20},"ev","str","^\"I shall do my best not to faint unless it is useful.\"","/str","/ev",{"*":".^.c-3","flg":20},"ev","str","^\"Restraint is what timid people call obedience after they have forgotten who trained them.\"","/str","/ev",{"*":".^.c-4","flg":20},{"c-0":["\n","#","^route:lover","/#","ev",{"VAR?":"lover"},1,"+",{"VAR=":"lover","re":true},"/ev","ev","str","^provocation","/str","/ev",{"VAR=":"viktor_relation","re":true},"ev",{"VAR?":"viktor_trust"},1,"-",{"VAR=":"viktor_trust","re":true},"/ev","ev",{"VAR?":"viktor_suspicion"},1,"+",{"VAR=":"viktor_suspicion","re":true},"/ev","^\"If gentlemen were less easily led, Herr Nowak, ladies would require fewer methods.\"","\n","^For the first time, amusement almost reaches his mouth.","\n","^\"A dangerous doctrine.\"","\n",{"->":"viktor_lover_followup"},{"#f":5}],"c-1":["\n","#","^route:sapphic","/#","ev",{"VAR?":"sapphic"},1,"+",{"VAR=":"sapphic","re":true},"/ev","ev","str","^tension","/str","/ev",{"VAR=":"viktor_relation","re":true},"ev",{"VAR?":"viktor_suspicion"},1,"+",{"VAR=":"viktor_suspicion","re":true},"/ev","^\"If you wish me to pass as harmless, you must stop warning me like a gaoler.\"","\n","^His gaze sharpens.","\n","^\"I am not your gaoler.\"","\n",{"->":"viktor_sapphic_followup"},{"#f":5}],"c-2":["\n","#","^route:detective","/#","ev",{"VAR?":"detective"},1,"+",{"VAR=":"detective","re":true},"/ev","ev","str","^professional","/str","/ev",{"VAR=":"viktor_relation","re":true},"ev",{"VAR?":"viktor_trust"},1,"+",{"VAR=":"viktor_trust","re":true},"/ev","^\"Then let us be exact. What do they know, what do they suspect, and what am I permitted to verify?\"","\n","^He gives the smallest nod, as if you have chosen the only answer fit for adults.","\n","^\"They know that you come recommended. They suspect that you may be able to settle disturbances without police, priest, or press. You are permitted to verify fraud, coercion, threat to public order, or credible phenomena not presently classifiable.\"","\n",{"->":"viktor_detective_followup"},{"#f":5}],"c-3":["\n","#","^route:careless","/#","ev",{"VAR?":"careless"},1,"+",{"VAR=":"careless","re":true},"/ev","ev","str","^dependence","/str","/ev",{"VAR=":"viktor_relation","re":true},"ev",{"VAR?":"viktor_trust"},1,"-",{"VAR=":"viktor_trust","re":true},"/ev","^\"I shall do my best not to faint unless it is useful.\"","\n","^Something in his expression tightens; not contempt exactly, but readiness.","\n","^\"I would prefer you did not faint at all.\"","\n",{"->":"viktor_careless_followup"},{"#f":5}],"c-4":["\n","#","^route:eccentric","/#","ev",{"VAR?":"eccentric"},1,"+",{"VAR=":"eccentric","re":true},"/ev","ev","str","^challenge","/str","/ev",{"VAR=":"viktor_relation","re":true},"ev",{"VAR?":"viktor_suspicion"},2,"+",{"VAR=":"viktor_suspicion","re":true},"/ev","^\"Restraint is what timid people call obedience after they have forgotten who trained them.\"","\n","^Viktor studies you as he might study an unfamiliar weapon found in luggage.","\n","^\"You enjoy making enemies.\"","\n",{"->":"viktor_eccentric_followup"},{"#f":5}]}],null],"viktor_lover_followup":[["ev","str","^\"A practical one.\"","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^\"Dangerous doctrines travel best in good gloves.\"","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["\n","^\"A practical one.\"","\n","^\"You intend to practice it at Hohenreith?\"","\n",{"->":"viktor_lover_second"},{"#f":5}],"c-1":["\n","#","^route:eccentric","/#","ev",{"VAR?":"eccentric"},1,"+",{"VAR=":"eccentric","re":true},"/ev","^\"Dangerous doctrines travel best in good gloves.\"","\n","^\"You intend to charm Hohenreith into confession?\"","\n","^If Hohenreith insists on being charmed, it will hardly be your fault.","\n",{"->":"viktor_explains_orders"},{"#f":5}]}],null],"viktor_lover_second":[["ev","str","^\"Only where patriotism requires sacrifice.\"","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^\"Only where men mistake desire for judgement.\"","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["\n","#","^route:lover","/#","ev",{"VAR?":"lover"},1,"+",{"VAR=":"lover","re":true},"/ev","^\"Only where patriotism requires sacrifice.\"","\n","^He looks down at the memorandum, but not quickly enough to conceal that he is reassessing you.","\n",{"->":"viktor_explains_orders"},{"#f":5}],"c-1":["\n","#","^route:lover","/#","ev",{"VAR?":"lover"},1,"+",{"VAR=":"lover","re":true},"/ev","^\"Only where men mistake desire for judgement.\"","\n","^\"That may include more territory than the maps admit.\"","\n",{"->":"viktor_explains_orders"},{"#f":5}]}],null],"viktor_sapphic_followup":[["ev","str","^\"No. A gaoler is at least honest about the key.\"","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^\"Then do not stand between me and every locked door before I have touched the handle.\"","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["\n","^\"No. A gaoler is at least honest about the key.\"","\n","^The words surprise you by leaving a mark. Not on him, perhaps. On yourself. The closer the train carries you to Amalia's world, though you do not yet know her face, the more intolerable it seems that every female life there might be guarded by men who call the guarding concern.","\n","^Viktor folds the memorandum once, precisely.","\n",{"->":"viktor_explains_orders"},{"#f":5}],"c-1":["\n","ev",{"VAR?":"viktor_trust"},1,"+",{"VAR=":"viktor_trust","re":true},"/ev","^\"Then do not stand between me and every locked door before I have touched the handle.\"","\n","^\"Some doors are locked for cause.\"","\n","^Any cause worthy of the lock should survive the indignity of being examined.","\n","^Viktor folds the memorandum once, precisely.","\n",{"->":"viktor_explains_orders"},{"#f":5}]}],null],"viktor_detective_followup":[["ev","str","^\"Credible phenomena not presently classifiable.\"","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^\"And if the phenomena become classifiable?\"","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["\n","^\"Credible phenomena not presently classifiable.\"","\n","^\"That is the phrase.\"","\n","^The phrase files itself in your mind as a bureaucratic ghost.","\n","^\"The safest kind,\" he says.","\n",{"->":"viktor_explains_orders"},{"#f":5}],"c-1":["\n","^\"And if the phenomena become classifiable?\"","\n","^\"Then we classify them before others do.\"","\n","^The sentence has the chill of a report written quickly over a grave.","\n",{"->":"viktor_explains_orders"},{"#f":5}]}],null],"viktor_careless_followup":[["ev","str","^\"How ungallant.\"","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^\"Then you must remain close enough to catch me.\"","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["\n","^\"How ungallant.\"","\n","^\"How practical.\"","\n","^The burden of practicality passes toward him as gracefully as a fainting couch dragged into a field hospital.","\n","^His answer is delayed by half a breath.","\n","^\"That, gnädiges Fräulein, is precisely what concerns me.\"","\n",{"->":"viktor_explains_orders"},{"#f":5}],"c-1":["\n","#","^route:lover","/#","ev",{"VAR?":"lover"},1,"+",{"VAR=":"lover","re":true},"/ev","^\"Then you must remain close enough to catch me.\"","\n","^\"My orders did not specify theatrical collapses.\"","\n","^The omission does not flatter the thoroughness of his superiors.","\n",{"->":"viktor_explains_orders"},{"#f":5}]}],null],"viktor_eccentric_followup":[["ev","str","^\"No. I dislike the laziness of letting fools remain undecided.\"","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^\"Enemies are merely people honest enough to stand in the right place.\"","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["\n","^\"No. I dislike the laziness of letting fools remain undecided.\"","\n","^\"At Hohenreith, that dislike may become expensive.\"","\n","^If the Graf wanted docility, he could have invited someone cheaper.","\n",{"->":"viktor_explains_orders"},{"#f":5}],"c-1":["\n","^\"Enemies are merely people honest enough to stand in the right place.\"","\n","^\"You speak as though conflict were housekeeping.\"","\n","^Conflict has always been a form of housekeeping; one discovers what belongs where.","\n",{"->":"viktor_explains_orders"},{"#f":5}]}],null],"viktor_explains_orders":[["^The wheels strike a curve. The compartment leans. For a moment the two of you are held in the same narrow imbalance.","\n","^Viktor gives you the memorandum at last.","\n","^The document is not long. That is part of its menace. Long documents invite argument; short ones carry authority.","\n","^A comital household. A hunting residence in Upper Styria, not the family's principal seat. Reports of disturbances among servants and villagers. No police action requested. No public ecclesiastical inquiry desired. No press. No correspondence beyond approved channels. Your presence to be explained as a discreet consultation requested by the family. Herr Nowak to assist in practical matters.","\n","^No one has written the word ghost.","\n","^No one has written the word fraud.","\n","^No one has written the word daughter.","\n","^Yet the omissions arrange themselves around the page like furniture around a corpse.","\n","ev","str","^\"There is another instruction.\"","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^\"Your version is shorter than your silence. That means there is another instruction.\"","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^\"How touching. Vienna trusts us both so little it had to divide the distrust.\"","/str","/ev",{"*":".^.c-2","flg":20},{"c-0":["\n","^\"There is another instruction.\"","\n","^Viktor does not ask how you know.","\n","^\"There is always another instruction,\" he says.","\n",{"->":"viktor_second_instruction"},{"#f":5}],"c-1":["\n","#","^route:detective","/#","ev",{"VAR?":"detective"},1,"+",{"VAR=":"detective","re":true},"/ev","ev",{"VAR?":"viktor_trust"},1,"+",{"VAR=":"viktor_trust","re":true},"/ev","^\"Your version is shorter than your silence. That means there is another instruction.\"","\n","^Viktor does not ask how you know.","\n","^\"There is always another instruction,\" he says.","\n",{"->":"viktor_second_instruction"},{"#f":5}],"c-2":["\n","#","^route:eccentric","/#","ev",{"VAR?":"eccentric"},1,"+",{"VAR=":"eccentric","re":true},"/ev","ev",{"VAR?":"viktor_suspicion"},1,"+",{"VAR=":"viktor_suspicion","re":true},"/ev","^\"How touching. Vienna trusts us both so little it had to divide the distrust.\"","\n","^Viktor does not ask how you know.","\n","^\"There is always another instruction,\" he says.","\n",{"->":"viktor_second_instruction"},{"#f":5}]}],null],"viktor_second_instruction":[["ev","str","^\"For you.\"","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^\"Concerning me.\"","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^\"Concerning whether I am fraud, fool, or useful animal.\"","/str","/ev",{"*":".^.c-2","flg":20},{"c-0":["\n","^\"For you.\"","\n","^\"Yes.\"","\n",{"->":"viktor_instruction_concerns"},{"#f":5}],"c-1":["\n","^\"Concerning me.\"","\n","^\"Partly.\"","\n",{"->":"train_slows"},{"#f":5}],"c-2":["\n","ev",{"VAR?":"viktor_suspicion"},1,"+",{"VAR=":"viktor_suspicion","re":true},"/ev","^\"Concerning whether I am fraud, fool, or useful animal.\"","\n","^\"Partly,\" he says, and this time the honesty has a blade in it.","\n",{"->":"train_slows"},{"#f":5}]}],null],"viktor_instruction_concerns":[["ev","str","^\"Concerning me.\"","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^\"Concerning whether I am fraud, fool, or useful animal.\"","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["\n","^\"Concerning me.\"","\n","^\"Partly.\"","\n",{"->":"train_slows"},{"#f":5}],"c-1":["\n","ev",{"VAR?":"viktor_suspicion"},1,"+",{"VAR=":"viktor_suspicion","re":true},"/ev","^\"Concerning whether I am fraud, fool, or useful animal.\"","\n","^\"Partly,\" he says, and this time the honesty has a blade in it.","\n",{"->":"train_slows"},{"#f":5}]}],null],"train_slows":[["^The train begins to slow. The rhythm changes first in the floor, then in the window, then in the body. Houses gather beside the line. A station roof appears between drifting smoke and the dark combs of forested slopes. ","#","^sfx[steam-whistle.ogg]","/#","\n","ev","str","^\"Then I shall try to be worth the ink.\"","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^\"Then I shall disappoint the instruction as creatively as circumstances permit.\"","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^\"Then keep your second instruction, Herr Nowak. I prefer primary sources.\"","/str","/ev",{"*":".^.c-2","flg":20},{"c-0":["\n","^\"Then I shall try to be worth the ink.\"","\n","^\"I sincerely hope so.\"","\n",{"->":"train_slows_end"},{"#f":5}],"c-1":["\n","#","^route:eccentric","/#","ev",{"VAR?":"eccentric"},1,"+",{"VAR=":"eccentric","re":true},"/ev","^\"Then I shall disappoint the instruction as creatively as circumstances permit.\"","\n","^\"I sincerely hope you do not.\"","\n",{"->":"train_slows_end"},{"#f":5}],"c-2":["\n","#","^route:detective","/#","ev",{"VAR?":"detective"},1,"+",{"VAR=":"detective","re":true},"/ev","^\"Then keep your second instruction, Herr Nowak. I prefer primary sources.\"","\n","^\"A preference not always granted in imperial service.\"","\n",{"->":"train_slows_end"},{"#f":5}]}],null],"train_slows_end":["^You cannot decide whether his answer is an insult, a prayer, or his first honest sentence.","\n",{"->":"railway_station"},null],"railway_station":[["^The station is small enough that the train seems briefly embarrassed to stop there. ","#","^chapter[The Station] ","/#","#","^image[muerzzuschlag.png](portrait)","/#","\n","^A porter in a cap too large for him hurries along the platform. A woman with a basket steps back from the steam as if from an animal. Somewhere beyond the station building, a cart horse stamps at frozen mud. The signboard gives the place a name you have seen in the timetable but will not remember with affection.","\n","^Your luggage descends in stages.","\n","ev","str","^A disciplined official set: trunk, dispatch case, hatbox, and black séance case.","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^An elegant noblewoman's luggage: too correct to be accidental.","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^A performer's luggage: harmless on top, less harmless beneath.","/str","/ev",{"*":".^.c-2","flg":20},"ev","str","^A practical assortment that betrays too much preparation.","/str","/ev",{"*":".^.c-3","flg":20},"ev","str","^An excessive pile that makes concealment impossible.","/str","/ev",{"*":".^.c-4","flg":20},{"c-0":["\n","ev","str","^official","/str","/ev",{"VAR=":"baggage_style","re":true},"ev",{"VAR?":"detective"},1,"+",{"VAR=":"detective","re":true},"/ev","^First comes a sober travelling trunk with brass corners dulled by use, then a dispatch case, then a hatbox, then the narrow black case whose contents would embarrass both a priest and a conjurer if either searched it without imagination.","\n",{"->":"station_luggage_common"},{"#f":5}],"c-1":["\n","ev","str","^elegant","/str","/ev",{"VAR=":"baggage_style","re":true},"ev",{"VAR?":"class_confidence"},1,"+",{"VAR=":"class_confidence","re":true},"/ev","^First comes a large trunk in dark leather, then a second smaller one for linen, then a round hatbox, a fitted toilette case, and a reticule kept too close to your hand for any porter to misunderstand its importance.","\n",{"->":"station_luggage_common"},{"#f":5}],"c-2":["\n","ev","str","^performer","/str","/ev",{"VAR=":"baggage_style","re":true},"ev",{"VAR?":"medium_reputation"},1,"+",{"VAR=":"medium_reputation","re":true},"/ev","^First comes a respectable trunk, then a hatbox, then a travelling case of gloves, veils, ribbons, calling cards, and the small objects by which a room may be persuaded to believe in forces already present.","\n",{"->":"station_luggage_common"},{"#f":5}],"c-3":["\n","ev","str","^practical","/str","/ev",{"VAR=":"baggage_style","re":true},"ev",{"VAR?":"detective"},1,"+",{"VAR=":"detective","re":true},"/ev","^First comes a battered trunk reinforced at the corners, then a leather case with notebooks, pencils, folded maps, spare gloves, a hand-lamp, and enough small necessities to offend anyone who prefers women decorative.","\n",{"->":"station_luggage_common"},{"#f":5}],"c-4":["\n","ev","str","^excessive","/str","/ev",{"VAR=":"baggage_style","re":true},"ev",{"VAR?":"careless"},1,"+",{"VAR=":"careless","re":true},"/ev","^First comes one trunk, then another, then a hatbox, then a rug, then a dressing case, then the narrow black case, then a smaller parcel you had forgotten had survived packing. By the end even Viktor looks faintly outnumbered.","\n",{"->":"station_luggage_common"},{"#f":5}]}],null],"station_luggage_common":["^Viktor oversees the transfer with clipped civility. He does not carry like a servant. He directs like a man pretending not to command.","\n","^The coach from Hohenreith waits beyond the station yard: dark green paint, black wheels, the comital crest discreetly worn on the door, and two horses already restless beneath harness. The driver removes his hat when he sees you. Not too deeply. Deep enough for rank, not deep enough for reverence. ","#","^sfx[horse-neigh.ogg]","/#","\n","^\"Gnädiges Fräulein? Herr Sekretär?\"","\n","ev",{"VAR?":"birth_class"},"str","^noble","/str","==","/ev",[{"->":".^.b","c":true},{"b":["\n","^He has been told enough to place you. That is a courtesy. It is also a warning.","\n",{"->":".^.^.^.18"},null]}],[{"->":".^.b"},{"b":["\n","^He hesitates over you by the smallest measure. The hesitation is not rudeness. It is calculation. First-class carriage, court letter, no title beyond Fräulein, and a man beside you who looks like he has arrested people for less than staring.","\n",{"->":".^.^.^.18"},null]}],"nop","\n","^Viktor answers before you can.","\n","^\"From Jagdhaus Hohenreith?\"","\n","^\"Jawohl, Herr Sekretär. The road is passable. If the mist holds, we should reach Eibenreith before dark.\"","\n","^The word enters the air without ceremony.","\n","^Eibenreith.","\n","^Not Hohenreith, the name printed on the invitation in a clean hand. Eibenreith: the village below it. A smaller name. Older in the mouth. A name with roots rather than stationery.","\n",{"->":"coach_journey"},null],"coach_journey":[["^The coach leaves the station behind and with it the last easy evidence of empire. ","#","^chapter[The Graben] ","/#","#","^music[Kaiserpunk Jodler.mp3](crossfade, loop, lead=4)","/#","\n","^At first the road follows a valley where telegraph wire still keeps company with it and the river moves in a pale, stony bed. Sawmills, fenced meadows, and farmhouses appear and vanish behind stands of spruce. The mountains do not rise all at once. They advance by jurisdiction. A wooded slope claims the left-hand sky, then a grey wall of limestone closes the north, then another ridge gathers to the east until even the clouds seem to have entered service.","\n","^The driver names places when Viktor asks, but the names are local and practical, meant for men who know which bridge floods and which farm breeds stubborn horses. Somewhere beyond the visible ridges, he says, lies the great white back of the Hochschwab. Eastward, beyond forest and pass, the Hohe Veitsch keeps its own weather. He says this not as a guide would say it, but as a man explaining neighbours who may or may not be in a temper.","\n","^The main valley narrows.","\n","^The road turns from it into a side Graben, and the change is immediate. Sound alters. The wheels no longer ring against open distance but grind between banks, roots, and wet stone. The air smells of leaf mould, resin, and cold water. Yews appear among the firs in dark, improbable patience, their needles too black for the afternoon.","\n","^\"Eibenreither Graben,\" the driver says, and crosses himself so quickly that the gesture might have been meant for a rut in the road.","\n","^Viktor notices. Of course he notices.","\n","^\"Bad road?\" he asks.","\n","^\"Old road,\" the driver says.","\n","^No one speaks for a while.","\n","^You watch the trees.","\n","^There are forests that invite stories because they are pretty, and forests that reject stories because whatever happened there did not require witnesses. This one belongs to the second kind. Its trunks stand close, not wildly, but with the air of a crowd making room for something carried through it long ago. The snow that remains in hollows is not clean. It has gathered needles, bark, and a yellowish stain where water has risen underneath.","\n","^On a slope above the road, half swallowed by undergrowth, you glimpse stone.","\n","^A shrine, perhaps. A boundary marker. A figure. The coach has passed before your eyes can persuade themselves of its shape. For one instant you are left with the impression of a woman's head inclined not in prayer, but in listening. ","#","^image[statue.png](square)","/#","\n","ev",{"VAR?":"supernatural_senses"},"str","^genuine","/str","==",{"VAR?":"supernatural_senses"},"str","^ambiguous","/str","==","||",{"VAR?":"supernatural_senses"},"str","^repressed","/str","==","||","/ev",[{"->":".^.b","c":true},{"b":["\n","^The back of your neck tightens.","\n","^Not fear. Recognition would be worse.","\n","ev",{"VAR?":"supernatural_exposure"},1,"+",{"VAR=":"supernatural_exposure","re":true},"/ev",{"->":".^.^.^.58"},null]}],[{"->":".^.b"},{"b":["\n","^You tell yourself that old stone, seen through moving branches, will become whatever the mind is cowardly enough to supply.","\n",{"->":".^.^.^.58"},null]}],"nop","\n","^Viktor has turned slightly toward the same slope.","\n","^\"Did you see something?\"","\n","ev","str","^\"A woman in the wood, perhaps. Or a stone that wanted to be one.\"","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^\"A marker. I would like to know where that path leads.\"","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^\"Only trees. The sort that make one grateful for gentlemen with revolvers.\"","/str","/ev",{"*":".^.c-2","flg":20},"ev","str","^\"Would you believe me if I said I had?\"","/str","/ev",{"*":".^.c-3","flg":20},"ev","str","^\"No.\"","/str","/ev",{"*":".^.c-4","flg":20},{"c-0":["\n","#","^route:eccentric","/#","#","^statue_hint","/#","ev",{"VAR?":"eccentric"},1,"+",{"VAR=":"eccentric","re":true},"/ev","ev",{"VAR?":"viktor_suspicion"},1,"+",{"VAR=":"viktor_suspicion","re":true},"/ev","^\"A woman in the wood, perhaps. Or a stone that wanted to be one.\"","\n","^He studies the passing trees.","\n","^\"A local shrine?\"","\n",{"->":"statue_eccentric_followup"},{"#f":5}],"c-1":["\n","#","^route:detective","/#","#","^statue_hint","/#","ev",{"VAR?":"detective"},1,"+",{"VAR=":"detective","re":true},"/ev","ev",{"VAR?":"viktor_trust"},1,"+",{"VAR=":"viktor_trust","re":true},"/ev","^\"A marker. I would like to know where that path leads.\"","\n","^\"You saw a path?\"","\n",{"->":"statue_detective_followup"},{"#f":5}],"c-2":["\n","#","^route:careless","/#","ev",{"VAR?":"careless"},1,"+",{"VAR=":"careless","re":true},"/ev","ev","str","^dependence","/str","/ev",{"VAR=":"viktor_relation","re":true},"^\"Only trees. The sort that make one grateful for gentlemen with revolvers.\"","\n","^His expression darkens by one official degree.","\n","^\"A revolver is a poor instrument against trees.\"","\n",{"->":"statue_careless_followup"},{"#f":5}],"c-3":["\n","#","^route:lover","/#","ev",{"VAR?":"lover"},1,"+",{"VAR=":"lover","re":true},"/ev","ev",{"VAR?":"viktor_suspicion"},1,"+",{"VAR=":"viktor_suspicion","re":true},"/ev","^\"Would you believe me if I said I had?\"","\n","^\"That would depend on what advantage you expected from the answer.\"","\n",{"->":"statue_lover_followup"},{"#f":5}],"c-4":["\n","#","^route:sapphic","/#","ev",{"VAR?":"sapphic"},1,"+",{"VAR=":"sapphic","re":true},"/ev","^\"No.\"","\n","^The denial is too quick, and you both hear it.","\n","^You are not thinking of the stone now. You are thinking of the young woman waiting somewhere ahead: the Graf's daughter, the reason carefully not written into the memorandum, the stranger whose household has summoned you under a title both absurd and useful.","\n",{"->":"statue_sapphic_followup"},{"#f":5}]}],null],"statue_eccentric_followup":[["ev","str","^\"If it is a shrine, it has not been loved recently.\"","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^\"No. Shrines face the faithful. That thing was listening sideways.\"","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["\n","^\"If it is a shrine, it has not been loved recently.\"","\n","^\"You speak as if stones notice neglect.\"","\n","^Soldiers, too, notice neglect. His silence admits enough.","\n","^He does not answer.","\n",{"->":"coach_nears_village"},{"#f":5}],"c-1":["\n","ev",{"VAR?":"supernatural_exposure"},1,"+",{"VAR=":"supernatural_exposure","re":true},"/ev","^\"No. Shrines face the faithful. That thing was listening sideways.\"","\n","^Viktor's hand rests on the coach strap, still and ready.","\n",{"->":"coach_nears_village"},{"#f":5}]}],null],"statue_detective_followup":[["ev","str","^\"Not clearly. Enough to ask later.\"","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^\"Only a suggestion of one. If it exists, someone maintains the absence of it.\"","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["\n","^\"Not clearly. Enough to ask later.\"","\n","^Viktor looks back through the small rear window. The bend has already erased the slope.","\n","^\"Ask carefully. Places people fail to mention are often more informative than those they recommend.\"","\n",{"->":"coach_nears_village"},{"#f":5}],"c-1":["\n","#","^route:detective","/#","ev",{"VAR?":"detective"},1,"+",{"VAR=":"detective","re":true},"/ev","^\"Only a suggestion of one. If it exists, someone maintains the absence of it.\"","\n","^\"You make absences sound expensive.\"","\n","^They usually are; absence is expensive when someone maintains it.","\n",{"->":"coach_nears_village"},{"#f":5}]}],null],"statue_careless_followup":[["ev","str","^\"Then I shall rely on your conversation to intimidate them.\"","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^\"How unfortunate. You seemed so professionally reassuring.\"","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["\n","^\"Then I shall rely on your conversation to intimidate them.\"","\n","^The driver pretends not to hear. His shoulders, however, hear everything.","\n",{"->":"coach_nears_village"},{"#f":5}],"c-1":["\n","#","^route:lover","/#","ev",{"VAR?":"lover"},1,"+",{"VAR=":"lover","re":true},"/ev","^\"How unfortunate. You seemed so professionally reassuring.\"","\n","^\"I prefer enemies that identify themselves.\"","\n",{"->":"coach_nears_village"},{"#f":5}]}],null],"statue_lover_followup":[["ev","str","^\"Herr Nowak. You wound me.\"","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^\"Then watch the slope, not my intentions. One of them may be useful.\"","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["\n","^\"Herr Nowak. You wound me.\"","\n","^\"Not yet.\"","\n","^It is the first thing he has said all day that almost sounds like flirtation, though perhaps only because danger has a talent for borrowing warmer clothes.","\n",{"->":"coach_nears_village"},{"#f":5}],"c-1":["\n","ev",{"VAR?":"viktor_trust"},1,"+",{"VAR=":"viktor_trust","re":true},"/ev","^\"Then watch the slope, not my intentions. One of them may be useful.\"","\n","^He obeys without admitting that he has done so.","\n",{"->":"coach_nears_village"},{"#f":5}]}],null],"statue_sapphic_followup":[["ev","str","^\"It was only shadow.\"","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^\"Or if I did, I prefer not to have it explained before I understand why it matters.\"","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["\n","^\"It was only shadow.\"","\n","^If this place keeps women in stone, you think, what does it do to them in houses?","\n",{"->":"coach_nears_village"},{"#f":5}],"c-1":["\n","#","^route:detective","/#","ev",{"VAR?":"detective"},1,"+",{"VAR=":"detective","re":true},"/ev","^\"Or if I did, I prefer not to have it explained before I understand why it matters.\"","\n","^If this place keeps women in stone, you think, what does it do to them in houses?","\n",{"->":"coach_nears_village"},{"#f":5}]}],null],"coach_nears_village":[["^The Graben opens reluctantly.","\n","^First comes the smell of smoke. Then a roof, low and dark with weather. Then another. Then a church tower, not high, not graceful, but thick-shouldered and pale against the slope behind it. Its walls look older than the village around them and less certain of victory. The windows are small. The churchyard wall holds the road at a distance, as if the dead require fortification from the living, or the living from something else. ","#","^chapter[Eibenreith Village] ","/#","#","^sfx[church-bells.ogg](max=8, fade) ","/#","#","^image[eibenreith.png](landscape)","/#","\n","^Eibenreith appears not as a village in a picture appears, all at once and composed for admiration, but by fragments.","\n","^A woman in a dark kerchief pauses with a pail in her hand. A boy stops driving geese and lets them complain around his boots. Two men outside a shed end their conversation at the same moment without looking at each other. Curtains stir in windows where no one admits to standing. A blacksmith's sign moves slightly in air you cannot feel. Water runs somewhere under boards, under stone, under the road itself, quick and cold and hidden.","\n","^The houses are not poor, not exactly. Many are solid, whitewashed, shingled, kept with the stubborn decency of people who repair what they cannot replace. Yet something in their arrangement troubles the eye. They turn toward the church but not fully. They keep the road but lean from it. They leave, between yard and fence and woodpile, narrow passages where shadow gathers too early.","\n","^The coach slows.","\n","^No one runs to greet it.","\n","^No one needs to. News has already entered the village by means faster than railway, telegraph, or imperial seal.","\n","^You sit very straight as Eibenreith takes its first look at you.","\n","^Beside you, Viktor lowers his voice.","\n","^\"Remember: at Hohenreith, every courtesy will mean something. Here, every silence will.\"","\n","ev","str","^\"Then we are already being received.\"","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^\"You make it sound as if the village outranks the Graf.\"","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^\"How fortunate that I packed several silences.\"","/str","/ev",{"*":".^.c-2","flg":20},"ev","str","^\"I dislike being watched by people who will not introduce themselves.\"","/str","/ev",{"*":".^.c-3","flg":20},"ev","str","^\"If Amalia has lived under this gaze all her life, I begin to understand why they sent for ghosts.\"","/str","/ev",{"*":".^.c-4","flg":20},{"c-0":["\n","#","^route:detective","/#","ev",{"VAR?":"detective"},1,"+",{"VAR=":"detective","re":true},"/ev","^\"Then we are already being received.\"","\n","^\"Yes,\" he says. \"And examined.\"","\n",{"->":"village_final_image"},{"#f":5}],"c-1":["\n","#","^route:eccentric","/#","ev",{"VAR?":"eccentric"},1,"+",{"VAR=":"eccentric","re":true},"/ev","^\"You make it sound as if the village outranks the Graf.\"","\n","^\"No,\" Viktor says. \"Only as if it may have survived more than one.\"","\n",{"->":"village_final_image"},{"#f":5}],"c-2":["\n","#","^route:lover","/#","ev",{"VAR?":"lover"},1,"+",{"VAR=":"lover","re":true},"/ev","^\"How fortunate that I packed several silences.\"","\n","^His mouth almost moves. \"Use the plainest one first.\"","\n",{"->":"village_final_image"},{"#f":5}],"c-3":["\n","#","^route:careless","/#","ev",{"VAR?":"careless"},1,"+",{"VAR=":"careless","re":true},"/ev","^\"I dislike being watched by people who will not introduce themselves.\"","\n","^\"That,\" he says, \"is unlikely to improve today.\"","\n",{"->":"village_final_image"},{"#f":5}],"c-4":["\n","#","^route:sapphic","/#","ev",{"VAR?":"sapphic"},1,"+",{"VAR=":"sapphic","re":true},"/ev","^\"If Amalia has lived under this gaze all her life, I begin to understand why they sent for ghosts.\"","\n","^Viktor glances at you, but whatever answer he considers, he keeps it behind his teeth.","\n",{"->":"village_final_image"},{"#f":5}]}],null],"village_final_image":["^The horses draw the coach past the churchyard wall. Above it, on the old plaster beside the gate, a faded painted woman looks down from beneath a flaking blue mantle. Her hands are folded in prayer. Her eyes, damaged by weather, no longer point in the same direction.","\n","^For one breath, as the wheels pass over a buried runnel of water, the painted face seems less like the Holy Mother than like a mask put on something that had been waiting longer.","\n","^Then the coach enters the village proper, and the road bends toward the unseen height where Jagdhaus Hohenreith stands above Eibenreith under its newer name","\n","done",null],"global decl":["ev","str","^unset","/str",{"VAR=":"birth_class"},"str","^","/str",{"VAR=":"title_part"},"str","^","/str",{"VAR=":"given_names"},"str","^","/str",{"VAR=":"common_name"},"str","^","/str",{"VAR=":"surname"},"str","^unset","/str",{"VAR=":"baggage_style"},"str","^unset","/str",{"VAR=":"hair_detail"},"str","^unset","/str",{"VAR=":"complexion_detail"},"str","^unset","/str",{"VAR=":"face_detail"},"str","^unset","/str",{"VAR=":"outfit_detail"},"str","^unset","/str",{"VAR=":"supernatural_belief"},"str","^unset","/str",{"VAR=":"supernatural_senses"},"str","^unset","/str",{"VAR=":"viktor_relation"},0,{"VAR=":"lover"},0,{"VAR=":"sapphic"},0,{"VAR=":"detective"},0,{"VAR=":"careless"},0,{"VAR=":"eccentric"},0,{"VAR=":"class_confidence"},0,{"VAR=":"medium_reputation"},0,{"VAR=":"court_loyalty"},0,{"VAR=":"viktor_trust"},0,{"VAR=":"viktor_suspicion"},0,{"VAR=":"supernatural_exposure"},"/ev","end",null]}],"listDefs":{}} \ No newline at end of file diff --git a/data/ink/story.ink.json b/data/ink/story.ink.json deleted file mode 100644 index d360f20..0000000 --- a/data/ink/story.ink.json +++ /dev/null @@ -1 +0,0 @@ -{"inkVersion":21,"root":[["ev",{"VAR?":"DEBUG"},"/ev",[{"->":".^.b","c":true},{"b":["\n","^IN DEBUG MODE!","\n","ev","str","^Beginning...","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Framing Hooper...","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^In with Hooper...","/str","/ev",{"*":".^.c-2","flg":20},{"->":"0.5"},{"c-0":["^\t",{"->":"start"},"\n",{"#f":5}],"c-1":["^ ",{"->":"claim_hooper_took_component"},"\n",{"#f":5}],"c-2":["^ ",{"->":"inside_hoopers_hut"},"\n",{"#f":5}]}]}],[{"->":".^.b"},{"b":["\n",{"->":"start"},{"->":"0.5"},null]}],"nop","\n",["done",{"#n":"g-0"}],null],"done",{"lower":[{"temp=":"x"},"ev",{"VAR?":"x"},1,"-","/ev",{"temp=":"x","re":true},null],"raise":[{"temp=":"x"},"ev",{"VAR?":"x"},1,"+","/ev",{"temp=":"x","re":true},null],"start":[[["^They are keeping me waiting.","\n",["ev",{"^->":"start.0.g-0.2.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-0","flg":18},{"s":["^Hut 14",{"->":"$r","var":true},null]}],{"c-0":["ev",{"^->":"start.0.g-0.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.2.s"},[{"#n":"$r2"}],"^. The door was locked after I sat down. ","\n","^I don't even have a pen to do any work. There's a copy of the morning's intercept in my pocket, but staring at the jumbled letters will only drive me mad.","\n","^I am not a machine, whatever they say about me.","\n",{"->":".^.^.^.opts"},{"#f":5}],"#n":"g-0"}],{"opts":[["ev","visit",2,"MIN","/ev","ev","du",0,"==","/ev",{"->":".^.s0","c":true},"ev","du",1,"==","/ev",{"->":".^.s1","c":true},"ev","du",2,"==","/ev",{"->":".^.s2","c":true},"nop",{"s0":["pop",{"->":".^.^.23"},null],"s1":["pop","^I rattle my fingers on the field table.",{"->":".^.^.23"},null],"s2":["pop",{"->":".^.^.23"},null],"#f":5}],"\n","ev","str","^Think","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Plan","/str","/ev",{"*":".^.c-2","flg":20},"ev","str","^Wait","/str","/ev",{"*":".^.c-3","flg":20},{"c-1":["^ ","\n","^They suspect me to be a traitor. They think I stole the component from the calculating machine. They will be searching my bunk and cases.","\n","^When they don't find it, ","ev",{"CNT?":".^.^.c-2"},"/ev",[{"->":".^.b","c":true},{"b":["^then",{"->":".^.^.^.9"},null]}],"nop","^ they'll come back and demand I talk.","\n",{"->":".^.^"},{"->":".^.^.^.g-1"},{"#f":5}],"c-2":["\n","ev",{"CNT?":".^.^.c-1"},"!","/ev",[{"->":".^.b","c":true},{"b":["^What I am is",{"->":".^.^.^.7"},null]}],[{"->":".^.b"},{"b":["^I am",{"->":".^.^.^.7"},null]}],"nop","^ a problem—solver. Good with figures, quick with crosswords, excellent at chess.","\n","^But in this scenario — in this trap — what is the winning play?","\n",["ev","str","^Co—operate","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Dissemble","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Divert","/str","/ev",{"*":".^.c-2","flg":20},{"c-0":["^ ","\n","^I must co—operate. My credibility is my main asset. To contradict myself, or another source, would be fatal.","\n","^I must simply hope they do not ask the questions I do not want to answer.","\n","ev",{"^var":"forceful","ci":-1},{"f()":"lower"},"pop","/ev","\n",{"->":"start.0.g-1"},{"#f":5}],"c-1":["^ ","\n","^Misinformation, then. Just as the war in Europe is one of plans and interceptions, not planes and bombs.","\n","^My best hope is a story they prefer to the truth.","\n","ev",{"^var":"forceful","ci":-1},{"f()":"raise"},"pop","/ev","\n",{"->":"start.0.g-1"},{"#f":5}],"c-2":["^ ","\n","^Avoidance and delay. The military machine never fights on a single front. If I move slowly enough, things will resolve themselves some other way, my reputation intact.","\n","ev",{"^var":"evasive","ci":-1},{"f()":"raise"},"pop","/ev","\n",{"->":"start.0.g-1"},{"#f":5}]}],{"#f":5}],"c-3":["^\t\t","\n",{"->":".^.^.^.g-1"},{"#f":5}]}],"g-1":[{"->":"start.waited"},null]}],{"waited":[[["^Half an hour goes by before Commander Harris returns. He closes the door behind him quickly, as though afraid a loose word might slip inside.","\n","^\"Well, then,\" he begins, awkwardly. This is an unseemly situation.","\n",["ev",{"^->":"start.waited.0.g-0.4.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-0","flg":18},{"s":["^\"Commander.\"",{"->":"$r","var":true},null]}],["ev",{"^->":"start.waited.0.g-0.5.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str",{"CNT?":"start.0.opts.c-2.12.c-2"},"!","/ev",{"*":".^.^.c-1","flg":19},{"s":["^\"Tell me what this is about.\"",{"->":"$r","var":true},null]}],"ev","str","^Wait","/str","/ev",{"*":".^.c-2","flg":20},{"c-0":["ev",{"^->":"start.waited.0.g-0.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.4.s"},[{"#n":"$r2"}],"\n","^He nods. ","<>","\n",{"->":".^.^.^.g-1"},{"#f":5}],"c-1":["ev",{"^->":"start.waited.0.g-0.c-1.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.5.s"},[{"#n":"$r2"}],"\n","^He shakes his head.","\n","^\"Now, don't let's pretend.\"","\n",{"->":".^.^.^.g-1"},{"#f":5}],"c-2":["\n","^I say nothing.","\n",{"->":".^.^.^.g-1"},{"#f":5}],"#n":"g-0"}],{"g-1":["^He has brought two cups of tea in metal mugs: he sets them down on the tabletop between us.","\n","ev","str","^Deny","/str",{"CNT?":".^.^.g-0.c-1"},"/ev",{"*":".^.c-3","flg":21},"ev","str","^Take one","/str","/ev",{"*":".^.c-4","flg":20},["ev",{"^->":"start.waited.0.g-1.15.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str",{"CNT?":".^.^.^.g-0.c-1"},"!","/ev",{"*":".^.^.c-5","flg":19},{"s":["^\"What's going on?\"",{"->":"$r","var":true},null]}],"ev","str","^Wait","/str","/ev",{"*":".^.c-6","flg":20},{"c-3":["^ \"I'm not pretending anything.\"","\n","ev",{"CNT?":"start.0.opts.c-2.12.c-0"},"/ev",[{"->":".^.b","c":true},{"b":["^I'm lying already, despite my good intentions.",{"->":".^.^.^.6"},null]}],"nop","\n","^Harris looks disapproving. ",{"->":".^.^.c-6.3.pushes_cup"},"\n",{"->":".^.^.^.g-2"},{"#f":5}],"c-4":["\n","ev",true,"/ev",{"VAR=":"teacup","re":true},"^I take a mug and warm my hands. It's ","<>","\n",{"->":".^.^.^.g-2"},{"#f":5}],"c-5":["ev",{"^->":"start.waited.0.g-1.c-5.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.15.s"},[{"#n":"$r2"}],"\n","^\"You know already.\"","\n",{"->":".^.^.c-6.3.pushes_cup"},{"->":".^.^.^.g-2"},{"#f":5}],"c-6":["\n","^I wait for him to speak.","\n",[["^He pushes one mug halfway towards me: ","<>","\n",{"->":".^.^.^.^.^.g-2"},{"#n":"pushes_cup"}],null],{"#f":5}]}],"g-2":["^a small gesture of friendship.","\n","^Enough to give me hope?","\n","ev","str","^Take it","/str",{"VAR?":"teacup"},"!","/ev",{"*":".^.c-7","flg":21},"ev","str","^Don't take it","/str",{"VAR?":"teacup"},"!","/ev",{"*":".^.c-8","flg":21},"ev","str","^Drink","/str",{"VAR?":"teacup"},"/ev",{"*":".^.c-9","flg":21},"ev","str","^Wait","/str",{"VAR?":"teacup"},"/ev",{"*":".^.c-10","flg":21},{"c-7":["^ ","\n","^I ","ev",{"CNT?":".^.^.^.g-1.c-4"},"/ev",[{"->":".^.b","c":true},{"b":["^lift the mug",{"->":".^.^.^.8"},null]}],[{"->":".^.b"},{"b":["^take the mug,",{"->":".^.^.^.8"},null]}],"nop","^ and blow away the steam. It is too hot to drink.","\n","^Harris picks his own up and just holds it.","\n","ev",true,"/ev",{"VAR=":"teacup","re":true},"ev",{"^var":"forceful","ci":-1},{"f()":"lower"},"pop","/ev","\n",{"->":".^.^.^.g-3"},{"#f":5}],"c-8":["^ ","\n","^Just a cup of insipid canteen tea. I leave it where it is.","\n","ev",{"^var":"forceful","ci":-1},{"f()":"raise"},"pop","/ev","\n",{"->":".^.^.^.g-3"},{"#f":5}],"c-9":["^ ","\n","^I raise the cup to my mouth but it's too hot to drink.","\n",{"->":".^.^.^.g-3"},{"#f":5}],"c-10":["^ \t\t","\n","^I say nothing as ",{"->":".^.^.c-7"},"\n",{"->":".^.^.^.g-3"},{"#f":5}]}],"g-3":["^\"Quite a difficult situation,\" ","ev",{"CNT?":".^.^.g-2.c-7"},"/ev",[{"->":".^.b","c":true},{"b":["^he",{"->":".^.^.^.6"},null]}],[{"->":".^.b"},{"b":["^Harris",{"->":".^.^.^.6"},null]}],"nop","^ begins","ev",{"VAR?":"forceful"},0,"<=","/ev",[{"->":".^.b","c":true},{"b":["^, sternly",{"->":".^.^.^.14"},null]}],"nop","^. I've seen him adopt this stiff tone of voice before, but only when talking to the brass. \"I'm sure you agree.\"","\n","ev","str","^Agree","/str","/ev",{"*":".^.c-11","flg":20},"ev","str","^Disagree","/str","/ev",{"*":".^.c-12","flg":20},"ev","str","^Lie","/str","/ev",{"*":".^.c-13","flg":20},"ev","str","^Evade","/str","/ev",{"*":".^.c-14","flg":20},{"c-11":["^ ","\n","^\"Awkward,\" I reply","\n",{"->":".^.^.^.g-4"},{"#f":5}],"c-12":["^ ","\n","^\"I don't see why,\" I reply","\n","ev",{"^var":"forceful","ci":-1},{"f()":"raise"},"pop","/ev","\n","ev",{"^var":"evasive","ci":-1},{"f()":"raise"},"pop","/ev","\n",{"->":".^.^.^.g-4"},{"#f":5}],"c-13":["^ ",{"->":".^.^.c-12"},"\n",{"->":".^.^.^.g-4"},{"#f":5}],"c-14":["^ ","\n","^\"I'm sure you've handled worse,\" I reply casually","\n","ev",{"^var":"evasive","ci":-1},{"f()":"raise"},"pop","/ev","\n",{"->":".^.^.^.g-4"},{"#f":5}]}],"g-4":["ev",{"VAR?":"teacup"},"/ev",[{"->":".^.b","c":true},{"b":["\n","ev",true,"/ev",{"VAR=":"drugged","re":true},"<>","^, sipping at my tea as though we were old friends","\n",{"->":".^.^.^.4"},null]}],"nop","\n","<>","^.","\n",["ev","str","^Watch him","/str","/ev",{"*":".^.c-15","flg":20},"ev","str","^Wait","/str","/ev",{"*":".^.c-16","flg":20},"ev","str","^Smile","/str",{"CNT?":".^.^.^.g-3.c-12"},"!","/ev",{"*":".^.c-17","flg":21},{"c-15":["\n","^His face is telling me nothing. I've seen Harris broad and full of laughter. Today he is tight, as much part of the military machine as the device in Hut 5.","\n",{"->":".^.^.^.^.g-6"},{"#f":5}],"c-16":["\n","^I wait to see how he'll respond.","\n",{"->":".^.^.^.^.g-6"},{"#f":5}],"c-17":["\n","^I try a weak smile. It is not returned.","\n","ev",{"^var":"forceful","ci":-1},{"f()":"lower"},"pop","/ev","\n",{"->":".^.^.^.^.g-6"},{"#f":5}],"#n":"g-5"}],null],"g-6":["^\"We need that component,\" he says.","\n",["ev",{"CNT?":"missing_reel"},"!","/ev",[{"->":".^.b","c":true},{"b":["\n",{"->t->":"missing_reel"},{"->":"harris_demands_component"},{"->":".^.^.^.5"},null]}],"nop","\n",["ev","str","^Yes","/str","/ev",{"*":".^.c-18","flg":20},"ev","str","^No","/str","/ev",{"*":".^.c-19","flg":20},"ev","str","^Evade","/str","/ev",{"*":".^.c-20","flg":20},"ev","str","^Lie","/str","/ev",{"*":".^.c-21","flg":20},{"c-18":["\n","^\"Of course I do,\" I answer.","\n",{"->":".^.^.^.^.^.g-9"},{"#f":5}],"c-19":["\n","^\"No I don't. And I've got work to do...\"","\n","^\"Work that will be rather difficult for you to do, don't you think?\" Harris interrupts.","\n",{"->":".^.^.^.^.^.g-9"},{"#f":5}],"c-20":["\n",{"->":"here_at_bletchley_diversion"},{"->":".^.^.^.^.^.g-9"},{"#f":5}],"c-21":["^ ","\n",{"->":".^.^.c-19"},{"->":".^.^.^.^.^.g-9"},{"#f":5}],"#n":"g-8"}],{"#n":"g-7"}],null],"g-9":[{"->t->":"missing_reel"},{"->":"harris_demands_component"},null]}],null]}],"missing_reel":[["ev","str","^The stolen component...","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Shrug","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["\n","^I shrug.","\n","ev","void","/ev","->->",{"->":".^.^.g-0"},{"#f":5}],"g-0":["^The reel went missing from the Bombe this afternoon. The four of us were in the Hut, working on the latest German intercept. The results were garbage. It was Russell who found the gap in the plugboard.","\n",["^Any of us could have taken it; and no one else would have known its worth.","\n","ev","str","^Panic","/str",{"VAR?":"forceful"},0,"<=","/ev",{"*":".^.c-2","flg":21},"ev","str","^Calculate","/str","/ev",{"*":".^.c-3","flg":20},"ev","str","^Deny","/str",{"VAR?":"evasive"},0,">=","/ev",{"*":".^.c-4","flg":21},{"c-2":["^ They will pin it on me. They need a scapegoat so that the work can continue. I'm a likely target. Weaker than the rest. ","\n","ev",{"^var":"forceful","ci":-1},{"f()":"lower"},"pop","/ev","\n",{"->":".^.^.^.^.g-2"},{"#f":5}],"c-3":["^ My odds, then, are one in four. Not bad; although the stakes themselves are higher than I would like.","\n","ev",{"^var":"evasive","ci":-1},{"f()":"raise"},"pop","/ev","\n",{"->":".^.^.^.^.g-2"},{"#f":5}],"c-4":["^ But this is still a mere formality. The work will not stop. A replacement component will be made and we will all be put back to work. We are too valuable to shoot. ","\n","ev",{"^var":"forceful","ci":-1},{"f()":"raise"},"pop","/ev","\n",{"->":".^.^.^.^.g-2"},{"#f":5}],"#n":"g-1"}],null],"g-2":["ev","void","/ev","->->",null]}],{"#f":1}],"here_at_bletchley_diversion":[["^\"Here at Bletchley? Of course.\"","\n","ev",{"^var":"evasive","ci":-1},{"f()":"raise"},"pop","/ev","\n","ev",{"^var":"forceful","ci":-1},{"f()":"lower"},"pop","/ev","\n","^\"Here, now,\" Harris corrects. \"We are not talking to everyone. I can imagine you might feel pretty sore about that. I can imagine you feeling picked on. ","ev",{"VAR?":"forceful"},0,"<","/ev",[{"->":".^.b","c":true},{"b":["^You're a sensitive soul.",{"->":".^.^.^.21"},null]}],"nop","^\"","\n",["ev",{"^->":"here_at_bletchley_diversion.0.24.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","str","^.\"","/str","/ev",{"*":".^.^.c-0","flg":22},{"s":["^\"I'm fine",{"->":"$r","var":true},null]}],["ev",{"^->":"here_at_bletchley_diversion.0.25.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str",{"VAR?":"forceful"},0,"<","/ev",{"*":".^.^.c-1","flg":19},{"s":["^\"What do you mean by that?\"",{"->":"$r","var":true},null]}],["ev",{"^->":"here_at_bletchley_diversion.0.26.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","str","^.\"","/str",{"VAR?":"forceful"},0,">=","/ev",{"*":".^.^.c-2","flg":23},{"s":["^\"Damn right",{"->":"$r","var":true},null]}],"ev","str","^Be honest","/str","/ev",{"*":".^.c-3","flg":20},"ev","str","^Lie","/str","/ev",{"*":".^.c-4","flg":20},{"c-0":["ev",{"^->":"here_at_bletchley_diversion.0.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.24.s"},[{"#n":"$r2"}],"^,\" I reply. \"This is all some misunderstanding and the quicker we have it cleared up the better.\"","\n","ev",{"^var":"forceful","ci":-1},{"f()":"lower"},"pop","/ev","\n","^\"I couldn't agree more.\" And then he comes right out with it, with an accusation.","\n",{"->":".^.^.done"},{"#f":5}],"c-1":["ev",{"^->":"here_at_bletchley_diversion.0.c-1.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.25.s"},[{"#n":"$r2"}],"\n",{"->":".^.^.done"},{"#f":5}],"c-2":["ev",{"^->":"here_at_bletchley_diversion.0.c-2.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.26.s"},[{"#n":"$r2"}],"^ I'm sore. Was it one of the others who put you up to this? Was it Hooper? He's always been jealous of me. He's...\"","\n","ev",{"^var":"forceful","ci":-1},{"f()":"raise"},"pop","/ev","\n","ev",true,"/ev",{"VAR=":"hooper_mentioned","re":true},"^The Commander moustache bristles as he purses his lips. \"Has he now? Of your achievements, do you think?\"","\n","^It's difficult not to shake the sense that he's ","ev",{"VAR?":"evasive"},1,">","/ev",[{"->":".^.b","c":true},{"b":["^mocking",{"->":".^.^.^.28"},null]}],[{"->":".^.b"},{"b":["^simply humouring",{"->":".^.^.^.28"},null]}],"nop","^ me.","\n","^\"Or of your brain? Or something else?\"","\n",[["ev",{"^->":"here_at_bletchley_diversion.0.c-2.33.0.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","str","^\"","/str","/ev",{"*":".^.^.c-0","flg":22},{"s":["^\"Of my genius.",{"->":"$r","var":true},null]}],["ev",{"^->":"here_at_bletchley_diversion.0.c-2.33.1.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","str","^\"","/str","/ev",{"*":".^.^.c-1","flg":22},{"s":["^\"Of my standing.",{"->":"$r","var":true},null]}],"ev","str","^Evade","/str","/ev",{"*":".^.c-2","flg":20},{"c-0":["ev",{"^->":"here_at_bletchley_diversion.0.c-2.33.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.0.s"},[{"#n":"$r2"}],"^ Hooper simply can't stand that I'm cleverer than he is. We work so closely together, cooped up in that Hut all day. It drives him to distraction. To worse.\"","\n","^\"You're suggesting Hooper would sabotage this country's future simply to spite you?\" Harris chooses his words like the military man he is, each lining up to create a ring around me.","\n",["ev","str","^Yes","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^No","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Evade","/str","/ev",{"*":".^.c-2","flg":20},{"c-0":["^ \t\t\t","\n","^\"","ev",{"VAR?":"forceful"},0,">","/ev",[{"->":".^.b","c":true},{"b":["^He's petty enough, certainly",{"->":".^.^.^.10"},null]}],[{"->":".^.b"},{"b":["^I wouldn't put it past him",{"->":".^.^.^.10"},null]}],"nop","^. He's a creep.\" ","ev",{"VAR?":"teacup"},"/ev",[{"->":".^.b","c":true},{"b":["^ I set the teacup down.",{"->":".^.^.^.17"},null]}],[{"->":".^.b"},{"b":["^I wipe a hand across my forehead.",{"->":".^.^.^.17"},null]}],"nop","\n","ev",{"^var":"forceful","ci":-1},{"f()":"raise"},"pop","/ev","\n","ev",false,"/ev",{"VAR=":"teacup","re":true},{"->":".^.^.suggest_its_a_lie"},{"#f":5}],"c-1":["^ \t\t\t","\n","^\"No, ","ev",{"VAR?":"forceful"},0,">","/ev",[{"->":".^.b","c":true},{"b":["^of course not",{"->":".^.^.^.10"},null]}],[{"->":".^.b"},{"b":["^I suppose not",{"->":".^.^.^.10"},null]}],"nop","^.\" ","ev",{"VAR?":"teacup"},"/ev",[{"->":".^.b","c":true},{"b":["^I put the teacup back down on the table",{"->":".^.^.^.17"},null]}],[{"->":".^.b"},{"b":["^I push the teacup around on its base",{"->":".^.^.^.17"},null]}],"nop","^.","\n","ev",{"^var":"forceful","ci":-1},{"f()":"lower"},"pop","/ev","\n","ev",false,"/ev",{"VAR=":"teacup","re":true},{"->":".^.^.suggest_its_a_lie"},{"#f":5}],"c-2":["^ \t\t","\n","^\"I don't know what I'm suggesting. I don't understand what's going on.\"","\n","ev",{"^var":"evasive","ci":-1},{"f()":"raise"},"pop","/ev","\n","^\"But of course you do.\" Harris narrows his eyes.","\n",{"->":".^.^.^.^.^.^.done"},{"->":".^.^.suggest_its_a_lie"},{"#f":5}],"suggest_its_a_lie":["^\"All I can say is, ever since I arrived here, he's been looking to ways to bring me down a peg. I wouldn't be surprised if he set this whole affair up just to have me court—martialled.\"","\n","^\"We don't court—martial civilians,\" Harris replies. \"Traitors are simply hung at her Majesty's pleasure.\"","\n",["ev",{"^->":"here_at_bletchley_diversion.0.c-2.33.c-0.10.suggest_its_a_lie.4.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","str","^.\"","/str","/ev",{"*":".^.^.c-3","flg":22},{"s":["^\"Quite right",{"->":"$r","var":true},null]}],["ev",{"^->":"here_at_bletchley_diversion.0.c-2.33.c-0.10.suggest_its_a_lie.5.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","str","^.\"","/str","/ev",{"*":".^.^.c-4","flg":22},{"s":["^\"I'm no traitor",{"->":"$r","var":true},null]}],"ev","str","^Lie","/str","/ev",{"*":".^.c-5","flg":20},{"c-3":["ev",{"^->":"here_at_bletchley_diversion.0.c-2.33.c-0.10.suggest_its_a_lie.c-3.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.4.s"},[{"#n":"$r2"}],"^,\" I answer smartly.","\n",{"->":".^.^.^.g-0"},{"#f":5}],"c-4":["ev",{"^->":"here_at_bletchley_diversion.0.c-2.33.c-0.10.suggest_its_a_lie.c-4.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.5.s"},[{"#n":"$r2"}],"^,\" I answer","ev",{"VAR?":"forceful"},0,">","/ev",[{"->":".^.b","c":true},{"b":["^smartly",{"->":".^.^.^.14"},null]}],[{"->":".^.b"},{"b":["^, voice quivering. \"For God's sake!\"",{"->":".^.^.^.14"},null]}],"nop","\n",{"->":".^.^.^.g-0"},{"#f":5}],"c-5":["^ ",{"->":".^.^.c-4"},"\n",{"->":".^.^.^.g-0"},{"#f":5}]}],"g-0":["^He stares back at me.","\n",{"->":".^.^.^.^.^.^.done"},null]}],{"#f":5}],"c-1":["ev",{"^->":"here_at_bletchley_diversion.0.c-2.33.c-1.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.1.s"},[{"#n":"$r2"}],"^ My reputation.\" ","ev",{"VAR?":"forceful"},0,">","/ev",[{"->":".^.b","c":true},{"b":["^I'm aware of how arrogant I must sound but I plough on all the same.",{"->":".^.^.^.14"},null]}],[{"->":".^.b"},{"b":["^I don't like to talk of myself like this, but I carry on all the same.",{"->":".^.^.^.14"},null]}],"nop","^ \"Hooper simply can't bear knowing that, once all this is over, I'll be the one receiving the knighthood and he...\"","\n","^\"No—one will be getting a knighthood if the Germans make landfall,\" Harris answers sharply. He casts a quick eye to the door of the Hut to check the latch is still down, then continues in more of a murmur: \"Not you and not Hooper. Now answer me.\"","\n","^For the first time since the door closed, I wonder what the threat might be if I do not.","\n",{"->":".^.^.^.^.done"},{"#f":5}],"c-2":["^ \t\t\t\t","\n","ev",false,"/ev",{"VAR=":"teacup","re":true},"ev",{"^var":"forceful","ci":-1},{"f()":"raise"},"pop","/ev","\n","^\"How should I know?\" I reply, defensively. ","ev",{"VAR?":"teacup"},"/ev",[{"->":".^.b","c":true},{"b":["^I set the teacup back on the table.",{"->":".^.^.^.17"},null]}],"nop","^ ",{"->":".^.^.c-0.10.suggest_its_a_lie"},"\n",{"->":".^.^.^.^.done"},{"#f":5}]}],{"#f":5}],"c-3":["^ \t",{"->":".^.^.c-2"},"\n",{"->":".^.^.done"},{"#f":5}],"c-4":["^ \t\t",{"->":".^.^.c-0"},"\n",{"->":".^.^.done"},{"#f":5}],"done":[{"->":"harris_demands_component"},null]}],{"#f":1}],"harris_demands_component":[["^\"","ev",{"CNT?":"here_at_bletchley_diversion"},"/ev",[{"->":".^.b","c":true},{"b":["^Please",{"->":".^.^.^.6"},null]}],[{"->":".^.b"},{"b":["^So",{"->":".^.^.^.6"},null]}],"nop","^. Do you have it?\" Harris is ","ev",{"VAR?":"forceful"},3,">","/ev",[{"->":".^.b","c":true},{"b":["^sweating slightly",{"->":".^.^.^.15"},null]}],[{"->":".^.b"},{"b":["^wasting no time",{"->":".^.^.^.15"},null]}],"nop","^: Bletchley is his watch. \"Do you know where it is?\"","\n","ev","str","^Yes","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^No","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Lie","/str","/ev",{"*":".^.c-2","flg":20},"ev","str","^Evade","/str","/ev",{"*":".^.c-3","flg":20},{"c-0":["\n","^\"I do.\"","\n",{"->":"admitted_to_something"},{"->":".^.^.g-0"},{"#f":5}],"c-1":["^ \"I have no idea.\" ","\n",{"->":".^.^.silence"},{"->":".^.^.g-0"},{"#f":5}],"c-2":["^ \t\t",{"->":".^.^.c-1"},"\n",{"->":".^.^.g-0"},{"#f":5}],"c-3":["^ \t\t","\n","^\"The component?\"","\n","ev",{"^var":"evasive","ci":-1},{"f()":"raise"},"pop","/ev","\n","ev",{"^var":"forceful","ci":-1},{"f()":"lower"},"pop","/ev","\n","^\"Don't play stupid,\" he replies. \"","ev",{"CNT?":"missing_reel"},"!","/ev",[{"->":".^.b","c":true},{"b":["^The component that went missing this afternoon. ",{"->":".^.^.^.22"},null]}],"nop","^Where is it?\"","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":["ev",{"CNT?":"missing_reel"},"!","/ev",[{"->":".^.b","c":true},{"b":["\n",{"->t->":"missing_reel"},{"->":".^.^.^.5"},null]}],"nop","\n","ev","str","^Co-operate","/str","/ev",{"*":".^.c-4","flg":20},"ev","str","^Delay","/str","/ev",{"*":".^.c-5","flg":20},"ev","str","^Lie","/str","/ev",{"*":".^.c-6","flg":20},"ev","str","^Evade","/str","/ev",{"*":".^.c-7","flg":20},{"c-4":["^ \"I know where it is.\"","\n",{"->":"admitted_to_something"},{"->":".^.^.^.silence"},{"#f":5}],"c-5":["^ \"I know nothing about it.\" My voice shakes","ev",{"VAR?":"forceful"},0,">","/ev",[{"->":".^.b","c":true},{"b":["^ with anger",{"->":".^.^.^.8"},null]}],[{"->":".^.b"},{"b":["^; I'm unaccustomed to facing off against men with holstered guns",{"->":".^.^.^.8"},null]}],"nop","^. ","\n",{"->":".^.^.^.silence"},{"#f":5}],"c-6":["^ ",{"->":".^.^.c-5"},"\n",{"->":".^.^.^.silence"},{"#f":5}],"c-7":["^ ","\n","^\"I don't know what gives you the right to pick on me. ","ev",{"VAR?":"forceful"},0,">","/ev",[{"->":".^.b","c":true},{"b":["^I demand a lawyer.",{"->":".^.^.^.10"},null]}],[{"->":".^.b"},{"b":["^I want a lawyer.",{"->":".^.^.^.10"},null]}],"nop","^\"","\n","^\"This is time of war,\" Harris answers. \"And by God, if I have to shoot you to recover the component, I will. Understand?\" He points at the mug, ",{"->":".^.^.^.silence.drinkit"},"\n",{"->":".^.^.^.silence"},{"#f":5}]}],"silence":["^There's an icy silence. ","ev",{"VAR?":"forceful"},2,">","/ev",[{"->":".^.b","c":true},{"b":["^I've cracked him a little.",{"->":".^.^.^.8"},null]}],[{"->":".^.b"},{"b":["ev",{"VAR?":"evasive"},2,">","/ev",[{"->":".^.b","c":true},{"b":["^He's tiring of my evasiveness.",{"->":".^.^.^.6"},null]}],"nop",{"->":".^.^.^.8"},null]}],"nop","\n",["^\"Now drink your tea and talk.\"","\n","ev","str","^Drink","/str",{"VAR?":"teacup"},"/ev",{"*":".^.c-8","flg":21},"ev","str","^Put the cup down","/str",{"VAR?":"teacup"},"/ev",{"*":".^.c-9","flg":21},"ev","str","^Take the cup","/str",{"VAR?":"teacup"},"!","/ev",{"*":".^.c-10","flg":21},"ev","str","^Don't take it","/str",{"VAR?":"teacup"},"!","/ev",{"*":".^.c-11","flg":21},{"c-8":["^ \t\t\t",{"->":".^.^.c-10.2.drinkfromcup"},"\n",{"->":".^.^.^.^.g-1"},{"#f":5}],"c-9":["^ ","\n","^I set the cup carefully down on the table once more.","\n","ev",false,"/ev",{"VAR=":"teacup","re":true},"ev",{"^var":"forceful","ci":-1},{"f()":"raise"},"pop","/ev","\n",{"->":".^.^.c-11.10.whatsinit"},{"->":".^.^.^.^.g-1"},{"#f":5}],"c-10":["^ ","\n",[["^I lift the cup ","ev",{"VAR?":"teacup"},"/ev",[{"->":".^.b","c":true},{"b":["^to my lips ",{"->":".^.^.^.5"},null]}],"nop","^and sip. He waits for me to swallow before speaking again.","\n","ev",true,"/ev",{"VAR=":"drugged","re":true},"ev",true,"/ev",{"VAR=":"teacup","re":true},{"->":".^.^.^.^.^.^.g-1"},{"#f":5,"#n":"drinkfromcup"}],null],{"#f":5}],"c-11":["^ ","\n","^I leave the cup where it is.","\n","ev",{"^var":"forceful","ci":-1},{"f()":"raise"},"pop","/ev","\n",[["^\"Why?\" I ask coldly. \"What's in it?\"","\n",{"->":".^.^.^.^.^.^.g-1"},{"#n":"whatsinit"}],null],{"#f":5}],"#n":"drinkit"}],null],"g-1":["^\"Lapsang Souchong,\" he ","ev",{"CNT?":".^.^.silence.drinkit.c-10.2.drinkfromcup"},"/ev",[{"->":".^.b","c":true},{"b":["^remarks",{"->":".^.^.^.6"},null]}],[{"->":".^.b"},{"b":["^replies",{"->":".^.^.^.6"},null]}],"nop","^, placing his own cup back on the table untouched. \"Such a curious flavour. It might almost not be tea at all. You might say it hides a multitude of sins. As do you. Isn't that right?\"","\n","ev","str","^Agree","/str","/ev",{"*":".^.c-12","flg":20},"ev","str","^Disagree","/str",{"VAR?":"drugged"},"!","/ev",{"*":".^.c-13","flg":21},"ev","str","^Disagree","/str",{"VAR?":"drugged"},"/ev",{"*":".^.c-14","flg":21},"ev","str","^Lie","/str",{"VAR?":"drugged"},"/ev",{"*":".^.c-15","flg":21},"ev","str","^Lie","/str",{"VAR?":"drugged"},"!","/ev",{"*":".^.c-16","flg":21},"ev","str","^Evade","/str",{"VAR?":"drugged"},"/ev",{"*":".^.c-17","flg":21},"ev","str","^Evade","/str",{"VAR?":"drugged"},"!","/ev",{"*":".^.c-18","flg":21},{"c-12":["^ ","\n","^\"I suppose so,\" I reply. \"I've done things I shouldn't have done.\"","\n","ev",{"^var":"forceful","ci":-1},{"f()":"lower"},"pop","/ev","\n",{"->":"harris_presses_for_details"},{"#f":5}],"c-13":["\n","^\"I've done nothing that I'm ashamed of.\"","\n",{"->":"harris_asks_for_theory"},{"#f":5}],"c-14":["^ ","\n","^I open my mouth to disagree, but the words I want won't come. It is like Harris has taken a screwdriver to the sides of my jaw.","\n",{"->":"admitted_to_something.ive_done_things"},{"#f":5}],"c-15":["^ \t",{"->":".^.^.c-14"},"\n",{"#f":5}],"c-16":["^ \t",{"->":".^.^.c-13"},"\n",{"#f":5}],"c-17":["^ ",{"->":".^.^.c-14"},"\n",{"#f":5}],"c-18":["^ ","\n","^\"None of us are blameless, Harris. ","ev",{"VAR?":"forceful"},1,">","/ev",[{"->":".^.b","c":true},{"b":["^But you're not my priest and I'm not yours",{"->":".^.^.^.10"},null]}],[{"->":".^.b"},{"b":["^But I've done nothing to deserve this treatment",{"->":".^.^.^.10"},null]}],"nop","^. Now, please. Let me go. I'll help you find this damn component, of course I will.\"","\n","^He appears to consider the offer.","\n",{"->":"harris_asks_for_theory"},{"#f":5}]}]}],null],"harris_presses_for_details":[["^\"You mean you've left yourself open,\" Harris answers. \"To pressure. Is that what you're saying?\"","\n","ev","str","^Yes","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^No","/str",{"VAR?":"drugged"},"!","/ev",{"*":".^.c-1","flg":21},"ev","str","^No","/str",{"VAR?":"drugged"},"/ev",{"*":".^.c-2","flg":21},"ev","str","^Evade","/str",{"VAR?":"drugged"},"!","/ev",{"*":".^.c-3","flg":21},"ev","str","^Evade","/str",{"VAR?":"drugged"},"/ev",{"*":".^.c-4","flg":21},{"c-0":["^ ",{"->":".^.^.^.admit_open_to_pressure"},"\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["^ ","\n","^\"I'm not saying anything of the sort,\" I snap back. \"What is this, Harris? You're accusing me of treachery but I don't see a shred of evidence for it! Why don't you put your cards on the table?\"","\n","ev",{"^var":"forceful","ci":-1},{"f()":"raise"},"pop","/ev","\n",{"->":".^.^.g-0"},{"#f":5}],"c-2":["^ ","\n","^I shake my head violently, to say no, that's not it, but whatever is wrong with tongue is wrong with neck too. I look across at the table at Harris' face and realise with a start how sympathetic he is. Such a kind, generous man. How can I hold anything back from him?","\n","ev",{"^var":"forceful","ci":-1},{"f()":"lower"},"pop","/ev","\n","^I take another mouthful of the bitter, strange—tasting tea before answering.","\n",{"->":".^.^.^.admit_open_to_pressure"},{"->":".^.^.g-0"},{"#f":5}],"c-3":["^ ","\n","^\"You're the one applying pressure here,\" I answer ","ev",{"VAR?":"forceful"},1,">","/ev",[{"->":".^.b","c":true},{"b":["^smartly",{"->":".^.^.^.10"},null]}],[{"->":".^.b"},{"b":["^somewhat miserably",{"->":".^.^.^.10"},null]}],"nop","^. \"I'm just waiting until you tell me what is really going on.\"","\n","ev",{"^var":"evasive","ci":-1},{"f()":"raise"},"pop","/ev","\n",{"->":".^.^.g-0"},{"#f":5}],"c-4":["^ \t\t\t\t ","\n","^\"We're all under pressure here.\"","\n","^He looks at me with pity. ",{"->":"harris_has_seen_it_before"},"\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":["^\"It's simple enough,\" Harris says. ",{"->":"harris_has_seen_it_before"},"\n",null]}],{"admit_open_to_pressure":["^\"That's it,\" I reply. \"There are some things... which a man shouldn't do.\"","\n","ev",true,"/ev",{"VAR=":"admitblackmail","re":true},"^Harris doesn't stiffen. Doesn't lean away, as though my condition might be infectious. I had thought they trained them in the army to shoot my kind on sight.","\n","^He offers no sympathy either. He nods, once. His understanding of me is a mere turning cog in his calculations, with no meaning to it.","\n",{"->":"harris_has_seen_it_before"},null]}],"admitted_to_something":[["ev",{"VAR?":"drugged"},"!","/ev",[{"->":".^.b","c":true},{"b":["\n","^Harris stares back at me. ","ev",{"VAR?":"evasive"},0,"==","/ev",[{"->":".^.b","c":true},{"b":["^He cannot have expected it to be so easy to break me.",{"->":".^.^.^.8"},null]}],"nop","\n",{"->":".^.^.^.6"},null]}],[{"->":".^.b"},{"b":["\n","^Harris smiles with satisfaction, as if your willingness to talk was somehow his doing.","\n",{"->":".^.^.^.6"},null]}],"nop","\n","^\"I see.\"","\n","^There's a long pause, like the delay between feeding a line of cypher into the Bombe and waiting for its valves to warm up enough to begin processing.","\n","^\"You want to explain that?\"","\n","ev","str","^Explain","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Don't explain","/str",{"VAR?":"drugged"},"!","/ev",{"*":".^.c-1","flg":21},"ev","str","^Lie","/str",{"VAR?":"drugged"},"!","/ev",{"*":".^.c-2","flg":21},"ev","str","^Evade","/str",{"VAR?":"drugged"},"!","/ev",{"*":".^.c-3","flg":21},"ev","str","^Say nothing","/str",{"VAR?":"drugged"},"/ev",{"*":".^.c-4","flg":21},{"c-0":["^ ","\n","^I pause a moment, trying to choose my words. To just come out and say it, after a lifetime of hiding... that is a circle I cannot square.","\n",["ev","str","^Explain","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Say nothing","/str",{"VAR?":"drugged"},"/ev",{"*":".^.c-1","flg":21},"ev","str","^Lie","/str",{"VAR?":"drugged"},"!","/ev",{"*":".^.c-2","flg":21},{"c-0":["^ \t",{"->":".^.^.^.^.^.ive_done_things"},"\n",{"#f":5}],"c-1":["^ \t",{"->":".^.^.^.^.c-4"},"\n",{"#f":5}],"c-2":["^ \t",{"->":"claim_hooper_took_component"},"\n",{"#f":5}]}],{"#f":5}],"c-1":["\n","^\"There's nothing to explain,\" I reply stiffly. ",{"->":".^.^.^.i_know_where"},"\n",{"#f":5}],"c-2":["^ ",{"->":"claim_hooper_took_component"},"\n",{"#f":5}],"c-3":["\n","^\"Explain what you should be doing, do you mean, rather than bullying me? Certainly.\" I fold my arms. ",{"->":".^.^.^.i_know_where"},"\n",{"#f":5}],"c-4":["\n","^I fold my arms, intended firmly to say nothing. But somehow, watching Harris' face, I cannot bring myself to do it. I want to confess. I want to tell him everything I can, to explain myself to him, to earn his forgiveness. The sensation is so strong my will is powerless in the face of it.","\n","^Something is wrong with me, I am sure of it. There is a strange, bitter flavour on my tongue. I taste it as words start to form.","\n",{"->":".^.^.^.ive_done_things"},{"#f":5}]}],{"i_know_where":["^\"I know where your component is because it's obvious where your component is. That doesn't mean I took it, just because I can figure out a simple problem, any more than it means I'm a German spy because I can crack their codes.\"","\n",{"->":"harris_asks_for_theory"},null],"ive_done_things":["^\"I've done things,\" I begin","ev",{"CNT?":"harris_demands_component.0.g-1.c-14"},"/ev",[{"->":".^.b","c":true},{"b":["^ helplessly",{"->":".^.^.^.5"},null]}],"nop","^. \"Things I didn't want to do. I tried not to. But in the end, it felt like cutting off my own arm to resist.\"","\n",{"->":"harris_presses_for_details"},null]}],"harris_asks_for_theory":[["^\"Tell me, then,\" he asks. \"What's your theory? You're a smart fellow — as smart as they come around here, and that's saying something. What's your opinion on the missing component? Accident, perhaps? Or do you blame one of the other men? ","ev",{"VAR?":"hooper_mentioned"},"/ev",[{"->":".^.b","c":true},{"b":["^Hooper?",{"->":".^.^.^.5"},null]}],"nop","^\"","\n","ev","str","^Blame no—one","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Blame someone","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["^ ","\n",{"->":".^.^.^.an_accident"},{"#f":5}],"c-1":["^ ",{"->":"claim_hooper_took_component"},"\n",{"#f":5}]}],{"an_accident":[["^\"An accident, naturally.\" I risk a smile. \"That damned machine is made from spare parts and string. Even these Huts leak when it rains. It wouldn't take more than one fellow to trip over a cable to shake out a component. Have you tried looking under the thing?\"","\n","^\"Do you believe we haven't?\"","\n","^In a sudden moment I understand that his reply is a threat.","\n","^\"Now,\" he continues. \"Are you sure there isn't anything you want to tell me?\"","\n","ev","str","^Co-operate","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Evade","/str",{"VAR?":"evasive"},0,">","/ev",{"*":".^.c-1","flg":21},{"c-0":["\n","^\"All right.\" With a sigh, your defiance collapses. \"If you're searched my things then I suppose you've found ","ev",{"VAR?":"evasive"},1,">","/ev",[{"->":".^.b","c":true},{"b":["^ what you need",{"->":".^.^.^.9"},null]}],[{"->":".^.b"},{"b":["^my letters. Haven't you? In fact, if you haven't, don't tell me",{"->":".^.^.^.9"},null]}],"nop","^.","\n","ev",true,"/ev",{"VAR=":"admitblackmail","re":true},"^Harris nods once.","\n","<>","^ ",{"->":"harris_has_seen_it_before"},"\n",{"#f":5}],"c-1":["^ \"Only that you're being unreasonable, and behaving like a swine.\"","\n","^\"You imbecile,\" Harris replies, with sudden force. He is half out of his chair. \"You know the situation as well as I do. Why the fencing? The Hun are poised like rats, ready to run all over this country. They'll destroy everything. You understand that, don't you? You're not so locked up inside your crossword puzzles that you don't see that, are you? This machine we have here — you men — you are the best and only hope this country has. God help her.\"","\n","ev",true,"/ev",{"VAR=":"losttemper","re":true},"^I sit back, startled by the force of his outburst. His carefully sculpted expression has curled to angry disgust. He really does hate me, I think. He'll have my blood for the taste of it.","\n",["ev","str","^Placate","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Mock","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Dismiss","/str","/ev",{"*":".^.c-2","flg":20},{"c-0":["\n","^\"Now steady on,\" I reply, gesturing for him to be calm.","\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["^ ","\n","^\"I can imagine how being surrounded by clever men is pretty threatening for you, Commander,\" I reply with a sneer. \"They don't train you to think in the Armed Forces.\"","\n","ev",{"^var":"forceful","ci":-1},{"f()":"raise"},"pop","/ev","\n",{"->":".^.^.g-0"},{"#f":5}],"c-2":["\n","^\"Then I'll be going, on and getting on with my job of saving her, shall I?\" I even rise half to my feet, before he slams the tabletop.","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":["^\"Talk,\" Harris demands. \"Talk now. Tell me where you've hidden it or who you passed it to. Or God help me, I'll take your wretched pansy body to pieces looking for it.\"","\n",{"->":"harris_demands_you_speak"},null]}],{"#f":5}]}],null]}],"harris_has_seen_it_before":[["^\"I've seen it before. A young man like you — clever, removed. The kind that doesn't go to parties. Who takes himself too seriously. Who takes things too far.\"","\n","^He slides his thumb between two fingers.","\n","^\"Now they own you.\"","\n","ev","str","^Agree","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Disagree","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Lie","/str",{"VAR?":"drugged"},"!","/ev",{"*":".^.c-2","flg":21},"ev","str","^Apologise","/str",{"VAR?":"drugged"},{"VAR?":"forceful"},0,"<","&&","/ev",{"*":".^.c-3","flg":21},{"c-0":["^ ","\n","^\"What could I do?\" I'm shaking now. The night is cold and the heat—lamp in the Hut has been removed. \"","ev",{"VAR?":"forceful"},2,">","/ev",[{"->":".^.b","c":true},{"b":["^I won't",{"->":".^.^.^.10"},null]}],[{"->":".^.b"},{"b":["^I don't want to",{"->":".^.^.^.10"},null]}],"nop","^ go to prison.\"","\n","^\"Smart man,\" he replies. \"You wouldn't last.","\n",{"->":".^.^.tell_me_now"},{"#f":5}],"c-1":["^ ","\n","^\"I can still fix this.\"","\n","^Harris shakes his head. \"You'll do nothing. This is beyond you now. You may go to prison or may go to firing squad - or we can change your name and move you somewhere where your indiscretions can't hurt you. But right now, none of that matters. What happens to you doesn't matter. All that matters is where that component is.","\n",{"->":".^.^.tell_me_now"},{"#f":5}],"c-2":["^ ","\n","^\"I wanted to tell you,\" I tell him. \"I thought I could find out who they were. Lead you to them.\"","\n","^Harris looks at me with contempt. \"You wretch. You'll pay for what you've done to this country today. If a single man loses his life because of your pride and your perversions then God help your soul.","\n",{"->":".^.^.tell_me_now"},{"#f":5}],"c-3":["\n","^\"Harris, I...\"","\n","ev",{"^var":"forceful","ci":-1},{"f()":"lower"},"pop","/ev","\n","^\"Stop it,\" he interrupts. \"There's no jury here to sway. And there's no time.","\n",{"->":".^.^.tell_me_now"},{"#f":5}],"tell_me_now":["<>","^ So why don't you tell me, right now. Where is it?\"","\n",{"->":"harris_demands_you_speak"},null]}],null],"harris_demands_you_speak":[["^His eyes bear down like carbonised drill—bits.","\n","ev","str","^Confess","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Dissemble","/str",{"VAR?":"drugged"},"!","/ev",{"*":".^.c-1","flg":21},"ev","str","^Dissemble","/str",{"VAR?":"drugged"},"/ev",{"*":".^.c-2","flg":21},{"c-0":["^ ","\n","ev",{"VAR?":"forceful"},1,">","/ev",[{"->":".^.b","c":true},{"b":["\n","^\"You want me to tell you what happened? You'll be disgusted.\"","\n",{"->":".^.^.^.9"},null]}],[{"->":".^.b"},{"b":["\n","^\"All right. I'll tell you what happened.\" And never mind my shame.","\n",{"->":".^.^.^.9"},null]}],"nop","\n","^\"I can imagine how it starts,\" he replies.","\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["^ ",{"->":"claim_hooper_took_component"},"\n",{"->":".^.^.g-0"},{"#f":5}],"c-2":["\n","^My plan now is to blame Hooper, but I cannot seem to tell the story. Whatever they put in my tea, it rules my tongue. ","ev",{"VAR?":"forceful"},1,">","/ev",[{"->":".^.b","c":true},{"b":["^I fight it as hard as I can but it does no good.",{"->":".^.^.^.9"},null]}],[{"->":".^.b"},{"b":["^I am desperate to tell him everything. I am weeping with shame.",{"->":".^.^.^.9"},null]}],"nop","\n","ev",{"^var":"forceful","ci":-1},{"f()":"lower"},"pop","/ev","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":[{"->":"i_met_a_young_man"},null]}],null],"i_met_a_young_man":[["ev","str","^Talk","/str","/ev",{"*":".^.c-0","flg":20},{"c-0":["\n","^\"There was a young man. I met him in the town. A few months ago now. We got to talking. Not about work. And I used my cover story, but he seemed to know it wasn't true. That got me wondering if he might be one of us.\"","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":["^Harris is not letting me off any more.","\n","^\"You seriously entertained that possibility?\"","\n","ev","str","^Yes","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^No","/str","/ev",{"*":".^.c-2","flg":20},"ev","str","^Lie","/str","/ev",{"*":".^.c-3","flg":20},{"c-1":["\n","^\"Yes, I considered it. ","<>","\n",{"->":".^.^.^.g-1"},{"#f":5}],"c-2":["^ ","\n","^\"No. Not for more than a moment, of course. Everyone here is marked out by how little we would be willing to say about it.\"","\n","^\"Only you told this young man more than a little, didn't you?\"","\n","^I nod. \"","<>","\n",{"->":".^.^.^.g-1"},{"#f":5}],"c-3":["^ ","\n","^\"I was quite certain, after a while. After we'd been talking. ","<>","\n",{"->":".^.^.^.g-1"},{"#f":5}]}],"g-1":["^He seemed to know all about me. He... he was quite enchanted by my achievements.\"","\n","^The way Harris is staring I expect him to strike me, but he does not. He replies, \"I can see how that must have been attractive to you,\" with such plain—spokeness that I think I must have misheard.","\n","ev","str","^Yes","/str","/ev",{"*":".^.c-4","flg":20},"ev","str","^No","/str",{"VAR?":"drugged"},"!","/ev",{"*":".^.c-5","flg":21},"ev","str","^No","/str",{"VAR?":"drugged"},"/ev",{"*":".^.c-6","flg":21},"ev","str","^Lie","/str",{"VAR?":"drugged"},"!","/ev",{"*":".^.c-7","flg":21},{"c-4":["^ \"It's a lonely life in this place,\" I reply. \"Lonely - and still one never gets a moment to oneself.\"","\n","^\"That's how it is in the Service,\" Harris answers.","\n",["ev","str","^Argue","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Agree","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["^ \"I'm not in the Service.\"","\n","^Harris shakes his head. \"Yes, you are.\"","\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["^ \"Perhaps. But I didn't choose this life.\" ","\n","^Harris shakes his head. \"No. And there's plenty of others who didn't who are suffering far worse.\"","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":["^Then he waves the thought aside.","\n",{"->":".^.^.^.^.^.g-2"},null]}],{"#f":5}],"c-5":["^ \"The boy was a pretty simpleton. Quite inferior. His good opinion meant nothing to be. Harris, do not misunderstand. I was simply after his body.\"","\n","ev",{"^var":"evasive","ci":-1},{"f()":"raise"},"pop","/ev","\n","^Harris, to his credit, doesn't flinch; but I can see he will have nightmares of this moment later tonight. I'm tempted to reach out and take his hand to worsen it for him.","\n",{"->":".^.^.^.g-2"},{"#f":5}],"c-6":["^ ","\n","^\"It wasn't,\" I reply. \"But I doubt you'd understand.\"","\n","^He simply nods.","\n",{"->":".^.^.^.g-2"},{"#f":5}],"c-7":["^ ",{"->":".^.^.c-5"},"\n",{"->":".^.^.^.g-2"},{"#f":5}]}],"g-2":["^\"Go on with your confession.\"","\n",["ev",{"CNT?":".^.^.^.g-1.c-5"},"!","/ev",[{"->":".^.b","c":true},{"b":["\n","^That gives me pause. I hadn't thought of it as such. But I suppose he's right. I am about to admit what I did.","\n",{"->":".^.^.^.5"},null]}],"nop","\n","^\"There's not much else to say. I took the part from Bombe computing device. You seem to know that already. I had to. He was going to expose me if I didn't.\"","\n","^\"This young man was blackmailing you over your affair?\"","\n","ev",{"VAR?":"drugged"},"/ev",{"temp=":"harris_thinks_youre_drugged"},"ev",{"VAR?":"drugged"},"/ev",[{"->":".^.b","c":true},{"b":["\n","ev",false,"/ev",{"VAR=":"drugged","re":true},"^As Harris speaks I find myself suddenly sharply aware, as if waking from a long sleep. The table, the corrugated walls of the hut, everything seems suddenly more tangible than a moment before.","\n","^Whatever it was they put in my drink is wearing off.","\n",{"->":".^.^.^.19"},null]}],"nop","\n","ev","str","^Yes","/str","/ev",{"*":".^.c-8","flg":20},"ev","str","^No","/str","/ev",{"*":".^.c-9","flg":20},"ev","str","^Tell the truth","/str","/ev",{"*":".^.c-10","flg":20},"ev","str","^Lie","/str","/ev",{"*":".^.c-11","flg":20},{"c-8":["^ ","\n","^\"Yes. I suppose he was their agent. I should have realised but I didn't. Then he threatened to tell you. I thought you would have me locked up: I couldn't bear the thought of it. I love working here. I've never been so happy, so successful, anywhere before. I didn't want to lose it.\"","\n","^\"So what did you do with the component?\" Harris talks urgently. He grips his gloves tightly in one hand, perhaps prepared to lift them and strike if it is required. \"Have you passed it to this man already? Have you left it somewhere for him to find?\"","\n",["ev","str","^I have it","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^I don't have it","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Lie","/str","/ev",{"*":".^.c-2","flg":20},"ev","str","^Tell the truth","/str","/ev",{"*":".^.c-3","flg":20},{"c-0":["^ \t","\n","^\"I still have it. Not on me, of course. ",{"->":"reveal_location_of_component"},"\n",{"#f":5}],"c-1":["^ \t",{"->":".^.^.^.^.^.^.^.i_dont_have_it"},"\n",{"#f":5}],"c-2":["^ \t\t\t\t\t\t\t",{"->":".^.^.c-1"},"\n",{"#f":5}],"c-3":["^ \t\t\t\t",{"->":".^.^.c-0"},"\n",{"#f":5}]}],{"#f":5}],"c-9":["^ ","\n","^\"No, Harris. The young man wasn't blackmailing me.\" I take a deep breath. \"It was Hooper.\"","\n","ev",{"VAR?":"hooper_mentioned"},"!","/ev",[{"->":".^.b","c":true},{"b":["\n","^\"Hooper!\" Harris exclaims, in surprise. ","ev",{"VAR?":"harris_thinks_youre_drugged"},"/ev",[{"->":".^.b","c":true},{"b":["^He does not doubt me for a moment.",{"->":".^.^.^.6"},null]}],"nop","\n",{"->":".^.^.^.10"},null]}],[{"->":".^.b"},{"b":["\n","^\"Now look here,\" Harris interrupts. \"Don't start that again.\"","\n",{"->":".^.^.^.10"},null]}],"nop","\n","^\"It's the truth, Harris. If I'm going to jail, so be it, but I won't hang at Traitor's Gate. Hooper was the one who told the boy about our work. Hooper put the boy on to me. ","ev",{"VAR?":"forceful"},2,"<","/ev",[{"->":".^.b","c":true},{"b":["^I should have realised, of course. These things don't happen by chance. I was a fool to think they might.",{"->":".^.^.^.19"},null]}],"nop","^ And then, once he had me compromised, he demanded I steal the part from the machine.\"","\n","ev",true,"/ev",{"VAR=":"revealedhooperasculprit","re":true},"^\"Which you did.\" Harris leans forward. \"And then what? You still have it? You've stashed it somewhere?\"","\n",["ev","str","^Yes","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^No","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Lie","/str","/ev",{"*":".^.c-2","flg":20},"ev","str","^Evade","/str","/ev",{"*":".^.c-3","flg":20},{"c-0":["^ ","\n","^\"Yes. I only had a moment. ",{"->":"reveal_location_of_component"},"\n",{"#f":5}],"c-1":["^ ",{"->":".^.^.^.^.^.^.^.passed_onto_hooper"},"\n",{"#f":5}],"c-2":["^ \t\t\t",{"->":".^.^.c-1"},"\n",{"#f":5}],"c-3":["^ \t\t","\n","^\"I can't remember.\"","\n","^He draws his gun and lays it lightly on the field table.","\n","^\"I'm sorry to threaten you, friend. But His Majesty needs that brain of yours, and that brain alone. There are plenty of other parts to you that our country could do better without. Now I'll ask you again. Did you hide the component?\"","\n",["ev","str","^Yes","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^No","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Lie","/str","/ev",{"*":".^.c-2","flg":20},"ev","str","^Evade","/str","/ev",{"*":".^.c-3","flg":20},{"c-0":["^ ",{"->":".^.^.^.^.c-0"},"\n",{"#f":5}],"c-1":["^ ","\n","^\"Very well then.\" I swallow nervously, to make it look more genuine. ",{"->":"i_met_a_young_man.passed_onto_hooper"},"\n",{"#f":5}],"c-2":["^ ",{"->":".^.^.c-1"},"\n",{"#f":5}],"c-3":["^ ",{"->":"i_met_a_young_man.i_dont_have_it"},"\n",{"#f":5}]}],{"#f":5}]}],{"#f":5}],"c-10":["^ \t",{"->":".^.^.c-8"},"\n",{"#f":5}],"c-11":["^ \t\t\t\t",{"->":".^.^.c-9"},"\n",{"#f":5}],"#n":"paused"}],null]}],{"i_dont_have_it":[["^\"I don't have it any more. I passed it through the fence to my contact straight after taking it, before it was discovered to be missing. It would have been idiocy to do differently. It's long gone, I'm afraid.\"","\n","^\"You fool, Manning,\" Harris curses, getting quickly to his feet. \"You utter fool. Do you suppose you will be any better off living under Hitler? It's men like you who will get us all killed. Men too feeble, too weak in their hearts to stand up and take a man's responsibility for the world. You're happier to stay a child all your life and play with your little childish toys.\"","\n","ev","str","^Answer back","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Say nothing","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["\n","^\"Really, Commander,\" I reply. \"It rather sounds like you want to spank me.\"","\n","^\"For God's sake,\" he declares with thick disgust, then swoops away out of the room.","\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["^ ","\n","^I say nothing. It's true, isn't it? I can't deny that I know there is a world out there, a complicated world of pain and suffering. And I can't deny that I don't think about it a moment longer than I have to. What use is thinking on a problem that cannot be solved? It is precisely our ability to avoid such endless spirals that makes us human and not machine.","\n","^\"God have mercy on your soul,\" Harris says finally, as he gets to his feet and heads for the door. \"I fear no—one else will.\"","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":[{"->":"left_alone"},null]}],null],"passed_onto_hooper":[["ev",true,"/ev",{"VAR=":"hooper_mentioned","re":true},"^\"No. I passed it on to Hooper.\"","\n","^\"I see. And what did he do with it?\"","\n","ev","str","^Evade","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Tell the truth","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Lie","/str","/ev",{"*":".^.c-2","flg":20},{"c-0":["^ ","\n","^\"I don't know.\"","\n","^\"You can do better than that. Remember, there's a hangman's noose waiting for traitors.\"","\n",["ev","str","^Theorise","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Shrug","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["^ ","\n","^\"Well, then,\" I answer, nervously. \"What would he do? Either get rid of it straight away — or if that wasn't possible, which it probably wouldn't be, since he'd have to arrange things with his contacts — so most likely, he'd hide it somewhere and wait, until you had the rope around my neck and he could be sure he was safe.\"","\n",{"->":"claim_hooper_took_component.harris_being_convinced"},{"#f":5}],"c-1":["^ ",{"->":"claim_hooper_took_component.its_your_problem"},"\n",{"#f":5}]}],{"#f":5}],"c-1":["^ ","\n","^\"I don't think Hooper could have planned this in advance. So he'd need to get word to whoever he's working with, and that would take time. So I think he would have hidden it somewhere, and be waiting to make sure I soundly take the fall. That way, if anything goes wrong, he can arrange for the part to be conveniently re—found.\"","\n",{"->":"claim_hooper_took_component.harris_being_convinced"},{"#f":5}],"c-2":["\n","^\"I'm sure I saw him this evening, talking to someone by the fence on the woodland side of the compound. He's probably passed it on already. You'll have to ask him.\"","\n",{"->":"claim_hooper_took_component.harrumphs"},{"#f":5}]}],null],"#f":1}],"claim_hooper_took_component":[["^\"I saw Hooper take it.\"","\n","ev",true,"/ev",{"VAR=":"hooper_mentioned","re":true},"ev",{"VAR?":"losttemper"},"/ev",[{"->":".^.b","c":true},{"b":["\n","^\"Did you?\"","\n","^The worst of his rage is passing; he is now moving into a kind of contemptuous despair. I can imagine him wrapping up our interview soon, leaving the hut, locking the door, and dropping the key down the well in the yard.","\n","^And why wouldn't he? With my name tarnished they will not let me back to work on the Bombe — if there is the slightest smell of treachery about my name I would be lucky not be locked up for the remainder of the war.","\n",{"->":".^.^.^.11"},null]}],[{"->":".^.b"},{"b":["\n","^\"I see.\" He is starting to lose his patience. I have seen Harris angry a few times, with lackeys and secretaries. But never with us. With the 'brains' he has always been cautious, treating us like children.","\n","^And now I see that, like a father, he wants to smack us when we disobey him.","\n",{"->":".^.^.^.11"},null]}],"nop","\n","^\"Just get to the truth, man. Every minute matters.\"","\n","ev","str","^Persist with this","/str",{"VAR?":"admitblackmail"},"/ev",{"*":".^.c-0","flg":21},"ev","str","^Tell the truth","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Persist with this","/str",{"VAR?":"admitblackmail"},"!","/ev",{"*":".^.c-2","flg":21},{"c-0":["\n","^\"I know what you're thinking. If I've transgressed once then I must be guilty of everything else... But I'm not. We were close to cracking the 13th's intercept. We were getting correlations in the data. Then Hooper disappeared for a moment, and next minute the machine was down.\"","\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["^ ","\n","^\"Very well. I see there's no point in covering up. You know everything anyway.\"","\n","^Harris nods, and waits for me to continue.","\n",{"->":"i_met_a_young_man"},{"->":".^.^.g-0"},{"#f":5}],"c-2":["\n","^\"This is the truth.\"","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":["^I have become, somehow, an accustomed liar — the words roll easily off my tongue. Perhaps I am a traitor, I think, now that I dissemble as easily as one.","\n","^\"Go on,\" Harris says, giving me no indication of whether he believes my tale.","\n","ev","str","^Assert","/str","/ev",{"*":".^.c-3","flg":20},"ev","str","^Imply","/str","/ev",{"*":".^.c-4","flg":20},{"c-3":["^ \"I saw him take it,\" I continue. \"Collins was outside having a cigarette. Peterson was at the table. But I was at the front of the machine. I saw Hooper go around the side. He leant down and pulled something free. I even challenged him. I said, 'What's that? Someone put a nail somewhere they shouldn't have?' He didn't reply.\"","\n","^Harris watches me for a long moment.","\n",{"->":".^.^.^.g-1"},{"#f":5}],"c-4":["^ \"At the moment the machine halted, Peterson was at the bench and Collins was outside having a smoke. I was checking the dip—switches. Hooper was the only one at the back of the Bombe. No—one else could have done it.\"","\n","^\"That's not quite the same as seeing him do it,\" Harris remarks.","\n",["ev","str","^Logical","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Persuasive","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Confident","/str","/ev",{"*":".^.c-2","flg":20},{"c-0":["\n","^\"When you have eliminated the impossible...\" I begin, but Harris cuts me off.","\n",{"->":".^.^.^.^.^.g-1"},{"#f":5}],"c-1":["^ ","\n","^\"You have to believe me.\"","\n","^\"We don't have to believe anyone,\" Harris returns. \"I will only be happy with the truth, and your story doesn't tie up. We know you've been leaving yourself open to pressure. We've been watching your activities for some time. But we thought you were endangering the reputation of this site; not risking the country herself. Perhaps I put too much trust in your intellectual pride.\"","\n","^He pauses for a moment, considering something. Then he continues:","\n","^\"It might have been Hooper. It might have been you. ",{"->":".^.^.^.^.^.we_wont_guess"},"\n",{"->":".^.^.^.^.^.g-1"},{"#f":5}],"c-2":["^ ","\n","^\"Ask the others,\" I reply, leaning back. \"They'll tell you. If they haven't already, that's only because they're protecting Hooper. Hoping he'll come to his senses and stop being an idiot. I hope he does too. And if you lock him up in a freezing hut like you've done me, I'm sure he will.\"","\n","^\"We have,\" Harris replies simply.","\n","^It's all I can do not to gape.","\n",{"->":".^.^.^.^.^.g-1.hoopers_hut_3"},{"->":".^.^.^.^.^.g-1"},{"#f":5}]}],{"#f":5}]}],"g-1":["^\"We are left with two possibilities. You, or Hooper.\" The Commander pauses to smooth down his moustache. ","<>","\n",["^\"Hooper's in Hut 3 with the Captain, having a similar conversation.\"","\n",["ev",{"^->":"claim_hooper_took_component.0.g-1.hoopers_hut_3.2.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","str","^\"","/str","/ev",{"*":".^.^.c-5","flg":22},{"s":["^\"And the other men?",{"->":"$r","var":true},null]}],["ev",{"^->":"claim_hooper_took_component.0.g-1.hoopers_hut_3.3.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","str","^\"","/str","/ev",{"*":".^.^.c-6","flg":22},{"s":["^\"Then you know I'm right.",{"->":"$r","var":true},null]}],{"c-5":["ev",{"^->":"claim_hooper_took_component.0.g-1.hoopers_hut_3.c-5.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.2.s"},[{"#n":"$r2"}],"^ Do we have a hut each? Are there enough senior officers to go round?\"","\n","^\"Collins was outside when it happened, and Peterson can't get round the machine in that chair of his,\" Harris replies. \"That leaves you and Hooper.","\n",{"->":".^.^.^.^.we_wont_guess"},{"#f":5}],"c-6":["ev",{"^->":"claim_hooper_took_component.0.g-1.hoopers_hut_3.c-6.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.3.s"},[{"#n":"$r2"}],"^ You knew all along. Why did you threaten me?\"","\n","^\"All we know is that we have a traitor, holding the fate of the country in his hands.","\n",{"->":".^.^.^.^.we_wont_guess"},{"#f":5}],"#f":5,"#n":"hoopers_hut_3"}],null],"we_wont_guess":["<>","^ We're not in the business of guessing here at Bletchley. We are military intelligence. We get answers.\" Harris points a finger. \"And if that component has left these grounds, then every minute is critical.\"","\n","ev","str","^Co-operate","/str","/ev",{"*":".^.c-7","flg":20},"ev","str","^Block","/str","/ev",{"*":".^.c-8","flg":20},{"c-7":["^ ","\n","^\"I'd be happy to help,\" I answer, leaning forwards. \"I'm sure there's something I could do.\"","\n","^\"Like what, exactly?\"","\n",[["ev",{"^->":"claim_hooper_took_component.0.we_wont_guess.c-7.6.0.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-0","flg":18},{"s":["^\"Put me in with Hooper.\"",{"->":"$r","var":true},null]}],["ev",{"^->":"claim_hooper_took_component.0.we_wont_guess.c-7.6.1.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","str","^\"","/str","/ev",{"*":".^.^.c-1","flg":22},{"s":["^\"Tell Hooper I've confessed.",{"->":"$r","var":true},null]}],{"c-0":["ev",{"^->":"claim_hooper_took_component.0.we_wont_guess.c-7.6.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.0.s"},[{"#n":"$r2"}],"\n",{"->":".^.^.^.^.^.^.putmein"},{"#f":5}],"c-1":["ev",{"^->":"claim_hooper_took_component.0.we_wont_guess.c-7.6.c-1.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.1.s"},[{"#n":"$r2"}],"^ Better yet. Let him see you marching me off in handcuffs. Then let him go, and see what he does. Ten to one he'll go straight to wherever he's hidden that component and his game will be up.\"","\n","^Harris nods slowly, chewing over the idea. It isn't a bad plan even — except, of course, Hooper has not hidden the component, and won't lead them anywhere. But that's a problem I might be able to solve once I'm out of this place; and once they're too busy dogging Hooper's steps from hut to hut.","\n","^\"Interesting,\" the Commander muses. \"But I'm not so sure he'd be that stupid. And if he's already passed the part on, the whole thing will only be a waste of time.\"","\n",[["ev",{"^->":"claim_hooper_took_component.0.we_wont_guess.c-7.6.c-1.12.0.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","str","^\"","/str","/ev",{"*":".^.^.c-0","flg":22},{"s":["^\"Trust me. He hasn't.",{"->":"$r","var":true},null]}],["ev",{"^->":"claim_hooper_took_component.0.we_wont_guess.c-7.6.c-1.12.1.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","str","^.\"","/str","/ev",{"*":".^.^.c-1","flg":22},{"s":["^\"You're right. Let me talk to him",{"->":"$r","var":true},null]}],["ev",{"^->":"claim_hooper_took_component.0.we_wont_guess.c-7.6.c-1.12.2.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-2","flg":18},{"s":["^\"You're right.\" ",{"->":"$r","var":true},null]}],{"c-0":["ev",{"^->":"claim_hooper_took_component.0.we_wont_guess.c-7.6.c-1.12.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.0.s"},[{"#n":"$r2"}],"^ If I know that man, and I do, he'll be wanting to keep his options open as long as possible. If the component's gone then he's in it up to his neck. He'll take a week at least to make sure he's escaped suspicion. Then he'll pass it on.\"","\n","^\"And if we keep applying pressure to him, you think the component will eventually just turn up?\"","\n",[["ev",{"^->":"claim_hooper_took_component.0.we_wont_guess.c-7.6.c-1.12.c-0.10.0.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","str","^\"","/str","/ev",{"*":".^.^.c-0","flg":22},{"s":["^\"Yes.",{"->":"$r","var":true},null]}],["ev",{"^->":"claim_hooper_took_component.0.we_wont_guess.c-7.6.c-1.12.c-0.10.1.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-1","flg":18},{"s":["^\"Or be thrown into the river.\" ",{"->":"$r","var":true},null]}],{"c-0":["ev",{"^->":"claim_hooper_took_component.0.we_wont_guess.c-7.6.c-1.12.c-0.10.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.0.s"},[{"#n":"$r2"}],"^ Probably under my bunk.\"","\n","^Harris smiles wryly. \"We'll know that for a fake, then. We've looked there already.","\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["ev",{"^->":"claim_hooper_took_component.0.we_wont_guess.c-7.6.c-1.12.c-0.10.c-1.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.1.s"},[{"#n":"$r2"}],"\n","^\"Hmm.\" Harris chews his moustache thoughtfully. \"Well, that would put us in a spot, seeing as how we'd never know for certain. We'd have to be ready to change our whole approach just in case the part had got through to the Germans.","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":["<>","^ I don't mind telling you, this is a disaster, this whole thing. What I want is to find that little bit of mechanical trickery. I don't care where. In your luncheon box or under Hooper's pillow. Just somewhere, and within the grounds of this place.\"","\n",["ev",{"^->":"claim_hooper_took_component.0.we_wont_guess.c-7.6.c-1.12.c-0.10.g-0.3.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","str","^\"","/str","/ev",{"*":".^.^.c-2","flg":22},{"s":["^\"Then let him he think he's off the hook.",{"->":"$r","var":true},null]}],["ev",{"^->":"claim_hooper_took_component.0.we_wont_guess.c-7.6.c-1.12.c-0.10.g-0.4.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","str","^.\"","/str","/ev",{"*":".^.^.c-3","flg":22},{"s":["^\"Then you'd better get searching",{"->":"$r","var":true},null]}],{"c-2":["ev",{"^->":"claim_hooper_took_component.0.we_wont_guess.c-7.6.c-1.12.c-0.10.g-0.c-2.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.3.s"},[{"#n":"$r2"}],"^ Make a show of me. And then you'll get your man.\"","\n","^Somehow, I think. But that's the part I need to work.","\n",{"->":"harris_takes_you_to_hooper"},{"#f":5}],"c-3":["ev",{"^->":"claim_hooper_took_component.0.we_wont_guess.c-7.6.c-1.12.c-0.10.g-0.c-3.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.4.s"},[{"#n":"$r2"}],"^,\" I reply, tiring of his complaining. A war is a war, you have to expect an enemy. ",{"->":".^.^.^.^.^.^.^.^.^.^.^.its_your_problem"},"\n",{"#f":5}]}]}],{"#f":5}],"c-1":["ev",{"^->":"claim_hooper_took_component.0.we_wont_guess.c-7.6.c-1.12.c-1.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.1.s"},[{"#n":"$r2"}],"^, then. As a colleague. Maybe I can get something useful out of him.\"","\n",{"->":".^.^.^.^.^.^.^.^.putmein"},{"#f":5}],"c-2":["ev",{"^->":"claim_hooper_took_component.0.we_wont_guess.c-7.6.c-1.12.c-2.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.2.s"},[{"#n":"$r2"}],{"->":".^.^.^.^.^.^.^.^.shake_head"},"\n",{"#f":5}]}],{"#f":5}]}],{"#f":5}],"c-8":["^ ",{"->":".^.^.^.^.its_your_problem"},"\n",{"#f":5}]}]}],{"harris_being_convinced":[["^\"Makes sense,\" Harris agrees, cautiously. ","ev",{"VAR?":"evasive"},1,">","/ev",[{"->":".^.b","c":true},{"b":["^I can see he's still not entirely convinced by my tale, as well he might not be — I've hardly been entirely straight with him.",{"->":".^.^.^.8"},null]}],[{"->":".^.b"},{"b":["^I can see he's still not certain whether he can trust me.",{"->":".^.^.^.8"},null]}],"nop","^ \"Which means the question is, what can we do to rat him out?\"","\n","ev","str","^Offer to help","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Don't offer to help","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["^ ","\n","^\"Maybe I can help with that.\"","\n","^\"Oh, yes? And how, exactly?\"","\n",[["ev",{"^->":"claim_hooper_took_component.harris_being_convinced.0.c-0.6.0.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-0","flg":18},{"s":["^\"I'll talk to him.\" ",{"->":"$r","var":true},null]}],["ev",{"^->":"claim_hooper_took_component.harris_being_convinced.0.c-0.6.1.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","str","^\"","/str","/ev",{"*":".^.^.c-1","flg":22},{"s":["^\"We'll fool him.",{"->":"$r","var":true},null]}],{"c-0":["ev",{"^->":"claim_hooper_took_component.harris_being_convinced.0.c-0.6.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.0.s"},[{"#n":"$r2"}],"\n","^\"What?\"","\n","^\"Put me in with Hooper with him. Maybe I can get something useful out of him.\"","\n",{"->":".^.^.^.^.^.^.putmein"},{"#f":5}],"c-1":["ev",{"^->":"claim_hooper_took_component.harris_being_convinced.0.c-0.6.c-1.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.1.s"},[{"#n":"$r2"}],"^ He's waiting to be sure that I've been strung up for this, so let's give him what he wants. If he sees me taken away, clapped in irons — he'll go straight to that component and set about getting rid of it.\"","\n",{"->":"harris_takes_you_to_hooper"},{"#f":5}]}],{"#f":5}],"c-1":["\n","^I lean back. ",{"->":".^.^.^.^.its_your_problem"},"\n",{"#f":5}]}],null],"putmein":[["^Harris shakes his head.","\n","^\"He despises you. I don't see why he'd give himself up to you.\"","\n","ev","str","^Insist","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Give in","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["^ \"Try me. Just me and him.\" ","\n",{"->":".^.^.^.^.go_in_alone"},{"#f":5}],"c-1":["^ \"You're right.\" ","\n",{"->":".^.^.^.^.shake_head"},{"#f":5}]}],{"#f":1}],"shake_head":["<>","^ I shake my head. \"You're right. I don't see how I can help you. So there's only one conclusion.\"","\n","^\"Oh, yes? And what's that?\"","\n",{"->":".^.^.its_your_problem"},null],"its_your_problem":["^\"It's your problem. Your security breach. So much for your careful vetting process.\"","\n","^I lean back in my chair and fold my arms so the way they shake will not be visible.","\n","^\"You'd better get on with solving it, instead of wasting your time in here with me.\"","\n",{"->":".^.^.harrumphs"},null],"harrumphs":[["^Harris harrumphs. He's thinking it all over.","\n","ev","str","^Wait","/str",{"CNT?":".^.^.^.putmein"},"/ev",{"*":".^.c-0","flg":21},"ev","str","^Wait","/str",{"CNT?":".^.^.^.putmein"},"!","/ev",{"*":".^.c-1","flg":21},{"c-0":["^ ","\n","^\"All right,\" he declares, gruffly. \"We'll try it. But if this doesn't work, I might just put the both of you in front of a firing squad and be done with these games. Worse things happen in time of war, you know.\"","\n","^\"Alone,\" I add.","\n",{"->":".^.^.^.^.go_in_alone"},{"#f":5}],"c-1":["^ ","\n","^\"No,\" Harris declares, finally. \"I think you're lying about Hooper. I think you're a clever, scheming young man — that's why we hired you — and you're looking for the only reasonable out this situation has to offer. But I'm not taking it. We know you were in the room with the machine, we know you're of a perverted persuasion, we know you have compromised yourself. There's nothing more to say here. Either you tell me what you've done with that component, or we will hang you and search just as hard. It's your choice.\"","\n",{"->":"harris_threatens_lynching"},{"#f":5}]}],null],"go_in_alone":[["^\"Alone?\"","\n","^\"Alone.\"","\n","^Harris considers it. I watch his eyes, flicking backwards and forwards over mine, like a ribbon—reader loading its program.","\n","ev","str","^Patient","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Impatient","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["^ \"Well?\"","\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["^ \"For God's sake, man, what do you have to lose?\" ","\n","ev",{"^var":"forceful","ci":-1},{"f()":"raise"},"pop","/ev","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":["^\"We'll be outside the door,\" Harris replies, seriously. \"The first sign of any funny business and we'll have you both on the floor in minutes. You understand? The country needs your brain, but it's not too worried about your legs. Remember that.\"","\n","^Then he gets to his feet, and opens the door, and marches me out across the yard. The evening is drawing in and there's a chill in the air. My mind is racing. I have one opportunity here — a moment in which to put the fear of God into Hooper and make him do something foolish that places him in harm's way. But how to achieve it?","\n","^\"You ready?\" Harris demands.","\n","ev","str","^Yes","/str","/ev",{"*":".^.c-2","flg":20},"ev","str","^No","/str","/ev",{"*":".^.c-3","flg":20},"ev","str","^Lie","/str","/ev",{"*":".^.c-4","flg":20},{"c-2":["\n","^\"Absolutely.\"","\n",{"->":".^.^.^.g-1"},{"#f":5}],"c-3":["\n","^\"No.\"","\n","^\"Too bad.\"","\n",{"->":".^.^.^.g-1"},{"#f":5}],"c-4":["^ ",{"->":".^.^.c-2"},"\n",{"->":".^.^.^.g-1"},{"#f":5}]}],"g-1":[{"->":"inside_hoopers_hut"},null]}],null]}],"harris_takes_you_to_hooper":[["^Harris gets to his feet. \"All right,\" he says. \"I should no better than to trust a clever man, but we'll give it a go.\"","\n","^Then, he smiles, with all his teeth, like a wolf.","\n","ev",{"CNT?":"claim_hooper_took_component.0.g-1.hoopers_hut_3"},"/ev",[{"->":".^.b","c":true},{"b":["\n","^\"Especially since this is a plan that involves keeping you in handcuffs. I don't see what I have to lose.\"","\n",{"->":".^.^.^.9"},null]}],[{"->":".^.b"},{"b":["\n","^\"Hooper's in Hut 3 being debriefed by the Captain. Let's see if we can't get his attention somehow.\"","\n",{"->":".^.^.^.9"},null]}],"nop","\n","^He raps on the door for the guard and gives the man a quick instruction. He returns a moment later with a cool pair of iron cuffs.","\n","^\"Put 'em up,\" Harris instructs, and I do so. The metal closes around my wrists like a trap. I stand and follow Harris willingly out through the door.","\n","^But whatever I'm doing with my body, my mind is scheming. Somehow, I'm thinking, I have to get away from these men long enough to get that component behind Hut 2 and put it somewhere Hooper will go. Or, otherwise, somehow get Hooper to go there himself...","\n","^Harris marches me over to Hut 3, and gestures for the guard to stand aside. Pushing me forward, he opens the door nice and wide.","\n","^\"Captain. Manning talked. If you'd step out for a moment?\"","\n","ev","str","^Play the part, head down","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Look inside the hut","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Call to Hooper","/str","/ev",{"*":".^.c-2","flg":20},{"c-0":["\n","^From where he's sitting, I know Hooper can see me, so I keep my head down and look guilty as sin. The bastard is probably smiling.","\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["\n","^I look in through the door and catch Hooper's expression. I had half expected him to be smiling be he isn't. He looks shocked, almost hurt. \"Iain,\" he murmurs. \"You couldn't...\"","\n",{"->":".^.^.g-0"},{"#f":5}],"c-2":["^ ","\n","^I have a single moment to shout something to Hooper before the door closes.","\n","^\"I'll get you Hooper, you'll see!\" I cry. Then:","\n",[["ev",{"^->":"harris_takes_you_to_hooper.0.c-2.6.0.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-0","flg":18},{"s":["^\"Queen to rook two, checkmate!\"",{"->":"$r","var":true},null]}],{"c-0":["ev",{"^->":"harris_takes_you_to_hooper.0.c-2.6.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.0.s"},[{"#n":"$r2"}],"^ I call, then laugh viciously, as if I am damning him straight to hell.","\n","ev",2,"/ev",{"VAR=":"hooperClueType","re":true},{"->":".^.^.only_catch"},{"#f":5}],"only_catch":["^I only catch Hooper's reaction for a moment — his eyebrow lifts in surprise and alarm. Good. If he thinks it is a threat then he just might be careless enough to go looking for what it might mean.","\n",["ev",{"^->":"harris_takes_you_to_hooper.0.c-2.6.only_catch.2.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-1","flg":18},{"s":["^\"Ask not for whom the bell tolls!\"",{"->":"$r","var":true},null]}],["ev",{"^->":"harris_takes_you_to_hooper.0.c-2.6.only_catch.3.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-2","flg":18},{"s":["^\"Two words: messy, without one missing!\"",{"->":"$r","var":true},null]}],{"c-1":["ev",{"^->":"harris_takes_you_to_hooper.0.c-2.6.only_catch.c-1.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.2.s"},[{"#n":"$r2"}],"\n","^He stares back at me, as if were a madman and perhaps for a split second I see him shudder.","\n",{"->":".^.^.^.^.^.g-0"},{"#f":5}],"c-2":["ev",{"^->":"harris_takes_you_to_hooper.0.c-2.6.only_catch.c-2.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.3.s"},[{"#n":"$r2"}],"^ I cry, laughing. It isn't the best clue, hardly worthy of The Times, but it will have to do.","\n","ev",3,"/ev",{"VAR=":"hooperClueType","re":true},{"->":".^.^"},{"->":".^.^.^.^.^.g-0"},{"#f":5}]}]}],{"#f":5}],"g-0":["^The Captain comes outside, pulling the door to. \"What's this?\" he asks. \"A confession? Just like that?\"","\n","^\"No,\" the Commander admits, in a low voice. \"I'm afraid not. Rather more a scheme. The idea is to let Hooper go and see what he does. If he believes we have Manning here in irons, he'll try to shift the component.\"","\n","^\"If he has it.\"","\n","^\"Indeed.\"","\n","^The Captain peers at me for a moment, like I was some kind of curious insect.","\n","^\"Sometimes, I think you people are magicians,\" he remarks. \"Other times you seem more like witches. Very well.\"","\n","^With that he opens the door to the Hut and goes back inside. The Commander uses the moment to hustle me roughly forward.","\n","ev",{"CNT?":".^.^.c-2"},"/ev",[{"->":".^.b","c":true},{"b":["\n","^\"And what was all that shouting about?\" he hisses in my ear as we move towards the barracks. \"Are you trying to pull something? Or just make me look incompetent?\"","\n",{"->":".^.^.^.19"},null]}],[{"->":".^.b"},{"b":["\n","^\"This scheme of yours had better come off,\" he hisses in my ear. \"Otherwise the Captain is going to start having men tailing me to see where I go on Saturdays.\"","\n",{"->":".^.^.^.19"},null]}],"nop","\n","ev","str","^Reassure","/str","/ev",{"*":".^.c-3","flg":20},"ev","str","^Dissuade","/str","/ev",{"*":".^.c-4","flg":20},"ev","str","^Evade","/str","/ev",{"*":".^.c-5","flg":20},"ev","str","^Say nothing","/str","/ev",{"*":".^.c-6","flg":20},{"c-3":["^ ","\n","ev",{"CNT?":".^.^.^.c-2"},"!","/ev",[{"->":".^.b","c":true},{"b":["\n","^\"It will. Hooper's running scared,\" I reply, hoping I sound more confident than I feel.","\n",{"->":".^.^.^.8"},null]}],[{"->":".^.b"},{"b":["\n","^\"Just adding to the drama,\" I tell him, confidently. \"I'm sure you can understand that.\"","\n",{"->":".^.^.^.8"},null]}],"nop","\n","^\"I think we've had enough drama today already,\" Harris replies. \"Let's hope for a clean kill.\"","\n",{"->":".^.^.^.g-1"},{"#f":5}],"c-4":["^ ","\n","ev",{"CNT?":".^.^.^.c-2"},"!","/ev",[{"->":".^.b","c":true},{"b":["\n","^\"The Captain thought it was a good scheme. You'll most likely get a promotion.\"","\n",{"->":".^.^.^.8"},null]}],[{"->":".^.b"},{"b":["\n","^\"I'm not trying to do anything except save my neck.\"","\n",{"->":".^.^.^.8"},null]}],"nop","\n","^\"Let's hope things work out,\" Harris agrees darkly.","\n",{"->":".^.^.^.g-1"},{"#f":5}],"c-5":["^ ","\n","^\"We're still in ear—shot if they let Hooper go. Best get us inside and then we can talk, if we must.\"","\n","^\"I've had enough of your voice for one day,\" Harris replies grimly. ","<>","\n",{"->":".^.^.^.g-1"},{"#f":5}],"c-6":["\n","^I let him have his rant. ","<>","\n",{"->":".^.^.^.g-1"},{"#f":5}]}],"g-1":["^He hustles me up the steps of the barracks, keeping me firmly gripped as if I had any chance of giving him, a trained military man, the slip. It's all I can do not to fall into the room.","\n",{"->":"slam_door_shut_and_gone"},null]}],null],"inside_hoopers_hut":[[["^Harris opens the door and pushes me inside. \"Captain,\" he calls. \"Could I have a moment?\"","\n","^The Captain, looking puzzled, steps out. The door is closed. Hooper stares at me, open—mouthed, about to say something. I probably have less than a minute before the Captain storms back in and declares this plan to be bunkum.","\n","ev","str","^Threaten","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Bargain","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Plead","/str","/ev",{"*":".^.c-2","flg":20},{"c-0":["\n","^\"Listen to me, Hooper. We were the only men in that hut today, so we know what happened. But I want you to know this. I put the component inside a breeze—block in the foundations of Hut 2, wrapped in one of your shirts. They're going to find it eventually, and that's going to be what tips the balance. And there's nothing you can do to stop any of that from happening.\"","\n","ev",1,"/ev",{"VAR=":"hooperClueType","re":true},"^His eyes bulge with terror. \"What did I do, to you? What did I ever do?\"","\n",["ev","str","^Tell the truth","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Lie","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Evade","/str","/ev",{"*":".^.c-2","flg":20},{"c-0":["^ ","\n","^\"You treated me like vermin. Like something abhorrent.\"","\n","^\"You are something abhorrent.\"","\n","^\"I wasn't. Not when I came here. And I won't be, once you're gone.\"","\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["^ ","\n","^\"Nothing,\" I reply. \"You're just the other man in the room. One of us has to get the blame.\"","\n",{"->":".^.^.g-0"},{"#f":5}],"c-2":["^ ","\n","^\"It doesn't matter. Just remember what I said. I've beaten you, Hooper. Remember that.\"","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":["^I get to my feet and open the door of the Hut. The Captain storms back inside and I'm quickly thrown out. ",{"->":".^.^.^.^.^.^.hustled_out"},"\n",null]}],{"#f":5}],"c-1":["^ ","\n","^\"Hooper, I'll make a deal with you. We both know what happened in that hut this afternoon. I know because I did it, and you know because you know you didn't. But once this is done I'll be rich, and I'll split that with you. I'll let you have the results, too. Your name on the discovery of the Bombe. And it won't hurt the war effort — you know as well as me that the component on its own is worthless, it's the wiring of the Bombe, the usage, that's what's valuable. So how about it?\"","\n","^Hooper looks back at me, appalled. \"You're asking me to commit treason?\"","\n",["ev","str","^Yes","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^No","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Lie","/str","/ev",{"*":".^.c-2","flg":20},"ev","str","^Evade","/str","/ev",{"*":".^.c-3","flg":20},{"c-0":["\n","^\"Yes, perhaps. But also to ensure your name goes down in the annals of mathematics. ",{"->":".^.^.^.^.^.^.back_of_hut_2"},"\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["^ ","\n","^\"No. It's not treason. It's a trade, plain and simple.\"","\n",{"->":".^.^.g-0"},{"#f":5}],"c-2":["^ ","\n","^\"I'm suggesting you save your own skin. I've wrapped that component in one of your shirts, Hooper. They'll be searching this place top to bottom. They'll find it eventually, and when they do, that's the thing that will swing it against you. So take my advice now. Hut 2.\"","\n","ev",1,"/ev",{"VAR=":"hooperClueType","re":true},{"->":".^.^.g-0"},{"#f":5}],"c-3":["^ ",{"->":".^.^.c-2"},"\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":[{"->":".^.^.^.^.^.^.no_chance"},null]}],{"#f":5}],"c-2":["^ ","\n","^\"Please, Hooper. You don't understand. They have information on me. I don't need to tell you what I've done, you know. Have a soul. And the component — it's nothing. It's not the secret of the Bombe. It's just a part. The German's think it's a weapon — a missile component. Let them have it. Please, man. Just help me.\"","\n","^\"Help you?\" Hooper stares. \"Help you? You're a traitor. A snake in the grass. And you're queer.\"","\n",["ev","str","^Deny","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Accept","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Evade","/str","/ev",{"*":".^.c-2","flg":20},{"c-0":["^ ","\n","^\"I'm no traitor. You know I'm not. How much work have I done here against the Germans? I've given my all. And you know as well as I do, if the Reich were to invade, I would be a dead man. Please, Hooper. I'm not doing any of this lightly.\"","\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["\n","^\"I am what I am,\" I reply. \"I'm the way I was made. But they'll hang me unless you help, Hooper. Don't let them hang me.\"","\n",{"->":".^.^.g-0"},{"#f":5}],"c-2":["^ ","\n","^\"That's not important now. What matters is what you do, this evening.\"","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":["^\"Assuming I wanted to help you,\" he replies, carefully. \"Which I don't. What would I do?\"","\n","^\"Nothing. Almost nothing.","\n",{"->":".^.^.^.^.^.^.back_of_hut_2"},null]}],{"#f":5}],"#n":"g-0"}],null],{"back_of_hut_2":["<>","^ All you have to do is go to the back of Hut 2. There's a breeze—block with a cavity. That's where I've put it. I'll be locked up overnight. But you can pick it up and pass it to my contact. He'll be at the south fence around two AM.\"","\n","ev",1,"/ev",{"VAR=":"hooperClueType","re":true},{"->":".^.^.no_chance"},{"#f":1}],"no_chance":["^\"If you think I'll do that then you're crazy,\" Hooper replies.","\n","^At that moment the door flies open and the Captain comes storming back inside.","\n",{"->":".^.^.hustled_out"},null],"hustled_out":["^Harris hustles me over to the barracks. \"I hope that's the end of it,\" he mutters.","\n","^\"Just be sure to let him out,\" I reply. \"And then see where he goes.\"","\n",{"->":"slam_door_shut_and_gone"},null]}],"slam_door_shut_and_gone":[["^Then they slam the door shut, and it locks.","\n","ev",{"VAR?":"hooperClueType"},0,"==","/ev",[{"->":".^.b","c":true},{"b":["\n","<>","^ How am I supposed to manage anything from in here?","\n","ev","str","^Try the door","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Try the windows","/str","/ev",{"*":".^.c-1","flg":20},{"->":".^.^.^.9"},{"c-0":["^ ",{"->":".^.^.^.^.^.try_the_door"},"\n",{"#f":5}],"c-1":["^ ",{"->":".^.^.^.^.^.try_the_windows"},"\n",{"#f":5}]}]}],[{"->":".^.b"},{"b":["\n","^I can only hope that Hooper bites. If he thinks I'm bitter enough to have framed him, and arrogant enough to have taunted him with ","ev",{"VAR?":"hooperClueType"},1,">","/ev",[{"->":".^.b","c":true},{"b":["^a clue to",{"->":".^.^.^.8"},null]}],"nop","^ where the damning evidence is hidden...","\n","^If he hates me enough, and is paranoid enough, then he might ","ev",{"VAR?":"hooperClueType"},1,">","/ev",[{"->":".^.b","c":true},{"b":["^unravel my little riddle and",{"->":".^.^.^.18"},null]}],"nop","^ go searching around Hut 2.","\n",{"->":".^.^.^.9"},null]}],"nop","\n","ev","str","^Wait","/str","/ev",{"*":".^.c-0","flg":20},{"c-0":["^ \t",{"->":"night_falls"},"\n",{"#f":5}]}],{"try_the_door":["^I try the door. It's locked, of course.","\n",{"->":".^.^.from_outside_heard"},{"#f":1}],"from_outside_heard":[["^From outside, I hear a voice. Hooper's. He's haranguing someone.","\n",["ev","str","^Listen at the keyhole","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Try the window","/str",{"CNT?":".^.^.^.^.try_the_windows"},"!","/ev",{"*":".^.c-1","flg":21},"ev","str","^Try the door","/str",{"CNT?":".^.^.^.^.try_the_door"},"!",{"CNT?":".^.c-0"},"&&","/ev",{"*":".^.c-2","flg":21},"ev","str","^Smash the window","/str",{"CNT?":".^.^.^.^.try_the_windows"},"/ev",{"*":".^.c-3","flg":21},"ev","str","^Wait","/str",{"CNT?":".^.^.^.^.try_the_door"},{"CNT?":".^.^.^.^.try_the_windows"},"&&","/ev",{"*":".^.c-4","flg":21},{"c-0":["^ ","\n","^I put my ear down to the keyhole, but there's nothing now. Probably still a guard outside, of course, but they're keeping mum.","\n",{"->":".^.^"},{"#f":5}],"c-1":["^ ",{"->":".^.^.^.^.^.try_the_windows"},"\n",{"#f":5}],"c-2":["^ ",{"->":".^.^.^.^.^.try_the_door"},"\n",{"#f":5}],"c-3":["^ ",{"->":".^.^.^.^.^.try_to_smash_the_window"},"\n",{"#f":5}],"c-4":["^ ","\n","^It's useless. There's nothing I can do but hope. I sit down on one corner of the bunk to wait.","\n",{"->":"night_falls"},{"#f":5}],"#n":"opts"}],null],null],"try_the_windows":["^I go over to the window and try to jimmy it open. Not much luck, but in my struggling I notice this window only backs on the thin little brook that runs down the back of the compound. Which means, if I smashed it, I might get away with no—one seeing.","\n",{"->":".^.^.from_outside_heard"},{"#f":1}],"try_to_smash_the_window":[["^The window is my only way out of here. I just need a way to smash it.","\n","ev","str","^Punch it","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Find something","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Use something you've got","/str","/ev",{"*":".^.c-2","flg":20},{"c-0":["^ ","\n","^I suppose my fist would do a good enough job. But I'd cut myself to ribbons, most likely. ","<>","\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["^ ","\n","ev",2,"/ev",{"VAR=":"smashingWindowItem","re":true},"^I cast around the small room. There's a bucket in one corner for emergencies — I suppose I could use that. I pick it up but it's not very easy to heft. ","<>","\n",{"->":".^.^.g-0"},{"#f":5}],"c-2":["^ ","\n","^I pat down my pockets but all I'm carrying is the intercept, which is no good at all.","\n",["ev","str","^Something you're wearing?","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Look around","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["^ ","\n","^Ah, but of course! I slip off one shoe and heft it by the toe. The heel will make a decent enough hammer, if I give it enough wallop.","\n","ev",1,"/ev",{"VAR=":"smashingWindowItem","re":true},"^But I'll cut my hand to ribbons doing it. ","<>","\n",{"->":".^.^.^.^.g-0"},{"#f":5}],"c-1":["^ ",{"->":".^.^.^.^.c-1"},"\n",{"->":".^.^.^.^.g-0"},{"#f":5}]}],{"#f":5}],"g-0":["^And the noise would be terrible. There must be a way of making this easier. I'm supposed to be a thief now. What would a burglar do?","\n","ev","str","^Work slowly","/str","/ev",{"*":".^.c-3","flg":20},"ev","str","^Find something to help","/str","/ev",{"*":".^.c-4","flg":20},{"c-3":["^ ","\n","^Work carefully? It's difficult to work carefully when all one's has is ","ev",{"VAR?":"smashingWindowItem"},2,"==","/ev",[{"->":".^.b","c":true},{"b":["^a bucket. It's rather like the sledgehammer for the proverbial nut",{"->":".^.^.^.10"},null]}],[{"->":".^.b"},{"b":["ev",{"VAR?":"smashingWindowItem"},1,"==","/ev",[{"->":".^.b","c":true},{"b":["^a shoe",{"->":".^.^.^.7"},null]}],[{"->":".^.b"},{"b":["^nothing but brute force",{"->":".^.^.^.7"},null]}],"nop",{"->":".^.^.^.10"},null]}],"nop","^.","\n",["ev","str","^Just do it","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Look around for something","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["^ ",{"->":".^.^.^.^.^.^.^.time_to_move_now"},"\n",{"->":".^.^.^.^.^.g-1"},{"#f":5}],"c-1":["^ ","\n",{"->":".^.^.^.^.^.g-1"},{"#f":5}]}],{"#f":5}],"c-4":["^ ","\n",{"->":".^.^.^.g-1"},{"#f":5}]}],"g-1":[{"->":".^.^.^.^.find_something_to_smash_window"},null]}],null],"time_to_move_now":[["^Enough of this. There isn't any time to lose. Right now they'll be following Hooper as he goes to bed, and goes to sleep; and then that's it. The minute he closes his eyelids and drifts off that's the moment that this trap swings shut on me.","\n","^So I punch out the glass with my ","ev",{"VAR?":"smashingWindowItem"},2,"==","/ev",[{"->":".^.b","c":true},{"b":["^bucket",{"->":".^.^.^.10"},null]}],[{"->":".^.b"},{"b":["ev",{"VAR?":"smashingWindowItem"},1,"==","/ev",[{"->":".^.b","c":true},{"b":["^shoe",{"->":".^.^.^.7"},null]}],[{"->":".^.b"},{"b":["^fist",{"->":".^.^.^.7"},null]}],"nop",{"->":".^.^.^.10"},null]}],"nop","^ and it shatters with a terrific noise. Then I stop, and wait, to see if anyone will come in through the door.","\n","^Nothing.","\n","ev","str","^Wait a little longer","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Clear the frame of shards","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["^ ","\n","^I pause for a moment longer. It doesn't do to be too careless...","\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["\n","^With my jacket wrapped round my arm, I sweep out the remaining shards of glass. It's not a big window, but I'm not a big man. If I was Harris, I'd be stuffed, but as it is...","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":["^Then the door locks turns. The door opens. Then Jeremy — one of the guards, rather — sticks his head through the door. \"I thought I heard...\"","\n","^He stops. Looks for a moment. ","ev",{"VAR?":"smashingWindowItem"},2,"==","/ev",[{"->":".^.b","c":true},{"b":["^Sees the bucket in my hand.",{"->":".^.^.^.10"},null]}],[{"->":".^.b"},{"b":["^Sees the broken window.",{"->":".^.^.^.10"},null]}],"nop","^ Then without a moment's further thought he blows his shrill whistles and hustles into the hut, grabbing me roughly by my arms.","\n","ev",{"CNT?":".^.^.c-0"},"/ev",[{"->":".^.b","c":true},{"b":["\n","^I'll never know if I hadn't have waited that extra moment — maybe I still could have got away. But, how far?","\n",{"->":".^.^.^.17"},null]}],"nop","\n","^I'm hustled into one of the huts. Nowhere to sleep, but they're not interested in my comfort any longer. Harris comes in with the Captain.","\n","^\"So,\" Harris remarks. \"Looks like your little trap worked. Only it worked to show you out for what you are.\"","\n","ev","str","^Tell the truth","/str","/ev",{"*":".^.c-2","flg":20},"ev","str","^Lie","/str","/ev",{"*":".^.c-3","flg":20},"ev","str","^Evade","/str","/ev",{"*":".^.c-4","flg":20},{"c-2":["^ ","\n","ev",{"CNT?":"i_met_a_young_man"},"/ev",[{"->":".^.b","c":true},{"b":["\n","^\"Please, Harris. You can't understand the pressure they put me under. You can't understand what it's like, to be in love but be able to do nothing about it...\"","\n",{"->":".^.^.^.7"},null]}],[{"->":".^.b"},{"b":["\n","^\"Harris. They were blackmailing me. They knew about... certain indiscretions. You can understand, can't you, Harris? I was in an impossible bind...\"","\n",{"->":".^.^.^.7"},null]}],"nop","\n",{"->":".^.^.^.g-1"},{"#f":5}],"c-3":["\n","^\"I had to get out, Harris. I had to provoke Hooper into doing something that would incriminate himself fully. He's too clever, you see...\"","\n",{"->":".^.^.^.g-1"},{"#f":5}],"c-4":["^ ","\n","^\"This proves nothing,\" I reply stubbornly. \"You still don't have the component and without it, I don't see what you can hope to prove.\"","\n",{"->":".^.^.^.g-1"},{"#f":5}]}],"g-1":["^\"Be quiet, man. We know all about your and your sordid affairs.\" The Captain curls his lip. \"Don't you know there's a war on? Do you know the kind of place they would have sent you if it haven't had been for that brain of yours? Don't you think you owe it to your country to use it a little more?\"","\n","^Do I, I wonder? Do I owe this country anything, this country that has spurned who and what am I since the day I became a man?","\n","ev","str","^Yes","/str","/ev",{"*":".^.c-5","flg":20},"ev","str","^No","/str","/ev",{"*":".^.c-6","flg":20},"ev","str","^Lie","/str","/ev",{"*":".^.c-7","flg":20},"ev","str","^Evade","/str","/ev",{"*":".^.c-8","flg":20},{"c-5":["^ ","\n","^My anger deflates like a collapsing equation, all arguments cancelling each other out. The world, of course, owes me nothing; and I owe it everything.","\n",{"->":".^.^.^.g-2"},{"#f":5}],"c-6":["^ ","\n","^Of course not. I am alone; that is what they wanted me to be, because of who and what I love. So I have no nation, no country.","\n",{"->":".^.^.^.g-2"},{"#f":5}],"c-7":["^ \t",{"->":".^.^.c-6"},"\n",{"->":".^.^.^.g-2"},{"#f":5}],"c-8":["^ \t","\n","^But what is a country, after all? A country is not a concept, not an ideal. Every country falls, its borders shift and move, its language disappears to be replaced by another. Neither the Reich nor the British Empire will survive forever, so what use is my loyalty to either? ","\n","^I may as well, therefore, look after myself. Something I have attempted, but failed miserably, to do.","\n",{"->":".^.^.^.g-2"},{"#f":5}]}],"g-2":["^\"I'm afraid we have only one option, Manning,\" Harris says. \"Please, man. Tell us where the component is.\"","\n","ev",true,"/ev",{"VAR=":"notraitor","re":true},"ev",false,"/ev",{"VAR=":"losttemper","re":true},"ev","str","^Tell them","/str","/ev",{"*":".^.c-9","flg":20},"ev","str","^Say nothing","/str","/ev",{"*":".^.c-10","flg":20},{"c-9":["\n","ev",false,"/ev",{"VAR=":"revealedhooperasculprit","re":true},"^\"All right.\" I am beaten, after all. \"","<>",{"->":"reveal_location_of_component"},"\n",{"#f":5}],"c-10":["^ ",{"->":"my_lips_are_sealed"},"\n",{"#f":5}]}]}],{"#f":1}],"find_something_to_smash_window":[["^Let me see. There's the bunk, ","ev",{"VAR?":"smashingWindowItem"},"!",2,"==","/ev",[{"->":".^.b","c":true},{"b":["^a bucket,",{"->":".^.^.^.8"},null]}],"nop","^ nothing else. I have my jacket but nothing in the pockets — no handkerchief, for instance.","\n",["ev","str","^The bunk","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^The jacket","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^The bucket","/str",{"VAR?":"smashingWindowItem"},"!",2,"==","/ev",{"*":".^.c-2","flg":21},{"c-0":["^ \t","\n","^The bunk has a solid metal frame, a blanket, a pillow, nothing more.","\n",[["ev","str","^The frame","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^The blanket","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^The pillow","/str","/ev",{"*":".^.c-2","flg":20},"ev","str","^Something else","/str",{"CNT?":".^"},1,">","/ev",{"*":".^.c-3","flg":21},{"c-0":["\n","^The frame is heavy and solid. I couldn't lift it or shift it without help from another man. And it wouldn't do me any good here anyway. I can reach the window perfectly well.","\n",{"->":".^.^"},{"#f":5}],"c-1":["^ ","\n","^The blanket. Perfect. I scoop it up off the bed and hold it in place over the window. ",{"->":"smash_the_window"},"\n",{"#f":5}],"c-2":["^ ","\n","^The pillow is fat and fluffy. I could put it over the window and it would muffle the sound of breaking glass, certainly; but I wouldn't be able to break any glass through it either.","\n",{"->":".^.^"},{"#f":5}],"c-3":["^ ",{"->":".^.^.^.^.^"},"\n",{"#f":5}],"#f":5,"#n":"bunk_opts"}],null],{"#f":5}],"c-1":["^ ","\n","^I slip off my jacket and hold it with one hand over the glass. ",{"->":"smash_the_window"},"\n",{"#f":5}],"c-2":["^ ","\n","^The bucket? Hardly. The bucket might do some good if I wanted to sweep up the glass afterwards, but it won't help me smash the glass quietly.","\n",{"->":".^.^"},{"#f":5}],"#n":"opts"}],null],null]}],"smash_the_window":[["^Then I heft ","ev",{"VAR?":"smashingWindowItem"},2,"==","/ev",[{"->":".^.b","c":true},{"b":["^up the bucket — this really is quite a fiddly thing to be doing in cuffs — ",{"->":".^.^.^.8"},null]}],[{"->":".^.b"},{"b":["ev",{"VAR?":"smashingWindowItem"},1,"==","/ev",[{"->":".^.b","c":true},{"b":["^ my shoe by its toe, ",{"->":".^.^.^.7"},null]}],[{"->":".^.b"},{"b":["^back my arm, ",{"->":".^.^.^.7"},null]}],"nop",{"->":".^.^.^.8"},null]}],"nop","^ and take a strong swing, trying to imagine it's Harris' face on the other side.","\n","ev",true,"/ev",{"VAR=":"smashedglass","re":true},"ev",0,"/ev",{"VAR=":"smashingWindowItem","re":true},"ev","str","^Smash!","/str","/ev",{"*":".^.c-0","flg":20},{"c-0":["\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":["^The sound of the impact is muffled. With my arm still covered, I sweep out the remaining glass in the frame.","\n",["^I'm ready to escape. The only trouble is — when they look in on me in the morning, there will be no question what has happened. It won't help me one jot with shifting suspicion off my back.","\n","ev","str","^Wait","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Slip out","/str","/ev",{"*":".^.c-2","flg":20},{"c-1":["\n","^So perhaps I should wait it out, after all. Who knows? I might have a better opportunity later.","\n",{"->":"night_passes"},{"->":".^.^.^.^.g-2"},{"#f":5}],"c-2":["^ ","\n","^Moving quickly and quietly, I hoist myself up onto the window—frame and worm my way outside into the freezing night air. Then I am away, slipping down the paths between the Huts, sticking to the shadows, on my way to Hut 2.","\n",{"->":".^.^.^.^.g-2"},{"#f":5}],"#n":"g-1"}],null],"g-2":["ev","str","^Go the shortest way","/str","/ev",{"*":".^.c-3","flg":20},"ev","str","^Take a longer route","/str","/ev",{"*":".^.c-4","flg":20},{"c-3":["^ ","\n","^There's no time to lose. Throwing caution to the wind I make my way quickly to Hut 2, and around the back. I don't think I've been seen but if I have it is too late. My actions are suspicious enough for the noose. I have no choice but to follow through.","\n",{"->":".^.^.^.g-3"},{"#f":5}],"c-4":["\n","^In case I'm being followed, I divert around the perimeter of the compound. It's a much longer path, and it takes me across some terrain that's difficult to negotiate in the dark — muddy, and thick with thistles and nestles.","\n","ev",true,"/ev",{"VAR=":"muddyshoes","re":true},"^Still, I can be confident no—one is behind me. I crouch down behind the rear wall of Hut 2. ","<>","\n",{"->":".^.^.^.g-3"},{"#f":5}]}],"g-3":["^The component is still there, wrapped in a tea—towel and shoved into a cavity in a breeze—block at the base of the Hut wall.","\n","ev","str","^Take it","/str","/ev",{"*":".^.c-5","flg":20},"ev","str","^Leave it","/str","/ev",{"*":".^.c-6","flg":20},{"c-5":["^ ","\n","^Quickly, I pull it free, and slip it into the pocket of my jacket.","\n","ev",true,"/ev",{"VAR=":"gotcomponent","re":true},{"->":".^.^.^.g-4"},{"#f":5}],"c-6":["^ ","\n","^Still there means no—one has found it, which means it is probably well—hidden. And short of skipping the compound now, I can afford to leave it hidden there a while longer. So I leave it in place.","\n",{"->":".^.^.^.g-4"},{"#f":5}]}],"g-4":["^Where now?","\n","ev","str","^Back to the barracks","/str","/ev",{"*":".^.c-7","flg":20},"ev","str","^Go to Hooper's dorm","/str",{"VAR?":"gotcomponent"},"/ev",{"*":".^.c-8","flg":21},"ev","str","^Escape the compound","/str","/ev",{"*":".^.c-9","flg":20},{"c-7":["^ ",{"->":"return_to_room_after_excursion"},"\n",{"#f":5}],"c-8":["^ ",{"->":"go_to_hoopers_dorm"},"\n",{"#f":5}],"c-9":["^ ","\n","^Enough of this place. Time for me to get moving. I can get to the train station on foot, catch the postal train to Scotland and be somewhere else before anyone realises that I'm gone.","\n","^Of course, then they'll be looking for me in earnest. ","ev",{"VAR?":"framedhooper"},"!","/ev",[{"->":".^.b","c":true},{"b":["^As a confirmed traitor.",{"->":".^.^.^.11"},null]}],[{"->":".^.b"},{"b":["^Perhaps not as a traitor — they might take the idea that Hooper was involved with the theft — but certainly as a valuable mind, one containing valuable secrets and all too easily threatened. They will think I am running away because of my indiscretions. I suppose, in fairness, that I am.",{"->":".^.^.^.11"},null]}],"nop","\n",["ev","str","^Go","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Don't go","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["^ \t\t\t",{"->":"live_on_the_run"},"\n",{"#f":5}],"c-1":["^ ","\n","^It's no good. That's only half a solution. I couldn't be happy with that.","\n",["ev","str","^Back to the barracks","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^To Hooper's dorm","/str",{"VAR?":"gotcomponent"},{"CNT?":"go_to_hoopers_dorm"},"!","&&","/ev",{"*":".^.c-1","flg":21},{"c-0":["^ \t\t\t",{"->":"return_to_room_after_excursion"},"\n",{"#f":5}],"c-1":["^ ",{"->":"go_to_hoopers_dorm"},"\n",{"#f":5}]}],{"#f":5}]}],{"#f":5}]}]}],null],"go_to_hoopers_dorm":[["^I creep around the outside of the huts towards Hooper's dorm. Time to wrap up this little game once and for all. A few guards patrol the area at night but not many — after all, very few know this place even exists.","\n","^Our quarters are arranged away from the main house; where we sleep is of less importance than where we work. We each have our own hut, through some are less permanent than others. Hooper's is a military issue tent: quite a large canopy, with two rooms inside and a short porch area where he insists people leave their shoes. It's all zipped up for the night and no light shines from inside.","\n","^I hang back for a moment. If Harris is keeping to the terms of our deal then someone will be watching this place. But I can see no—one.","\n","ev","str","^Open the outer zip","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Look for another opening","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Hide the component somewhere","/str","/ev",{"*":".^.c-2","flg":20},{"c-0":["^ ","\n","^I creep forward to the tent, intent on lifting the zip to the front porch area just a little — enough to slip the component inside, and without the risk of the noise waking Hooper from his snoring.","\n","^The work is careful, and more than little fiddly — Hooper has tied the zips down on the inside, the fastidious little bastard! — but after a little work I manage to make a hole large enough for my hand.","\n",["ev","str","^Slip in the component","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^No, some other way","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["^ \t\t","\n","^I slide the component into the tent, work the zip closed, and move quickly away into the shadows. It takes a few minutes for my breath to slow, and my heart to stop hammering, but I see no other movement. If anyone is watching Hooper's tent, they are asleep at their posts.","\n","ev",true,"/ev",{"VAR=":"putcomponentintent","re":true},"ev",false,"/ev",{"VAR=":"gotcomponent","re":true},{"->":"return_to_room_after_excursion"},{"#f":5}],"c-1":["^ \t\t\t","\n","^Then pause. This is too transparent. Too blatant. If I leave it here, like this, Hooper will never be seen to go looking for it: he will stumble over it in plain sight, and the men watching will wonder why it was not there when he went to bed.","\n","^No, I must try something else — or nothing at all.","\n",["ev","str","^On top of the tent","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Throw the component into the long grass","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Give up","/str","/ev",{"*":".^.c-2","flg":20},{"c-0":["^ ",{"->":".^.^.^.^.^.^.^.put_component_on_tent"},"\n",{"#f":5}],"c-1":["^ ","\n","^From inspiration — or desperation, I am not certain — a simple approach occurs to me. ",{"->":".^.^.^.^.^.^.^.toss_component_into_bushes"},"\n",{"#f":5}],"c-2":["^ ","\n","^There is nothing to be gained here. I have the component now; maybe it will be of some value tomorrow.","\n",["ev","str","^Return to my barrack","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Escape the compound","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["^ ",{"->":"return_to_room_after_excursion"},"\n",{"#f":5}],"c-1":["^ ",{"->":"live_on_the_run"},"\n",{"#f":5}]}],{"#f":5}]}],{"#f":5}]}],{"#f":5}],"c-1":["^ ","\n","^Making a wide circuit I creep around the tent. It has plenty of other flaps and openings, tied down with Gordian complexity. But nothing afford itself to slipping the component inside.","\n",["ev","str","^Try the porch zip","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Try on top of the tent","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Give up","/str","/ev",{"*":".^.c-2","flg":20},{"c-0":["^ \t\t\t",{"->":".^.^.^.^.c-0"},"\n",{"#f":5}],"c-1":["^ \t\t",{"->":".^.^.^.^.^.put_component_on_tent"},"\n",{"#f":5}],"c-2":["^ \t\t\t\t\t\t","\n","^It's no good. Nothing I can do will be any less than obvious — something appearing where something was not there before. The men watching Hooper will know it is a deception and Hooper's protestations will be taken at face value.","\n","^If I can't find a way for Hooper to pick the component up, as if from a hiding place of his own devising, and be caught doing it, then I have no plan at all.","\n",["ev","str","^Return to my barrack","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Escape the compound","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Toss the component into the bushes","/str","/ev",{"*":".^.c-2","flg":20},{"c-0":["^ ",{"->":"return_to_room_after_excursion"},"\n",{"#f":5}],"c-1":["^ ",{"->":"live_on_the_run"},"\n",{"#f":5}],"c-2":["^ ",{"->":".^.^.^.^.^.^.^.toss_component_into_bushes"},"\n",{"#f":5}]}],{"#f":5}]}],{"#f":5}],"c-2":["^ ","\n","^If I leave the component here somewhere it should be somewhere I can rely on Hooper finding it, but no—one before Hooper. In particular.","\n",["ev","str","^Behind the tent","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Inside the porch section","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^On top of the canvas","/str","/ev",{"*":".^.c-2","flg":20},{"c-0":["^\t\t\t \t",{"->":".^.^.^.^.c-1"},"\n",{"#f":5}],"c-1":["^ \t\t",{"->":".^.^.^.^.c-0"},"\n",{"#f":5}],"c-2":["^ \t\t\t",{"->":".^.^.^.^.^.put_component_on_tent"},"\n",{"#f":5}]}],{"#f":5}]}],{"put_component_on_tent":["^A neat idea strikes me. If I could place it on top of the canvas, somewhere in the middle where it would bow the cloth inwards, then it would be invisible to anyone passing by. But to Hooper, it would be above him: a shadow staring him in the face as he awoke. What could be more natural than getting up, coming out, and looking to see what had fallen on him during the night?","\n","^It's the work of a moment. I was once an excellent bowler for the second XI back at school. This time I throw underarm, of course, but I still land the vital missing component exactly where I want it to go.","\n","ev",true,"/ev",{"VAR=":"framedhooper","re":true},"ev",false,"/ev",{"VAR=":"gotcomponent","re":true},"^For a second I hold my breath, but nothing and no—one stirs. ",{"->":"return_to_room_after_excursion"},"\n",null],"toss_component_into_bushes":["^I toss the component away into the bushes behind Hooper's tent and return to my barrack, wishing myself a long sleep followed by a morning, free of this business.","\n","ev",false,"/ev",{"VAR=":"gotcomponent","re":true},"ev",true,"/ev",{"VAR=":"throwncomponentaway","re":true},{"->":"return_to_room_after_excursion"},null],"#f":1}],"live_on_the_run":["^Better to live on the run than die on the spit. Creeping around the edge of the compound","ev",{"VAR?":"gotcomponent"},"/ev",[{"->":".^.b","c":true},{"b":["^, the Bombe component heavy in my pocket",{"->":".^.^.^.5"},null]}],"nop","^, I make my way to the front gate. As always, it's manned by two guards, but I slip past their box by crawling on my belly.","\n","^And then I'm on the road. Walking, not running. Silent. Free.","\n","^For the moment, at least.","\n","end",null],"return_to_room_after_excursion":[["ev",{"VAR?":"gotcomponent"},"/ev",[{"->":".^.b","c":true},{"b":["^The weight of the Bombe component safely in my jacket",{"->":".^.^.^.5"},null]}],[{"->":".^.b"},{"b":["^Satisfied",{"->":".^.^.^.5"},null]}],"nop","^, I return the short way up the paths between the huts to the barrack block and the broken window.","\n","^It's a little harder getting back through — the window is higher off the ground than the floor inside — but after a decent bit of jumping and hauling I manage to get my elbows up, and then one leg, and finally I collapse inside, quite winded and out breath.","\n","ev","str","^Wait","/str","/ev",{"*":".^.c-0","flg":20},{"c-0":["^ \t",{"->":"night_passes"},"\n",{"#f":5}]}],null],"night_passes":[["^The rest of the night passes slowly. I sleep a little, dozing mostly. Then I'm woken by the rooster in the yard. The door opens, and Harris comes in. He takes one look at the broken window and frowns with puzzlement.","\n","ev",{"VAR?":"putcomponentintent"},"/ev",[{"->":".^.b","c":true},{"b":["^ ",{"->":".^.^.^.^.put_component_inside_tent"},{"->":".^.^.^.6"},null]}],"nop","\n","^\"What happened there?\"","\n","ev","str","^Confess","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Deny","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Show him the component","/str",{"VAR?":"gotcomponent"},"/ev",{"*":".^.c-2","flg":21},{"c-0":["^ ","\n","^\"I broke it,\" I reply. There doesn't seem any use in trying to lie. \"I thought I could escape. But I couldn't get myself through.\"","\n","^The Commander laughs. ",{"->":".^.^.^.glad_youre_here"},"\n",{"#f":5}],"c-1":["^ ","\n","^\"I'm not sure. I was asleep: I woke up when someone broke the window. I looked out to see who it was, but they were already gone.\"","\n","^Harris looks at me with puzzlement. \"Someone came by to break the window, and then ran off? That's absurd. That's utterly absurd. Admit it, Manning. You tried to escape and you couldn't get through.\"","\n",["ev","str","^Admit it","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Deny it","/str",{"VAR?":"framedhooper"},"!","/ev",{"*":".^.c-1","flg":21},"ev","str","^Deny it","/str",{"VAR?":"framedhooper"},"/ev",{"*":".^.c-2","flg":21},{"c-0":["\n","^\"All right. ","ev",{"VAR?":"forceful"},1,">","/ev",[{"->":".^.b","c":true},{"b":["^Damn you.",{"->":".^.^.^.8"},null]}],"nop","^ That's exactly it.\"","\n",{"->":".^.^.^.^.^.glad_youre_here"},{"#f":5}],"c-1":["\n","^\"If I wanted to escape, I would have made damn sure that I could,\" I tell him sternly.","\n",{"->":"harris_certain_is_you"},{"#f":5}],"c-2":["^ ","\n","^\"I tell you, someone broke it. Someone wanted to threaten me, I think.\"","\n","^Harris shakes his head. \"Well, we can look into that matter later. For now, you probably want to hear the more pressing news. ",{"->":".^.^.^.^.^.found_missing_component"},"\n",{"#f":5}]}],{"#f":5}],"c-2":["^ ",{"->":".^.^.^.someone_threw_component"},"\n",{"#f":5}]}],{"put_component_inside_tent":[["^He takes one look around, and sighs, a deep, wistful sigh.","\n","^\"Things just get worse and worse for you, Manning,\" he remarks. \"You are your own worst enemy.\"","\n","ev","str","^Agree","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Disagree","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Evade","/str","/ev",{"*":".^.c-2","flg":20},{"c-0":["^ ","\n","^\"I've thought so before.\" ","ev",{"VAR?":"admitblackmail"},"/ev",[{"->":".^.b","c":true},{"b":["^Certainly in the matter of getting blackmailed.",{"->":".^.^.^.7"},null]}],"nop","\n","^\"Let me tell you what happened this morning. ","<>","\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["\n","^\"Right now, I think you take that role, Harris,\" I reply coolly.","\n",[["^\"Very droll,\" he replies. \"Let me tell you what happened this morning. It will take the smile off your face. ","<>","\n",{"->":".^.^.^.^.g-0"},{"#n":"droll"}],null],{"#f":5}],"c-2":["^ ","\n","^\"I'm looking forward to having a wash and a change of clothes; which should make me a little less evil to be around.\"","\n",{"->":".^.^.c-1.3.droll"},{"->":".^.^.g-0"},{"#f":5}],"g-0":["^Our men watching Hooper's tent saw Hooper wake up, get dressed, clamber out of his tent and then step on something in at the entrance of his tent.\"","\n","ev",true,"/ev",{"VAR=":"piecereturned","re":true},"ev","str","^Be interested","/str","/ev",{"*":".^.c-3","flg":20},"ev","str","^Be dismissive","/str","/ev",{"*":".^.c-4","flg":20},"ev","str","^Say nothing","/str","/ev",{"*":".^.c-5","flg":20},{"c-3":["^ ","\n","^\"You mean he didn't even hide it? He put it in his shoe?\"","\n",[["^\"No,\" Harris replies. \"That isn't really what I mean. ","<>","\n",{"->":".^.^.^.^.^.g-1"},{"#n":"not_that"}],null],{"#f":5}],"c-4":["\n","^\"So he's an idiot, and he hid it in his shoe.\"","\n",{"->":".^.^.c-3.4.not_that"},{"->":".^.^.^.g-1"},{"#f":5}],"c-5":["^ ","\n","^I say quiet, listening, not sure how this will go.","\n","^\"In case I'm not making myself clear,\" Harris continues, \"","<>","\n",{"->":".^.^.^.g-1"},{"#f":5}]}],"g-1":["^I mean, he managed to find it, by accident, somewhere where it wasn't the night before. And at the same time, you're sitting here with your window broken. So, I rather think you've played your last hand and lost. It's utterly implausible that Hooper stole that component and then left it lying around in the doorway of his tent. So I came to tell you that the game is up, for you.\"","\n","^He nods and gets to his feet. ",{"->":"left_alone"},"\n",null]}],null],"someone_threw_component":[["^\"Someone threw this in through the window over night,\" I reply, and open my jacket to reveal the component from the Bombe. \"I couldn't see who, it was too dark. But I know what it is.\"","\n","^He reaches out and takes it. \"Well, I'll be damned,\" he murmurs. \"That's it all right. And you didn't have it on you when we put you in here. But it can't have been Hooper — I had men watching him all night. And there's no—one else it could have been.\"","\n","^He turns the component over in his hands, bemused.","\n","ev",true,"/ev",{"VAR=":"piecereturned","re":true},"ev","str","^Suggest something","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Suggest nothing","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["^ ","\n","^\"Perhaps Hooper had an accomplice. Someone else who works on site.\"","\n","^Harris shakes his head, distractedly. \"That doesn't make sense,\" he says. \"Why go to all the trouble of stealing it only to give it back? And why like this?\"","\n",["ev","str","^Suggest something","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Suggest nothing","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["^ ","\n","^\"Perhaps the accomplice thought it was Hooper being kept in here. Maybe they saw the guard...\"","\n",{"->":"all_too_farfetched"},{"->":".^.^.^.^.g-0"},{"#f":5}],"c-1":["^ ","\n",{"->":".^.^.^.^.g-0"},{"#f":5}]}],{"#f":5}],"c-1":["^ ","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":["^I shrug, eloquently.","\n",[{"->":"all_too_farfetched"},{"#n":"g-1"}],null]}],null],"glad_youre_here":[["^\"Shame,\" he remarks. \"I should have left that window open and put a guard on you. Might have been interesting to see where you went. Anyway, I'm glad you're still here, even if you do smell like a dog.\"","\n","ev","str","^Be optimistic","/str",{"VAR?":"framedhooper"},"!","/ev",{"*":".^.c-0","flg":21},"ev","str","^Be pessimistic","/str",{"VAR?":"framedhooper"},"!","/ev",{"*":".^.c-1","flg":21},"ev","str","^Be optimistic","/str",{"VAR?":"framedhooper"},"/ev",{"*":".^.c-2","flg":21},"ev","str","^Be pessimistic","/str",{"VAR?":"framedhooper"},"/ev",{"*":".^.c-3","flg":21},{"c-0":["^ ","\n",{"->":"night_falls.morning_not_saved.0.c-0"},{"->":".^.^.g-0"},{"#f":5}],"c-1":["^ ","\n",{"->":"night_falls.morning_not_saved.0.c-1"},{"->":".^.^.g-0"},{"#f":5}],"c-2":["^ ","\n","^\"I'm looking forward to having a bath.\"","\n","^\"Well, you should enjoy it. ","<>","\n",{"->":".^.^.g-0"},{"#f":5}],"c-3":["\n","^\"I imagine I'll smell worse after another couple of days of this.\"","\n","^\"That won't be necessary. ","<>","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":[{"->":".^.^.^.^.found_missing_component"},null]}],null],"found_missing_component":[["^We found the missing component. Or rather, Hooper found it for us. He snuck out and retrieved it from on top. Of all the damnest places — you would never have known it was there. He claimed ignorance when we jumped him, of course. But it's good enough for me.\"","\n","ev","str","^Approve","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Disapprove","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["\n","^\"I can't tell you enough, I'm glad to hear it. I've had a devil of a night.\"","\n","^His gaze flicks to the broken window, but only for a moment. I think he genuinely cannot believe I could have done it.","\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["^ ","\n","^\"You should never have hired him. A below-average intelligence can't be expected to cope with the pressure of our work.\"","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":["^Harris rolls his eyes, but he might almost be smiling. \"You'd better get along, ","ev",{"CNT?":".^.^.c-0"},"/ev",[{"->":".^.b","c":true},{"b":["^and work through your devils",{"->":".^.^.^.6"},null]}],[{"->":".^.b"},{"b":["^Mr Intelligent",{"->":".^.^.^.6"},null]}],"nop","^. There's a 24—hour—late message to be tackled and we're a genius short. So you'd better be ready to work twice as hard.\"","\n","ev","str","^Thank him","/str","/ev",{"*":".^.c-2","flg":20},"ev","str","^Argue with him","/str","/ev",{"*":".^.c-3","flg":20},{"c-2":["^ \t","\n","^\"I'll enjoy it. Thank you for helping me clear this up.\"","\n","^\"Don't thank me yet. There's still a war to fight. Now get a move on.\"","\n","^I nod, and hurry out of the door. The air outside has never tasted fresher and more invigorating. ","<>","\n",{"->":".^.^.^.g-1"},{"#f":5}],"c-3":["^ ","\n","^\"I'll work as hard as I work.\"","\n","^\"Get out,\" Harris growls. \"Before I decide to arrest you as an accessory.\"","\n","^I do as he says. Outside the barrack, the air has never smelt sweeter.","\n",{"->":".^.^.^.g-1"},{"#f":5}]}],"g-1":[{"->":"head_for_my_dorm_free"},null]}],null]}],"night_falls":[["^Night falls. The clockwork of the heavens keeps turning, whatever state I might be in. No—one can steal the components that make the sun go down and the stars come out. I watch it performing its operations. I can't sleep.","\n","ev",{"VAR?":"hooperClueType"},0,">","/ev",[{"->":".^.b","c":true},{"b":["\n","^Has Hooper taken my bait?","\n",{"->":".^.^.^.8"},null]}],"nop","\n","ev","str","^Look of out the window","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Listen at the door","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Wait","/str","/ev",{"*":".^.c-2","flg":20},{"c-0":["^ ","\n","^I peer out of the window, but it looks out onto the little brook at the back of the compound, with no view of the other huts or the House. Who knows if there are men up, searching the base of Hut 2, following one another with flashlights...","\n","ev",{"CNT?":"inside_hoopers_hut.back_of_hut_2"},"/ev",[{"->":".^.b","c":true},{"b":["\n","^Perhaps Hooper is there, in the dark, trying to help me after all?","\n",{"->":".^.^.^.8"},null]}],"nop","\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["^ \t","\n","^I put my ear to the keyhole but can make out nothing. Are there still guards posted? ","ev",{"VAR?":"hooperClueType"},0,">","/ev",[{"->":".^.b","c":true},{"b":["^Perhaps, if Hooper has managed to incriminate himself, the guards have been removed?",{"->":".^.^.^.10"},null]}],[{"->":".^.b"},{"b":["^Perhaps the component has been found and the crisis is over.",{"->":".^.^.^.10"},null]}],"nop","\n","^Perhaps the door is unlocked and they left me to sleep?","\n",["ev","str","^Try it","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Leave it","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["^ I try the handle. No such luck.","\n",{"->":".^.^.^.^.g-0"},{"#f":5}],"c-1":["^ I don't touch it. I don't want anyone outside thinking I'm trying to escape.","\n",{"->":".^.^.^.^.g-0"},{"#f":5}]}],{"#f":5}],"c-2":["^ \t\t\t\t\t","\n","^There is nothing I can do to speed up time.","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":["^The night moves at its own pace. I suppose by morning I will know my fate.","\n","ev","str","^Wait","/str",{"VAR?":"hooperClueType"},0,">","/ev",{"*":".^.c-3","flg":21},"ev","str","^Wait","/str",{"VAR?":"hooperClueType"},0,"==","/ev",{"*":".^.c-4","flg":21},{"c-3":["^ ","\n","^Morning comes. I'm woken by a rooster calling from the yard behind the House. I must have slept after all. I pull myself up from the bunk, shivering slightly. There is condensation on the inside of the window. I have probably given myself a chill.","\n","^Without knocking, Harris comes inside. \"You're up,\" he remarks, and then, \"You smell like an animal.\"","\n",["ev","str","^Be friendly","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Be cold","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["^ ","\n","^\"I suppose I do rather.\" I laugh, but Harris does not.","\n","^\"This damn business gets worse and worse,\" he says, talking as he goes over to unlock and throw open the window. ","<>","\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["^ ","\n","^\"So would you,\" I reply tartly. Harris shrugs.","\n","^\"I've been through worse than this,\" he replies matter—of—factly. \"It's hardly my fault if you sleep in your clothes.\"","\n","^I glare back. He goes over to the window, unlocks it and throws it open, relishing the fresh air from outside.","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":["^\"Hooper's confessed, you know.\"","\n","ev","str","^Be eager","/str","/ev",{"*":".^.c-2","flg":20},"ev","str","^Be cautious","/str","/ev",{"*":".^.c-3","flg":20},{"c-2":["^ ","\n","^\"He has? I knew he would. The worm.\"","\n","^\"Steady now. Matters aren't over yet. ","<>","\n",{"->":".^.^.^.hooper_didnt_give_himself_up"},{"#f":5}],"c-3":["^ ","\n","^\"Oh, yes?\"","\n","^\"Yes. For what that's worth. ","<>","\n",{"->":".^.^.^.hooper_didnt_give_himself_up"},{"#f":5}]}],"hooper_didnt_give_himself_up":["^There's still the issue of the component. It hasn't turned up. He didn't lead us to it. I guess he figured you must have had something on him. I don't know.\"","\n","^He looks quite put out by the whole affair. He is not the kind of man to deal well with probabilities.","\n","ev","str","^Be interested","/str","/ev",{"*":".^.c-4","flg":20},"ev","str","^Be disinterested","/str","/ev",{"*":".^.c-5","flg":20},{"c-4":["^ ","\n","^\"You mean he confessed of his own accord? You didn't catch him?\"","\n",{"->":".^.^.^.g-1"},{"#f":5}],"c-5":["^ ","\n","^\"Well, I'm glad his conscience finally caught up with him,\" I reply dismissively.","\n",{"->":".^.^.^.g-1"},{"#f":5}],"#f":5}],"g-1":["^\"The Captain went back into that hut and he confessed immediately. We were so surprised we didn't let you go.\" He wrinkles his nose. \"I'm rather sorry about that now. I suggest you have a wash.\"","\n","^And with that he gestures to the doorway.","\n","ev","str","^Go","/str","/ev",{"*":".^.c-6","flg":20},"ev","str","^Wait","/str","/ev",{"*":".^.c-7","flg":20},{"c-6":["^ ","\n",{"->":".^.^.^.g-2"},{"#f":5}],"c-7":["^ ","\n","^I hang back a moment. Something does not seem quite right. After all, Hooper did not steal the component. He has no reason to confess to anything. Perhaps this is another trap?","\n","^\"Well?\" Harris asks. \"What are you waiting for? Please don't tell me you want to confess now as well, I don't think my head could stand it.\"","\n",["ev","str","^Confess","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Don't confess","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["^ ","\n","^After a chance like this? A chance — however real — to save my neck? To hand it over — what, to save Hooper's worthless skin?","\n",["ev","str","^Confess","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Don't confess","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["^ ","\n","^I see. Perhaps you think I bullied the man into giving himself up. Perhaps he understood my little clue far enough to know it was a threat against him, but not well enough to understand where he should look to find it. So he took the easy route out and folded. Gave me the hand.","\n","ev",true,"/ev",{"VAR=":"hooperConfessed","re":true},"^Hardly sporting, of course.","\n",["ev","str","^Confess","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Don't confess","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["\n","^Well, then. I suppose this must be what it feels like to have a conscience. I suppose I had always wondered.","\n","^\"Harris, sir. I don't know what Hooper's playing at, sir. But I can't let him do this.\"","\n","^\"Do what?\"","\n","^\"Take the rope for this. I took it, sir.","\n","ev",false,"/ev",{"VAR=":"revealedhooperasculprit","re":true},"ev",false,"/ev",{"VAR=":"losttemper","re":true},{"->":"reveal_location_of_component"},{"->":".^.^.^.^.^.^.g-0"},{"#f":5}],"c-1":["^ ","\n",{"->":".^.^.^.^.^.^.g-0"},{"#f":5}]}],{"#f":5}],"c-1":["^ ","\n",{"->":".^.^.^.^.g-0"},{"#f":5}]}],{"#f":5}],"c-1":["^ ","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":["^\"I certainly don't. But still, I'm surprised. I had Hooper down for a full—blown double agent, a traitor. He knows he'll face the rope, doesn't he?\"","\n","^\"Don't ask me to explain why he did what he did,\" Harris sighs. \"Just be grateful that he did, and you're now off the hook.\"","\n",{"->":".^.^.^.^.^.g-2"},null]}],{"#f":5}]}],"g-2":["^Curiouser and curiouser. I nod once to Harris and slip outside into the cold morning air.","\n","ev",{"VAR?":"hooperClueType"},0,"==","/ev",[{"->":".^.b","c":true},{"b":["\n","^Hooper's confession only makes sense in one fashion","ev",{"VAR?":"hooperConfessed"},"/ev",[{"->":".^.b","c":true},{"b":["^, and that is his being dim—witted and slow",{"->":".^.^.^.7"},null]}],[{"->":".^.b"},{"b":["^ — if I successfully implied to him that I had him framed, but he did not unpack my little clue well enough to go looking for the component. Well, I had figured him for a more intelligent opponent, but a resignation from the game will suffice",{"->":".^.^.^.7"},null]}],"nop","^. Or perhaps he knew he would be followed if he went to check, and decided he would be doomed either way.","\n",{"->":".^.^.^.9"},null]}],[{"->":".^.b"},{"b":["\n","^Hooper's confession only makes sense in one way — and that's that he believed me. He reasoned that he would be followed. To try and uncover the component would have got him arrested, and to confess was the same.","\n","^He simply caved, and threw in his hand.","\n",{"->":".^.^.^.9"},null]}],"nop","\n","^Of course, however, there is only one way to be certain that Harris is telling the truth, and that is to check the breeze—block at the back of Hut 2.","\n","ev","str","^Check","/str","/ev",{"*":".^.c-8","flg":20},"ev","str","^Don't check","/str","/ev",{"*":".^.c-9","flg":20},{"c-8":["^ ",{"->":"go_to_where_component_is_hidden"},"\n",{"#f":5}],"c-9":["\n","^But there will time for that later. If there is nothing there, then Hooper discovered the component after all and Harris' men will have swooped on him, and the story about his confession is just a ruse to test me out.","\n","^And if the component is still there — well. It will be just as valuable to my contact in a week's time, and his deadline of the 31st is not yet upon us.","\n",{"->":"head_for_my_dorm_free"},{"#f":5}]}]}],{"#f":5}],"c-4":["^ ",{"->":".^.^.^.^.morning_not_saved"},"\n",{"#f":5}]}]}],{"morning_not_saved":[["^Morning comes with the call of a rooster from the yard of the House. I must have slept after all. I pull myself up off the bunk, shivering slightly. There is condensation on the inside of the window. I have probably given myself a chill.","\n","^It's not long after that Harris enters the hut. He closes the door behind him, careful as ever, then takes a chair across from me.","\n","^\"You smell like a dog,\" he remarks.","\n","ev","str","^Be optimistic","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Be pessimistic","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["^ ","\n","^\"I'm looking forward to a long bath,\" I reply. \"And getting back to work.\"","\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["^ ","\n","^\"So would you after the night I've had.\"","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":[{"->":"harris_certain_is_you"},null]}],null]}],"harris_certain_is_you":["^\"Well, I'm afraid it is going to get worse for you,\" Harris replies soberly. \"We followed Hooper, and he took himself neatly to bed and slept like a boy scout. Which puts us back to square one, and you firmly in the frame. And I'm afraid I don't have time for any more games. I want you to tell me where that component is, or we will hang you as a traitor.\"","\n","ev",false,"/ev",{"VAR=":"revealedhooperasculprit","re":true},"ev",false,"/ev",{"VAR=":"losttemper","re":true},{"->":"harris_threatens_lynching"},{"#f":1}],"head_for_my_dorm_free":[["^I head for my dorm, intent on a bath, breakfast, a glance at the crossword before the other men get to it, and then on with work. They should have replaced the component in the Bombe by now. We will only be a day behind.","\n","ev",{"VAR?":"framedhooper"},"!","/ev",[{"->":".^.b","c":true},{"b":["\n","^And then everything will proceed as before. The component will mean nothing to the Germans — this is the one fact I could never have explained to a man like Harris, even though the principle behind the Bombe is the same as the principle behind the army. The individual pieces — the men, the components — do not matter. They are identical. It is how they are arranged that counts.","\n",{"->":".^.^.^.7"},null]}],"nop","\n","^I bump into Russell in the dorm hut.","\n","^\"Did you hear?\" he whispers. \"Terrible news about Hooper. Absolutely terrible.\"","\n","ev","str","^Yes","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^No","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Lie","/str","/ev",{"*":".^.c-2","flg":20},"ev","str","^Evade","/str","/ev",{"*":".^.c-3","flg":20},{"c-0":["^ ","\n","^\"Quite terrible. I would never have guessed.\"","\n","^\"Well.\" Russell harrumphs.","\n",[["^\"Quince was saying this morning, apparently his grandfather was German. So perhaps it's to be expected. See you there?\"","\n",{"->":".^.^.^.^.g-0"},{"#n":"quince"}],null],{"#f":5}],"c-1":["\n","^\"Heard what?\"","\n",[["^\"Hooper's been taken away. They caught him, uncovering that missing Bombe component from a hiding place somewhere, apparently about to take it to his contact.\" Russell harrumphs. ",{"->":".^.^.^.^.c-0.6.quince"},"\n",{"->":".^.^.^.^.g-0"},{"#n":"hooper_taken"}],null],{"#f":5}],"c-2":["^ ","\n","^\"I don't know what you're talking about.\"","\n",{"->":".^.^.c-1.3.hooper_taken"},{"->":".^.^.g-0"},{"#f":5}],"c-3":["\n","^\"If you'll excuse me, Russell. I was about to take a bath.\"","\n","^\"Oh, of course. Worked all night, did you? Well, you'll hear soon enough. Can hardly hide the fact there'll only be three of us from now on.\"","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":["^I wave to him and move away, my thoughts turning to the young man in the village. My lover. My contact. My blackmailer. Hooper may have taken the fall for the missing component, but ","ev",{"VAR?":"framedhooper"},"!","/ev",[{"->":".^.b","c":true},{"b":["^if he did recover it from Hut 2 then ",{"->":".^.^.^.7"},null]}],[{"->":".^.b"},{"b":["^ its recovery does mean ",{"->":".^.^.^.7"},null]}],"nop","^I have nothing to sell to save my reputation","ev",{"CNT?":"i_met_a_young_man"},"/ev",[{"->":".^.b","c":true},{"b":["^, if I have any left",{"->":".^.^.^.13"},null]}],"nop","^.","\n","ev",{"VAR?":"framedhooper"},"!","/ev",[{"->":".^.b","c":true},{"b":["\n","^If he didn't, of course, and Harris was telling the truth about his sudden confession, then I will be able to buy my freedom once and for all.","\n",{"->":".^.^.^.21"},null]}],"nop","\n","ev","str","^Get the component","/str",{"VAR?":"framedhooper"},"!","/ev",{"*":".^.c-4","flg":21},"ev","str","^Leave it","/str",{"VAR?":"framedhooper"},"!","/ev",{"*":".^.c-5","flg":21},"ev","str","^Act normal","/str","/ev",{"*":".^.c-6","flg":20},{"c-4":["^ ",{"->":"go_to_where_component_is_hidden"},"\n",{"#f":5}],"c-5":["^ ","\n","^I will have to leave that question for another day. To return there now, when they're probably watching my every step, would be suicide. After all, if Hooper ","ev",{"VAR?":"hooperClueType"},1,"==","/ev",[{"->":".^.b","c":true},{"b":["^followed",{"->":".^.^.^.10"},null]}],[{"->":".^.b"},{"b":["^understood",{"->":".^.^.^.10"},null]}],"nop","^ my clue, he will have explained it to them to save his neck. They won't believe him — but they won't quite disbelieve him either. We're locked in a cycle now, him and me, of half—truth and probability. There's nothing either of us can do to put the other entirely into blame.","\n",{"->":"ending_return_to_normal"},{"#f":5}],"c-6":["^ ","\n","^But there is nothing to be done about it. ",{"->":"ending_return_to_normal"},"\n",{"#f":5}]}]}],null],"ending_return_to_normal":[["^Nothing, that is, except to act as if there is no game being played. I'll have a bath, then start work as normal. I've got a week to find something to give my blackmailer","ev",{"CNT?":"i_met_a_young_man"},"/ev",[{"->":".^.b","c":true},{"b":["^ — or give him nothing: it seems my superiors know about my indiscretions now already",{"->":".^.^.^.5"},null]}],"nop","^.","\n","ev","str","^Co-operate","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Dissemble","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Lie","/str","/ev",{"*":".^.c-2","flg":20},"ev","str","^Evade","/str","/ev",{"*":".^.c-3","flg":20},{"c-0":["^ ","\n","^Something will turn up. It always does. An opportunity will present itself, and more easily now that Hooper is out of the way.","\n","^But for now, there's yesterday's intercept to be resolved.","\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["^ ","\n","^Or perhaps I might hand my young blackmailer over my superiors instead for being the spy he is.","\n","^Perhaps that would be the moral thing to do, even, and not just the most smart.","\n","^But not today. Today, there's an intercept to resolve.","\n",{"->":".^.^.g-0"},{"#f":5}],"c-2":["\n","^In a week's time, this whole affair will be in the past and quite forgotten. I'm quite sure of that. ",{"->":".^.^.c-3"},"\n",{"->":".^.^.g-0"},{"#f":5}],"c-3":["^ I've more important problems to think about now. There's still yesterday's intercept to be resolved. ","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":["^The Bombe needs to be set up once more and set running.","\n","^It's time I tackled a problem I can solve.","\n","end",null]}],null],"go_to_where_component_is_hidden":[["^It won't take a moment to settle the matter. I can justify a walk past Hut 2 as part of my morning stroll. It will be obvious in a moment if the component is still there.","\n","^On my way across the paddocks, between the huts and the House, I catch sight of young Miss Lyon, arriving for work on her bicycle. She giggles as she sees me and waves.","\n","ev","str","^Wave back","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Ignore her","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["^ ","\n","^I wave cheerily back and she giggles, almost drops her bicycle, then dashes away inside the House. Judging by the clock on the front gable, she's running a little late this morning.","\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["^ ","\n","^I give no reaction. She sighs to herself, as if this kind of behaviour is normal, and trots away inside the House to begin her duties.","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":["^I turn the corner of Hut 3 and walk down the short gravel path to Hut 2. It was a good spot to choose — Hut 2 is where the electricians work, and they're generally focussed on what they're doing. They don't often come outside to smoke a cigarette so it's easy to slip past the doorway unnoticed.","\n","ev","str","^Check inside","/str","/ev",{"*":".^.c-2","flg":20},"ev","str","^Go around the back","/str","/ev",{"*":".^.c-3","flg":20},{"c-2":["^ \t\t","\n","^I hop up the steps and put my head inside all the same. Nobody about. Still too early in the AM for sparks, I suppose. ","<>","\n",{"->":".^.^.^.g-1"},{"#f":5}],"c-3":["^ ","\n",{"->":".^.^.^.g-1"},{"#f":5}]}],"g-1":["^I head on around the back of the hut. The breeze—block with the cavity is on the left side.","\n","ev","str","^Check","/str","/ev",{"*":".^.c-4","flg":20},"ev","str","^Look around","/str","/ev",{"*":".^.c-5","flg":20},{"c-4":["^ \t\t","\n","^No time to waste. I drop to my knees and check the breeze—block. Sure enough, there's nothing there. Hooper took the bait.","\n","^Suddenly, there's a movement behind me. I look up to see, first a snub pistol, and then, Harris.","\n",{"->":".^.^.^.g-2"},{"#f":5}],"c-5":["^ ","\n","^I pause to glance around, and catch a glimpse of movement. Someone ducking around the corner of the hut. Or a canvas sheet flapping in the light breeze. Impossible to be sure.","\n",["ev","str","^Check the breeze—block","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Check around the side of the hut","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["^ ",{"->":".^.^.^.^.c-4"},"\n",{"->":".^.^.^.^.^.g-2"},{"#f":5}],"c-1":["^ ","\n","^But too important to guess. I move back around the side of the hut.","\n","^Harris is there, leaning in against the wall. He holds a stub pistol in his hand.","\n",{"->":".^.^.^.^.^.g-2"},{"#f":5}]}],{"#f":5}]}],"g-2":["ev",{"VAR?":"hooperClueType"},1,">","/ev",[{"->":".^.b","c":true},{"b":["\n","^\"","ev",{"VAR?":"hooperClueType"},2,"==","/ev",[{"->":".^.b","c":true},{"b":["^Queen to rook two",{"->":".^.^.^.9"},null]}],[{"->":".^.b"},{"b":["^Messy without one missing whatever it was",{"->":".^.^.^.9"},null]}],"nop","^,\" he declares. \"I wouldn't have fathomed it but Hooper did. Explained it right after we sprung him doing what you're doing now. We weren't sure what to believe but now, you seem to have resolved that for us.\"","\n",{"->":".^.^.^.7"},null]}],[{"->":".^.b"},{"b":["\n","^\"Hooper said you'd told him where to look. I didn't believe him. Or, well. I wasn't sure what to believe. Now I rather think you've settled it.\"","\n",{"->":".^.^.^.7"},null]}],"nop","\n","ev","str","^Agree","/str","/ev",{"*":".^.c-6","flg":20},"ev","str","^Lie","/str","/ev",{"*":".^.c-7","flg":20},"ev","str","^Evade","/str","/ev",{"*":".^.c-8","flg":20},{"c-6":["^ ","\n","^\"I have, rather.\" I put my hands into my pockets. \"I seem to have done exactly that.\"","\n","^\"I'm afraid my little story about Hooper confessing wasn't true. I wanted to see if you'd go to retrieve the part.\" Harris gestures me to start walking. \"You were close, Manning, I'll give you that. I wanted to believe you. But I'm glad I didn't.\"","\n",{"->":".^.^.^.g-3.done"},{"->":".^.^.^.g-3"},{"#f":5}],"c-7":["^ ","\n","^\"I spoke to Russell. He said he saw Hooper doing something round here. I wanted to see what it was.\"","\n",{"->":".^.^.^.g-3"},{"#f":5}],"c-8":["^ ","\n","^\"Harris, you'd better watch out. He's planted a time—bomb here.\"","\n","^Harris stares at me for a moment, then laughs. \"Oh, goodness. That's rich.\"","\n","^I almost wish I had a way to make the hut explode, but of course I don't.","\n",{"->":".^.^.^.g-3"},{"#f":5}]}],"g-3":["^\"Enough.\" Harris gestures for me to start walking. \"This story couldn't be simpler. You took it to cover your back. You hid it. You lied to get Hooper into trouble, and when you thought you'd won, you came to scoop your prize. A good hand but ultimately, ","ev",{"VAR?":"hooperClueType"},1,"<=","/ev",[{"->":".^.b","c":true},{"b":["^if it hadn't have been you who hid the component, then you wouldn't be here now",{"->":".^.^.^.8"},null]}],[{"->":".^.b"},{"b":["^you told Hooper where to look with your little riddle",{"->":".^.^.^.8"},null]}],"nop","^.\"","\n",["^He leads me across the yard. Back towards Hut 5 to be decoded, and taken to pieces, once again.","\n","end",{"#n":"done"}],null]}],null],"harris_threatens_lynching":[["ev",{"CNT?":"harris_certain_is_you"},"/ev",[{"->":".^.b","c":true},{"b":["^He passes a hand across his eyes with a long look of despair.",{"->":".^.^.^.5"},null]}],[{"->":".^.b"},{"b":["^He gets to his feet, and gathers his gloves from the table top.",{"->":".^.^.^.5"},null]}],"nop","\n","^\"I'm going to go outside and organise a rope. That'll take about twelve minutes. That's how long you have to decide.\"","\n","ev","str","^Protest","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Confess","/str",{"VAR?":"gotcomponent"},"!",{"VAR?":"throwncomponentaway"},"!","&&","/ev",{"*":".^.c-1","flg":21},"ev","str","^Stay silent","/str","/ev",{"*":".^.c-2","flg":20},"ev","str","^Show him the component","/str",{"VAR?":"gotcomponent"},"/ev",{"*":".^.c-3","flg":21},{"c-0":["^ ","\n","^\"You can't do this!\" I cry. \"It's murder! I demand a trial, a lawyer; for God's sake, man, you can't just throw me overboard, we're not barbarians...!\"","\n",[["^\"You leave me no choice,\" Harris snaps back, eyes cold as gun—metal. \"You and your damn cyphers. Your damn clever problems. If men like you didn't exist, if we could just all be straight with one another.\" He gets to his feet and heads for the door. \"I fear for the future of this world, with men like you in. Reich or no Reich, Mr Manning, people like you simply complicate matters.\"","\n",{"->":"left_alone"},{"->":".^.^.^.^.g-0"},{"#f":5,"#n":"too_clever"}],null],{"#f":5}],"c-1":["^ ","\n","^I nod. \"I don't need twelve minutes. ",{"->":"reveal_location_of_component"},"\n",{"->":".^.^.g-0"},{"#f":5}],"c-2":["^ ",{"->":"my_lips_are_sealed"},"\n",{"->":".^.^.g-0"},{"#f":5}],"c-3":["^ ","\n","^\"I don't need twelve minutes. Here it is.\"","\n","^I open my jacket and pull the Bombe component out of my pocket. Harris takes it from me, whistling, curious.","\n","^\"Well, I'll be. That's it all right.\"","\n","^\"That's it.\"","\n","^\"But you didn't have it on you yesterday.\"","\n",["ev","str","^Explain","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Don't explain","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["^ ","\n","^\"I climbed out of the window overnight,\" I explain. \"I went and got this from where it was hidden, and brought it back here.\"","\n",{"->":".^.^.^.^.g-0"},{"#f":5}],"c-1":["\n","^\"No. I didn't.\"","\n",{"->":".^.^.^.^.g-0"},{"#f":5}]}],{"#f":5}],"g-0":[{"->":"all_too_farfetched"},"ev","str","^Confess","/str",{"VAR?":"throwncomponentaway"},"/ev",{"*":".^.c-4","flg":21},"ev","str","^Frame Hooper","/str",{"VAR?":"throwncomponentaway"},"/ev",{"*":".^.c-5","flg":21},{"c-4":["\n","^\"I don't need twelve minutes. The component is in the long grass behind Hooper's tent. I threw it there hoping to somehow frame him, but now I see that won't be possible. I was naive, I suppose.\"","\n","ev",true,"/ev",{"VAR=":"piecereturned","re":true},{"->":"reveal_location_of_component.harris_believes"},{"#f":5}],"c-5":["^ ","\n","^\"Look, I know where it is. The missing piece of the Bombe is in the long grasses behind Hooper's tent. I saw him throw it there right after we finished work. He knew you'd scour the camp but I suppose he thought you'd more obvious places first. I suppose he was right about that. Look there. That proves his guilt.\"","\n","ev",true,"/ev",{"VAR=":"longgrasshooperframe","re":true},"ev",true,"/ev",{"VAR=":"piecereturned","re":true},"^\"That doesn't prove anything,\" Harris returns sharply. \"But we'll check what you say, all the same.\" He gets to his feet and heads out of the door.","\n",{"->":"left_alone"},{"#f":5}]}]}],null],"reveal_location_of_component":["<>","^ The missing component of the Bombe computer is hidden in a small cavity in a breeze—block supporting the left rear post of Hut 2. I put in there anticipating a search. I intended to ","ev",{"VAR?":"revealedhooperasculprit"},"/ev",[{"->":".^.b","c":true},{"b":["^pass it to Hooper",{"->":".^.^.^.7"},null]}],[{"->":".^.b"},{"b":["^dispose of it",{"->":".^.^.^.7"},null]}],"nop","^ once the fuss had died down. I suppose I was foolish to think that it might.\"","\n","ev",true,"/ev",{"VAR=":"piecereturned","re":true},{"->":".^.harris_believes"},{"harris_believes":["ev",{"CNT?":"night_falls.0.g-0.c-3.6.hooper_didnt_give_himself_up"},"!","/ev",[{"->":".^.b","c":true},{"b":["\n","^\"Indeed. And Mr Manning: God help you if you're lying to me.\"","\n",{"->":".^.^.^.6"},null]}],[{"->":".^.b"},{"b":["\n","^\"I thought as much. I hadn't expected you to give it out so easily, however. You understand, Hooper has said nothing, of course. In fact, he went to Hut 2 directly after we released him and uncovered the component. But he told us you had instructed him where to go. Hence my little double bluff. Frankly, I'll be glad when I'm shot of the lot of you mathematicians.\"","\n",{"->":".^.^.^.6"},null]}],"nop","\n","^Harris stands, and slips away smartly. ",{"->":"left_alone"},"\n",null]}],"my_lips_are_sealed":["^I say nothing, my lips tightly, firmly sealed. It's true I am a traitor, to the very laws of nature. The world has taught me that since a very early age. But not to my country — should the Reich win this war, I would hardly be treated as an honoured hero. I was doomed from the very start.","\n","ev",true,"/ev",{"VAR=":"notraitor","re":true},"^I explain none of this. How could a man like Harris understand?","\n","^The Commander takes one look back from the doorway as he pulls it to.","\n","^\"It's been a pleasure working with you, Mr Manning,\" he declares. \"You've done a great service to this country. If we come through, I'm sure they'll remember you name. I'm sorry it had to end this way and I'll do my best to keep it quiet. No—one need know what you did.\"","\n",{"->":"left_alone"},null],"all_too_farfetched":["^\"This is all too far—fetched,\" Harris says. \"I'm glad to have this back, but I need to think.\"","\n","^Getting to his feet, he nods once. \"You'll have to wait a little longer, I'm afraid, Manning.\"","\n","^Then he steps out of the door, muttering to himself.","\n",{"->":"make_your_peace"},null],"left_alone":["ev",{"CNT?":"slam_door_shut_and_gone.time_to_move_now"},"/ev",[{"->":".^.b","c":true},{"b":["^The Commander holds the door for his superior, and follows him out.",{"->":".^.^.^.4"},null]}],"nop","^ Then the door closes. I am alone again, as I have been for most of my short life.","\n",{"->":"make_your_peace"},null],"make_your_peace":[["ev","str","^Make your peace","/str","/ev",{"*":".^.c-0","flg":20},{"c-0":["\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":["^I am waiting again. I have no God to make my peace with. I find it difficult to believe in goodness of any kind, in a world such as this.","\n","ev",{"VAR?":"notraitor"},"!","/ev",[{"->":".^.b","c":true},{"b":["\n","ev",true,"/ev",{"VAR=":"notraitor","re":true},"^But I am no traitor. Not to my country. To my sex, perhaps. But how could I support the Reich? If the Nazis were to come to power, I would be worse off than ever.","\n",{"->":".^.^.^.7"},null]}],"nop","\n","ev",{"CNT?":"harris_threatens_lynching.0.c-0.4.too_clever"},"/ev",[{"->":".^.b","c":true},{"b":["\n","^In truth, it is men like Harris who are complex, not men like me. I live to make things ordered, systematic. I like my pencils sharpened and lined up in a row. I do not deal in difficult borders, or uncertainties, or alliances. If I could, I would reduce the world to something easier to understand, something finite.","\n","^But I cannot, not even here, in our little haven from the horrors of the war.","\n",{"->":".^.^.^.13"},null]}],"nop","\n","^I have no place here. No way to fit. I am caught, in the middle, cryptic and understood only thinly, through my machines.","\n",["ev",{"^->":"make_your_peace.0.g-0.17.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-1","flg":18},{"s":["^I must seem very calm. \t\t\t",{"->":"$r","var":true},null]}],["ev",{"^->":"make_your_peace.0.g-0.18.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-2","flg":18},{"s":["^Perhaps I should try to escape.",{"->":"$r","var":true},null]}],{"c-1":["ev",{"^->":"make_your_peace.0.g-0.c-1.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.17.s"},[{"#n":"$r2"}],"\n",{"->":".^.^.^.g-1"},{"#f":5}],"c-2":["ev",{"^->":"make_your_peace.0.g-0.c-2.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.18.s"},[{"#n":"$r2"}],"^ But escape to where? I am already a prisoner. Jail would be a blessing. ",{"->":".^.^.^.g-1.monastic"},"\n",{"->":".^.^.^.g-1"},{"#f":5}]}],"g-1":["<>","^ I suppose I do not believe they will hang me. They will lock me up and continue to use my brain, if they can. I wonder what they will tell the world — perhaps that I have taken my own life. That would be simplest. The few who know me would believe it.","\n","^Well, then. Not a bad existence, in prison. Removed from temptation.","\n",["^A monastic life, with plenty of problems to keep me going.","\n","^I wonder what else I might yet unravel before I'm done?","\n",["ev",{"^->":"make_your_peace.0.g-1.monastic.4.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-3","flg":18},{"s":["^The door is opening.",{"->":"$r","var":true},null]}],{"c-3":["ev",{"^->":"make_your_peace.0.g-1.monastic.c-3.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.4.s"},[{"#n":"$r2"}],"^ Harris is returning. Our little calculation here is complete. ","ev",{"VAR?":"piecereturned"},"!","/ev",[{"->":".^.b","c":true},{"b":["^ I can only hope one of the others will be able to explain to him that the part I stole will mean nothing to the Germans.",{"->":".^.^.^.13"},null]}],[{"->":".^.b"},{"b":["^We are just pieces in this machine; interchangeable and prone to wear.",{"->":".^.^.^.13"},null]}],"nop","\n",{"->":".^.^.^.^.g-2"},{"#f":5}],"#n":"monastic"}],null],"g-2":["^That is the true secret of the calculating engine, and the source of its power. It is not the components that matter, they are quite repetitive. What matters is how they are wired; the diversity of the patterns and structures they can form. Much like people — it is how they connect that determines our victories and tragedies, and not their genius.","\n","^Which makes me wonder. Should I give ","ev",{"CNT?":"i_met_a_young_man"},"/ev",[{"->":".^.b","c":true},{"b":["^up my beautiful young man",{"->":".^.^.^.8"},null]}],[{"->":".^.b"},{"b":["^the young man who put me in this spot",{"->":".^.^.^.8"},null]}],"nop","^ to them as well as myself?","\n","ev","str","^Yes","/str","/ev",{"*":".^.c-4","flg":20},"ev","str","^No","/str","/ev",{"*":".^.c-5","flg":20},"ev","str","^Lie","/str","/ev",{"*":".^.c-6","flg":20},"ev","str","^Evade","/str","/ev",{"*":".^.c-7","flg":20},{"c-4":["^ ","\n","^But of course I will. ","ev",{"VAR?":"forceful"},2,">","/ev",[{"->":".^.b","c":true},{"b":["^Perhaps I can persuade them to put him in my cell.",{"->":".^.^.^.10"},null]}],[{"->":".^.b"},{"b":["^A little vengeance, disguised as doing something good.",{"->":".^.^.^.10"},null]}],"nop","\n",{"->":".^.^.^.g-3"},{"#f":5}],"c-5":["^ ","\n","^No. What would be the use? He will be long gone, and the name he told me is no doubt hokum. No: I was alone before in guilt, and I am thus alone again.","\n",{"->":".^.^.^.g-3"},{"#f":5}],"c-6":["^ ","\n","^No. Why would I? He is no doubt an innocent himself, trapped by some dire circumstance. Forced to act the way he did. I have every sympathy for him.","\n","^Of course I do.","\n",{"->":".^.^.^.g-3"},{"#f":5}],"c-7":["^ ","\n","^It depends, perhaps, on what his name his worth. If it were to prove valuable, well; perhaps I can concoct a few more such lovers with which to ease my later days.","\n","ev",{"VAR?":"hooper_mentioned"},"/ev",[{"->":".^.b","c":true},{"b":["^ Hooper, perhaps. He wouldn't like that. ",{"->":".^.^.^.8"},null]}],"nop","\n",{"->":".^.^.^.g-3"},{"#f":5}]}],"g-3":["ev",{"VAR?":"longgrasshooperframe"},"!","/ev",[{"->":".^.b","c":true},{"b":["\n","^Harris put the cuffs around my wrists. \"I still have the intercept in my pocket,\" I remark. \"Wherever we're going, could I have a pencil?\"","\n",{"->":".^.^.^.6"},null]}],[{"->":".^.b"},{"b":["\n","^\"We recovered the part, just where you said it was,\" Harris reports, as he puts the cuffs around my wrists. \"Of course, a couple of the men swear blind they searched there yesterday, so I'm afraid, what with the broken window... we've formed a perfectly good theory which doesn't bode well for you.\"","\n",{"->":".^.^.^.6"},null]}],"nop","\n","ev",true,"/ev",{"VAR=":"piecereturned","re":true},"ev",{"VAR?":"longgrasshooperframe"},"/ev",[{"->":".^.b","c":true},{"b":["\n","^\"I see.\" It doesn't seem worth arguing any further. \"I still have the intercept in my pocket,\" I remark. \"Wherever we're going, could I have a pencil?\"","\n",{"->":".^.^.^.16"},null]}],"nop","\n","^He looks me in the eye.","\n","ev",{"VAR?":"losttemper"},"!","/ev",[{"->":".^.b","c":true},{"b":["\n","^\"Of course. And one of your computing things, if I get my way. And when we're old, and smoking pipes together in The Rag like heroes, I'll explain to you the way that decent men have affairs.","\n",{"->":".^.^.^.26"},null]}],[{"->":".^.b"},{"b":["\n","^\"I'll give you a stone to chisel notches in the wall. And that's all the calculations you'll be doing. And as you sit there, pissing into a bucket and growing a beard down to your toes, you have a think about how a smart man would conduct his illicit affairs. With a bit of due decorum you could have learnt off any squaddie.","\n",{"->":".^.^.^.26"},null]}],"nop","\n","<>","^ You scientists.\"","\n","^He drags me up to my feet.","\n","^\"You think you have to re—invent everything.\"","\n","^With that, he hustles me out of the door and I can't help thinking that, with a little more strategy, I could still have won the day. But too late now, of course.","\n","end",null]}],null],"global decl":["ev",0,{"VAR=":"forceful"},0,{"VAR=":"evasive"},false,{"VAR=":"teacup"},false,{"VAR=":"gotcomponent"},false,{"VAR=":"drugged"},false,{"VAR=":"hooper_mentioned"},false,{"VAR=":"losttemper"},false,{"VAR=":"admitblackmail"},0,{"VAR=":"hooperClueType"},false,{"VAR=":"hooperConfessed"},0,{"VAR=":"smashingWindowItem"},false,{"VAR=":"notraitor"},false,{"VAR=":"revealedhooperasculprit"},false,{"VAR=":"smashedglass"},false,{"VAR=":"muddyshoes"},false,{"VAR=":"framedhooper"},false,{"VAR=":"putcomponentintent"},false,{"VAR=":"throwncomponentaway"},false,{"VAR=":"piecereturned"},false,{"VAR=":"longgrasshooperframe"},false,{"VAR=":"DEBUG"},"/ev","end",null]}],"listDefs":{}} \ No newline at end of file diff --git a/data/worlds/example_world.yml b/data/worlds/example_world.yml deleted file mode 100644 index 32b666d..0000000 --- a/data/worlds/example_world.yml +++ /dev/null @@ -1,706 +0,0 @@ -title: The Mysterious Mansion -author: AI Interactive Fiction -version: 1.0.0 -introduction: | - - #chapter[The Mysterious Mansion] - #music[Dark Jodler.mp3](lead=10) - - The last thing you remember is the letter: heavy paper, black wax, your name written in a hand you almost recognized. - It asked you to come after dusk, alone, and promised that the house would answer what the sender could not. - - Now you stand beyond the wrought iron gate, with rain cooling your face and the hill rising before you. - At its crest waits the old Victorian mansion, every dark window turned toward the path as if the building has been expecting you. - - The gate gives under your hand with no protest, though its ironwork is wet enough to shine black. - Gravel shifts beneath your boots as you pass between the pillars, and the garden closes behind you with the soft finality of a curtain. - - Halfway up the path, you stop and listen. - The rain has thinned to a whisper, but the house answers with other sounds: timber settling, gutters ticking, and something deep inside the walls that might be machinery or breath. - - For a heartbeat you think the mansion is about to speak ... but only the wind moves through the ivy. - It drags the leaves across the brickwork in slow strokes, as if wiping dust from an old name. - -# Room definitions -rooms: - # Starting area - front_yard: - name: Front Yard - description: | - You follow the gravel path up the hill. - The rain softens to a drizzle, and moonlight peeks through gaps in the clouds. - Ancient oak trees frame the property, their branches swaying in the gentle breeze. - At the top of three worn stone steps, the mansion's front door waits under a sagging porch roof. - The porch boards are swollen with rain, each one bending under your weight before it remembers its shape. - A brass knocker hangs at eye level, polished bright at the edges where countless hands have touched it and left no warmth behind. - The letter in your pocket presses against your ribs. - You remember the last line now: come before the clocks learn your name. - Somewhere above you, behind a blind upper window, a pale shape passes from left to right and is gone. - You tell yourself it was a reflection, then look back at the path and find no light behind you bright enough to make one. - The house waits. - - #sfx[squeaky-door.ogg] - When you reach for the handle, it turns before your fingers touch it, and the door opens with a long, complaining squeak. - exits: - - direction: north - targetRoomId: entrance_hall - description: large wooden doors lead into the mansion - - direction: south - targetRoomId: street - description: wrought iron gates lead back to the street - objects: - - strange_letter - - garden_statue - characters: [] - - # Main entrance - entrance_hall: - name: Entrance Hall - description: | - Grand chandeliers hang from the high ceiling, their crystals covered in cobwebs. - A wide staircase curves up to the second floor, and paintings of stern-faced - individuals watch you from ornate frames on the walls. - The floor is polished marble, though dusty from neglect. - exits: - - direction: south - targetRoomId: front_yard - description: the main entrance doors - - direction: north - targetRoomId: grand_staircase - description: the grand staircase - - direction: east - targetRoomId: dining_room - description: an archway leads to what appears to be a dining room - - direction: west - targetRoomId: library - description: a door marked 'Library' - objects: - - dusty_key - - umbrella_stand - characters: - - butler_ghost - - # Library - library: - name: Library - description: | - Bookshelves line every wall, reaching from floor to ceiling. - A reading desk sits in the center of the room, a leather-bound book - open upon it. A gentle fire crackles in the fireplace, casting - dancing shadows across the room. - exits: - - direction: east - targetRoomId: entrance_hall - description: the door back to the entrance hall - - direction: north - targetRoomId: secret_study - description: a hidden door in the bookshelf - isLocked: true - keyId: old_brass_key - objects: - - leather_book - - reading_glasses - - old_brass_key - characters: [] - - # Dining Room - dining_room: - name: Dining Room - description: | - A long table dominates this room, set for a dinner party that never happened. - Fine china and silverware rest atop an elegant tablecloth, now gray with dust. - A chandelier hangs above, and a sideboard against the wall holds various serving dishes. - exits: - - direction: west - targetRoomId: entrance_hall - description: the archway back to the entrance hall - - direction: north - targetRoomId: kitchen - description: a swinging door to what must be the kitchen - objects: - - silver_candlestick - - dusty_plate - characters: - - dining_ghost - - # Kitchen - kitchen: - name: Kitchen - description: | - This once-busy kitchen now stands silent. Copper pots and pans hang from hooks, - and an old cast-iron stove sits cold against the wall. A large preparation table - occupies the center of the room, and a pantry door stands ajar. - exits: - - direction: south - targetRoomId: dining_room - description: the swinging door back to the dining room - - direction: east - targetRoomId: pantry - description: the pantry door - objects: - - rusty_knife - - cookbook - characters: [] - - # Pantry - pantry: - name: Pantry - description: | - Shelves line the walls of this small room, holding preserves in dusty jars - and sacks of long-expired ingredients. A small window provides minimal light, - and a musty smell permeates the air. - exits: - - direction: west - targetRoomId: kitchen - description: the door back to the kitchen - objects: - - dusty_jar - - strange_bottle - characters: [] - - # Grand Staircase - grand_staircase: - name: Grand Staircase - description: | - The staircase curves gracefully upward, its wooden railings polished to a soft glow - despite the overall neglect of the mansion. Family portraits line the walls, - following your movement with their painted eyes. - exits: - - direction: south - targetRoomId: entrance_hall - description: back down to the entrance hall - - direction: north - targetRoomId: upper_landing - description: up to the second floor - objects: - - family_portrait - characters: [] - - # Upper Landing - upper_landing: - name: Upper Landing - description: | - The upper landing connects several rooms on the second floor. A faded - carpet runs down the center of the hallway, and doors line both sides. - A large window at the end of the hall shows the rainy night outside. - exits: - - direction: south - targetRoomId: grand_staircase - description: down the grand staircase - - direction: east - targetRoomId: master_bedroom - description: a door marked 'Master Bedroom' - - direction: west - targetRoomId: study - description: a door marked 'Study' - objects: [] - characters: [] - - # Master Bedroom - master_bedroom: - name: Master Bedroom - description: | - A large four-poster bed dominates this room, its once-luxurious hangings - now faded and torn. A vanity sits in the corner, its mirror clouded with age, - and a wardrobe stands against the far wall. - exits: - - direction: west - targetRoomId: upper_landing - description: the door back to the upper landing - objects: - - jewelry_box - - old_diary - characters: - - lady_ghost - - # Study - study: - name: Study - description: | - This cozy room contains a large desk covered in papers, a comfortable - armchair, and a globe that seems to rotate slowly on its own. Bookshelves - line the walls, filled with volumes on various esoteric subjects. - exits: - - direction: east - targetRoomId: upper_landing - description: the door back to the upper landing - objects: - - strange_device - - important_letter - characters: [] - - # Secret Study (hidden room) - secret_study: - name: Secret Study - description: | - Hidden behind the library bookshelf, this small room appears to be a - private study. A desk with a locked drawer sits against one wall, and - shelves hold unusual artifacts and rare books. A single candle provides - dim illumination. - exits: - - direction: south - targetRoomId: library - description: the hidden door back to the library - objects: - - ancient_tome - - crystal_key - characters: [] - - # Street (exit area) - street: - name: Street - description: | - The quiet street outside the mansion is shrouded in fog. Streetlamps cast - pools of yellow light that barely penetrate the mist. The mansion's gates - loom behind you, while the way back to town lies ahead. - exits: - - direction: north - targetRoomId: front_yard - description: the mansion gates - objects: [] - characters: [] - -# Object definitions -objects: - strange_letter: - name: Strange Letter - description: | - A weathered envelope containing an invitation to visit the mansion. - The handwriting is elegant but unfamiliar, and the letter is signed - simply with the initial "M". - traits: - - takeable - - readable - states: {} - allowedActions: - - take - - read - - examine - - garden_statue: - name: Garden Statue - description: | - A weathered stone statue of a weeping angel. Its face is covered by its hands, - and detailed wings spread out from its back. Something about it makes you uneasy. - traits: - - fixed - states: {} - allowedActions: - - examine - - dusty_key: - name: Dusty Key - description: | - An old iron key, covered in dust. It looks like it might fit an old door somewhere. - traits: - - takeable - - key - states: {} - allowedActions: - - take - - examine - - use - - umbrella_stand: - name: Umbrella Stand - description: | - A brass stand holding several antique umbrellas, all in various states of decay. - traits: - - fixed - - container - states: - open: true - containedObjects: [] - allowedActions: - - examine - - leather_book: - name: Leather Book - description: | - A thick tome bound in dark leather. The pages are filled with strange symbols - and diagrams that seem to shift slightly when you're not looking directly at them. - traits: - - takeable - - readable - states: {} - allowedActions: - - take - - read - - examine - - reading_glasses: - name: Reading Glasses - description: | - A pair of wire-rimmed spectacles. The lenses have a slight blue tint to them. - traits: - - takeable - - wearable - states: - worn: false - allowedActions: - - take - - wear - - examine - - old_brass_key: - name: Brass Key - description: | - A small brass key with intricate engravings. It seems to be quite old but well-maintained. - traits: - - takeable - - key - states: {} - allowedActions: - - take - - examine - - use - - silver_candlestick: - name: Silver Candlestick - description: | - A tarnished silver candlestick with an unlit candle. It feels heavy in your hand. - traits: - - takeable - - light_source - states: - lit: false - allowedActions: - - take - - light - - examine - - dusty_plate: - name: Dusty Plate - description: | - A fine china plate covered in a layer of dust. Despite its age, the painted pattern is still vivid. - traits: - - takeable - states: {} - allowedActions: - - take - - examine - - rusty_knife: - name: Rusty Knife - description: | - An old kitchen knife with a rusted blade. It's dull, but still might be useful. - traits: - - takeable - - sharp - states: {} - allowedActions: - - take - - examine - - use - - cookbook: - name: Cookbook - description: | - A yellowed cookbook filled with strange recipes. Some ingredients are unusual, and - the instructions sometimes reference phases of the moon or specific star alignments. - traits: - - takeable - - readable - states: {} - allowedActions: - - take - - read - - examine - - dusty_jar: - name: Dusty Jar - description: | - A glass jar containing what might once have been fruit preserves, now unidentifiable. - Best not to open it. - traits: - - takeable - - container - states: - open: false - allowedActions: - - take - - examine - - strange_bottle: - name: Strange Bottle - description: | - A small bottle containing a glowing blue liquid. The label is written in a language you don't recognize. - traits: - - takeable - - drinkable - states: {} - allowedActions: - - take - - drink - - examine - - family_portrait: - name: Family Portrait - description: | - A large painting of a stern-looking family - a husband, wife, and three children. - The father's eyes seem to follow you, and there's something oddly familiar about his face. - traits: - - fixed - states: {} - allowedActions: - - examine - - jewelry_box: - name: Jewelry Box - description: | - An ornate wooden box inlaid with mother-of-pearl. Inside are several pieces of - antique jewelry, including a ruby necklace that catches the light strangely. - traits: - - takeable - - container - states: - open: true - containedObjects: - - ruby_necklace - allowedActions: - - take - - open - - close - - examine - - ruby_necklace: - name: Ruby Necklace - description: | - A delicate gold chain with a large ruby pendant. The gem seems to glow with an inner light, - and it feels warm to the touch. - traits: - - takeable - - wearable - states: - worn: false - allowedActions: - - take - - wear - - examine - - old_diary: - name: Old Diary - description: | - A leather-bound diary with yellowed pages. The entries detail the daily life of - the mansion's former mistress, and hint at a growing fear of something in the house. - traits: - - takeable - - readable - states: {} - allowedActions: - - take - - read - - examine - - strange_device: - name: Strange Device - description: | - A brass contraption with gears, dials, and a glass dome. It's purpose isn't clear, - but it occasionally ticks and whirs on its own. - traits: - - takeable - states: - active: false - allowedActions: - - take - - use - - examine - - important_letter: - name: Important Letter - description: | - A sealed envelope addressed to "The Heir." The wax seal bears the same crest - that you've seen throughout the mansion. - traits: - - takeable - - readable - states: {} - allowedActions: - - take - - read - - examine - - ancient_tome: - name: Ancient Tome - description: | - A massive book bound in what appears to be human skin. The title, "Liber Umbrarum," - is embossed in gold on the spine. The pages contain rituals and incantations. - traits: - - takeable - - readable - states: {} - allowedActions: - - take - - read - - examine - - crystal_key: - name: Crystal Key - description: | - A key made of clear crystal that catches the light in mesmerizing ways. Despite - its appearance, it feels as solid as metal and cool to the touch. - traits: - - takeable - - key - states: {} - allowedActions: - - take - - use - - examine - -# Character definitions -characters: - butler_ghost: - name: Ghostly Butler - description: | - The translucent figure of an elderly butler, dressed in formal attire from a bygone era. - He stands with perfect posture, hands clasped behind his back. - dialogue: - greeting: "Welcome to the mansion, sir/madam. We've been expecting you." - mansion: "This estate has belonged to the Montgomery family for generations. Such a shame what happened to them." - family: "The Montgomerys? All gone now, I'm afraid. The master, his wife, and their children. A tragedy." - tragedy: "I'm not at liberty to discuss the details, but the answers you seek may be found in the study." - yourself: "Me? I've served this house for longer than I care to remember. Even death couldn't release me from my duties." - defaultResponse: "I'm afraid I cannot help you with that particular inquiry." - inventory: [] - mood: formal - - dining_ghost: - name: Dining Guest - description: | - A spectral figure in elegant dinner attire, seated at the table. She appears to be - a young woman, and she plays absently with a spectral fork. - dialogue: - greeting: "Oh, a new guest! How delightful. Will you join us for dinner? It's been so long since we had fresh company." - dinner: "We've been waiting for the main course for... goodness, how long has it been now? Years, I suppose." - herself: "My name? It's... it's strange, I can't quite recall. I remember coming here for a dinner party, but then..." - party: "It was supposed to be a celebration. The master of the house had made some sort of discovery. Something important." - discovery: "In the secret study, I believe. Behind the library. The master was very excited about it." - defaultResponse: "I'm sorry, my mind isn't what it used to be. The years blur together when you're like this." - inventory: [] - mood: wistful - - lady_ghost: - name: Ghostly Lady - description: | - The elegant apparition of a woman in Victorian dress, her face partly obscured by a veil. - She sits at the vanity, brushing her long hair with a ghostly brush. - dialogue: - greeting: "A visitor? How unusual. Are you lost, or are you here for a purpose?" - purpose: "Everyone who comes to this house has a purpose, whether they know it or not." - herself: "I was the lady of this house once. Now I am bound to it, as are we all." - family: "My husband was obsessed with his research. My children... I tried to protect them. I failed." - research: "The barriers between worlds, the nature of reality itself. He found something, in the end. Something that should have remained hidden." - hidden: "In his secret study. The key is... well, I suppose you'll have to find that yourself. Some secrets reveal themselves only to those who seek them." - defaultResponse: "There are some things I cannot speak of. The house has its rules, even for the dead." - inventory: [] - mood: melancholy - -# Action definitions -actions: - look: - patterns: - - "look around" - - "look at [object]" - - "examine [object]" - - "check [object]" - - "inspect [object]" - - "observe [object]" - - "view [object]" - handler: "look" - - go: - patterns: - - "go [direction]" - - "move [direction]" - - "walk [direction]" - - "head [direction]" - - "travel [direction]" - - "enter [direction]" - requiresObject: true - handler: "go" - - take: - patterns: - - "take [object]" - - "get [object]" - - "pick up [object]" - - "grab [object]" - - "collect [object]" - requiresObject: true - handler: "take" - - drop: - patterns: - - "drop [object]" - - "put down [object]" - - "discard [object]" - - "leave [object]" - requiresObject: true - handler: "drop" - - inventory: - patterns: - - "inventory" - - "check inventory" - - "show inventory" - - "what am I carrying" - - "what do I have" - handler: "inventory" - - use: - patterns: - - "use [object]" - - "use [object] on [target]" - - "use [object] with [target]" - - "apply [object] to [target]" - requiresObject: true - requiresTarget: false - handler: "use" - - talk: - patterns: - - "talk to [object]" - - "speak to [object]" - - "ask [object] about [topic]" - - "tell [object] about [topic]" - - "converse with [object]" - requiresObject: true - handler: "talk" - - read: - patterns: - - "read [object]" - - "read from [object]" - - "examine [object]" - - "look at [object]" - requiresObject: true - handler: "look" - - help: - patterns: - - "help" - - "commands" - - "what can I do" - - "show help" - handler: "help" - - wear: - patterns: - - "wear [object]" - - "put on [object]" - - "don [object]" - requiresObject: true - handler: "use" - -# Initial game state -initialState: - currentRoomId: front_yard - inventory: - - strange_letter - visitedRooms: [] - flags: - hasMetButler: false - hasFoundSecret: false - counters: - moveCount: 0 diff --git a/data/z-code/README.md b/data/z-code/README.md deleted file mode 100644 index 8d057bd..0000000 --- a/data/z-code/README.md +++ /dev/null @@ -1,30 +0,0 @@ -# Z-Code Story Files - -Place your Z-machine story files here. The Z-code narrator engine looks for -`zork1.bin` by default. This can be overridden with the `ZCODE_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 `ZCODE_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 deleted file mode 100644 index 5b79e24fd2ecd39fdfd680da5a9b5b668e5b21e9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 92160 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⪙DESKAXb`Mn*#)A*?J$>uxq+{21u#A<)Fp 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 diff --git a/data/zcode-prompts/character-generation.yml b/data/zcode-prompts/character-generation.yml deleted file mode 100644 index 94c0ea4..0000000 --- a/data/zcode-prompts/character-generation.yml +++ /dev/null @@ -1,44 +0,0 @@ -# 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/zcode-prompts/command-translator.yml b/data/zcode-prompts/command-translator.yml deleted file mode 100644 index 8915baa..0000000 --- a/data/zcode-prompts/command-translator.yml +++ /dev/null @@ -1,112 +0,0 @@ -# Command Translator Prompt -# Called for every player input. Converts free natural-language text into a -# Z-machine 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 Z-machine 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/zcode-prompts/output-evaluator.yml b/data/zcode-prompts/output-evaluator.yml deleted file mode 100644 index e5cb43e..0000000 --- a/data/zcode-prompts/output-evaluator.yml +++ /dev/null @@ -1,76 +0,0 @@ -# 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: - --- - {{zcodeOutput}} - --- - - Decide now: accept and rewrite, or retry with a new command? - Respond with the appropriate JSON. diff --git a/data/zcode-prompts/text-rewriter.yml b/data/zcode-prompts/text-rewriter.yml deleted file mode 100644 index a7426a0..0000000 --- a/data/zcode-prompts/text-rewriter.yml +++ /dev/null @@ -1,77 +0,0 @@ -# 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 Z-machine 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 Z-machine 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: - --- - {{zcodeOutput}} - --- - - Rewrite the above as prose for the player now. diff --git a/dist/cli/game-runner.d.ts b/dist/cli/game-runner.d.ts deleted file mode 100644 index cd529b2..0000000 --- a/dist/cli/game-runner.d.ts +++ /dev/null @@ -1,64 +0,0 @@ -/** - * Command-line interface for running the interactive fiction game - */ -export declare class GameRunner { - private engine; - private llmProvider; - private rl; - private gameContext; - private gameHistory; - private suggestedCommands; - constructor(); - /** - * Initialize the game - */ - initialize(worldPath: string): Promise; - /** - * Start the game in CLI mode - */ - start(): Promise; - /** - * The main game loop for CLI mode - */ - private gameLoop; - /** - * Process a player command and return the narrative response - * Used by both CLI and web interfaces - */ - processCommand(input: string): Promise; - /** - * End the game - */ - end(): void; - /** - * Update the game context with new narrative - */ - private updateGameContext; - /** - * Get the current game state - * Used by web interface - */ - getGameState(): { - world: import("../interfaces/world-model").WorldModel; - currentRoomId: string; - inventory: string[]; - visitedRooms: string[]; - flags: Record; - counters: Record; - }; - /** - * Get the current room description - * Used by web interface - */ - getCurrentRoomDescription(): string; - /** - * Get suggested actions for the current game state - * Used by web interface - */ - getSuggestions(): string[]; - /** - * Load a saved game state - * Used by web interface - */ - loadGameState(savedState: any): void; -} diff --git a/dist/cli/game-runner.js b/dist/cli/game-runner.js deleted file mode 100644 index 6a86286..0000000 --- a/dist/cli/game-runner.js +++ /dev/null @@ -1,262 +0,0 @@ -"use strict"; -/** - * Command-line interface for running the interactive fiction game - */ -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; - }; -})(); -Object.defineProperty(exports, "__esModule", { value: true }); -exports.GameRunner = void 0; -const readline = __importStar(require("readline")); -const path = __importStar(require("path")); -const dotenv = __importStar(require("dotenv")); -const game_engine_1 = require("../engine/game-engine"); -const openrouter_provider_1 = require("../llm/openrouter-provider"); -// Load environment variables -dotenv.config(); -class GameRunner { - constructor() { - this.rl = null; - this.gameContext = ''; - this.gameHistory = []; - this.suggestedCommands = []; - this.engine = new game_engine_1.TextAdventureEngine(); - this.llmProvider = new openrouter_provider_1.OpenRouterProvider(); - } - /** - * Initialize the game - */ - async initialize(worldPath) { - console.log('Initializing game...'); - // Initialize LLM provider - 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/or OPENROUTER_MODEL'); - } - await this.llmProvider.initialize({ - apiKey, - model, - temperature: 0.7, - maxTokens: 800 - }); - // Load the world - const resolvedPath = path.resolve(worldPath); - console.log(`Loading world from ${resolvedPath}...`); - await this.engine.loadWorld(resolvedPath); - console.log('Game initialized successfully!'); - } - /** - * Start the game in CLI mode - */ - async start() { - // Create readline interface for CLI mode - this.rl = readline.createInterface({ - input: process.stdin, - output: process.stdout - }); - try { - // Display introduction - const introText = await this.engine.start(); - console.log('\n' + introText + '\n'); - // Look at initial room - const initialLook = this.engine.processAction({ action: 'look', confidence: 1 }); - // Generate narrative description - const narrativeRequest = { - action: 'look', - result: initialLook.message, - roomDescription: this.engine.getCurrentRoomDescription(), - visibleObjects: this.engine.getVisibleObjects(), - visibleCharacters: this.engine.getVisibleCharacters(), - tone: 'descriptive' - }; - const narrative = await this.llmProvider.generateNarrative(narrativeRequest); - console.log('\n' + narrative.text + '\n'); - // Store suggestions if available - if (narrative.suggestions && narrative.suggestions.length > 0) { - this.suggestedCommands = narrative.suggestions; - } - // Update game context - this.updateGameContext(narrative.text); - // Start the game loop - this.gameLoop(); - } - catch (error) { - console.error('Error starting game:', error); - this.end(); - } - } - /** - * The main game loop for CLI mode - */ - gameLoop() { - if (!this.rl) - return; - this.rl.question('> ', async (input) => { - if (input.toLowerCase() === 'quit' || input.toLowerCase() === 'exit') { - this.end(); - return; - } - const response = await this.processCommand(input); - console.log('\n' + response + '\n'); - // Continue the game loop - this.gameLoop(); - }); - } - /** - * Process a player command and return the narrative response - * Used by both CLI and web interfaces - */ - async processCommand(input) { - try { - // Process player input - const actionRequest = { - playerInput: input, - currentRoom: this.engine.getWorldModel().rooms[this.engine.getCurrentState().currentRoomId].name, - visibleObjects: this.engine.getVisibleObjects().map(id => this.engine.getWorldModel().objects[id].name), - visibleCharacters: this.engine.getVisibleCharacters().map(id => this.engine.getWorldModel().characters[id].name), - possibleActions: this.engine.getAvailableActions(), - inventory: this.engine.getCurrentState().inventory.map(id => this.engine.getWorldModel().objects[id].name), - gameContext: this.gameContext - }; - if (this.rl) { - console.log('Thinking...'); - } - // Translate player input to action - const action = await this.llmProvider.translateAction(actionRequest); - // Process the action in the game engine - const actionResult = this.engine.processAction(action); - // If state changed, update it - if (actionResult.stateChanged && actionResult.newState) { - this.engine.getCurrentState().currentRoomId = actionResult.newState.currentRoomId; - this.engine.getCurrentState().inventory = actionResult.newState.inventory; - this.engine.getCurrentState().visitedRooms = actionResult.newState.visitedRooms; - this.engine.getCurrentState().flags = actionResult.newState.flags; - this.engine.getCurrentState().counters = actionResult.newState.counters; - } - // Generate narrative description - const narrativeRequest = { - action: `${action.action}${action.object ? ' ' + action.object : ''}${action.target ? ' on ' + action.target : ''}`, - result: actionResult.message, - roomDescription: this.engine.getCurrentRoomDescription(), - visibleObjects: this.engine.getVisibleObjects().map(id => this.engine.getWorldModel().objects[id].name), - visibleCharacters: this.engine.getVisibleCharacters().map(id => this.engine.getWorldModel().characters[id].name), - previousContext: this.gameHistory.slice(-3).join('\n'), - tone: 'descriptive' - }; - const narrative = await this.llmProvider.generateNarrative(narrativeRequest); - // Store suggestions if available - if (narrative.suggestions && narrative.suggestions.length > 0) { - this.suggestedCommands = narrative.suggestions; - } - // Update game context with the new narrative - this.updateGameContext(narrative.text); - // Return the narrative text - return narrative.text; - } - catch (error) { - console.error('Error processing input:', error); - return 'Something went wrong. Please try again.'; - } - } - /** - * End the game - */ - end() { - console.log('\nThanks for playing!'); - if (this.rl) { - this.rl.close(); - this.rl = null; - } - this.engine.end(); - if (process.env.NODE_ENV !== 'production') { - process.exit(0); - } - } - /** - * Update the game context with new narrative - */ - updateGameContext(narrative) { - // Add to history - this.gameHistory.push(narrative); - // Keep history limited to last 10 entries - if (this.gameHistory.length > 10) { - this.gameHistory.shift(); - } - // Update current context (last 5 entries) - this.gameContext = this.gameHistory.slice(-5).join('\n'); - } - /** - * Get the current game state - * Used by web interface - */ - getGameState() { - return { - world: this.engine.getWorldModel(), - currentRoomId: this.engine.getCurrentState().currentRoomId, - inventory: this.engine.getCurrentState().inventory, - visitedRooms: this.engine.getCurrentState().visitedRooms, - flags: this.engine.getCurrentState().flags, - counters: this.engine.getCurrentState().counters - }; - } - /** - * Get the current room description - * Used by web interface - */ - getCurrentRoomDescription() { - const roomId = this.engine.getCurrentState().currentRoomId; - return this.engine.getWorldModel().rooms[roomId].description; - } - /** - * Get suggested actions for the current game state - * Used by web interface - */ - getSuggestions() { - return this.suggestedCommands; - } - /** - * Load a saved game state - * Used by web interface - */ - loadGameState(savedState) { - // Set the current state to match the saved state - this.engine.getCurrentState().currentRoomId = savedState.currentRoomId; - this.engine.getCurrentState().inventory = savedState.inventory; - this.engine.getCurrentState().visitedRooms = savedState.visitedRooms; - this.engine.getCurrentState().flags = savedState.flags; - this.engine.getCurrentState().counters = savedState.counters; - } -} -exports.GameRunner = GameRunner; -//# sourceMappingURL=game-runner.js.map \ No newline at end of file diff --git a/dist/cli/game-runner.js.map b/dist/cli/game-runner.js.map deleted file mode 100644 index a212908..0000000 --- a/dist/cli/game-runner.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"game-runner.js","sourceRoot":"","sources":["../../src/cli/game-runner.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/dist/config/game-config.d.ts b/dist/config/game-config.d.ts deleted file mode 100644 index 78396d7..0000000 --- a/dist/config/game-config.d.ts +++ /dev/null @@ -1,39 +0,0 @@ -export type EngineName = 'yaml' | 'ink' | 'zcode' | string; -export interface GameMetadata { - title: string; - author?: string; - subtitle?: string; - version?: string; - copyright?: string; - language?: string; -} -export interface GamePaths { - mainGameFile: string; - inkSource?: string; - inkCompiled?: string; - promptDir?: string; - music?: string; - sfx?: string; - images?: string; - [key: string]: string | undefined; -} -export interface GameEngineConfig { - engine: EngineName; - locale: 'en_US' | 'de_DE' | string; - paths: GamePaths; - metadata: GameMetadata; -} -export declare function projectPath(relativeOrAbsolutePath: string): string; -export declare function loadGameConfig(configPath: string, engine: EngineName): GameEngineConfig; -export declare function ensureConfiguredAssetDirectories(config: GameEngineConfig): void; -export declare function clientGameConfig(config: GameEngineConfig): { - engine: string; - locale: string; - metadata: GameMetadata; - assets: { - music: string; - sfx: string; - sounds: string; - images: string; - }; -}; diff --git a/dist/config/game-config.js b/dist/config/game-config.js deleted file mode 100644 index 6cabcfd..0000000 --- a/dist/config/game-config.js +++ /dev/null @@ -1,96 +0,0 @@ -"use strict"; -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { "default": mod }; -}; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.projectPath = projectPath; -exports.loadGameConfig = loadGameConfig; -exports.ensureConfiguredAssetDirectories = ensureConfiguredAssetDirectories; -exports.clientGameConfig = clientGameConfig; -const path_1 = __importDefault(require("path")); -const fs_1 = require("fs"); -const PROJECT_ROOT = path_1.default.resolve(__dirname, '../..'); -function fallbackConfig(engine) { - return { - engine, - locale: 'en_US', - paths: { - mainGameFile: engine === 'ink' - ? 'data/ink/story.ink.json' - : engine === 'zcode' - ? 'data/z-code/zork1.bin' - : 'data/worlds/example_world.yml', - music: 'public/music', - sfx: 'public/sounds', - images: 'public/images', - }, - metadata: { - title: 'AI Interactive Fiction', - author: 'Generative AI', - subtitle: 'An open-world text adventure', - version: '1.0.0', - copyright: '', - language: 'en_US', - }, - }; -} -function projectPath(relativeOrAbsolutePath) { - return path_1.default.isAbsolute(relativeOrAbsolutePath) - ? relativeOrAbsolutePath - : path_1.default.resolve(PROJECT_ROOT, relativeOrAbsolutePath); -} -function loadGameConfig(configPath, engine) { - const absolutePath = projectPath(configPath); - if (!(0, fs_1.existsSync)(absolutePath)) { - console.warn(`[config] Missing ${absolutePath}; using ${engine} defaults.`); - return fallbackConfig(engine); - } - const parsed = JSON.parse((0, fs_1.readFileSync)(absolutePath, 'utf8')); - const fallback = fallbackConfig(engine); - return { - engine: parsed.engine ?? fallback.engine, - locale: parsed.locale ?? fallback.locale, - paths: { - ...fallback.paths, - ...(parsed.paths ?? {}), - }, - metadata: { - ...fallback.metadata, - ...(parsed.metadata ?? {}), - language: parsed.metadata?.language ?? parsed.locale ?? fallback.metadata.language, - }, - }; -} -function ensureConfiguredAssetDirectories(config) { - const directories = [ - config.paths.music, - config.paths.sfx, - config.paths.images, - config.paths.inkSource ? path_1.default.dirname(config.paths.inkSource) : undefined, - config.paths.inkCompiled ? path_1.default.dirname(config.paths.inkCompiled) : undefined, - config.paths.mainGameFile ? path_1.default.dirname(config.paths.mainGameFile) : undefined, - config.paths.promptDir, - ]; - for (const directory of directories) { - if (!directory) - continue; - const absolutePath = projectPath(directory); - if (!(0, fs_1.existsSync)(absolutePath)) { - (0, fs_1.mkdirSync)(absolutePath, { recursive: true }); - } - } -} -function clientGameConfig(config) { - return { - engine: config.engine, - locale: config.locale, - metadata: config.metadata, - assets: { - music: '/music/', - sfx: '/sounds/', - sounds: '/sounds/', - images: '/images/', - }, - }; -} -//# sourceMappingURL=game-config.js.map \ No newline at end of file diff --git a/dist/config/game-config.js.map b/dist/config/game-config.js.map deleted file mode 100644 index 41632f0..0000000 --- a/dist/config/game-config.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"game-config.js","sourceRoot":"","sources":["../../src/config/game-config.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/dist/engine/game-engine.d.ts b/dist/engine/game-engine.d.ts deleted file mode 100644 index 44a14ee..0000000 --- a/dist/engine/game-engine.d.ts +++ /dev/null @@ -1,77 +0,0 @@ -/** - * Core Game Engine - * Manages game state and processes actions - */ -import { GameEngine, ActionResult } from '../interfaces/engine'; -import { WorldModel, GameState } from '../interfaces/world-model'; -import { ActionResponse } from '../interfaces/llm'; -export declare class TextAdventureEngine implements GameEngine { - private worldModel; - private gameState; - private actionHandlers; - constructor(); - /** - * Load a world model from a file - */ - loadWorld(worldModelPath: string): Promise; - /** - * Get the current game state - */ - getCurrentState(): GameState; - /** - * Get the world model - */ - getWorldModel(): WorldModel; - /** - * Process an action from the player - */ - processAction(action: ActionResponse): ActionResult; - /** - * Save the current game state to a file - */ - saveGame(filename: string): Promise; - /** - * Load a game state from a save file - */ - loadGame(filename: string): Promise; - /** - * Get a list of available actions in the current context - */ - getAvailableActions(): string[]; - /** - * Get a list of visible objects in the current room - */ - getVisibleObjects(): string[]; - /** - * Get a list of visible characters in the current room - */ - getVisibleCharacters(): string[]; - /** - * Get the description of the current room - */ - getCurrentRoomDescription(): string; - /** - * Start the game and return the introduction text - */ - start(): Promise; - /** - * End the game (cleanup resources if needed) - */ - end(): void; - /** - * Get the current room object - */ - private getCurrentRoom; - /** - * Register default action handlers - */ - private registerDefaultActionHandlers; - /** - * Find an object by name in a list of object IDs - */ - private findObjectByName; - /** - * Find a character by name in a list of character IDs - */ - private findCharacterByName; -} diff --git a/dist/engine/game-engine.js b/dist/engine/game-engine.js deleted file mode 100644 index 6b15bae..0000000 --- a/dist/engine/game-engine.js +++ /dev/null @@ -1,607 +0,0 @@ -"use strict"; -/** - * Core Game Engine - * Manages game state and processes actions - */ -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; - }; -})(); -Object.defineProperty(exports, "__esModule", { value: true }); -exports.TextAdventureEngine = void 0; -const fs = __importStar(require("fs/promises")); -const yaml_parser_1 = require("../world-model/yaml-parser"); -class TextAdventureEngine { - constructor() { - this.worldModel = null; - this.gameState = null; - this.actionHandlers = {}; - this.registerDefaultActionHandlers(); - } - /** - * Load a world model from a file - */ - async loadWorld(worldModelPath) { - try { - this.worldModel = await yaml_parser_1.YamlWorldParser.loadFromFile(worldModelPath); - this.gameState = { ...this.worldModel.initialState }; - // Mark the initial room as visited - if (!this.gameState.visitedRooms.includes(this.gameState.currentRoomId)) { - this.gameState.visitedRooms.push(this.gameState.currentRoomId); - } - } - catch (error) { - console.error(`Failed to load world from ${worldModelPath}:`, error); - throw new Error(`Could not load world: ${error instanceof Error ? error.message : String(error)}`); - } - } - /** - * Get the current game state - */ - getCurrentState() { - if (!this.gameState) { - throw new Error('Game state not initialized. Please load a world first.'); - } - return { ...this.gameState }; - } - /** - * Get the world model - */ - getWorldModel() { - if (!this.worldModel) { - throw new Error('World model not initialized. Please load a world first.'); - } - return this.worldModel; - } - /** - * Process an action from the player - */ - processAction(action) { - if (!this.worldModel || !this.gameState) { - return { - success: false, - message: 'Game not initialized', - stateChanged: false - }; - } - const handler = this.actionHandlers[action.action.toLowerCase()]; - if (!handler) { - return { - success: false, - message: `I don't know how to "${action.action}"`, - stateChanged: false - }; - } - return handler(this.gameState, this.worldModel, action); - } - /** - * Save the current game state to a file - */ - async saveGame(filename) { - if (!this.gameState || !this.worldModel) { - throw new Error('Cannot save: game not initialized'); - } - const saveData = { - worldModelName: this.worldModel.title, - worldModelVersion: this.worldModel.version, - timestamp: new Date().toISOString(), - gameState: this.gameState - }; - try { - await fs.writeFile(filename, JSON.stringify(saveData, null, 2), 'utf8'); - } - catch (error) { - console.error(`Failed to save game to ${filename}:`, error); - throw new Error(`Could not save game: ${error instanceof Error ? error.message : String(error)}`); - } - } - /** - * Load a game state from a save file - */ - async loadGame(filename) { - try { - const fileContents = await fs.readFile(filename, 'utf8'); - const saveData = JSON.parse(fileContents); - // Check if the save file matches the current world model - if (!this.worldModel) { - throw new Error('World model not loaded'); - } - if (saveData.worldModelName !== this.worldModel.title || - saveData.worldModelVersion !== this.worldModel.version) { - throw new Error('Save file is for a different world or version'); - } - // Load the game state - this.gameState = saveData.gameState; - } - catch (error) { - console.error(`Failed to load game from ${filename}:`, error); - throw new Error(`Could not load save file: ${error instanceof Error ? error.message : String(error)}`); - } - } - /** - * Get a list of available actions in the current context - */ - getAvailableActions() { - if (!this.worldModel) - return []; - // Common actions always available - const availableActions = ['look', 'inventory', 'help']; - // Add movement actions based on current room exits - const currentRoom = this.getCurrentRoom(); - if (currentRoom) { - currentRoom.exits.forEach(exit => { - availableActions.push(`go ${exit.direction.toLowerCase()}`); - }); - } - // Add object interactions based on visible objects - const visibleObjects = this.getVisibleObjects(); - const objects = this.worldModel.objects; - visibleObjects.forEach(objId => { - const obj = objects[objId]; - if (obj) { - obj.allowedActions.forEach(action => { - availableActions.push(`${action} ${obj.name.toLowerCase()}`); - }); - } - }); - // Add character interactions - const visibleCharacters = this.getVisibleCharacters(); - visibleCharacters.forEach(charId => { - availableActions.push(`talk to ${this.worldModel.characters[charId].name.toLowerCase()}`); - }); - // Add inventory object actions - this.gameState.inventory.forEach(objId => { - const obj = objects[objId]; - if (obj) { - obj.allowedActions.forEach(action => { - availableActions.push(`${action} ${obj.name.toLowerCase()}`); - }); - } - }); - return Array.from(new Set(availableActions)); // Remove duplicates - } - /** - * Get a list of visible objects in the current room - */ - getVisibleObjects() { - if (!this.worldModel || !this.gameState) - return []; - const currentRoom = this.getCurrentRoom(); - if (!currentRoom) - return []; - const visibleObjects = [...currentRoom.objects]; - // Add objects from open containers - currentRoom.objects.forEach(objId => { - const obj = this.worldModel.objects[objId]; - if (obj && obj.traits.includes('container') && obj.states?.open && obj.containedObjects) { - visibleObjects.push(...obj.containedObjects); - } - }); - return visibleObjects; - } - /** - * Get a list of visible characters in the current room - */ - getVisibleCharacters() { - if (!this.worldModel || !this.gameState) - return []; - const currentRoom = this.getCurrentRoom(); - return currentRoom ? currentRoom.characters : []; - } - /** - * Get the description of the current room - */ - getCurrentRoomDescription() { - const currentRoom = this.getCurrentRoom(); - if (!currentRoom) - return 'You are in a void. Something has gone wrong.'; - return currentRoom.description; - } - /** - * Start the game and return the introduction text - */ - async start() { - if (!this.worldModel) { - throw new Error('World not loaded. Please load a world before starting.'); - } - // Reset game state to initial state - this.gameState = { ...this.worldModel.initialState }; - return this.worldModel.introduction; - } - /** - * End the game (cleanup resources if needed) - */ - end() { - // Cleanup could happen here if needed - console.log('Game ended'); - } - /** - * Get the current room object - */ - getCurrentRoom() { - if (!this.worldModel || !this.gameState) - return null; - const roomId = this.gameState.currentRoomId; - return this.worldModel.rooms[roomId] || null; - } - /** - * Register default action handlers - */ - registerDefaultActionHandlers() { - // Look action - this.actionHandlers['look'] = (state, world, action) => { - const room = world.rooms[state.currentRoomId]; - // If an object is specified, look at that object - if (action.object) { - // Try to find the object in the room or inventory - const visibleObjects = this.getVisibleObjects(); - const objId = this.findObjectByName(action.object, [...visibleObjects, ...state.inventory]); - if (!objId) { - return { - success: false, - message: `You don't see any ${action.object} here.`, - stateChanged: false - }; - } - const obj = world.objects[objId]; - return { - success: true, - message: obj.description, - stateChanged: false - }; - } - // Look at the room - const objectDescriptions = room.objects - .map(id => world.objects[id]) - .map(obj => `You can see ${obj.name.toLowerCase()} here.`); - const characterDescriptions = room.characters - .map(id => world.characters[id]) - .map(char => `${char.name} is here.`); - const exitDescriptions = room.exits - .map(exit => `There is an exit ${exit.direction.toLowerCase()}${exit.description ? ` (${exit.description})` : ''}.`); - const fullDescription = [ - room.description, - ...objectDescriptions, - ...characterDescriptions, - ...exitDescriptions - ].join('\n'); - return { - success: true, - message: fullDescription, - stateChanged: false - }; - }; - // Go action - this.actionHandlers['go'] = (state, world, action) => { - const room = world.rooms[state.currentRoomId]; - if (!action.object) { - return { - success: false, - message: 'Go where?', - stateChanged: false - }; - } - // Find the exit that matches the direction - const direction = action.object.toLowerCase(); - const exit = room.exits.find(e => e.direction.toLowerCase() === direction); - if (!exit) { - return { - success: false, - message: `You can't go ${direction} from here.`, - stateChanged: false - }; - } - if (exit.isLocked) { - if (!exit.keyId) { - return { - success: false, - message: `The way ${direction} is locked.`, - stateChanged: false - }; - } - if (!state.inventory.includes(exit.keyId)) { - return { - success: false, - message: `The way ${direction} is locked and you don't have the key.`, - stateChanged: false - }; - } - // Player has the key, unlock the exit - exit.isLocked = false; - return { - success: true, - message: `You unlock the way ${direction} and proceed.`, - stateChanged: true, - newState: { - ...state, - currentRoomId: exit.targetRoomId, - visitedRooms: state.visitedRooms.includes(exit.targetRoomId) - ? state.visitedRooms - : [...state.visitedRooms, exit.targetRoomId] - } - }; - } - // Exit is not locked, just move - return { - success: true, - message: `You go ${direction}.`, - stateChanged: true, - newState: { - ...state, - currentRoomId: exit.targetRoomId, - visitedRooms: state.visitedRooms.includes(exit.targetRoomId) - ? state.visitedRooms - : [...state.visitedRooms, exit.targetRoomId] - } - }; - }; - // Take action - this.actionHandlers['take'] = (state, world, action) => { - if (!action.object) { - return { - success: false, - message: 'Take what?', - stateChanged: false - }; - } - // Find the object in the current room - const visibleObjects = this.getVisibleObjects(); - const objId = this.findObjectByName(action.object, visibleObjects); - if (!objId) { - return { - success: false, - message: `You don't see any ${action.object} here.`, - stateChanged: false - }; - } - const obj = world.objects[objId]; - // Check if the object can be taken - if (!obj.traits.includes('takeable')) { - return { - success: false, - message: `You can't take the ${obj.name.toLowerCase()}.`, - stateChanged: false - }; - } - // Remove object from room and add to inventory - const room = world.rooms[state.currentRoomId]; - const newRoomObjects = room.objects.filter(id => id !== objId); - room.objects = newRoomObjects; - // Update state - return { - success: true, - message: `You take the ${obj.name.toLowerCase()}.`, - stateChanged: true, - newState: { - ...state, - inventory: [...state.inventory, objId] - } - }; - }; - // Inventory action - this.actionHandlers['inventory'] = (state, world) => { - if (state.inventory.length === 0) { - return { - success: true, - message: 'Your inventory is empty.', - stateChanged: false - }; - } - const items = state.inventory - .map(id => world.objects[id]) - .map(obj => obj.name) - .join(', '); - return { - success: true, - message: `You are carrying: ${items}.`, - stateChanged: false - }; - }; - // Drop action - this.actionHandlers['drop'] = (state, world, action) => { - if (!action.object) { - return { - success: false, - message: 'Drop what?', - stateChanged: false - }; - } - // Find the object in the inventory - const objId = this.findObjectByName(action.object, state.inventory); - if (!objId) { - return { - success: false, - message: `You don't have any ${action.object}.`, - stateChanged: false - }; - } - const obj = world.objects[objId]; - // Remove object from inventory and add to room - const room = world.rooms[state.currentRoomId]; - room.objects.push(objId); - // Update state - return { - success: true, - message: `You drop the ${obj.name.toLowerCase()}.`, - stateChanged: true, - newState: { - ...state, - inventory: state.inventory.filter(id => id !== objId) - } - }; - }; - // Use action - this.actionHandlers['use'] = (state, world, action) => { - if (!action.object) { - return { - success: false, - message: 'Use what?', - stateChanged: false - }; - } - // Find the object in inventory or visible objects - const visibleObjects = this.getVisibleObjects(); - const objId = this.findObjectByName(action.object, [...state.inventory, ...visibleObjects]); - if (!objId) { - return { - success: false, - message: `You don't see any ${action.object} here.`, - stateChanged: false - }; - } - const obj = world.objects[objId]; - // Check if the object can be used - if (!obj.allowedActions.includes('use')) { - return { - success: false, - message: `You can't use the ${obj.name.toLowerCase()}.`, - stateChanged: false - }; - } - // Check if there's a target - if (action.target) { - const targetId = this.findObjectByName(action.target, [...state.inventory, ...visibleObjects]); - if (!targetId) { - return { - success: false, - message: `You don't see any ${action.target} here.`, - stateChanged: false - }; - } - const target = world.objects[targetId]; - // TODO: Implement object-specific use logic (could be extended with a more sophisticated system) - return { - success: true, - message: `You use the ${obj.name.toLowerCase()} on the ${target.name.toLowerCase()}.`, - stateChanged: false - }; - } - // Simple use without target - return { - success: true, - message: `You use the ${obj.name.toLowerCase()}.`, - stateChanged: false - }; - }; - // Talk action - this.actionHandlers['talk'] = (state, world, action) => { - if (!action.object) { - return { - success: false, - message: 'Talk to whom?', - stateChanged: false - }; - } - // Find the character in the room - const visibleCharacters = this.getVisibleCharacters(); - const charId = this.findCharacterByName(action.object, visibleCharacters); - if (!charId) { - return { - success: false, - message: `You don't see anyone called ${action.object} here.`, - stateChanged: false - }; - } - const character = world.characters[charId]; - // If a topic is provided - if (action.parameters?.topic) { - const topic = action.parameters.topic.toLowerCase(); - const response = character.dialogue[topic] || character.defaultResponse; - return { - success: true, - message: `${character.name}: "${response}"`, - stateChanged: false - }; - } - // No specific topic - return { - success: true, - message: `${character.name} looks ready to talk. You could ask about: ${Object.keys(character.dialogue).join(', ')}.`, - stateChanged: false - }; - }; - // Help action - this.actionHandlers['help'] = () => { - return { - success: true, - message: [ - 'Available commands:', - '- look: Examine your surroundings or a specific object', - '- go [direction]: Move in a direction', - '- take [object]: Pick up an object', - '- drop [object]: Put down an object', - '- inventory: Check what you\'re carrying', - '- use [object] (on [target]): Use an object, optionally on another object', - '- talk to [character] (about [topic]): Speak with a character', - '- help: Show this help text', - '', - 'You can type commands in natural language. The AI will interpret your intent.' - ].join('\n'), - stateChanged: false - }; - }; - // Examine action (alias for look) - this.actionHandlers['examine'] = this.actionHandlers['look']; - } - /** - * Find an object by name in a list of object IDs - */ - findObjectByName(name, objectIds) { - if (!this.worldModel) - return null; - const normalizedName = name.toLowerCase(); - for (const id of objectIds) { - const obj = this.worldModel.objects[id]; - if (obj && obj.name.toLowerCase() === normalizedName) { - return id; - } - } - return null; - } - /** - * Find a character by name in a list of character IDs - */ - findCharacterByName(name, characterIds) { - if (!this.worldModel) - return null; - const normalizedName = name.toLowerCase(); - for (const id of characterIds) { - const character = this.worldModel.characters[id]; - if (character && character.name.toLowerCase() === normalizedName) { - return id; - } - } - return null; - } -} -exports.TextAdventureEngine = TextAdventureEngine; -//# sourceMappingURL=game-engine.js.map \ No newline at end of file diff --git a/dist/engine/game-engine.js.map b/dist/engine/game-engine.js.map deleted file mode 100644 index 82ea354..0000000 --- a/dist/engine/game-engine.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"game-engine.js","sourceRoot":"","sources":["../../src/engine/game-engine.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/dist/engine/ink-engine.d.ts b/dist/engine/ink-engine.d.ts deleted file mode 100644 index 1e6e176..0000000 --- a/dist/engine/ink-engine.d.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { TurnResult } from '../interfaces/turn-result'; -export interface InkCompileResult { - sourcePath: string; - outputPath: string; - warningCount: number; -} -export declare function compileInkSource(sourcePath: string, outputPath: string): InkCompileResult; -export declare class InkEngine { - private readonly storyPath; - private story; - private nextTurnId; - private storyJson; - private readonly choicePreviewTagKeys; - constructor(storyPath: string); - isRunning(): boolean; - newGame(): TurnResult; - chooseChoice(choiceIndex: number): TurnResult; - saveGame(): string; - resumeGame(savedState: string): void; - loadGame(savedState: string): TurnResult; - private restoreState; - private loadStory; - private continueStory; - private getChoiceTags; - private extractChoicePreviewTags; - private resolveInkPath; - private findNamedInkChild; - private getInkContainerMap; - private isNamedContainerMap; -} diff --git a/dist/engine/ink-engine.js b/dist/engine/ink-engine.js deleted file mode 100644 index 07f36e3..0000000 --- a/dist/engine/ink-engine.js +++ /dev/null @@ -1,292 +0,0 @@ -"use strict"; -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { "default": mod }; -}; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.InkEngine = void 0; -exports.compileInkSource = compileInkSource; -const fs_1 = require("fs"); -const path_1 = __importDefault(require("path")); -const inkjs_1 = require("inkjs"); -const tag_parser_1 = require("../utils/tag-parser"); -const { Compiler } = require('inkjs/full'); -function compileInkSource(sourcePath, outputPath) { - const resolvedSource = path_1.default.resolve(sourcePath); - const resolvedOutput = path_1.default.resolve(outputPath); - if (!(0, fs_1.existsSync)(resolvedSource)) { - throw new Error(`Ink source file not found: ${resolvedSource}`); - } - const warnings = []; - const errors = []; - const source = (0, fs_1.readFileSync)(resolvedSource, 'utf8').replace(/^\uFEFF/, ''); - const sourceDir = path_1.default.dirname(resolvedSource); - const fileHandler = { - ResolveInkFilename: (filename) => path_1.default.isAbsolute(filename) ? filename : path_1.default.resolve(sourceDir, filename), - LoadInkFileContents: (filename) => (0, fs_1.readFileSync)(path_1.default.isAbsolute(filename) ? filename : path_1.default.resolve(sourceDir, filename), 'utf8') - .replace(/^\uFEFF/, ''), - }; - const compiler = new Compiler(source, { - sourceFilename: resolvedSource, - fileHandler, - errorHandler: (message, type) => { - if (type === 1) { - warnings.push(message); - } - else { - errors.push(message); - } - }, - }); - const story = compiler.Compile(); - if (!story || errors.length > 0) { - throw new Error(`Ink compilation failed:\n${errors.join('\n')}`); - } - if (warnings.length > 0) { - warnings.forEach((warning) => console.warn(`[ink] ${warning}`)); - } - (0, fs_1.mkdirSync)(path_1.default.dirname(resolvedOutput), { recursive: true }); - (0, fs_1.writeFileSync)(resolvedOutput, story.ToJson(), 'utf8'); - return { - sourcePath: resolvedSource, - outputPath: resolvedOutput, - warningCount: warnings.length, - }; -} -class InkEngine { - constructor(storyPath) { - this.storyPath = storyPath; - this.story = null; - this.nextTurnId = 1; - this.storyJson = null; - this.choicePreviewTagKeys = new Set(['action', 'key', 'letter', 'optional', 'gated', 'sort']); - } - isRunning() { - if (!this.story) - return false; - return this.story.canContinue || this.story.currentChoices.length > 0; - } - newGame() { - this.story = this.loadStory(); - this.nextTurnId = 1; - return this.continueStory(); - } - chooseChoice(choiceIndex) { - if (!this.story) { - throw new Error('No active Ink story'); - } - const choice = this.story.currentChoices.find((item) => item.index === choiceIndex); - if (!choice) { - throw new Error(`Ink choice ${choiceIndex} is not available`); - } - this.story.ChooseChoiceIndex(choice.index); - return this.continueStory(); - } - saveGame() { - if (!this.story) { - throw new Error('No active Ink story to save'); - } - return JSON.stringify({ - inkState: this.story.state.toJson(), - nextTurnId: this.nextTurnId, - }); - } - resumeGame(savedState) { - this.restoreState(savedState); - } - loadGame(savedState) { - this.restoreState(savedState); - return this.continueStory(); - } - restoreState(savedState) { - this.story = this.loadStory(); - let inkState = savedState; - try { - const parsed = JSON.parse(savedState); - if (parsed && typeof parsed.inkState === 'string') { - inkState = parsed.inkState; - if (Number.isInteger(parsed.nextTurnId)) { - this.nextTurnId = Math.max(1, parsed.nextTurnId); - } - } - } - catch { - // Backward compatibility with raw Ink state JSON. - } - this.story.state.LoadJson(inkState); - } - loadStory() { - const resolvedPath = path_1.default.resolve(this.storyPath); - if (!(0, fs_1.existsSync)(resolvedPath)) { - throw new Error(`Ink story file not found: ${resolvedPath}`); - } - this.storyJson = JSON.parse((0, fs_1.readFileSync)(resolvedPath, 'utf8')); - return new inkjs_1.Story(this.storyJson); - } - continueStory() { - if (!this.story) { - throw new Error('No active Ink story'); - } - const paragraphs = []; - const globalTags = []; - const turnTags = []; - while (this.story.canContinue) { - const rawText = this.story.Continue(); - const text = String(rawText || '').trim(); - const tags = (0, tag_parser_1.parseTags)(this.story.currentTags || []); - turnTags.push(...tags); - tags - .filter((tag) => tag.key === 'title' || tag.key === 'author') - .forEach((tag) => globalTags.push(tag)); - if (text) { - paragraphs.push({ text, tags }); - } - else { - tags.forEach((tag) => globalTags.push(tag)); - } - } - const choices = this.story.currentChoices.map((choice) => { - const tags = this.getChoiceTags(choice); - const category = (0, tag_parser_1.getTagValue)(tags, 'action'); - const letter = (0, tag_parser_1.getTagValue)(tags, 'letter') || (0, tag_parser_1.getTagValue)(tags, 'key'); - return { - index: choice.index, - text: String(choice.text || '').trim(), - tags, - category, - letter, - }; - }); - const inputMode = choices.length > 0 ? 'choice' : 'end'; - const gameState = {}; - if (inputMode === 'end') { - const errorTag = turnTags.find((tag) => tag.key === 'error'); - const scoreTag = turnTags.find((tag) => tag.key === 'score'); - if (!errorTag && !scoreTag) { - const message = 'Ink story ended without an explicit #score ending tag.'; - const generatedErrorTag = { key: 'error', value: message }; - globalTags.push(generatedErrorTag); - turnTags.push(generatedErrorTag); - } - const finalErrorTag = turnTags.find((tag) => tag.key === 'error'); - const finalScoreTag = turnTags.find((tag) => tag.key === 'score'); - if (finalErrorTag) { - gameState.endState = { - type: 'error', - message: finalErrorTag.value || finalErrorTag.param, - }; - } - else if (finalScoreTag) { - const numericScore = Number(finalScoreTag?.value); - if (Number.isFinite(numericScore)) { - gameState.score = numericScore; - } - gameState.endState = { - type: 'intended', - message: finalScoreTag.value || finalScoreTag.param, - }; - } - } - return { - turnId: this.nextTurnId++, - paragraphs, - choices, - inputMode, - globalTags: globalTags.length > 0 ? globalTags : undefined, - gameState: Object.keys(gameState).length > 0 ? gameState : undefined, - }; - } - getChoiceTags(choice) { - const directTags = (0, tag_parser_1.parseTags)(choice?.tags || []); - const previewTags = this.extractChoicePreviewTags(choice); - const merged = new Map(); - [...previewTags, ...directTags].forEach((tag) => { - merged.set(`${tag.key}:${tag.value || ''}:${tag.param || ''}`, tag); - }); - return Array.from(merged.values()); - } - extractChoicePreviewTags(choice) { - const pathString = String(choice?.pathStringOnChoice || choice?.targetPath?.toString?.() || '').trim(); - if (!pathString || !this.storyJson) - return []; - const container = this.resolveInkPath(pathString); - if (!Array.isArray(container)) - return []; - const tags = []; - for (let index = 0; index < container.length; index += 1) { - const token = container[index]; - if (typeof token === 'string' && token.replace(/^\^/, '').trim() === '') - continue; - if (token === '\n') - continue; - if (token !== '#') - break; - const rawParts = []; - index += 1; - while (index < container.length && container[index] !== '/#') { - const part = container[index]; - if (typeof part === 'string') { - rawParts.push(part.replace(/^\^/, '')); - } - index += 1; - } - const tag = (0, tag_parser_1.parseTags)([rawParts.join('').trim()])[0]; - if (tag && this.choicePreviewTagKeys.has(tag.key)) { - tags.push(tag); - } - } - return tags; - } - resolveInkPath(pathString) { - const parts = pathString.split('.').filter(Boolean); - let node = this.storyJson?.root; - for (const part of parts) { - if (!node) - return null; - if (Array.isArray(node) && /^\d+$/.test(part)) { - node = node[Number(part)]; - } - else if (Array.isArray(node)) { - node = this.findNamedInkChild(node, part); - } - else if (this.isNamedContainerMap(node) && part in node) { - node = node[part]; - } - else { - return null; - } - } - return node; - } - findNamedInkChild(container, part) { - for (let index = container.length - 1; index >= 0; index -= 1) { - const item = container[index]; - if (this.isNamedContainerMap(item) && part in item) { - return item[part]; - } - if (!Array.isArray(item)) - continue; - const namedMap = this.getInkContainerMap(item); - if (namedMap?.['#n'] === part) { - return item; - } - if (namedMap && part in namedMap) { - return namedMap[part]; - } - } - return null; - } - getInkContainerMap(container) { - for (let index = container.length - 1; index >= 0; index -= 1) { - const item = container[index]; - if (this.isNamedContainerMap(item)) { - return item; - } - } - return null; - } - isNamedContainerMap(value) { - return Boolean(value) && typeof value === 'object' && !Array.isArray(value); - } -} -exports.InkEngine = InkEngine; -//# sourceMappingURL=ink-engine.js.map \ No newline at end of file diff --git a/dist/engine/ink-engine.js.map b/dist/engine/ink-engine.js.map deleted file mode 100644 index 9d12749..0000000 --- a/dist/engine/ink-engine.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"ink-engine.js","sourceRoot":"","sources":["../../src/engine/ink-engine.ts"],"names":[],"mappings":";;;;;;AA4BA,4CA8CC;AA1ED,2BAAwE;AACxE,gDAAwB;AACxB,iCAA8B;AAM9B,oDAA6D;AAE7D,MAAM,EAAE,QAAQ,EAAE,GAAG,OAAO,CAAC,YAAY,CAUgB,CAAC;AAQ1D,SAAgB,gBAAgB,CAAC,UAAkB,EAAE,UAAkB;IACrE,MAAM,cAAc,GAAG,cAAI,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;IAChD,MAAM,cAAc,GAAG,cAAI,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;IAChD,IAAI,CAAC,IAAA,eAAU,EAAC,cAAc,CAAC,EAAE,CAAC;QAChC,MAAM,IAAI,KAAK,CAAC,8BAA8B,cAAc,EAAE,CAAC,CAAC;IAClE,CAAC;IAED,MAAM,QAAQ,GAAa,EAAE,CAAC;IAC9B,MAAM,MAAM,GAAa,EAAE,CAAC;IAC5B,MAAM,MAAM,GAAG,IAAA,iBAAY,EAAC,cAAc,EAAE,MAAM,CAAC,CAAC,OAAO,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC;IAC3E,MAAM,SAAS,GAAG,cAAI,CAAC,OAAO,CAAC,cAAc,CAAC,CAAC;IAC/C,MAAM,WAAW,GAAG;QAClB,kBAAkB,EAAE,CAAC,QAAgB,EAAE,EAAE,CACvC,cAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,cAAI,CAAC,OAAO,CAAC,SAAS,EAAE,QAAQ,CAAC;QAC1E,mBAAmB,EAAE,CAAC,QAAgB,EAAE,EAAE,CACxC,IAAA,iBAAY,EAAC,cAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,cAAI,CAAC,OAAO,CAAC,SAAS,EAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;aAC3F,OAAO,CAAC,SAAS,EAAE,EAAE,CAAC;KAC5B,CAAC;IACF,MAAM,QAAQ,GAAG,IAAI,QAAQ,CAAC,MAAM,EAAE;QACpC,cAAc,EAAE,cAAc;QAC9B,WAAW;QACX,YAAY,EAAE,CAAC,OAAe,EAAE,IAAY,EAAE,EAAE;YAC9C,IAAI,IAAI,KAAK,CAAC,EAAE,CAAC;gBACf,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YACzB,CAAC;iBAAM,CAAC;gBACN,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YACvB,CAAC;QACH,CAAC;KACF,CAAC,CAAC;IAEH,MAAM,KAAK,GAAG,QAAQ,CAAC,OAAO,EAAE,CAAC;IACjC,IAAI,CAAC,KAAK,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAChC,MAAM,IAAI,KAAK,CAAC,4BAA4B,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACnE,CAAC;IAED,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACxB,QAAQ,CAAC,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,SAAS,OAAO,EAAE,CAAC,CAAC,CAAC;IAClE,CAAC;IAED,IAAA,cAAS,EAAC,cAAI,CAAC,OAAO,CAAC,cAAc,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC7D,IAAA,kBAAa,EAAC,cAAc,EAAE,KAAK,CAAC,MAAM,EAAE,EAAE,MAAM,CAAC,CAAC;IACtD,OAAO;QACL,UAAU,EAAE,cAAc;QAC1B,UAAU,EAAE,cAAc;QAC1B,YAAY,EAAE,QAAQ,CAAC,MAAM;KAC9B,CAAC;AACJ,CAAC;AAED,MAAa,SAAS;IAMpB,YAA6B,SAAiB;QAAjB,cAAS,GAAT,SAAS,CAAQ;QALtC,UAAK,GAAiB,IAAI,CAAC;QAC3B,eAAU,GAAG,CAAC,CAAC;QACf,cAAS,GAAQ,IAAI,CAAC;QACb,yBAAoB,GAAG,IAAI,GAAG,CAAC,CAAC,QAAQ,EAAE,KAAK,EAAE,QAAQ,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC;IAEzD,CAAC;IAElD,SAAS;QACP,IAAI,CAAC,IAAI,CAAC,KAAK;YAAE,OAAO,KAAK,CAAC;QAC9B,OAAO,IAAI,CAAC,KAAK,CAAC,WAAW,IAAI,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,MAAM,GAAG,CAAC,CAAC;IACxE,CAAC;IAED,OAAO;QACL,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,SAAS,EAAE,CAAC;QAC9B,IAAI,CAAC,UAAU,GAAG,CAAC,CAAC;QACpB,OAAO,IAAI,CAAC,aAAa,EAAE,CAAC;IAC9B,CAAC;IAED,YAAY,CAAC,WAAmB;QAC9B,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC;YAChB,MAAM,IAAI,KAAK,CAAC,qBAAqB,CAAC,CAAC;QACzC,CAAC;QAED,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,KAAK,WAAW,CAAC,CAAC;QACpF,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,MAAM,IAAI,KAAK,CAAC,cAAc,WAAW,mBAAmB,CAAC,CAAC;QAChE,CAAC;QAED,IAAI,CAAC,KAAK,CAAC,iBAAiB,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QAC3C,OAAO,IAAI,CAAC,aAAa,EAAE,CAAC;IAC9B,CAAC;IAED,QAAQ;QACN,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC;YAChB,MAAM,IAAI,KAAK,CAAC,6BAA6B,CAAC,CAAC;QACjD,CAAC;QACD,OAAO,IAAI,CAAC,SAAS,CAAC;YACpB,QAAQ,EAAE,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,MAAM,EAAE;YACnC,UAAU,EAAE,IAAI,CAAC,UAAU;SAC5B,CAAC,CAAC;IACL,CAAC;IAED,UAAU,CAAC,UAAkB;QAC3B,IAAI,CAAC,YAAY,CAAC,UAAU,CAAC,CAAC;IAChC,CAAC;IAED,QAAQ,CAAC,UAAkB;QACzB,IAAI,CAAC,YAAY,CAAC,UAAU,CAAC,CAAC;QAC9B,OAAO,IAAI,CAAC,aAAa,EAAE,CAAC;IAC9B,CAAC;IAEO,YAAY,CAAC,UAAkB;QACrC,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,SAAS,EAAE,CAAC;QAC9B,IAAI,QAAQ,GAAG,UAAU,CAAC;QAC1B,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;YACtC,IAAI,MAAM,IAAI,OAAO,MAAM,CAAC,QAAQ,KAAK,QAAQ,EAAE,CAAC;gBAClD,QAAQ,GAAG,MAAM,CAAC,QAAQ,CAAC;gBAC3B,IAAI,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC,UAAU,CAAC,EAAE,CAAC;oBACxC,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC,CAAC;gBACnD,CAAC;YACH,CAAC;QACH,CAAC;QAAC,MAAM,CAAC;YACP,kDAAkD;QACpD,CAAC;QACD,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;IACtC,CAAC;IAEO,SAAS;QACf,MAAM,YAAY,GAAG,cAAI,CAAC,OAAO,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;QAClD,IAAI,CAAC,IAAA,eAAU,EAAC,YAAY,CAAC,EAAE,CAAC;YAC9B,MAAM,IAAI,KAAK,CAAC,6BAA6B,YAAY,EAAE,CAAC,CAAC;QAC/D,CAAC;QAED,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,KAAK,CAAC,IAAA,iBAAY,EAAC,YAAY,EAAE,MAAM,CAAC,CAAC,CAAC;QAChE,OAAO,IAAI,aAAK,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IACnC,CAAC;IAEO,aAAa;QACnB,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC;YAChB,MAAM,IAAI,KAAK,CAAC,qBAAqB,CAAC,CAAC;QACzC,CAAC;QAED,MAAM,UAAU,GAA6B,EAAE,CAAC;QAChD,MAAM,UAAU,GAAe,EAAE,CAAC;QAClC,MAAM,QAAQ,GAAe,EAAE,CAAC;QAEhC,OAAO,IAAI,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC;YAC9B,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAC;YACtC,MAAM,IAAI,GAAG,MAAM,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;YAC1C,MAAM,IAAI,GAAG,IAAA,sBAAS,EAAC,IAAI,CAAC,KAAK,CAAC,WAAW,IAAI,EAAE,CAAC,CAAC;YACrD,QAAQ,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,CAAC;YAEvB,IAAI;iBACD,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,GAAG,KAAK,OAAO,IAAI,GAAG,CAAC,GAAG,KAAK,QAAQ,CAAC;iBAC5D,OAAO,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;YAE1C,IAAI,IAAI,EAAE,CAAC;gBACT,UAAU,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;YAClC,CAAC;iBAAM,CAAC;gBACN,IAAI,CAAC,OAAO,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;YAC9C,CAAC;QACH,CAAC;QAED,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,GAAG,CAAC,CAAC,MAAM,EAAgB,EAAE;YACrE,MAAM,IAAI,GAAG,IAAI,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC;YACxC,MAAM,QAAQ,GAAG,IAAA,wBAAW,EAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;YAC7C,MAAM,MAAM,GAAG,IAAA,wBAAW,EAAC,IAAI,EAAE,QAAQ,CAAC,IAAI,IAAA,wBAAW,EAAC,IAAI,EAAE,KAAK,CAAC,CAAC;YACvE,OAAO;gBACL,KAAK,EAAE,MAAM,CAAC,KAAK;gBACnB,IAAI,EAAE,MAAM,CAAC,MAAM,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE;gBACtC,IAAI;gBACJ,QAAQ;gBACR,MAAM;aACP,CAAC;QACJ,CAAC,CAAC,CAAC;QACH,MAAM,SAAS,GAAG,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC;QACxD,MAAM,SAAS,GAA4B,EAAE,CAAC;QAE9C,IAAI,SAAS,KAAK,KAAK,EAAE,CAAC;YACxB,MAAM,QAAQ,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,GAAG,KAAK,OAAO,CAAC,CAAC;YAC7D,MAAM,QAAQ,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,GAAG,KAAK,OAAO,CAAC,CAAC;YAE7D,IAAI,CAAC,QAAQ,IAAI,CAAC,QAAQ,EAAE,CAAC;gBAC3B,MAAM,OAAO,GAAG,wDAAwD,CAAC;gBACzE,MAAM,iBAAiB,GAAa,EAAE,GAAG,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC;gBACrE,UAAU,CAAC,IAAI,CAAC,iBAAiB,CAAC,CAAC;gBACnC,QAAQ,CAAC,IAAI,CAAC,iBAAiB,CAAC,CAAC;YACnC,CAAC;YAED,MAAM,aAAa,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,GAAG,KAAK,OAAO,CAAC,CAAC;YAClE,MAAM,aAAa,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,GAAG,KAAK,OAAO,CAAC,CAAC;YAClE,IAAI,aAAa,EAAE,CAAC;gBAClB,SAAS,CAAC,QAAQ,GAAG;oBACnB,IAAI,EAAE,OAAO;oBACb,OAAO,EAAE,aAAa,CAAC,KAAK,IAAI,aAAa,CAAC,KAAK;iBACpD,CAAC;YACJ,CAAC;iBAAM,IAAI,aAAa,EAAE,CAAC;gBACzB,MAAM,YAAY,GAAG,MAAM,CAAC,aAAa,EAAE,KAAK,CAAC,CAAC;gBAClD,IAAI,MAAM,CAAC,QAAQ,CAAC,YAAY,CAAC,EAAE,CAAC;oBAClC,SAAS,CAAC,KAAK,GAAG,YAAY,CAAC;gBACjC,CAAC;gBACD,SAAS,CAAC,QAAQ,GAAG;oBACnB,IAAI,EAAE,UAAU;oBAChB,OAAO,EAAE,aAAa,CAAC,KAAK,IAAI,aAAa,CAAC,KAAK;iBACpD,CAAC;YACJ,CAAC;QACH,CAAC;QAED,OAAO;YACL,MAAM,EAAE,IAAI,CAAC,UAAU,EAAE;YACzB,UAAU;YACV,OAAO;YACP,SAAS;YACT,UAAU,EAAE,UAAU,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,SAAS;YAC1D,SAAS,EAAE,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS;SACrE,CAAC;IACJ,CAAC;IAEO,aAAa,CAAC,MAAW;QAC/B,MAAM,UAAU,GAAG,IAAA,sBAAS,EAAC,MAAM,EAAE,IAAI,IAAI,EAAE,CAAC,CAAC;QACjD,MAAM,WAAW,GAAG,IAAI,CAAC,wBAAwB,CAAC,MAAM,CAAC,CAAC;QAC1D,MAAM,MAAM,GAAG,IAAI,GAAG,EAAoB,CAAC;QAC3C,CAAC,GAAG,WAAW,EAAE,GAAG,UAAU,CAAC,CAAC,OAAO,CAAC,CAAC,GAAG,EAAE,EAAE;YAC9C,MAAM,CAAC,GAAG,CAAC,GAAG,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,KAAK,IAAI,EAAE,IAAI,GAAG,CAAC,KAAK,IAAI,EAAE,EAAE,EAAE,GAAG,CAAC,CAAC;QACtE,CAAC,CAAC,CAAC;QACH,OAAO,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC;IACrC,CAAC;IAEO,wBAAwB,CAAC,MAAW;QAC1C,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,kBAAkB,IAAI,MAAM,EAAE,UAAU,EAAE,QAAQ,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;QACvG,IAAI,CAAC,UAAU,IAAI,CAAC,IAAI,CAAC,SAAS;YAAE,OAAO,EAAE,CAAC;QAE9C,MAAM,SAAS,GAAG,IAAI,CAAC,cAAc,CAAC,UAAU,CAAC,CAAC;QAClD,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,SAAS,CAAC;YAAE,OAAO,EAAE,CAAC;QAEzC,MAAM,IAAI,GAAe,EAAE,CAAC;QAC5B,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,SAAS,CAAC,MAAM,EAAE,KAAK,IAAI,CAAC,EAAE,CAAC;YACzD,MAAM,KAAK,GAAG,SAAS,CAAC,KAAK,CAAC,CAAC;YAC/B,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE;gBAAE,SAAS;YAClF,IAAI,KAAK,KAAK,IAAI;gBAAE,SAAS;YAC7B,IAAI,KAAK,KAAK,GAAG;gBAAE,MAAM;YAEzB,MAAM,QAAQ,GAAa,EAAE,CAAC;YAC9B,KAAK,IAAI,CAAC,CAAC;YACX,OAAO,KAAK,GAAG,SAAS,CAAC,MAAM,IAAI,SAAS,CAAC,KAAK,CAAC,KAAK,IAAI,EAAE,CAAC;gBAC7D,MAAM,IAAI,GAAG,SAAS,CAAC,KAAK,CAAC,CAAC;gBAC9B,IAAI,OAAO,IAAI,KAAK,QAAQ,EAAE,CAAC;oBAC7B,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,CAAC;gBACzC,CAAC;gBACD,KAAK,IAAI,CAAC,CAAC;YACb,CAAC;YAED,MAAM,GAAG,GAAG,IAAA,sBAAS,EAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;YACrD,IAAI,GAAG,IAAI,IAAI,CAAC,oBAAoB,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;gBAClD,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YACjB,CAAC;QACH,CAAC;QAED,OAAO,IAAI,CAAC;IACd,CAAC;IAEO,cAAc,CAAC,UAAkB;QACvC,MAAM,KAAK,GAAG,UAAU,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;QACpD,IAAI,IAAI,GAAQ,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC;QAErC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACzB,IAAI,CAAC,IAAI;gBAAE,OAAO,IAAI,CAAC;YAEvB,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;gBAC9C,IAAI,GAAG,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC;YAC5B,CAAC;iBAAM,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;gBAC/B,IAAI,GAAG,IAAI,CAAC,iBAAiB,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;YAC5C,CAAC;iBAAM,IAAI,IAAI,CAAC,mBAAmB,CAAC,IAAI,CAAC,IAAI,IAAI,IAAI,IAAI,EAAE,CAAC;gBAC1D,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC;YACpB,CAAC;iBAAM,CAAC;gBACN,OAAO,IAAI,CAAC;YACd,CAAC;QACH,CAAC;QAED,OAAO,IAAI,CAAC;IACd,CAAC;IAEO,iBAAiB,CAAC,SAAgB,EAAE,IAAY;QACtD,KAAK,IAAI,KAAK,GAAG,SAAS,CAAC,MAAM,GAAG,CAAC,EAAE,KAAK,IAAI,CAAC,EAAE,KAAK,IAAI,CAAC,EAAE,CAAC;YAC9D,MAAM,IAAI,GAAG,SAAS,CAAC,KAAK,CAAC,CAAC;YAE9B,IAAI,IAAI,CAAC,mBAAmB,CAAC,IAAI,CAAC,IAAI,IAAI,IAAI,IAAI,EAAE,CAAC;gBACnD,OAAO,IAAI,CAAC,IAAI,CAAC,CAAC;YACpB,CAAC;YAED,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC;gBAAE,SAAS;YAEnC,MAAM,QAAQ,GAAG,IAAI,CAAC,kBAAkB,CAAC,IAAI,CAAC,CAAC;YAC/C,IAAI,QAAQ,EAAE,CAAC,IAAI,CAAC,KAAK,IAAI,EAAE,CAAC;gBAC9B,OAAO,IAAI,CAAC;YACd,CAAC;YACD,IAAI,QAAQ,IAAI,IAAI,IAAI,QAAQ,EAAE,CAAC;gBACjC,OAAO,QAAQ,CAAC,IAAI,CAAC,CAAC;YACxB,CAAC;QACH,CAAC;QAED,OAAO,IAAI,CAAC;IACd,CAAC;IAEO,kBAAkB,CAAC,SAAgB;QACzC,KAAK,IAAI,KAAK,GAAG,SAAS,CAAC,MAAM,GAAG,CAAC,EAAE,KAAK,IAAI,CAAC,EAAE,KAAK,IAAI,CAAC,EAAE,CAAC;YAC9D,MAAM,IAAI,GAAG,SAAS,CAAC,KAAK,CAAC,CAAC;YAC9B,IAAI,IAAI,CAAC,mBAAmB,CAAC,IAAI,CAAC,EAAE,CAAC;gBACnC,OAAO,IAAI,CAAC;YACd,CAAC;QACH,CAAC;QACD,OAAO,IAAI,CAAC;IACd,CAAC;IAEO,mBAAmB,CAAC,KAAc;QACxC,OAAO,OAAO,CAAC,KAAK,CAAC,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;IAC9E,CAAC;CACF;AAnQD,8BAmQC"} \ No newline at end of file diff --git a/dist/engine/zcode-llm-engine.d.ts b/dist/engine/zcode-llm-engine.d.ts deleted file mode 100644 index 11df7c7..0000000 --- a/dist/engine/zcode-llm-engine.d.ts +++ /dev/null @@ -1,84 +0,0 @@ -/** - * Z-code LLM Engine - * - * Runs a Z-machine story file as a headless subprocess via the - * `ifvms` CLI, and wraps every I/O exchange with OpenRouter LLM calls that - * translate free natural-language player input into parser commands and - * re-voice the Z-machine's raw output as polished narrative prose. - * - * Configuration (environment variables): - * ZCODE_STORY_FILE - path to the .z5/.z8/.bin story file (default: ./data/z-code/zork1.bin) - * ZCODE_MAX_RETRIES - maximum command retry attempts per turn (default: 3) - * ZCODE_HISTORY_SIZE - player-facing outputs stored per room (default: 5) - * OPENROUTER_API_KEY, OPENROUTER_MODEL - required - */ -import { TurnResult } from '../interfaces/turn-result'; -export interface ZcodeSession { - 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; -} -export type ZcodeTurnResult = TurnResult; -export declare class ZcodeLlmEngine { - private zmachine; - private session; - private prompts; - private llm; - private model; - private resolvedFallbackModel; - private llmCallCounter; - private maxRetries; - private historySize; - private nextTurnId; - private storyPath; - private static readonly DEPRECATED_MODEL_REPLACEMENTS; - constructor(options?: { - storyPath?: string; - promptDir?: string; - }); - private createCompletion; - private resolveFallbackModel; - isRunning(): boolean; - /** - * Start a new game: launch the Z-machine story, generate the player character, rewrite the - * intro text, and return the first TurnResult for the client. - */ - newGame(): Promise; - /** - * 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/zcode-llm-engine.js b/dist/engine/zcode-llm-engine.js deleted file mode 100644 index f371e51..0000000 --- a/dist/engine/zcode-llm-engine.js +++ /dev/null @@ -1,989 +0,0 @@ -"use strict"; -/** - * Z-code LLM Engine - * - * Runs a Z-machine story file as a headless subprocess via the - * `ifvms` CLI, and wraps every I/O exchange with OpenRouter LLM calls that - * translate free natural-language player input into parser commands and - * re-voice the Z-machine's raw output as polished narrative prose. - * - * Configuration (environment variables): - * ZCODE_STORY_FILE - path to the .z5/.z8/.bin story file (default: ./data/z-code/zork1.bin) - * ZCODE_MAX_RETRIES - maximum command retry attempts per turn (default: 3) - * ZCODE_HISTORY_SIZE - player-facing outputs stored per room (default: 5) - * OPENROUTER_API_KEY, OPENROUTER_MODEL - required - */ -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - 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.ZcodeLlmEngine = 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")); -const turn_result_1 = require("../interfaces/turn-result"); -dotenv.config(); -const DEBUG_ENABLED = /^(1|true|yes|on)$/i.test(process.env.ZCODE_DEBUG ?? ''); -function debugLog(message, details) { - if (!DEBUG_ENABLED) - return; - if (typeof details === 'undefined') { - console.log(`[ZcodeLlm:debug] ${message}`); - return; - } - console.log(`[ZcodeLlm: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, zcodeOutput) { - const object = command.replace(/^READ\s+/i, '').trim().toLowerCase(); - const label = object ? `the ${object}` : 'it'; - const cleanedOutput = zcodeOutput - .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]; -} -// --------------------------------------------------------------------------- -// ZcodeProcess – manages the ifvms zvm child process -// --------------------------------------------------------------------------- -class ZcodeProcess { - 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 a Z-machine 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(`[ZcodeLlm] ${scope} failed: ${ax.message}`); - if (ax.response) { - console.error(`[ZcodeLlm] ${scope} status=${ax.response.status} data=`, ax.response.data); - if (ax.response.status === 404) { - console.error('[ZcodeLlm] Hint: OPENROUTER_MODEL is likely invalid or unavailable for your API key.'); - } - } - return; - } - console.error(`[ZcodeLlm] ${scope} failed:`, err); -} -// --------------------------------------------------------------------------- -// ZcodeLlmEngine -// --------------------------------------------------------------------------- -class ZcodeLlmEngine { - constructor(options = {}) { - this.zmachine = new ZcodeProcess(); - this.session = null; - this.resolvedFallbackModel = null; - this.llmCallCounter = 0; - this.nextTurnId = 1; - 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 = ZcodeLlmEngine.DEPRECATED_MODEL_REPLACEMENTS[model] ?? null; - if (replacement) { - this.model = replacement; - console.warn(`[ZcodeLlm] 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.ZCODE_MAX_RETRIES ?? '3', 10); - this.historySize = parseInt(process.env.ZCODE_HISTORY_SIZE ?? '5', 10); - this.storyPath = path.resolve(options.storyPath ?? process.env.ZCODE_STORY_FILE ?? './data/z-code/zork1.bin'); - const promptDir = path.resolve(options.promptDir ?? './data/zcode-prompts'); - this.prompts = loadPrompts(promptDir); - this.llm = axios_1.default.create({ - baseURL: 'https://openrouter.ai/api/v1', - 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(`[ZcodeLlm] 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.zmachine.isAlive(); - } - /** - * Start a new game: launch the Z-machine story, generate the player character, rewrite the - * intro text, and return the first TurnResult for the client. - */ - async newGame() { - // Kill any existing game - if (this.zmachine.isAlive()) - this.zmachine.kill(); - this.nextTurnId = 1; - if (!fs.existsSync(this.storyPath)) { - throw new Error(`Story file not found: ${this.storyPath}\n` + - 'Place zork1.bin in ./data/z-code/ (see README in that folder).'); - } - debugLog('launching Z-machine', { storyPath: this.storyPath }); - const rawIntro = await this.zmachine.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 Z-machine command, continue to game loop - if (!cmdResponse.command && !cmdResponse.commands?.length) { - // Pure tool action — generate a brief acknowledgement via the rewriter - const ack = await this.rewriteText(`(The narrator pauses. ${userInput})`); - 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(), `zcode-save-${Date.now()}.qzl`); - try { - // Ask the Z-machine to save, supply the temp file path, and discard the output - await this.zmachine.sendLine('SAVE'); - await this.zmachine.sendLine(tmpFile); - let zcodeSave = ''; - if (fs.existsSync(tmpFile)) { - zcodeSave = fs.readFileSync(tmpFile).toString('base64'); - } - return JSON.stringify({ session: this.session, zcodeSave }); - } - 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, zcodeSave } = JSON.parse(savedJson); - if (this.zmachine.isAlive()) - this.zmachine.kill(); - const tmpFile = path.join(os.tmpdir(), `zcode-restore-${Date.now()}.qzl`); - try { - fs.writeFileSync(tmpFile, Buffer.from(zcodeSave, 'base64')); - await this.zmachine.launch(this.storyPath); - await this.zmachine.sendLine('RESTORE'); - const restoreOutput = await this.zmachine.sendLine(tmpFile); - this.session = { ...session, running: true }; - (_a = this.session).rawTranscript ?? (_a.rawTranscript = []); - (_b = this.session).recentParagraphs ?? (_b.recentParagraphs = []); - (_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.zmachine.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(zcodeOutput) { - const cfg = this.prompts.textRewriter; - const vars = this.buildCommonVars(); - vars['zcodeOutput'] = zcodeOutput; - 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 zcodeOutput; - } - } - 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 Z-machine parser - return { type: 'command', command: userInput.toUpperCase() }; - } - } - async evaluateOutput(userIntent, commandTried, zcodeOutput, attempt) { - const cfg = this.prompts.outputEvaluator; - const vars = this.buildCommonVars(); - vars['userIntent'] = userIntent; - vars['commandTried'] = commandTried; - vars['zcodeOutput'] = zcodeOutput; - 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: zcodeOutput }; - } - } - // ---- 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.zmachine.isAlive(); - if (!alive && this.session) - this.session.running = false; - const paragraphs = (0, turn_result_1.textToParagraphs)(text); - return { - turnId: this.nextTurnId++, - paragraphs, - choices: [], - inputMode: alive ? 'text' : 'end', - gameState: { statusLine: this.session?.currentRoom }, - }; - } -} -exports.ZcodeLlmEngine = ZcodeLlmEngine; -ZcodeLlmEngine.DEPRECATED_MODEL_REPLACEMENTS = { - 'anthropic/claude-3-opus-20240229': 'openai/gpt-5.5', - 'openai/gpt-5.4-mini': 'openai/gpt-5.5', -}; -//# sourceMappingURL=zcode-llm-engine.js.map \ No newline at end of file diff --git a/dist/engine/zcode-llm-engine.js.map b/dist/engine/zcode-llm-engine.js.map deleted file mode 100644 index 62bf410..0000000 --- a/dist/engine/zcode-llm-engine.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"zcode-llm-engine.js","sourceRoot":"","sources":["../../src/engine/zcode-llm-engine.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/dist/index.d.ts b/dist/index.d.ts deleted file mode 100644 index 32af2d4..0000000 --- a/dist/index.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -/** - * Main entry point for the AI Interactive Fiction application - */ -export {}; diff --git a/dist/index.js b/dist/index.js deleted file mode 100644 index 4732732..0000000 --- a/dist/index.js +++ /dev/null @@ -1,112 +0,0 @@ -"use strict"; -/** - * Main entry point for the AI Interactive Fiction application - */ -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; - }; -})(); -Object.defineProperty(exports, "__esModule", { value: true }); -const dotenv = __importStar(require("dotenv")); -const game_runner_1 = require("./cli/game-runner"); -// YAML CLI entry point. The web default is selected by scripts/run-engine.js. -const server_yaml_1 = require("./server-yaml"); -const game_config_1 = require("./config/game-config"); -// Load environment variables -console.log('Loading environment variables...'); -try { - const result = dotenv.config(); - if (result.error) { - console.error('Error loading .env file:', result.error); - } - else { - console.log('Environment variables loaded successfully'); - } -} -catch (error) { - console.error('Exception when loading env:', error); -} -async function main() { - try { - console.log('=== AI Interactive Fiction ==='); - console.log('A modern take on classic text adventures with LLM-powered interactions'); - console.log(''); - // Get the world file path from the YAML engine config, with environment override. - const engineConfig = (0, game_config_1.loadGameConfig)(process.env.YAML_CONFIG_FILE || './config/engines/yaml.json', 'yaml'); - const worldFile = (0, game_config_1.projectPath)(process.env.DEFAULT_WORLD_FILE || engineConfig.paths.mainGameFile); - console.log(`Using world file: ${worldFile}`); - console.log(`OpenRouter API Key: ${process.env.OPENROUTER_API_KEY ? 'Found' : 'Missing'}`); - console.log(`OpenRouter Model: ${process.env.OPENROUTER_MODEL || 'Not specified'}`); - // Check if we should run in CLI mode - const args = process.argv.slice(2); - const cliMode = args.includes('--cli') || args.includes('-c'); - if (cliMode) { - // CLI mode - console.log('Starting in CLI mode...'); - // Create game runner and initialize - console.log('Creating game runner...'); - const gameRunner = new game_runner_1.GameRunner(); - console.log('Initializing game...'); - await gameRunner.initialize(worldFile); - // Start the CLI game - console.log('Starting CLI game...'); - await gameRunner.start(); - } - else { - // Web interface mode - explicitly start the server with port fallback - console.log('Starting in web interface mode...'); - // Get port configuration - const DEFAULT_PORT = 3000; - const PORT = process.env.PORT ? parseInt(process.env.PORT) : DEFAULT_PORT; - const PORT_RANGE = 300; - // Start the web server with port fallback - console.log('Starting web server...'); - await (0, server_yaml_1.startServer)(PORT, PORT_RANGE); - } - } - catch (error) { - console.error('Failed to start:', error); - if (error instanceof Error) { - console.error('Error name:', error.name); - console.error('Error message:', error.message); - console.error('Error stack:', error.stack); - } - process.exit(1); - } -} -// Start the application -console.log('Starting application...'); -main().catch(error => { - console.error('Unhandled error in main:', error); - process.exit(1); -}); -//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/dist/index.js.map b/dist/index.js.map deleted file mode 100644 index 7a4cadf..0000000 --- a/dist/index.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/dist/interfaces/engine.d.ts b/dist/interfaces/engine.d.ts deleted file mode 100644 index ecdde6d..0000000 --- a/dist/interfaces/engine.d.ts +++ /dev/null @@ -1,39 +0,0 @@ -/** - * Interfaces for the game engine - */ -import { WorldModel, GameState } from './world-model'; -import { ActionResponse, NarrativeResponse } from './llm'; -export interface ActionResult { - success: boolean; - message: string; - stateChanged: boolean; - newState?: GameState; -} -export interface GameEngine { - loadWorld(worldModelPath: string): Promise; - getCurrentState(): GameState; - getWorldModel(): WorldModel; - processAction(action: ActionResponse): ActionResult; - saveGame(filename: string): Promise; - loadGame(filename: string): Promise; - getAvailableActions(): string[]; - getVisibleObjects(): string[]; - getVisibleCharacters(): string[]; - getCurrentRoomDescription(): string; - start(): Promise; - end(): void; -} -export interface GameSession { - engine: GameEngine; - history: { - playerInput: string; - actionResponse: ActionResponse; - actionResult: ActionResult; - narrativeResponse: NarrativeResponse; - }[]; - startTime: Date; - lastInteractionTime: Date; -} -export interface ActionHandler { - execute(gameState: GameState, worldModel: WorldModel, action: ActionResponse): ActionResult; -} diff --git a/dist/interfaces/engine.js b/dist/interfaces/engine.js deleted file mode 100644 index 2892d4b..0000000 --- a/dist/interfaces/engine.js +++ /dev/null @@ -1,6 +0,0 @@ -"use strict"; -/** - * Interfaces for the game engine - */ -Object.defineProperty(exports, "__esModule", { value: true }); -//# sourceMappingURL=engine.js.map \ No newline at end of file diff --git a/dist/interfaces/engine.js.map b/dist/interfaces/engine.js.map deleted file mode 100644 index 0a23924..0000000 --- a/dist/interfaces/engine.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"engine.js","sourceRoot":"","sources":["../../src/interfaces/engine.ts"],"names":[],"mappings":";AAAA;;GAEG"} \ No newline at end of file diff --git a/dist/interfaces/llm.d.ts b/dist/interfaces/llm.d.ts deleted file mode 100644 index 740da5a..0000000 --- a/dist/interfaces/llm.d.ts +++ /dev/null @@ -1,46 +0,0 @@ -/** - * Interfaces for LLM integration - */ -export interface LlmConfig { - apiKey: string; - model: string; - temperature?: number; - maxTokens?: number; - topP?: number; - frequencyPenalty?: number; - presencePenalty?: number; -} -export interface ActionRequest { - playerInput: string; - currentRoom: string; - visibleObjects: string[]; - visibleCharacters: string[]; - possibleActions: string[]; - inventory: string[]; - gameContext: string; -} -export interface ActionResponse { - action: string; - object?: string; - target?: string; - parameters?: Record; - confidence: number; -} -export interface NarrativeRequest { - action: string; - result: string; - roomDescription: string; - visibleObjects: string[]; - visibleCharacters: string[]; - previousContext?: string; - tone?: string; -} -export interface NarrativeResponse { - text: string; - suggestions?: string[]; -} -export interface LlmProvider { - initialize(config: LlmConfig): Promise; - translateAction(request: ActionRequest): Promise; - generateNarrative(request: NarrativeRequest): Promise; -} diff --git a/dist/interfaces/llm.js b/dist/interfaces/llm.js deleted file mode 100644 index 1aa0d45..0000000 --- a/dist/interfaces/llm.js +++ /dev/null @@ -1,6 +0,0 @@ -"use strict"; -/** - * Interfaces for LLM integration - */ -Object.defineProperty(exports, "__esModule", { value: true }); -//# sourceMappingURL=llm.js.map \ No newline at end of file diff --git a/dist/interfaces/llm.js.map b/dist/interfaces/llm.js.map deleted file mode 100644 index 957cb4b..0000000 --- a/dist/interfaces/llm.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"llm.js","sourceRoot":"","sources":["../../src/interfaces/llm.ts"],"names":[],"mappings":";AAAA;;GAEG"} \ No newline at end of file diff --git a/dist/interfaces/turn-result.d.ts b/dist/interfaces/turn-result.d.ts deleted file mode 100644 index 7bb8c0d..0000000 --- a/dist/interfaces/turn-result.d.ts +++ /dev/null @@ -1,36 +0,0 @@ -export type InputMode = 'text' | 'choice' | 'end'; -export interface StoryTag { - key: string; - value?: string; - param?: string; -} -export interface ParagraphResult { - text: string; - tags: StoryTag[]; -} -export interface ChoiceResult { - index: number; - text: string; - tags: StoryTag[]; - category?: string; - letter?: string; -} -export interface TurnResult { - turnId: number; - paragraphs: ParagraphResult[]; - choices: ChoiceResult[]; - inputMode: InputMode; - globalTags?: StoryTag[]; - gameState?: { - currentRoomId?: string; - score?: number; - moves?: number; - statusLine?: string; - endState?: { - type: 'intended' | 'error'; - message?: string; - }; - }; - suggestions?: string[]; -} -export declare function textToParagraphs(text: string, tags?: StoryTag[]): ParagraphResult[]; diff --git a/dist/interfaces/turn-result.js b/dist/interfaces/turn-result.js deleted file mode 100644 index 452413f..0000000 --- a/dist/interfaces/turn-result.js +++ /dev/null @@ -1,36 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.textToParagraphs = textToParagraphs; -/** - * Shared engine-to-client turn protocol. - */ -const tag_parser_1 = require("../utils/tag-parser"); -function textToParagraphs(text, tags = []) { - return String(text || '') - .replace(/\r\n?/g, '\n') - .split(/\n{2,}/) - .map((paragraph) => paragraph.trim()) - .filter(Boolean) - .map((paragraph) => { - const lines = paragraph.split('\n'); - const paragraphTags = [...tags]; - const textLines = []; - let tagPrefixOpen = true; - for (const line of lines) { - const trimmed = line.trim(); - const maybeTag = tagPrefixOpen && trimmed.startsWith('#') ? (0, tag_parser_1.parseTag)(trimmed) : null; - if (maybeTag) { - paragraphTags.push(maybeTag); - } - else { - tagPrefixOpen = false; - textLines.push(line); - } - } - return { - text: textLines.join('\n').trim(), - tags: paragraphTags, - }; - }); -} -//# sourceMappingURL=turn-result.js.map \ No newline at end of file diff --git a/dist/interfaces/turn-result.js.map b/dist/interfaces/turn-result.js.map deleted file mode 100644 index b0d8ec6..0000000 --- a/dist/interfaces/turn-result.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"turn-result.js","sourceRoot":"","sources":["../../src/interfaces/turn-result.ts"],"names":[],"mappings":";;AA6CA,4CA6BC;AA1ED;;GAEG;AACH,oDAA+C;AA0C/C,SAAgB,gBAAgB,CAAC,IAAY,EAAE,OAAmB,EAAE;IAClE,OAAO,MAAM,CAAC,IAAI,IAAI,EAAE,CAAC;SACtB,OAAO,CAAC,QAAQ,EAAE,IAAI,CAAC;SACvB,KAAK,CAAC,QAAQ,CAAC;SACf,GAAG,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,SAAS,CAAC,IAAI,EAAE,CAAC;SACpC,MAAM,CAAC,OAAO,CAAC;SACf,GAAG,CAAC,CAAC,SAAS,EAAE,EAAE;QACjB,MAAM,KAAK,GAAG,SAAS,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QACpC,MAAM,aAAa,GAAe,CAAC,GAAG,IAAI,CAAC,CAAC;QAC5C,MAAM,SAAS,GAAa,EAAE,CAAC;QAC/B,IAAI,aAAa,GAAG,IAAI,CAAC;QAEzB,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACzB,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;YAC5B,MAAM,QAAQ,GAAG,aAAa,IAAI,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAA,qBAAQ,EAAC,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;YAErF,IAAI,QAAQ,EAAE,CAAC;gBACb,aAAa,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;YAC/B,CAAC;iBAAM,CAAC;gBACN,aAAa,GAAG,KAAK,CAAC;gBACtB,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACvB,CAAC;QACH,CAAC;QAED,OAAO;YACL,IAAI,EAAE,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE;YACjC,IAAI,EAAE,aAAa;SACpB,CAAC;IACJ,CAAC,CAAC,CAAC;AACP,CAAC"} \ No newline at end of file diff --git a/dist/interfaces/world-model.d.ts b/dist/interfaces/world-model.d.ts deleted file mode 100644 index b6b5895..0000000 --- a/dist/interfaces/world-model.d.ts +++ /dev/null @@ -1,61 +0,0 @@ -/** - * Core interfaces for the interactive fiction world model - */ -export interface Room { - id: string; - name: string; - description: string; - exits: Exit[]; - objects: string[]; - characters: string[]; -} -export interface Exit { - direction: string; - targetRoomId: string; - description?: string; - isLocked?: boolean; - keyId?: string; -} -export interface GameObject { - id: string; - name: string; - description: string; - traits: string[]; - states: Record; - containedObjects?: string[]; - allowedActions: string[]; -} -export interface Character { - id: string; - name: string; - description: string; - dialogue: Record; - inventory: string[]; - defaultResponse: string; - mood?: string; -} -export interface Action { - name: string; - patterns: string[]; - requiresObject?: boolean; - requiresTarget?: boolean; - handler: string; -} -export interface GameState { - currentRoomId: string; - inventory: string[]; - visitedRooms: string[]; - flags: Record; - counters: Record; -} -export interface WorldModel { - title: string; - author: string; - version: string; - introduction: string; - rooms: Record; - objects: Record; - characters: Record; - actions: Record; - initialState: GameState; -} diff --git a/dist/interfaces/world-model.js b/dist/interfaces/world-model.js deleted file mode 100644 index 314a13f..0000000 --- a/dist/interfaces/world-model.js +++ /dev/null @@ -1,6 +0,0 @@ -"use strict"; -/** - * Core interfaces for the interactive fiction world model - */ -Object.defineProperty(exports, "__esModule", { value: true }); -//# sourceMappingURL=world-model.js.map \ No newline at end of file diff --git a/dist/interfaces/world-model.js.map b/dist/interfaces/world-model.js.map deleted file mode 100644 index ff8ecab..0000000 --- a/dist/interfaces/world-model.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"world-model.js","sourceRoot":"","sources":["../../src/interfaces/world-model.ts"],"names":[],"mappings":";AAAA;;GAEG"} \ No newline at end of file diff --git a/dist/llm/openrouter-provider.d.ts b/dist/llm/openrouter-provider.d.ts deleted file mode 100644 index 54387ce..0000000 --- a/dist/llm/openrouter-provider.d.ts +++ /dev/null @@ -1,36 +0,0 @@ -/** - * OpenRouter LLM Provider - * Handles communication with OpenRouter API for LLM interactions - */ -import { LlmProvider, LlmConfig, ActionRequest, ActionResponse, NarrativeRequest, NarrativeResponse } from '../interfaces/llm'; -export declare class OpenRouterProvider implements LlmProvider { - private apiKey; - private model; - private client; - private temperature; - private maxTokens; - /** - * Initialize the OpenRouter provider with configuration - */ - initialize(config: LlmConfig): Promise; - /** - * Translate player input into a structured action for the game engine - */ - translateAction(request: ActionRequest): Promise; - /** - * Generate narrative prose based on game events - */ - generateNarrative(request: NarrativeRequest): Promise; - /** - * Build the system and user prompts for action translation - */ - private buildActionPrompt; - /** - * Build the system and user prompts for narrative generation - */ - private buildNarrativePrompt; - /** - * Validate and normalize the action response - */ - private validateActionResponse; -} diff --git a/dist/llm/openrouter-provider.js b/dist/llm/openrouter-provider.js deleted file mode 100644 index c7ea00a..0000000 --- a/dist/llm/openrouter-provider.js +++ /dev/null @@ -1,192 +0,0 @@ -"use strict"; -/** - * OpenRouter LLM Provider - * Handles communication with OpenRouter API for LLM interactions - */ -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { "default": mod }; -}; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.OpenRouterProvider = void 0; -const axios_1 = __importDefault(require("axios")); -class OpenRouterProvider { - constructor() { - this.apiKey = ''; - this.model = ''; - this.temperature = 0.7; - this.maxTokens = 800; - } - /** - * Initialize the OpenRouter provider with configuration - */ - async initialize(config) { - this.apiKey = config.apiKey; - this.model = config.model; - this.temperature = config.temperature ?? 0.7; - this.maxTokens = config.maxTokens ?? 800; - this.client = axios_1.default.create({ - baseURL: 'https://openrouter.ai/api/v1', - headers: { - 'Authorization': `Bearer ${this.apiKey}`, - 'Content-Type': 'application/json' - } - }); - } - /** - * Translate player input into a structured action for the game engine - */ - async translateAction(request) { - try { - const prompt = this.buildActionPrompt(request); - const response = await this.client.post('/chat/completions', { - model: this.model, - messages: [ - { - role: 'system', - content: prompt.system - }, - { - role: 'user', - content: prompt.user - } - ], - temperature: 0.2, // Lower temperature for more deterministic outputs - max_tokens: 150, - response_format: { type: 'json_object' } - }); - const content = response.data.choices[0].message.content; - const parsedResponse = JSON.parse(content); - return this.validateActionResponse(parsedResponse); - } - catch (error) { - console.error('Error translating action:', error); - // Fallback to a simple "look" action when errors occur - return { - action: 'look', - confidence: 0.5 - }; - } - } - /** - * Generate narrative prose based on game events - */ - async generateNarrative(request) { - try { - const prompt = this.buildNarrativePrompt(request); - const response = await this.client.post('/chat/completions', { - model: this.model, - messages: [ - { - role: 'system', - content: prompt.system - }, - { - role: 'user', - content: prompt.user - } - ], - temperature: this.temperature, - max_tokens: this.maxTokens - }); - const content = response.data.choices[0].message.content; - // Check if response is JSON format or plain text - try { - const parsedResponse = JSON.parse(content); - return { - text: parsedResponse.text, - suggestions: parsedResponse.suggestions || [] - }; - } - catch { - // Plain text response, just use the content directly - return { - text: content - }; - } - } - catch (error) { - console.error('Error generating narrative:', error); - return { - text: `Something happened, but the narrator is at a loss for words. (Error: ${error instanceof Error ? error.message : String(error)})` - }; - } - } - /** - * Build the system and user prompts for action translation - */ - buildActionPrompt(request) { - const systemPrompt = `You are an AI assistant that translates natural language input into structured action commands for an interactive fiction game. -Your task is to convert player input into a JSON object representing an action that can be understood by the game engine. - -The player is currently in the "${request.currentRoom}" room. -Visible objects: ${request.visibleObjects.join(', ')} -Visible characters: ${request.visibleCharacters.join(', ')} -Inventory: ${request.inventory.join(', ')} -Available actions: ${request.possibleActions.join(', ')} - -Game context: ${request.gameContext} - -Respond ONLY with a JSON object that follows this structure: -{ - "action": "string", // Name of the action (e.g., "take", "examine", "go", "talk", etc.) - "object": "string", // Optional: Primary object of the action - "target": "string", // Optional: Secondary object/target of the action - "parameters": {}, // Optional: Additional parameters as key-value pairs - "confidence": number // How confident you are in this interpretation (0.0-1.0) -} - -Choose the action from the list of available actions. If the player's input is ambiguous or doesn't map well to an available action, choose the closest match and set a lower confidence score.`; - const userPrompt = request.playerInput; - return { - system: systemPrompt, - user: userPrompt - }; - } - /** - * Build the system and user prompts for narrative generation - */ - buildNarrativePrompt(request) { - const tone = request.tone || 'descriptive'; - const systemPrompt = `You are an AI assistant that generates engaging narrative prose for an interactive fiction game. -Your task is to describe what happens when a player performs an action in the game world. - -Craft a vivid, ${tone} description that tells the player what happened as a result of their action. Make your prose engaging and atmospheric. - -Current room description: "${request.roomDescription}" -Visible objects: ${request.visibleObjects.join(', ')} -Visible characters: ${request.visibleCharacters.join(', ')} - -${request.previousContext ? `Previous context: ${request.previousContext}` : ''} - -Respond with engaging prose that describes the outcome of the player's action. -You can optionally include 1-3 subtle hints about interesting things to try next.`; - const userPrompt = `The player has performed this action: "${request.action}". -The result of the action is: "${request.result}". -Please describe what happens in an engaging, narrative way.`; - return { - system: systemPrompt, - user: userPrompt - }; - } - /** - * Validate and normalize the action response - */ - validateActionResponse(response) { - const validatedResponse = { - action: typeof response.action === 'string' ? response.action : 'look', - confidence: typeof response.confidence === 'number' ? response.confidence : 0.5 - }; - if (typeof response.object === 'string') { - validatedResponse.object = response.object; - } - if (typeof response.target === 'string') { - validatedResponse.target = response.target; - } - if (response.parameters && typeof response.parameters === 'object') { - validatedResponse.parameters = response.parameters; - } - return validatedResponse; - } -} -exports.OpenRouterProvider = OpenRouterProvider; -//# sourceMappingURL=openrouter-provider.js.map \ No newline at end of file diff --git a/dist/llm/openrouter-provider.js.map b/dist/llm/openrouter-provider.js.map deleted file mode 100644 index 6ea9069..0000000 --- a/dist/llm/openrouter-provider.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"openrouter-provider.js","sourceRoot":"","sources":["../../src/llm/openrouter-provider.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/dist/server-ink.d.ts b/dist/server-ink.d.ts deleted file mode 100644 index e3439af..0000000 --- a/dist/server-ink.d.ts +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Ink Engine Server - * - * Serves the shared client UI and runs a compiled Ink JSON story through the - * unified TurnResult socket protocol. - */ -import http from 'http'; -import { Server as SocketIOServer } from 'socket.io'; -declare const app: import("express-serve-static-core").Express; -declare const server: http.Server; -declare const io: SocketIOServer; -export declare function startServer(initialPort: number, range: number): Promise; -export { app, server, io }; diff --git a/dist/server-ink.js b/dist/server-ink.js deleted file mode 100644 index 68243c5..0000000 --- a/dist/server-ink.js +++ /dev/null @@ -1,293 +0,0 @@ -"use strict"; -/** - * Ink Engine Server - * - * Serves the shared client UI and runs a compiled Ink JSON story through the - * unified TurnResult socket protocol. - */ -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.io = exports.server = exports.app = void 0; -exports.startServer = startServer; -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 ink_engine_1 = require("./engine/ink-engine"); -const game_config_1 = require("./config/game-config"); -dotenv.config(); -const app = (0, express_1.default)(); -exports.app = app; -const server = http_1.default.createServer(app); -exports.server = server; -const io = new socket_io_1.Server(server); -exports.io = io; -const DEFAULT_PORT = 3003; -const PORT = process.env.PORT ? parseInt(process.env.PORT, 10) : DEFAULT_PORT; -const PORT_RANGE = 300; -const engineConfig = (0, game_config_1.loadGameConfig)(process.env.INK_CONFIG_FILE || './config/engines/ink.json', 'ink'); -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'); - }, -})); -app.get('/api/game-config', (_req, res) => { - res.json((0, game_config_1.clientGameConfig)(engineConfig)); -}); -const sessions = new Map(); -const saveSlots = new Map(); -function normalizeSaveSlot(slot) { - const n = Number(slot); - return Number.isInteger(n) && n > 0 ? n : 1; -} -function getStoryPath() { - return (0, game_config_1.projectPath)(process.env.INK_STORY_FILE || - engineConfig.paths.inkCompiled || - engineConfig.paths.mainGameFile); -} -function getSourcePath() { - return (0, game_config_1.projectPath)(process.env.INK_SOURCE_FILE || engineConfig.paths.inkSource || ''); -} -function compileConfiguredStory() { - const sourcePath = getSourcePath(); - const outputPath = getStoryPath(); - const result = (0, ink_engine_1.compileInkSource)(sourcePath, outputPath); - console.log(`[ink] Compiled ${result.sourcePath} -> ${result.outputPath}` + - (result.warningCount > 0 ? ` (${result.warningCount} warnings)` : '')); -} -function getSlots(socketId) { - let slots = saveSlots.get(socketId); - if (!slots) { - slots = new Map(); - saveSlots.set(socketId, slots); - } - return slots; -} -function getOrCreateEngine(socketId) { - let engine = sessions.get(socketId); - if (!engine) { - engine = new ink_engine_1.InkEngine(getStoryPath()); - sessions.set(socketId, engine); - } - return engine; -} -async function handleGameApi(socket, method, args) { - const slots = getSlots(socket.id); - switch (method) { - case 'newGame': - case 'newGame()': { - const engine = new ink_engine_1.InkEngine(getStoryPath()); - sessions.set(socket.id, engine); - socket.emit('narrativeResponse', engine.newGame()); - return { success: true, result: true, running: true, canLoad: slots.size > 0 }; - } - case 'chooseChoice': - case 'chooseChoice()': { - const engine = sessions.get(socket.id); - if (!engine?.isRunning()) { - return { success: false, error: 'game_not_running', result: false }; - } - const choiceIndex = Number(args[0]); - if (!Number.isInteger(choiceIndex)) { - return { success: false, error: 'invalid_choice', result: false }; - } - socket.emit('narrativeResponse', engine.chooseChoice(choiceIndex)); - return { success: true, result: true }; - } - case 'loadGame': - case 'loadGame()': { - const slot = normalizeSaveSlot(args[0]); - const browserSave = typeof args[1] === 'string' ? args[1] : null; - if (!browserSave && !slots.has(slot)) { - return { success: false, error: 'missing_save', result: false }; - } - const engine = getOrCreateEngine(socket.id); - socket.emit('narrativeResponse', engine.loadGame(browserSave || slots.get(slot))); - socket.emit('gameLoaded', { slot }); - return { success: true, result: true, running: true, slot }; - } - case 'resumeGame': - case 'resumeGame()': { - const browserSave = typeof args[0] === 'string' ? args[0] : null; - if (!browserSave) { - return { success: false, error: 'missing_state', result: false }; - } - const engine = new ink_engine_1.InkEngine(getStoryPath()); - engine.resumeGame(browserSave); - sessions.set(socket.id, engine); - return { success: true, result: true, running: engine.isRunning() }; - } - case 'exportGameState': - case 'exportGameState()': { - const engine = sessions.get(socket.id); - if (!engine?.isRunning()) { - return { success: false, error: 'game_not_running', result: false }; - } - return { success: true, result: true, savedState: engine.saveGame() }; - } - 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 savedState = engine.saveGame(); - slots.set(slot, savedState); - socket.emit('gameSaved', { slot }); - return { success: true, result: true, slot, savedState }; - } - 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}` }; - } -} -io.on('connection', (socket) => { - console.log(`[ink] Client connected: ${socket.id}`); - socket.emit('gameConfig', (0, game_config_1.clientGameConfig)(engineConfig)); - socket.on('gameApi', async (request, respond) => { - try { - const result = await handleGameApi(socket, String(request?.method ?? ''), Array.isArray(request?.args) ? request.args : []); - if (typeof respond === 'function') - respond(result); - } - catch (error) { - console.error('[ink] gameApi error:', error); - if (typeof respond === 'function') { - respond({ - success: false, - error: error instanceof Error ? error.message : String(error), - }); - } - } - }); - socket.on('disconnect', () => { - console.log(`[ink] Client disconnected: ${socket.id}`); - sessions.delete(socket.id); - saveSlots.delete(socket.id); - }); -}); -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'), - ]; - for (const dir of dirs) { - if (!(0, fs_1.existsSync)(dir)) - (0, fs_1.mkdirSync)(dir, { recursive: true }); - } - (0, game_config_1.ensureConfiguredAssetDirectories)(engineConfig); -} -function ensureKokoroJs() { - const source = path_1.default.join(__dirname, '../node_modules/kokoro-js/dist/index.js'); - const destination = path_1.default.join(__dirname, '../public/js/kokoro-js.js'); - if ((0, fs_1.existsSync)(source) && !(0, fs_1.existsSync)(destination)) { - (0, fs_1.copyFileSync)(source, destination); - } -} -async function startServer(initialPort, range) { - ensureDirectories(); - try { - ensureKokoroJs(); - } - catch { /* optional */ } - compileConfiguredStory(); - if (!(0, fs_1.existsSync)(getStoryPath())) { - console.error(`[ink] Story file missing: ${getStoryPath()}`); - console.error('[ink] Set INK_SOURCE_FILE or configure paths.inkSource in config/engines/ink.json.'); - } - let port = initialPort; - while (port < initialPort + range) { - try { - await new Promise((resolve, reject) => { - server.removeAllListeners('error'); - server.removeAllListeners('listening'); - server.once('listening', () => { - console.log(`[ink] Ink server running on http://localhost:${port}`); - resolve(); - }); - server.once('error', (error) => { - if (error.code === 'EADDRINUSE' || error.code === 'EACCES') { - console.log(`Port ${port} unavailable (${error.code}), trying ${port + 1}...`); - server.close(); - port++; - reject(); - } - else { - reject(error); - } - }); - server.listen(port); - }); - return; - } - catch { - if (port >= initialPort + range - 1) { - throw new Error(`Failed to start server on ports ${initialPort} to ${initialPort + range - 1}`); - } - } - } -} -if (require.main === module) { - startServer(PORT, PORT_RANGE).catch((error) => { - console.error('[ink] Failed to start:', error); - process.exit(1); - }); -} -//# sourceMappingURL=server-ink.js.map \ No newline at end of file diff --git a/dist/server-ink.js.map b/dist/server-ink.js.map deleted file mode 100644 index fa1170e..0000000 --- a/dist/server-ink.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"server-ink.js","sourceRoot":"","sources":["../src/server-ink.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/dist/server-yaml.d.ts b/dist/server-yaml.d.ts deleted file mode 100644 index 456d5fb..0000000 --- a/dist/server-yaml.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * AI Interactive Fiction - Web Server - * Serves the web UI and handles WebSocket communication - */ -import http from 'http'; -import { Server as SocketIOServer } from 'socket.io'; -declare const app: import("express-serve-static-core").Express; -declare const server: http.Server; -declare const io: SocketIOServer; -export declare function startServer(initialPort: number, range: number): Promise; -export { app, server, io }; diff --git a/dist/server-yaml.js b/dist/server-yaml.js deleted file mode 100644 index 4449148..0000000 --- a/dist/server-yaml.js +++ /dev/null @@ -1,308 +0,0 @@ -"use strict"; -/** - * AI Interactive Fiction - Web Server - * Serves the web UI and handles WebSocket communication - */ -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.io = exports.server = exports.app = void 0; -exports.startServer = startServer; -const path_1 = __importDefault(require("path")); -const express_1 = __importDefault(require("express")); -const http_1 = __importDefault(require("http")); -const socket_io_1 = require("socket.io"); -const dotenv = __importStar(require("dotenv")); -const game_runner_1 = require("./cli/game-runner"); -const fs_1 = require("fs"); -const turn_result_1 = require("./interfaces/turn-result"); -const game_config_1 = require("./config/game-config"); -// Load environment variables -dotenv.config(); -// Create Express application -const app = (0, express_1.default)(); -exports.app = app; -const server = http_1.default.createServer(app); -exports.server = server; -const io = new socket_io_1.Server(server); -exports.io = io; -// Get port from environment variables or use default -const DEFAULT_PORT = 3001; -const PORT = process.env.PORT ? parseInt(process.env.PORT) : DEFAULT_PORT; -const PORT_RANGE = 300; // Try enough ports to skip OS-excluded ranges. -const engineConfig = (0, game_config_1.loadGameConfig)(process.env.YAML_CONFIG_FILE || './config/engines/yaml.json', 'yaml'); -// Serve static files from the public directory. During local development the -// browser must not keep stale ES modules, otherwise UI fixes appear to do -// nothing until a hard cache clear. -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'); - } -})); -app.get('/api/game-config', (_req, res) => { - res.json((0, game_config_1.clientGameConfig)(engineConfig)); -}); -// Set up game sessions -const gameSessions = new Map(); -const nextTurnIds = new Map(); -function nextTurnId(socketId) { - const current = nextTurnIds.get(socketId) || 1; - nextTurnIds.set(socketId, current + 1); - return current; -} -function createTextTurn(socketId, text, gameState = {}, suggestions) { - const paragraphs = (0, turn_result_1.textToParagraphs)(text); - return { - turnId: nextTurnId(socketId), - paragraphs, - choices: [], - inputMode: 'text', - gameState, - suggestions, - }; -} -function normalizeSaveSlot(slot) { - const value = Number(slot); - return Number.isInteger(value) && value > 0 ? value : 1; -} -async function startDemoGameForSocket(socket) { - nextTurnIds.set(socket.id, 1); - const gameRunner = new game_runner_1.GameRunner(); - const worldFile = (0, game_config_1.projectPath)(process.env.DEFAULT_WORLD_FILE || engineConfig.paths.mainGameFile); - await gameRunner.initialize(worldFile); - gameSessions.set(socket.id, gameRunner); - const gameState = gameRunner.getGameState(); - const paragraphs = [ - ...(0, turn_result_1.textToParagraphs)(gameState.world.introduction), - ...(0, turn_result_1.textToParagraphs)(gameRunner.getCurrentRoomDescription()), - ]; - socket.emit('narrativeResponse', { - turnId: nextTurnId(socket.id), - paragraphs, - choices: [], - inputMode: 'text', - gameState: { - currentRoomId: gameState.currentRoomId, - }, - }); - return gameRunner; -} -async function handleGameApi(socket, method, args = []) { - const saveGames = socket.data.saveGames || new Map(); - socket.data.saveGames = saveGames; - switch (method) { - case 'newGame': - case 'newGame()': - await startDemoGameForSocket(socket); - return { success: true, result: true, running: true, canLoad: saveGames.size > 0 }; - case 'loadGame': - case 'loadGame()': { - const slot = normalizeSaveSlot(args[0]); - if (!saveGames.has(slot)) { - return { success: false, error: 'missing_save', result: false }; - } - await startDemoGameForSocket(socket); - socket.emit('gameLoaded', { slot }); - return { success: true, result: true, running: true, slot }; - } - case 'saveGame': - case 'saveGame()': { - const gameRunner = gameSessions.get(socket.id); - if (!gameRunner) { - return { success: false, error: 'game_not_running', result: false }; - } - const slot = normalizeSaveSlot(args[0]); - saveGames.set(slot, gameRunner.getGameState()); - socket.emit('gameSaved', { slot }); - return { success: true, result: true, slot }; - } - case 'hasSaveGame': - case 'hasSaveGame()': { - const slot = normalizeSaveSlot(args[0]); - return { success: true, result: saveGames.has(slot), slot }; - } - case 'getSaveGames': - case 'getSaveGames()': - return { success: true, result: Array.from(saveGames.keys()).sort((a, b) => a - b) }; - case 'isGameRunning': - case 'isGameRunning()': - return { success: true, result: gameSessions.has(socket.id) }; - default: - return { success: false, error: `unknown_method:${method}` }; - } -} -// Handle socket connections -io.on('connection', (socket) => { - console.log(`New client connected: ${socket.id}`); - socket.emit('gameConfig', (0, game_config_1.clientGameConfig)(engineConfig)); - socket.data.saveGames = new Map(); - socket.on('gameApi', async (request, respond) => { - try { - const response = await handleGameApi(socket, String(request?.method || ''), Array.isArray(request?.args) ? request.args : []); - if (typeof respond === 'function') { - respond(response); - } - } - catch (error) { - console.error('Game API error:', error); - if (typeof respond === 'function') { - respond({ success: false, error: error instanceof Error ? error.message : String(error) }); - } - } - }); - // Process player command - socket.on('playerCommand', async (data) => { - try { - const gameRunner = gameSessions.get(socket.id); - if (!gameRunner) { - socket.emit('error', { message: 'Game session not found. Please start a new game.' }); - return; - } - const command = String(data?.command || '').trim(); - // During typography and animation work, mirror the command back through - // the real socket path so the UI pipeline can be tested end to end. - socket.emit('narrativeResponse', createTextTurn(socket.id, command, { - currentRoomId: gameRunner.getGameState().currentRoomId - }, gameRunner.getSuggestions())); - } - catch (error) { - console.error('Error processing command:', error); - socket.emit('error', { message: 'Failed to process command. Please try again.' }); - } - }); - // Handle disconnection - socket.on('disconnect', () => { - console.log(`Client disconnected: ${socket.id}`); - // Clean up game session - if (gameSessions.has(socket.id)) { - gameSessions.delete(socket.id); - } - nextTurnIds.delete(socket.id); - }); -}); -// Ensure required asset folders exist -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') - ]; - for (const dir of dirs) { - if (!(0, fs_1.existsSync)(dir)) { - (0, fs_1.mkdirSync)(dir, { recursive: true }); - } - } - (0, game_config_1.ensureConfiguredAssetDirectories)(engineConfig); -} -// Copy kokoro-js library from node_modules if not already present -function ensureKokoroJs() { - const source = path_1.default.join(__dirname, '../node_modules/kokoro-js/dist/index.js'); - const destination = path_1.default.join(__dirname, '../public/js/kokoro-js.js'); - if ((0, fs_1.existsSync)(source) && !(0, fs_1.existsSync)(destination)) { - (0, fs_1.copyFileSync)(source, destination); - console.log(`Copied kokoro-js from ${source} to ${destination}`); - } -} -// Start the server with port fallback -async function startServer(initialPort, range) { - let currentPort = initialPort; - const maxPort = initialPort + range; - // Try ports in the specified range - while (currentPort < maxPort) { - try { - // Ensure directories exist - ensureDirectories(); - // Ensure kokoro-js is copied - try { - ensureKokoroJs(); - } - catch (error) { - console.error('Error copying kokoro-js:', error); - } - // Try to start the server on the current port - await new Promise((resolve, reject) => { - server.removeAllListeners('error'); - server.removeAllListeners('listening'); - server.once('listening', () => { - console.log(`AI Interactive Fiction web server running on http://localhost:${currentPort}`); - resolve(); - }); - server.once('error', (error) => { - // If port is in use, try next port - if (error.code === 'EADDRINUSE' || error.code === 'EACCES') { - console.log(`Port ${currentPort} is unavailable (${error.code}), trying next port...`); - server.close(); - currentPort++; - reject(); - } - else { - // For other errors, log and reject - console.error('Server error:', error); - reject(error); - } - }); - server.listen(currentPort); - }); - // If we reach here, server started successfully - return; - } - catch (error) { - // If we reach the max port and still fail, throw an error - if (currentPort >= maxPort - 1) { - throw new Error(`Failed to start server on ports ${initialPort} to ${maxPort - 1}`); - } - // Otherwise try the next port - // The loop continues as the rejection above increments currentPort - } - } -} -// Start the server when this module is run directly -if (require.main === module) { - startServer(PORT, PORT_RANGE).catch(error => { - console.error('Failed to start server:', error); - process.exit(1); - }); -} -//# sourceMappingURL=server-yaml.js.map \ No newline at end of file diff --git a/dist/server-yaml.js.map b/dist/server-yaml.js.map deleted file mode 100644 index 799345e..0000000 --- a/dist/server-yaml.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"server-yaml.js","sourceRoot":"","sources":["../src/server-yaml.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/dist/server-zcode.d.ts b/dist/server-zcode.d.ts deleted file mode 100644 index 08f653c..0000000 --- a/dist/server-zcode.d.ts +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Z-code LLM Server - * - * Starts an Express + Socket.IO server that runs Zork I through the - * ZcodeLlmEngine and serves the same shared client UI as the YAML engine. - * - * Usage: - * npm run dev:zcode (development, with file watching) - * npm run start:zcode (production, from compiled dist/) - * - * Environment variables: - * PORT – HTTP port (default: 3002) - * ZCODE_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-zcode.js b/dist/server-zcode.js deleted file mode 100644 index dcb6e97..0000000 --- a/dist/server-zcode.js +++ /dev/null @@ -1,353 +0,0 @@ -"use strict"; -/** - * Z-code LLM Server - * - * Starts an Express + Socket.IO server that runs Zork I through the - * ZcodeLlmEngine and serves the same shared client UI as the YAML engine. - * - * Usage: - * npm run dev:zcode (development, with file watching) - * npm run start:zcode (production, from compiled dist/) - * - * Environment variables: - * PORT – HTTP port (default: 3002) - * ZCODE_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 zcode_llm_engine_1 = require("./engine/zcode-llm-engine"); -const game_config_1 = require("./config/game-config"); -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 = 300; -const DEBUG_ENABLED = /^(1|true|yes|on)$/i.test(process.env.ZCODE_DEBUG ?? ''); -const engineConfig = (0, game_config_1.loadGameConfig)(process.env.ZCODE_CONFIG_FILE || './config/engines/zcode.json', 'zcode'); -function debugLog(message, details) { - if (!DEBUG_ENABLED) - return; - if (typeof details === 'undefined') { - console.log(`[zcode:debug] ${message}`); - return; - } - console.log(`[zcode: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'); - }, -})); -app.get('/api/game-config', (_req, res) => { - res.json((0, game_config_1.clientGameConfig)(engineConfig)); -}); -// One engine instance per connected socket -const sessions = new Map(); -// Save-game slot maps: socketId → Map -const saveSlots = new Map(); -function toClientTurn(turn) { - return { - ...turn, - gameState: { - ...turn.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 zcode_llm_engine_1.ZcodeLlmEngine({ - storyPath: (0, game_config_1.projectPath)(process.env.ZCODE_STORY_FILE || engineConfig.paths.mainGameFile), - promptDir: (0, game_config_1.projectPath)(engineConfig.paths.promptDir || 'data/zcode-prompts'), - }); - 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', toClientTurn(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', toClientTurn(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 = (0, game_config_1.projectPath)(process.env.ZCODE_STORY_FILE ?? engineConfig.paths.mainGameFile); - const promptDir = (0, game_config_1.projectPath)(engineConfig.paths.promptDir || 'data/zcode-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('[zcode] Missing OPENROUTER_API_KEY in environment.'); - } - if (!process.env.OPENROUTER_MODEL) { - console.error('[zcode] Missing OPENROUTER_MODEL in environment.'); - } - if (!(0, fs_1.existsSync)(storyPath)) { - console.error(`[zcode] Story file missing: ${storyPath}`); - console.error('[zcode] Place zork1.bin in ./data/z-code/ or set ZCODE_STORY_FILE.'); - } - if (missingPrompts.length > 0) { - console.error('[zcode] 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(`[zcode] Client connected: ${socket.id}`); - socket.emit('gameConfig', (0, game_config_1.clientGameConfig)(engineConfig)); - 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('[zcode] 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', toClientTurn(turn)); - } - catch (error) { - console.error('[zcode] playerCommand error:', error); - socket.emit('error', { - message: error instanceof Error ? error.message : 'An error occurred.', - }); - } - }); - socket.on('disconnect', () => { - console.log(`[zcode] 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/zcode-prompts'), - ]; - for (const dir of dirs) { - if (!(0, fs_1.existsSync)(dir)) - (0, fs_1.mkdirSync)(dir, { recursive: true }); - } - (0, game_config_1.ensureConfiguredAssetDirectories)(engineConfig); -} -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.removeAllListeners('error'); - server.removeAllListeners('listening'); - server.once('listening', () => { - console.log(`[zcode] Z-code Narrator server running on http://localhost:${port}`); - resolve(); - }); - server.once('error', (err) => { - if (err.code === 'EADDRINUSE' || err.code === 'EACCES') { - console.log(`Port ${port} unavailable (${err.code}), trying ${port + 1}...`); - server.close(); - port++; - reject(); - } - else { - reject(err); - } - }); - server.listen(port); - }); - 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('[zcode] Failed to start:', err); - process.exit(1); - }); -} -//# sourceMappingURL=server-zcode.js.map \ No newline at end of file diff --git a/dist/server-zcode.js.map b/dist/server-zcode.js.map deleted file mode 100644 index 1acfcb4..0000000 --- a/dist/server-zcode.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"server-zcode.js","sourceRoot":"","sources":["../src/server-zcode.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/dist/test-server-yaml.d.ts b/dist/test-server-yaml.d.ts deleted file mode 100644 index 67ec3a8..0000000 --- a/dist/test-server-yaml.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -/** - * Test Server for AI Interactive Fiction - * Simplified version that sends test paragraphs instead of using LLM - */ -import http from 'http'; -import { Server as SocketIOServer } from 'socket.io'; -declare const app: import("express-serve-static-core").Express; -declare const server: http.Server; -declare const io: SocketIOServer; -export { app, server, io }; diff --git a/dist/test-server-yaml.js b/dist/test-server-yaml.js deleted file mode 100644 index 80d71aa..0000000 --- a/dist/test-server-yaml.js +++ /dev/null @@ -1,282 +0,0 @@ -"use strict"; -/** - * Test Server for AI Interactive Fiction - * Simplified version that sends test paragraphs instead of using LLM - */ -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.io = exports.server = exports.app = void 0; -const path_1 = __importDefault(require("path")); -const express_1 = __importDefault(require("express")); -const http_1 = __importDefault(require("http")); -const socket_io_1 = require("socket.io"); -const dotenv = __importStar(require("dotenv")); -const fs_1 = require("fs"); -const turn_result_1 = require("./interfaces/turn-result"); -// Load environment variables -dotenv.config(); -// Create Express application -const app = (0, express_1.default)(); -exports.app = app; -const server = http_1.default.createServer(app); -exports.server = server; -const io = new socket_io_1.Server(server); -exports.io = io; -// Get port from environment variables or use default -const DEFAULT_PORT = 3001; -const PORT = process.env.PORT ? parseInt(process.env.PORT) : DEFAULT_PORT; -const PORT_RANGE = 300; // Try enough ports to skip OS-excluded ranges. -// Serve static files from the public directory. Keep browser modules uncached -// during local development so fixes are visible without a hard cache clear. -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'); - } -})); -// Test paragraphs to send to the client -const TEST_PARAGRAPHS = [ - "You stand at the entrance of a mysterious cave. The air is cool and damp, carrying the scent of earth and ancient stone. Shadows dance on the walls as your torch flickers in the gentle breeze.", - "As you venture deeper, the passage narrows. Stalactites hang from the ceiling like stone daggers, their surfaces glistening with moisture. The sound of dripping water echoes through the silence.", - "Suddenly, the passage opens into a vast chamber. Crystal formations catch the light of your torch, sending rainbow reflections across the walls. In the center of the room stands an ancient stone pedestal, its surface carved with symbols from a forgotten language." -]; -// Handle socket connections -io.on('connection', (socket) => { - console.log(`New client connected: ${socket.id}`); - let currentParagraphIndex = 0; - let gameRunning = false; - let nextTurnId = 1; - const saveGames = new Set(); - const startDemoGame = () => { - gameRunning = true; - nextTurnId = 1; - currentParagraphIndex = 0; - socket.emit('narrativeResponse', { - turnId: nextTurnId++, - paragraphs: [ - ...(0, turn_result_1.textToParagraphs)("#chapter[Interactive Fiction Test]\n\nWelcome to the Interactive Fiction Test. This is a simplified version that sends predefined paragraphs instead of using an LLM."), - ...(0, turn_result_1.textToParagraphs)(TEST_PARAGRAPHS[0]), - ], - choices: [], - inputMode: 'text', - gameState: { - currentRoomId: 'test-room', - }, - }); - }; - const normalizeSaveSlot = (slot) => { - const value = Number(slot); - return Number.isInteger(value) && value > 0 ? value : 1; - }; - socket.on('gameApi', (request, respond) => { - try { - const method = String(request?.method || ''); - const args = Array.isArray(request?.args) ? request.args : []; - let response; - switch (method) { - case 'newGame': - case 'newGame()': - startDemoGame(); - response = { success: true, result: true, running: true, canLoad: saveGames.size > 0 }; - break; - case 'loadGame': - case 'loadGame()': { - const slot = normalizeSaveSlot(args[0]); - if (!saveGames.has(slot)) { - response = { success: false, error: 'missing_save', result: false }; - break; - } - startDemoGame(); - socket.emit('gameLoaded', { slot }); - response = { success: true, result: true, running: true, slot }; - break; - } - case 'saveGame': - case 'saveGame()': { - if (!gameRunning) { - response = { success: false, error: 'game_not_running', result: false }; - break; - } - const slot = normalizeSaveSlot(args[0]); - saveGames.add(slot); - socket.emit('gameSaved', { slot }); - response = { success: true, result: true, slot }; - break; - } - case 'hasSaveGame': - case 'hasSaveGame()': { - const slot = normalizeSaveSlot(args[0]); - response = { success: true, result: saveGames.has(slot), slot }; - break; - } - case 'getSaveGames': - case 'getSaveGames()': - response = { success: true, result: Array.from(saveGames).sort((a, b) => a - b) }; - break; - case 'isGameRunning': - case 'isGameRunning()': - response = { success: true, result: gameRunning }; - break; - default: - response = { success: false, error: `unknown_method:${method}` }; - } - if (typeof respond === 'function') - respond(response); - } - catch (error) { - if (typeof respond === 'function') { - respond({ success: false, error: error instanceof Error ? error.message : String(error) }); - } - } - }); - // Process player command - socket.on('playerCommand', async (data) => { - try { - console.log(`Received command: ${data.command}`); - // Send narrative response to client - socket.emit('narrativeResponse', { - turnId: nextTurnId++, - paragraphs: (0, turn_result_1.textToParagraphs)(String(data.command || '')), - choices: [], - inputMode: 'text', - gameState: { - currentRoomId: "test-room" - }, - suggestions: ["look around", "examine pedestal", "touch crystals"] - }); - } - catch (error) { - console.error('Error processing command:', error); - socket.emit('error', { message: 'Failed to process command. Please try again.' }); - } - }); - // Handle disconnection - socket.on('disconnect', () => { - console.log(`Client disconnected: ${socket.id}`); - }); -}); -// Ensure required asset folders exist -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') - ]; - for (const dir of dirs) { - if (!(0, fs_1.existsSync)(dir)) { - (0, fs_1.mkdirSync)(dir, { recursive: true }); - } - } -} -// Copy kokoro-js library from node_modules if not already present -function ensureKokoroJs() { - const source = path_1.default.join(__dirname, '../node_modules/kokoro-js/dist/index.js'); - const destination = path_1.default.join(__dirname, '../public/js/kokoro-js.js'); - if ((0, fs_1.existsSync)(source) && !(0, fs_1.existsSync)(destination)) { - (0, fs_1.copyFileSync)(source, destination); - console.log(`Copied kokoro-js from ${source} to ${destination}`); - } -} -// Start the server with port fallback -async function startServer(initialPort, range) { - let currentPort = initialPort; - const maxPort = initialPort + range; - // Try ports in the specified range - while (currentPort < maxPort) { - try { - // Ensure directories exist - ensureDirectories(); - // Ensure kokoro-js is copied - try { - ensureKokoroJs(); - } - catch (error) { - console.error('Error copying kokoro-js:', error); - } - // Try to start the server on the current port - await new Promise((resolve, reject) => { - server.removeAllListeners('error'); - server.removeAllListeners('listening'); - server.once('listening', () => { - console.log(`AI Interactive Fiction TEST SERVER running on http://localhost:${currentPort}`); - console.log('This server is sending predefined test paragraphs instead of using an LLM'); - resolve(); - }); - server.once('error', (error) => { - // If port is in use, try next port - if (error.code === 'EADDRINUSE' || error.code === 'EACCES') { - console.log(`Port ${currentPort} is unavailable (${error.code}), trying next port...`); - server.close(); - currentPort++; - reject(); - } - else { - // For other errors, log and reject - console.error('Server error:', error); - reject(error); - } - }); - server.listen(currentPort); - }); - // If we reach here, server started successfully - return; - } - catch (error) { - // If we reach the max port and still fail, throw an error - if (currentPort >= maxPort - 1) { - throw new Error(`Failed to start server on ports ${initialPort} to ${maxPort - 1}`); - } - // Otherwise try the next port - } - } -} -// Start the server when this module is run directly -if (require.main === module) { - startServer(PORT, PORT_RANGE).catch(error => { - console.error('Failed to start server:', error); - process.exit(1); - }); -} -//# sourceMappingURL=test-server-yaml.js.map \ No newline at end of file diff --git a/dist/test-server-yaml.js.map b/dist/test-server-yaml.js.map deleted file mode 100644 index 155e173..0000000 --- a/dist/test-server-yaml.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"test-server-yaml.js","sourceRoot":"","sources":["../src/test-server-yaml.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/dist/utils/tag-parser.d.ts b/dist/utils/tag-parser.d.ts deleted file mode 100644 index 9db6f8d..0000000 --- a/dist/utils/tag-parser.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -import type { StoryTag } from '../interfaces/turn-result'; -export declare function parseTag(raw: string): StoryTag | null; -export declare function parseTags(rawTags: unknown[] | undefined): StoryTag[]; -export declare function getTagValue(tags: StoryTag[], key: string): string | undefined; diff --git a/dist/utils/tag-parser.js b/dist/utils/tag-parser.js deleted file mode 100644 index 50e6ab7..0000000 --- a/dist/utils/tag-parser.js +++ /dev/null @@ -1,53 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.parseTag = parseTag; -exports.parseTags = parseTags; -exports.getTagValue = getTagValue; -const LEGACY_TAG_ALIASES = { - audio: 'sfx', - audioloop: 'music', - separator: 'section', -}; -function normalizeKey(key) { - const normalized = key.trim().toLowerCase().replace(/[^a-z0-9_-]+/g, '-'); - return LEGACY_TAG_ALIASES[normalized] || normalized; -} -function parseTag(raw) { - const text = String(raw || '').trim().replace(/^#\s*/, ''); - if (!text) - return null; - const bracketMatch = text.match(/^([A-Za-z][\w-]*)(?:\[([^\]]*)\])?(?:\(([^)]*)\))?$/); - if (bracketMatch) { - const tag = { key: normalizeKey(bracketMatch[1]) }; - if (typeof bracketMatch[2] !== 'undefined') - tag.value = bracketMatch[2].trim(); - if (typeof bracketMatch[3] !== 'undefined') - tag.param = bracketMatch[3].trim(); - return tag; - } - const colonMatch = text.match(/^([A-Za-z][\w-]*)\s*:\s*(.*?)\s*(?:\(([^)]*)\))?$/); - if (colonMatch) { - const tag = { key: normalizeKey(colonMatch[1]) }; - tag.value = colonMatch[2].trim(); - if (typeof colonMatch[3] !== 'undefined') - tag.param = colonMatch[3].trim(); - return tag; - } - const bareMatch = text.match(/^[A-Za-z][\w-]*$/); - if (bareMatch) { - return { key: normalizeKey(text) }; - } - return null; -} -function parseTags(rawTags) { - if (!Array.isArray(rawTags)) - return []; - return rawTags - .map((raw) => parseTag(String(raw ?? ''))) - .filter((tag) => Boolean(tag)); -} -function getTagValue(tags, key) { - const normalizedKey = normalizeKey(key); - return tags.find((tag) => tag.key === normalizedKey)?.value; -} -//# sourceMappingURL=tag-parser.js.map \ No newline at end of file diff --git a/dist/utils/tag-parser.js.map b/dist/utils/tag-parser.js.map deleted file mode 100644 index 6115676..0000000 --- a/dist/utils/tag-parser.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"tag-parser.js","sourceRoot":"","sources":["../../src/utils/tag-parser.ts"],"names":[],"mappings":";;AAaA,4BA0BC;AAED,8BAMC;AAED,kCAGC;AAlDD,MAAM,kBAAkB,GAA2B;IACjD,KAAK,EAAE,KAAK;IACZ,SAAS,EAAE,OAAO;IAClB,SAAS,EAAE,SAAS;CACrB,CAAC;AAEF,SAAS,YAAY,CAAC,GAAW;IAC/B,MAAM,UAAU,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,eAAe,EAAE,GAAG,CAAC,CAAC;IAC1E,OAAO,kBAAkB,CAAC,UAAU,CAAC,IAAI,UAAU,CAAC;AACtD,CAAC;AAED,SAAgB,QAAQ,CAAC,GAAW;IAClC,MAAM,IAAI,GAAG,MAAM,CAAC,GAAG,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;IAC3D,IAAI,CAAC,IAAI;QAAE,OAAO,IAAI,CAAC;IAEvB,MAAM,YAAY,GAAG,IAAI,CAAC,KAAK,CAAC,qDAAqD,CAAC,CAAC;IACvF,IAAI,YAAY,EAAE,CAAC;QACjB,MAAM,GAAG,GAAa,EAAE,GAAG,EAAE,YAAY,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QAC7D,IAAI,OAAO,YAAY,CAAC,CAAC,CAAC,KAAK,WAAW;YAAE,GAAG,CAAC,KAAK,GAAG,YAAY,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;QAC/E,IAAI,OAAO,YAAY,CAAC,CAAC,CAAC,KAAK,WAAW;YAAE,GAAG,CAAC,KAAK,GAAG,YAAY,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;QAC/E,OAAO,GAAG,CAAC;IACb,CAAC;IAED,MAAM,UAAU,GAAG,IAAI,CAAC,KAAK,CAAC,mDAAmD,CAAC,CAAC;IACnF,IAAI,UAAU,EAAE,CAAC;QACf,MAAM,GAAG,GAAa,EAAE,GAAG,EAAE,YAAY,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QAC3D,GAAG,CAAC,KAAK,GAAG,UAAU,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;QACjC,IAAI,OAAO,UAAU,CAAC,CAAC,CAAC,KAAK,WAAW;YAAE,GAAG,CAAC,KAAK,GAAG,UAAU,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;QAC3E,OAAO,GAAG,CAAC;IACb,CAAC;IAED,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,CAAC,kBAAkB,CAAC,CAAC;IACjD,IAAI,SAAS,EAAE,CAAC;QACd,OAAO,EAAE,GAAG,EAAE,YAAY,CAAC,IAAI,CAAC,EAAE,CAAC;IACrC,CAAC;IAED,OAAO,IAAI,CAAC;AACd,CAAC;AAED,SAAgB,SAAS,CAAC,OAA8B;IACtD,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC;QAAE,OAAO,EAAE,CAAC;IAEvC,OAAO,OAAO;SACX,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,QAAQ,CAAC,MAAM,CAAC,GAAG,IAAI,EAAE,CAAC,CAAC,CAAC;SACzC,MAAM,CAAC,CAAC,GAAG,EAAmB,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC;AACpD,CAAC;AAED,SAAgB,WAAW,CAAC,IAAgB,EAAE,GAAW;IACvD,MAAM,aAAa,GAAG,YAAY,CAAC,GAAG,CAAC,CAAC;IACxC,OAAO,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,GAAG,KAAK,aAAa,CAAC,EAAE,KAAK,CAAC;AAC9D,CAAC"} \ No newline at end of file diff --git a/dist/world-model/yaml-parser.d.ts b/dist/world-model/yaml-parser.d.ts deleted file mode 100644 index 91b5f1f..0000000 --- a/dist/world-model/yaml-parser.d.ts +++ /dev/null @@ -1,71 +0,0 @@ -/** - * YAML World Model Parser - * Loads and validates world definitions from YAML files - */ -import { WorldModel } from '../interfaces/world-model'; -export declare class YamlWorldParser { - /** - * Load a world model from a YAML file - */ - static loadFromFile(filePath: string): Promise; - /** - * Validate the loaded YAML data and transform it into a WorldModel - */ - private static validateAndTransform; - /** - * Validate that an object has all required fields - */ - private static validateRequiredFields; - /** - * Validate that a value is a string - */ - private static validateString; - /** - * Validate room definitions - */ - private static validateRooms; - /** - * Validate exit definitions - */ - private static validateExits; - /** - * Validate object definitions - */ - private static validateObjects; - /** - * Validate character definitions - */ - private static validateCharacters; - /** - * Validate action definitions - */ - private static validateActions; - /** - * Validate initial game state - */ - private static validateInitialState; - /** - * Validate object states (record of boolean values) - */ - private static validateObjectStates; - /** - * Validate dialogue (record of string values) - */ - private static validateDialogue; - /** - * Validate flags (record of boolean values) - */ - private static validateFlags; - /** - * Validate counters (record of number values) - */ - private static validateCounters; - /** - * Validate that an array of strings is valid - */ - private static validateStringArray; - /** - * Validate references between entities - */ - private static validateReferences; -} diff --git a/dist/world-model/yaml-parser.js b/dist/world-model/yaml-parser.js deleted file mode 100644 index c2bf6ac..0000000 --- a/dist/world-model/yaml-parser.js +++ /dev/null @@ -1,399 +0,0 @@ -"use strict"; -/** - * YAML World Model Parser - * Loads and validates world definitions from YAML files - */ -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; - }; -})(); -Object.defineProperty(exports, "__esModule", { value: true }); -exports.YamlWorldParser = void 0; -const fs = __importStar(require("fs/promises")); -const yaml = __importStar(require("js-yaml")); -class YamlWorldParser { - /** - * Load a world model from a YAML file - */ - static async loadFromFile(filePath) { - try { - const fileContents = await fs.readFile(filePath, 'utf8'); - const worldData = yaml.load(fileContents); - return this.validateAndTransform(worldData); - } - catch (error) { - console.error(`Error loading world from ${filePath}:`, error); - throw new Error(`Failed to load world from ${filePath}: ${error instanceof Error ? error.message : String(error)}`); - } - } - /** - * Validate the loaded YAML data and transform it into a WorldModel - */ - static validateAndTransform(data) { - if (!data || typeof data !== 'object') { - throw new Error('Invalid world data: must be an object'); - } - const worldData = data; - // Validate required top-level fields - this.validateRequiredFields(worldData, ['title', 'author', 'version', 'introduction', 'rooms', 'initialState']); - // Transform and validate the world model - const worldModel = { - title: this.validateString(worldData.title, 'title'), - author: this.validateString(worldData.author, 'author'), - version: this.validateString(worldData.version, 'version'), - introduction: this.validateString(worldData.introduction, 'introduction'), - rooms: this.validateRooms(worldData.rooms), - objects: this.validateObjects(worldData.objects), - characters: this.validateCharacters(worldData.characters), - actions: this.validateActions(worldData.actions), - initialState: this.validateInitialState(worldData.initialState) - }; - // Validate references between entities - this.validateReferences(worldModel); - return worldModel; - } - /** - * Validate that an object has all required fields - */ - static validateRequiredFields(data, requiredFields) { - for (const field of requiredFields) { - if (!(field in data)) { - throw new Error(`Missing required field: ${field}`); - } - } - } - /** - * Validate that a value is a string - */ - static validateString(value, fieldName) { - if (typeof value !== 'string') { - throw new Error(`Field ${fieldName} must be a string`); - } - return value; - } - /** - * Validate room definitions - */ - static validateRooms(rooms) { - if (!rooms || typeof rooms !== 'object') { - throw new Error('Rooms must be an object mapping room IDs to room definitions'); - } - const roomsData = rooms; - const validatedRooms = {}; - for (const [roomId, roomData] of Object.entries(roomsData)) { - if (!roomData || typeof roomData !== 'object') { - throw new Error(`Room ${roomId} must be an object`); - } - const room = roomData; - this.validateRequiredFields(room, ['name', 'description', 'exits']); - validatedRooms[roomId] = { - id: roomId, - name: this.validateString(room.name, `rooms.${roomId}.name`), - description: this.validateString(room.description, `rooms.${roomId}.description`), - exits: this.validateExits(room.exits, roomId), - objects: this.validateStringArray(room.objects || [], `rooms.${roomId}.objects`), - characters: this.validateStringArray(room.characters || [], `rooms.${roomId}.characters`) - }; - } - return validatedRooms; - } - /** - * Validate exit definitions - */ - static validateExits(exits, roomId) { - if (!Array.isArray(exits)) { - throw new Error(`Exits for room ${roomId} must be an array`); - } - return exits.map((exit, index) => { - if (!exit || typeof exit !== 'object') { - throw new Error(`Exit ${index} in room ${roomId} must be an object`); - } - const exitData = exit; - this.validateRequiredFields(exitData, ['direction', 'targetRoomId']); - return { - direction: this.validateString(exitData.direction, `rooms.${roomId}.exits[${index}].direction`), - targetRoomId: this.validateString(exitData.targetRoomId, `rooms.${roomId}.exits[${index}].targetRoomId`), - description: exitData.description ? this.validateString(exitData.description, `rooms.${roomId}.exits[${index}].description`) : undefined, - isLocked: typeof exitData.isLocked === 'boolean' ? exitData.isLocked : false, - keyId: exitData.keyId ? this.validateString(exitData.keyId, `rooms.${roomId}.exits[${index}].keyId`) : undefined - }; - }); - } - /** - * Validate object definitions - */ - static validateObjects(objects) { - if (!objects) - return {}; // Objects are optional - if (typeof objects !== 'object') { - throw new Error('Objects must be an object mapping object IDs to object definitions'); - } - const objectsData = objects; - const validatedObjects = {}; - for (const [objectId, objectData] of Object.entries(objectsData)) { - if (!objectData || typeof objectData !== 'object') { - throw new Error(`Object ${objectId} must be an object`); - } - const obj = objectData; - this.validateRequiredFields(obj, ['name', 'description', 'traits', 'allowedActions']); - validatedObjects[objectId] = { - id: objectId, - name: this.validateString(obj.name, `objects.${objectId}.name`), - description: this.validateString(obj.description, `objects.${objectId}.description`), - traits: this.validateStringArray(obj.traits, `objects.${objectId}.traits`), - states: this.validateObjectStates(obj.states, objectId), - allowedActions: this.validateStringArray(obj.allowedActions, `objects.${objectId}.allowedActions`), - containedObjects: obj.containedObjects ? this.validateStringArray(obj.containedObjects, `objects.${objectId}.containedObjects`) : [] - }; - } - return validatedObjects; - } - /** - * Validate character definitions - */ - static validateCharacters(characters) { - if (!characters) - return {}; // Characters are optional - if (typeof characters !== 'object') { - throw new Error('Characters must be an object mapping character IDs to character definitions'); - } - const charactersData = characters; - const validatedCharacters = {}; - for (const [characterId, characterData] of Object.entries(charactersData)) { - if (!characterData || typeof characterData !== 'object') { - throw new Error(`Character ${characterId} must be an object`); - } - const character = characterData; - this.validateRequiredFields(character, ['name', 'description', 'dialogue', 'defaultResponse']); - validatedCharacters[characterId] = { - id: characterId, - name: this.validateString(character.name, `characters.${characterId}.name`), - description: this.validateString(character.description, `characters.${characterId}.description`), - dialogue: this.validateDialogue(character.dialogue, characterId), - inventory: this.validateStringArray(character.inventory || [], `characters.${characterId}.inventory`), - defaultResponse: this.validateString(character.defaultResponse, `characters.${characterId}.defaultResponse`), - mood: character.mood ? this.validateString(character.mood, `characters.${characterId}.mood`) : undefined - }; - } - return validatedCharacters; - } - /** - * Validate action definitions - */ - static validateActions(actions) { - if (!actions) - return {}; // Actions are optional - if (typeof actions !== 'object') { - throw new Error('Actions must be an object mapping action names to action definitions'); - } - const actionsData = actions; - const validatedActions = {}; - for (const [actionName, actionData] of Object.entries(actionsData)) { - if (!actionData || typeof actionData !== 'object') { - throw new Error(`Action ${actionName} must be an object`); - } - const action = actionData; - this.validateRequiredFields(action, ['patterns', 'handler']); - validatedActions[actionName] = { - name: actionName, - patterns: this.validateStringArray(action.patterns, `actions.${actionName}.patterns`), - requiresObject: typeof action.requiresObject === 'boolean' ? action.requiresObject : false, - requiresTarget: typeof action.requiresTarget === 'boolean' ? action.requiresTarget : false, - handler: this.validateString(action.handler, `actions.${actionName}.handler`) - }; - } - return validatedActions; - } - /** - * Validate initial game state - */ - static validateInitialState(initialState) { - if (!initialState || typeof initialState !== 'object') { - throw new Error('Initial state must be an object'); - } - const stateData = initialState; - this.validateRequiredFields(stateData, ['currentRoomId']); - return { - currentRoomId: this.validateString(stateData.currentRoomId, 'initialState.currentRoomId'), - inventory: this.validateStringArray(stateData.inventory || [], 'initialState.inventory'), - visitedRooms: this.validateStringArray(stateData.visitedRooms || [], 'initialState.visitedRooms'), - flags: this.validateFlags(stateData.flags), - counters: this.validateCounters(stateData.counters) - }; - } - /** - * Validate object states (record of boolean values) - */ - static validateObjectStates(states, objectId) { - if (!states) - return {}; - if (typeof states !== 'object') { - throw new Error(`States for object ${objectId} must be an object`); - } - const statesData = states; - const validatedStates = {}; - for (const [stateName, stateValue] of Object.entries(statesData)) { - if (typeof stateValue !== 'boolean') { - throw new Error(`State ${stateName} for object ${objectId} must be a boolean value`); - } - validatedStates[stateName] = stateValue; - } - return validatedStates; - } - /** - * Validate dialogue (record of string values) - */ - static validateDialogue(dialogue, characterId) { - if (!dialogue || typeof dialogue !== 'object') { - throw new Error(`Dialogue for character ${characterId} must be an object`); - } - const dialogueData = dialogue; - const validatedDialogue = {}; - for (const [topic, response] of Object.entries(dialogueData)) { - validatedDialogue[topic] = this.validateString(response, `characters.${characterId}.dialogue.${topic}`); - } - return validatedDialogue; - } - /** - * Validate flags (record of boolean values) - */ - static validateFlags(flags) { - if (!flags) - return {}; - if (typeof flags !== 'object') { - throw new Error('Flags must be an object'); - } - const flagsData = flags; - const validatedFlags = {}; - for (const [flagName, flagValue] of Object.entries(flagsData)) { - if (typeof flagValue !== 'boolean') { - throw new Error(`Flag ${flagName} must be a boolean value`); - } - validatedFlags[flagName] = flagValue; - } - return validatedFlags; - } - /** - * Validate counters (record of number values) - */ - static validateCounters(counters) { - if (!counters) - return {}; - if (typeof counters !== 'object') { - throw new Error('Counters must be an object'); - } - const countersData = counters; - const validatedCounters = {}; - for (const [counterName, counterValue] of Object.entries(countersData)) { - if (typeof counterValue !== 'number') { - throw new Error(`Counter ${counterName} must be a numeric value`); - } - validatedCounters[counterName] = counterValue; - } - return validatedCounters; - } - /** - * Validate that an array of strings is valid - */ - static validateStringArray(arr, fieldName) { - if (!arr) - return []; - if (!Array.isArray(arr)) { - throw new Error(`Field ${fieldName} must be an array`); - } - return arr.map((item, index) => { - if (typeof item !== 'string') { - throw new Error(`Item at index ${index} in ${fieldName} must be a string`); - } - return item; - }); - } - /** - * Validate references between entities - */ - static validateReferences(worldModel) { - const { rooms, objects, characters, initialState } = worldModel; - // Check that the initial room exists - if (!rooms[initialState.currentRoomId]) { - throw new Error(`Initial room ${initialState.currentRoomId} does not exist`); - } - // Check room exits - for (const [roomId, room] of Object.entries(rooms)) { - for (const exit of room.exits) { - if (!rooms[exit.targetRoomId]) { - throw new Error(`Room ${roomId} has an exit to non-existent room ${exit.targetRoomId}`); - } - if (exit.keyId && !objects[exit.keyId]) { - throw new Error(`Room ${roomId} has an exit requiring non-existent key ${exit.keyId}`); - } - } - // Check room objects - for (const objectId of room.objects) { - if (!objects[objectId]) { - throw new Error(`Room ${roomId} contains non-existent object ${objectId}`); - } - } - // Check room characters - for (const characterId of room.characters) { - if (!characters[characterId]) { - throw new Error(`Room ${roomId} contains non-existent character ${characterId}`); - } - } - } - // Check object containment - for (const [objectId, object] of Object.entries(objects)) { - if (object.containedObjects) { - for (const containedId of object.containedObjects) { - if (!objects[containedId]) { - throw new Error(`Object ${objectId} contains non-existent object ${containedId}`); - } - } - } - } - // Check character inventory - for (const [characterId, character] of Object.entries(characters)) { - for (const objectId of character.inventory) { - if (!objects[objectId]) { - throw new Error(`Character ${characterId} has non-existent object ${objectId} in inventory`); - } - } - } - // Check player inventory - for (const objectId of initialState.inventory) { - if (!objects[objectId]) { - throw new Error(`Initial inventory contains non-existent object ${objectId}`); - } - } - } -} -exports.YamlWorldParser = YamlWorldParser; -//# sourceMappingURL=yaml-parser.js.map \ No newline at end of file diff --git a/dist/world-model/yaml-parser.js.map b/dist/world-model/yaml-parser.js.map deleted file mode 100644 index 195c730..0000000 --- a/dist/world-model/yaml-parser.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"yaml-parser.js","sourceRoot":"","sources":["../../src/world-model/yaml-parser.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/jest.config.js b/jest.config.js deleted file mode 100644 index 606f360..0000000 --- a/jest.config.js +++ /dev/null @@ -1,12 +0,0 @@ -module.exports = { - preset: 'ts-jest', - testEnvironment: 'node', - roots: ['/src'], - testMatch: ['**/__tests__/**/*.ts', '**/?(*.)+(spec|test).ts'], - transform: { - '^.+\\.tsx?$': 'ts-jest', - }, - collectCoverage: true, - coverageDirectory: 'coverage', - collectCoverageFrom: ['src/**/*.ts', '!src/**/*.d.ts'], -}; \ No newline at end of file diff --git a/package.json b/package.json index 1a82941..7a5011e 100644 --- a/package.json +++ b/package.json @@ -6,43 +6,16 @@ "scripts": { "check:node": "node scripts/check-node-version.js", "prestart": "npm run check:node", - "start": "node scripts/run-engine.js start", - "prestart:cli": "npm run check:node", - "start:cli": "node dist/index.js --cli", + "start": "node dist/server-ink.js", "predev": "npm run check:node", - "dev": "node scripts/run-engine.js dev", - "predev:yaml": "npm run check:node", - "dev:yaml": "nodemon --watch src --watch data/worlds --watch config/engines/yaml.json --ext ts,json,yml --exec \"ts-node src/server-yaml.ts\"", - "dev:yaml:debug": "powershell -NoProfile -Command \"$env:YAML_DEBUG='1'; npm run dev:yaml\"", - "dev:yaml:inspect": "powershell -NoProfile -Command \"$env:YAML_DEBUG='1'; nodemon --watch src --watch data/worlds --watch config/engines/yaml.json --ext ts,json,yml --exec \\\"node --inspect=127.0.0.1:9230 -r ts-node/register src/server-yaml.ts\\\"\"", - "predev:cli": "npm run check:node", - "dev:cli": "nodemon --watch src --ext ts,json --exec \"ts-node src/index.ts --cli\"", - "predev:zcode": "npm run check:node", - "dev:zcode": "nodemon --watch src --watch data/zcode-prompts --watch config/engines/zcode.json --ext ts,json,yml --exec \"ts-node src/server-zcode.ts\"", - "dev:zcode:debug": "powershell -NoProfile -Command \"$env:ZCODE_DEBUG='1'; npm run dev:zcode\"", - "dev:zcode:inspect": "powershell -NoProfile -Command \"$env:ZCODE_DEBUG='1'; nodemon --watch src --watch data/zcode-prompts --watch config/engines/zcode.json --ext ts,json,yml --exec \\\"node --inspect=127.0.0.1:9229 -r ts-node/register src/server-zcode.ts\\\"\"", - "predev:ink": "npm run check:node", - "dev:ink": "nodemon --watch src --watch data/ink-src --watch config/engines/ink.json --ext ts,json,ink --exec \"ts-node src/server-ink.ts\"", - "dev:ink:debug": "powershell -NoProfile -Command \"$env:INK_DEBUG='1'; npm run dev:ink\"", - "dev:ink:inspect": "powershell -NoProfile -Command \"$env:INK_DEBUG='1'; nodemon --watch src --watch data/ink-src --watch config/engines/ink.json --ext ts,json,ink --exec \\\"node --inspect=127.0.0.1:9231 -r ts-node/register src/server-ink.ts\\\"\"", - "prestart:yaml": "npm run check:node && npm run build", - "start:yaml": "node dist/server-yaml.js", - "start:yaml:debug": "powershell -NoProfile -Command \"$env:YAML_DEBUG='1'; npm run start:yaml\"", - "start:yaml:inspect": "powershell -NoProfile -Command \"$env:YAML_DEBUG='1'; node --inspect=127.0.0.1:9230 dist/server-yaml.js\"", - "prestart:zcode": "npm run check:node && npm run build", - "start:zcode": "node dist/server-zcode.js", - "start:zcode:debug": "powershell -NoProfile -Command \"$env:ZCODE_DEBUG='1'; npm run start:zcode\"", - "start:zcode:inspect": "powershell -NoProfile -Command \"$env:ZCODE_DEBUG='1'; node --inspect=127.0.0.1:9229 dist/server-zcode.js\"", - "prestart:ink": "npm run check:node && npm run build", - "start:ink": "node dist/server-ink.js", - "start:ink:debug": "powershell -NoProfile -Command \"$env:INK_DEBUG='1'; npm run start:ink\"", - "start:ink:inspect": "powershell -NoProfile -Command \"$env:INK_DEBUG='1'; node --inspect=127.0.0.1:9231 dist/server-ink.js\"", - "pretest-server": "npm run check:node", - "test-server": "ts-node src/test-server-yaml.ts", - "build": "tsc", - "test": "jest", - "lint": "eslint --ext .ts src/", - "lint:fix": "eslint --ext .ts src/ --fix" + "dev": "nodemon --watch src --watch data/ink-src --watch config/engines/ink.json --ext ts,json,ink --exec \"ts-node src/server-ink.ts\"", + "dev:debug": "node -e \"process.env.INK_DEBUG='1'; require('child_process').spawn('npm', ['run', 'dev'], { stdio: 'inherit', shell: true, env: process.env })\"", + "dev:inspect": "nodemon --watch src --watch data/ink-src --watch config/engines/ink.json --ext ts,json,ink --exec \"node --inspect=0.0.0.0:9231 -r ts-node/register src/server-ink.ts\"", + "prestart:debug": "npm run check:node", + "start:debug": "node -e \"process.env.INK_DEBUG='1'; require('./dist/server-ink.js')\"", + "prestart:inspect": "npm run check:node", + "start:inspect": "node --inspect=0.0.0.0:9231 dist/server-ink.js", + "build": "tsc" }, "engines": { "node": ">=18.17" diff --git a/scripts/run-engine.js b/scripts/run-engine.js deleted file mode 100644 index 7a55f6d..0000000 --- a/scripts/run-engine.js +++ /dev/null @@ -1,51 +0,0 @@ -#!/usr/bin/env node - -const { spawnSync } = require('child_process'); -const fs = require('fs'); -const path = require('path'); - -function loadDotEnv(filePath) { - if (!fs.existsSync(filePath)) return; - const lines = fs.readFileSync(filePath, 'utf8').split(/\r?\n/); - for (const line of lines) { - const trimmed = line.trim(); - if (!trimmed || trimmed.startsWith('#')) continue; - const match = trimmed.match(/^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/); - if (!match) continue; - const [, key, rawValue] = match; - if (process.env[key] != null) continue; - process.env[key] = rawValue.replace(/^["']|["']$/g, ''); - } -} - -const projectRoot = path.resolve(__dirname, '..'); -loadDotEnv(path.join(projectRoot, '.env')); - -const mode = process.argv[2] || 'dev'; -const engine = String(process.env.DEFAULT_GAME_ENGINE || process.env.GAME_ENGINE || 'ink') - .trim() - .toLowerCase(); -const allowedModes = new Set(['dev', 'start']); -const allowedEngines = new Set(['ink', 'yaml', 'zcode']); - -if (!allowedModes.has(mode)) { - console.error(`Unsupported run mode "${mode}". Use "dev" or "start".`); - process.exit(1); -} - -if (!allowedEngines.has(engine)) { - console.error(`Unsupported DEFAULT_GAME_ENGINE "${engine}". Use "ink", "yaml", or "zcode".`); - process.exit(1); -} - -const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm'; -const script = `${mode}:${engine}`; -console.log(`[run-engine] DEFAULT_GAME_ENGINE=${engine}; running npm run ${script}`); - -const result = spawnSync(npmCommand, ['run', script], { - cwd: projectRoot, - env: process.env, - stdio: 'inherit', -}); - -process.exit(result.status == null ? 1 : result.status); diff --git a/src/cli/game-runner.ts b/src/cli/game-runner.ts deleted file mode 100644 index 8736e77..0000000 --- a/src/cli/game-runner.ts +++ /dev/null @@ -1,265 +0,0 @@ -/** - * Command-line interface for running the interactive fiction game - */ - -import * as readline from 'readline'; -import * as path from 'path'; -import * as dotenv from 'dotenv'; -import { TextAdventureEngine } from '../engine/game-engine'; -import { OpenRouterProvider } from '../llm/openrouter-provider'; -import { ActionRequest, NarrativeRequest } from '../interfaces/llm'; - -// Load environment variables -dotenv.config(); - -export class GameRunner { - private engine: TextAdventureEngine; - private llmProvider: OpenRouterProvider; - private rl: readline.Interface | null = null; - private gameContext: string = ''; - private gameHistory: string[] = []; - private suggestedCommands: string[] = []; - - constructor() { - this.engine = new TextAdventureEngine(); - this.llmProvider = new OpenRouterProvider(); - } - - /** - * Initialize the game - */ - public async initialize(worldPath: string): Promise { - console.log('Initializing game...'); - - // Initialize LLM provider - 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/or OPENROUTER_MODEL'); - } - - await this.llmProvider.initialize({ - apiKey, - model, - temperature: 0.7, - maxTokens: 800 - }); - - // Load the world - const resolvedPath = path.resolve(worldPath); - console.log(`Loading world from ${resolvedPath}...`); - await this.engine.loadWorld(resolvedPath); - - console.log('Game initialized successfully!'); - } - - /** - * Start the game in CLI mode - */ - public async start(): Promise { - // Create readline interface for CLI mode - this.rl = readline.createInterface({ - input: process.stdin, - output: process.stdout - }); - - try { - // Display introduction - const introText = await this.engine.start(); - console.log('\n' + introText + '\n'); - - // Look at initial room - const initialLook = this.engine.processAction({ action: 'look', confidence: 1 }); - - // Generate narrative description - const narrativeRequest: NarrativeRequest = { - action: 'look', - result: initialLook.message, - roomDescription: this.engine.getCurrentRoomDescription(), - visibleObjects: this.engine.getVisibleObjects(), - visibleCharacters: this.engine.getVisibleCharacters(), - tone: 'descriptive' - }; - - const narrative = await this.llmProvider.generateNarrative(narrativeRequest); - console.log('\n' + narrative.text + '\n'); - - // Store suggestions if available - if (narrative.suggestions && narrative.suggestions.length > 0) { - this.suggestedCommands = narrative.suggestions; - } - - // Update game context - this.updateGameContext(narrative.text); - - // Start the game loop - this.gameLoop(); - } catch (error) { - console.error('Error starting game:', error); - this.end(); - } - } - - /** - * The main game loop for CLI mode - */ - private gameLoop(): void { - if (!this.rl) return; - - this.rl.question('> ', async (input) => { - if (input.toLowerCase() === 'quit' || input.toLowerCase() === 'exit') { - this.end(); - return; - } - - const response = await this.processCommand(input); - console.log('\n' + response + '\n'); - - // Continue the game loop - this.gameLoop(); - }); - } - - /** - * Process a player command and return the narrative response - * Used by both CLI and web interfaces - */ - public async processCommand(input: string): Promise { - try { - // Process player input - const actionRequest: ActionRequest = { - playerInput: input, - currentRoom: this.engine.getWorldModel().rooms[this.engine.getCurrentState().currentRoomId].name, - visibleObjects: this.engine.getVisibleObjects().map(id => this.engine.getWorldModel().objects[id].name), - visibleCharacters: this.engine.getVisibleCharacters().map(id => this.engine.getWorldModel().characters[id].name), - possibleActions: this.engine.getAvailableActions(), - inventory: this.engine.getCurrentState().inventory.map(id => this.engine.getWorldModel().objects[id].name), - gameContext: this.gameContext - }; - - if (this.rl) { - console.log('Thinking...'); - } - - // Translate player input to action - const action = await this.llmProvider.translateAction(actionRequest); - - // Process the action in the game engine - const actionResult = this.engine.processAction(action); - - // If state changed, update it - if (actionResult.stateChanged && actionResult.newState) { - this.engine.getCurrentState().currentRoomId = actionResult.newState.currentRoomId; - this.engine.getCurrentState().inventory = actionResult.newState.inventory; - this.engine.getCurrentState().visitedRooms = actionResult.newState.visitedRooms; - this.engine.getCurrentState().flags = actionResult.newState.flags; - this.engine.getCurrentState().counters = actionResult.newState.counters; - } - - // Generate narrative description - const narrativeRequest: NarrativeRequest = { - action: `${action.action}${action.object ? ' ' + action.object : ''}${action.target ? ' on ' + action.target : ''}`, - result: actionResult.message, - roomDescription: this.engine.getCurrentRoomDescription(), - visibleObjects: this.engine.getVisibleObjects().map(id => this.engine.getWorldModel().objects[id].name), - visibleCharacters: this.engine.getVisibleCharacters().map(id => this.engine.getWorldModel().characters[id].name), - previousContext: this.gameHistory.slice(-3).join('\n'), - tone: 'descriptive' - }; - - const narrative = await this.llmProvider.generateNarrative(narrativeRequest); - - // Store suggestions if available - if (narrative.suggestions && narrative.suggestions.length > 0) { - this.suggestedCommands = narrative.suggestions; - } - - // Update game context with the new narrative - this.updateGameContext(narrative.text); - - // Return the narrative text - return narrative.text; - - } catch (error) { - console.error('Error processing input:', error); - return 'Something went wrong. Please try again.'; - } - } - - /** - * End the game - */ - public end(): void { - console.log('\nThanks for playing!'); - if (this.rl) { - this.rl.close(); - this.rl = null; - } - this.engine.end(); - if (process.env.NODE_ENV !== 'production') { - process.exit(0); - } - } - - /** - * Update the game context with new narrative - */ - private updateGameContext(narrative: string): void { - // Add to history - this.gameHistory.push(narrative); - - // Keep history limited to last 10 entries - if (this.gameHistory.length > 10) { - this.gameHistory.shift(); - } - - // Update current context (last 5 entries) - this.gameContext = this.gameHistory.slice(-5).join('\n'); - } - - /** - * Get the current game state - * Used by web interface - */ - public getGameState() { - return { - world: this.engine.getWorldModel(), - currentRoomId: this.engine.getCurrentState().currentRoomId, - inventory: this.engine.getCurrentState().inventory, - visitedRooms: this.engine.getCurrentState().visitedRooms, - flags: this.engine.getCurrentState().flags, - counters: this.engine.getCurrentState().counters - }; - } - - /** - * Get the current room description - * Used by web interface - */ - public getCurrentRoomDescription(): string { - const roomId = this.engine.getCurrentState().currentRoomId; - return this.engine.getWorldModel().rooms[roomId].description; - } - - /** - * Get suggested actions for the current game state - * Used by web interface - */ - public getSuggestions(): string[] { - return this.suggestedCommands; - } - - /** - * Load a saved game state - * Used by web interface - */ - public loadGameState(savedState: any): void { - // Set the current state to match the saved state - this.engine.getCurrentState().currentRoomId = savedState.currentRoomId; - this.engine.getCurrentState().inventory = savedState.inventory; - this.engine.getCurrentState().visitedRooms = savedState.visitedRooms; - this.engine.getCurrentState().flags = savedState.flags; - this.engine.getCurrentState().counters = savedState.counters; - } -} \ No newline at end of file diff --git a/src/config/game-config.ts b/src/config/game-config.ts index 8b4311e..31a689f 100644 --- a/src/config/game-config.ts +++ b/src/config/game-config.ts @@ -1,7 +1,7 @@ import path from 'path'; import { existsSync, mkdirSync, readFileSync } from 'fs'; -export type EngineName = 'yaml' | 'ink' | 'zcode' | string; +export type EngineName = 'ink' | string; export interface GameMetadata { title: string; @@ -37,12 +37,9 @@ function fallbackConfig(engine: EngineName): GameEngineConfig { engine, locale: 'en_US', paths: { - mainGameFile: - engine === 'ink' - ? 'data/ink/story.ink.json' - : engine === 'zcode' - ? 'data/z-code/zork1.bin' - : 'data/worlds/example_world.yml', + mainGameFile: 'data/ink/eibenreith.ink.json', + inkSource: 'data/ink-src/eibenreith.ink', + inkCompiled: 'data/ink/eibenreith.ink.json', music: 'public/music', sfx: 'public/sounds', images: 'public/images', diff --git a/src/engine/game-engine.ts b/src/engine/game-engine.ts deleted file mode 100644 index a8e75df..0000000 --- a/src/engine/game-engine.ts +++ /dev/null @@ -1,661 +0,0 @@ -/** - * Core Game Engine - * Manages game state and processes actions - */ - -import * as fs from 'fs/promises'; -import { GameEngine, ActionResult } from '../interfaces/engine'; -import { WorldModel, GameState, Room, GameObject, Character } from '../interfaces/world-model'; -import { ActionResponse } from '../interfaces/llm'; -import { YamlWorldParser } from '../world-model/yaml-parser'; - -export class TextAdventureEngine implements GameEngine { - private worldModel: WorldModel | null = null; - private gameState: GameState | null = null; - private actionHandlers: Record ActionResult> = {}; - - constructor() { - this.registerDefaultActionHandlers(); - } - - /** - * Load a world model from a file - */ - public async loadWorld(worldModelPath: string): Promise { - try { - this.worldModel = await YamlWorldParser.loadFromFile(worldModelPath); - this.gameState = { ...this.worldModel.initialState }; - - // Mark the initial room as visited - if (!this.gameState.visitedRooms.includes(this.gameState.currentRoomId)) { - this.gameState.visitedRooms.push(this.gameState.currentRoomId); - } - } catch (error) { - console.error(`Failed to load world from ${worldModelPath}:`, error); - throw new Error(`Could not load world: ${error instanceof Error ? error.message : String(error)}`); - } - } - - /** - * Get the current game state - */ - public getCurrentState(): GameState { - if (!this.gameState) { - throw new Error('Game state not initialized. Please load a world first.'); - } - return { ...this.gameState }; - } - - /** - * Get the world model - */ - public getWorldModel(): WorldModel { - if (!this.worldModel) { - throw new Error('World model not initialized. Please load a world first.'); - } - return this.worldModel; - } - - /** - * Process an action from the player - */ - public processAction(action: ActionResponse): ActionResult { - if (!this.worldModel || !this.gameState) { - return { - success: false, - message: 'Game not initialized', - stateChanged: false - }; - } - - const handler = this.actionHandlers[action.action.toLowerCase()]; - if (!handler) { - return { - success: false, - message: `I don't know how to "${action.action}"`, - stateChanged: false - }; - } - - return handler(this.gameState, this.worldModel, action); - } - - /** - * Save the current game state to a file - */ - public async saveGame(filename: string): Promise { - if (!this.gameState || !this.worldModel) { - throw new Error('Cannot save: game not initialized'); - } - - const saveData = { - worldModelName: this.worldModel.title, - worldModelVersion: this.worldModel.version, - timestamp: new Date().toISOString(), - gameState: this.gameState - }; - - try { - await fs.writeFile(filename, JSON.stringify(saveData, null, 2), 'utf8'); - } catch (error) { - console.error(`Failed to save game to ${filename}:`, error); - throw new Error(`Could not save game: ${error instanceof Error ? error.message : String(error)}`); - } - } - - /** - * Load a game state from a save file - */ - public async loadGame(filename: string): Promise { - try { - const fileContents = await fs.readFile(filename, 'utf8'); - const saveData = JSON.parse(fileContents); - - // Check if the save file matches the current world model - if (!this.worldModel) { - throw new Error('World model not loaded'); - } - - if (saveData.worldModelName !== this.worldModel.title || - saveData.worldModelVersion !== this.worldModel.version) { - throw new Error('Save file is for a different world or version'); - } - - // Load the game state - this.gameState = saveData.gameState; - } catch (error) { - console.error(`Failed to load game from ${filename}:`, error); - throw new Error(`Could not load save file: ${error instanceof Error ? error.message : String(error)}`); - } - } - - /** - * Get a list of available actions in the current context - */ - public getAvailableActions(): string[] { - if (!this.worldModel) return []; - - // Common actions always available - const availableActions = ['look', 'inventory', 'help']; - - // Add movement actions based on current room exits - const currentRoom = this.getCurrentRoom(); - if (currentRoom) { - currentRoom.exits.forEach(exit => { - availableActions.push(`go ${exit.direction.toLowerCase()}`); - }); - } - - // Add object interactions based on visible objects - const visibleObjects = this.getVisibleObjects(); - const objects = this.worldModel.objects; - - visibleObjects.forEach(objId => { - const obj = objects[objId]; - if (obj) { - obj.allowedActions.forEach(action => { - availableActions.push(`${action} ${obj.name.toLowerCase()}`); - }); - } - }); - - // Add character interactions - const visibleCharacters = this.getVisibleCharacters(); - visibleCharacters.forEach(charId => { - availableActions.push(`talk to ${this.worldModel!.characters[charId].name.toLowerCase()}`); - }); - - // Add inventory object actions - this.gameState!.inventory.forEach(objId => { - const obj = objects[objId]; - if (obj) { - obj.allowedActions.forEach(action => { - availableActions.push(`${action} ${obj.name.toLowerCase()}`); - }); - } - }); - - return Array.from(new Set(availableActions)); // Remove duplicates - } - - /** - * Get a list of visible objects in the current room - */ - public getVisibleObjects(): string[] { - if (!this.worldModel || !this.gameState) return []; - - const currentRoom = this.getCurrentRoom(); - if (!currentRoom) return []; - - const visibleObjects: string[] = [...currentRoom.objects]; - - // Add objects from open containers - currentRoom.objects.forEach(objId => { - const obj = this.worldModel!.objects[objId]; - if (obj && obj.traits.includes('container') && obj.states?.open && obj.containedObjects) { - visibleObjects.push(...obj.containedObjects); - } - }); - - return visibleObjects; - } - - /** - * Get a list of visible characters in the current room - */ - public getVisibleCharacters(): string[] { - if (!this.worldModel || !this.gameState) return []; - - const currentRoom = this.getCurrentRoom(); - return currentRoom ? currentRoom.characters : []; - } - - /** - * Get the description of the current room - */ - public getCurrentRoomDescription(): string { - const currentRoom = this.getCurrentRoom(); - if (!currentRoom) return 'You are in a void. Something has gone wrong.'; - - return currentRoom.description; - } - - /** - * Start the game and return the introduction text - */ - public async start(): Promise { - if (!this.worldModel) { - throw new Error('World not loaded. Please load a world before starting.'); - } - - // Reset game state to initial state - this.gameState = { ...this.worldModel.initialState }; - - return this.worldModel.introduction; - } - - /** - * End the game (cleanup resources if needed) - */ - public end(): void { - // Cleanup could happen here if needed - console.log('Game ended'); - } - - /** - * Get the current room object - */ - private getCurrentRoom(): Room | null { - if (!this.worldModel || !this.gameState) return null; - - const roomId = this.gameState.currentRoomId; - return this.worldModel.rooms[roomId] || null; - } - - /** - * Register default action handlers - */ - private registerDefaultActionHandlers(): void { - // Look action - this.actionHandlers['look'] = (state, world, action): ActionResult => { - const room = world.rooms[state.currentRoomId]; - - // If an object is specified, look at that object - if (action.object) { - // Try to find the object in the room or inventory - const visibleObjects = this.getVisibleObjects(); - const objId = this.findObjectByName(action.object, [...visibleObjects, ...state.inventory]); - - if (!objId) { - return { - success: false, - message: `You don't see any ${action.object} here.`, - stateChanged: false - }; - } - - const obj = world.objects[objId]; - return { - success: true, - message: obj.description, - stateChanged: false - }; - } - - // Look at the room - const objectDescriptions = room.objects - .map(id => world.objects[id]) - .map(obj => `You can see ${obj.name.toLowerCase()} here.`); - - const characterDescriptions = room.characters - .map(id => world.characters[id]) - .map(char => `${char.name} is here.`); - - const exitDescriptions = room.exits - .map(exit => `There is an exit ${exit.direction.toLowerCase()}${exit.description ? ` (${exit.description})` : ''}.`); - - const fullDescription = [ - room.description, - ...objectDescriptions, - ...characterDescriptions, - ...exitDescriptions - ].join('\n'); - - return { - success: true, - message: fullDescription, - stateChanged: false - }; - }; - - // Go action - this.actionHandlers['go'] = (state, world, action): ActionResult => { - const room = world.rooms[state.currentRoomId]; - - if (!action.object) { - return { - success: false, - message: 'Go where?', - stateChanged: false - }; - } - - // Find the exit that matches the direction - const direction = action.object.toLowerCase(); - const exit = room.exits.find(e => e.direction.toLowerCase() === direction); - - if (!exit) { - return { - success: false, - message: `You can't go ${direction} from here.`, - stateChanged: false - }; - } - - if (exit.isLocked) { - if (!exit.keyId) { - return { - success: false, - message: `The way ${direction} is locked.`, - stateChanged: false - }; - } - - if (!state.inventory.includes(exit.keyId)) { - return { - success: false, - message: `The way ${direction} is locked and you don't have the key.`, - stateChanged: false - }; - } - - // Player has the key, unlock the exit - exit.isLocked = false; - return { - success: true, - message: `You unlock the way ${direction} and proceed.`, - stateChanged: true, - newState: { - ...state, - currentRoomId: exit.targetRoomId, - visitedRooms: state.visitedRooms.includes(exit.targetRoomId) - ? state.visitedRooms - : [...state.visitedRooms, exit.targetRoomId] - } - }; - } - - // Exit is not locked, just move - return { - success: true, - message: `You go ${direction}.`, - stateChanged: true, - newState: { - ...state, - currentRoomId: exit.targetRoomId, - visitedRooms: state.visitedRooms.includes(exit.targetRoomId) - ? state.visitedRooms - : [...state.visitedRooms, exit.targetRoomId] - } - }; - }; - - // Take action - this.actionHandlers['take'] = (state, world, action): ActionResult => { - if (!action.object) { - return { - success: false, - message: 'Take what?', - stateChanged: false - }; - } - - // Find the object in the current room - const visibleObjects = this.getVisibleObjects(); - const objId = this.findObjectByName(action.object, visibleObjects); - - if (!objId) { - return { - success: false, - message: `You don't see any ${action.object} here.`, - stateChanged: false - }; - } - - const obj = world.objects[objId]; - - // Check if the object can be taken - if (!obj.traits.includes('takeable')) { - return { - success: false, - message: `You can't take the ${obj.name.toLowerCase()}.`, - stateChanged: false - }; - } - - // Remove object from room and add to inventory - const room = world.rooms[state.currentRoomId]; - const newRoomObjects = room.objects.filter(id => id !== objId); - room.objects = newRoomObjects; - - // Update state - return { - success: true, - message: `You take the ${obj.name.toLowerCase()}.`, - stateChanged: true, - newState: { - ...state, - inventory: [...state.inventory, objId] - } - }; - }; - - // Inventory action - this.actionHandlers['inventory'] = (state, world): ActionResult => { - if (state.inventory.length === 0) { - return { - success: true, - message: 'Your inventory is empty.', - stateChanged: false - }; - } - - const items = state.inventory - .map(id => world.objects[id]) - .map(obj => obj.name) - .join(', '); - - return { - success: true, - message: `You are carrying: ${items}.`, - stateChanged: false - }; - }; - - // Drop action - this.actionHandlers['drop'] = (state, world, action): ActionResult => { - if (!action.object) { - return { - success: false, - message: 'Drop what?', - stateChanged: false - }; - } - - // Find the object in the inventory - const objId = this.findObjectByName(action.object, state.inventory); - - if (!objId) { - return { - success: false, - message: `You don't have any ${action.object}.`, - stateChanged: false - }; - } - - const obj = world.objects[objId]; - - // Remove object from inventory and add to room - const room = world.rooms[state.currentRoomId]; - room.objects.push(objId); - - // Update state - return { - success: true, - message: `You drop the ${obj.name.toLowerCase()}.`, - stateChanged: true, - newState: { - ...state, - inventory: state.inventory.filter(id => id !== objId) - } - }; - }; - - // Use action - this.actionHandlers['use'] = (state, world, action): ActionResult => { - if (!action.object) { - return { - success: false, - message: 'Use what?', - stateChanged: false - }; - } - - // Find the object in inventory or visible objects - const visibleObjects = this.getVisibleObjects(); - const objId = this.findObjectByName(action.object, [...state.inventory, ...visibleObjects]); - - if (!objId) { - return { - success: false, - message: `You don't see any ${action.object} here.`, - stateChanged: false - }; - } - - const obj = world.objects[objId]; - - // Check if the object can be used - if (!obj.allowedActions.includes('use')) { - return { - success: false, - message: `You can't use the ${obj.name.toLowerCase()}.`, - stateChanged: false - }; - } - - // Check if there's a target - if (action.target) { - const targetId = this.findObjectByName(action.target, [...state.inventory, ...visibleObjects]); - - if (!targetId) { - return { - success: false, - message: `You don't see any ${action.target} here.`, - stateChanged: false - }; - } - - const target = world.objects[targetId]; - - // TODO: Implement object-specific use logic (could be extended with a more sophisticated system) - return { - success: true, - message: `You use the ${obj.name.toLowerCase()} on the ${target.name.toLowerCase()}.`, - stateChanged: false - }; - } - - // Simple use without target - return { - success: true, - message: `You use the ${obj.name.toLowerCase()}.`, - stateChanged: false - }; - }; - - // Talk action - this.actionHandlers['talk'] = (state, world, action): ActionResult => { - if (!action.object) { - return { - success: false, - message: 'Talk to whom?', - stateChanged: false - }; - } - - // Find the character in the room - const visibleCharacters = this.getVisibleCharacters(); - const charId = this.findCharacterByName(action.object, visibleCharacters); - - if (!charId) { - return { - success: false, - message: `You don't see anyone called ${action.object} here.`, - stateChanged: false - }; - } - - const character = world.characters[charId]; - - // If a topic is provided - if (action.parameters?.topic) { - const topic = action.parameters.topic.toLowerCase(); - const response = character.dialogue[topic] || character.defaultResponse; - - return { - success: true, - message: `${character.name}: "${response}"`, - stateChanged: false - }; - } - - // No specific topic - return { - success: true, - message: `${character.name} looks ready to talk. You could ask about: ${Object.keys(character.dialogue).join(', ')}.`, - stateChanged: false - }; - }; - - // Help action - this.actionHandlers['help'] = (): ActionResult => { - return { - success: true, - message: [ - 'Available commands:', - '- look: Examine your surroundings or a specific object', - '- go [direction]: Move in a direction', - '- take [object]: Pick up an object', - '- drop [object]: Put down an object', - '- inventory: Check what you\'re carrying', - '- use [object] (on [target]): Use an object, optionally on another object', - '- talk to [character] (about [topic]): Speak with a character', - '- help: Show this help text', - '', - 'You can type commands in natural language. The AI will interpret your intent.' - ].join('\n'), - stateChanged: false - }; - }; - - // Examine action (alias for look) - this.actionHandlers['examine'] = this.actionHandlers['look']; - } - - /** - * Find an object by name in a list of object IDs - */ - private findObjectByName(name: string, objectIds: string[]): string | null { - if (!this.worldModel) return null; - - const normalizedName = name.toLowerCase(); - - for (const id of objectIds) { - const obj = this.worldModel.objects[id]; - if (obj && obj.name.toLowerCase() === normalizedName) { - return id; - } - } - - return null; - } - - /** - * Find a character by name in a list of character IDs - */ - private findCharacterByName(name: string, characterIds: string[]): string | null { - if (!this.worldModel) return null; - - const normalizedName = name.toLowerCase(); - - for (const id of characterIds) { - const character = this.worldModel.characters[id]; - if (character && character.name.toLowerCase() === normalizedName) { - return id; - } - } - - return null; - } -} \ No newline at end of file diff --git a/src/engine/zcode-llm-engine.ts b/src/engine/zcode-llm-engine.ts deleted file mode 100644 index 19554a7..0000000 --- a/src/engine/zcode-llm-engine.ts +++ /dev/null @@ -1,1160 +0,0 @@ -/** - * Z-code LLM Engine - * - * Runs a Z-machine story file as a headless subprocess via the - * `ifvms` CLI, and wraps every I/O exchange with OpenRouter LLM calls that - * translate free natural-language player input into parser commands and - * re-voice the Z-machine's raw output as polished narrative prose. - * - * Configuration (environment variables): - * ZCODE_STORY_FILE - path to the .z5/.z8/.bin story file (default: ./data/z-code/zork1.bin) - * ZCODE_MAX_RETRIES - maximum command retry attempts per turn (default: 3) - * ZCODE_HISTORY_SIZE - player-facing outputs stored per room (default: 5) - * OPENROUTER_API_KEY, OPENROUTER_MODEL - required - */ - -import { 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'; -import { - textToParagraphs, - TurnResult, -} from '../interfaces/turn-result'; - -dotenv.config(); - -const DEBUG_ENABLED = /^(1|true|yes|on)$/i.test(process.env.ZCODE_DEBUG ?? ''); - -function debugLog(message: string, details?: unknown): void { - if (!DEBUG_ENABLED) return; - if (typeof details === 'undefined') { - console.log(`[ZcodeLlm:debug] ${message}`); - return; - } - console.log(`[ZcodeLlm: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 ZcodeSession { - 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; -} - -export type ZcodeTurnResult = TurnResult; - -interface PromptConfig { - system: string; - user_template: string; -} - -interface ZcodePrompts { - 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, zcodeOutput: string): string { - const object = command.replace(/^READ\s+/i, '').trim().toLowerCase(); - const label = object ? `the ${object}` : 'it'; - const cleanedOutput = zcodeOutput - .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]; -} - -// --------------------------------------------------------------------------- -// ZcodeProcess – manages the ifvms zvm child process -// --------------------------------------------------------------------------- - -class ZcodeProcess { - 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 a Z-machine 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): ZcodePrompts { - 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(`[ZcodeLlm] ${scope} failed: ${ax.message}`); - if (ax.response) { - console.error( - `[ZcodeLlm] ${scope} status=${ax.response.status} data=`, - ax.response.data, - ); - if (ax.response.status === 404) { - console.error( - '[ZcodeLlm] Hint: OPENROUTER_MODEL is likely invalid or unavailable for your API key.', - ); - } - } - return; - } - - console.error(`[ZcodeLlm] ${scope} failed:`, err); -} - -// --------------------------------------------------------------------------- -// ZcodeLlmEngine -// --------------------------------------------------------------------------- - -export class ZcodeLlmEngine { - private zmachine = new ZcodeProcess(); - private session: ZcodeSession | null = null; - private prompts: ZcodePrompts; - private llm: AxiosInstance; - private model: string; - private resolvedFallbackModel: string | null = null; - private llmCallCounter = 0; - private maxRetries: number; - private historySize: number; - private nextTurnId = 1; - 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(options: { storyPath?: string; promptDir?: string } = {}) { - 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 = - ZcodeLlmEngine.DEPRECATED_MODEL_REPLACEMENTS[model] ?? null; - if (replacement) { - this.model = replacement; - console.warn( - `[ZcodeLlm] 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.ZCODE_MAX_RETRIES ?? '3', 10); - this.historySize = parseInt(process.env.ZCODE_HISTORY_SIZE ?? '5', 10); - this.storyPath = path.resolve( - options.storyPath ?? process.env.ZCODE_STORY_FILE ?? './data/z-code/zork1.bin', - ); - - const promptDir = path.resolve(options.promptDir ?? './data/zcode-prompts'); - this.prompts = loadPrompts(promptDir); - - this.llm = axios.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( - `[ZcodeLlm] 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.zmachine.isAlive(); - } - - /** - * Start a new game: launch the Z-machine story, generate the player character, rewrite the - * intro text, and return the first TurnResult for the client. - */ - async newGame(): Promise { - // Kill any existing game - if (this.zmachine.isAlive()) this.zmachine.kill(); - this.nextTurnId = 1; - - if (!fs.existsSync(this.storyPath)) { - throw new Error( - `Story file not found: ${this.storyPath}\n` + - 'Place zork1.bin in ./data/z-code/ (see README in that folder).', - ); - } - - debugLog('launching Z-machine', { storyPath: this.storyPath }); - const rawIntro = await this.zmachine.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 Z-machine command, continue to game loop - if (!cmdResponse.command && !cmdResponse.commands?.length) { - // Pure tool action — generate a brief acknowledgement via the rewriter - const ack = await this.rewriteText( - `(The narrator pauses. ${userInput})`, - ); - 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(), `zcode-save-${Date.now()}.qzl`); - try { - // Ask the Z-machine to save, supply the temp file path, and discard the output - await this.zmachine.sendLine('SAVE'); - await this.zmachine.sendLine(tmpFile); - - let zcodeSave = ''; - if (fs.existsSync(tmpFile)) { - zcodeSave = fs.readFileSync(tmpFile).toString('base64'); - } - - return JSON.stringify({ session: this.session, zcodeSave }); - } 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, zcodeSave } = JSON.parse(savedJson) as { - session: ZcodeSession; - zcodeSave: string; - }; - - if (this.zmachine.isAlive()) this.zmachine.kill(); - - const tmpFile = path.join(os.tmpdir(), `zcode-restore-${Date.now()}.qzl`); - try { - fs.writeFileSync(tmpFile, Buffer.from(zcodeSave, 'base64')); - - await this.zmachine.launch(this.storyPath); - await this.zmachine.sendLine('RESTORE'); - const restoreOutput = await this.zmachine.sendLine(tmpFile); - - this.session = { ...session, running: true }; - 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.zmachine.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(zcodeOutput: string): Promise { - const cfg = this.prompts.textRewriter; - const vars = this.buildCommonVars(); - vars['zcodeOutput'] = zcodeOutput; - - 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 zcodeOutput; - } - } - - 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 Z-machine parser - return { type: 'command', command: userInput.toUpperCase() }; - } - } - - private async evaluateOutput( - userIntent: string, - commandTried: string, - zcodeOutput: string, - attempt: number, - ): Promise { - const cfg = this.prompts.outputEvaluator; - const vars = this.buildCommonVars(); - vars['userIntent'] = userIntent; - vars['commandTried'] = commandTried; - vars['zcodeOutput'] = zcodeOutput; - 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: zcodeOutput }; - } - } - - // ---- 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): ZcodeTurnResult { - const alive = this.zmachine.isAlive(); - if (!alive && this.session) this.session.running = false; - const paragraphs = textToParagraphs(text); - return { - turnId: this.nextTurnId++, - paragraphs, - choices: [], - inputMode: alive ? 'text' : 'end', - gameState: { statusLine: this.session?.currentRoom }, - }; - } -} diff --git a/src/index.ts b/src/index.ts deleted file mode 100644 index 1322c3b..0000000 --- a/src/index.ts +++ /dev/null @@ -1,88 +0,0 @@ -/** - * Main entry point for the AI Interactive Fiction application - */ - -import * as path from 'path'; -import * as dotenv from 'dotenv'; -import { GameRunner } from './cli/game-runner'; -// YAML CLI entry point. The web default is selected by scripts/run-engine.js. -import { startServer } from './server-yaml'; -import { loadGameConfig, projectPath } from './config/game-config'; - -// Load environment variables -console.log('Loading environment variables...'); -try { - const result = dotenv.config(); - if (result.error) { - console.error('Error loading .env file:', result.error); - } else { - console.log('Environment variables loaded successfully'); - } -} catch (error) { - console.error('Exception when loading env:', error); -} - -async function main(): Promise { - try { - console.log('=== AI Interactive Fiction ==='); - console.log('A modern take on classic text adventures with LLM-powered interactions'); - console.log(''); - - // Get the world file path from the YAML engine config, with environment override. - const engineConfig = loadGameConfig( - process.env.YAML_CONFIG_FILE || './config/engines/yaml.json', - 'yaml', - ); - const worldFile = projectPath(process.env.DEFAULT_WORLD_FILE || engineConfig.paths.mainGameFile); - console.log(`Using world file: ${worldFile}`); - console.log(`OpenRouter API Key: ${process.env.OPENROUTER_API_KEY ? 'Found' : 'Missing'}`); - console.log(`OpenRouter Model: ${process.env.OPENROUTER_MODEL || 'Not specified'}`); - - // Check if we should run in CLI mode - const args = process.argv.slice(2); - const cliMode = args.includes('--cli') || args.includes('-c'); - - if (cliMode) { - // CLI mode - console.log('Starting in CLI mode...'); - - // Create game runner and initialize - console.log('Creating game runner...'); - const gameRunner = new GameRunner(); - - console.log('Initializing game...'); - await gameRunner.initialize(worldFile); - - // Start the CLI game - console.log('Starting CLI game...'); - await gameRunner.start(); - } else { - // Web interface mode - explicitly start the server with port fallback - console.log('Starting in web interface mode...'); - - // Get port configuration - const DEFAULT_PORT = 3000; - const PORT = process.env.PORT ? parseInt(process.env.PORT) : DEFAULT_PORT; - const PORT_RANGE = 300; - - // Start the web server with port fallback - console.log('Starting web server...'); - await startServer(PORT, PORT_RANGE); - } - } catch (error) { - console.error('Failed to start:', error); - if (error instanceof Error) { - console.error('Error name:', error.name); - console.error('Error message:', error.message); - console.error('Error stack:', error.stack); - } - process.exit(1); - } -} - -// Start the application -console.log('Starting application...'); -main().catch(error => { - console.error('Unhandled error in main:', error); - process.exit(1); -}); diff --git a/src/interfaces/engine.ts b/src/interfaces/engine.ts deleted file mode 100644 index 0f6b4d2..0000000 --- a/src/interfaces/engine.ts +++ /dev/null @@ -1,56 +0,0 @@ -/** - * Interfaces for the game engine - */ - -import { WorldModel, GameState } from './world-model'; -import { ActionResponse, NarrativeResponse } from './llm'; - -export interface ActionResult { - success: boolean; - message: string; - stateChanged: boolean; - newState?: GameState; -} - -export interface GameEngine { - loadWorld(worldModelPath: string): Promise; - getCurrentState(): GameState; - getWorldModel(): WorldModel; - - // Action processing - processAction(action: ActionResponse): ActionResult; - - // State management - saveGame(filename: string): Promise; - loadGame(filename: string): Promise; - - // Helper methods for world interaction - getAvailableActions(): string[]; - getVisibleObjects(): string[]; - getVisibleCharacters(): string[]; - getCurrentRoomDescription(): string; - - // Game flow - start(): Promise; // Returns introduction text - end(): void; -} - -export interface GameSession { - engine: GameEngine; - history: { - playerInput: string; - actionResponse: ActionResponse; - actionResult: ActionResult; - narrativeResponse: NarrativeResponse; - }[]; - startTime: Date; - lastInteractionTime: Date; -} - -export interface ActionHandler { - execute( - gameState: GameState, - worldModel: WorldModel, - action: ActionResponse - ): ActionResult; -} \ No newline at end of file diff --git a/src/interfaces/llm.ts b/src/interfaces/llm.ts deleted file mode 100644 index 4d06367..0000000 --- a/src/interfaces/llm.ts +++ /dev/null @@ -1,52 +0,0 @@ -/** - * Interfaces for LLM integration - */ - -export interface LlmConfig { - apiKey: string; - model: string; - temperature?: number; - maxTokens?: number; - topP?: number; - frequencyPenalty?: number; - presencePenalty?: number; -} - -export interface ActionRequest { - playerInput: string; - currentRoom: string; - visibleObjects: string[]; - visibleCharacters: string[]; - possibleActions: string[]; - inventory: string[]; - gameContext: string; -} - -export interface ActionResponse { - action: string; - object?: string; - target?: string; - parameters?: Record; - confidence: number; -} - -export interface NarrativeRequest { - action: string; - result: string; - roomDescription: string; - visibleObjects: string[]; - visibleCharacters: string[]; - previousContext?: string; - tone?: string; // e.g., "mysterious", "humorous", "dramatic" -} - -export interface NarrativeResponse { - text: string; - suggestions?: string[]; // Optional hints for the player -} - -export interface LlmProvider { - initialize(config: LlmConfig): Promise; - translateAction(request: ActionRequest): Promise; - generateNarrative(request: NarrativeRequest): Promise; -} \ No newline at end of file diff --git a/src/interfaces/world-model.ts b/src/interfaces/world-model.ts deleted file mode 100644 index db74c06..0000000 --- a/src/interfaces/world-model.ts +++ /dev/null @@ -1,68 +0,0 @@ -/** - * Core interfaces for the interactive fiction world model - */ - -export interface Room { - id: string; - name: string; - description: string; - exits: Exit[]; - objects: string[]; // References to object IDs - characters: string[]; // References to character IDs -} - -export interface Exit { - direction: string; - targetRoomId: string; - description?: string; - isLocked?: boolean; - keyId?: string; // ID of the key object needed to unlock -} - -export interface GameObject { - id: string; - name: string; - description: string; - traits: string[]; // e.g., "takeable", "container", "edible" - states: Record; // e.g., { "open": false, "lit": true } - containedObjects?: string[]; // IDs of objects inside if this is a container - allowedActions: string[]; // What actions can be performed on this object -} - -export interface Character { - id: string; - name: string; - description: string; - dialogue: Record; // Topic -> response mapping - inventory: string[]; // IDs of objects the character has - defaultResponse: string; // Response when topic not found - mood?: string; // Current mood affecting responses -} - -export interface Action { - name: string; - patterns: string[]; // Example natural language patterns this action matches - requiresObject?: boolean; - requiresTarget?: boolean; - handler: string; // Name of method to handle this action -} - -export interface GameState { - currentRoomId: string; - inventory: string[]; // IDs of objects in player's inventory - visitedRooms: string[]; // IDs of rooms the player has visited - flags: Record; // Game state flags - counters: Record; // Game state counters -} - -export interface WorldModel { - title: string; - author: string; - version: string; - introduction: string; - rooms: Record; - objects: Record; - characters: Record; - actions: Record; - initialState: GameState; -} \ No newline at end of file diff --git a/src/llm/openrouter-provider.ts b/src/llm/openrouter-provider.ts deleted file mode 100644 index 9c3c302..0000000 --- a/src/llm/openrouter-provider.ts +++ /dev/null @@ -1,212 +0,0 @@ -/** - * OpenRouter LLM Provider - * Handles communication with OpenRouter API for LLM interactions - */ - -import axios, { AxiosInstance } from 'axios'; -import { - LlmProvider, - LlmConfig, - ActionRequest, - ActionResponse, - NarrativeRequest, - NarrativeResponse -} from '../interfaces/llm'; - -export class OpenRouterProvider implements LlmProvider { - private apiKey: string = ''; - private model: string = ''; - private client!: AxiosInstance; - private temperature: number = 0.7; - private maxTokens: number = 800; - - /** - * Initialize the OpenRouter provider with configuration - */ - public async initialize(config: LlmConfig): Promise { - this.apiKey = config.apiKey; - this.model = config.model; - this.temperature = config.temperature ?? 0.7; - this.maxTokens = config.maxTokens ?? 800; - - this.client = axios.create({ - baseURL: 'https://openrouter.ai/api/v1', - headers: { - 'Authorization': `Bearer ${this.apiKey}`, - 'Content-Type': 'application/json' - } - }); - } - - /** - * Translate player input into a structured action for the game engine - */ - public async translateAction(request: ActionRequest): Promise { - try { - const prompt = this.buildActionPrompt(request); - - const response = await this.client.post('/chat/completions', { - model: this.model, - messages: [ - { - role: 'system', - content: prompt.system - }, - { - role: 'user', - content: prompt.user - } - ], - temperature: 0.2, // Lower temperature for more deterministic outputs - max_tokens: 150, - response_format: { type: 'json_object' } - }); - - const content = response.data.choices[0].message.content; - const parsedResponse = JSON.parse(content); - - return this.validateActionResponse(parsedResponse); - } catch (error) { - console.error('Error translating action:', error); - // Fallback to a simple "look" action when errors occur - return { - action: 'look', - confidence: 0.5 - }; - } - } - - /** - * Generate narrative prose based on game events - */ - public async generateNarrative(request: NarrativeRequest): Promise { - try { - const prompt = this.buildNarrativePrompt(request); - - const response = await this.client.post('/chat/completions', { - model: this.model, - messages: [ - { - role: 'system', - content: prompt.system - }, - { - role: 'user', - content: prompt.user - } - ], - temperature: this.temperature, - max_tokens: this.maxTokens - }); - - const content = response.data.choices[0].message.content; - - // Check if response is JSON format or plain text - try { - const parsedResponse = JSON.parse(content); - return { - text: parsedResponse.text, - suggestions: parsedResponse.suggestions || [] - }; - } catch { - // Plain text response, just use the content directly - return { - text: content - }; - } - } catch (error) { - console.error('Error generating narrative:', error); - return { - text: `Something happened, but the narrator is at a loss for words. (Error: ${error instanceof Error ? error.message : String(error)})` - }; - } - } - - /** - * Build the system and user prompts for action translation - */ - private buildActionPrompt(request: ActionRequest): { system: string; user: string } { - const systemPrompt = `You are an AI assistant that translates natural language input into structured action commands for an interactive fiction game. -Your task is to convert player input into a JSON object representing an action that can be understood by the game engine. - -The player is currently in the "${request.currentRoom}" room. -Visible objects: ${request.visibleObjects.join(', ')} -Visible characters: ${request.visibleCharacters.join(', ')} -Inventory: ${request.inventory.join(', ')} -Available actions: ${request.possibleActions.join(', ')} - -Game context: ${request.gameContext} - -Respond ONLY with a JSON object that follows this structure: -{ - "action": "string", // Name of the action (e.g., "take", "examine", "go", "talk", etc.) - "object": "string", // Optional: Primary object of the action - "target": "string", // Optional: Secondary object/target of the action - "parameters": {}, // Optional: Additional parameters as key-value pairs - "confidence": number // How confident you are in this interpretation (0.0-1.0) -} - -Choose the action from the list of available actions. If the player's input is ambiguous or doesn't map well to an available action, choose the closest match and set a lower confidence score.`; - - const userPrompt = request.playerInput; - - return { - system: systemPrompt, - user: userPrompt - }; - } - - /** - * Build the system and user prompts for narrative generation - */ - private buildNarrativePrompt(request: NarrativeRequest): { system: string; user: string } { - const tone = request.tone || 'descriptive'; - - const systemPrompt = `You are an AI assistant that generates engaging narrative prose for an interactive fiction game. -Your task is to describe what happens when a player performs an action in the game world. - -Craft a vivid, ${tone} description that tells the player what happened as a result of their action. Make your prose engaging and atmospheric. - -Current room description: "${request.roomDescription}" -Visible objects: ${request.visibleObjects.join(', ')} -Visible characters: ${request.visibleCharacters.join(', ')} - -${request.previousContext ? `Previous context: ${request.previousContext}` : ''} - -Respond with engaging prose that describes the outcome of the player's action. -You can optionally include 1-3 subtle hints about interesting things to try next.`; - - const userPrompt = `The player has performed this action: "${request.action}". -The result of the action is: "${request.result}". -Please describe what happens in an engaging, narrative way.`; - - return { - system: systemPrompt, - user: userPrompt - }; - } - - /** - * Validate and normalize the action response - */ - private validateActionResponse(response: Record): ActionResponse { - const validatedResponse: ActionResponse = { - action: typeof response.action === 'string' ? response.action : 'look', - confidence: typeof response.confidence === 'number' ? response.confidence : 0.5 - }; - - if (typeof response.object === 'string') { - validatedResponse.object = response.object; - } - - if (typeof response.target === 'string') { - validatedResponse.target = response.target; - } - - if (response.parameters && typeof response.parameters === 'object') { - validatedResponse.parameters = response.parameters as Record; - } - - return validatedResponse; - } -} \ No newline at end of file diff --git a/src/server-yaml.ts b/src/server-yaml.ts deleted file mode 100644 index 7355126..0000000 --- a/src/server-yaml.ts +++ /dev/null @@ -1,321 +0,0 @@ -/** - * AI Interactive Fiction - Web Server - * Serves the web UI and handles WebSocket communication - */ - -import path from 'path'; -import express from 'express'; -import http from 'http'; -import { Server as SocketIOServer } from 'socket.io'; -import * as dotenv from 'dotenv'; -import { GameRunner } from './cli/game-runner'; -import { existsSync, mkdirSync, copyFileSync } from 'fs'; -import { - textToParagraphs, - TurnResult, -} from './interfaces/turn-result'; -import { - clientGameConfig, - ensureConfiguredAssetDirectories, - loadGameConfig, - projectPath, -} from './config/game-config'; - -// Load environment variables -dotenv.config(); - -// Create Express application -const app = express(); -const server = http.createServer(app); -const io = new SocketIOServer(server); - -// Get port from environment variables or use default -const DEFAULT_PORT = 3001; -const PORT = process.env.PORT ? parseInt(process.env.PORT) : DEFAULT_PORT; -const PORT_RANGE = 300; // Try enough ports to skip OS-excluded ranges. -const engineConfig = loadGameConfig( - process.env.YAML_CONFIG_FILE || './config/engines/yaml.json', - 'yaml', -); - -// Serve static files from the public directory. During local development the -// browser must not keep stale ES modules, otherwise UI fixes appear to do -// nothing until a hard cache clear. -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'); - } -})); - -app.get('/api/game-config', (_req, res) => { - res.json(clientGameConfig(engineConfig)); -}); - -// Set up game sessions -const gameSessions = new Map(); -const nextTurnIds = new Map(); - -function nextTurnId(socketId: string): number { - const current = nextTurnIds.get(socketId) || 1; - nextTurnIds.set(socketId, current + 1); - return current; -} - -function createTextTurn( - socketId: string, - text: string, - gameState: TurnResult['gameState'] = {}, - suggestions?: string[], -): TurnResult { - const paragraphs = textToParagraphs(text); - return { - turnId: nextTurnId(socketId), - paragraphs, - choices: [], - inputMode: 'text', - gameState, - suggestions, - }; -} - -function normalizeSaveSlot(slot: unknown): number { - const value = Number(slot); - return Number.isInteger(value) && value > 0 ? value : 1; -} - -async function startDemoGameForSocket(socket: any): Promise { - nextTurnIds.set(socket.id, 1); - const gameRunner = new GameRunner(); - const worldFile = projectPath(process.env.DEFAULT_WORLD_FILE || engineConfig.paths.mainGameFile); - - await gameRunner.initialize(worldFile); - gameSessions.set(socket.id, gameRunner); - - const gameState = gameRunner.getGameState(); - const paragraphs = [ - ...textToParagraphs(gameState.world.introduction), - ...textToParagraphs(gameRunner.getCurrentRoomDescription()), - ]; - socket.emit('narrativeResponse', { - turnId: nextTurnId(socket.id), - paragraphs, - choices: [], - inputMode: 'text', - gameState: { - currentRoomId: gameState.currentRoomId, - }, - }); - - return gameRunner; -} - -async function handleGameApi(socket: any, method: string, args: unknown[] = []) { - const saveGames: Map = socket.data.saveGames || new Map(); - socket.data.saveGames = saveGames; - - switch (method) { - case 'newGame': - case 'newGame()': - await startDemoGameForSocket(socket); - return { success: true, result: true, running: true, canLoad: saveGames.size > 0 }; - - case 'loadGame': - case 'loadGame()': { - const slot = normalizeSaveSlot(args[0]); - if (!saveGames.has(slot)) { - return { success: false, error: 'missing_save', result: false }; - } - await startDemoGameForSocket(socket); - socket.emit('gameLoaded', { slot }); - return { success: true, result: true, running: true, slot }; - } - - case 'saveGame': - case 'saveGame()': { - const gameRunner = gameSessions.get(socket.id); - if (!gameRunner) { - return { success: false, error: 'game_not_running', result: false }; - } - const slot = normalizeSaveSlot(args[0]); - saveGames.set(slot, gameRunner.getGameState()); - socket.emit('gameSaved', { slot }); - return { success: true, result: true, slot }; - } - - case 'hasSaveGame': - case 'hasSaveGame()': { - const slot = normalizeSaveSlot(args[0]); - return { success: true, result: saveGames.has(slot), slot }; - } - - case 'getSaveGames': - case 'getSaveGames()': - return { success: true, result: Array.from(saveGames.keys()).sort((a, b) => a - b) }; - - case 'isGameRunning': - case 'isGameRunning()': - return { success: true, result: gameSessions.has(socket.id) }; - - default: - return { success: false, error: `unknown_method:${method}` }; - } -} - -// Handle socket connections -io.on('connection', (socket) => { - console.log(`New client connected: ${socket.id}`); - socket.emit('gameConfig', clientGameConfig(engineConfig)); - - socket.data.saveGames = new Map(); - - socket.on('gameApi', async (request, respond) => { - try { - const response = await handleGameApi(socket, String(request?.method || ''), Array.isArray(request?.args) ? request.args : []); - if (typeof respond === 'function') { - respond(response); - } - } catch (error) { - console.error('Game API error:', error); - if (typeof respond === 'function') { - respond({ success: false, error: error instanceof Error ? error.message : String(error) }); - } - } - }); - - // Process player command - socket.on('playerCommand', async (data) => { - try { - const gameRunner = gameSessions.get(socket.id); - - if (!gameRunner) { - socket.emit('error', { message: 'Game session not found. Please start a new game.' }); - return; - } - - const command = String(data?.command || '').trim(); - - // During typography and animation work, mirror the command back through - // the real socket path so the UI pipeline can be tested end to end. - socket.emit('narrativeResponse', createTextTurn(socket.id, command, { - currentRoomId: gameRunner.getGameState().currentRoomId - }, gameRunner.getSuggestions())); - - } catch (error) { - console.error('Error processing command:', error); - socket.emit('error', { message: 'Failed to process command. Please try again.' }); - } - }); - - // Handle disconnection - socket.on('disconnect', () => { - console.log(`Client disconnected: ${socket.id}`); - - // Clean up game session - if (gameSessions.has(socket.id)) { - gameSessions.delete(socket.id); - } - nextTurnIds.delete(socket.id); - }); -}); - -// Ensure required asset folders exist -function ensureDirectories() { - 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') - ]; - - for (const dir of dirs) { - if (!existsSync(dir)) { - mkdirSync(dir, { recursive: true }); - } - } - ensureConfiguredAssetDirectories(engineConfig); -} - -// Copy kokoro-js library from node_modules if not already present -function ensureKokoroJs() { - const source = path.join(__dirname, '../node_modules/kokoro-js/dist/index.js'); - const destination = path.join(__dirname, '../public/js/kokoro-js.js'); - - if (existsSync(source) && !existsSync(destination)) { - copyFileSync(source, destination); - console.log(`Copied kokoro-js from ${source} to ${destination}`); - } -} - -// Start the server with port fallback -export async function startServer(initialPort: number, range: number): Promise { - let currentPort = initialPort; - const maxPort = initialPort + range; - - // Try ports in the specified range - while (currentPort < maxPort) { - try { - // Ensure directories exist - ensureDirectories(); - - // Ensure kokoro-js is copied - try { - ensureKokoroJs(); - } catch (error) { - console.error('Error copying kokoro-js:', error); - } - - // Try to start the server on the current port - await new Promise((resolve, reject) => { - server.removeAllListeners('error'); - server.removeAllListeners('listening'); - server.once('listening', () => { - console.log(`AI Interactive Fiction web server running on http://localhost:${currentPort}`); - resolve(); - }); - server.once('error', (error: NodeJS.ErrnoException) => { - // If port is in use, try next port - if (error.code === 'EADDRINUSE' || error.code === 'EACCES') { - console.log(`Port ${currentPort} is unavailable (${error.code}), trying next port...`); - server.close(); - currentPort++; - reject(); - } else { - // For other errors, log and reject - console.error('Server error:', error); - reject(error); - } - }); - server.listen(currentPort); - }); - - // If we reach here, server started successfully - return; - - } catch (error) { - // If we reach the max port and still fail, throw an error - if (currentPort >= maxPort - 1) { - throw new Error(`Failed to start server on ports ${initialPort} to ${maxPort - 1}`); - } - - // Otherwise try the next port - // The loop continues as the rejection above increments currentPort - } - } -} - -// Start the server when this module is run directly -if (require.main === module) { - startServer(PORT, PORT_RANGE).catch(error => { - console.error('Failed to start server:', error); - process.exit(1); - }); -} - -export { app, server, io }; diff --git a/src/server-zcode.ts b/src/server-zcode.ts deleted file mode 100644 index 44dd7a2..0000000 --- a/src/server-zcode.ts +++ /dev/null @@ -1,375 +0,0 @@ -/** - * Z-code LLM Server - * - * Starts an Express + Socket.IO server that runs Zork I through the - * ZcodeLlmEngine and serves the same shared client UI as the YAML engine. - * - * Usage: - * npm run dev:zcode (development, with file watching) - * npm run start:zcode (production, from compiled dist/) - * - * Environment variables: - * PORT – HTTP port (default: 3002) - * ZCODE_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 { ZcodeLlmEngine, ZcodeTurnResult } from './engine/zcode-llm-engine'; -import { - clientGameConfig, - ensureConfiguredAssetDirectories, - loadGameConfig, - projectPath, -} from './config/game-config'; - -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 = 300; -const DEBUG_ENABLED = /^(1|true|yes|on)$/i.test(process.env.ZCODE_DEBUG ?? ''); -const engineConfig = loadGameConfig( - process.env.ZCODE_CONFIG_FILE || './config/engines/zcode.json', - 'zcode', -); - -function debugLog(message: string, details?: unknown): void { - if (!DEBUG_ENABLED) return; - if (typeof details === 'undefined') { - console.log(`[zcode:debug] ${message}`); - return; - } - console.log(`[zcode: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'); - }, - }), -); - -app.get('/api/game-config', (_req, res) => { - res.json(clientGameConfig(engineConfig)); -}); - -// One engine instance per connected socket -const sessions = new Map(); -// Save-game slot maps: socketId → Map -const saveSlots = new Map>(); - -function toClientTurn(turn: ZcodeTurnResult): ZcodeTurnResult { - return { - ...turn, - gameState: { - ...turn.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): ZcodeLlmEngine { - let engine = sessions.get(socketId); - if (!engine) { - engine = new ZcodeLlmEngine({ - storyPath: projectPath(process.env.ZCODE_STORY_FILE || engineConfig.paths.mainGameFile), - promptDir: projectPath(engineConfig.paths.promptDir || 'data/zcode-prompts'), - }); - 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', toClientTurn(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', toClientTurn(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 = projectPath(process.env.ZCODE_STORY_FILE ?? engineConfig.paths.mainGameFile); - const promptDir = projectPath(engineConfig.paths.promptDir || 'data/zcode-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('[zcode] Missing OPENROUTER_API_KEY in environment.'); - } - if (!process.env.OPENROUTER_MODEL) { - console.error('[zcode] Missing OPENROUTER_MODEL in environment.'); - } - if (!existsSync(storyPath)) { - console.error(`[zcode] Story file missing: ${storyPath}`); - console.error('[zcode] Place zork1.bin in ./data/z-code/ or set ZCODE_STORY_FILE.'); - } - if (missingPrompts.length > 0) { - console.error('[zcode] 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(`[zcode] Client connected: ${socket.id}`); - socket.emit('gameConfig', clientGameConfig(engineConfig)); - - 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('[zcode] 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: ZcodeTurnResult = await engine.processInput(input); - debugLog(`narrativeResponse to ${socket.id}`, { - inputMode: turn.inputMode, - paragraphs: turn.paragraphs.length, - statusLine: turn.gameState?.statusLine, - }); - socket.emit('narrativeResponse', toClientTurn(turn)); - } catch (error) { - console.error('[zcode] playerCommand error:', error); - socket.emit('error', { - message: - error instanceof Error ? error.message : 'An error occurred.', - }); - } - }, - ); - - socket.on('disconnect', () => { - console.log(`[zcode] 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/zcode-prompts'), - ]; - for (const dir of dirs) { - if (!existsSync(dir)) mkdirSync(dir, { recursive: true }); - } - ensureConfiguredAssetDirectories(engineConfig); -} - -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.removeAllListeners('error'); - server.removeAllListeners('listening'); - server.once('listening', () => { - console.log( - `[zcode] Z-code Narrator server running on http://localhost:${port}`, - ); - resolve(); - }); - server.once('error', (err: NodeJS.ErrnoException) => { - if (err.code === 'EADDRINUSE' || err.code === 'EACCES') { - console.log(`Port ${port} unavailable (${err.code}), trying ${port + 1}...`); - server.close(); - port++; - reject(); - } else { - reject(err); - } - }); - server.listen(port); - }); - 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('[zcode] Failed to start:', err); - process.exit(1); - }); -} diff --git a/src/test-server-yaml.ts b/src/test-server-yaml.ts deleted file mode 100644 index 38a01e0..0000000 --- a/src/test-server-yaml.ts +++ /dev/null @@ -1,263 +0,0 @@ -/** - * Test Server for AI Interactive Fiction - * Simplified version that sends test paragraphs instead of using LLM - */ - -import path from 'path'; -import express from 'express'; -import http from 'http'; -import { Server as SocketIOServer } from 'socket.io'; -import * as dotenv from 'dotenv'; -import { existsSync, mkdirSync, copyFileSync } from 'fs'; -import { textToParagraphs } from './interfaces/turn-result'; - -// Load environment variables -dotenv.config(); - -// Create Express application -const app = express(); -const server = http.createServer(app); -const io = new SocketIOServer(server); - -// Get port from environment variables or use default -const DEFAULT_PORT = 3001; -const PORT = process.env.PORT ? parseInt(process.env.PORT) : DEFAULT_PORT; -const PORT_RANGE = 300; // Try enough ports to skip OS-excluded ranges. - -// Serve static files from the public directory. Keep browser modules uncached -// during local development so fixes are visible without a hard cache clear. -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'); - } -})); - -// Test paragraphs to send to the client -const TEST_PARAGRAPHS = [ - "You stand at the entrance of a mysterious cave. The air is cool and damp, carrying the scent of earth and ancient stone. Shadows dance on the walls as your torch flickers in the gentle breeze.", - "As you venture deeper, the passage narrows. Stalactites hang from the ceiling like stone daggers, their surfaces glistening with moisture. The sound of dripping water echoes through the silence.", - "Suddenly, the passage opens into a vast chamber. Crystal formations catch the light of your torch, sending rainbow reflections across the walls. In the center of the room stands an ancient stone pedestal, its surface carved with symbols from a forgotten language." -]; - -// Handle socket connections -io.on('connection', (socket) => { - console.log(`New client connected: ${socket.id}`); - let currentParagraphIndex = 0; - let gameRunning = false; - let nextTurnId = 1; - const saveGames = new Set(); - - const startDemoGame = () => { - gameRunning = true; - nextTurnId = 1; - currentParagraphIndex = 0; - socket.emit('narrativeResponse', { - turnId: nextTurnId++, - paragraphs: [ - ...textToParagraphs("#chapter[Interactive Fiction Test]\n\nWelcome to the Interactive Fiction Test. This is a simplified version that sends predefined paragraphs instead of using an LLM."), - ...textToParagraphs(TEST_PARAGRAPHS[0]), - ], - choices: [], - inputMode: 'text', - gameState: { - currentRoomId: 'test-room', - }, - }); - }; - - const normalizeSaveSlot = (slot: unknown): number => { - const value = Number(slot); - return Number.isInteger(value) && value > 0 ? value : 1; - }; - - socket.on('gameApi', (request, respond) => { - try { - const method = String(request?.method || ''); - const args = Array.isArray(request?.args) ? request.args : []; - let response: any; - - switch (method) { - case 'newGame': - case 'newGame()': - startDemoGame(); - response = { success: true, result: true, running: true, canLoad: saveGames.size > 0 }; - break; - case 'loadGame': - case 'loadGame()': { - const slot = normalizeSaveSlot(args[0]); - if (!saveGames.has(slot)) { - response = { success: false, error: 'missing_save', result: false }; - break; - } - startDemoGame(); - socket.emit('gameLoaded', { slot }); - response = { success: true, result: true, running: true, slot }; - break; - } - case 'saveGame': - case 'saveGame()': { - if (!gameRunning) { - response = { success: false, error: 'game_not_running', result: false }; - break; - } - const slot = normalizeSaveSlot(args[0]); - saveGames.add(slot); - socket.emit('gameSaved', { slot }); - response = { success: true, result: true, slot }; - break; - } - case 'hasSaveGame': - case 'hasSaveGame()': { - const slot = normalizeSaveSlot(args[0]); - response = { success: true, result: saveGames.has(slot), slot }; - break; - } - case 'getSaveGames': - case 'getSaveGames()': - response = { success: true, result: Array.from(saveGames).sort((a, b) => a - b) }; - break; - case 'isGameRunning': - case 'isGameRunning()': - response = { success: true, result: gameRunning }; - break; - default: - response = { success: false, error: `unknown_method:${method}` }; - } - - if (typeof respond === 'function') respond(response); - } catch (error) { - if (typeof respond === 'function') { - respond({ success: false, error: error instanceof Error ? error.message : String(error) }); - } - } - }); - - // Process player command - socket.on('playerCommand', async (data) => { - try { - console.log(`Received command: ${data.command}`); - - // Send narrative response to client - socket.emit('narrativeResponse', { - turnId: nextTurnId++, - paragraphs: textToParagraphs(String(data.command || '')), - choices: [], - inputMode: 'text', - gameState: { - currentRoomId: "test-room" - }, - suggestions: ["look around", "examine pedestal", "touch crystals"] - }); - - } catch (error) { - console.error('Error processing command:', error); - socket.emit('error', { message: 'Failed to process command. Please try again.' }); - } - }); - - // Handle disconnection - socket.on('disconnect', () => { - console.log(`Client disconnected: ${socket.id}`); - }); -}); - -// Ensure required asset folders exist -function ensureDirectories() { - 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') - ]; - - for (const dir of dirs) { - if (!existsSync(dir)) { - mkdirSync(dir, { recursive: true }); - } - } -} - -// Copy kokoro-js library from node_modules if not already present -function ensureKokoroJs() { - const source = path.join(__dirname, '../node_modules/kokoro-js/dist/index.js'); - const destination = path.join(__dirname, '../public/js/kokoro-js.js'); - - if (existsSync(source) && !existsSync(destination)) { - copyFileSync(source, destination); - console.log(`Copied kokoro-js from ${source} to ${destination}`); - } -} - -// Start the server with port fallback -async function startServer(initialPort: number, range: number): Promise { - let currentPort = initialPort; - const maxPort = initialPort + range; - - // Try ports in the specified range - while (currentPort < maxPort) { - try { - // Ensure directories exist - ensureDirectories(); - - // Ensure kokoro-js is copied - try { - ensureKokoroJs(); - } catch (error) { - console.error('Error copying kokoro-js:', error); - } - - // Try to start the server on the current port - await new Promise((resolve, reject) => { - server.removeAllListeners('error'); - server.removeAllListeners('listening'); - server.once('listening', () => { - console.log(`AI Interactive Fiction TEST SERVER running on http://localhost:${currentPort}`); - console.log('This server is sending predefined test paragraphs instead of using an LLM'); - resolve(); - }); - server.once('error', (error: NodeJS.ErrnoException) => { - // If port is in use, try next port - if (error.code === 'EADDRINUSE' || error.code === 'EACCES') { - console.log(`Port ${currentPort} is unavailable (${error.code}), trying next port...`); - server.close(); - currentPort++; - reject(); - } else { - // For other errors, log and reject - console.error('Server error:', error); - reject(error); - } - }); - server.listen(currentPort); - }); - - // If we reach here, server started successfully - return; - - } catch (error) { - // If we reach the max port and still fail, throw an error - if (currentPort >= maxPort - 1) { - throw new Error(`Failed to start server on ports ${initialPort} to ${maxPort - 1}`); - } - - // Otherwise try the next port - } - } -} - -// Start the server when this module is run directly -if (require.main === module) { - startServer(PORT, PORT_RANGE).catch(error => { - console.error('Failed to start server:', error); - process.exit(1); - }); -} - -export { app, server, io }; diff --git a/src/world-model/yaml-parser.ts b/src/world-model/yaml-parser.ts deleted file mode 100644 index 8291322..0000000 --- a/src/world-model/yaml-parser.ts +++ /dev/null @@ -1,429 +0,0 @@ -/** - * YAML World Model Parser - * Loads and validates world definitions from YAML files - */ - -import * as fs from 'fs/promises'; -import * as yaml from 'js-yaml'; -import { WorldModel } from '../interfaces/world-model'; - -export class YamlWorldParser { - /** - * Load a world model from a YAML file - */ - public static async loadFromFile(filePath: string): Promise { - try { - const fileContents = await fs.readFile(filePath, 'utf8'); - const worldData = yaml.load(fileContents) as unknown; - - return this.validateAndTransform(worldData); - } catch (error) { - console.error(`Error loading world from ${filePath}:`, error); - throw new Error(`Failed to load world from ${filePath}: ${error instanceof Error ? error.message : String(error)}`); - } - } - - /** - * Validate the loaded YAML data and transform it into a WorldModel - */ - private static validateAndTransform(data: unknown): WorldModel { - if (!data || typeof data !== 'object') { - throw new Error('Invalid world data: must be an object'); - } - - const worldData = data as Record; - - // Validate required top-level fields - this.validateRequiredFields(worldData, ['title', 'author', 'version', 'introduction', 'rooms', 'initialState']); - - // Transform and validate the world model - const worldModel: WorldModel = { - title: this.validateString(worldData.title, 'title'), - author: this.validateString(worldData.author, 'author'), - version: this.validateString(worldData.version, 'version'), - introduction: this.validateString(worldData.introduction, 'introduction'), - rooms: this.validateRooms(worldData.rooms), - objects: this.validateObjects(worldData.objects), - characters: this.validateCharacters(worldData.characters), - actions: this.validateActions(worldData.actions), - initialState: this.validateInitialState(worldData.initialState) - }; - - // Validate references between entities - this.validateReferences(worldModel); - - return worldModel; - } - - /** - * Validate that an object has all required fields - */ - private static validateRequiredFields(data: Record, requiredFields: string[]): void { - for (const field of requiredFields) { - if (!(field in data)) { - throw new Error(`Missing required field: ${field}`); - } - } - } - - /** - * Validate that a value is a string - */ - private static validateString(value: unknown, fieldName: string): string { - if (typeof value !== 'string') { - throw new Error(`Field ${fieldName} must be a string`); - } - return value; - } - - /** - * Validate room definitions - */ - private static validateRooms(rooms: unknown): WorldModel['rooms'] { - if (!rooms || typeof rooms !== 'object') { - throw new Error('Rooms must be an object mapping room IDs to room definitions'); - } - - const roomsData = rooms as Record; - const validatedRooms: WorldModel['rooms'] = {}; - - for (const [roomId, roomData] of Object.entries(roomsData)) { - if (!roomData || typeof roomData !== 'object') { - throw new Error(`Room ${roomId} must be an object`); - } - - const room = roomData as Record; - this.validateRequiredFields(room, ['name', 'description', 'exits']); - - validatedRooms[roomId] = { - id: roomId, - name: this.validateString(room.name, `rooms.${roomId}.name`), - description: this.validateString(room.description, `rooms.${roomId}.description`), - exits: this.validateExits(room.exits, roomId), - objects: this.validateStringArray(room.objects || [], `rooms.${roomId}.objects`), - characters: this.validateStringArray(room.characters || [], `rooms.${roomId}.characters`) - }; - } - - return validatedRooms; - } - - /** - * Validate exit definitions - */ - private static validateExits(exits: unknown, roomId: string): WorldModel['rooms'][string]['exits'] { - if (!Array.isArray(exits)) { - throw new Error(`Exits for room ${roomId} must be an array`); - } - - return exits.map((exit, index) => { - if (!exit || typeof exit !== 'object') { - throw new Error(`Exit ${index} in room ${roomId} must be an object`); - } - - const exitData = exit as Record; - this.validateRequiredFields(exitData, ['direction', 'targetRoomId']); - - return { - direction: this.validateString(exitData.direction, `rooms.${roomId}.exits[${index}].direction`), - targetRoomId: this.validateString(exitData.targetRoomId, `rooms.${roomId}.exits[${index}].targetRoomId`), - description: exitData.description ? this.validateString(exitData.description, `rooms.${roomId}.exits[${index}].description`) : undefined, - isLocked: typeof exitData.isLocked === 'boolean' ? exitData.isLocked : false, - keyId: exitData.keyId ? this.validateString(exitData.keyId, `rooms.${roomId}.exits[${index}].keyId`) : undefined - }; - }); - } - - /** - * Validate object definitions - */ - private static validateObjects(objects: unknown): WorldModel['objects'] { - if (!objects) return {}; // Objects are optional - - if (typeof objects !== 'object') { - throw new Error('Objects must be an object mapping object IDs to object definitions'); - } - - const objectsData = objects as Record; - const validatedObjects: WorldModel['objects'] = {}; - - for (const [objectId, objectData] of Object.entries(objectsData)) { - if (!objectData || typeof objectData !== 'object') { - throw new Error(`Object ${objectId} must be an object`); - } - - const obj = objectData as Record; - this.validateRequiredFields(obj, ['name', 'description', 'traits', 'allowedActions']); - - validatedObjects[objectId] = { - id: objectId, - name: this.validateString(obj.name, `objects.${objectId}.name`), - description: this.validateString(obj.description, `objects.${objectId}.description`), - traits: this.validateStringArray(obj.traits, `objects.${objectId}.traits`), - states: this.validateObjectStates(obj.states, objectId), - allowedActions: this.validateStringArray(obj.allowedActions, `objects.${objectId}.allowedActions`), - containedObjects: obj.containedObjects ? this.validateStringArray(obj.containedObjects, `objects.${objectId}.containedObjects`) : [] - }; - } - - return validatedObjects; - } - - /** - * Validate character definitions - */ - private static validateCharacters(characters: unknown): WorldModel['characters'] { - if (!characters) return {}; // Characters are optional - - if (typeof characters !== 'object') { - throw new Error('Characters must be an object mapping character IDs to character definitions'); - } - - const charactersData = characters as Record; - const validatedCharacters: WorldModel['characters'] = {}; - - for (const [characterId, characterData] of Object.entries(charactersData)) { - if (!characterData || typeof characterData !== 'object') { - throw new Error(`Character ${characterId} must be an object`); - } - - const character = characterData as Record; - this.validateRequiredFields(character, ['name', 'description', 'dialogue', 'defaultResponse']); - - validatedCharacters[characterId] = { - id: characterId, - name: this.validateString(character.name, `characters.${characterId}.name`), - description: this.validateString(character.description, `characters.${characterId}.description`), - dialogue: this.validateDialogue(character.dialogue, characterId), - inventory: this.validateStringArray(character.inventory || [], `characters.${characterId}.inventory`), - defaultResponse: this.validateString(character.defaultResponse, `characters.${characterId}.defaultResponse`), - mood: character.mood ? this.validateString(character.mood, `characters.${characterId}.mood`) : undefined - }; - } - - return validatedCharacters; - } - - /** - * Validate action definitions - */ - private static validateActions(actions: unknown): WorldModel['actions'] { - if (!actions) return {}; // Actions are optional - - if (typeof actions !== 'object') { - throw new Error('Actions must be an object mapping action names to action definitions'); - } - - const actionsData = actions as Record; - const validatedActions: WorldModel['actions'] = {}; - - for (const [actionName, actionData] of Object.entries(actionsData)) { - if (!actionData || typeof actionData !== 'object') { - throw new Error(`Action ${actionName} must be an object`); - } - - const action = actionData as Record; - this.validateRequiredFields(action, ['patterns', 'handler']); - - validatedActions[actionName] = { - name: actionName, - patterns: this.validateStringArray(action.patterns, `actions.${actionName}.patterns`), - requiresObject: typeof action.requiresObject === 'boolean' ? action.requiresObject : false, - requiresTarget: typeof action.requiresTarget === 'boolean' ? action.requiresTarget : false, - handler: this.validateString(action.handler, `actions.${actionName}.handler`) - }; - } - - return validatedActions; - } - - /** - * Validate initial game state - */ - private static validateInitialState(initialState: unknown): WorldModel['initialState'] { - if (!initialState || typeof initialState !== 'object') { - throw new Error('Initial state must be an object'); - } - - const stateData = initialState as Record; - this.validateRequiredFields(stateData, ['currentRoomId']); - - return { - currentRoomId: this.validateString(stateData.currentRoomId, 'initialState.currentRoomId'), - inventory: this.validateStringArray(stateData.inventory || [], 'initialState.inventory'), - visitedRooms: this.validateStringArray(stateData.visitedRooms || [], 'initialState.visitedRooms'), - flags: this.validateFlags(stateData.flags), - counters: this.validateCounters(stateData.counters) - }; - } - - /** - * Validate object states (record of boolean values) - */ - private static validateObjectStates(states: unknown, objectId: string): Record { - if (!states) return {}; - - if (typeof states !== 'object') { - throw new Error(`States for object ${objectId} must be an object`); - } - - const statesData = states as Record; - const validatedStates: Record = {}; - - for (const [stateName, stateValue] of Object.entries(statesData)) { - if (typeof stateValue !== 'boolean') { - throw new Error(`State ${stateName} for object ${objectId} must be a boolean value`); - } - validatedStates[stateName] = stateValue; - } - - return validatedStates; - } - - /** - * Validate dialogue (record of string values) - */ - private static validateDialogue(dialogue: unknown, characterId: string): Record { - if (!dialogue || typeof dialogue !== 'object') { - throw new Error(`Dialogue for character ${characterId} must be an object`); - } - - const dialogueData = dialogue as Record; - const validatedDialogue: Record = {}; - - for (const [topic, response] of Object.entries(dialogueData)) { - validatedDialogue[topic] = this.validateString(response, `characters.${characterId}.dialogue.${topic}`); - } - - return validatedDialogue; - } - - /** - * Validate flags (record of boolean values) - */ - private static validateFlags(flags: unknown): Record { - if (!flags) return {}; - - if (typeof flags !== 'object') { - throw new Error('Flags must be an object'); - } - - const flagsData = flags as Record; - const validatedFlags: Record = {}; - - for (const [flagName, flagValue] of Object.entries(flagsData)) { - if (typeof flagValue !== 'boolean') { - throw new Error(`Flag ${flagName} must be a boolean value`); - } - validatedFlags[flagName] = flagValue; - } - - return validatedFlags; - } - - /** - * Validate counters (record of number values) - */ - private static validateCounters(counters: unknown): Record { - if (!counters) return {}; - - if (typeof counters !== 'object') { - throw new Error('Counters must be an object'); - } - - const countersData = counters as Record; - const validatedCounters: Record = {}; - - for (const [counterName, counterValue] of Object.entries(countersData)) { - if (typeof counterValue !== 'number') { - throw new Error(`Counter ${counterName} must be a numeric value`); - } - validatedCounters[counterName] = counterValue; - } - - return validatedCounters; - } - - /** - * Validate that an array of strings is valid - */ - private static validateStringArray(arr: unknown, fieldName: string): string[] { - if (!arr) return []; - - if (!Array.isArray(arr)) { - throw new Error(`Field ${fieldName} must be an array`); - } - - return arr.map((item, index) => { - if (typeof item !== 'string') { - throw new Error(`Item at index ${index} in ${fieldName} must be a string`); - } - return item; - }); - } - - /** - * Validate references between entities - */ - private static validateReferences(worldModel: WorldModel): void { - const { rooms, objects, characters, initialState } = worldModel; - - // Check that the initial room exists - if (!rooms[initialState.currentRoomId]) { - throw new Error(`Initial room ${initialState.currentRoomId} does not exist`); - } - - // Check room exits - for (const [roomId, room] of Object.entries(rooms)) { - for (const exit of room.exits) { - if (!rooms[exit.targetRoomId]) { - throw new Error(`Room ${roomId} has an exit to non-existent room ${exit.targetRoomId}`); - } - if (exit.keyId && !objects[exit.keyId]) { - throw new Error(`Room ${roomId} has an exit requiring non-existent key ${exit.keyId}`); - } - } - - // Check room objects - for (const objectId of room.objects) { - if (!objects[objectId]) { - throw new Error(`Room ${roomId} contains non-existent object ${objectId}`); - } - } - - // Check room characters - for (const characterId of room.characters) { - if (!characters[characterId]) { - throw new Error(`Room ${roomId} contains non-existent character ${characterId}`); - } - } - } - - // Check object containment - for (const [objectId, object] of Object.entries(objects)) { - if (object.containedObjects) { - for (const containedId of object.containedObjects) { - if (!objects[containedId]) { - throw new Error(`Object ${objectId} contains non-existent object ${containedId}`); - } - } - } - } - - // Check character inventory - for (const [characterId, character] of Object.entries(characters)) { - for (const objectId of character.inventory) { - if (!objects[objectId]) { - throw new Error(`Character ${characterId} has non-existent object ${objectId} in inventory`); - } - } - } - - // Check player inventory - for (const objectId of initialState.inventory) { - if (!objects[objectId]) { - throw new Error(`Initial inventory contains non-existent object ${objectId}`); - } - } - } -} \ No newline at end of file