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 {
|
||||
#items: Item[];
|
||||
#user: User;
|
||||
import { Effect, Item, EntityProperties } from "./types.ts";
|
||||
import Entity from "./Entity.ts";
|
||||
|
||||
constructor(items?: Item[]) {
|
||||
this.#items = items || [];
|
||||
this.#user = new User();
|
||||
// The Player is a type of "entity" which is a generic object that can have effects
|
||||
// and items.
|
||||
export default class Player extends Entity<EntityProperties> {
|
||||
constructor(items: Item[] = [], _effects: Effect<EntityProperties>[] = []) {
|
||||
super({ items }, { effects: [] });
|
||||
}
|
||||
|
||||
drop(item: Item) {
|
||||
this.#items.push(item);
|
||||
// inventory is a getter that returns the items in the user's inventory
|
||||
get inventory(): Item[] | null {
|
||||
return this._properties.items || null;
|
||||
}
|
||||
|
||||
async inventory() {
|
||||
const vowels = ["a", "e", "i", "o", "u"];
|
||||
const description = this.#items
|
||||
.map(({ name }, i) => {
|
||||
let anItem = `${vowels.includes(name[0]) ? "an" : "a"} ${name}`;
|
||||
// look returns a string that describes the players inventory
|
||||
look(): string {
|
||||
const description = super.description(this._properties.items || []);
|
||||
|
||||
if (i + 1 === this.#items.length) {
|
||||
anItem = `and ${anItem}`;
|
||||
}
|
||||
if (description) {
|
||||
return `You have ${description}`;
|
||||
} else {
|
||||
return "You have nothing.";
|
||||
}
|
||||
}
|
||||
|
||||
return anItem;
|
||||
})
|
||||
.join(", ");
|
||||
|
||||
await this.#user.tell(`You have ${description}.`);
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
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 {
|
||||
#items: Item[];
|
||||
#user: User;
|
||||
import type { Exit, StoryScene, Item, SceneProperties } from "./types.ts";
|
||||
import Entity from "./Entity.ts";
|
||||
|
||||
constructor(gameData: GameData) {
|
||||
this.#items = gameData.items;
|
||||
this.#user = new User();
|
||||
// The Scene is a type of "Entity" which is a generic object that can have effects
|
||||
// and items.
|
||||
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() {
|
||||
const description = this.#items
|
||||
.map(({ description }) => description)
|
||||
.join(" ");
|
||||
changeScene(identifier: string) {
|
||||
const scene = Scene.getScene(identifier, this._map);
|
||||
|
||||
await this.#user.tell(description);
|
||||
if (scene) {
|
||||
const { conditions, properties } = scene;
|
||||
|
||||
this.properties = properties;
|
||||
this.conditions = conditions;
|
||||
}
|
||||
}
|
||||
|
||||
async take(target: string): Promise<Item | null> {
|
||||
const idx = this.#items.findIndex(({ name }) => name === target);
|
||||
// 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 = this.#items[idx];
|
||||
this.#items.splice(idx, 1);
|
||||
|
||||
await this.#user.tell("Taken.");
|
||||
const item = items[idx];
|
||||
items.splice(idx, 1);
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
// if an item wasn't found and returned, 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 = ">";
|
||||
|
||||
export default class User {
|
||||
#prompt: string;
|
||||
#out: (text: string) => Promise<void>;
|
||||
|
||||
constructor() {
|
||||
this.#prompt = DEFAULT_PROMPT;
|
||||
this.#out = async (text: string) => {
|
||||
const data = new TextEncoder().encode(text);
|
||||
await Deno.stdout.write(data);
|
||||
};
|
||||
constructor(prompt: string = DEFAULT_PROMPT) {
|
||||
this.#prompt = prompt;
|
||||
}
|
||||
|
||||
ask(question: string): string {
|
||||
@ -23,6 +22,11 @@ export default class User {
|
||||
}
|
||||
|
||||
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 Scene from "./Scene.ts";
|
||||
import Interpreter from "./Interpreter.ts";
|
||||
import Player from "./Player.ts";
|
||||
import { hall } from "./data/data.ts";
|
||||
import parseCommand from "./parseCommand.ts";
|
||||
import Scene from "./Scene.ts";
|
||||
import User from "./User.ts";
|
||||
import game from "./data/rooms.ts";
|
||||
|
||||
async function main() {
|
||||
const user = new User();
|
||||
const scene = new Scene(hall);
|
||||
const player = new Player();
|
||||
const question = "";
|
||||
let running = true;
|
||||
let statement = "";
|
||||
const user = new User(); // for communication with the user
|
||||
const scene = new Scene("hall", game.map); // the room that player is in.
|
||||
const player = new Player(); // the players current state
|
||||
let running = true; // running flag for the game loop. Stops on false.
|
||||
let statement = ""; // holds a statement for the user.
|
||||
|
||||
while (running) {
|
||||
const prompts = `${statement}${question}`;
|
||||
const prompts = `${statement}\n`;
|
||||
const answer = await user.ask(prompts);
|
||||
|
||||
const { action, target } = parseCommand(answer);
|
||||
|
||||
statement = "";
|
||||
|
||||
switch (action) {
|
||||
// User commands that are not player moves, but are about running the
|
||||
// game itself (e.g. loading, saving, quitting) may need to get handled
|
||||
// here.
|
||||
switch (answer) {
|
||||
// kills the game loop.
|
||||
case "quit":
|
||||
running = quit(user);
|
||||
break;
|
||||
|
||||
case "look":
|
||||
await scene.look();
|
||||
break;
|
||||
|
||||
case "take":
|
||||
await pickUpItem(scene, player, target);
|
||||
break;
|
||||
|
||||
case "inventory":
|
||||
await player.inventory();
|
||||
statement = "";
|
||||
break;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
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 true;
|
||||
}
|
||||
|
||||
async function pickUpItem(scene: Scene, player: Player, target: string) {
|
||||
const item = await scene.take(target);
|
||||
|
||||
if (item !== null) {
|
||||
player.drop(item);
|
||||
}
|
||||
}
|
||||
|
||||
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