From 717446b25e8ad3dd4c60c0ed55c9e32c3fa1e1ee Mon Sep 17 00:00:00 2001 From: Ashelyn Dawn Date: Sat, 25 Jul 2020 21:04:25 -0600 Subject: [PATCH] Typescript conversion complete - implemented picking up items, unlocking, opening, going, looking --- package-lock.json | 6 + package.json | 3 + src/GameState/index.js | 104 ------------- src/Parser/commands.js | 26 ---- src/Parser/index.js | 195 ------------------------ src/RuleEngine/index.js | 87 ----------- src/RuleEngine/rules/core.js | 33 ---- src/RuleEngine/rules/game.js | 48 ------ src/engine/Game.ts | 17 ++- src/engine/Parser.ts | 28 ++-- src/engine/Renderer.tsx | 11 +- src/engine/RulesEngine.ts | 10 +- src/engine/definitions/go.ts | 47 ++++++ src/engine/definitions/index.js | 9 ++ src/engine/definitions/look.ts | 16 ++ src/engine/definitions/lookAt.ts | 26 ++++ src/engine/definitions/lookDirection.ts | 29 ++++ src/engine/definitions/open.ts | 26 ++++ src/engine/definitions/take.ts | 26 ++++ src/engine/definitions/unlockDoor.ts | 31 ++++ src/engine/index.ts | 14 ++ src/engine/types/GameState.ts | 7 +- src/index.tsx | 70 +++------ src/utils/printArea.js | 2 +- 24 files changed, 303 insertions(+), 568 deletions(-) delete mode 100644 src/GameState/index.js delete mode 100644 src/Parser/commands.js delete mode 100644 src/Parser/index.js delete mode 100644 src/RuleEngine/index.js delete mode 100644 src/RuleEngine/rules/core.js delete mode 100644 src/RuleEngine/rules/game.js create mode 100644 src/engine/definitions/go.ts create mode 100644 src/engine/definitions/index.js create mode 100644 src/engine/definitions/look.ts create mode 100644 src/engine/definitions/lookAt.ts create mode 100644 src/engine/definitions/lookDirection.ts create mode 100644 src/engine/definitions/open.ts create mode 100644 src/engine/definitions/take.ts create mode 100644 src/engine/definitions/unlockDoor.ts create mode 100644 src/engine/index.ts diff --git a/package-lock.json b/package-lock.json index fc7e25d..e006e5e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1928,6 +1928,12 @@ } } }, + "@types/webpack-env": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/@types/webpack-env/-/webpack-env-1.15.2.tgz", + "integrity": "sha512-67ZgZpAlhIICIdfQrB5fnDvaKFcDxpKibxznfYRVAT4mQE41Dido/3Ty+E3xGBmTogc5+0Qb8tWhna+5B8z1iQ==", + "dev": true + }, "@types/yargs": { "version": "13.0.9", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-13.0.9.tgz", diff --git a/package.json b/package.json index 67a454d..ff61799 100644 --- a/package.json +++ b/package.json @@ -39,5 +39,8 @@ "last 1 firefox version", "last 1 safari version" ] + }, + "devDependencies": { + "@types/webpack-env": "^1.15.2" } } diff --git a/src/GameState/index.js b/src/GameState/index.js deleted file mode 100644 index ed46111..0000000 --- a/src/GameState/index.js +++ /dev/null @@ -1,104 +0,0 @@ -import autoBind from 'auto-bind' -import produce, {createDraft, finishDraft} from 'immer' - -export default class GameState { - constructor() { - this.state = finishDraft(createDraft({ - directions: ['north', 'south', 'east', 'west'], - player: { - location: 'empty' - }, - locations: {}, - items: {} - })) - - autoBind(this) - } - - getDraft() { - return createDraft(this.state) - } - - getState() { - return this.state - } - - sealState(draft) { - this._updateState(() => draft) - } - - createItem(item) { - if(!item.id) - throw new Error('Item must have an id field') - - if(this.state.items[item.id]) - throw new Error(`Item with id ${item.id} already defined`) - - this._updateState(state => { - state.items[item.id] = item - }) - } - - createLocation(location) { - if(!location.id) - throw new Error('Location must have an id field') - - if(this.state.locations[location.id]) - throw new Error(`Location with id ${location.id} already defined`) - - this._updateState(state => { - state.locations[location.id] = location - }) - } - - _updateState(reducer) { - const newState = produce(this.state, reducer) - - // Make sure player can never be in an invalid location - if(!newState.locations[newState.player.location]) - throw new Error('Player cannot be in invalid location') - - this.state = newState - } - - filterCommandParsingsByValidNouns(parsings) { - return parsings.filter(({tokens}) => { - let nounExpressions = tokens.filter(token => token.type === 'expression') - - for(const expression of nounExpressions) { - const itemType = expression.name - const itemName = expression.value - - // TODO: Add options for several names - if(itemType === 'direction' && !this.state.directions.includes(itemName)) - return false; - - if(itemType === 'door') { - const door = Object.values(this.state.locations).filter(loc => loc.type === 'door').find(door => itemName === door.name) - if(door) - expression.id = door.id - else - return false; - } - - if(itemType === 'room') { - const room = Object.values(this.state.locations).filter(loc => loc.type === 'room').find(room => itemName === room.name) - if(room) - expression.id = room.id - else - return false; - } - - if(itemType === 'item') { - const item = Object.values(this.state.items).find(item => itemName === item.name) - if(item) - expression.id = item.id - else - return false; - } - } - - return true; - }) - } -} diff --git a/src/Parser/commands.js b/src/Parser/commands.js deleted file mode 100644 index 7a5514a..0000000 --- a/src/Parser/commands.js +++ /dev/null @@ -1,26 +0,0 @@ -const commands = [] -export default commands -function defineCommand(verb, template) { - commands.push({verb, template}) -} - -defineCommand('look', 'look') -defineCommand('look', 'describe') - -defineCommand('lookDirection', 'look [direction]') - -defineCommand('go', 'go [direction]') -defineCommand('go', '[direction]') - -defineCommand('take', 'take [item]') -defineCommand('take', 'get [item]') -defineCommand('take', 'pick up [item]') -defineCommand('take', 'grab [item]') -defineCommand('take', 'snatch [item]') -defineCommand('take', 'steal [item]') - -defineCommand('unlockDoor', 'unlock [door|subject] with [item|object]') -defineCommand('unlockDoor', 'unlock [door]') - -defineCommand('openDoor', 'open [door]') -defineCommand('openDoor', 'open [door|subject] with [item|object]') diff --git a/src/Parser/index.js b/src/Parser/index.js deleted file mode 100644 index d08fe84..0000000 --- a/src/Parser/index.js +++ /dev/null @@ -1,195 +0,0 @@ -import RuleEngine from "../RuleEngine"; -import autoBind from 'auto-bind' -import commandTemplates from './commands' - -/** - * TODO: - * - Convert commands, templates, and object/door/room names to lower case for easier comparison - */ - -export default class Parser { - constructor() { - this.callbacks = {} - this.gameState = null - this.engine = null - this.commands = commandTemplates.map(this._convertCommandFromTemplate) - - autoBind(this) - } - - setEngine(engine) { - this.engine = engine; - } - - setGameState(gameState) { - this.gameState = gameState - } - - afterCommand(callback) { - this.callbacks.afterCommand = callback - } - - handleCommand(commandString) { - if(!this.engine) - throw new Error('Parser has no command engine') - - if(!this.gameState) - throw new Error('Parser has no state container') - - // Get all possible ways to fit the command string into our defined commands - const potentialParsings = this._parseCommandString(commandString) - - // Filter out potential parsings that are invalid - const validParsings = this.gameState.filterCommandParsingsByValidNouns(potentialParsings) - console.log(validParsings) - // (unknown objects, invalid directions, etc) - - if(validParsings.length > 1) - throw new Error(`Multiple ways to parse command "${commandString}"`) - - if(validParsings.length < 1) - throw new Error(`Unknown command`) - - const [command] = validParsings - const {verb, tokens} = command - const subject = tokens.filter(({type, position}) => (type === 'expression' && position === 'subject'))[0] - const object = tokens.filter(({type, position}) => (type === 'expression' && position === 'object' ))[0] - - const action = {type: 'player', verb, subject, object}; - action.getSubject = () => subject.id - action.getObject = () => object.id - - console.log(action) - this._processAction(action) - } - - start() { - this._processAction({type: 'internal', verb: 'playStarted'}) - } - - _processAction(action) { - // Pass to actions - const draftState = this.gameState.getDraft() - const {messages, resultCode} = this.engine.run(action, draftState) - - if(resultCode === RuleEngine.SUCCESS) { - this.gameState.sealState(draftState) - } - - if(this.callbacks.afterCommand) - this.callbacks.afterCommand(messages) - else - console.error('Parser has no afterCommand callback registered') - - } - - _convertCommandFromTemplate({verb, template}) { - const expressionRegex = /^\[([a-z|]+)\]$/ - - const tokens = template.split(' ').map(token => { - if(!token.includes('[') && !token.includes(']')) - return {type: 'literal', word: token} - - if(expressionRegex.test(token)) { - let expressionType = token.match(expressionRegex)[1] - let expressionPosition = 'subject' - - if(expressionType.includes('|')) { - const parts = expressionType.split('|') - if(parts.length !== 2) - throw new Error(`Error parsing expression token "${token}": Too many | symbols`) - - expressionType = parts[0]; - expressionPosition = parts[1]; - } - return {type: 'expression', name: expressionType, position: expressionPosition} - } - - throw new Error(`Unknown token "${token}"`) - }) - - // Check that we do not have two object expressions with the same position - const nounPositions = {} - for(const expression of tokens.filter(token => token.type === 'expression')){ - if(nounPositions[expression.position]) - throw new Error(`Error parsing command template "${template}" - more than one ${expression.position} expression`) - nounPositions[expression.position] = expression.position - } - - return {verb, tokens} - } - - _parseCommandString(commandString) { - const words = commandString.split(' ').filter(chunk => chunk !== '') - - let parsings = [] - for(const command of this.commands){ - let potentialParsings = this._attemptParseForCommand(command.tokens, words) - parsings = [...parsings, ...potentialParsings.map(parsing => ({ - verb: command.verb, - tokens: parsing - }))] - } - - return parsings - } - - // Returns array of parsings - _attemptParseForCommand(commandTokens, words) { - // The only way to "parse" no words into no tokens is to have an empty array - if(commandTokens.length < 1 && words.length < 1) - return [ [] ] - - // If we reached the end of one but not the other, we have no possible parsings - if(commandTokens.length < 1 || words.length < 1) - return [] - - let nextToken = commandTokens[0] - - if(nextToken.type === 'literal') { - let nextWord = words[0] - - // If literal word match - if(nextWord === nextToken.word) { - // Try to parse the remaining sentence, and prepend this to the front - // of all possible parsings of the rest of the sentence - const parsingsOfRemainder = this._attemptParseForCommand(commandTokens.slice(1), words.slice(1)) - - const parsingsOfCommand = parsingsOfRemainder.map(remainderParsing => [ - {type: 'literal', word: nextWord}, - ...remainderParsing - ]) - - return parsingsOfCommand - } - // If the word doesn't match, we have no way to parse this, we have no possible - // parsings. Return empty array - else - return [] - - } else if (nextToken.type === 'expression') { - let parsings = [] - - // For all possible lengths of words we could capture in this expression - for(let n = 1; n <= words.length; n++) { - // Figure out what would be in the expression - const potentialExpressionParse = words.slice(0, n).join(' ') - const remainingWords = words.slice(n) - - // Attempt to parse the remainder of the command - const parsingsOfRemainder = this._attemptParseForCommand(commandTokens.slice(1), remainingWords) - const parsingsOfCommand = parsingsOfRemainder.map(remainderParsing => [ - {type: 'expression', name: nextToken.name, position: nextToken.position, value: potentialExpressionParse}, - ...remainderParsing - ]) - - // Add command parsings to the array we've been building - parsings = [...parsings, ...parsingsOfCommand] - } - - return parsings - } else { - throw new Error(`Unknown token type "${nextToken.type}"`) - } - } -} diff --git a/src/RuleEngine/index.js b/src/RuleEngine/index.js deleted file mode 100644 index 6848cda..0000000 --- a/src/RuleEngine/index.js +++ /dev/null @@ -1,87 +0,0 @@ -import autoBind from 'auto-bind' - -import coreRules from './rules/core' -import gameRules from './rules/game' - -export default class RuleEngine { - constructor() { - this.rules = [] - - autoBind(this) - - this.defineRules(coreRules) - this.defineRules(gameRules) - } - - defineRule({type, location, verb, subject, object, filter, hooks}) { - this.rules.push({ - type, location, verb, subject, object, filter, hooks - }) - } - - defineRules(rules) { - rules.forEach(this.defineRule) - } - - run(action, state) { - if(!this.rules.length) - throw new Error('Rules engine has no rules') - - const applicableRules = this.rules.filter(rule => { - if(rule.type && !testType(state, action, rule)) return false; - if(rule.location && !testLocation(state, action, rule)) return false; - if(rule.verb && !testVerb(state, action, rule)) return false; - if(rule.subject && !testSubject(state, action, rule)) return false; - if(rule.object && !testObject(state, action, rule)) return false; - - return true; - }) - - let messages = [] - - try { - console.log('executing before hooks') - const beforeHooks = applicableRules.map(rule => rule.hooks?.before).filter(a => a !== undefined) - runHooks(beforeHooks, messages, state, action) - - console.log('executing during hooks') - const carryOut = applicableRules.map(rule => rule.hooks?.carryOut).filter(a => a !== undefined) - runHooks(carryOut, messages, state, action) - - console.log('executing after hooks') - const afterHooks = applicableRules.map(rule => rule.hooks?.after).filter(a => a !== undefined) - runHooks(afterHooks, messages, state, action) - } catch (err) { - console.error('Rules stopped') - console.error(err.message) - return {messages, resultCode: RuleEngine.FAILURE} - } - - return {messages, resultCode: RuleEngine.SUCCESS} - } -} - -RuleEngine.SUCCESS = 'success' -RuleEngine.FAILURE = 'failure' - -// TODO: Apply rules conditionally depending on state, location, verb, etc -function testType(state, action, rule) { return action.type === rule.type } -function testLocation(state, action, rule) { return true } -function testVerb(state, action, rule) { return true } -function testSubject(state, action, rule) { return true } -function testObject(state, action, rule) { return true } - -function runHooks(hooks, messages, state, action) { - for(const hook of hooks){ - try { - // Hook gets state, action, and "say" function - hook(state, action, messages.push.bind(messages)) - - } catch (err) { - // If error has messages property append that as well - if(err.message) - messages.push(err.message) - throw err - } - } -} diff --git a/src/RuleEngine/rules/core.js b/src/RuleEngine/rules/core.js deleted file mode 100644 index f1147e0..0000000 --- a/src/RuleEngine/rules/core.js +++ /dev/null @@ -1,33 +0,0 @@ -import printArea from '../../utils/printArea' -// import {current} from 'immer' - -let pastArea - -export default [{ - hooks: { - // After the player's location changes, print the new area's description - after: (state, action, say) => { - const {location} = state.player - if(location !== pastArea) { - printArea(state.locations[location], say) - pastArea = location - } - } - } -},{ - type: 'player', - verb: 'take', - hooks: { - carryOut: (state, action, say) => { - const item = state.items[action.getSubject()] - const player = state.player - - if(item.location !== player.location) { - say(`You cannot see the ${action.subject.value}`) - } else { - item.location = 'inventory' - say('Taken.') - } - } - } -}] diff --git a/src/RuleEngine/rules/game.js b/src/RuleEngine/rules/game.js deleted file mode 100644 index ccb7044..0000000 --- a/src/RuleEngine/rules/game.js +++ /dev/null @@ -1,48 +0,0 @@ -export default [{ - type: 'internal', - verb: 'playStarted', - hooks: { - carryOut: (state, action, say) => { - state.player.location = 'entry' - state.locations['entry'] = { - id: 'entry', - type: 'room', - name: 'Entry Hall', - description: `A quaint little hall at the entry to the house.`, - neighbors: { - east: 'lockedDoor' - } - } - - state.locations['safe'] = { - id: 'safe', - type: 'room', - name: 'Safe', - description: 'A large walk-in safe.', - neighbors: { - west: 'lockedDoor' - } - } - - state.locations['lockedDoor'] = { - id: 'lockedDoor', - type: 'door', - name: 'white door', - description: 'A faded white door with an old brass handle.', - locked: true, - key: 'brass key', - neighbors: { - west: 'entry', - east: 'safe' - } - } - - state.items['brass key'] = { - id: 'brass key', - name: 'brass key', - description: 'A heavy brass key', - location: 'safe' - } - } - } -}] diff --git a/src/engine/Game.ts b/src/engine/Game.ts index bf618b9..1d0383d 100644 --- a/src/engine/Game.ts +++ b/src/engine/Game.ts @@ -29,12 +29,12 @@ export default class Game { constructor() { console.log('adding directions') let state = this.getState() - state.directions.set('north', {type: ObjectType.Direction, name: 'north', aliases: ['n']}) - state.directions.set('east', {type: ObjectType.Direction, name: 'east', aliases: ['e']}) - state.directions.set('south', {type: ObjectType.Direction, name: 'south', aliases: ['s']}) - state.directions.set('west', {type: ObjectType.Direction, name: 'west', aliases: ['w']}) - state.directions.set('up', {type: ObjectType.Direction, name: 'up', aliases: ['u']}) - state.directions.set('down', {type: ObjectType.Direction, name: 'down', aliases: ['d']}) + state.directions.set('north', {type: ObjectType.Direction, name: 'north', printableName: 'north', aliases: ['n']}) + state.directions.set('east', {type: ObjectType.Direction, name: 'east', printableName: 'east', aliases: ['e']}) + state.directions.set('south', {type: ObjectType.Direction, name: 'south', printableName: 'south', aliases: ['s']}) + state.directions.set('west', {type: ObjectType.Direction, name: 'west', printableName: 'west', aliases: ['w']}) + state.directions.set('up', {type: ObjectType.Direction, name: 'up', printableName: 'up', aliases: ['u']}) + state.directions.set('down', {type: ObjectType.Direction, name: 'down', printableName: 'down', aliases: ['d']}) this.saveDraft() } @@ -122,7 +122,9 @@ export default class Game { state.items.set(item.name, item) } - findObjectByName(name : string, type : ObjectType) : GameObject | null { + findObjectByName(name : string | undefined | null, type : ObjectType) : GameObject | null { + if(!name) return null; + let collection switch(type) { case ObjectType.Door: @@ -180,6 +182,7 @@ export default class Game { case ObjectType.Item: return state.player.location === (object as Item).location + || (object as Item).location === 'inventory' default: return false diff --git a/src/engine/Parser.ts b/src/engine/Parser.ts index 663c193..64bf82d 100644 --- a/src/engine/Parser.ts +++ b/src/engine/Parser.ts @@ -3,6 +3,7 @@ import RulesEngine from './RulesEngine' import ParsedCommand, { InvalidCommandDetails } from "./types/ParsedCommand"; import Verb from './types/Verb'; import VerbBuilder from "./types/VerbBuilder"; +import { game } from "."; export default class Parser { private game : Game @@ -14,9 +15,24 @@ export default class Parser { this.engine = engine } - handleCommand(rawCommand : string) { + handlePlayerCommand(rawCommand : string) { this.game.outputCommand(rawCommand) + try { + this.runCommand(rawCommand) + } catch (err) { + if(typeof err === 'string') + game.say(err) + else if(err.message) + game.say(err.message) + else{ + game.say('An unknown error occured') + console.error(err) + } + } + this.game.saveDraft() + } + runCommand(rawCommand : string) { // Parse command for syntactical validity // (according to known verb templates) const grammaticalParsings : ParsedCommand[] = this.parseCommandString(rawCommand) @@ -30,10 +46,6 @@ export default class Parser { } else { this.engine.runCommand(validationResult.validCommands[0]) } - - this.game.saveDraft() - - console.log(validationResult) } parseCommandString(rawCommand: string): ParsedCommand[] { @@ -51,15 +63,13 @@ export default class Parser { console.log(invalidCommands) if(!invalidCommands.length){ - this.game.say("I'm unsure what you're trying to do") - return + throw new Error("I'm unsure what you're trying to do") } const mostValid = invalidCommands[0] if(mostValid.command.verb.name === 'go'){ - this.game.say("I'm unsure what you're trying to do") - return + throw new Error("I'm unsure what you're trying to do") } this.game.say(mostValid.reason) diff --git a/src/engine/Renderer.tsx b/src/engine/Renderer.tsx index 8362734..815a9a2 100644 --- a/src/engine/Renderer.tsx +++ b/src/engine/Renderer.tsx @@ -14,6 +14,7 @@ export default class Renderer { private game : Game private rules : RulesEngine private output : GameEvent[] = [] + private target : HTMLElement | null = null constructor(parser : Parser, game : Game, rules : RulesEngine) { this.parser = parser @@ -21,7 +22,8 @@ export default class Renderer { this.rules = rules } - start() { + start(target : HTMLElement | null) { + this.target = target this.rules.gameStart() this.render() } @@ -30,15 +32,18 @@ export default class Renderer { this.output.push(new GameEventCommand(command)) this.render() - this.parser.handleCommand(command) + this.parser.handlePlayerCommand(command) } private render() { + if(!this.target) + throw new Error("Renderer error: target is null") + ReactDOM.render( , - document.getElementById('root') + this.target ) } } diff --git a/src/engine/RulesEngine.ts b/src/engine/RulesEngine.ts index 33b449e..3983ebd 100644 --- a/src/engine/RulesEngine.ts +++ b/src/engine/RulesEngine.ts @@ -36,9 +36,7 @@ export default class RulesEngine extends EventEmitter{ // since that would maybe allow us to _cancel_ actions? runCommand(action: ValidCommandDetails) { this.emit('beforeCommand', action, this.game) - - console.log('doing action') - + this.emit('command', action) this.emit('afterCommand', action, this.game) } @@ -46,4 +44,10 @@ export default class RulesEngine extends EventEmitter{ onBeforeCommand = (cb : (command : ValidCommandDetails, game : Game) => void) => this.on('beforeCommand', cb) onAfterCommand = (cb : (command : ValidCommandDetails, game : Game) => void) => this.on('afterCommand', cb) onLocationChange = (cb : (game : Game) => void) => this.on('locationChange', cb) + + + onCommand = (type : string, cb : (command : ValidCommandDetails) => void ) => this.on('command', (command : ValidCommandDetails) => { + if(command.verb.name === type) + cb(command) + }) } diff --git a/src/engine/definitions/go.ts b/src/engine/definitions/go.ts new file mode 100644 index 0000000..9989dd3 --- /dev/null +++ b/src/engine/definitions/go.ts @@ -0,0 +1,47 @@ +import Parser from "../Parser"; +import RulesEngine from "../RulesEngine"; +import Game from "../Game"; +import { Direction, ObjectType, Door } from "../types/GameState"; + +export default function(parser : Parser, rules : RulesEngine, game : Game) { + parser.understand('go') + .as('go [direction]') + .as('[direction]') + + rules.onCommand('go', command => { + const direction = command.subject as Direction + const current = game.getCurrentRoom() + + const neighborName = current?.neighbors.get(direction.name) + if(!neighborName) + game.say(`You cannot go to the ${direction.name}`) + + let lookingAt = game.findObjectByName(neighborName, ObjectType.Room) + || game.findObjectByName(neighborName, ObjectType.Door) + + if(!lookingAt){ + console.warn(`Unable to find object ${neighborName}`) + game.say(`You cannot go to the ${direction.name}`) + return; + } + + let room = lookingAt + if(lookingAt.type === ObjectType.Door) { + + if(!(lookingAt as Door).open) { + game.say(`(First opening the ${lookingAt.name})`) + parser.runCommand(`open ${lookingAt.name}`) + } + + const nextNeighborName = (lookingAt as Door).neighbors.get(direction.name) + const nextNeighbor = game.findObjectByName(nextNeighborName, ObjectType.Room) + + if(!nextNeighbor) + throw new Error(`Door ${lookingAt.name} does not lead anywhere to the ${direction.name}`) + else + room = nextNeighbor + } + + game.getState().player.location = room.name + }) +} \ No newline at end of file diff --git a/src/engine/definitions/index.js b/src/engine/definitions/index.js new file mode 100644 index 0000000..a2517c8 --- /dev/null +++ b/src/engine/definitions/index.js @@ -0,0 +1,9 @@ +export default [ + require('./look'), + require('./lookDirection'), + require('./lookAt'), + require('./go'), + require('./open'), + require('./unlockDoor'), + require('./take') +] \ No newline at end of file diff --git a/src/engine/definitions/look.ts b/src/engine/definitions/look.ts new file mode 100644 index 0000000..2dd5a19 --- /dev/null +++ b/src/engine/definitions/look.ts @@ -0,0 +1,16 @@ +import Parser from "../Parser"; +import RulesEngine from "../RulesEngine"; +import Game from "../Game"; +import { ValidCommandDetails } from "../types/ParsedCommand"; +import printArea from "../../utils/printArea"; + +export default function(parser : Parser, rules : RulesEngine, game : Game) { + parser.understand('look') + .as('look') + .as('describe') + .as('l') + + rules.onCommand('look', (command : ValidCommandDetails) => { + printArea(game.getCurrentRoom(), game.say.bind(game)) + }) +} \ No newline at end of file diff --git a/src/engine/definitions/lookAt.ts b/src/engine/definitions/lookAt.ts new file mode 100644 index 0000000..b1a9c89 --- /dev/null +++ b/src/engine/definitions/lookAt.ts @@ -0,0 +1,26 @@ +import Parser from "../Parser"; +import RulesEngine from "../RulesEngine"; +import Game from "../Game"; + +export default function(parser : Parser, rules : RulesEngine, game : Game) { + 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]') + .as('l [item]') + .as('l at [item]') + + rules.onCommand('lookAt', command => { + const subject = command.subject! + + if(subject.description) + game.say(subject.description) + else + game.say(`You see nothing remarkable about the ${subject.name}`) + }) +} \ No newline at end of file diff --git a/src/engine/definitions/lookDirection.ts b/src/engine/definitions/lookDirection.ts new file mode 100644 index 0000000..53cc198 --- /dev/null +++ b/src/engine/definitions/lookDirection.ts @@ -0,0 +1,29 @@ +import Parser from "../Parser"; +import RulesEngine from "../RulesEngine"; +import Game from "../Game"; +import { Direction, ObjectType } from "../types/GameState"; + +export default function(parser : Parser, rules : RulesEngine, game : Game) { + parser.understand('lookDirection') + .as('look [direction]') + .as('l [direction]') + + rules.onCommand('lookDirection', command => { + const direction = command.subject as Direction + const current = game.getCurrentRoom() + + const lookingAtName = current?.neighbors.get(direction.name) + if(!lookingAtName) + game.say(`There is nothing to the ${direction.name}`) + + let lookingAt = game.findObjectByName(lookingAtName!, ObjectType.Room) + || game.findObjectByName(lookingAtName!, ObjectType.Door) + + if(!lookingAt){ + console.warn(`Unable to find object ${lookingAtName}`) + return; + } + + game.say(`To the ${direction.name} you see the ${lookingAt.printableName}`) + }) +} \ No newline at end of file diff --git a/src/engine/definitions/open.ts b/src/engine/definitions/open.ts new file mode 100644 index 0000000..8091678 --- /dev/null +++ b/src/engine/definitions/open.ts @@ -0,0 +1,26 @@ +import Parser from "../Parser"; +import RulesEngine from "../RulesEngine"; +import Game from "../Game"; +import { Direction, ObjectType, Door } from "../types/GameState"; +import { Draft } from "immer"; + +export default function(parser : Parser, rules : RulesEngine, game : Game) { + parser.understand('openDoor') + .as('open [door]') + + rules.onCommand('openDoor', command => { + const door = command.subject as Door + + if(door.open) { + game.say(`The ${door.name} is already open`) + return + } + + if(door.locked) { + throw new Error(`The ${door.name} is locked.`) + } + + const mutable = game.findObjectByName(door.name, ObjectType.Door); + (mutable as Draft).open = true + }) +} \ No newline at end of file diff --git a/src/engine/definitions/take.ts b/src/engine/definitions/take.ts new file mode 100644 index 0000000..d5ace40 --- /dev/null +++ b/src/engine/definitions/take.ts @@ -0,0 +1,26 @@ +import Parser from "../Parser"; +import RulesEngine from "../RulesEngine"; +import Game from "../Game"; +import { Direction, ObjectType, Door, Item } from "../types/GameState"; +import { Draft } from "immer"; + +export default function(parser : Parser, rules : RulesEngine, game : Game) { + parser.understand('take') + .as('take [item]') + .as('get [item]') + .as('pick up [item]') + .as('grab [item]') + .as('snatch [item]') + .as('steal [item]') + + + rules.onCommand('take', command => { + const item = command.subject as Draft + + if(item.location !== game.getState().player.location) + throw new Error(`You cannot see the ${item.name}`) + + item.location = 'inventory' + game.say('Taken.') + }) +} \ No newline at end of file diff --git a/src/engine/definitions/unlockDoor.ts b/src/engine/definitions/unlockDoor.ts new file mode 100644 index 0000000..ae22c4f --- /dev/null +++ b/src/engine/definitions/unlockDoor.ts @@ -0,0 +1,31 @@ +import Parser from "../Parser"; +import RulesEngine from "../RulesEngine"; +import Game from "../Game"; +import { Direction, ObjectType, Door, Item } from "../types/GameState"; +import { Draft } from "immer"; + +export default function(parser : Parser, rules : RulesEngine, game : Game) { + parser.understand('unlockDoor') + .as('unlock [door|subject] with [item|object]') + .as('unlock [door]') + .as('use [item|object] to unlock [door|subject]') + .as('use [item|object] on [door|subject]') + .as('open [door|subject] with [item|object]') + + rules.onCommand('unlockDoor', command => { + if(!command.object) + throw new Error(`Please specify what you would like to unlock the ${command.subject!.name} with`) + + const door = command.subject as Draft + const key = command.object as Item + + if(key.location !== 'inventory') + throw new Error(`You do not have the ${key.name}.`) + + if(door.key !== key.name) + throw new Error(`The ${key.name} doesn't fit.`) + + door.locked = false + game.say(`With a sharp **click** the ${door.name} unlocks.`) + }) +} \ No newline at end of file diff --git a/src/engine/index.ts b/src/engine/index.ts new file mode 100644 index 0000000..434aa90 --- /dev/null +++ b/src/engine/index.ts @@ -0,0 +1,14 @@ +import Game from './Game' +import Parser from './Parser' +import Renderer from './Renderer' +import RulesEngine from './RulesEngine' +import definitions from './definitions' + +export const game = new Game() +export const rules = new RulesEngine(game) +export const parser = new Parser(game, rules) +export const renderer = new Renderer(parser, game, rules) + +for(const {default: func} of definitions) { + func(parser, rules, game) +} diff --git a/src/engine/types/GameState.ts b/src/engine/types/GameState.ts index f4e5a2d..ff772e1 100644 --- a/src/engine/types/GameState.ts +++ b/src/engine/types/GameState.ts @@ -26,7 +26,7 @@ export type GameObject = { readonly type : ObjectType, readonly name : ObjectID, readonly aliases : string[], - readonly printableName?: string | undefined, + readonly printableName: string | undefined, readonly description?: string } @@ -41,9 +41,10 @@ export type Room = GameObject & { export type Door = GameObject & { readonly type : ObjectType.Door, - readonly neighbors : Map + readonly neighbors : Map, readonly locked : boolean, - readonly key : ObjectID + readonly key : ObjectID, + readonly open: boolean } export type Item = GameObject & { diff --git a/src/index.tsx b/src/index.tsx index bb63977..57145e2 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,73 +1,40 @@ -import Game from './engine/Game' -import Parser from './engine/Parser' -import Renderer from './engine/Renderer' -import RulesEngine from './engine/RulesEngine' +import {game, renderer} from './engine/' import { ObjectType, Room, Door } from './engine/types/GameState' -let game = new Game() -let rules = new RulesEngine(game) -let parser = new Parser(game, rules) -let renderer = new Renderer(parser, game, rules) - -parser.understand('look') - .as('look') - .as('describe') - -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]') - -parser.understand('take') - .as('take [item]') - .as('get [item]') - .as('pick up [item]') - .as('grab [item]') - .as('snatch [item]') - .as('steal [item]') - -parser.understand('unlockDoor') - .as('unlock [door|subject] with [item|object]') - .as('unlock [door]') - .as('use [item|object] to unlock [door|subject]') - -parser.understand('openDoor') - .as('open [door]') - .as('open [door|subject] with [item|object]') - const entry : Room = { type: ObjectType.Room, name: 'entry', + printableName: 'entry', aliases: [], neighbors: new Map(), description: 'A tight corridor with yellow faded walls.' } +const closet : Room = { + type: ObjectType.Room, + name: 'closet', + printableName: 'closet', + aliases: [], + neighbors: new Map(), + description: 'A small closet' +} + const door : Door = { type: ObjectType.Door, name: 'door', + printableName: 'white door', aliases: ['white door'], neighbors: new Map(), locked: true, key: 'brass key', - description: 'A large white door with but a single keybole.' + description: 'A large white door with but a single keybole.', + open: false } const office : Room = { type: ObjectType.Room, name: 'office', + printableName: 'office', aliases: [], neighbors: new Map(), description: 'An opulent office' @@ -77,13 +44,17 @@ entry.neighbors.set('east', 'door') office.neighbors.set('west', 'door') door.neighbors.set('east', 'office') door.neighbors.set('west', 'entry') +entry.neighbors.set('west', 'closet') +closet.neighbors.set('east', 'entry') game.addRoom(entry) game.addRoom(office) +game.addRoom(closet) game.addDoor(door) game.addItem({ type: ObjectType.Item, + printableName: 'brass key', name: 'brass key', aliases: ['key'], location: 'entry' @@ -91,6 +62,7 @@ game.addItem({ game.addItem({ type: ObjectType.Item, + printableName: 'gem', name: 'ruby', aliases: ['gem'], location: 'office' @@ -101,4 +73,4 @@ game.getState().player.location = 'entry' game.saveDraft() -renderer.start() +renderer.start(document.getElementById('root')) diff --git a/src/utils/printArea.js b/src/utils/printArea.js index 713d757..5424347 100644 --- a/src/utils/printArea.js +++ b/src/utils/printArea.js @@ -1,6 +1,6 @@ import capitalize from "./capitalize" export default function printArea(location, say) { - say(`**${location.printableName || capitalize(location.name)}**`) + say(`**${capitalize(location.printableName)}**`) say(location.description) }