Files
detective/Interpreter.ts
2021-05-27 14:47:45 -07:00

137 lines
3.8 KiB
TypeScript

/**
* 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;
}
}
}