Typescript conversion complete - implemented picking up items, unlocking, opening, going, looking

main
Ashelyn Dawn 4 years ago
parent 7e1b423b55
commit 717446b25e

6
package-lock.json generated

@ -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": { "@types/yargs": {
"version": "13.0.9", "version": "13.0.9",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-13.0.9.tgz", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-13.0.9.tgz",

@ -39,5 +39,8 @@
"last 1 firefox version", "last 1 firefox version",
"last 1 safari version" "last 1 safari version"
] ]
},
"devDependencies": {
"@types/webpack-env": "^1.15.2"
} }
} }

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

@ -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]')

@ -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}"`)
}
}
}

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

@ -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.')
}
}
}
}]

@ -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'
}
}
}
}]

@ -29,12 +29,12 @@ export default class Game {
constructor() { constructor() {
console.log('adding directions') console.log('adding directions')
let state = this.getState() let state = this.getState()
state.directions.set('north', {type: ObjectType.Direction, name: 'north', aliases: ['n']}) state.directions.set('north', {type: ObjectType.Direction, name: 'north', printableName: 'north', aliases: ['n']})
state.directions.set('east', {type: ObjectType.Direction, name: 'east', aliases: ['e']}) state.directions.set('east', {type: ObjectType.Direction, name: 'east', printableName: 'east', aliases: ['e']})
state.directions.set('south', {type: ObjectType.Direction, name: 'south', aliases: ['s']}) state.directions.set('south', {type: ObjectType.Direction, name: 'south', printableName: 'south', aliases: ['s']})
state.directions.set('west', {type: ObjectType.Direction, name: 'west', aliases: ['w']}) state.directions.set('west', {type: ObjectType.Direction, name: 'west', printableName: 'west', aliases: ['w']})
state.directions.set('up', {type: ObjectType.Direction, name: 'up', aliases: ['u']}) state.directions.set('up', {type: ObjectType.Direction, name: 'up', printableName: 'up', aliases: ['u']})
state.directions.set('down', {type: ObjectType.Direction, name: 'down', aliases: ['d']}) state.directions.set('down', {type: ObjectType.Direction, name: 'down', printableName: 'down', aliases: ['d']})
this.saveDraft() this.saveDraft()
} }
@ -122,7 +122,9 @@ export default class Game {
state.items.set(item.name, item) 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 let collection
switch(type) { switch(type) {
case ObjectType.Door: case ObjectType.Door:
@ -180,6 +182,7 @@ export default class Game {
case ObjectType.Item: case ObjectType.Item:
return state.player.location === (object as Item).location return state.player.location === (object as Item).location
|| (object as Item).location === 'inventory'
default: default:
return false return false

@ -3,6 +3,7 @@ import RulesEngine from './RulesEngine'
import ParsedCommand, { InvalidCommandDetails } from "./types/ParsedCommand"; import ParsedCommand, { InvalidCommandDetails } from "./types/ParsedCommand";
import Verb from './types/Verb'; import Verb from './types/Verb';
import VerbBuilder from "./types/VerbBuilder"; import VerbBuilder from "./types/VerbBuilder";
import { game } from ".";
export default class Parser { export default class Parser {
private game : Game private game : Game
@ -14,9 +15,24 @@ export default class Parser {
this.engine = engine this.engine = engine
} }
handleCommand(rawCommand : string) { handlePlayerCommand(rawCommand : string) {
this.game.outputCommand(rawCommand) 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 // Parse command for syntactical validity
// (according to known verb templates) // (according to known verb templates)
const grammaticalParsings : ParsedCommand[] = this.parseCommandString(rawCommand) const grammaticalParsings : ParsedCommand[] = this.parseCommandString(rawCommand)
@ -30,10 +46,6 @@ export default class Parser {
} else { } else {
this.engine.runCommand(validationResult.validCommands[0]) this.engine.runCommand(validationResult.validCommands[0])
} }
this.game.saveDraft()
console.log(validationResult)
} }
parseCommandString(rawCommand: string): ParsedCommand[] { parseCommandString(rawCommand: string): ParsedCommand[] {
@ -51,15 +63,13 @@ export default class Parser {
console.log(invalidCommands) console.log(invalidCommands)
if(!invalidCommands.length){ if(!invalidCommands.length){
this.game.say("I'm unsure what you're trying to do") throw new Error("I'm unsure what you're trying to do")
return
} }
const mostValid = invalidCommands[0] const mostValid = invalidCommands[0]
if(mostValid.command.verb.name === 'go'){ if(mostValid.command.verb.name === 'go'){
this.game.say("I'm unsure what you're trying to do") throw new Error("I'm unsure what you're trying to do")
return
} }
this.game.say(mostValid.reason) this.game.say(mostValid.reason)

@ -14,6 +14,7 @@ export default class Renderer {
private game : Game private game : Game
private rules : RulesEngine private rules : RulesEngine
private output : GameEvent[] = [] private output : GameEvent[] = []
private target : HTMLElement | null = null
constructor(parser : Parser, game : Game, rules : RulesEngine) { constructor(parser : Parser, game : Game, rules : RulesEngine) {
this.parser = parser this.parser = parser
@ -21,7 +22,8 @@ export default class Renderer {
this.rules = rules this.rules = rules
} }
start() { start(target : HTMLElement | null) {
this.target = target
this.rules.gameStart() this.rules.gameStart()
this.render() this.render()
} }
@ -30,15 +32,18 @@ export default class Renderer {
this.output.push(new GameEventCommand(command)) this.output.push(new GameEventCommand(command))
this.render() this.render()
this.parser.handleCommand(command) this.parser.handlePlayerCommand(command)
} }
private render() { private render() {
if(!this.target)
throw new Error("Renderer error: target is null")
ReactDOM.render( ReactDOM.render(
<React.StrictMode> <React.StrictMode>
<App game={this.game} onCommand={this.handleCommand.bind(this)}/> <App game={this.game} onCommand={this.handleCommand.bind(this)}/>
</React.StrictMode>, </React.StrictMode>,
document.getElementById('root') this.target
) )
} }
} }

@ -36,9 +36,7 @@ export default class RulesEngine extends EventEmitter{
// since that would maybe allow us to _cancel_ actions? // since that would maybe allow us to _cancel_ actions?
runCommand(action: ValidCommandDetails) { runCommand(action: ValidCommandDetails) {
this.emit('beforeCommand', action, this.game) this.emit('beforeCommand', action, this.game)
this.emit('command', action)
console.log('doing action')
this.emit('afterCommand', action, this.game) 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) onBeforeCommand = (cb : (command : ValidCommandDetails, game : Game) => void) => this.on('beforeCommand', cb)
onAfterCommand = (cb : (command : ValidCommandDetails, game : Game) => void) => this.on('afterCommand', cb) onAfterCommand = (cb : (command : ValidCommandDetails, game : Game) => void) => this.on('afterCommand', cb)
onLocationChange = (cb : (game : Game) => void) => this.on('locationChange', 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)
})
} }

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

@ -0,0 +1,9 @@
export default [
require('./look'),
require('./lookDirection'),
require('./lookAt'),
require('./go'),
require('./open'),
require('./unlockDoor'),
require('./take')
]

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

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

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

@ -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<Door>).open = true
})
}

@ -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<Item>
if(item.location !== game.getState().player.location)
throw new Error(`You cannot see the ${item.name}`)
item.location = 'inventory'
game.say('Taken.')
})
}

