Compare commits
	
		
			10 Commits
		
	
	
		
			fbf841b551
			...
			08ee3d1b5d
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 08ee3d1b5d | |||
| 8f6d7e4db4 | |||
| b134f1849a | |||
| e26747cc7c | |||
| cc306dbf87 | |||
| ccd4974266 | |||
| b3058097b4 | |||
| 03eddfcd74 | |||
| 09e10e94b5 | |||
| 8d854945e1 | 
							
								
								
									
										102
									
								
								Entity.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										102
									
								
								Entity.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,102 @@
 | 
				
			|||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Entity.ts contains the Entity class which is a base class that handles effects and
 | 
				
			||||||
 | 
					 * containing items. It is the base class for Player and Scene.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import type { Conditions, Item, EntityProperties } from "./types.ts";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default class Entity<T extends EntityProperties> {
 | 
				
			||||||
 | 
						protected _conditions: Conditions<T>;
 | 
				
			||||||
 | 
						protected _properties: T;
 | 
				
			||||||
 | 
						protected _activeEffects: string[];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						constructor(
 | 
				
			||||||
 | 
							properties: T,
 | 
				
			||||||
 | 
							conditions: Conditions<T>,
 | 
				
			||||||
 | 
							activeEffects: string[] = []
 | 
				
			||||||
 | 
						) {
 | 
				
			||||||
 | 
							this._conditions = conditions; // Conditional properties
 | 
				
			||||||
 | 
							this._properties = properties; // Base properties
 | 
				
			||||||
 | 
							this._activeEffects = activeEffects;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						get activeEffects(): string[] {
 | 
				
			||||||
 | 
							return this._activeEffects;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						set conditions(conditions: Conditions<T>) {
 | 
				
			||||||
 | 
							this._conditions = conditions;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						get properties(): T {
 | 
				
			||||||
 | 
							return this._properties;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						set properties(properties: T) {
 | 
				
			||||||
 | 
							this._properties = properties;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						addEffect(effect: string): void {
 | 
				
			||||||
 | 
							this.activeEffects.push(effect);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Player effects should be applied first, then scene effects should be applied.
 | 
				
			||||||
 | 
						// This will mean that scene effects will take precedence over player effects.
 | 
				
			||||||
 | 
						// Returns base properties with conditional properties applied.
 | 
				
			||||||
 | 
						applyEffects(
 | 
				
			||||||
 | 
							activePlayerEffects: string[],
 | 
				
			||||||
 | 
							activeSceneEffects: string[]
 | 
				
			||||||
 | 
						): T {
 | 
				
			||||||
 | 
							const playerSet = new Set(activePlayerEffects);
 | 
				
			||||||
 | 
							const sceneSet = new Set(activeSceneEffects);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							const effects = this._conditions.effects.filter(({ name, source }) => {
 | 
				
			||||||
 | 
								const set = source === "player" ? playerSet : sceneSet;
 | 
				
			||||||
 | 
								return set.has(name);
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							const appliedProperties = { ...this._properties };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// for each effect, apply changed properties to the
 | 
				
			||||||
 | 
							for (const effect of effects) {
 | 
				
			||||||
 | 
								Object.assign(appliedProperties, effect.properties);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return appliedProperties;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Generate a description of the contained items
 | 
				
			||||||
 | 
						description(items: Item[]): string | null {
 | 
				
			||||||
 | 
							if (items.length === 0) return null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							const vowels = ["a", "e", "i", "o", "u"];
 | 
				
			||||||
 | 
							const description = items
 | 
				
			||||||
 | 
								.map(({ name }, i) => {
 | 
				
			||||||
 | 
									let anItem = `${vowels.includes(name[0]) ? "an" : "a"} ${name}`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									// if we have more than one item, and this is the last item...
 | 
				
			||||||
 | 
									if (i > 1 && i + 1 === items.length) {
 | 
				
			||||||
 | 
										anItem = `and ${anItem}`;
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									return anItem;
 | 
				
			||||||
 | 
								})
 | 
				
			||||||
 | 
								.join(", ");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return description;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Checks if a given effect exists as an active effect on this entity
 | 
				
			||||||
 | 
						hasActiveEffect(effect: string): boolean {
 | 
				
			||||||
 | 
							return this._activeEffects.includes(effect);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Removes an effect if it is active on this entity
 | 
				
			||||||
 | 
						removeEffect(effect: string): void {
 | 
				
			||||||
 | 
							const idx = this._activeEffects.findIndex((e) => e === effect);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if (idx >= 0) {
 | 
				
			||||||
 | 
								this._activeEffects.splice(idx, 1);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										136
									
								
								Interpreter.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										136
									
								
								Interpreter.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,136 @@
 | 
				
			|||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Interpreter.ts contains the Interpreter class which takes the command that the user
 | 
				
			||||||
 | 
					 * made as well as the player and scene state. By checking each word of the user command,
 | 
				
			||||||
 | 
					 * the interpreter figures out what action the user wants to take and then execute that
 | 
				
			||||||
 | 
					 * command.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import type Player from "./Player.ts";
 | 
				
			||||||
 | 
					import type Scene from "./Scene.ts";
 | 
				
			||||||
 | 
					import { terms } from "./terms/terms.ts";
 | 
				
			||||||
 | 
					import type {
 | 
				
			||||||
 | 
						ActionTerm,
 | 
				
			||||||
 | 
						ActionFn,
 | 
				
			||||||
 | 
						Category,
 | 
				
			||||||
 | 
						Constant,
 | 
				
			||||||
 | 
						Term,
 | 
				
			||||||
 | 
					} from "./terms/Term.ts";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const DEFAULT_ISSUE = "I don't understand that.";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default class Interpreter {
 | 
				
			||||||
 | 
						action: ActionFn | null; // The final command to run
 | 
				
			||||||
 | 
						expectedCategories: Category[]; // command categories that might follow
 | 
				
			||||||
 | 
						expectedConstants: Constant[]; // constants that might follow
 | 
				
			||||||
 | 
						isExpectingVariable: boolean; // a variable might follow
 | 
				
			||||||
 | 
						isValid: boolean; // signals whether the command is understood
 | 
				
			||||||
 | 
						issue: string | null; // a statement for a command that isn't clear
 | 
				
			||||||
 | 
						player: Player; // represents the player state
 | 
				
			||||||
 | 
						scene: Scene; // represents the scene state
 | 
				
			||||||
 | 
						tokens: string[]; // user command split by spaces (tokens)
 | 
				
			||||||
 | 
						vars: string[]; // variables taken from commandf
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						constructor(player: Player, scene: Scene, command: string) {
 | 
				
			||||||
 | 
							this.action = null;
 | 
				
			||||||
 | 
							this.expectedCategories = ["action"];
 | 
				
			||||||
 | 
							this.expectedConstants = [];
 | 
				
			||||||
 | 
							this.isExpectingVariable = false;
 | 
				
			||||||
 | 
							this.isValid = true;
 | 
				
			||||||
 | 
							this.issue = null;
 | 
				
			||||||
 | 
							this.player = player;
 | 
				
			||||||
 | 
							this.scene = scene;
 | 
				
			||||||
 | 
							this.tokens = command.split(" ");
 | 
				
			||||||
 | 
							this.vars = [];
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// If the intepreter can figure out what the user meant, execute runs the action
 | 
				
			||||||
 | 
						// function it found with whatever variables it found. If the interpreter could not
 | 
				
			||||||
 | 
						// interpret the user command, then it will respond with whatever the issue was.
 | 
				
			||||||
 | 
						execute(): string {
 | 
				
			||||||
 | 
							let response = DEFAULT_ISSUE;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if (this.isValid && this.action) {
 | 
				
			||||||
 | 
								response = this.action(this.player, this.scene, ...this.vars);
 | 
				
			||||||
 | 
							} else if (this.issue) {
 | 
				
			||||||
 | 
								response = this.issue;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return response;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// interpret tries to figure out what the user wants. If it can, it sets as
 | 
				
			||||||
 | 
						// action function for this object and whatever variables it finds. If it
 | 
				
			||||||
 | 
						// cannot, then it sets the issue.
 | 
				
			||||||
 | 
						interpret() {
 | 
				
			||||||
 | 
							for (const token of this.tokens) {
 | 
				
			||||||
 | 
								if (!this.isValid) break;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								this.interpretToken(token);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						private interpretToken(token: string) {
 | 
				
			||||||
 | 
							const term = Interpreter.lookupTerm(token); // is the token a term?
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if (!term) {
 | 
				
			||||||
 | 
								// if not...
 | 
				
			||||||
 | 
								if (this.isExpectingVariable) {
 | 
				
			||||||
 | 
									// ...are we expecting a variable?
 | 
				
			||||||
 | 
									this.processVariable(token); // add variable
 | 
				
			||||||
 | 
								} else {
 | 
				
			||||||
 | 
									// ...or...
 | 
				
			||||||
 | 
									this.stopParsing(); // this isn't a valid command.
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							} else if (
 | 
				
			||||||
 | 
								this.expectedConstants.find(({ constant }) => constant === token)
 | 
				
			||||||
 | 
							) {
 | 
				
			||||||
 | 
								// if this matches an expected constant string...
 | 
				
			||||||
 | 
								this.processTerm(term);
 | 
				
			||||||
 | 
							} else if (this.expectedCategories.includes(term.category)) {
 | 
				
			||||||
 | 
								// ...or category
 | 
				
			||||||
 | 
								this.processTerm(term);
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								this.stopParsing(); // ...this is not a valid command
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						private processTerm(term: ActionTerm | Term): void {
 | 
				
			||||||
 | 
							if ("action" in term) {
 | 
				
			||||||
 | 
								this.addAction(term);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							this.expectedCategories = term.precedesCategories;
 | 
				
			||||||
 | 
							this.expectedConstants = term.precedesConstants;
 | 
				
			||||||
 | 
							this.isExpectingVariable = term.canPrecedeVariable;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						private processVariable(token: string) {
 | 
				
			||||||
 | 
							if (this.vars.length >= 2) {
 | 
				
			||||||
 | 
								this.stopParsing();
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								this.vars.unshift(token.toLocaleLowerCase());
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						private addAction(term: ActionTerm) {
 | 
				
			||||||
 | 
							// make sure we expected an action and process it if it was
 | 
				
			||||||
 | 
							if (this.expectedCategories.includes("action")) {
 | 
				
			||||||
 | 
								this.action = term.action;
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								this.stopParsing();
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						private stopParsing(issue = DEFAULT_ISSUE) {
 | 
				
			||||||
 | 
							this.issue = issue;
 | 
				
			||||||
 | 
							this.isValid = false;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						private static lookupTerm(token: string): Term | null {
 | 
				
			||||||
 | 
							if (token in terms) {
 | 
				
			||||||
 | 
								return terms[token];
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								return null;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										50
									
								
								Player.ts
									
									
									
									
									
								
							
							
						
						
									
										50
									
								
								Player.ts
									
									
									
									
									
								
							@ -1,33 +1,37 @@
 | 
				
			|||||||
import { Item } from "./data/data.ts";
 | 
					/**
 | 
				
			||||||
import User from "./User.ts";
 | 
					 * Player.ts contains the Player class. It represents the player's state as the game
 | 
				
			||||||
 | 
					 * goes on.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default class Player {
 | 
					import { Effect, Item, EntityProperties } from "./types.ts";
 | 
				
			||||||
	#items: Item[];
 | 
					import Entity from "./Entity.ts";
 | 
				
			||||||
	#user: User;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	constructor(items?: Item[]) {
 | 
					// The Player is a type of "entity" which is a generic object that can have effects
 | 
				
			||||||
		this.#items = items || [];
 | 
					// and items.
 | 
				
			||||||
		this.#user = new User();
 | 
					export default class Player extends Entity<EntityProperties> {
 | 
				
			||||||
 | 
						constructor(items: Item[] = [], _effects: Effect<EntityProperties>[] = []) {
 | 
				
			||||||
 | 
							super({ items }, { effects: [] });
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	drop(item: Item) {
 | 
						// inventory is a getter that returns the items in the user's inventory
 | 
				
			||||||
		this.#items.push(item);
 | 
						get inventory(): Item[] | null {
 | 
				
			||||||
 | 
							return this._properties.items || null;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	async inventory() {
 | 
						// look returns a string that describes the players inventory
 | 
				
			||||||
		const vowels = ["a", "e", "i", "o", "u"];
 | 
						look(): string {
 | 
				
			||||||
		const description = this.#items
 | 
							const description = super.description(this._properties.items || []);
 | 
				
			||||||
			.map(({ name }, i) => {
 | 
					 | 
				
			||||||
				let anItem = `${vowels.includes(name[0]) ? "an" : "a"} ${name}`;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
				if (i + 1 === this.#items.length) {
 | 
							if (description) {
 | 
				
			||||||
					anItem = `and ${anItem}`;
 | 
								return `You have ${description}`;
 | 
				
			||||||
				}
 | 
							} else {
 | 
				
			||||||
 | 
								return "You have nothing.";
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				return anItem;
 | 
						// put takes an item and puts it into the players inventory
 | 
				
			||||||
			})
 | 
						put(item: Item) {
 | 
				
			||||||
			.join(", ");
 | 
							if (!this._properties.items) this._properties.items = [];
 | 
				
			||||||
 | 
							this._properties.items.push(item);
 | 
				
			||||||
		await this.#user.tell(`You have ${description}.`);
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										112
									
								
								Scene.ts
									
									
									
									
									
								
							
							
						
						
									
										112
									
								
								Scene.ts
									
									
									
									
									
								
							@ -1,35 +1,109 @@
 | 
				
			|||||||
import { GameData, Item } from "./data/data.ts";
 | 
					/**
 | 
				
			||||||
import User from "./User.ts";
 | 
					 * Scene.ts contains the Scene class. It represents the state of the current scene.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default class Scene {
 | 
					import type { Exit, StoryScene, Item, SceneProperties } from "./types.ts";
 | 
				
			||||||
	#items: Item[];
 | 
					import Entity from "./Entity.ts";
 | 
				
			||||||
	#user: User;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	constructor(gameData: GameData) {
 | 
					// The Scene is a type of "Entity" which is a generic object that can have effects
 | 
				
			||||||
		this.#items = gameData.items;
 | 
					// and items.
 | 
				
			||||||
		this.#user = new User();
 | 
					export default class Scene extends Entity<SceneProperties> {
 | 
				
			||||||
 | 
						protected _map: StoryScene<SceneProperties>[];
 | 
				
			||||||
 | 
						protected _identifier: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						constructor(identifier: string, map: StoryScene<SceneProperties>[]) {
 | 
				
			||||||
 | 
							const scene = Scene.getScene(identifier, map);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if (!scene) {
 | 
				
			||||||
 | 
								throw new Error("cannot find scene!");
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							const { properties, conditions } = scene;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							super(properties, conditions);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							this._identifier = identifier;
 | 
				
			||||||
 | 
							this._map = map;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	async look() {
 | 
						changeScene(identifier: string) {
 | 
				
			||||||
		const description = this.#items
 | 
							const scene = Scene.getScene(identifier, this._map);
 | 
				
			||||||
			.map(({ description }) => description)
 | 
					 | 
				
			||||||
			.join(" ");
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
		await this.#user.tell(description);
 | 
							if (scene) {
 | 
				
			||||||
 | 
								const { conditions, properties } = scene;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								this.properties = properties;
 | 
				
			||||||
 | 
								this.conditions = conditions;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	async take(target: string): Promise<Item | null> {
 | 
						// get removes an items from the scene and returns it
 | 
				
			||||||
		const idx = this.#items.findIndex(({ name }) => name === target);
 | 
						get(target: string): Item | null {
 | 
				
			||||||
 | 
							const { items = [] } = this._properties;
 | 
				
			||||||
 | 
							const idx = items.findIndex(({ name }) => name === target);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// if we found an index for the given item
 | 
				
			||||||
		if (idx >= 0) {
 | 
							if (idx >= 0) {
 | 
				
			||||||
			const item = this.#items[idx];
 | 
								const item = items[idx];
 | 
				
			||||||
			this.#items.splice(idx, 1);
 | 
								items.splice(idx, 1);
 | 
				
			||||||
 | 
					 | 
				
			||||||
			await this.#user.tell("Taken.");
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
			return item;
 | 
								return item;
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// if an item wasn't found and returned, return null
 | 
				
			||||||
		return null;
 | 
							return null;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// look returns a string that describes the scene and the items in it
 | 
				
			||||||
 | 
						look(activePlayerEffects: string[], activeSceneEffects: string[]): string {
 | 
				
			||||||
 | 
							// the properties of the scene after effects have been examined an applied
 | 
				
			||||||
 | 
							const properties = this.applyEffects(
 | 
				
			||||||
 | 
								activePlayerEffects,
 | 
				
			||||||
 | 
								activeSceneEffects
 | 
				
			||||||
 | 
							);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// description of the items in the scene
 | 
				
			||||||
 | 
							const itemsDescription = super.description(properties.items || []);
 | 
				
			||||||
 | 
							const { description, exits = [] } = properties; // description of the room
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// will be a string that includes both the room and item descriptions
 | 
				
			||||||
 | 
							let fullDescription = description || "Nothing to see here...";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							const exitsDescription = Scene.describeExits(exits);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							fullDescription +=
 | 
				
			||||||
 | 
								exitsDescription && `\n\nThere is ${exitsDescription}.`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// if there is a description of the items...
 | 
				
			||||||
 | 
							if (itemsDescription) {
 | 
				
			||||||
 | 
								fullDescription += `\n\nThere is ${itemsDescription}`;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return fullDescription;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Turn exits into a string description with an "and" separating the last two items
 | 
				
			||||||
 | 
						private static describeExits(exits: Exit[]) {
 | 
				
			||||||
 | 
							const exitDescriptions = exits.map(({ description }) => description);
 | 
				
			||||||
 | 
							if (exits.length === 0) {
 | 
				
			||||||
 | 
								return "";
 | 
				
			||||||
 | 
							} else if (exits.length === 1) {
 | 
				
			||||||
 | 
								return exitDescriptions[0];
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								const lastExit = exitDescriptions[exitDescriptions.length - 1];
 | 
				
			||||||
 | 
								const restExits = exitDescriptions.slice(
 | 
				
			||||||
 | 
									0,
 | 
				
			||||||
 | 
									exitDescriptions.length - 1
 | 
				
			||||||
 | 
								);
 | 
				
			||||||
 | 
								return `${restExits.join(", ")} and ${lastExit}`;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// This needs to be static so that we can use it in the constructor
 | 
				
			||||||
 | 
						private static getScene(
 | 
				
			||||||
 | 
							identifier: string,
 | 
				
			||||||
 | 
							map: StoryScene<SceneProperties>[]
 | 
				
			||||||
 | 
						): StoryScene<SceneProperties> | null {
 | 
				
			||||||
 | 
							return map.find((scene) => scene.identifier === identifier) || null;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										20
									
								
								User.ts
									
									
									
									
									
								
							
							
						
						
									
										20
									
								
								User.ts
									
									
									
									
									
								
							@ -1,15 +1,14 @@
 | 
				
			|||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * User.ts export the User class that provides methods for communicating with the player
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const DEFAULT_PROMPT = ">";
 | 
					const DEFAULT_PROMPT = ">";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default class User {
 | 
					export default class User {
 | 
				
			||||||
	#prompt: string;
 | 
						#prompt: string;
 | 
				
			||||||
	#out: (text: string) => Promise<void>;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	constructor() {
 | 
						constructor(prompt: string = DEFAULT_PROMPT) {
 | 
				
			||||||
		this.#prompt = DEFAULT_PROMPT;
 | 
							this.#prompt = prompt;
 | 
				
			||||||
		this.#out = async (text: string) => {
 | 
					 | 
				
			||||||
			const data = new TextEncoder().encode(text);
 | 
					 | 
				
			||||||
			await Deno.stdout.write(data);
 | 
					 | 
				
			||||||
		};
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	ask(question: string): string {
 | 
						ask(question: string): string {
 | 
				
			||||||
@ -23,6 +22,11 @@ export default class User {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	async tell(statement: string): Promise<void> {
 | 
						async tell(statement: string): Promise<void> {
 | 
				
			||||||
		await this.#out(`\n${statement}\n`);
 | 
							await this.out(`\n${statement}\n`);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						private async out(text: string): Promise<void> {
 | 
				
			||||||
 | 
							const data = new TextEncoder().encode(text);
 | 
				
			||||||
 | 
							await Deno.stdout.write(data);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										10
									
								
								data/data.ts
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								data/data.ts
									
									
									
									
									
								
							@ -1,10 +0,0 @@
 | 
				
			|||||||
export * from "./rooms.ts";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export interface Item {
 | 
					 | 
				
			||||||
	name: string;
 | 
					 | 
				
			||||||
	description: string;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export interface GameData {
 | 
					 | 
				
			||||||
	items: Item[];
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
							
								
								
									
										128
									
								
								data/rooms.ts
									
									
									
									
									
								
							
							
						
						
									
										128
									
								
								data/rooms.ts
									
									
									
									
									
								
							@ -1,3 +1,127 @@
 | 
				
			|||||||
export const hall = {
 | 
					/**
 | 
				
			||||||
	items: [{ name: "flashlight", description: "There is a flashlight." }],
 | 
					 * rooms.ts represents the game data that drives the story. It should contain all of the
 | 
				
			||||||
 | 
					 * elements that are needed to tell the story. Although it is currently written in
 | 
				
			||||||
 | 
					 * TypeScript, it should only contain objects which could just as easily be JSON. The
 | 
				
			||||||
 | 
					 * reason to make this file TypeScript for now is that it lets the TypeScript compiler
 | 
				
			||||||
 | 
					 * tell us if we have made errors. Later, we should add a file parser that can figure out
 | 
				
			||||||
 | 
					 * if there are errors in a JSON file and load it if there are not.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { StoryScene, SceneProperties } from "../types.ts";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const game: { map: StoryScene<SceneProperties>[] } = {
 | 
				
			||||||
 | 
						map: [
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								identifier: "hall",
 | 
				
			||||||
 | 
								// properties represent the base, default state for this room.
 | 
				
			||||||
 | 
								properties: {
 | 
				
			||||||
 | 
									// The room is called "Hall"
 | 
				
			||||||
 | 
									name: "Hall",
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									// This is what the user will see if the look while in the room.
 | 
				
			||||||
 | 
									description: "It's very dark",
 | 
				
			||||||
 | 
									// This array lists all the items that are in the room.
 | 
				
			||||||
 | 
									items: [
 | 
				
			||||||
 | 
										// flashlight is an item that can be used. When it is, it applies an effect
 | 
				
			||||||
 | 
										// to the room that it's being used in.
 | 
				
			||||||
 | 
										{
 | 
				
			||||||
 | 
											name: "flashlight",
 | 
				
			||||||
 | 
											actions: {
 | 
				
			||||||
 | 
												// Use is the name of the ActionTerm that will be used here. If a
 | 
				
			||||||
 | 
												// player types "use flashlight" then this describes what should
 | 
				
			||||||
 | 
												// happen.
 | 
				
			||||||
 | 
												use: {
 | 
				
			||||||
 | 
													// These are the pieces of information that the engine needs to
 | 
				
			||||||
 | 
													// exectue a use action of type "applyEffect."
 | 
				
			||||||
 | 
													args: {
 | 
				
			||||||
 | 
														// This is the name of the effect. It is defined in this file
 | 
				
			||||||
 | 
														// and should correspond to some condition on the player or
 | 
				
			||||||
 | 
														// scene.
 | 
				
			||||||
 | 
														effect: "flashlight-on",
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
														// can be "player" or "scene"
 | 
				
			||||||
 | 
														applyTo: "player",
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
														// When it's used, this is what should be reported back to
 | 
				
			||||||
 | 
														// the user.
 | 
				
			||||||
 | 
														result: "The flashlight is on.",
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
														// If the effect is reversed—unapplied—this is what should be
 | 
				
			||||||
 | 
														// reported back to the user.
 | 
				
			||||||
 | 
														reverseResult: "The flashlight is off.",
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
														// This indicates that the effect can be unapplied after it
 | 
				
			||||||
 | 
														// is applied. This should be done by calling use again after
 | 
				
			||||||
 | 
														// it has already been called and the effect has been applied.
 | 
				
			||||||
 | 
														reversible: true,
 | 
				
			||||||
 | 
													},
 | 
				
			||||||
 | 
													// applyEffect means that when this item is used, it will apply
 | 
				
			||||||
 | 
													// an effect to either the player or the scene.
 | 
				
			||||||
 | 
													type: "applyEffect",
 | 
				
			||||||
 | 
												},
 | 
				
			||||||
 | 
											},
 | 
				
			||||||
 | 
										},
 | 
				
			||||||
 | 
									],
 | 
				
			||||||
 | 
									// exit to the north leads to the Hall Closet
 | 
				
			||||||
 | 
									exits: [
 | 
				
			||||||
 | 
										{
 | 
				
			||||||
 | 
											description: "a closet to the north",
 | 
				
			||||||
 | 
											direction: "north",
 | 
				
			||||||
 | 
											scene: "hall closet",
 | 
				
			||||||
 | 
										},
 | 
				
			||||||
 | 
										{
 | 
				
			||||||
 | 
											description: "a dining room to the south",
 | 
				
			||||||
 | 
											direction: "south",
 | 
				
			||||||
 | 
											scene: "dining room",
 | 
				
			||||||
 | 
										},
 | 
				
			||||||
 | 
									],
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								// conditions are a set of conditions that, when met, will make alterations to the
 | 
				
			||||||
 | 
								// player or the scene.
 | 
				
			||||||
 | 
								conditions: {
 | 
				
			||||||
 | 
									// effects are conditions that revolve around effects that have been applied to
 | 
				
			||||||
 | 
									// either the player or the scene.
 | 
				
			||||||
 | 
									effects: [
 | 
				
			||||||
 | 
										{
 | 
				
			||||||
 | 
											// This is the name of the effect that, if present, will make alterations
 | 
				
			||||||
 | 
											// to the scene.
 | 
				
			||||||
 | 
											name: "flashlight-on",
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
											// properties describes the properties on the scene that will be altered
 | 
				
			||||||
 | 
											// by the presents of this effect.
 | 
				
			||||||
 | 
											properties: {
 | 
				
			||||||
 | 
												// description means that, when this effect is applied, i.e. when
 | 
				
			||||||
 | 
												// the flashlight is on, the description will change to the one
 | 
				
			||||||
 | 
												// written here.
 | 
				
			||||||
 | 
												description:
 | 
				
			||||||
 | 
													"You are standing in a big hall. There's lots of nooks, " +
 | 
				
			||||||
 | 
													"crannies, and room for general testing. Aw yeah... sweet testing!",
 | 
				
			||||||
 | 
											},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
											// source indicates where the effect should be applied. In this case, the
 | 
				
			||||||
 | 
											// effect should be applied to the player.
 | 
				
			||||||
 | 
											source: "player",
 | 
				
			||||||
 | 
										},
 | 
				
			||||||
 | 
									],
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								identifier: "hall closet",
 | 
				
			||||||
 | 
								properties: {
 | 
				
			||||||
 | 
									name: "Hall Closet",
 | 
				
			||||||
 | 
									description:
 | 
				
			||||||
 | 
										"It's a closet. You feel a little silly just standing in it.",
 | 
				
			||||||
 | 
									items: [
 | 
				
			||||||
 | 
										{
 | 
				
			||||||
 | 
											name: "raincoat",
 | 
				
			||||||
 | 
										},
 | 
				
			||||||
 | 
									],
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								conditions: {
 | 
				
			||||||
 | 
									effects: [],
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						],
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default game;
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										76
									
								
								main.ts
									
									
									
									
									
								
							
							
						
						
									
										76
									
								
								main.ts
									
									
									
									
									
								
							@ -1,66 +1,64 @@
 | 
				
			|||||||
import User from "./User.ts";
 | 
					import Interpreter from "./Interpreter.ts";
 | 
				
			||||||
import Scene from "./Scene.ts";
 | 
					 | 
				
			||||||
import Player from "./Player.ts";
 | 
					import Player from "./Player.ts";
 | 
				
			||||||
import { hall } from "./data/data.ts";
 | 
					import Scene from "./Scene.ts";
 | 
				
			||||||
import parseCommand from "./parseCommand.ts";
 | 
					import User from "./User.ts";
 | 
				
			||||||
 | 
					import game from "./data/rooms.ts";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function main() {
 | 
					async function main() {
 | 
				
			||||||
	const user = new User();
 | 
						const user = new User(); // for communication with the user
 | 
				
			||||||
	const scene = new Scene(hall);
 | 
						const scene = new Scene("hall", game.map); // the room that player is in.
 | 
				
			||||||
	const player = new Player();
 | 
						const player = new Player(); // the players current state
 | 
				
			||||||
	const question = "";
 | 
						let running = true; // running flag for the game loop. Stops on false.
 | 
				
			||||||
	let running = true;
 | 
						let statement = ""; // holds a statement for the user.
 | 
				
			||||||
	let statement = "";
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	while (running) {
 | 
						while (running) {
 | 
				
			||||||
		const prompts = `${statement}${question}`;
 | 
							const prompts = `${statement}\n`;
 | 
				
			||||||
		const answer = await user.ask(prompts);
 | 
							const answer = await user.ask(prompts);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		const { action, target } = parseCommand(answer);
 | 
							// User commands that are not player moves, but are about running the
 | 
				
			||||||
 | 
							// game itself (e.g. loading, saving, quitting) may need to get handled
 | 
				
			||||||
		statement = "";
 | 
							// here.
 | 
				
			||||||
 | 
							switch (answer) {
 | 
				
			||||||
		switch (action) {
 | 
								// kills the game loop.
 | 
				
			||||||
			case "quit":
 | 
								case "quit":
 | 
				
			||||||
				running = quit(user);
 | 
									running = quit(user);
 | 
				
			||||||
				break;
 | 
									statement = "";
 | 
				
			||||||
 | 
					 | 
				
			||||||
			case "look":
 | 
					 | 
				
			||||||
				await scene.look();
 | 
					 | 
				
			||||||
				break;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			case "take":
 | 
					 | 
				
			||||||
				await pickUpItem(scene, player, target);
 | 
					 | 
				
			||||||
				break;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			case "inventory":
 | 
					 | 
				
			||||||
				await player.inventory();
 | 
					 | 
				
			||||||
				break;
 | 
									break;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			default:
 | 
								default:
 | 
				
			||||||
				statement = "I didn't understand that.\n";
 | 
									// Game moves require more complicated commands, this switch is
 | 
				
			||||||
 | 
									// not adequate to handle them. Here we will pass them off to
 | 
				
			||||||
 | 
									// other functions to parse.
 | 
				
			||||||
 | 
									statement = interpretUserCommand(player, scene, answer);
 | 
				
			||||||
				break;
 | 
									break;
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Takes player and scene state and a command. Tries to figure out what the
 | 
				
			||||||
 | 
					// user is trying to do and, if it can, it executes the command.
 | 
				
			||||||
 | 
					function interpretUserCommand(
 | 
				
			||||||
 | 
						player: Player,
 | 
				
			||||||
 | 
						scene: Scene,
 | 
				
			||||||
 | 
						command: string
 | 
				
			||||||
 | 
					): string {
 | 
				
			||||||
 | 
						const interpreter = new Interpreter(player, scene, command);
 | 
				
			||||||
 | 
						interpreter.interpret();
 | 
				
			||||||
 | 
						return interpreter.execute();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// When it returns false, the loop should be stopped. When it returns true,
 | 
				
			||||||
 | 
					// the loop should continue.
 | 
				
			||||||
function quit(user: User): boolean {
 | 
					function quit(user: User): boolean {
 | 
				
			||||||
	const confirmQuit = user.ask("Are you sure you want to quit?\n");
 | 
						const confirmQuit = user.ask("Are you sure you want to quit?\n");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if (confirmQuit[0] === "y") {
 | 
						// Anything that starts with a "y" or a "Y" is considered an affirmative
 | 
				
			||||||
 | 
						// response.
 | 
				
			||||||
 | 
						if (confirmQuit[0].toLocaleLowerCase() === "y") {
 | 
				
			||||||
		return false;
 | 
							return false;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return true;
 | 
						return true;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function pickUpItem(scene: Scene, player: Player, target: string) {
 | 
					 | 
				
			||||||
	const item = await scene.take(target);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if (item !== null) {
 | 
					 | 
				
			||||||
		player.drop(item);
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
main();
 | 
					main();
 | 
				
			||||||
 | 
				
			|||||||
@ -1,5 +0,0 @@
 | 
				
			|||||||
export default function parseCommand(command: string) {
 | 
					 | 
				
			||||||
	const [action, target] = command.toLocaleLowerCase().split(" ");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	return { action, target };
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
							
								
								
									
										43
									
								
								terms/Term.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								terms/Term.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,43 @@
 | 
				
			|||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Term.ts contains the type information for parsing user sentences.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import Player from "../Player.ts";
 | 
				
			||||||
 | 
					import Scene from "../Scene.ts";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ActionFn is the signature for functions that contain the logic associated with verbs
 | 
				
			||||||
 | 
					// the player uses.
 | 
				
			||||||
 | 
					export type ActionFn = (
 | 
				
			||||||
 | 
						player: Player,
 | 
				
			||||||
 | 
						scene: Scene,
 | 
				
			||||||
 | 
						target?: string, // the target is the target of the action.
 | 
				
			||||||
 | 
						object?: string // the object is an item that is used with the action on the target.
 | 
				
			||||||
 | 
					) => string; // should return a description of the result for the player.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Constant is a specific word that the engine knows about. If the player uses a word
 | 
				
			||||||
 | 
					// that is not a Constant, then it should be a variable that represents something that
 | 
				
			||||||
 | 
					// is defined in the writer's story files.
 | 
				
			||||||
 | 
					export interface Constant {
 | 
				
			||||||
 | 
						constant: string;
 | 
				
			||||||
 | 
						category: Category;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Category is a union of the different kinds of Constants.
 | 
				
			||||||
 | 
					export type Category = "action" | "direction" | "position" | "interaction";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Term describes a known word that the player can use. It tells the parse what kinds of
 | 
				
			||||||
 | 
					// words can come after it and what it means.
 | 
				
			||||||
 | 
					export interface Term {
 | 
				
			||||||
 | 
						precedesCategories: Category[]; // categories of Constant that can follow this Term
 | 
				
			||||||
 | 
						precedesConstants: Constant[]; // Constants that can follow this Term
 | 
				
			||||||
 | 
						category: Category; // the category of this Terms Constant is part of
 | 
				
			||||||
 | 
						constant: string; // the name of the Constant that this Term describes
 | 
				
			||||||
 | 
						canPrecedeVariable: boolean; // indicates that what follows might be a variable
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ActionTerm extends Term, adding the function needed to execute a verb. It also
 | 
				
			||||||
 | 
					// guarentees that the category is "action".
 | 
				
			||||||
 | 
					export interface ActionTerm extends Term {
 | 
				
			||||||
 | 
						action: ActionFn;
 | 
				
			||||||
 | 
						category: "action";
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										157
									
								
								terms/actions.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										157
									
								
								terms/actions.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,157 @@
 | 
				
			|||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * actions.ts contains the logic for various ActionTerms. Exported should match the
 | 
				
			||||||
 | 
					 * signature for "ActionFn," which is:
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * (player: Player, scene: Scene, target?: string, object?: string) => string
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * The returned string should be a message for the user that describes the result of the
 | 
				
			||||||
 | 
					 * action.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import type Scene from "../Scene.ts";
 | 
				
			||||||
 | 
					import type Player from "../Player.ts";
 | 
				
			||||||
 | 
					import type { ApplyEffectArgs, ItemAction } from "../types.ts";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const ITEM_MISSING = "I can't find that.";
 | 
				
			||||||
 | 
					const ITEM_UNUSABLE = "I don't know how to use that.";
 | 
				
			||||||
 | 
					const ITEM_ALREADY_USED = "I already did that.";
 | 
				
			||||||
 | 
					const DIRECTION_INACCESSIBLE = "I can't go that way.";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// look is what happens when a player uses the "look" verb. It gets a description of the
 | 
				
			||||||
 | 
					// scene.
 | 
				
			||||||
 | 
					export function look(player: Player, scene: Scene): string {
 | 
				
			||||||
 | 
						return scene.look(player.activeEffects, scene.activeEffects);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// pickUpItem is what happens when a player uses the "take" verb. It finds an item in the
 | 
				
			||||||
 | 
					// scene and, if it's there, moves it into the players inventory.
 | 
				
			||||||
 | 
					export function pickUpItem(
 | 
				
			||||||
 | 
						player: Player,
 | 
				
			||||||
 | 
						scene: Scene,
 | 
				
			||||||
 | 
						target?: string
 | 
				
			||||||
 | 
					): string {
 | 
				
			||||||
 | 
						if (!target) {
 | 
				
			||||||
 | 
							return "What do you want me to get?";
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// remove the item from the scene.
 | 
				
			||||||
 | 
						const item = scene.get(target);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// The item was found in the scene...
 | 
				
			||||||
 | 
						if (item !== null) {
 | 
				
			||||||
 | 
							player.put(item); // put it in the players inventory.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return "Taken.";
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return ITEM_MISSING;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// checkInventory is what happens when a player uses the "inventory" verb. It gets the
 | 
				
			||||||
 | 
					// description for what's in the player's inventory.
 | 
				
			||||||
 | 
					export function checkInventory(player: Player): string {
 | 
				
			||||||
 | 
						return player.look();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// move changes the scene around the player
 | 
				
			||||||
 | 
					export function move(_player: Player, scene: Scene, target?: string) {
 | 
				
			||||||
 | 
						if (!target) {
 | 
				
			||||||
 | 
							return "Where do you want me to go?";
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const { exits } = scene.properties;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (!exits) {
 | 
				
			||||||
 | 
							return DIRECTION_INACCESSIBLE;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const exit = exits.find(
 | 
				
			||||||
 | 
							({ direction }) => direction === target.toLocaleLowerCase()
 | 
				
			||||||
 | 
						);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (!exit) {
 | 
				
			||||||
 | 
							return DIRECTION_INACCESSIBLE;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const { scene: identifier } = exit;
 | 
				
			||||||
 | 
						scene.changeScene(identifier);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const { description, name } = scene.properties;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return `${name}\n${description}`;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// use is what happens when a player uses the "use" verb. Different items do different
 | 
				
			||||||
 | 
					// things when they are used, so the function needs to figure out what kind of usage an
 | 
				
			||||||
 | 
					// item is written for, and then execute that logic with whatever arguments the
 | 
				
			||||||
 | 
					// storywriter has provided for that item.
 | 
				
			||||||
 | 
					export function use(player: Player, scene: Scene, target?: string): string {
 | 
				
			||||||
 | 
						if (!target) {
 | 
				
			||||||
 | 
							return "What do you want to use?";
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const { inventory } = player;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// if there is no inventory...
 | 
				
			||||||
 | 
						if (inventory === null) {
 | 
				
			||||||
 | 
							return ITEM_MISSING;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// search the inventory for the named item
 | 
				
			||||||
 | 
						const item = inventory.find((i) => i.name === target);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (item === undefined) {
 | 
				
			||||||
 | 
							return ITEM_MISSING;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const itemAction = item?.actions?.["use"];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// The item doesn't have action defined for "use", so it cannot be used.
 | 
				
			||||||
 | 
						if (itemAction === undefined) {
 | 
				
			||||||
 | 
							return ITEM_UNUSABLE;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return executeItemAction(player, scene, itemAction);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// executeItemAction figures out what kind of action an item supports and then executes
 | 
				
			||||||
 | 
					// the logic for it.
 | 
				
			||||||
 | 
					function executeItemAction(
 | 
				
			||||||
 | 
						player: Player,
 | 
				
			||||||
 | 
						scene: Scene,
 | 
				
			||||||
 | 
						{ args, type }: ItemAction
 | 
				
			||||||
 | 
					): string {
 | 
				
			||||||
 | 
						switch (type) {
 | 
				
			||||||
 | 
							case "applyEffect":
 | 
				
			||||||
 | 
								return applyEffect(player, scene, args as ApplyEffectArgs);
 | 
				
			||||||
 | 
							default:
 | 
				
			||||||
 | 
								return ITEM_UNUSABLE;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// applyEffect contains the logic for applying an effect to the player or the scene.
 | 
				
			||||||
 | 
					function applyEffect(
 | 
				
			||||||
 | 
						player: Player,
 | 
				
			||||||
 | 
						scene: Scene,
 | 
				
			||||||
 | 
						{ applyTo, effect, reverseResult, reversible, result }: ApplyEffectArgs
 | 
				
			||||||
 | 
					): string {
 | 
				
			||||||
 | 
						const target = applyTo === "player" ? player : scene;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// if true, effect is already present.
 | 
				
			||||||
 | 
						const isApplied = target.hasActiveEffect(effect);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (!reversible && isApplied) {
 | 
				
			||||||
 | 
							// effect is present already and can't be reversed
 | 
				
			||||||
 | 
							return ITEM_ALREADY_USED;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (isApplied) {
 | 
				
			||||||
 | 
							// effect is present...
 | 
				
			||||||
 | 
							target.removeEffect(effect);
 | 
				
			||||||
 | 
							return reverseResult || result;
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							// effect is not present...
 | 
				
			||||||
 | 
							target.addEffect(effect);
 | 
				
			||||||
 | 
							return result;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										60
									
								
								terms/terms.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								terms/terms.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,60 @@
 | 
				
			|||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * terms.ts contains the Terms that describe the Constants that the player can use.
 | 
				
			||||||
 | 
					 * See Terms.ts for more information about what these terms can be.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import * as actions from "./actions.ts";
 | 
				
			||||||
 | 
					import { ActionTerm } from "./Term.ts";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const inventory: ActionTerm = {
 | 
				
			||||||
 | 
						action: actions.checkInventory,
 | 
				
			||||||
 | 
						canPrecedeVariable: false,
 | 
				
			||||||
 | 
						category: "action",
 | 
				
			||||||
 | 
						constant: "inventory",
 | 
				
			||||||
 | 
						precedesCategories: [],
 | 
				
			||||||
 | 
						precedesConstants: [],
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const look: ActionTerm = {
 | 
				
			||||||
 | 
						action: actions.look,
 | 
				
			||||||
 | 
						canPrecedeVariable: false,
 | 
				
			||||||
 | 
						category: "action",
 | 
				
			||||||
 | 
						constant: "look",
 | 
				
			||||||
 | 
						precedesCategories: [],
 | 
				
			||||||
 | 
						precedesConstants: [],
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const move: ActionTerm = {
 | 
				
			||||||
 | 
						action: actions.move,
 | 
				
			||||||
 | 
						canPrecedeVariable: true,
 | 
				
			||||||
 | 
						category: "action",
 | 
				
			||||||
 | 
						constant: "go",
 | 
				
			||||||
 | 
						precedesCategories: [],
 | 
				
			||||||
 | 
						precedesConstants: [],
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const take: ActionTerm = {
 | 
				
			||||||
 | 
						action: actions.pickUpItem,
 | 
				
			||||||
 | 
						canPrecedeVariable: true,
 | 
				
			||||||
 | 
						category: "action",
 | 
				
			||||||
 | 
						constant: "take",
 | 
				
			||||||
 | 
						precedesCategories: [],
 | 
				
			||||||
 | 
						precedesConstants: [],
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const use: ActionTerm = {
 | 
				
			||||||
 | 
						action: actions.use,
 | 
				
			||||||
 | 
						canPrecedeVariable: true,
 | 
				
			||||||
 | 
						category: "action",
 | 
				
			||||||
 | 
						constant: "use",
 | 
				
			||||||
 | 
						precedesCategories: [],
 | 
				
			||||||
 | 
						precedesConstants: [],
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const terms = {
 | 
				
			||||||
 | 
						[inventory.constant]: inventory,
 | 
				
			||||||
 | 
						[look.constant]: look,
 | 
				
			||||||
 | 
						[move.constant]: move,
 | 
				
			||||||
 | 
						[take.constant]: take,
 | 
				
			||||||
 | 
						[use.constant]: use,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										90
									
								
								types.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										90
									
								
								types.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,90 @@
 | 
				
			|||||||
 | 
					// ActionArgs is a union of all the different argument types that can be used with an
 | 
				
			||||||
 | 
					// action
 | 
				
			||||||
 | 
					export type ActionArgs = ApplyEffectArgs;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ApplyEffectArgs are the arguments required to apply effects to a player or scene
 | 
				
			||||||
 | 
					export interface ApplyEffectArgs extends Args {
 | 
				
			||||||
 | 
						effect: string;
 | 
				
			||||||
 | 
						applyTo: "player" | "scene";
 | 
				
			||||||
 | 
						reversible: boolean;
 | 
				
			||||||
 | 
						reverseResult?: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ApplyEffectItemAction describes the apply effect action
 | 
				
			||||||
 | 
					export interface ApplyEffectItemAction extends ItemAction {
 | 
				
			||||||
 | 
						type: "applyEffect";
 | 
				
			||||||
 | 
						args: ApplyEffectArgs;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Args is a base interface for the various argument interfaces
 | 
				
			||||||
 | 
					export interface Args {
 | 
				
			||||||
 | 
						result: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Conditions contains story elements that can be conditionally applied to the scene or
 | 
				
			||||||
 | 
					// player
 | 
				
			||||||
 | 
					export interface Conditions<T extends EntityProperties> {
 | 
				
			||||||
 | 
						effects: Effect<T>[];
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Direction is union that contains the different directions a player can go
 | 
				
			||||||
 | 
					export type Direction =
 | 
				
			||||||
 | 
						| "north"
 | 
				
			||||||
 | 
						| "northeast"
 | 
				
			||||||
 | 
						| "east"
 | 
				
			||||||
 | 
						| "southest"
 | 
				
			||||||
 | 
						| "south"
 | 
				
			||||||
 | 
						| "southwest"
 | 
				
			||||||
 | 
						| "west"
 | 
				
			||||||
 | 
						| "northwest"
 | 
				
			||||||
 | 
						| "up"
 | 
				
			||||||
 | 
						| "down";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Effect represents some effect that may be applied to a scene or player
 | 
				
			||||||
 | 
					export interface Effect<T extends EntityProperties> {
 | 
				
			||||||
 | 
						name: string;
 | 
				
			||||||
 | 
						properties: T;
 | 
				
			||||||
 | 
						source: "player" | "scene"; // where the effect is applied
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Exit describes the exits that the user can take to go to another scene
 | 
				
			||||||
 | 
					export interface Exit {
 | 
				
			||||||
 | 
						description: string;
 | 
				
			||||||
 | 
						direction: Direction;
 | 
				
			||||||
 | 
						scene: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Item represents some item either in the scene or in the player's inventory
 | 
				
			||||||
 | 
					export interface Item {
 | 
				
			||||||
 | 
						name: string;
 | 
				
			||||||
 | 
						actions?: { [name: string]: ItemAction };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ItemActionType is a union of all the types of action that can be used
 | 
				
			||||||
 | 
					export type ItemActionType = "applyEffect";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ItemAction represents an action that can be taken by an item
 | 
				
			||||||
 | 
					export interface ItemAction {
 | 
				
			||||||
 | 
						type: ItemActionType;
 | 
				
			||||||
 | 
						args: ActionArgs;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// SceneProperties are the properties (in addition to the EntityProperties) that are
 | 
				
			||||||
 | 
					// needed by the scene
 | 
				
			||||||
 | 
					export interface SceneProperties extends EntityProperties {
 | 
				
			||||||
 | 
						description?: string;
 | 
				
			||||||
 | 
						exits?: Exit[];
 | 
				
			||||||
 | 
						name?: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// StoryScene holds both the conditions and properties for a scene.
 | 
				
			||||||
 | 
					export interface StoryScene<T extends EntityProperties> {
 | 
				
			||||||
 | 
						identifier: string;
 | 
				
			||||||
 | 
						conditions: Conditions<T>;
 | 
				
			||||||
 | 
						properties: SceneProperties;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// EntityProperties is a base interface for the properties that a user or scene might have
 | 
				
			||||||
 | 
					export interface EntityProperties {
 | 
				
			||||||
 | 
						items?: Item[];
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
		Reference in New Issue
	
	Block a user