Typescript conversion complete - implemented picking up items, unlocking, opening, going, looking
parent
7e1b423b55
commit
717446b25e
@ -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'
|
||||
}
|
||||
}
|
||||
}
|
||||
}]
|
@ -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)
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
Loading…
Reference in New Issue