Add comments throughout.

This commit is contained in:
2021-05-27 14:47:45 -07:00
parent cc306dbf87
commit e26747cc7c
12 changed files with 211 additions and 83 deletions

18
Game.ts
View File

@ -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);
}
}

View File

@ -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 Player from "./Player.ts";
import type Scene from "./Scene.ts"; import type Scene from "./Scene.ts";
import { terms } from "./terms/terms.ts"; import { terms } from "./terms/terms.ts";

View File

@ -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 { Effect, Item, VesselProperties } from "./types.ts";
import Vessel from "./Vessel.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<VesselProperties> { export default class Player extends Vessel<VesselProperties> {
#effects: Effect<VesselProperties>[]; constructor(items: Item[] = [], _effects: Effect<VesselProperties>[] = []) {
constructor(items: Item[] = [], effects: Effect<VesselProperties>[] = []) {
super({ items }, { effects: [] }); 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 { look(): string {
const description = super.description(this._properties.items || []); const description = super.description(this._properties.items || []);
@ -20,12 +29,9 @@ export default class Player extends Vessel<VesselProperties> {
} }
} }
// put takes an item and puts it into the players inventory
put(item: Item) { put(item: Item) {
if (!this._properties.items) this._properties.items = []; if (!this._properties.items) this._properties.items = [];
this._properties.items.push(item); this._properties.items.push(item);
} }
get inventory(): Item[] | null {
return this._properties.items || null;
}
} }

View File

@ -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"; 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<SceneProperties> { export default class Scene extends Vessel<SceneProperties> {
constructor(gameData: GameData<SceneProperties>) { constructor(gameData: StoryScene<SceneProperties>) {
// TODO: [] is a placeholder for scene effects. // TODO: [] is a placeholder for scene effects.
super(gameData.properties, gameData.conditions); super(gameData.properties, gameData.conditions);
} }
// look returns a string that describes the scene and the items in it
look(activePlayerEffects: string[], activeSceneEffects: string[]): string { look(activePlayerEffects: string[], activeSceneEffects: string[]): string {
// the properties of the scene after effects have been examined an applied
const properties = this.applyEffects( const properties = this.applyEffects(
activePlayerEffects, activePlayerEffects,
activeSceneEffects 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..."; let fullDescription = description || "Nothing to see here...";
if (description) { // if there is a description of the items...
fullDescription = description;
}
if (itemsDescription) { if (itemsDescription) {
fullDescription += `\n\nThere is ${itemsDescription}`; fullDescription += `\n\nThere is ${itemsDescription}`;
} }
@ -28,10 +36,12 @@ export default class Scene extends Vessel<SceneProperties> {
return fullDescription; return fullDescription;
} }
// get removes an items from the scene and returns it
get(target: string): Item | null { get(target: string): Item | null {
const { items = [] } = this._properties; const { items = [] } = this._properties;
const idx = items.findIndex(({ name }) => name === target); const idx = items.findIndex(({ name }) => name === target);
// if we found an index for the given item
if (idx >= 0) { if (idx >= 0) {
const item = items[idx]; const item = items[idx];
items.splice(idx, 1); items.splice(idx, 1);
@ -39,6 +49,7 @@ export default class Scene extends Vessel<SceneProperties> {
return item; return item;
} }
// if an item wasn't found and returned, return null
return null; return null;
} }
} }

View File

@ -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<SceneProperties>) {
this.#player = new Player();
this.#scene = new Scene(gameData);
}
}

20
User.ts
View File

@ -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) {
this.#prompt = DEFAULT_PROMPT; this.#prompt = prompt || DEFAULT_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);
} }
} }

View File

