"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