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)
}