configureDefaults = (story)->
player = story.player
player.addVerb "drop", (sentence)-> player.drop(sentence)
player.addVerb "go", (sentence)-> player.go(sentence)
player.addVerb "inventory", (sentence)-> player.listInventory(sentence)
player.addVerb "take", (sentence)-> player.take(sentence)
story.addVerb "look", (sentence)-> story.look(sentence)
story.addVerb "restart", -> story.reset()
########################################################################################################################
class Actor
constructor: (story)->
@_verbs = {}
@story = story or this
# Public Methods ###############################################################################
addVerb: (verbs..., action)->
for verb in verbs
@_verbs[verb] = action
@story.parser.addVerb(verb)
can: (verb)->
return !! @_verbs[verb]
do: (sentence)->
if @can(sentence.verb)
@_verbs[sentence.verb](sentence)
return true
return false
reset: ->
@_verbs = {}
########################################################################################################################
class GameLog
constructor: (onChange=(->))->
@content = ""
@onChange = onChange
clear: ->
@content = ""
@onChange(this)
echoInput: (text)->
@content += "
> " + text + "
"
@onChange(this)
write: (text="")->
@content += text.replace(/\n/g, "
")
@onChange(this)
writeln: (text="")->
@write(text + "\n")
########################################################################################################################
class Inventory
constructor: ->
@clear()
# Properties ###################################################################################
Object.defineProperties @prototype,
items:
get: -> return (item for name, item of @_items)
length:
get: -> return @_length
# Public Methods ###############################################################################
add: (item)->
if not @_items[item.name]
@_items[item.name] = item
@_length += 1
all: ->
return (item for name, item of @_items)
clear: ->
@_items = {}
@_length = 0
describe: (options={})->
options.simple ?= false
result = []
for name, item of @_items
if options.simple
result.push(item.name)
else
result.push("There is a #{item.name} here.")
return result.join("\n")
eachItem: (withItem=(->))->
for name, item of @_items
withItem(item)
has: (item)->
return !! @_items[item.name]
remove: (itemToRemove)->
if @_items[itemToRemove.name]
delete @_items[itemToRemove.name]
@_length -= 1
########################################################################################################################
class Item extends Actor
constructor: (story, name, options={})->
options.fixed ?= false
super(story)
@description = "non-descript item"
@fixed = options.fixed
@name = name
# Public Methods ###############################################################################
take: ->
if @fixed
@story.log.writeln("You cannot take the #{@name}.")
return false
else
@story.log.writeln("Taken.")
return true
describe: ->
return @description
toString: ->
return @name
########################################################################################################################
class Location extends Actor
constructor: (story, name)->
super(story)
@description = "Non-descript Place"
@destinations = {}
@inventory = new Inventory()
@name = name
@transitions = []
@visited = false
# Public Methods ###############################################################################
addTransition: (direction, toLocation, locked=false)->
@transitions[direction] = new Transition(direction, toLocation, locked)
addItem: (item)->
@inventory.add(item)
describe: (options={})->
options.verbose ?= false
result = [@name]
if (not @visited) or options.verbose
result.push("\n")
result.push(@description)
console.log("@inventory.length: #{@inventory.length}")
if @inventory.length > 0
result.push("")
result.push(@inventory.describe())
return result.join("\n")
getTransitionTo: (location)->
for direction, transition of @transitions
if transition.toLocation is location
return transition
return undefined
removeItem: (item)->
@inventory.remove(item)
reset: ->
super()
@inventory.clear()
toString: ->
return @name
########################################################################################################################
class ParseError extends Error
########################################################################################################################
class Parser
constructor: (story)->
@story = story
@reset()
addAliases: (aliasMap)->
for alias, meaning of aliasMap
@aliases[alias] = meaning
addDirections: (words...)->
for word in words
@directions.push(word)
addFillerWords: (words...)->
for word in words
@fillerWords.add(word)
addVerb: (verb)->
@verbs[verb] = verb
interpret: (userInput)->
sentence = new Sentence
for rawWord in userInput.split(/\s\s*/)
rawWord = @_resolveAliases(rawWord)
if @_useAsFiller(rawWord, sentence) then continue
if @_useAsVerb(rawWord, sentence) then continue
if @_useAsItem(rawWord, sentence) then continue
if @_useAsLocation(rawWord, sentence) then continue
throw new ParseError("I'm not sure what you meant by #{rawWord}... can you re-phrase that?")
@_normalizeSentence(sentence)
@_validateSentence(sentence)
return sentence
reset: ->
@aliases = {}
@directions = []
@fillerWords = new Set()
@verbs = {}
# Private Methods ##############################################################################
_normalizeSentence: (sentence)->
if sentence.has(verb: 0, location: 1)
sentence.addWord(new WordToken("go", "verb"))
return sentence
_resolveAliases: (rawWord)->
meaning = @aliases[rawWord]
if meaning
return @_resolveAliases(meaning)
return rawWord
_useAsFiller: (rawWord)->
return @fillerWords.has(rawWord)
_useAsItem: (rawWord, sentence)->
candidates = {}
considerItem = (item)->
if item.name.indexOf(rawWord) isnt -1
candidate = candidates[item.name] ?= {item: item; count: 0}
candidate.count += 1
@story.player.inventory.eachItem(considerItem)
@story.currentLocation.inventory.eachItem(considerItem)
candidates = (item for name, item of candidates)
return false if candidates.length is 0
highestCount = 0
mostPopular = []
for candidate in candidates
if candidate.count is highestCount
mostPopular.push(candidate)
else if candidate.count > highestCount
highestCount = candidate.count
mostPopular = [candidate]
if mostPopular.length > 1
throw new ParseError(
"I'm not sure what you meant by #{rawWord}... which did you mean: #{mostPopular.join(", ")}"
)
sentence.addWord(new WordToken(rawWord, "item", mostPopular[0].item))
return true
_useAsVerb: (rawWord, sentence)->
if not @verbs[rawWord]
return false
sentence.addWord(new WordToken(rawWord, "verb"))
return true
_useAsLocation: (rawWord, sentence)->
candidates = new Set()
for directionWord in @directions
if rawWord is directionWord
transition = @story.currentLocation.transitions[directionWord]
if transition
candidates.add(transition.toLocation)
else
throw new ParseError("You can't go #{rawWord} from here.")
for direction, transition of @story.currentLocation.transitions
if transition.toLocation.name.indexOf(rawWord) isnt -1
candidates.add(transition.toLocation)
if @story.currentLocation.name.indexOf(rawWord) isnt -1
candidates.add(@story.currentLocation)
candidates = Array.from(candidates)
if candidates.length > 1
throw new ParseError("I'm not sure what you meant by #{rawWord}... did you mean: #{candidates.join(", ")}")
else if candidates.length is 0
return false
if candidates[0] is @story.currentLocation
throw new ParseError("You're already there!")
sentence.addWord(new WordToken(rawWord, "location", candidates[0]))
return true
_validateSentence: (sentence)->
if sentence.has(verb: 0)
throw new ParseError("I'm not sure what you wanted to do there.")
########################################################################################################################
class Player extends Actor
constructor: (story)->
super(story)
@inventory = new Inventory()
@onChange = (->)
# Properties ###################################################################################
Object.defineProperties @prototype,
score:
get: ->
return @_score
set: (value)->
@_score = value
@onChange(this)
# Public Methods ###############################################################################
drop: (sentence)->
items = sentence.items
if items.length is 0
items = @inventory.all()
for item in items
if @inventory.has(item)
@inventory.remove(item)
@story.currentLocation.inventory.add(item)
@story.log.writeln("Dropped #{item.name}.")
else
@story.log.writeln("You're not holding a #{item.name}.")
go: (sentence)->
if not sentence.location
throw new ParseError("I'm not sure where you want to go...")
location = sentence.location
transition = @story.currentLocation.getTransitionTo(location)
if not transition
throw new ParseError("You can't get to #{location.name} from here.")
@story.arrive(location)
listInventory: (sentence)->
if @inventory.length is 0
@story.log.writeln("You're not carrying anything.")
else
@story.log.writeln("You have:")
@story.log.writeln(@inventory.describe(simple: true))
take: (sentence)->
items = sentence.items
localItems = @story.currentLocation.inventory
if items.length is 0
foundTakeableItem = true
for item in localItems.items
if not item.fixed
items.push(item)
if not foundTakeableItem
@story.log.writeln("There's nothing here you can take.")
_doTake = (item)=>
if localItems.has(item)
if item.take()
localItems.remove(item)
@inventory.add(item)
else
throw new ParseError("There isn't a #{item} here.")
if items.length > 1
for item in items
@story.log.write("#{item}: ")
_doTake(item)
else
_doTake(items[0])
reset: ->
super()
@inventory.clear()
@score = 0
########################################################################################################################
class Sentence
constructor: ->
@tokens = item: [], location: [], verb: []
# Properties ###################################################################################
Object.defineProperties @prototype,
"item":
get: -> return @tokens.item[0].referant
"items":
get: -> (i.referant for i in @tokens.item)
"location":
get: -> return @tokens.location[0].referant
"verb":
get: -> return @tokens.verb[0].rawText
# Public Methods ###############################################################################
addWord: (wordToken)->
@tokens[wordToken.type].push(wordToken)
has: (patternMap)->
patternMap ?= {}
for type, count of patternMap
if @tokens[type].length isnt count
return false
return true
toString: ->
return (
"{" +
"items: [#{@tokens.item.join(", ")}], " +
"locations: [#{@tokens.location.join(", ")}], " +
"verbs: [#{@tokens.verb.join(", ")}]" +
"}"
)
########################################################################################################################
class Story extends Actor
constructor: (title, onRestart=(->))->
super()
@log = new GameLog()
@onChange = (->)
@onRestart = onRestart
@parser = new Parser(this)
@player = new Player(this)
@title = title
# Properties ###################################################################################
Object.defineProperties @prototype,
"turns":
get: ->
return @_turns
set: (value)->
@_turns = value
@onChange(this)
# Configuration Methods ###########3############################################################
addItem: (name, options={})->
item = new Item(this, name, options)
item.story = this
@items.push(item)
return item
addLocation: (name)->
location = new Location(this, name)
@locations.push(location)
if @initialLocation is null
@initialLocation = location
return location
# Game Action Methods ##########################################################################
arrive: (location)->
@currentLocation = location
@log.writeln(location.describe())
@log.writeln()
location.visited = true
interpret: (userInput)->
try
@log.echoInput(userInput)
@turns += 1
sentence = @parser.interpret(userInput)
if @do(sentence) then return
if @player.do(sentence) then return
if @currentLocation.do(sentence) then return
for item in @currentLocation.inventory.items
if item.do(sentence) then return
throw new ParseError("To be honest, I'm not sure how to #{sentence.verb} anything around here.")
catch e
if e instanceof ParseError
@log.writeln(e.message)
else
throw e
look: (sentence)->
if sentence.has(item: 1)
@story.log.writeln(sentence.item.describe(verbose: true))
else
@story.log.writeln(@story.currentLocation.describe(verbose: true))
reset: ->
super()
@currentLocation = null
@items = []
@locations = []
@turns = 0
@log.clear()
@parser.reset()
@player.reset()
@parser.addDirections(
"north", "northeast", "east", "southeast", "south", "southwest", "west", "northwest", "up", "down"
)
@parser.addFillerWords(
"a", "all", "an", "around", "at", "everything", "of", "the", "to"
)
@parser.addAliases({
"d": "down", "e": "east", "g": "go", "i": "inventory", "l": "look", "n": "north", "ne": "northeast",
"nw": "northwest", "s": "south", "se": "southeast", "sw": "southwest", "u": "up", "w": "west",
})
configureDefaults(this)
@onRestart()
@log.writeln(@title)
@log.writeln()
@arrive(@currentLocation)
########################################################################################################################
class Transition
constructor: (@direction, @toLocation)->
# do nothing
toString: ->
return "#{@direction} to #{@toLocation.name}"
########################################################################################################################
class WordToken
constructor: (@rawText, @type, @referant=undefined)->
# do nothing
toString: ->
return "{rawText: #{@rawText}, type: #{@type}, referant: #{@referant}}"
########################################################################################################################
window.Story = Story