diff --git a/Game.ts b/Game.ts deleted file mode 100644 index aeef051..0000000 --- a/Game.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type Player from "./Player.ts"; -import type Scene from "./Scene.ts"; - -export default class Game { - #player: Player; - #scene: Scene; - - constructor(player: Player, scene: Scene) { - this.#player = player; - this.#scene = scene; - } - - lookAtScene(): string { - const { effects } = this.#player; - - return this.#scene.look(effects); - } -} diff --git a/Interpreter.ts b/Interpreter.ts index e5b843e..f951391 100644 --- a/Interpreter.ts +++ b/Interpreter.ts @@ -1,3 +1,10 @@ +/** + * 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"; diff --git a/Player.ts b/Player.ts index bf56031..2c446e0 100644 --- a/Player.ts +++ b/Player.ts @@ -1,15 +1,24 @@ +/** + * Player.ts contains the Player class. It represents the player's state as the game + * goes on. + */ + import { Effect, Item, VesselProperties } from "./types.ts"; import Vessel from "./Vessel.ts"; +// The Player is a type of "Vessel" which is a generic object that can have effects +// and items. export default class Player extends Vessel { - #effects: Effect[]; - - constructor(items: Item[] = [], effects: Effect[] = []) { + constructor(items: Item[] = [], _effects: Effect[] = []) { super({ items }, { effects: [] }); - - this.#effects = effects; } + // inventory is a getter that returns the items in the user's inventory + get inventory(): Item[] | null { + return this._properties.items || null; + } + + // look returns a string that describes the players inventory look(): string { const description = super.description(this._properties.items || []); @@ -20,12 +29,9 @@ export default class Player extends Vessel { } } + // put takes an item and puts it into the players inventory put(item: Item) { if (!this._properties.items) this._properties.items = []; this._properties.items.push(item); } - - get inventory(): Item[] | null { - return this._properties.items || null; - } } diff --git a/Scene.ts b/Scene.ts index 6582377..3a6ab33 100644 --- a/Scene.ts +++ b/Scene.ts @@ -1,26 +1,34 @@ -import { GameData, Item, SceneProperties } from "./types.ts"; +/** + * Scene.ts contains the Scene class. It represents the state of the current scene. + */ + +import { StoryScene, Item, SceneProperties } from "./types.ts"; import Vessel from "./Vessel.ts"; +// The Scene is a type of "Vessel" which is a generic object that can have effects +// and items. export default class Scene extends Vessel { - constructor(gameData: GameData) { + constructor(gameData: StoryScene) { // TODO: [] is a placeholder for scene effects. super(gameData.properties, gameData.conditions); } + // 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 ); - const itemsDescription = super.description(properties.items || []); - const { description } = properties; + // description of the items in the scene + const itemsDescription = super.description(properties.items || []); + const { description } = properties; // description of the room + + // will be a string that includes both the room and item descriptions let fullDescription = description || "Nothing to see here..."; - if (description) { - fullDescription = description; - } - + // if there is a description of the items... if (itemsDescription) { fullDescription += `\n\nThere is ${itemsDescription}`; } @@ -28,10 +36,12 @@ export default class Scene extends Vessel { return fullDescription; } + // get removes an items from the scene and returns it 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) { const item = items[idx]; items.splice(idx, 1); @@ -39,6 +49,7 @@ export default class Scene extends Vessel { return item; } + // if an item wasn't found and returned, return null return null; } } diff --git a/State.ts b/State.ts deleted file mode 100644 index 1f8b757..0000000 --- a/State.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { GameData, SceneProperties } from "./types.ts"; -import Player from "./Player.ts"; -import Scene from "./Scene.ts"; - -export class State { - #player: Player; - #scene: Scene; - - constructor(gameData: GameData) { - this.#player = new Player(); - this.#scene = new Scene(gameData); - } -} diff --git a/User.ts b/User.ts index cf2930a..2c0f422 100644 --- a/User.ts +++ b/User.ts @@ -1,15 +1,14 @@ +/** + * User.ts export the User class that provides methods for communicating with the player + */ + const DEFAULT_PROMPT = ">"; export default class User { #prompt: string; - #out: (text: string) => Promise; - constructor() { - this.#prompt = DEFAULT_PROMPT; - this.#out = async (text: string) => { - const data = new TextEncoder().encode(text); - await Deno.stdout.write(data); - }; + constructor(prompt: string) { + this.#prompt = prompt || DEFAULT_PROMPT; } ask(question: string): string { @@ -23,6 +22,11 @@ export default class User { } async tell(statement: string): Promise { - await this.#out(`\n${statement}\n`); + await this.out(`\n${statement}\n`); + } + + private async out(text: string): Promise { + const data = new TextEncoder().encode(text); + await Deno.stdout.write(data); } } diff --git a/Vessel.ts b/Vessel.ts index 866d3fb..7aa0572 100644 --- a/Vessel.ts +++ b/Vessel.ts @@ -1,3 +1,8 @@ +/** + * Vessel.ts contains the Vessel class which is a base class that handles effects and + * containing items. It is the base class for Player and Scene. + */ + import type { Conditions, Effect, Item, VesselProperties } from "./types.ts"; export default class Vessel { @@ -10,8 +15,8 @@ export default class Vessel { conditions: Conditions, activeEffects: string[] = [] ) { - this._conditions = conditions; - this._properties = properties; + this._conditions = conditions; // Conditional properties + this._properties = properties; // Base properties this._activeEffects = activeEffects; } @@ -25,6 +30,7 @@ export default class Vessel { // 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[] @@ -40,6 +46,7 @@ export default class Vessel { const effects = activeEffects.map((e) => map[e]); const appliedProperties = { ...this._properties }; + // for each effect, apply changed properties to the for (const effect of effects) { Object.assign(appliedProperties, effect.properties); } @@ -47,6 +54,7 @@ export default class Vessel { return appliedProperties; } + // Generate a description of the contained items description(items: Item[]): string | null { if (items.length === 0) return null; @@ -67,10 +75,12 @@ export default class Vessel { return description; } + // Checks if a given effect exists as an active effect on this vessel hasActiveEffect(effect: string): boolean { return this._activeEffects.includes(effect); } + // Removes an effect if it is active on this vessel removeEffect(effect: string): void { const idx = this._activeEffects.findIndex((e) => e === effect); diff --git a/data/rooms.ts b/data/rooms.ts index c42a292..7c435b6 100644 --- a/data/rooms.ts +++ b/data/rooms.ts @@ -1,35 +1,87 @@ -import { GameData, SceneProperties } from "../types.ts"; +/** + * 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. + */ -export const hall: GameData = { +import { StoryScene, SceneProperties } from "../types.ts"; + +// hall represents a room +export const hall: StoryScene = { + // properties represent the base, default state for this room. properties: { + // 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: { - effect: "lit-by-flashlight", + // 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: "scene", + + // 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", }, }, }, ], }, + // 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: [ { - name: "lit-by-flashlight", + // 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", }, ], diff --git a/terms/Term.ts b/terms/Term.ts index 842fade..16a293d 100644 --- a/terms/Term.ts +++ b/terms/Term.ts @@ -1,33 +1,42 @@ +/** + * 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, - object?: string -) => string; + 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; } -export type Category = - | "action" - | "direction" - | "compound" - | "position" - | "interaction"; +// 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[]; - precedesConstants: Constant[]; - category: Category; - constant: string; - canPrecedeVariable: boolean; + 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"; diff --git a/terms/actions.ts b/terms/actions.ts index a12a669..c168cd7 100644 --- a/terms/actions.ts +++ b/terms/actions.ts @@ -1,15 +1,29 @@ +/** + * 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 = "You don't have that."; +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."; +// 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, @@ -19,19 +33,29 @@ export function pickUpItem( 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); + player.put(item); // put it in the players inventory. + + return "Taken."; } - 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(); } +// 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?"; @@ -39,10 +63,12 @@ export function use(player: Player, scene: Scene, target?: string): string { 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) { @@ -51,6 +77,7 @@ export function use(player: Player, scene: Scene, target?: string): string { 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; } @@ -58,6 +85,8 @@ export function use(player: Player, scene: Scene, target?: string): string { 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, @@ -71,22 +100,28 @@ function executeItemAction( } } +// 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; } diff --git a/terms/terms.ts b/terms/terms.ts index eab388e..0fa068e 100644 --- a/terms/terms.ts +++ b/terms/terms.ts @@ -1,3 +1,8 @@ +/** + * 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"; diff --git a/types.ts b/types.ts index b860f67..ac8dabe 100644 --- a/types.ts +++ b/types.ts @@ -1,18 +1,28 @@ +/** + * types.ts describes basic types used by the engine. + */ + export * from "./data/rooms.ts"; +// ActionArgs is a union of all the different argument types that can be used with an +// action export type ActionArgs = ApplyEffectArgs; +// ItemActionType is a union of all the types of action that can be used export type ItemActionType = "applyEffect"; +// Args is a base interface for the various argument interfaces export interface Args { result: string; } +// ItemAction represents an action that can be taken by an item export interface ItemAction { type: ItemActionType; args: ActionArgs; } +// ApplyEffectArgs are the arguments required to apply effects to a player or scene export interface ApplyEffectArgs extends Args { effect: string; applyTo: "player" | "scene"; @@ -20,34 +30,44 @@ export interface ApplyEffectArgs extends Args { reverseResult?: string; } +// ApplyEffectItemAction describes the apply effect action export interface ApplyEffectItemAction extends ItemAction { type: "applyEffect"; + args: ApplyEffectArgs; } +// Item represents some item either in the scene or in the player's inventory export interface Item { name: string; actions: { [name: string]: ItemAction }; } -export interface Effect { +// Effect represents some effect that may be applied to a scene or player +export interface Effect { name: string; properties: T; - source: "player" | "scene"; + source: "player" | "scene"; // where the effect is applied } +// VesselProperties is a base interface for the properties that a user or scene might have export interface VesselProperties { items?: Item[]; } +// SceneProperties are the properties (in addition to the VesselProperties) that are +// needed by the scene export interface SceneProperties extends VesselProperties { description?: string; } -export interface Conditions { +// Conditions contains story elements that can be conditionally applied to the scene or +// player +export interface Conditions { effects: Effect[]; } -export interface GameData { +// StoryScene holds both the conditions and properties for a scene. +export interface StoryScene { conditions: Conditions; properties: SceneProperties; }