diff --git a/src/engine/Game.ts b/src/engine/Game.ts index eb68244..03a4a2e 100644 --- a/src/engine/Game.ts +++ b/src/engine/Game.ts @@ -1,12 +1,19 @@ import {enableMapSet, createDraft, finishDraft, Draft} from 'immer' -import GameState, { Room, Door, Item, ObjectType } from './types/GameState' +import GameState, { GameObject, Room, Door, Item, ObjectType } from './types/GameState' +import ParsedCommand, { TokenType, ParsedTokenExpression, ValidCommandDetails, InvalidCommandDetails } from './types/ParsedCommand' enableMapSet() type ChangeListener = (state : GameState) => void + +export type CommandValidateResult = { + validCommands: ValidCommandDetails [], + invalidCommands: InvalidCommandDetails [] +} + export default class Game { - private gameState : GameState = {directions: new Map(), rooms: new Map(), doors: new Map(), items: new Map()} + private gameState : GameState = {directions: new Map(), rooms: new Map(), doors: new Map(), items: new Map(), player: {location: ''}} private draft : Draft | null = null private onChangeListeners : ChangeListener [] = [] @@ -22,6 +29,25 @@ export default class Game { this.saveDraft() } + filterValidCommands(commands: ParsedCommand[]) : CommandValidateResult { + let validCommands : ValidCommandDetails[] = [] + let invalidCommands : InvalidCommandDetails[] = [] + + for(const command of commands) { + const commandValidationResult = command.areNounsValid(this) + + if(commandValidationResult.isValid){ + validCommands.push(commandValidationResult) + } else { + invalidCommands.push(commandValidationResult) + } + } + + invalidCommands.sort((a,b) => a.severity - b.severity) + + return {validCommands, invalidCommands} + } + onChange(callback : ChangeListener) { this.onChangeListeners.push(callback) } @@ -51,6 +77,12 @@ export default class Game { return this.draft! } + getCurrentRoom() : Draft | null { + const state = this.getState() + return Array.from(state.rooms.values()) + .find(room => room.name === state.player.location) || null + } + addRoom(room : Room) { let state = this.getState() state.rooms.set(room.name, room) @@ -65,4 +97,68 @@ export default class Game { let state = this.getState() state.items.set(item.name, item) } + + findObjectByName(name : string, type : ObjectType) : GameObject | null { + let collection + switch(type) { + case ObjectType.Door: + collection = this.getState().doors + break + + case ObjectType.Item: + collection = this.getState().items + break + + case ObjectType.Room: + collection = this.getState().rooms + break + + case ObjectType.Direction: + collection = this.getState().directions + break + } + + const objects = [...collection!.values()] + + const exactMatch = objects.find((object) => name === object.name) + if(exactMatch) + return exactMatch + + const aliasMatch = objects.find(({aliases}) => aliases.includes(name)) + if(aliasMatch) + return aliasMatch + + return null + } + + isVisible(object : GameObject) : boolean { + const state = this.getState() + const currentRoom = this.getCurrentRoom() + + switch(object.type) { + case ObjectType.Direction: + return true + + case ObjectType.Room: + return state.player.location === (object as Room).name + + case ObjectType.Door: + if(!currentRoom) + return false; + + const neighborIDs = Array.from(currentRoom.neighbors.values()) + + let neighbors = neighborIDs.map(name => state.doors.get(name)).filter(object => object !== undefined).map(o => o!) + for(const neighbor of neighbors) + if(neighbor.name === object.name) + return true; + return false + + case ObjectType.Item: + return state.player.location === (object as Item).location + + default: + return false + } + } } diff --git a/src/engine/Parser.ts b/src/engine/Parser.ts index a6cfb54..bcdb365 100644 --- a/src/engine/Parser.ts +++ b/src/engine/Parser.ts @@ -14,10 +14,14 @@ export default class Parser { } handleCommand(rawCommand : string) { + // Parse command for syntactical validity + // (according to known verb templates) const grammaticalParsings : ParsedCommand[] = this.parseCommandString(rawCommand) - console.log(grammaticalParsings) - // const validParsings : ParsedCommand[] = this.game.filterCommandsForValidity(grammaticalParsings) + // Ask the game state container to filter commands for object validity + // (nouns refer to valid objects, all objects are visible, etc) + const validationResult = this.game.filterValidCommands(grammaticalParsings) + console.log(validationResult) } parseCommandString(rawCommand: string): ParsedCommand[] { diff --git a/src/engine/types/GameState.ts b/src/engine/types/GameState.ts index 8ab7e82..4968dec 100644 --- a/src/engine/types/GameState.ts +++ b/src/engine/types/GameState.ts @@ -1,8 +1,11 @@ type GameState = { - readonly directions: Map, - readonly rooms: Map, - readonly doors: Map, - readonly items: Map + readonly directions: Map, + readonly rooms: Map, + readonly doors: Map, + readonly items: Map, + readonly player: { + readonly location: ObjectID + } } export default GameState @@ -16,21 +19,29 @@ export enum ObjectType { type ObjectID = string -type GameObject = { +export type GameObject = { readonly type : ObjectType, readonly name : ObjectID, readonly aliases : string[] } +export type Direction = GameObject & { + readonly type : ObjectType.Direction +} + export type Room = GameObject & { + readonly type : ObjectType.Room, readonly neighbors : Map } -export type Door = Room & { +export type Door = GameObject & { + readonly type : ObjectType.Door, + readonly neighbors : Map readonly locked : boolean, readonly key : ObjectID } export type Item = GameObject & { + readonly type : ObjectType.Item, readonly location: ObjectID | 'inventory' } diff --git a/src/engine/types/ParsedCommand.ts b/src/engine/types/ParsedCommand.ts index 1e54d06..d9480ce 100644 --- a/src/engine/types/ParsedCommand.ts +++ b/src/engine/types/ParsedCommand.ts @@ -1,6 +1,7 @@ -import { ObjectType } from "./GameState" +import { ObjectType, GameObject } from "./GameState" import NounPosition from "./NounPosition" import Verb from "./Verb" +import Game from "../Game" export enum TokenType { Expression = 'expression', @@ -37,6 +38,14 @@ export class ParsedTokenExpression extends ParsedToken { } } +export enum ParsingErrorSeverity { + NotVisible = 0, + NoSuchObject = 1 +} + +export type ValidCommandDetails = {isValid: true, command: ParsedCommand, subject: GameObject | null, object: GameObject | null} +export type InvalidCommandDetails = {isValid: false, command: ParsedCommand, reason: string, severity: ParsingErrorSeverity} + export default class ParsedCommand { readonly verb : Verb private tokens : ParsedToken[] = [] @@ -50,4 +59,42 @@ export default class ParsedCommand { newCommand.tokens = [token, ...this.tokens] return newCommand } + + areNounsValid(game : Game) : ValidCommandDetails | InvalidCommandDetails { + const nouns = this.tokens.filter(({type}) => type === TokenType.Expression).map(token => token as ParsedTokenExpression) + + let subject : GameObject | null = null + let object : GameObject | null = null + + for(const noun of nouns) { + let gameObject = game.findObjectByName(noun.name, noun.itemType) + if(!gameObject) + return { + isValid: false, + command: this, + reason: `You used the word ${noun.name} as if it was a ${noun.itemType}, but there is no such object`, + severity: ParsingErrorSeverity.NoSuchObject + } + + if(!game.isVisible(gameObject)) + return { + isValid: false, + command: this, + reason: `You cannot see ${noun.name}`, + severity: ParsingErrorSeverity.NotVisible + } + + if(noun.sentencePosition === NounPosition.Subject) + subject = gameObject + else + object = gameObject + } + + return { + isValid: true, + command: this, + subject, + object + } + } } diff --git a/src/index.tsx b/src/index.tsx index 8bd42d4..7e71a29 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -16,6 +16,16 @@ parser.understand('look') parser.understand('lookDirection') .as('look [direction]') +parser.understand('lookAt') + .as('look [item]') + .as('look [door]') + .as('look at [item]') + .as('look at [door]') + .as('examine [item]') + .as('examine [door]') + .as('x [item]') + .as('x [door]') + parser.understand('go') .as('go [direction]') .as('[direction]') @@ -76,6 +86,8 @@ game.addItem({ location: 'entry' }) +game.getState().player.location = 'entry' + game.saveDraft() diff --git a/tsconfig.json b/tsconfig.json index f2850b7..4acaa3a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -17,7 +17,8 @@ "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, - "jsx": "react" + "jsx": "react", + "downlevelIteration": true }, "include": [ "src"