diff --git a/src/components/App/App.js b/src/components/App/App.js index ce74e83..fa5b0e6 100644 --- a/src/components/App/App.js +++ b/src/components/App/App.js @@ -2,10 +2,11 @@ import React, {useRef, useEffect, useState} from 'react'; import ReactMarkdown from 'react-markdown' import styles from './App.module.css'; -function App({onCommand, messages, game}) { +function App({onCommand, game}) { const inputRef = useRef() const playAreaRef = useRef() const [state, setState] = useState({}) + const messages = state.messages || [] function onSubmit(ev) { if(ev) ev.preventDefault(); @@ -84,5 +85,8 @@ function jsonReplacer(key, value) { return obj; }, {}); + if(key === 'messages' && Array.isArray(value) && value.length) + return ['...'] + return value } diff --git a/src/components/App/App.module.css b/src/components/App/App.module.css index 47da986..2a2ca8d 100644 --- a/src/components/App/App.module.css +++ b/src/components/App/App.module.css @@ -48,6 +48,8 @@ .infoArea { width: 300px; overflow-x: hidden; + overflow-y: scroll; + max-height: 100vh; } .command { diff --git a/src/engine/Game.ts b/src/engine/Game.ts index 03a4a2e..bf618b9 100644 --- a/src/engine/Game.ts +++ b/src/engine/Game.ts @@ -1,6 +1,7 @@ import {enableMapSet, createDraft, finishDraft, Draft} from 'immer' import GameState, { GameObject, Room, Door, Item, ObjectType } from './types/GameState' -import ParsedCommand, { TokenType, ParsedTokenExpression, ValidCommandDetails, InvalidCommandDetails } from './types/ParsedCommand' +import ParsedCommand, { ValidCommandDetails, InvalidCommandDetails } from './types/ParsedCommand' +import { GameEventMessage, GameEventCommand } from './types/GameEvent' enableMapSet() @@ -13,7 +14,15 @@ export type CommandValidateResult = { } export default class Game { - private gameState : GameState = {directions: new Map(), rooms: new Map(), doors: new Map(), items: new Map(), player: {location: ''}} + private gameState : GameState = { + directions: new Map(), + rooms: new Map(), + doors: new Map(), + items: new Map(), + player: {location: ''}, + messages: [] + } + private draft : Draft | null = null private onChangeListeners : ChangeListener [] = [] @@ -29,6 +38,16 @@ export default class Game { this.saveDraft() } + outputCommand(commandString: string) { + const state = this.getState() + state.messages.push(new GameEventCommand(commandString)) + } + + say(message: string) { + const state = this.getState() + state.messages.push(new GameEventMessage(message)) + } + filterValidCommands(commands: ParsedCommand[]) : CommandValidateResult { let validCommands : ValidCommandDetails[] = [] let invalidCommands : InvalidCommandDetails[] = [] @@ -43,7 +62,12 @@ export default class Game { } } - invalidCommands.sort((a,b) => a.severity - b.severity) + invalidCommands.sort((a,b) => { + if(a.severity !== b.severity) + return a.severity - b.severity + + return b.command.getNumTokens() - a.command.getNumTokens() + }) return {validCommands, invalidCommands} } @@ -104,15 +128,15 @@ export default class Game { 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 @@ -123,7 +147,7 @@ export default class Game { const exactMatch = objects.find((object) => name === object.name) if(exactMatch) return exactMatch - + const aliasMatch = objects.find(({aliases}) => aliases.includes(name)) if(aliasMatch) return aliasMatch diff --git a/src/engine/Parser.ts b/src/engine/Parser.ts index bcdb365..57a1369 100644 --- a/src/engine/Parser.ts +++ b/src/engine/Parser.ts @@ -1,6 +1,6 @@ import Game from "./Game"; import RulesEngine from './RulesEngine' -import ParsedCommand from "./types/ParsedCommand"; +import ParsedCommand, { InvalidCommandDetails } from "./types/ParsedCommand"; import Verb from './types/Verb'; export default class Parser { @@ -14,6 +14,8 @@ export default class Parser { } handleCommand(rawCommand : string) { + this.game.outputCommand(rawCommand) + // Parse command for syntactical validity // (according to known verb templates) const grammaticalParsings : ParsedCommand[] = this.parseCommandString(rawCommand) @@ -21,6 +23,15 @@ export default class Parser { // 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) + + if(validationResult.validCommands.length < 1) { + this.handleError(validationResult.invalidCommands) + } else { + // TODO: Do the thing + } + + this.game.saveDraft() + console.log(validationResult) } @@ -35,6 +46,25 @@ export default class Parser { return parsings } + handleError(invalidCommands: InvalidCommandDetails []) { + console.log(invalidCommands) + + if(!invalidCommands.length){ + this.game.say("I'm unsure what you're trying to do") + return + } + + const mostValid = invalidCommands[0] + + if(mostValid.command.verb.name === 'go'){ + this.game.say("I'm unsure what you're trying to do") + return + } + + this.game.say(mostValid.reason) + console.log(mostValid) + } + understand(name : string) : VerbBuilder { const verb = new Verb(name) this.verbs.push(verb) diff --git a/src/engine/Renderer.tsx b/src/engine/Renderer.tsx index 70ddc45..bdd4583 100644 --- a/src/engine/Renderer.tsx +++ b/src/engine/Renderer.tsx @@ -32,7 +32,7 @@ export default class Renderer { private render() { ReactDOM.render( - + , document.getElementById('root') ) diff --git a/src/engine/types/GameEvent.ts b/src/engine/types/GameEvent.ts index 680ce91..55221e2 100644 --- a/src/engine/types/GameEvent.ts +++ b/src/engine/types/GameEvent.ts @@ -3,24 +3,28 @@ export enum GameEventType { Command = 'command' } -export default interface GameEvent { - getType() : GameEventType; +export default class GameEvent { + readonly type : GameEventType + + constructor(type : GameEventType) { + this.type = type + } } -export class GameEventMessage implements GameEvent { - private message : string +export class GameEventMessage extends GameEvent { + readonly message : string - getType() : GameEventType { return GameEventType.Message } constructor(message : string) { + super(GameEventType.Message) this.message = message } } -export class GameEventCommand implements GameEvent { - private rawCommand : string +export class GameEventCommand extends GameEvent { + readonly command : string - getType() : GameEventType { return GameEventType.Command } - constructor(rawCommand : string) { - this.rawCommand = rawCommand + constructor(command : string) { + super(GameEventType.Command) + this.command = command } } diff --git a/src/engine/types/GameState.ts b/src/engine/types/GameState.ts index 4968dec..867d820 100644 --- a/src/engine/types/GameState.ts +++ b/src/engine/types/GameState.ts @@ -1,3 +1,5 @@ +import GameEvent from './GameEvent' + type GameState = { readonly directions: Map, readonly rooms: Map, @@ -6,6 +8,7 @@ type GameState = { readonly player: { readonly location: ObjectID } + readonly messages: GameEvent [] } export default GameState diff --git a/src/engine/types/ParsedCommand.ts b/src/engine/types/ParsedCommand.ts index d9480ce..b462618 100644 --- a/src/engine/types/ParsedCommand.ts +++ b/src/engine/types/ParsedCommand.ts @@ -54,6 +54,10 @@ export default class ParsedCommand { this.verb = verb } + getNumTokens() : number { + return this.tokens.length + } + prepend(token : ParsedToken) { const newCommand = new ParsedCommand(this.verb) newCommand.tokens = [token, ...this.tokens] @@ -65,7 +69,7 @@ export default class ParsedCommand { 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) @@ -76,11 +80,14 @@ export default class ParsedCommand { severity: ParsingErrorSeverity.NoSuchObject } + // TODO: Optionally print "the" depending on if the original + // command name had one at the beginning (don't print the the book) + // (but also don't do "you cannot see heart of the cards") if(!game.isVisible(gameObject)) return { isValid: false, command: this, - reason: `You cannot see ${noun.name}`, + reason: `You cannot see the ${noun.name}`, severity: ParsingErrorSeverity.NotVisible } diff --git a/src/index.tsx b/src/index.tsx index 7e71a29..68bd57b 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -86,6 +86,13 @@ game.addItem({ location: 'entry' }) +game.addItem({ + type: ObjectType.Item, + name: 'ruby', + aliases: ['gem'], + location: 'office' +}) + game.getState().player.location = 'entry' game.saveDraft()