@ -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<Door>
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.`)
})
}

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

@ -26,7 +26,7 @@ export type GameObject = {
readonly type : ObjectType, readonly type : ObjectType,
readonly name : ObjectID, readonly name : ObjectID,
readonly aliases : string[], readonly aliases : string[],
readonly printableName?: string | undefined, readonly printableName: string | undefined,
readonly description?: string readonly description?: string
} }
@ -41,9 +41,10 @@ export type Room = GameObject & {
export type Door = GameObject & { export type Door = GameObject & {
readonly type : ObjectType.Door, readonly type : ObjectType.Door,
readonly neighbors : Map<ObjectID, ObjectID> readonly neighbors : Map<ObjectID, ObjectID>,
readonly locked : boolean, readonly locked : boolean,
readonly key : ObjectID readonly key : ObjectID,
readonly open: boolean
} }
export type Item = GameObject & { export type Item = GameObject & {

@ -1,73 +1,40 @@
import Game from './engine/Game' import {game, renderer} from './engine/'
import Parser from './engine/Parser'
import Renderer from './engine/Renderer'
import RulesEngine from './engine/RulesEngine'
import { ObjectType, Room, Door } from './engine/types/GameState' 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 = { const entry : Room = {
type: ObjectType.Room, type: ObjectType.Room,
name: 'entry', name: 'entry',
printableName: 'entry',
aliases: [], aliases: [],
neighbors: new Map(), neighbors: new Map(),
description: 'A tight corridor with yellow faded walls.' 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 = { const door : Door = {
type: ObjectType.Door, type: ObjectType.Door,
name: 'door', name: 'door',
printableName: 'white door',
aliases: ['white door'], aliases: ['white door'],
neighbors: new Map(), neighbors: new Map(),
locked: true, locked: true,
key: 'brass key', 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 = { const office : Room = {
type: ObjectType.Room, type: ObjectType.Room,
name: 'office', name: 'office',
printableName: 'office',
aliases: [], aliases: [],
neighbors: new Map(), neighbors: new Map(),
description: 'An opulent office' description: 'An opulent office'
@ -77,13 +44,17 @@ entry.neighbors.set('east', 'door')
office.neighbors.set('west', 'door') office.neighbors.set('west', 'door')
door.neighbors.set('east', 'office') door.neighbors.set('east', 'office')
door.neighbors.set('west', 'entry') door.neighbors.set('west', 'entry')
entry.neighbors.set('west', 'closet')
closet.neighbors.set('east', 'entry')
game.addRoom(entry) game.addRoom(entry)
game.addRoom(office) game.addRoom(office)
game.addRoom(closet)
game.addDoor(door) game.addDoor(door)
game.addItem({ game.addItem({
type: ObjectType.Item, type: ObjectType.Item,
printableName: 'brass key',
name: 'brass key', name: 'brass key',
aliases: ['key'], aliases: ['key'],
location: 'entry' location: 'entry'
@ -91,6 +62,7 @@ game.addItem({
game.addItem({ game.addItem({
type: ObjectType.Item, type: ObjectType.Item,
printableName: 'gem',
name: 'ruby', name: 'ruby',
aliases: ['gem'], aliases: ['gem'],
location: 'office' location: 'office'
@ -101,4 +73,4 @@ game.getState().player.location = 'entry'
game.saveDraft() game.saveDraft()
renderer.start() renderer.start(document.getElementById('root'))

@ -1,6 +1,6 @@
import capitalize from "./capitalize" import capitalize from "./capitalize"
export default function printArea(location, say) { export default function printArea(location, say) {
say(`**${location.printableName || capitalize(location.name)}**`) say(`**${capitalize(location.printableName)}**`)
say(location.description) say(location.description)
} }

Loading…
Cancel
Save