32 Commits

Author SHA1 Message Date
Georg 7c5d194376 Stabilize TTS voice reload and reconnect logging 2026-05-19 17:10:21 +02:00
Georg 256cc2c7a7 Fix autosave resume choice restoration 2026-05-19 15:44:40 +02:00
Georg 90f81ee1b7 Prepare Ink Coolify release branch 2026-05-19 15:25:23 +02:00
Georg 9111dedaa2 Archive prototype outside main tree 2026-05-19 14:39:09 +02:00
Georg ebc8e1c7df Add Ink session recovery and Coolify Docker support 2026-05-19 13:14:46 +02:00
Georg dbcb8f4284 Consolidate engine docs and naming 2026-05-19 11:09:37 +02:00
Georg 121b174f2c Add glossary hover presentation 2026-05-19 07:34:52 +02:00
Georg 751ac5f62b Stabilize playback state and cursor feedback 2026-05-18 20:57:20 +02:00
Georg 6e908037fb Preload media assets and refine process cursors 2026-05-18 11:15:39 +02:00
Georg 4f6300042c Fix portrait image flow and drop-cap spacing 2026-05-18 03:08:23 +02:00
Georg d7bb175167 Checkpoint current UI and ink integration state 2026-05-18 02:46:02 +02:00
Georg 2c54498ee2 Document markup and improve choice tags 2026-05-17 15:52:41 +02:00
Georg c2fb27b6b8 Tighten detailed book page calibration 2026-05-16 22:19:12 +02:00
Georg e1a5d5809d Adapt book skin to detailed artwork 2026-05-16 22:06:56 +02:00
Georg f8911f6fc8 Keep live appends out of history reflow 2026-05-16 21:46:44 +02:00
Georg e368d252ad Refine line-based story scrolling 2026-05-16 21:40:36 +02:00
Georg b9ae7f71c5 Checkpoint line-grid renderer state 2026-05-16 15:57:03 +02:00
Georg fe33e4f0ab Checkpoint before line-grid scrolling refactor 2026-05-16 13:44:03 +02:00
Georg 42582352d6 Add storage-backed story history 2026-05-15 21:58:30 +02:00
Georg f2e786d5bc Add ink integration UI and media playback 2026-05-15 21:23:46 +02:00
Georg 44dc64f830 Add Ink integration notes 2026-05-15 08:11:35 +02:00
Georg 6faee20268 Add Zork engine integration work 2026-05-15 07:55:05 +02:00
Georg b8fe8535aa Fix story page scrolling and ellipsis spacing 2026-05-15 07:35:27 +02:00
Georg 74be77b267 Updated animations. 2026-05-14 23:19:06 +02:00
Georg 9a6bb009f2 Fixed Ducking. Refined UI. 2026-05-14 23:18:30 +02:00
Georg b5829ed773 Remove consolidated reference documentation 2026-05-14 21:18:54 +02:00
Georg 873049f7e6 Checkpoint current interactive fiction state 2026-05-14 21:17:43 +02:00
Georg c745efd1d2 Latest state before reworking with cluade 4.6. 2026-02-12 22:44:44 +00:00
Georg b1387f4833 Fixed kokoro loading process. 2025-04-07 06:51:45 +00:00
Georg 0842cbfefc Cleaned persistence manager, updated ui-options connectivity 2025-04-06 19:35:05 +00:00
Georg 0ab639fd25 Refactored modules and updated loader. 2025-04-06 18:35:04 +00:00
Georg fc693ae695 Fix Kokoro TTS integration issues: Remove API key requirement and ensure system-specific options display correctly 2025-04-05 22:06:22 +00:00
524 changed files with 946090 additions and 20835 deletions
-19
View File
@@ -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
-25
View File
@@ -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"
}
+10
View File
@@ -0,0 +1,10 @@
node_modules
dist
.git
.env
.env.*
!.env.example
npm-debug.log*
coverage
.nyc_output
data/ink-src/eibenreith.old.ink
+2 -1
View File
@@ -1,6 +1,7 @@
# OpenRouter API Configuration
OPENROUTER_API_KEY=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
OPENROUTER_MODEL=anthropic/claude-3-opus-20240229
OPENROUTER_MODEL=openai/gpt-5.5
OPENROUTER_REASONING_EFFORT=none
# Application Configuration
PORT=3001
+4 -1
View File
@@ -1,10 +1,13 @@
# OpenRouter API Configuration
OPENROUTER_API_KEY=your_openrouter_api_key_here
OPENROUTER_MODEL=your_selected_model_here
OPENROUTER_MODEL=openai/gpt-5.5
# GPT-5 reasoning tokens can consume short completion budgets; keep narration calls direct by default.
OPENROUTER_REASONING_EFFORT=none
# Application Configuration
PORT=3000
NODE_ENV=development
# Game Configuration
DEFAULT_GAME_ENGINE=ink
DEFAULT_WORLD_FILE=./data/worlds/example_world.yml
-23
View File
@@ -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
}
}
+5
View File
@@ -2,3 +2,8 @@ node_modules
# windsurf rules
.windsurfrules
# local inspection / generated scratch artifacts
.tmp/
*.orig
*.bkp
+1
View File
@@ -0,0 +1 @@
22
+29
View File
@@ -0,0 +1,29 @@
FROM node:22-bookworm-slim AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:22-bookworm-slim AS runtime
WORKDIR /app
ENV NODE_ENV=production
ENV DEFAULT_GAME_ENGINE=ink
ENV PORT=3000
COPY package*.json ./
RUN npm ci --omit=dev && npm cache clean --force
COPY --from=build /app/dist ./dist
COPY --from=build /app/public ./public
COPY --from=build /app/config ./config
COPY --from=build /app/data ./data
COPY --from=build /app/scripts ./scripts
EXPOSE 3000
CMD ["node", "dist/server-ink.js"]
+89 -24
View File
@@ -1,38 +1,103 @@
# AI Interactive Fiction
# AI Interactive Fiction - Ink Coolify Release
A modern take on classic text adventures that combines traditional world modeling with Large Language Models (LLMs) to create natural language interactive fiction experiences.
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.
## Project Overview
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`.
This application reimagines the classic text adventure game genre by replacing the traditional parser with an LLM. The system consists of:
## Local Ink Development
1. **World Model**: A traditional game engine that manages rooms, objects, actions, and game state - similar to old-school Infocom games.
Use Node.js 22 LTS.
2. **LLM Interface**: An AI layer that processes natural language input from players and translates it into actions the game engine can understand.
```powershell
nvm install 22
nvm use 22
npm install
npm run dev
```
3. **Narrative Generation**: The LLM converts the world state changes into rich, contextual prose for the player.
`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.
## Key Features
Useful commands:
- **Natural Language Understanding**: Players can express their intent in plain language without worrying about specific command syntax.
- **Rich Narrative**: Dynamic descriptions that adapt to the current game state and player history.
- **Consistent World Model**: The underlying game engine enforces world rules to prevent hallucinations or inconsistencies.
- **Modular Design**: Easily swap between different world models, including YAML-based custom worlds or integrations with classic Z-machine games.
```powershell
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
```
## How It Works
Set `PORT` to choose the server port. The Docker image defaults to `3000`.
1. Player enters natural language input
2. LLM analyzes input and translates it into game actions
3. Game engine processes valid actions and updates the game state
4. LLM receives the state change information and generates narrative prose
5. Player receives the beautifully written response
## Coolify 4 Deployment
## Technical Structure
Configure Coolify to deploy this branch with the repository `Dockerfile`.
- YAML-based world definition (rooms, objects, actions)
- OpenRouter API integration for accessing suitable LLMs
- Modular design allowing for Z-machine integration in the future
Recommended environment:
## Getting Started
```text
NODE_ENV=production
PORT=3000
INK_CONFIG_FILE=./config/engines/ink.json
```
[Installation and running instructions will be added here]
Coolify can watch `release/coolify-ink` and redeploy on webhook pushes. The intended flow is:
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.
The container builds TypeScript during image build and compiles the configured Ink source at server startup.
## Ink Configuration
The active game is configured in `config/engines/ink.json`.
Important paths:
- `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.
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.
## Browser Client
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.
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.
## Story Tags
Ink tags are parsed server-side into structured output objects. The client consumes structured turn data only.
Common tags:
```text
#chapter[Title]
#section
#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]
```
Choice-local tags:
```text
#key:x
#optional
#action[name]
```
Explicit choice keys are reserved first. Remaining choices receive keys from `1` through `0`, then `a` through `z`.
+7
View File
@@ -0,0 +1,7 @@
# Third-Party Notices
The browser-visible third-party notices and license text live at:
`public/THIRD_PARTY_NOTICES.md`
That file is served by the game UI and is the source used by the in-game credits dialog.
-106
View File
@@ -1,106 +0,0 @@
# Module System Refactoring TODO
## High Priority (Critical Architectural Issues)
### 1. Asynchronous Flow Control Improvements
- [ ] Remove all `setTimeout` calls used for synchronization in modules
- [X] Replace timeout in `browser-tts-handler.js` with proper Promise handling for voice loading
- [X] Eliminate race condition in `tts-player.js` that uses a hard-coded 1000ms timeout
- [ ] Remove all `setTimeout` calls in `ui-controller.js` for UI updates
- [ ] Implement proper Promise-based flow control in all modules
- [ ] Update `kokoro-handler.js` to correctly handle loading events
- [ ] Ensure all `async/await` patterns follow best practices
- [ ] Fix race conditions in module loading sequences
### 2. Module State Management
- [ ] Fix premature reporting of `FINISHED` state
- [ ] Ensure `tts-player.js` properly waits for Kokoro loading before reporting FINISHED
- [ ] Add proper state checks in all modules before reporting FINISHED
- [ ] Implement proper state transition reporting
- [ ] Update modules to use event system for reporting state transitions
- [ ] Add better error handling during module initialization
### 3. Module Dependencies & Loading
- [ ] Fix missing dependency declarations
- [ ] Update `ui-controller.js` to properly declare its TTS dependency
- [ ] Ensure all modules correctly specify all dependencies
- [ ] Remove dependency availability checks within modules
- [ ] Remove conditional checks like `if (!this.ttsHandler)` in `ui-controller.js`
- [ ] Rely on the module loader for dependency management
## Medium Priority (Functionality & Implementation Issues)
### 4. TTS Handler Implementation
- [ ] Implement missing `tts-handler.js` file content
- [ ] Create proper implementation with consistent interface
- [ ] Ensure it uses proper event-based communication
- [ ] Fix inconsistent event usage across TTS handlers
- [ ] Replace direct callbacks with event system
- [ ] Standardize event names and parameters
### 5. Animation Queue Enhancements
- [ ] Implement proper queue control mechanisms
- [ ] Add pause/resume functionality
- [ ] Implement more robust animation timing
- [ ] Add priority management for animations
### 6. UI Controller Cleanup
- [ ] Fix duplicate methods in UI Controller
- [ ] Deduplicate code for creating UI elements
- [ ] Consolidate event handling functions
- [ ] Remove redundant `ModuleEvent` class implementation
- [ ] Use the shared implementation from `base-module.js`
### 7. Kokoro Loading Implementation
- [ ] Implement proper `requestIdleCallback` for Kokoro loading
- [ ] Follow the pattern described in the specification
- [ ] Add progress reporting during Kokoro loading
- [ ] Fix event handling for loading completion
## Lower Priority (Refinements & Optimizations)
### 8. Code Quality & Consistency
- [ ] Standardize module registration pattern
- [ ] Ensure all modules follow the same pattern
- [ ] Fix inconsistencies in export approaches
- [ ] Improve module progress reporting
- [ ] Make progress reporting more granular
- [ ] Add more descriptive status messages
### 9. Error Handling Improvements
- [ ] Add better error recovery mechanisms
- [ ] Implement fallbacks for critical failures
- [ ] Add user-friendly error messages
- [ ] Improve error logging
- [ ] Add structured error reporting
- [ ] Implement debugging tools
### 10. Performance Optimizations
- [ ] Optimize module loading sequence
- [ ] Prioritize critical modules
- [ ] Defer non-essential loading
- [ ] Improve resource utilization
- [ ] Minimize memory footprint
- [ ] Reduce CPU usage during animations
## Documentation & Testing
### 11. Documentation
- [ ] Add JSDoc comments to all public methods
- [ ] Create architectural documentation
- [ ] Document module dependencies
- [ ] Explain event system
- [ ] Add example usage for modules
### 12. Testing
- [ ] Create unit tests for modules
- [ ] Implement integration tests for module system
- [ ] Add browser compatibility tests
## Future Enhancements
### 13. New Features
- [ ] Add module versioning support
- [ ] Implement module hot-reloading
- [ ] Create plugin system for extending modules
- [ ] Add internationalization support for UI
-7
View File
@@ -1,7 +0,0 @@
{
"folders": [
{
"path": "."
}
]
}
+20
View File
@@ -0,0 +1,20 @@
{
"engine": "ink",
"locale": "de_DE",
"paths": {
"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"
},
"metadata": {
"title": "Eibenreith",
"author": "Georg Tomitsch",
"subtitle": "Ein Kaiserpunk Abenteuer",
"version": "0.1.0",
"language": "de_DE",
"copyright": "© 2026 Bad Tools Studio"
}
}
+11
View File
@@ -0,0 +1,11 @@
# Coolify environment for the Ink-only web deployment.
# Set these in Coolify's environment variable UI, not in a committed .env file.
NODE_ENV=production
DEFAULT_GAME_ENGINE=ink
PORT=3000
INK_CONFIG_FILE=./config/engines/ink.json
# Optional server-side LLM variables are only needed by non-Ink engines.
# OPENROUTER_API_KEY=
# OPENROUTER_MODEL=
+110
View File
@@ -0,0 +1,110 @@
// Eibenreith.ink
// Main index file for the German intro of the choice-based horror text game.
// This file contains global state, global functions, the initial divert, INCLUDE statements,
// and an index of important knots. Chapter content lives in separate .ink files.
// -----------------------------------------------------------------------------
// INCLUDES
// -----------------------------------------------------------------------------
INCLUDE eibenreith_01_zug.ink
INCLUDE eibenreith_02_bahnhof.ink
INCLUDE eibenreith_03_graben.ink
INCLUDE eibenreith_04_dorf.ink
// -----------------------------------------------------------------------------
// GLOBAL STATE
// -----------------------------------------------------------------------------
VAR birth_class = "unset"
VAR title_part = ""
VAR given_names = ""
VAR common_name = ""
VAR surname = ""
VAR religion_stance = "unset"
VAR supernatural_belief = "unset"
VAR supernatural_senses = "unset"
VAR body_detail = "unset"
VAR hair_colour = "unset"
VAR hairstyle = "unset"
VAR complexion_detail = "unset"
VAR face_detail = "unset"
VAR outfit_detail = "unset"
VAR baggage_style = "unset"
VAR viktor_relation = "unset"
VAR tut_choice_intro = false
VAR tut_optional_intro = false
VAR tut_character_intro = false
VAR tut_dialog_intro = false
VAR tut_manners_intro = false
VAR tut_gated_intro = false
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
// -----------------------------------------------------------------------------
// GLOBAL FUNCTIONS
// -----------------------------------------------------------------------------
// Add global Ink functions here when needed.
// -----------------------------------------------------------------------------
// KNOT INDEX
// -----------------------------------------------------------------------------
// eibenreith_01_zug.ink
// intro_train
// train_compartment
// next_compartment_definition
// compartment_room
// compartment_letter
// look_out_window
// observe_viktor
// define_class_and_name
// choose_name_noble
// choose_surname_noble
// choose_name_middle
// choose_surname_middle
// choose_name_working
// choose_surname_working
// assemble_full_name
// define_religion_and_supernatural
// define_appearance
// first_viktor_exchange
// viktor_class_noble
// viktor_class_middle
// viktor_class_working
// viktor_mission_briefing
// eibenreith_02_bahnhof.ink
// railway_station
// station_platform_options
// station_baggage
// eibenreith_03_graben.ink
// coach_journey
// coach_road_options
// coach_after_road_options
// eibenreith_04_dorf.ink
// village_arrival_options
// village_exit_puzzle
// -----------------------------------------------------------------------------
// ENTRY POINT
// -----------------------------------------------------------------------------
-> intro_train
File diff suppressed because it is too large Load Diff
+174
View File
@@ -0,0 +1,174 @@
// eibenreith_02_bahnhof.ink
// Kapitel: Der Bahnhof.
// Enthält Stations-Erkundung, Gepäckwahl und erstes Manierenpuzzle.
=== railway_station ===
Die Station ist klein genug, dass der Zug kurz verlegen wirkt, als er dort hält. #chapter[Der Bahnhof] #image[muerzzuschlag.png](portrait)
Ein Gepäckträger mit einer zu großen Kappe eilt über den Bahnsteig. Eine Frau mit Korb tritt vor dem Dampf zurück wie vor einem Tier. Irgendwo jenseits des Stationsgebäudes stampft ein Kutschpferd im gefrorenen Schlamm. Das Schild gibt dem Ort einen Namen, den du im Fahrplan gesehen hast und an den du dich nicht mit Zuneigung erinnern wirst.
-> station_platform_options
=== station_platform_options ===
{not tut_optional_intro:
#alert[Manche Wahlen sind Erkundungen. Sie öffnen Beobachtungen, Stimmungen oder Hinweise, ohne dich meist sofort auf einen unwiderruflichen Schritt festzulegen.]
~ tut_optional_intro = true
}
* [__Schaue__: Auf das Stationsschild.] #action:orientation #optional #key:l
Der Ortsname auf dem Schild ist mit schwarzer Farbe auf hellem Grund gemalt, zweckmäßig, kaiserlich, ohne jede Rücksicht auf den Eindruck, den er auf Ankommende macht. Die Buchstaben sehen aus, als hätten sie nie vorgehabt, in einem Salon ausgesprochen zu werden.
-> station_platform_options
* [__Höre__: Auf den Bahnsteig.] #action:orientation #optional
Unter dem Zischen der Lokomotive liegen kleinere Geräusche: ein Koffer, der auf Holz abgesetzt wird; das kurze Räuspern eines Beamten; Pferdehufe im gefrorenen Schlamm; eine Frau, die ein Gebet beginnt und beim zweiten Wort wieder verschluckt.
-> station_platform_options
* [__Untersuche__: Die Wartenden.] #action:orientation #optional
Niemand starrt offen. Das wäre grob. Stattdessen entstehen kleine Leerstellen in den Bewegungen der Leute: ein Blick, der zu spät weiterwandert; ein Schritt, der seine Richtung ändert; ein Gespräch, das plötzlich nur noch aus Endungen besteht.
-> station_platform_options
* [__Überblicke__: Dein Gepäck.] #action:object
-> station_baggage
=== station_baggage ===
Dein Gepäck wird in Etappen ausgeladen.
* [__Überblicke__: Eine disziplinierte amtliche Zusammenstellung.] #action:object
~ baggage_style = "official"
~ detective += 1
Zuerst kommt ein nüchterner Reisekoffer mit vom Gebrauch stumpfen Messingecken, dann eine Aktenmappe, dann eine Hutschachtel, dann der schmale schwarze Kasten, dessen Inhalt sowohl einen Priester als auch einen Taschenspieler in Verlegenheit bringen würde, falls einer von beiden ihn ohne Phantasie durchsuchte.
* [__Überblicke__: Das Gepäck einer eleganten Dame.] #action:object
~ baggage_style = "elegant"
~ class_confidence += 1
Zuerst kommt ein großer Koffer aus dunklem Leder, dann ein kleinerer für Wäsche, dann eine runde Hutschachtel, ein Reise-Necessaire und ein Ridikül, das du zu nahe bei der Hand behältst, als dass ein Gepäckträger seine Bedeutung missverstehen dürfte.
* [__Überblicke__: Das Gepäck einer Darstellerin.] #action:object
~ baggage_style = "performer"
~ medium_reputation += 1
Zuerst kommt ein respektabler Koffer, dann eine Hutschachtel, dann ein Reisekasten mit Handschuhen, Schleiern, Bändern, Visitenkarten und kleinen Gegenständen, mit denen man ein Zimmer überreden kann, an Kräfte zu glauben, die längst anwesend sind.
* [__Überblicke__: Eine praktische Auswahl, die zu viel Vorbereitung verrät.] #action:object
~ baggage_style = "practical"
~ detective += 1
Zuerst kommt ein abgenützter, an den Ecken verstärkter Koffer, dann eine Ledertasche mit Notizheften, Bleistiften, gefalteten Karten, Ersatzhandschuhen, einer Handlampe und genug kleinen Notwendigkeiten, um jeden zu beleidigen, der Frauen lieber dekorativ hat.
* [__Überblicke__: Ein übertriebener Haufen, der jede Tarnung erschwert.] #action:object
~ baggage_style = "excessive"
~ careless += 1
Zuerst kommt ein Koffer, dann ein zweiter, dann eine Hutschachtel, dann eine Reisedecke, dann ein Toilettenkasten, dann der schmale schwarze Kasten, dann ein kleineres Paket, von dem du vergessen hattest, dass es das Packen überlebt hat. Am Ende sieht selbst Viktor einen Augenblick lang zahlenmäßig unterlegen aus.
-
Viktor überwacht die Umladung mit knapper Höflichkeit. Er trägt nicht wie ein Diener. Er weist an wie ein Mann, der vorgibt, nicht zu befehlen.
Die kleine Szene vor dem Waggon ist harmlos genug, um gefährlich zu sein. Ein Gepäckträger wartet mit geneigtem Kopf. Der Kutscher steht einige Schritte entfernt. Viktor ist nah genug, um dir beim Aussteigen die Hand zu reichen, aber nicht so nah, dass er es ohne dein stilles Einverständnis täte. Drei Männer, drei Stände, drei verschiedene Arten von Nützlichkeit.
Was hier geschieht, wird niemand in einem Bericht erwähnen. Gerade deshalb wird es behalten.
{
- baggage_style == "practical":
Weil dein Gepäck nach Vorbereitung aussieht, wirkt die nüchterne Anweisung an einen Gepäckträger weniger wie Anmaßung und mehr wie Gewohnheit.
- baggage_style == "excessive":
Weil dein Gepäck zu zahlreich ist, wird schon vor dem ersten Wort sichtbar, dass jemand in deiner Nähe arbeiten muss.
- baggage_style == "performer":
Der schmale schwarze Kasten zieht Viktors Blick eine Spur länger auf sich als die übrigen Stücke.
- else:
Das Gepäck gibt den Männern genug zu tun, um ihnen ihre Rollen zu erklären.
}
{not tut_manners_intro:
#alert[In Gesellschaft entscheidet oft nicht nur, was du tust, sondern wann und vor wem. Höflichkeit, Rang und Timing können ebenso viel verraten wie ein Geständnis.]
~ tut_manners_intro = true
}
{not tut_gated_intro:
{
- birth_class == "noble":
#alert[Manche Möglichkeiten erkennst du nur, weil deine Herkunft, dein Glaube oder deine bisherigen Entscheidungen sie dir öffnen. Der hervorgehobene Hinweis nach dem Mittelpunkt zeigt, wodurch sie möglich wurde.]
~ tut_gated_intro = true
- birth_class == "working":
#alert[Manche Möglichkeiten erkennst du nur, weil deine Herkunft, dein Glaube oder deine bisherigen Entscheidungen sie dir öffnen. Der hervorgehobene Hinweis nach dem Mittelpunkt zeigt, wodurch sie möglich wurde.]
~ tut_gated_intro = true
}
}
* {birth_class == "noble"} [__Warte__ · **Adel**: Bis Viktor seine Hand anbietet.] #action:social #gated:noble #key:z
#manners:excellent
~ class_confidence += 2
~ court_loyalty += 1
Du wartest einen Atemzug, bis Viktor seine Hand anbietet, und nimmst sie dann, als wäre dies keine Hilfe, sondern die Ordnung der Welt.
Du gibst ihm nicht dein Gewicht. Nur deine Hand. Genau genug, dass er dienen darf, ohne Diener zu werden. Der Gepäckträger senkt den Blick ein wenig tiefer. Der Kutscher sieht, was er sehen muss: eine Dame, die ihren Rang nicht beweist, weil Beweise für Leute ohne Rang sind.
* [__Nicke__: Viktor zu und überlasse dem Gepäckträger das Gepäck.] #action:social
#manners:good
~ viktor_trust += 1
Du nimmst Viktors angebotene Hand knapp und sicher, dankst ihm mit einem Nicken und lässt den Gepäckträger das Gepäck nehmen.
Es ist gutes Benehmen ohne Prunk: nicht zu vertraut gegenüber Viktor, nicht zu freundlich gegenüber dem Gepäckträger, nicht so kalt, dass es nach Unsicherheit riecht. Mittelstand könnte dies lernen. Adel könnte es billigen. Dienstboten würden erkennen, dass du ihre Arbeit nicht mit Herablassung verwechselst.
* [__Bitte den Gepäckträger__: „Zuerst den kleineren Kasten, wenn ich bitten darf.“] #action:social
#route:detective
#manners:practical
~ detective += 1
Du steigst selbst aus, bevor Viktor sich entscheiden kann, und bittest den Gepäckträger sachlich, zuerst den kleineren Kasten zu nehmen.
Das ist nicht ganz falsch, aber auch nicht ganz richtig. Viktor bemerkt die kleine Missachtung der erwarteten Form. Der Gepäckträger gehorcht erleichtert, weil klare Anweisungen leichter zu tragen sind als feine Ungewissheit. Der Kutscher ordnet dich eher der Nützlichkeit als dem Rang zu.
* [__Warte__: Einen Augenblick zu lange, bevor du Viktors Hand nimmst.] #action:social
#route:lover
#manners:provocative
~ lover += 1
~ viktor_suspicion += 1
Du lässt Viktor zu lange mit ausgestreckter Hand warten und lächelst erst dann, als hättest du ihn absichtlich geprüft.
Es ist fast ein Fauxpas, gerettet durch Anmut und die Tatsache, dass Männer Demütigungen leichter verzeihen, wenn sie sich wie Aufmerksamkeit anfühlen. Viktor hilft dir hinunter. Seine Hand bleibt vollkommen korrekt. Sein Blick nicht ganz.
* [__Greife__: Selbst nach einem Koffer.] #action:object
#route:careless
#manners:awkward
~ careless += 1
Du entschuldigst dich beim Gepäckträger dafür, dass deine Sachen Mühe machen, und greifst selbst nach einem Koffer.
Der Gepäckträger erstarrt, als hättest du ihm eine philosophische Frage gestellt. Viktor tritt sofort dazwischen, höflich genug, um die Rettung wie Zufall aussehen zu lassen. Du hast gegen keine Moral verstoßen, nur gegen die unsichtbare Arbeitsteilung, auf der diese kleine Welt ruht.
* {birth_class == "working"} [__Nimm__ · **Unterschicht**: Dem Gepäckträger beinahe den Koffer ab.] #action:object #gated:working #key:t
#manners:fauxpas
~ class_confidence -= 1
~ careless += 1
Du springst hinunter, bevor jemand dir helfen kann, und nimmst dem Gepäckträger beinahe den Koffer aus der Hand.
Für eine Sekunde bist du schneller als deine Verkleidung. Der Gepäckträger hält fest, Viktor greift nach deinem Ellbogen, der Kutscher sieht weg, weil Wegsehen manchmal die höflichste Form von Zeugenschaft ist. Es ist kein Unglück. Nur ein Riss, klein genug, um ihn mit Haltung zu schließen.
-
Die Kutsche aus Hohenreith wartet jenseits des Stationshofes: dunkelgrüner Lack, schwarze Räder, das gräfliche Wappen dezent auf der Tür, zwei Pferde bereits unruhig im Geschirr. Der Kutscher nimmt den Hut ab, als er dich sieht. Nicht zu tief. Tief genug für Rang, nicht tief genug für Ehrfurcht. #sfx[horse-neigh.ogg]
„Gnädiges Fräulein? Herr Sekretär?“
{birth_class == "noble":
Man hat ihm genug gesagt, um dich einzuordnen. Das ist eine Höflichkeit. Es ist auch eine Warnung.
- else:
Er zögert bei dir um das kleinste Maß. Das Zögern ist keine Unhöflichkeit. Es ist Berechnung. Erste Klasse, Hofschreiben, kein Titel außer Fräulein, und ein Mann neben dir, der aussieht, als hätte er Menschen für weniger verhaften lassen als Starren.
}
Viktor antwortet, bevor du es kannst.
„Vom Jagdhaus Hohenreith?“
„Jawohl, Herr Sekretär. Der Weg ist befahrbar. Wenn der Nebel nicht dichter wird, sollten wir Eibenreith vor Einbruch der Dunkelheit erreichen.“
Das Wort tritt ohne Zeremonie in die Luft.
Eibenreith.
Nicht Hohenreith, der Name, der in sauberer Hand auf der Einladung steht. Eibenreith: das Dorf darunter. Ein kleinerer Name. Älter im Mund. Ein Name mit Wurzeln statt Briefpapier.
-> coach_journey
+209
View File
@@ -0,0 +1,209 @@
// eibenreith_03_graben.ink
// Kapitel: Der Graben.
// Enthält Kutschfahrt, optionale Grabenbeobachtungen und Statue/Viktor-Reaktion.
=== coach_journey ===
Die Kutsche lässt die Station hinter sich und damit das letzte leicht erkennbare Zeichen der Monarchie. #chapter[Der Graben] #music[Kaiserpunk Jodler.mp3](crossfade, loop, lead=4)
Zuerst folgt der Weg einem Tal, in dem Telegraphendraht ihm noch Gesellschaft leistet und der Fluss in einem hellen, steinigen Bett läuft. Sägewerke, umzäunte Wiesen und Bauernhäuser erscheinen und verschwinden hinter Fichtenbeständen. Die Berge steigen nicht auf einmal. Sie rücken zuständigkeitsweise vor. Ein bewaldeter Hang beansprucht den linken Himmel, dann schließt eine graue Wand aus Kalk den Norden, dann sammelt sich im Osten ein weiterer Rücken, bis selbst die Wolken in Dienst getreten scheinen.
Der Kutscher nennt Orte, wenn Viktor fragt, doch die Namen sind örtlich und praktisch, gedacht für Männer, die wissen, welche Brücke bei Hochwasser nachgibt und welcher Hof störrische Pferde hält. Irgendwo hinter den sichtbaren Rücken, sagt er, liegt der große weiße Rücken des Hochschwab. Nach Osten, jenseits von Wald und Pass, hält die Hohe Veitsch ihr eigenes Wetter. Er sagt das nicht wie ein Führer, sondern wie ein Mann, der Nachbarn erklärt, die vielleicht guter Laune sind und vielleicht nicht.
Das Haupttal verengt sich.
Der Weg biegt davon in einen Seitengraben, und die Veränderung ist augenblicklich. Der Klang ändert sich. Die Räder klingen nicht mehr gegen offene Entfernung, sondern mahlen zwischen Böschungen, Wurzeln und nassem Stein. Die Luft riecht nach Lauberde, Harz und kaltem Wasser. Eiben erscheinen zwischen den Fichten in dunkler, unwahrscheinlicher Geduld, ihre Nadeln zu schwarz für den Nachmittag.
„Eibenreither Graben“, sagt der Kutscher und bekreuzigt sich so rasch, dass die Geste auch einem Schlagloch gegolten haben könnte.
Viktor bemerkt es. Natürlich bemerkt er es.
„Schlechter Weg?“, fragt er.
„Alter Weg“, sagt der Kutscher.
Eine Weile spricht niemand.
-> coach_road_options
=== coach_road_options ===
* [__Berühre__: Das kalte Kutschenfenster.] #action:object #optional
Das Glas ist kälter, als es im Inneren der Kutsche sein dürfte. Feuchtigkeit sammelt sich an deinem Handschuh und verschwindet sofort wieder, als hätte sie es sich anders überlegt. Draußen streifen Zweige so nah vorbei, dass sie die Scheibe beinahe mit Nägeln prüfen.
-> coach_road_options
* [__Höre__: Auf die Räder im Graben.] #action:orientation #optional
Das Geräusch der Räder hat sich verändert. Auf der offenen Straße war es ein Rhythmus; hier ist es ein Mahlen, ein Zählen, ein wiederholtes Bestehen gegen Stein und Wurzel. Der Weg klingt nicht befahren. Er klingt benutzt.
-> coach_road_options
* [__Untersuche__: Viktors Reaktion.] #action:orientation #optional
Viktor betrachtet nicht die Landschaft. Er betrachtet ihre Möglichkeiten: Engstellen, Böschungen, tote Winkel, die Entfernung bis zum Kutscher, die Frage, wie rasch man aus einer Kutsche steigt, wenn die Straße selbst dagegen ist.
-> coach_road_options
* [__Warte__: In der schaukelnden Kutsche.] #action:social #key:z
-> coach_after_road_options
=== coach_after_road_options ===
Du beobachtest die Bäume.
Es gibt Wälder, die zu Geschichten einladen, weil sie hübsch sind, und Wälder, die Geschichten zurückweisen, weil das, was dort geschah, keine Zeugen brauchte. Dieser gehört zur zweiten Art. Seine Stämme stehen eng, nicht wild, sondern mit der Haltung einer Menge, die Platz macht für etwas, das vor langer Zeit durch sie getragen wurde. Der Schnee in den Mulden ist nicht rein. Er hat Nadeln gesammelt, Rinde und einen gelblichen Fleck dort, wo Wasser von unten aufgestiegen ist.
An einem Hang oberhalb des Weges, halb vom Unterholz verschluckt, erblickst du Stein.
Ein Wegheiligtum vielleicht. Ein Grenzzeichen. Eine Figur. Die Kutsche rollt schon vorbei, bevor deine Augen sich auf ihre Form einigen können. Für einen Augenblick bleibt der Eindruck eines Frauenkopfes zurück, geneigt nicht im Gebet, sondern im Lauschen. #image[statue.png](square)
{
- supernatural_senses == "genuine":
Dein Nacken zieht sich zusammen.
Nicht Furcht. Wiedererkennen wäre schlimmer.
- supernatural_senses == "ambiguous":
Dein Nacken zieht sich zusammen.
Nicht Furcht. Wiedererkennen wäre schlimmer.
- supernatural_senses == "repressed":
Dein Nacken zieht sich zusammen.
Nicht Furcht. Wiedererkennen wäre schlimmer.
- else:
Du sagst dir, dass alter Stein, durch bewegte Zweige gesehen, zu allem wird, wozu der Geist feig genug ist.
}
Viktor wendet sich leicht demselben Hang zu.
„Haben Sie etwas gesehen?“
* [__Antworte__: „Vielleicht eine Frau im Wald. Oder ein Stein, der eine sein wollte.“] #action:conversation
#route:eccentric
#hint:statue
~ eccentric += 1
~ viktor_suspicion += 1
„Vielleicht eine Frau im Wald. Oder ein Stein, der eine sein wollte.“
Er betrachtet die vorbeiziehenden Bäume.
„Ein Wegheiligtum?“
** [__Antworte__: „Wenn es ein Heiligtum ist, so ist es seit langem nicht mehr geliebt worden.“] #action:conversation
„Wenn es ein Heiligtum ist, so ist es seit langem nicht mehr geliebt worden.“
„Sie sprechen, als bemerkten Steine Vernachlässigung.“
Soldaten bemerken Vernachlässigung ebenfalls. Sein Schweigen gesteht genug zu.
Er antwortet nicht.
** [__Antworte__: „Nein. Heiligtümer wenden sich den Gläubigen zu. Dieses Ding lauschte seitwärts.“] #action:conversation
~ supernatural_exposure += 1
„Nein. Heiligtümer wenden sich den Gläubigen zu. Dieses Ding lauschte seitwärts.“
Viktors Hand ruht am Halteriemen der Kutsche, still und bereit.
--
* [__Antworte__: „Ein Grenz- oder Wegzeichen. Ich wüsste gern, wohin dieser Pfad führt.“] #action:conversation
#route:detective
#hint:statue
~ detective += 1
~ viktor_trust += 1
„Ein Grenz- oder Wegzeichen. Ich wüsste gern, wohin dieser Pfad führt.“
„Sie haben einen Pfad gesehen?“
** [__Antworte__: „Nicht deutlich. Genug, um später danach zu fragen.“] #action:conversation
„Nicht deutlich. Genug, um später danach zu fragen.“
Viktor blickt durch das kleine rückwärtige Fenster. Die Biegung hat den Hang bereits ausgelöscht.
„Fragen Sie vorsichtig. Orte, die man nicht erwähnt, sind oft aufschlussreicher als jene, die man empfiehlt.“
** [__Antworte__: „Nur die Andeutung eines Pfades. Wenn er existiert, erhält jemand seine Abwesenheit aufrecht.“] #action:conversation
#route:detective
~ detective += 1
„Nur die Andeutung eines Pfades. Wenn er existiert, erhält jemand seine Abwesenheit aufrecht.“
„Sie lassen Abwesenheiten kostspielig klingen.“
Das sind sie meistens; Abwesenheit ist teuer, wenn jemand sie pflegt.
--
* [__Antworte__: „Nur Bäume. Die Art, bei der man für Herren mit Revolvern dankbar wird.“] #action:conversation
#route:careless
~ careless += 1
~ viktor_relation = "dependence"
„Nur Bäume. Die Art, bei der man für Herren mit Revolvern dankbar wird.“
Sein Ausdruck verdunkelt sich um einen amtlichen Grad.
„Ein Revolver ist ein schlechtes Werkzeug gegen Bäume.“
** [__Antworte__: „Dann werde ich mich darauf verlassen, dass Ihre Unterhaltung sie einschüchtert.“] #action:conversation
„Dann werde ich mich darauf verlassen, dass Ihre Unterhaltung sie einschüchtert.“
Der Kutscher tut, als höre er nichts. Seine Schultern jedoch hören alles.
** [__Antworte__: „Wie bedauerlich. Sie wirkten so berufsmäßig beruhigend.“] #action:conversation
#route:lover
~ lover += 1
„Wie bedauerlich. Sie wirkten so berufsmäßig beruhigend.“
„Ich bevorzuge Feinde, die sich zu erkennen geben.“
--
* [__Frage Viktor__: „Würden Sie mir glauben, wenn ich sagte, ich hätte etwas gesehen?“] #action:conversation
#route:lover
~ lover += 1
~ viktor_suspicion += 1
„Würden Sie mir glauben, wenn ich sagte, ich hätte etwas gesehen?“
„Das hinge davon ab, welchen Vorteil Sie sich von der Antwort versprechen.“
** [__Antworte__: „Herr Nowak. Sie verletzen mich.“] #action:conversation
„Herr Nowak. Sie verletzen mich.“
„Noch nicht.“
Es ist das Erste, was er an diesem Tag gesagt hat, das beinahe wie ein Flirt klingt, wenn auch vielleicht nur deshalb, weil Gefahr ein Talent dafür hat, wärmere Kleider zu borgen.
** [__Weise Viktor an__: „Beobachten Sie den Hang, nicht meine Absichten. Eines von beidem könnte nützlich sein.“] #action:social
~ viktor_trust += 1
„Dann beobachten Sie den Hang, nicht meine Absichten. Eines von beidem könnte nützlich sein.“
Er gehorcht, ohne zuzugeben, dass er es getan hat.
--
* [__Antworte__: „Nein.“] #action:conversation
#route:sapphic
~ sapphic += 1
„Nein.“
Die Verneinung kommt zu rasch, und ihr hört es beide.
Du denkst nicht mehr an den Stein. Du denkst an die junge Frau, die irgendwo vor euch wartet: die Tochter des Grafen, der Grund, der sorgsam nicht im Memorandum steht, die Fremde, deren Haushalt dich unter einem Titel herbeigerufen hat, der zugleich lächerlich und nützlich ist.
** [__Antworte__: „Es war nur Schatten.“] #action:conversation
„Es war nur Schatten.“
Wenn dieser Ort Frauen in Stein hält, denkst du, was tut er dann mit ihnen in den Häusern?
** [__Antworte__: „Oder, falls ich etwas sah, ziehe ich es vor, es mir nicht erklären zu lassen, bevor ich verstehe, warum es von Bedeutung ist.“] #action:conversation
#route:detective
~ detective += 1
„Oder, falls ich etwas sah, ziehe ich es vor, es mir nicht erklären zu lassen, bevor ich verstehe, warum es von Bedeutung ist.“
Wenn dieser Ort Frauen in Stein hält, denkst du, was tut er dann mit ihnen in den Häusern?
--
-
Der Graben öffnet sich widerwillig.
-> village_arrival_options
+161
View File
@@ -0,0 +1,161 @@
// eibenreith_04_dorf.ink
// Kapitel: Eibenreith.
// Enthält Dorfankunft, optionale Dorfbeobachtungen, Ausstiegs-Manierenpuzzle und Schluss des Intros.
=== village_arrival_options ===
Zuerst kommt der Geruch von Rauch. Dann ein Dach, niedrig und dunkel vom Wetter. Dann ein zweites. Dann ein Kirchturm, nicht hoch, nicht anmutig, sondern breitschultrig und blass vor dem Hang dahinter. Seine Mauern wirken älter als das Dorf um sie her und weniger sicher ihres Sieges. Die Fenster sind klein. Die Kirchhofmauer hält die Straße auf Abstand, als bräuchten die Toten Schutz vor den Lebenden oder die Lebenden vor etwas anderem. #chapter[Eibenreith] #sfx[church-bells.ogg](max=8, fade) #image[eibenreith.png](landscape)
Eibenreith erscheint nicht, wie ein Dorf auf einem Bild erscheint, auf einmal und zur Bewunderung geordnet, sondern in Bruchstücken.
Eine Frau mit einem dunklen Kopftuch hält mit einem Eimer in der Hand inne. Ein Bub hört auf, Gänse zu treiben, und lässt sie um seine Stiefel klagen. Zwei Männer vor einem Schuppen beenden im selben Augenblick ihr Gespräch, ohne einander anzusehen. Vorhänge rühren sich an Fenstern, hinter denen niemand zugibt zu stehen. Ein Schmiedeschild bewegt sich leicht in Luft, die du nicht fühlen kannst. Wasser läuft irgendwo unter Brettern, unter Stein, unter der Straße selbst, schnell, kalt und verborgen.
Die Häuser sind nicht arm, nicht eigentlich. Viele sind fest, weißgekalkt, geschindelt, erhalten mit der störrischen Anständigkeit von Menschen, die reparieren, was sie nicht ersetzen können. Und doch stört etwas in ihrer Anordnung das Auge. Sie wenden sich der Kirche zu, aber nicht ganz. Sie halten die Straße, aber lehnen sich von ihr weg. Sie lassen zwischen Hof, Zaun und Holzstoß schmale Durchgänge, in denen sich Schatten zu früh sammelt.
Die Kutsche wird langsamer.
Niemand läuft herbei, um sie zu begrüßen.
Niemand muss das. Die Nachricht ist bereits ins Dorf eingetreten, auf Wegen schneller als Bahn, Telegraph oder kaiserliches Siegel.
Du sitzt sehr gerade, während Eibenreith dich zum ersten Mal betrachtet.
* [__Schaue__: In die Gesichter am Straßenrand.] #action:orientation #optional #key:l
Die Gesichter verschwinden nicht, wenn du hinsiehst. Sie verändern nur ihre Begründung: Eine Frau prüft plötzlich ihren Eimer. Ein Bub entdeckt die Gänse neu. Ein Mann tut, als habe er schon immer zum Kirchtor gesehen. Das Dorf besitzt keine Bühne, aber jeder hier kennt seinen Auftritt.
-> village_arrival_options
* [__Höre__: Auf das Wasser unter der Straße.] #action:orientation #optional
Unter den Rädern, unter Brettern und Steinen, unter der höflichen Behauptung einer Dorfstraße läuft Wasser. Es klingt nicht tief, aber schnell. Als hätte der Ort einen zweiten Atem, einen kalten, verborgenen, der nicht durch menschliche Münder geht.
-> village_arrival_options
* [__Untersuche__: Die Kirche.] #action:orientation #optional
Der Turm ist nicht schlank genug, um in den Himmel zu zeigen. Er steht da wie eine Faust. Die kleinen Fenster geben wenig preis, und die Mauer des Kirchhofs wirkt weniger wie Einfriedung als wie eine alte Gewohnheit, sich gegen etwas zu stemmen.
{
- religion_stance == "devout_catholic":
Gerade das stört dich: nicht der Mangel an Schönheit, sondern der Mangel an Frieden.
- religion_stance == "josephinian_sceptic":
Du siehst weniger Andacht als Institution: Stein, Besitz, Grenze, Verwaltung der Furcht.
- else:
Die Kirche sieht nicht aus, als habe sie den älteren Dingen im Tal widersprochen. Eher, als habe sie gelernt, über ihnen zu stehen.
}
-> village_arrival_options
* [__Warte__: Bis die Kutsche hält.] #action:social #key:z
-> village_exit_puzzle
=== village_exit_puzzle ===
Der Kutscher hält vor dem Wirtshaus oder vielleicht nur vor dem Gebäude, das in einem besseren Dorf eines gewesen wäre. Ein Knecht aus dem Dorf tritt aus dem Schatten des Tors. Viktor öffnet die Kutschentür von innen nicht sofort; der Kutscher steigt ab, um den Schlag zu öffnen. Der Knecht sieht auf dein Gepäck, dann auf deine Handschuhe, dann auf Viktor.
Wieder stellt die Welt eine Frage, ohne sie auszusprechen: Wer darf dir helfen, wer muss dir helfen, und wem erlaubst du, dabei wichtig zu wirken?
* {birth_class == "noble"} [__Warte__ · **Adel**: Bis der Kutscher den Schlag öffnet und Viktor zuerst aussteigt.] #action:social #gated:noble #key:z
#manners:excellent
~ class_confidence += 2
Du wartest, bis der Kutscher den Schlag öffnet und Viktor zuerst aussteigt; erst dann reichst du Viktor die behandschuhte Hand.
Es geschieht langsam genug, dass alle Beteiligten ihre Rolle finden. Der Kutscher ist Dienst, Viktor ist Begleitung, der Knecht ist noch nicht wichtig genug, um dich zu berühren. Dein Fuß erreicht den Boden, als hätte die Straße sich dafür bereitgehalten.
* [__Nicke__: Dem Kutscher knapp zu, nachdem Viktor dir geholfen hat.] #action:social
#manners:good
~ viktor_trust += 1
Du lässt Viktor aussteigen, nimmst seine Hand beim Abtreten und dankst dem Kutscher erst danach mit einem knappen Blick.
Der Ablauf ist korrekt genug, um keine Geschichte zu erzeugen. In einem Dorf, das von Geschichten lebt, ist das ein kleiner Sieg.
* [__Weise den Knecht an__: „Zuerst den kleineren Kasten.“] #action:social
#route:detective
#manners:practical
~ detective += 1
Du gibst dem Knecht eine klare Anweisung, welches Gepäck zuerst abgeladen werden soll, bevor er danach fragen kann.
Er gehorcht sofort. Viktor registriert die Zweckmäßigkeit. Der Kutscher registriert die Ungewöhnlichkeit. Eine Dame, die Gepäckreihenfolgen kennt, ist entweder sehr erfahren, sehr nervös oder beides.
* [__Lächle__: Dem Kutscher zu freundlich zu.] #action:social
#route:lover
#manners:too_warm
~ lover += 1
Du bietest dem Kutscher ein sichtbares Lächeln und ein zu freundliches „Danke“ an.
Der Mann senkt den Blick, verwirrt und geschmeichelt. Viktor wird stiller. Freundlichkeit über Standesgrenzen hinweg kann Güte sein, Taktik oder Unachtsamkeit. Auf dem Dorf wird niemand lange brauchen, eine vierte Möglichkeit zu erfinden.
* [__Steige aus__: Zu früh, ehe alle Rollen verteilt sind.] #action:movement
#route:careless
#manners:awkward
~ careless += 1
~ viktor_relation = "dependence"
Du steigst zu früh aus, trittst beinahe in den Straßenschlamm und fängst dich an Viktors Arm.
Er hält dich ohne sichtbare Anstrengung fest. Für einen Augenblick sieht das Dorf genau das, was es am liebsten sieht: eine Dame, gerettet durch einen Mann. Es ist lächerlich nützlich und nützlich lächerlich.
* {birth_class == "working"} [__Steige aus__ · **Unterschicht**: Allein, bevor jemand dir helfen kann.] #action:movement #gated:working
#manners:fauxpas
~ class_confidence -= 1
Du steigst allein aus, nimmst deinen Rock hoch genug, um den Schlamm zu sehen, und sagst dem Knecht, er solle mit dem schweren Koffer vorsichtig sein.
Es ist praktisch, schnell und völlig falsch. Nicht, weil du unrecht hast, sondern weil du recht hast wie jemand, der selbst schon getragen hat. Der Knecht erkennt es. Viktor auch.
-
Neben dir senkt Viktor die Stimme.
„Vergessen Sie nicht: In Hohenreith wird jede Höflichkeit etwas bedeuten. Hier wird es jedes Schweigen tun.“
* [__Antworte__: „Dann werden wir bereits empfangen.“] #action:conversation
#route:detective
~ detective += 1
„Dann werden wir bereits empfangen.“
„Ja“, sagt er. „Und geprüft.“
* [__Antworte__: „Sie lassen es klingen, als stünde das Dorf über dem Grafen.“] #action:conversation
#route:eccentric
~ eccentric += 1
„Sie lassen es klingen, als stünde das Dorf über dem Grafen.“
„Nein“, sagt Viktor. „Nur, als hätte es vielleicht mehr als einen überlebt.“
* [__Antworte__: „Wie glücklich, dass ich mehrere Arten des Schweigens eingepackt habe.“] #action:conversation
#route:lover
~ lover += 1
„Wie glücklich, dass ich mehrere Arten des Schweigens eingepackt habe.“
Sein Mund bewegt sich beinahe. „Verwenden Sie zuerst das schlichteste.“
* [__Antworte__: „Ich mag es nicht, von Leuten beobachtet zu werden, die sich nicht vorstellen.“] #action:conversation
#route:careless
~ careless += 1
„Ich mag es nicht, von Leuten beobachtet zu werden, die sich nicht vorstellen.“
„Das“, sagt er, „wird sich heute kaum bessern.“
* [__Antworte__: „Wenn Amalia ihr ganzes Leben unter diesem Blick gelebt hat, beginne ich zu verstehen, weshalb man nach Geistern sandte.“] #action:conversation
#route:sapphic
~ sapphic += 1
„Wenn Amalia ihr ganzes Leben unter diesem Blick gelebt hat, beginne ich zu verstehen, weshalb man nach Geistern sandte.“
Viktor sieht dich an, doch welche Antwort er auch erwägt, er behält sie hinter den Zähnen.
-
Die Pferde ziehen die Kutsche an der Kirchhofmauer vorbei. Darüber, auf dem alten Putz neben dem Tor, blickt eine verblasste gemalte Frau unter einem abblätternden blauen Mantel herab. Ihre Hände sind zum Gebet gefaltet. Ihre Augen, vom Wetter beschädigt, zeigen nicht mehr in dieselbe Richtung.
{
- religion_stance == "devout_catholic":
Für einen Atemzug stört dich nicht, dass das Bild alt ist. Es stört dich, dass es nicht mehr ganz heilig wirkt.
- religion_stance == "josephinian_sceptic":
Für einen Atemzug wirkt das Bild weniger wie Andacht als wie Verwaltung: ein aufgemaltes Siegel über etwas, das man nicht fortschaffen konnte.
- religion_stance == "wounded_catholic":
Für einen Atemzug trifft dich das gemalte Gesicht an einer Stelle, die du lieber Schuld als Erinnerung nennen würdest.
- else:
Für einen Atemzug, als die Räder über ein verborgenes Wasserrinnsal fahren, wirkt das gemalte Gesicht weniger wie die Heilige Mutter als wie eine Maske, die etwas aufgesetzt wurde, das länger gewartet hatte.
}
Dann fährt die Kutsche in das eigentliche Dorf hinein, und die Straße biegt zu der unsichtbaren Höhe, auf der Jagdhaus Hohenreith über Eibenreith unter seinem neueren Namen steht.
#score[Du hast Eibenreith erreicht.]
-> END
File diff suppressed because one or more lines are too long
-681
View File
@@ -1,681 +0,0 @@
title: The Mysterious Mansion
author: AI Interactive Fiction
version: 1.0.0
introduction: |
You find yourself standing outside an old, abandoned mansion on a hill.
Rain patters gently on the gravel path leading to the front door.
A strange letter in your pocket invited you here, but you can't remember who sent it.
Perhaps the answers lie within...
# Room definitions
rooms:
# Starting area
front_yard:
name: Front Yard
description: |
You stand on a gravel path leading to an imposing Victorian mansion.
The rain has softened to a drizzle, and moonlight peeks through gaps in the clouds.
Ancient oak trees frame the property, their branches swaying in the gentle breeze.
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
-64
View File
@@ -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<void>;
/**
* Start the game in CLI mode
*/
start(): Promise<void>;
/**
* 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<string>;
/**
* 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<string, boolean>;
counters: Record<string, number>;
};
/**
* 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;
}
-262
View File
@@ -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
-1
View File
File diff suppressed because one or more lines are too long
-77
View File
@@ -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<void>;
/**
* 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<void>;
/**
* Load a game state from a save file
*/
loadGame(filename: string): Promise<void>;
/**
* 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<string>;
/**
* 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;
}
-607
View File
@@ -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
File diff suppressed because one or more lines are too long
-4
View File
@@ -1,4 +0,0 @@
/**
* Main entry point for the AI Interactive Fiction application
*/
export {};
-110
View File
@@ -1,110 +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");
// Import the server module and the startServer function for the web interface
const server_1 = require("./server");
// 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 environment variables or use default
const worldFile = process.env.DEFAULT_WORLD_FILE || './data/worlds/example_world.yml';
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 = 10;
// Start the web server with port fallback
console.log('Starting web server...');
await (0, server_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
-1
View File
@@ -1 +0,0 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
-39
View File
@@ -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<void>;
getCurrentState(): GameState;
getWorldModel(): WorldModel;
processAction(action: ActionResponse): ActionResult;
saveGame(filename: string): Promise<void>;
loadGame(filename: string): Promise<void>;
getAvailableActions(): string[];
getVisibleObjects(): string[];
getVisibleCharacters(): string[];
getCurrentRoomDescription(): string;
start(): Promise<string>;
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;
}
-6
View File
@@ -1,6 +0,0 @@
"use strict";
/**
* Interfaces for the game engine
*/
Object.defineProperty(exports, "__esModule", { value: true });
//# sourceMappingURL=engine.js.map
-1
View File
@@ -1 +0,0 @@
{"version":3,"file":"engine.js","sourceRoot":"","sources":["../../src/interfaces/engine.ts"],"names":[],"mappings":";AAAA;;GAEG"}
-46
View File
@@ -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<string, string>;
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<void>;
translateAction(request: ActionRequest): Promise<ActionResponse>;
generateNarrative(request: NarrativeRequest): Promise<NarrativeResponse>;
}
-6
View File
@@ -1,6 +0,0 @@
"use strict";
/**
* Interfaces for LLM integration
*/
Object.defineProperty(exports, "__esModule", { value: true });
//# sourceMappingURL=llm.js.map
-1
View File
@@ -1 +0,0 @@
{"version":3,"file":"llm.js","sourceRoot":"","sources":["../../src/interfaces/llm.ts"],"names":[],"mappings":";AAAA;;GAEG"}
-61
View File
@@ -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<string, boolean>;
containedObjects?: string[];
allowedActions: string[];
}
export interface Character {
id: string;
name: string;
description: string;
dialogue: Record<string, string>;
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<string, boolean>;
counters: Record<string, number>;
}
export interface WorldModel {
title: string;
author: string;
version: string;
introduction: string;
rooms: Record<string, Room>;
objects: Record<string, GameObject>;
characters: Record<string, Character>;
actions: Record<string, Action>;
initialState: GameState;
}
-6
View File
@@ -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
-1
View File
@@ -1 +0,0 @@
{"version":3,"file":"world-model.js","sourceRoot":"","sources":["../../src/interfaces/world-model.ts"],"names":[],"mappings":";AAAA;;GAEG"}
-36
View File
@@ -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<void>;
/**
* Translate player input into a structured action for the game engine
*/
translateAction(request: ActionRequest): Promise<ActionResponse>;
/**
* Generate narrative prose based on game events
*/
generateNarrative(request: NarrativeRequest): Promise<NarrativeResponse>;
/**
* 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;
}
-192
View File
@@ -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
-1
View File
@@ -1 +0,0 @@
{"version":3,"file":"openrouter-provider.js","sourceRoot":"","sources":["../../src/llm/openrouter-provider.ts"],"names":[],"mappings":"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"}
-11
View File
@@ -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<typeof http.IncomingMessage, typeof http.ServerResponse>;
declare const io: SocketIOServer<import("socket.io").DefaultEventsMap, import("socket.io").DefaultEventsMap, import("socket.io").DefaultEventsMap, any>;
export declare function startServer(initialPort: number, range: number): Promise<void>;
export { app, server, io };
-252
View File
@@ -1,252 +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");
// 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 = 10; // Try up to 10 ports starting from the default
// Serve static files from the public directory
app.use(express_1.default.static(path_1.default.join(__dirname, '../public')));
// Set up game sessions
const gameSessions = new Map();
// Handle socket connections
io.on('connection', (socket) => {
console.log(`New client connected: ${socket.id}`);
// Start a new game
socket.on('startGame', async () => {
try {
// Initialize game runner
const gameRunner = new game_runner_1.GameRunner();
const worldFile = process.env.DEFAULT_WORLD_FILE || './data/worlds/example_world.yml';
// Initialize the game
await gameRunner.initialize(worldFile);
// Store game session
gameSessions.set(socket.id, gameRunner);
// Send introduction to client
const gameState = gameRunner.getGameState();
socket.emit('gameIntroduction', {
introduction: gameState.world.introduction,
initialRoomDescription: gameRunner.getCurrentRoomDescription(),
currentRoomId: gameState.currentRoomId
});
}
catch (error) {
console.error('Error starting game:', error);
socket.emit('error', { message: 'Failed to start game. Please try again.' });
}
});
// 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;
}
// Process command and get response
const response = await gameRunner.processCommand(data.command);
// Send narrative response to client
socket.emit('narrativeResponse', {
text: response,
gameState: {
currentRoomId: gameRunner.getGameState().currentRoomId
},
suggestions: gameRunner.getSuggestions()
});
}
catch (error) {
console.error('Error processing command:', error);
socket.emit('error', { message: 'Failed to process command. Please try again.' });
}
});
// Save game state
socket.on('saveGame', () => {
try {
const gameRunner = gameSessions.get(socket.id);
if (!gameRunner) {
socket.emit('error', { message: 'Game session not found. Please start a new game.' });
return;
}
// Store save data in session
socket.data.savedGame = gameRunner.getGameState();
socket.emit('gameSaved');
}
catch (error) {
console.error('Error saving game:', error);
socket.emit('error', { message: 'Failed to save game. Please try again.' });
}
});
// Load game state
socket.on('loadGame', () => {
try {
const gameRunner = gameSessions.get(socket.id);
if (!gameRunner) {
socket.emit('error', { message: 'Game session not found. Please start a new game.' });
return;
}
// Check if there's a saved game
if (!socket.data.savedGame) {
socket.emit('error', { message: 'No saved game found.' });
return;
}
// Load saved game
gameRunner.loadGameState(socket.data.savedGame);
// Send current state to client
socket.emit('gameLoaded', {
currentRoomDescription: gameRunner.getCurrentRoomDescription(),
currentRoomId: gameRunner.getGameState().currentRoomId
});
}
catch (error) {
console.error('Error loading game:', error);
socket.emit('error', { message: 'Failed to load game. 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);
}
});
});
// 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/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.listen(currentPort, () => {
console.log(`AI Interactive Fiction web server running on http://localhost:${currentPort}`);
resolve();
});
server.on('error', (error) => {
// If port is in use, try next port
if (error.code === 'EADDRINUSE') {
console.log(`Port ${currentPort} is in use, trying next port...`);
server.close();
currentPort++;
reject();
}
else {
// For other errors, log and reject
console.error('Server error:', error);
reject(error);
}
});
});
// 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.js.map
-1
View File
File diff suppressed because one or more lines are too long
-10
View File
@@ -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<typeof http.IncomingMessage, typeof http.ServerResponse>;
declare const io: SocketIOServer<import("socket.io").DefaultEventsMap, import("socket.io").DefaultEventsMap, import("socket.io").DefaultEventsMap, any>;
export { app, server, io };
-197
View File
@@ -1,197 +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");
// 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 = 10; // Try up to 10 ports starting from the default
// Serve static files from the public directory
app.use(express_1.default.static(path_1.default.join(__dirname, '../public')));
// 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;
// Start a new game
socket.on('startGame', async () => {
try {
console.log('Starting test game session');
// Send introduction to client
socket.emit('gameIntroduction', {
introduction: "Welcome to the Interactive Fiction Test. This is a simplified version that sends predefined paragraphs instead of using an LLM.",
initialRoomDescription: TEST_PARAGRAPHS[0],
currentRoomId: "test-room"
});
}
catch (error) {
console.error('Error starting game:', error);
socket.emit('error', { message: 'Failed to start game. Please try again.' });
}
});
// Process player command
socket.on('playerCommand', async (data) => {
try {
console.log(`Received command: ${data.command}`);
// Move to the next paragraph
currentParagraphIndex = (currentParagraphIndex + 1) % TEST_PARAGRAPHS.length;
// Send narrative response to client
socket.emit('narrativeResponse', {
text: TEST_PARAGRAPHS[currentParagraphIndex],
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/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.listen(currentPort, () => {
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.on('error', (error) => {
// If port is in use, try next port
if (error.code === 'EADDRINUSE') {
console.log(`Port ${currentPort} is in use, trying next port...`);
server.close();
currentPort++;
reject();
}
else {
// For other errors, log and reject
console.error('Server error:', error);
reject(error);
}
});
});
// 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.js.map
-1
View File
@@ -1 +0,0 @@
{"version":3,"file":"test-server.js","sourceRoot":"","sources":["../src/test-server.ts"],"names":[],"mappings":"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"}
-71
View File
@@ -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<WorldModel>;
/**
* 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;
}
-399
View File
@@ -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
File diff suppressed because one or more lines are too long
-12
View File
@@ -1,12 +0,0 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
roots: ['<rootDir>/src'],
testMatch: ['**/__tests__/**/*.ts', '**/?(*.)+(spec|test).ts'],
transform: {
'^.+\\.tsx?$': 'ts-jest',
},
collectCoverage: true,
coverageDirectory: 'coverage',
collectCoverageFrom: ['src/**/*.ts', '!src/**/*.d.ts'],
};
+152 -17
View File
@@ -14,8 +14,11 @@
"dotenv": "^16.4.7",
"express": "^5.1.0",
"hyphenopoly": "^6.0.0",
"ifvms": "^1.1.6",
"inkjs": "^2.4.0",
"js-yaml": "^4.1.0",
"kokoro-js": "^1.2.0",
"marked": "^15.0.12",
"openai": "^4.91.0",
"socket.io": "^4.8.1"
},
@@ -32,6 +35,9 @@
"ts-jest": "^29.3.1",
"ts-node": "^10.9.2",
"typescript": "^5.8.2"
},
"engines": {
"node": ">=18.17"
}
},
"node_modules/@ampproject/remapping": {
@@ -2314,7 +2320,6 @@
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz",
"integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"type-fest": "^0.21.3"
@@ -2330,7 +2335,6 @@
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
@@ -2340,7 +2344,6 @@
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"license": "MIT",
"dependencies": {
"color-convert": "^2.0.1"
@@ -2707,7 +2710,6 @@
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
@@ -3031,6 +3033,15 @@
}
}
},
"node_modules/decamelize": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
"integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/dedent": {
"version": "1.5.3",
"resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.3.tgz",
@@ -3192,7 +3203,6 @@
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"dev": true,
"license": "MIT"
},
"node_modules/encodeurl": {
@@ -3913,7 +3923,6 @@
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
"integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
"dev": true,
"license": "MIT",
"dependencies": {
"locate-path": "^5.0.0",
@@ -4088,7 +4097,6 @@
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
"dev": true,
"license": "ISC",
"engines": {
"node": "6.* || 8.* || >= 10.*"
@@ -4154,6 +4162,15 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/glkote-term": {
"version": "0.4.4",
"resolved": "https://registry.npmjs.org/glkote-term/-/glkote-term-0.4.4.tgz",
"integrity": "sha512-5l2t4QC9Pr4DgMz/OBGojgaAZJ3p0yf+e8pIYuz63kT0gBaHqsAuASYWQVqSkj60v6nUxKYJRzE0GQucf9PDxg==",
"license": "MIT",
"dependencies": {
"ansi-escapes": "^4.0.0"
}
},
"node_modules/glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
@@ -4343,6 +4360,89 @@
"node": ">=0.10.0"
}
},
"node_modules/ifvms": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/ifvms/-/ifvms-1.1.6.tgz",
"integrity": "sha512-4OPV23gHu/YsyqcUuV4oqVBkicz6KsFdwKyMQkaUeN6nvv4maGcYA5qgjDse/iEdvsqSijLHRbx5VuM0zuXEMQ==",
"license": "MIT",
"dependencies": {
"glkote-term": "^0.4.0",
"mute-stream": "0.0.8",
"yargs": "^15.0.1"
},
"bin": {
"zvm": "bin/zvm.js"
},
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/ifvms/node_modules/cliui": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
"integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
"license": "ISC",
"dependencies": {
"string-width": "^4.2.0",
"strip-ansi": "^6.0.0",
"wrap-ansi": "^6.2.0"
}
},
"node_modules/ifvms/node_modules/wrap-ansi": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
"integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/ifvms/node_modules/y18n": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
"integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==",
"license": "ISC"
},
"node_modules/ifvms/node_modules/yargs": {
"version": "15.4.1",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",
"integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==",
"license": "MIT",
"dependencies": {
"cliui": "^6.0.0",
"decamelize": "^1.2.0",
"find-up": "^4.1.0",
"get-caller-file": "^2.0.1",
"require-directory": "^2.1.1",
"require-main-filename": "^2.0.0",
"set-blocking": "^2.0.0",
"string-width": "^4.2.0",
"which-module": "^2.0.0",
"y18n": "^4.0.0",
"yargs-parser": "^18.1.2"
},
"engines": {
"node": ">=8"
}
},
"node_modules/ifvms/node_modules/yargs-parser": {
"version": "18.1.3",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz",
"integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
"license": "ISC",
"dependencies": {
"camelcase": "^5.0.0",
"decamelize": "^1.2.0"
},
"engines": {
"node": ">=6"
}
},
"node_modules/ignore": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
@@ -4435,6 +4535,15 @@
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"license": "ISC"
},
"node_modules/inkjs": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/inkjs/-/inkjs-2.4.0.tgz",
"integrity": "sha512-EoPCYESIbMtfI8SqEDZCJwn+A5is0QozMLw250iic1ReJCgZpRKIezWj0VqgRUzAx0f3MmEbsUjY/ILe2815JQ==",
"license": "MIT",
"bin": {
"inkjs-compiler": "bin/inkjs-compiler.js"
}
},
"node_modules/ipaddr.js": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
@@ -4494,7 +4603,6 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
@@ -5396,7 +5504,6 @@
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
"integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
"dev": true,
"license": "MIT",
"dependencies": {
"p-locate": "^4.1.0"
@@ -5481,6 +5588,18 @@
"tmpl": "1.0.5"
}
},
"node_modules/marked": {
"version": "15.0.12",
"resolved": "https://registry.npmjs.org/marked/-/marked-15.0.12.tgz",
"integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==",
"license": "MIT",
"bin": {
"marked": "bin/marked.js"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@@ -5628,6 +5747,12 @@
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/mute-stream": {
"version": "0.0.8",
"resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz",
"integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==",
"license": "ISC"
},
"node_modules/natural-compare": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
@@ -5968,7 +6093,6 @@
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
"integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
"dev": true,
"license": "MIT",
"dependencies": {
"p-limit": "^2.2.0"
@@ -5981,7 +6105,6 @@
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
"integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
"dev": true,
"license": "MIT",
"dependencies": {
"p-try": "^2.0.0"
@@ -5997,7 +6120,6 @@
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
@@ -6048,7 +6170,6 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
@@ -6358,12 +6479,17 @@
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/require-main-filename": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
"license": "ISC"
},
"node_modules/resolve": {
"version": "1.22.10",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
@@ -6542,6 +6668,12 @@
"node": ">= 18"
}
},
"node_modules/set-blocking": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
"license": "ISC"
},
"node_modules/setprototypeof": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
@@ -6983,7 +7115,6 @@
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"dev": true,
"license": "MIT",
"dependencies": {
"emoji-regex": "^8.0.0",
@@ -6998,7 +7129,6 @@
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"
@@ -7319,7 +7449,6 @@
"version": "0.21.3",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz",
"integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==",
"dev": true,
"license": "(MIT OR CC0-1.0)",
"engines": {
"node": ">=10"
@@ -7501,6 +7630,12 @@
"node": ">= 8"
}
},
"node_modules/which-module": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz",
"integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==",
"license": "ISC"
},
"node_modules/word-wrap": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
+18 -11
View File
@@ -4,17 +4,21 @@
"description": "A modern take on classic text adventures that combines traditional world modeling with Large Language Models (LLMs) to create natural language interactive fiction experiences.",
"main": "index.js",
"scripts": {
"start": "node dist/index.js",
"start:web": "node dist/index.js",
"start:cli": "node dist/index.js --cli",
"dev": "nodemon --watch 'src/**' --ext 'ts,json' --exec 'ts-node src/index.ts'",
"dev:web": "nodemon --watch 'src/**' --ext 'ts,json' --exec 'ts-node src/index.ts'",
"dev:cli": "nodemon --watch 'src/**' --ext 'ts,json' --exec 'ts-node src/index.ts --cli'",
"test-server": "ts-node src/test-server.ts",
"build": "tsc",
"test": "jest",
"lint": "eslint --ext .ts src/",
"lint:fix": "eslint --ext .ts src/ --fix"
"check:node": "node scripts/check-node-version.js",
"prestart": "npm run check:node",
"start": "node dist/server-ink.js",
"predev": "npm run check:node",
"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"
},
"keywords": [],
"author": "",
@@ -40,8 +44,11 @@
"dotenv": "^16.4.7",
"express": "^5.1.0",
"hyphenopoly": "^6.0.0",
"ifvms": "^1.1.6",
"inkjs": "^2.4.0",
"js-yaml": "^4.1.0",
"kokoro-js": "^1.2.0",
"marked": "^15.0.12",
"openai": "^4.91.0",
"socket.io": "^4.8.1"
}
+88
View File
@@ -0,0 +1,88 @@
# Third-Party Notices
This application includes or interfaces with the following third-party libraries, tools, fonts, and services.
## Browser-vendored libraries
| Component | Local files | Local version | Latest checked | License | Status |
| --- | --- | --- | --- | --- | --- |
| SmartyPants.js | `public/js/smartypants.js` | Header says 0.0.6 | npm `smartypants` 0.2.2 | BSD-3-Clause | Local file is not identical to npm `smartypants` 0.0.5, 0.0.9, or 0.2.2. It appears to be an older browser bundle with local or unreleased changes. |
| Hyphenopoly | `public/js/Hyphenopoly.js`, `public/js/Hyphenopoly_Loader.js`, `public/js/hyphenopoly.module.js`, `public/js/patterns/*.wasm` | Browser file header says 5.2.0-beta.1 | npm `hyphenopoly` 6.1.0 | MIT | `Hyphenopoly.js` matches the 5.2.0-beta.1 npm file after line-ending normalization. Loader differs by a small local/prototype change. Package dependency is 6.0.0, so the browser vendored copy is older than both the installed package and latest npm. |
| Knuth-Plass line breaking adapter | `public/js/knuth-and-plass.js` | No upstream version header | Unknown | Unknown/inherited prototype code | Local file differs from the prototype file and is application-adapted. Exact upstream could not be identified from file headers or npm metadata. |
| Line breaking support | `public/js/linebreak.js`, `public/js/linked-list.js` | No upstream version header | Unknown | Unknown/inherited prototype code | Files are identical to the prototype copies. Exact upstream could not be identified from file headers. |
| Kokoro JS browser bundle | `public/js/kokoro-js.js` | 1.2.0 | npm `kokoro-js` 1.2.1 | Apache-2.0 | Local file is byte-identical to `kokoro-js` 1.2.0 `dist/kokoro.web.js`; not latest. |
| Marked browser bundle | `public/js/vendor/marked.esm.js` | 15.0.12 | npm `marked` 15.0.12 | MIT | Local file is copied from the installed `marked` package. |
| EB Garamond 12 | `public/fonts/EBGaramond12/**` | Local EB Garamond 12 distribution | https://github.com/octaviopardo/EBGaramond12 | SIL OFL 1.1 | Active UI/book font family. Includes regular, italic, bold, bold italic, semibold, variable fonts, and OpenType features including `smcp`, `c2sc`, `case`, ligatures, oldstyle figures, proportional figures, swashes, and stylistic sets. |
| EB Garamond Initials | `public/fonts/EB-Garamond-Initials/**` | EB Garamond 0.016 initials font | https://github.com/georgd/EB-Garamond | SIL OFL 1.1 | Active drop-cap font. The Fill1 and Fill2 sibling fonts are layer fonts for ornament/letter separation; the combined Initials font is used for single-glyph drop caps. |
## Direct npm runtime dependencies
| Package | Installed | Latest checked | License | Credit |
| --- | --- | --- | --- | --- |
| `inkjs` | 2.4.0 | 2.4.0 | MIT | Yannick Lohse; based on ink by Inkle |
| `hyphenopoly` | 6.0.0 | 6.1.0 | MIT | Mathias Nater |
| `kokoro-js` | 1.2.0 | 1.2.1 | Apache-2.0 | hexgrad, Xenova |
| `ifvms` | 1.1.6 | 1.1.6 | MIT | Dannii Willis |
| `openai` | 4.91.0 | 6.38.0 | Apache-2.0 | OpenAI |
| `socket.io` | 4.8.1 | 4.8.3 | MIT | Socket.IO contributors |
| `express` | 5.1.0 | 5.2.1 | MIT | Express contributors |
| `axios` | 1.8.4 | 1.16.1 | MIT | Axios contributors |
| `marked` | 15.0.12 | 15.0.12 | MIT | marked contributors |
| `cors` | 2.8.5 | 2.8.6 | MIT | Troy Goode |
| `dotenv` | 16.4.7 | 17.4.2 | BSD-2-Clause | dotenv contributors |
| `js-yaml` | 4.1.0 | 4.1.1 | MIT | Vladimir Zapparov and contributors |
## Fonts and services
| Component | Use | License/Credit |
| --- | --- | --- |
| EB Garamond 12 | UI and book text font | SIL Open Font License 1.1; Georg Duffner, Octavio Pardo |
| EB Garamond Initials | Drop-cap font | SIL Open Font License 1.1; Georg Duffner |
| OpenAI / ChatGPT / Codex / GPT-image-2 | Coding assistance, writing assistance, generated images | OpenAI |
| Claude Code | Coding assistance | Anthropic |
| Suno | Music generation | Suno |
## License Texts
### MIT License
Permission is hereby granted, free of charge, to any person obtaining a copy of software and associated documentation files licensed under the MIT License, to deal in the software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the software, and to permit persons to whom the software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
### BSD 2-Clause License
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the copyright notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
### BSD 3-Clause License
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the copyright notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
### Apache License 2.0
Licensed under the Apache License, Version 2.0. You may not use files licensed under the Apache License except in compliance with the License. You may obtain a copy of the License at:
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the Apache License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
### SIL Open Font License 1.1
EB Garamond 12 and EB Garamond Initials are distributed under the SIL Open Font License, Version 1.1. The license permits use, study, modification and redistribution of the font, with reserved font name restrictions and the requirement that derivative fonts remain under the same license.
Full license: https://openfontlicense.org/
Local copies of the OFL text for the active EB Garamond files are included at `public/fonts/EBGaramond12/OFL.txt` and `public/fonts/EB-Garamond-Initials/EBGaramond-0.016/COPYING`.
+1189 -227
View File
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,93 @@
Copyright (c) 2010-2013 Georg Duffner (http://www.georgduffner.at)
All "EB Garamond" Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
@@ -0,0 +1,38 @@
EB Garamond 0.016 (2014-04-07)
===============================
* License
- No reserved font name any more
* New Features:
- [12-It] fina for e.fina (more shall come)
- [12-It] ss07: h and k with alternate accent placement above the stem
- [12-Re] Smallcaps now dont enable ss20 any longer
- OS/2 Optical Size settings
- Change the naming scheme for the small-caps-fonts: they now relate to the preferred family “EB Garamond SC”, hence they are named “EBGaramondSCXX-Style” where XX is the design size and Style is Regular (Italic, Bold, … once they exist).
* New and redrawn Glyphs:
- [08-Re] Half-ring modifiers
- [12-Re] Redraw esh (with Siva Kalyan)
- [12-Re] Regional identifiers 1F1E6 ­— 1F1FF (Tim Larson)
- [12-Re] Arrows and mathematical symbols (Tim Larson): arrowdbldown, arrowdblleft, arrowdblright, arrowdblup, gradient, product, uni210E, uni214B, uni219E — uni21A2, uni21DA — uni21DD, uni2210, uni2B45, uni2B46, uniFFFD
- [12-Re] e less round
- [12-Re] exclam more delicate
- [12-Re] Missing glyphs in Latin Extended C and D (Capillatus)
- [12-Re] uni1DC4 (more shall come)
- [12-Re] find a latin chi
- [12-Re] ditto mark
- [12-It] Fully redraw the small-caps
- [12-It] Redraw the Euro
- [12-It] Redraw the asterisk
* Fixes:
- Caron and alternate caron position on l.sc, dcaron.sc and tcaron.sc
- Lots of kerning and spacing
- Lots of anchors freshly positioned
- Fix f-ligatures for German locale
- c2sc + German umlauts
- extended IPA small-caps are petite-caps now
EB Garamond 0.015d (2013-06-28) and older
=========================================
Please have a look at the git sources.
@@ -0,0 +1,36 @@
# EB Garamond
## Claude Garamonts designs go opensource.
This project aims at providing a free version of the Garamond types, based on the Designs of the Berner specimen from 1592.
In the end, the fonts shall cover extended latin, greek and cyrillic scripts in different styles (regular, italic, bold, bolditalic) and design sizes. There are also fonts containing initials based on those found in a 16th century french bible print. The fonts make heavy use of opentype features for specialities like small caps or different number styles as well as for imitating renaissance typography.
For the use with Xe- and LuaLaTeX Im working on a configuration for mycrotype. For the use on the web via @fontface, the make-script produces eot and woff files which can be found in the web section. But be aware that they are not subset but contain the whole fonts, which might result in undesirably big files. Webfont hosters like googlefonts or fontsquirrel might provide better solutions.
## Fonts in this repository:
- EBGaramond12-Regular: Regular font for design size 12pt
- EBGaramond12-Italic: Italic font for design size 12pt
- EBGaramond12-Bold: Bold font for design size 12pt (very rough/unusable; not included in releases)
- EBGaramond08-Regular: Regular font for design size 8pt
- EBGaramond08-Italic: Italic font for design size 8pt (very rough spacing!)
- EBGaramond12-SC: Smallcaps font for programs that ignore opentype features (12pt)
- EBGaramond12-AllSC: All smallcaps font for programs that ignore opentype features
- EBGaramond08-SC: Smallcaps font for programs that ignore opentype features (8pt)
- EBGaramond-Initials: Initials
- EBGaramond-InitialsF1: Background (the ornament) of initials
- EBGaramond-InitialsF2: Foreground (the letter) of initials
- EBGaramond-Lettrines: Workbench for Initials fonts (not included in releases)
This is a work in progress, so expect bugs! The qualitiy of the fonts still varies widely! You can see every fonts current state in its *-Glyphs.pdf file in the specimen section.
## Mirrors:
Due to Github deciding not to provide a download area any more, this project resides in two mirrored repositories on Github (https://github.com/georgd/EB-Garamond) and Bitbucket (https://bitbucket.org/georgd/eb-garamond).
- Downloadable zip-files are at https://bitbucket.org/georgd/eb-garamond/downloads
- The issue tracker continues to live at https://github.com/georgd/EB-Garamond/issues
- Forks and pull requests should be possible on both platforms
For more infos please visit http://www.georgduffner.at/ebgaramond/
@@ -0,0 +1,21 @@
Short note about using XeLaTeX and LuaLatex with EB Garamond via fontspec.
EB Garamond fonts are loaded through fontspec with
\setmainfont{EB Garamond}
This will load the appropriate fonts for the actual sizes, currently EBGaramond08* for sizes up to 10pt and EBGaramond12* from 10.1pt onwards.
If you want to stay with one design size, you have to load its regular font and switch off optical sizes:
\setmainfont[OpticalSize=0]{EB Garamond 12 Regular}
If you wish to use a specific font file, e.g. EB Garamond Italic:
\setmainfont{EB Garamond 12 Italic}
N.B. It is preferable to use the +c2sc and +scmp features for Small Caps, rather than the SC or ALLSC font files.
\newfontfamily{\smallcaps}[RawFeature={+c2sc,+scmp}]{EB Garamond}
The specimens preamble may be a good guide too.
@@ -0,0 +1,25 @@
<HTML>
<HEAD>
<TITLE>FFonts - Redirect</TITLE>
<meta http-equiv="refresh" content="1;url=https://www.ffonts.net">
<script language="javascript">
<!--
//location.replace("https://www.ffonts.net");
-->
</script>
</HEAD>
<BODY >
<CENTER>
<TABLE WIDTH="400" HEIGHT="100%" BORDER="0" CELLPADDING="0" CELLSPACING="0">
<TR><TD WIDTH="100%" HEIGHT="20" ALIGN="CENTER" VALIGN="LEFT" BGCOLOR="#FFFFFF"><FONT FACE="Arial, Sans Serif, Verdana" SIZE=3 COLOR="#000000"><B>Redirect</B></FONT></TD></TR>
<TR><TD WIDTH="100%" HEIGHT="300" ALIGN="CENTER" VALIGN="LEFT" BGCOLOR="#000000"><BR><BR><BR>
<FONT FACE="Arial, Sans Serif, Verdana" SIZE=3 COLOR="#FFFFFF">If this page does not automatically redirect you, click below to go to the FFonts homepage</FONT>
<BR><BR>
<FONT FACE="Arial, Sans Serif, Verdana" SIZE=3 COLOR="#FFFFFF"><A HREF="https://www.ffonts.net" TARGET="_top">http://www.ffonts.net</A>
</TD></TR>
</TD></TR>
</TABLE>
</CENTER>
</BODY>
</HTML>
@@ -0,0 +1,5 @@
Download Free fonts from FFonts:
https://www.ffonts.net
Free Fonts Donwload
@@ -0,0 +1,44 @@
Installing fonts is quick and simple. Once fonts are installed, they are available to yours programs.
The font packages you download from the www.ffonts.net is in compressed .zip files to reduce file size and make downloading faster.
If you have downloaded a font that is saved in .zip format, you can "unzip" it by double-clicking the icon for the font and following the instructions on the screen.
INSTALLING MORE THAN 1000 FONTS ONTO YOUR COMPUTER CAN CAUSE A REDUCTION IN SPEED.
WE RECOMMEND THAT YOU LIMIT YOURSELF TO A NUMBER LESS THAN 1000 (400-500).
Installing new fonts
How to install a font under Windows? Download Font
Click on the "Download" button, save the font file on your hard disk.
Under Windows Vista : Select the font files (.ttf, .otf or .fon) then Right-click > Install
Under any version of Windows : Place the font files (.ttf, .otf or .fon) into the Fonts folder, usually C:\Windows\Fonts or C:\WINNT\Fonts
(can be reached as well by the Start Menu > Control Panel > Appearance and Themes > Fonts).
Tip : if you punctually need a font, you don't need to install it. Just double-click on the .ttf file, and while the preview window is opened you can use it in most of the programs you'll launch (apart from a few exceptions like OpenOffice).
How to install a font under Mac OS ? Download Font
Click on the "Download" button, save the font file on your hard disk.
Under Mac OS X 10.3 or above (including the FontBook) : Double-click the font file > "Install font" button at the bottom of the preview.
Under any version of Mac OS X : Put the files into /Library/Fonts (for all users) or into /Users/Your_username/Library/Fonts (for you only).
Under Mac OS 9 or earlier : Download the font files (.ttf or .otf),Then drag the fonts suitcases into the System folder. The system will propose you to add them to the Fonts folder.
How to install a font under Linux ? Download Font
Click on the "Download" button, save the font file on your hard disk.
Copy the font files (.ttf or .otf) to fonts:/// in the File manager.
Notes
* To select more than one font to add, in step 6, hold down the CTRL key, and then click each of the fonts you want to add.
* You can also drag OpenType, TrueType, Type 1, and raster fonts from another location to add them to the Fonts folder. This works only if the font is not already in the Fonts folder.
* To add fonts from a network drive without using disk space on your computer, clear the Copy fonts to Fonts folder check box in the Add Fonts dialog box. This is available only when you install OpenType, TrueType, or raster fonts using the Install New Font option on the File menu.
@@ -0,0 +1,20 @@
<HTML>
<HEAD>
<TITLE>WhatFontIs - Redirect</TITLE>
<meta http-equiv="refresh" content="15;url=https://www.WhatFontIs.com">
<script language="javascript">
<!--
//location.replace("https://www.WhatFontIs.com");
-->
</script>
</HEAD>
<BODY >
<CENTER>
<A HREF="https://www.WhatFontIs.com/?utm_source=ff" TARGET="_top"><img src="https://www.whatfontis.com/sponsored/v1.png" border="0" alt="www.WhatFontIs.com"></A>
</TABLE>
</CENTER>
</BODY>
</HTML>
@@ -0,0 +1,93 @@
Copyright (c) 2010-2013 Georg Duffner (http://www.georgduffner.at)
All "EB Garamond" Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
@@ -0,0 +1,38 @@
EB Garamond 0.016 (2014-04-07)
===============================
* License
- No reserved font name any more
* New Features:
- [12-It] fina for e.fina (more shall come)
- [12-It] ss07: h and k with alternate accent placement above the stem
- [12-Re] Smallcaps now dont enable ss20 any longer
- OS/2 Optical Size settings
- Change the naming scheme for the small-caps-fonts: they now relate to the preferred family “EB Garamond SC”, hence they are named “EBGaramondSCXX-Style” where XX is the design size and Style is Regular (Italic, Bold, … once they exist).
* New and redrawn Glyphs:
- [08-Re] Half-ring modifiers
- [12-Re] Redraw esh (with Siva Kalyan)
- [12-Re] Regional identifiers 1F1E6 ­— 1F1FF (Tim Larson)
- [12-Re] Arrows and mathematical symbols (Tim Larson): arrowdbldown, arrowdblleft, arrowdblright, arrowdblup, gradient, product, uni210E, uni214B, uni219E — uni21A2, uni21DA — uni21DD, uni2210, uni2B45, uni2B46, uniFFFD
- [12-Re] e less round
- [12-Re] exclam more delicate
- [12-Re] Missing glyphs in Latin Extended C and D (Capillatus)
- [12-Re] uni1DC4 (more shall come)
- [12-Re] find a latin chi
- [12-Re] ditto mark
- [12-It] Fully redraw the small-caps
- [12-It] Redraw the Euro
- [12-It] Redraw the asterisk
* Fixes:
- Caron and alternate caron position on l.sc, dcaron.sc and tcaron.sc
- Lots of kerning and spacing
- Lots of anchors freshly positioned
- Fix f-ligatures for German locale
- c2sc + German umlauts
- extended IPA small-caps are petite-caps now
EB Garamond 0.015d (2013-06-28) and older
=========================================
Please have a look at the git sources.
@@ -0,0 +1,36 @@
# EB Garamond
## Claude Garamonts designs go opensource.
This project aims at providing a free version of the Garamond types, based on the Designs of the Berner specimen from 1592.
In the end, the fonts shall cover extended latin, greek and cyrillic scripts in different styles (regular, italic, bold, bolditalic) and design sizes. There are also fonts containing initials based on those found in a 16th century french bible print. The fonts make heavy use of opentype features for specialities like small caps or different number styles as well as for imitating renaissance typography.
For the use with Xe- and LuaLaTeX Im working on a configuration for mycrotype. For the use on the web via @fontface, the make-script produces eot and woff files which can be found in the web section. But be aware that they are not subset but contain the whole fonts, which might result in undesirably big files. Webfont hosters like googlefonts or fontsquirrel might provide better solutions.
## Fonts in this repository:
- EBGaramond12-Regular: Regular font for design size 12pt
- EBGaramond12-Italic: Italic font for design size 12pt
- EBGaramond12-Bold: Bold font for design size 12pt (very rough/unusable; not included in releases)
- EBGaramond08-Regular: Regular font for design size 8pt
- EBGaramond08-Italic: Italic font for design size 8pt (very rough spacing!)
- EBGaramond12-SC: Smallcaps font for programs that ignore opentype features (12pt)
- EBGaramond12-AllSC: All smallcaps font for programs that ignore opentype features
- EBGaramond08-SC: Smallcaps font for programs that ignore opentype features (8pt)
- EBGaramond-Initials: Initials
- EBGaramond-InitialsF1: Background (the ornament) of initials
- EBGaramond-InitialsF2: Foreground (the letter) of initials
- EBGaramond-Lettrines: Workbench for Initials fonts (not included in releases)
This is a work in progress, so expect bugs! The qualitiy of the fonts still varies widely! You can see every fonts current state in its *-Glyphs.pdf file in the specimen section.
## Mirrors:
Due to Github deciding not to provide a download area any more, this project resides in two mirrored repositories on Github (https://github.com/georgd/EB-Garamond) and Bitbucket (https://bitbucket.org/georgd/eb-garamond).
- Downloadable zip-files are at https://bitbucket.org/georgd/eb-garamond/downloads
- The issue tracker continues to live at https://github.com/georgd/EB-Garamond/issues
- Forks and pull requests should be possible on both platforms
For more infos please visit http://www.georgduffner.at/ebgaramond/
@@ -0,0 +1,21 @@
Short note about using XeLaTeX and LuaLatex with EB Garamond via fontspec.
EB Garamond fonts are loaded through fontspec with
\setmainfont{EB Garamond}
This will load the appropriate fonts for the actual sizes, currently EBGaramond08* for sizes up to 10pt and EBGaramond12* from 10.1pt onwards.
If you want to stay with one design size, you have to load its regular font and switch off optical sizes:
\setmainfont[OpticalSize=0]{EB Garamond 12 Regular}
If you wish to use a specific font file, e.g. EB Garamond Italic:
\setmainfont{EB Garamond 12 Italic}
N.B. It is preferable to use the +c2sc and +scmp features for Small Caps, rather than the SC or ALLSC font files.
\newfontfamily{\smallcaps}[RawFeature={+c2sc,+scmp}]{EB Garamond}
The specimens preamble may be a good guide too.

Some files were not shown because too many files have changed in this diff Show More