Initial commit
This commit is contained in:
@@ -0,0 +1,265 @@
|
||||
/**
|
||||
* Command-line interface for running the interactive fiction game
|
||||
*/
|
||||
|
||||
import * as readline from 'readline';
|
||||
import * as path from 'path';
|
||||
import * as dotenv from 'dotenv';
|
||||
import { TextAdventureEngine } from '../engine/game-engine';
|
||||
import { OpenRouterProvider } from '../llm/openrouter-provider';
|
||||
import { ActionRequest, NarrativeRequest } from '../interfaces/llm';
|
||||
|
||||
// Load environment variables
|
||||
dotenv.config();
|
||||
|
||||
export class GameRunner {
|
||||
private engine: TextAdventureEngine;
|
||||
private llmProvider: OpenRouterProvider;
|
||||
private rl: readline.Interface | null = null;
|
||||
private gameContext: string = '';
|
||||
private gameHistory: string[] = [];
|
||||
private suggestedCommands: string[] = [];
|
||||
|
||||
constructor() {
|
||||
this.engine = new TextAdventureEngine();
|
||||
this.llmProvider = new OpenRouterProvider();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the game
|
||||
*/
|
||||
public async initialize(worldPath: string): Promise<void> {
|
||||
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
|
||||
*/
|
||||
public async start(): Promise<void> {
|
||||
// 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: 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
|
||||
*/
|
||||
private gameLoop(): void {
|
||||
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
|
||||
*/
|
||||
public async processCommand(input: string): Promise<string> {
|
||||
try {
|
||||
// Process player input
|
||||
const actionRequest: 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: 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
|
||||
*/
|
||||
public end(): void {
|
||||
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
|
||||
*/
|
||||
private updateGameContext(narrative: string): void {
|
||||
// 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
|
||||
*/
|
||||
public 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
|
||||
*/
|
||||
public getCurrentRoomDescription(): string {
|
||||
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
|
||||
*/
|
||||
public getSuggestions(): string[] {
|
||||
return this.suggestedCommands;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a saved game state
|
||||
* Used by web interface
|
||||
*/
|
||||
public loadGameState(savedState: any): void {
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user