Initial commit
This commit is contained in:
Vendored
+607
@@ -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
|
||||
Reference in New Issue
Block a user