Initial commit

This commit is contained in:
2025-04-01 08:37:41 +02:00
commit 39c1b6ff0a
59 changed files with 14076 additions and 0 deletions
+64
View File
@@ -0,0 +1,64 @@
/**
* 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
@@ -0,0 +1,262 @@
"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
@@ -0,0 +1,77 @@
/**
* 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
@@ -0,0 +1,607 @@
"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
@@ -0,0 +1,4 @@
/**
* Main entry point for the AI Interactive Fiction application
*/
export {};
+110
View File
@@ -0,0 +1,110 @@
"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
@@ -0,0 +1 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAAA;;GAEG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAGH,+CAAiC;AACjC,mDAA+C;AAC/C,8EAA8E;AAC9E,qCAAuC;AAEvC,6BAA6B;AAC7B,OAAO,CAAC,GAAG,CAAC,kCAAkC,CAAC,CAAC;AAChD,IAAI,CAAC;IACH,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC;IAC/B,IAAI,MAAM,CAAC,KAAK,EAAE,CAAC;QACjB,OAAO,CAAC,KAAK,CAAC,0BAA0B,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC;IAC1D,CAAC;SAAM,CAAC;QACN,OAAO,CAAC,GAAG,CAAC,2CAA2C,CAAC,CAAC;IAC3D,CAAC;AACH,CAAC;AAAC,OAAO,KAAK,EAAE,CAAC;IACf,OAAO,CAAC,KAAK,CAAC,6BAA6B,EAAE,KAAK,CAAC,CAAC;AACtD,CAAC;AAED,KAAK,UAAU,IAAI;IACjB,IAAI,CAAC;QACH,OAAO,CAAC,GAAG,CAAC,gCAAgC,CAAC,CAAC;QAC9C,OAAO,CAAC,GAAG,CAAC,wEAAwE,CAAC,CAAC;QACtF,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAEhB,oEAAoE;QACpE,MAAM,SAAS,GAAG,OAAO,CAAC,GAAG,CAAC,kBAAkB,IAAI,iCAAiC,CAAC;QACtF,OAAO,CAAC,GAAG,CAAC,qBAAqB,SAAS,EAAE,CAAC,CAAC;QAC9C,OAAO,CAAC,GAAG,CAAC,uBAAuB,OAAO,CAAC,GAAG,CAAC,kBAAkB,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC;QAC/F,OAAO,CAAC,GAAG,CAAC,qBAAqB,OAAO,CAAC,GAAG,CAAC,gBAAgB,IAAI,iBAAiB,EAAE,CAAC,CAAC;QAEtF,qCAAqC;QACrC,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;QACnC,MAAM,OAAO,GAAG,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;QAE9D,IAAI,OAAO,EAAE,CAAC;YACZ,WAAW;YACX,OAAO,CAAC,GAAG,CAAC,yBAAyB,CAAC,CAAC;YAEvC,oCAAoC;YACpC,OAAO,CAAC,GAAG,CAAC,yBAAyB,CAAC,CAAC;YACvC,MAAM,UAAU,GAAG,IAAI,wBAAU,EAAE,CAAC;YAEpC,OAAO,CAAC,GAAG,CAAC,sBAAsB,CAAC,CAAC;YACpC,MAAM,UAAU,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC;YAEvC,qBAAqB;YACrB,OAAO,CAAC,GAAG,CAAC,sBAAsB,CAAC,CAAC;YACpC,MAAM,UAAU,CAAC,KAAK,EAAE,CAAC;QAC3B,CAAC;aAAM,CAAC;YACN,sEAAsE;YACtE,OAAO,CAAC,GAAG,CAAC,mCAAmC,CAAC,CAAC;YAEjD,yBAAyB;YACzB,MAAM,YAAY,GAAG,IAAI,CAAC;YAC1B,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC;YAC1E,MAAM,UAAU,GAAG,EAAE,CAAC;YAEtB,0CAA0C;YAC1C,OAAO,CAAC,GAAG,CAAC,wBAAwB,CAAC,CAAC;YACtC,MAAM,IAAA,oBAAW,EAAC,IAAI,EAAE,UAAU,CAAC,CAAC;QACtC,CAAC;IACH,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,CAAC,KAAK,CAAC,kBAAkB,EAAE,KAAK,CAAC,CAAC;QACzC,IAAI,KAAK,YAAY,KAAK,EAAE,CAAC;YAC3B,OAAO,CAAC,KAAK,CAAC,aAAa,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;YACzC,OAAO,CAAC,KAAK,CAAC,gBAAgB,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC;YAC/C,OAAO,CAAC,KAAK,CAAC,cAAc,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC;QAC7C,CAAC;QACD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;AACH,CAAC;AAED,wBAAwB;AACxB,OAAO,CAAC,GAAG,CAAC,yBAAyB,CAAC,CAAC;AACvC,IAAI,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE;IACnB,OAAO,CAAC,KAAK,CAAC,0BAA0B,EAAE,KAAK,CAAC,CAAC;IACjD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
+39
View File
@@ -0,0 +1,39 @@
/**
* 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
@@ -0,0 +1,6 @@
"use strict";
/**
* Interfaces for the game engine
*/
Object.defineProperty(exports, "__esModule", { value: true });
//# sourceMappingURL=engine.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"engine.js","sourceRoot":"","sources":["../../src/interfaces/engine.ts"],"names":[],"mappings":";AAAA;;GAEG"}
+46
View File
@@ -0,0 +1,46 @@
/**
* 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
@@ -0,0 +1,6 @@
"use strict";
/**
* Interfaces for LLM integration
*/
Object.defineProperty(exports, "__esModule", { value: true });
//# sourceMappingURL=llm.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"llm.js","sourceRoot":"","sources":["../../src/interfaces/llm.ts"],"names":[],"mappings":";AAAA;;GAEG"}
+61
View File
@@ -0,0 +1,61 @@
/**
* 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
@@ -0,0 +1,6 @@
"use strict";
/**
* Core interfaces for the interactive fiction world model
*/
Object.defineProperty(exports, "__esModule", { value: true });
//# sourceMappingURL=world-model.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"world-model.js","sourceRoot":"","sources":["../../src/interfaces/world-model.ts"],"names":[],"mappings":";AAAA;;GAEG"}
+36
View File
@@ -0,0 +1,36 @@
/**
* 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
@@ -0,0 +1,192 @@
"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
@@ -0,0 +1 @@
{"version":3,"file":"openrouter-provider.js","sourceRoot":"","sources":["../../src/llm/openrouter-provider.ts"],"names":[],"mappings":"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"}
+11
View File
@@ -0,0 +1,11 @@
/**
* 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
@@ -0,0 +1,252 @@
"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 = 3000;
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
+71
View File
@@ -0,0 +1,71 @@
/**
* 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
@@ -0,0 +1,399 @@
"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