From 03eddfcd7440ddea881572ea42ee99f8654873d5 Mon Sep 17 00:00:00 2001 From: nolwn Date: Mon, 17 May 2021 19:59:21 -0700 Subject: [PATCH] Refactor parser for some added flexibility. --- Container.ts | 4 +- Interpreter.ts | 129 +++++++++++++++++++++++++++++++++++++++++++++++ Player.ts | 10 ++-- Scene.ts | 12 +++-- main.ts | 83 +++++++++++++----------------- parseCommand.ts | 5 -- terms/Term.ts | 34 +++++++++++++ terms/actions.ts | 28 ++++++++++ terms/terms.ts | 35 +++++++++++++ 9 files changed, 279 insertions(+), 61 deletions(-) create mode 100644 Interpreter.ts delete mode 100644 parseCommand.ts create mode 100644 terms/Term.ts create mode 100644 terms/actions.ts create mode 100644 terms/terms.ts diff --git a/Container.ts b/Container.ts index ed50609..ed1fa69 100644 --- a/Container.ts +++ b/Container.ts @@ -7,7 +7,9 @@ export default class Container { this.items = items; } - description(items: Item[]): string { + description(items: Item[]): string | null { + if (items.length === 0) return null; + const vowels = ["a", "e", "i", "o", "u"]; const description = items .map(({ name }, i) => { diff --git a/Interpreter.ts b/Interpreter.ts new file mode 100644 index 0000000..e724b81 --- /dev/null +++ b/Interpreter.ts @@ -0,0 +1,129 @@ +import Player from "./Player.ts"; +import Scene from "./Scene.ts"; +import { terms } from "./terms/terms.ts"; +import type { + Action, + 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: Action | 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: Action) { + // 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; + } + } +} diff --git a/Player.ts b/Player.ts index 0859a91..f9814c3 100644 --- a/Player.ts +++ b/Player.ts @@ -10,13 +10,17 @@ export default class Player extends Container { this.#user = new User(); } - drop(item: Item) { + put(item: Item) { this.items.push(item); } - async look(): Promise { + look(): string { const description = super.description(this.items); - await this.#user.tell(`You have ${description}`); + if (description) { + return `You have ${description}`; + } else { + return "You have nothing."; + } } } diff --git a/Scene.ts b/Scene.ts index ddf426e..277675e 100644 --- a/Scene.ts +++ b/Scene.ts @@ -10,21 +10,23 @@ export default class Scene extends Container { this.#user = new User(); } - async look(): Promise { + look(): string { const description = super.description(this.items); - await this.#user.tell(`There is ${description}`); + if (description) { + return `There is ${description}`; + } else { + return "There is nothing around..."; + } } - async take(target: string): Promise { + get(target: string): Item | null { const idx = this.items.findIndex(({ name }) => name === target); if (idx >= 0) { const item = this.items[idx]; this.items.splice(idx, 1); - await this.#user.tell("Taken."); - return item; } diff --git a/main.ts b/main.ts index 0a6a2a6..0ef157c 100644 --- a/main.ts +++ b/main.ts @@ -1,75 +1,64 @@ -import User from "./User.ts"; -import Scene from "./Scene.ts"; +import Interpreter from "./Interpreter.ts"; import Player from "./Player.ts"; +import Scene from "./Scene.ts"; +import User from "./User.ts"; import { hall } from "./data/data.ts"; -import parseCommand from "./parseCommand.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); // 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(user, scene, player, target); - break; - - case "inventory": - await player.look(); + statement = ""; break; default: - statement = "\nI 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( - user: User, - scene: Scene, - player: Player, - target?: string -) { - if (!target) { - await user.tell("What do you want me to take?"); - return; - } - - const item = await scene.take(target); - - if (item !== null) { - player.drop(item); - } -} - main(); diff --git a/parseCommand.ts b/parseCommand.ts deleted file mode 100644 index 07174ce..0000000 --- a/parseCommand.ts +++ /dev/null @@ -1,5 +0,0 @@ -export default function parseCommand(command: string) { - const [action, target] = command.toLocaleLowerCase().split(" "); - - return { action, target }; -} diff --git a/terms/Term.ts b/terms/Term.ts new file mode 100644 index 0000000..2181023 --- /dev/null +++ b/terms/Term.ts @@ -0,0 +1,34 @@ +import Player from "../Player.ts"; +import Scene from "../Scene.ts"; + +export type ActionFn = ( + player: Player, + scene: Scene, + target?: string, + object?: string +) => string; + +export interface Constant { + constant: string; + category: Category; +} + +export type Category = + | "action" + | "direction" + | "compound" + | "position" + | "interaction"; + +export interface Term { + precedesCategories: Category[]; + precedesConstants: Constant[]; + category: Category; + constant: string; + canPrecedeVariable: boolean; +} + +export interface Action extends Term { + action: ActionFn; + category: "action"; +} diff --git a/terms/actions.ts b/terms/actions.ts new file mode 100644 index 0000000..00ccb49 --- /dev/null +++ b/terms/actions.ts @@ -0,0 +1,28 @@ +import Scene from "../Scene.ts"; +import Player from "../Player.ts"; + +export function look(_player: Player, scene: Scene): string { + return scene.look(); +} + +export function pickUpItem( + player: Player, + scene: Scene, + target?: string +): string { + if (!target) { + return "What do you want me to get?"; + } + + const item = scene.get(target); + + if (item !== null) { + player.put(item); + } + + return "Taken."; +} + +export function checkInventory(player: Player) { + return player.look(); +} diff --git a/terms/terms.ts b/terms/terms.ts new file mode 100644 index 0000000..694e6f9 --- /dev/null +++ b/terms/terms.ts @@ -0,0 +1,35 @@ +import * as actions from "./actions.ts"; +import { Action } from "./Term.ts"; + +const inventory: Action = { + action: actions.checkInventory, + canPrecedeVariable: false, + category: "action", + constant: "inventory", + precedesCategories: [], + precedesConstants: [], +}; + +const look: Action = { + action: actions.look, + canPrecedeVariable: false, + category: "action", + constant: "look", + precedesCategories: [], + precedesConstants: [], +}; + +const take: Action = { + action: actions.pickUpItem, + category: "action", + canPrecedeVariable: true, + constant: "take", + precedesCategories: [], + precedesConstants: [], +}; + +export const terms = { + [inventory.constant]: inventory, + [look.constant]: look, + [take.constant]: take, +};