Files
ai.interactive.fiction/dist/engine/game-engine.js
2025-04-01 08:37:41 +02:00

607 lines
23 KiB
JavaScript

"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