Refactor parser for some added flexibility.

This commit is contained in:
2021-05-17 19:59:21 -07:00
parent 09e10e94b5
commit 03eddfcd74
9 changed files with 279 additions and 61 deletions

View File

@ -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) => {

129
Interpreter.ts Normal file
View File

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

View File

@ -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<void> {
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.";
}
}
}

View File

@ -10,21 +10,23 @@ export default class Scene extends Container {
this.#user = new User();
}
async look(): Promise<void> {
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<Item | null> {
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;
}

83
main.ts
View File

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

View File

@ -1,5 +0,0 @@
export default function parseCommand(command: string) {
const [action, target] = command.toLocaleLowerCase().split(" ");
return { action, target };
}

34
terms/Term.ts Normal file
View File

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

28
terms/actions.ts Normal file
View File

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

35
terms/terms.ts Normal file
View File

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