@ -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"; import type { Conditions, Effect, Item, VesselProperties } from "./types.ts";
export default class Vessel<T extends VesselProperties> { export default class Vessel<T extends VesselProperties> {
@ -10,8 +15,8 @@ export default class Vessel<T extends VesselProperties> {
conditions: Conditions<T>, conditions: Conditions<T>,
activeEffects: string[] = [] activeEffects: string[] = []
) { ) {
this._conditions = conditions; this._conditions = conditions; // Conditional properties
this._properties = properties; this._properties = properties; // Base properties
this._activeEffects = activeEffects; this._activeEffects = activeEffects;
} }
@ -25,6 +30,7 @@ export default class Vessel<T extends VesselProperties> {
// Player effects should be applied first, then scene effects should be applied. // Player effects should be applied first, then scene effects should be applied.
// This will mean that scene effects will take precedence over player effects. // This will mean that scene effects will take precedence over player effects.
// Returns base properties with conditional properties applied.
applyEffects( applyEffects(
activePlayerEffects: string[], activePlayerEffects: string[],
activeSceneEffects: string[] activeSceneEffects: string[]
@ -40,6 +46,7 @@ export default class Vessel<T extends VesselProperties> {
const effects = activeEffects.map((e) => map[e]); const effects = activeEffects.map((e) => map[e]);
const appliedProperties = { ...this._properties }; const appliedProperties = { ...this._properties };
// for each effect, apply changed properties to the
for (const effect of effects) { for (const effect of effects) {
Object.assign(appliedProperties, effect.properties); Object.assign(appliedProperties, effect.properties);
} }
@ -47,6 +54,7 @@ export default class Vessel<T extends VesselProperties> {
return appliedProperties; return appliedProperties;
} }
// Generate a description of the contained items
description(items: Item[]): string | null { description(items: Item[]): string | null {
if (items.length === 0) return null; if (items.length === 0) return null;
@ -67,10 +75,12 @@ export default class Vessel<T extends VesselProperties> {
return description; return description;
} }
// Checks if a given effect exists as an active effect on this vessel
hasActiveEffect(effect: string): boolean { hasActiveEffect(effect: string): boolean {
return this._activeEffects.includes(effect); return this._activeEffects.includes(effect);
} }
// Removes an effect if it is active on this vessel
removeEffect(effect: string): void { removeEffect(effect: string): void {
const idx = this._activeEffects.findIndex((e) => e === effect); const idx = this._activeEffects.findIndex((e) => e === effect);

View File

@ -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<SceneProperties> = { import { StoryScene, SceneProperties } from "../types.ts";
// hall represents a room
export const hall: StoryScene<SceneProperties> = {
// properties represent the base, default state for this room.
properties: { properties: {
// This is what the user will see if the look while in the room.
description: "It's very dark", description: "It's very dark",
// This array lists all the items that are in the room.
items: [ 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", name: "flashlight",
actions: { 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: { use: {
// These are the pieces of information that the engine needs to
// exectue a use action of type "applyEffect."
args: { 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", applyTo: "scene",
// When it's used, this is what should be reported back to
// the user.
result: "The flashlight is on.", 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.", 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, reversible: true,
}, },
// applyEffect means that when this item is used, it will apply
// an effect to either the player or the scene.
type: "applyEffect", type: "applyEffect",
}, },
}, },
}, },
], ],
}, },
// conditions are a set of conditions that, when met, will make alterations to the
// player or the scene.
conditions: { conditions: {
// effects are conditions that revolve around effects that have been applied to
// either the player or the scene.
effects: [ 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: { 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: description:
"You are standing in a big hall. There's lots of nooks, " + "You are standing in a big hall. There's lots of nooks, " +
"crannies, and room for general testing. Aw yeah... sweet testing!", "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", source: "player",
}, },
], ],

View File

@ -1,33 +1,42 @@
/**
* Term.ts contains the type information for parsing user sentences.
*/
import Player from "../Player.ts"; import Player from "../Player.ts";
import Scene from "../Scene.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 = ( export type ActionFn = (
player: Player, player: Player,
scene: Scene, scene: Scene,
target?: string, target?: string, // the target is the target of the action.
object?: string object?: string // the object is an item that is used with the action on the target.
) => string; ) => 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 { export interface Constant {
constant: string; constant: string;
category: Category; category: Category;
} }
export type Category = // Category is a union of the different kinds of Constants.
| "action" export type Category = "action" | "direction" | "position" | "interaction";
| "direction"
| "compound"
| "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 { export interface Term {
precedesCategories: Category[]; precedesCategories: Category[]; // categories of Constant that can follow this Term
precedesConstants: Constant[]; precedesConstants: Constant[]; // Constants that can follow this Term
category: Category; category: Category; // the category of this Terms Constant is part of
constant: string; constant: string; // the name of the Constant that this Term describes
canPrecedeVariable: boolean; 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 { export interface ActionTerm extends Term {
action: ActionFn; action: ActionFn;
category: "action"; category: "action";

View File

@ -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 Scene from "../Scene.ts";
import type Player from "../Player.ts"; import type Player from "../Player.ts";
import type { ApplyEffectArgs, ItemAction } from "../types.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_UNUSABLE = "I don't know how to use that.";
const ITEM_ALREADY_USED = "I already did 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 { export function look(player: Player, scene: Scene): string {
return scene.look(player.activeEffects, scene.activeEffects); 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( export function pickUpItem(
player: Player, player: Player,
scene: Scene, scene: Scene,
@ -19,19 +33,29 @@ export function pickUpItem(
return "What do you want me to get?"; return "What do you want me to get?";
} }
// remove the item from the scene.
const item = scene.get(target); const item = scene.get(target);
// The item was found in the scene...
if (item !== null) { 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 { export function checkInventory(player: Player): string {
return player.look(); 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 { export function use(player: Player, scene: Scene, target?: string): string {
if (!target) { if (!target) {
return "What do you want to use?"; return "What do you want to use?";
@ -39,10 +63,12 @@ export function use(player: Player, scene: Scene, target?: string): string {
const { inventory } = player; const { inventory } = player;
// if there is no inventory...
if (inventory === null) { if (inventory === null) {
return ITEM_MISSING; return ITEM_MISSING;
} }
// search the inventory for the named item
const item = inventory.find((i) => i.name === target); const item = inventory.find((i) => i.name === target);
if (item === undefined) { if (item === undefined) {
@ -51,6 +77,7 @@ export function use(player: Player, scene: Scene, target?: string): string {
const itemAction = item?.actions["use"]; const itemAction = item?.actions["use"];
// The item doesn't have action defined for "use", so it cannot be used.
if (itemAction === undefined) { if (itemAction === undefined) {
return ITEM_UNUSABLE; return ITEM_UNUSABLE;
} }
@ -58,6 +85,8 @@ export function use(player: Player, scene: Scene, target?: string): string {
return executeItemAction(player, scene, itemAction); return executeItemAction(player, scene, itemAction);
} }
// executeItemAction figures out what kind of action an item supports and then executes
// the logic for it.
function executeItemAction( function executeItemAction(
player: Player, player: Player,
scene: Scene, scene: Scene,
@ -71,22 +100,28 @@ function executeItemAction(
} }
} }
// applyEffect contains the logic for applying an effect to the player or the scene.
function applyEffect( function applyEffect(
player: Player, player: Player,
scene: Scene, scene: Scene,
{ applyTo, effect, reverseResult, reversible, result }: ApplyEffectArgs { applyTo, effect, reverseResult, reversible, result }: ApplyEffectArgs
): string { ): string {
const target = applyTo === "player" ? player : scene; const target = applyTo === "player" ? player : scene;
// if true, effect is already present.
const isApplied = target.hasActiveEffect(effect); const isApplied = target.hasActiveEffect(effect);
if (!reversible && isApplied) { if (!reversible && isApplied) {
// effect is present already and can't be reversed
return ITEM_ALREADY_USED; return ITEM_ALREADY_USED;
} }
if (isApplied) { if (isApplied) {
// effect is present...
target.removeEffect(effect); target.removeEffect(effect);
return reverseResult || result; return reverseResult || result;
} else { } else {
// effect is not present...
target.addEffect(effect); target.addEffect(effect);
return result; return result;
} }

View File

@ -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 * as actions from "./actions.ts";
import { ActionTerm } from "./Term.ts"; import { ActionTerm } from "./Term.ts";

View File

@ -1,18 +1,28 @@
/**
* types.ts describes basic types used by the engine.
*/
export * from "./data/rooms.ts"; 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; export type ActionArgs = ApplyEffectArgs;
// ItemActionType is a union of all the types of action that can be used
export type ItemActionType = "applyEffect"; export type ItemActionType = "applyEffect";
// Args is a base interface for the various argument interfaces
export interface Args { export interface Args {
result: string; result: string;
} }
// ItemAction represents an action that can be taken by an item
export interface ItemAction { export interface ItemAction {
type: ItemActionType; type: ItemActionType;
args: ActionArgs; args: ActionArgs;
} }
// ApplyEffectArgs are the arguments required to apply effects to a player or scene
export interface ApplyEffectArgs extends Args { export interface ApplyEffectArgs extends Args {
effect: string; effect: string;
applyTo: "player" | "scene"; applyTo: "player" | "scene";
@ -20,34 +30,44 @@ export interface ApplyEffectArgs extends Args {
reverseResult?: string; reverseResult?: string;
} }
// ApplyEffectItemAction describes the apply effect action
export interface ApplyEffectItemAction extends ItemAction { export interface ApplyEffectItemAction extends ItemAction {
type: "applyEffect"; type: "applyEffect";
args: ApplyEffectArgs;
} }
// Item represents some item either in the scene or in the player's inventory
export interface Item { export interface Item {
name: string; name: string;
actions: { [name: string]: ItemAction }; actions: { [name: string]: ItemAction };
} }
export interface Effect<T> { // Effect represents some effect that may be applied to a scene or player
export interface Effect<T extends VesselProperties> {
name: string; name: string;
properties: T; 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 { export interface VesselProperties {
items?: Item[]; items?: Item[];
} }
// SceneProperties are the properties (in addition to the VesselProperties) that are
// needed by the scene
export interface SceneProperties extends VesselProperties { export interface SceneProperties extends VesselProperties {
description?: string; description?: string;
} }
export interface Conditions<T> { // Conditions contains story elements that can be conditionally applied to the scene or
// player
export interface Conditions<T extends VesselProperties> {
effects: Effect<T>[]; effects: Effect<T>[];
} }
export interface GameData<T> { // StoryScene holds both the conditions and properties for a scene.
export interface StoryScene<T extends VesselProperties> {
conditions: Conditions<T>; conditions: Conditions<T>;
properties: SceneProperties; properties: SceneProperties;
} }