Refactor parser for some added flexibility.
This commit is contained in:
@ -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
129
Interpreter.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
10
Player.ts
10
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<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.";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
12
Scene.ts
12
Scene.ts
@ -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
83
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();
|
||||
|
@ -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
34
terms/Term.ts
Normal 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
28
terms/actions.ts
Normal 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
35
terms/terms.ts
Normal 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,
|
||||
};
|
Reference in New Issue
Block a user