diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..f31b3e2
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+.DS_Store
+*.swp
diff --git a/game_engine.coffee b/game_engine.coffee
new file mode 100644
index 0000000..8d649f1
--- /dev/null
+++ b/game_engine.coffee
@@ -0,0 +1,420 @@
+class GameLog
+
+ constructor: (onChange=(->))->
+ @content = ""
+ @onChange = onChange
+
+ echoInput: (text)->
+ @content += "
> " + text + "
"
+ @onChange(this)
+
+ writeln: (text="")->
+ @content += text.replace(/\n/g, "
") + "
"
+ @onChange(this)
+
+
+########################################################################################################################
+
+class Inventory
+
+ constructor: ->
+ @items = []
+
+ add: (item)->
+ @items.push(item)
+
+ describe: ->
+ result = []
+
+ needsDelimiter = false
+ for item in @items
+ if needsDelimiter
+ result.push("\n")
+ result.push("There is a #{item.name} here.")
+ needsDelimiter = true
+
+ return result.join("\n")
+
+ eachItem: (withItem=(->))->
+ for item in @items
+ withItem(item)
+
+ isEmpty: ->
+ return @items.length is 0
+
+
+########################################################################################################################
+
+class Item
+
+ constructor: (@name)->
+ @description = "non-descript item"
+
+ toString: ->
+ return @name
+
+
+########################################################################################################################
+
+class Location
+
+ constructor: (@name)->
+ @description = "Non-descript Place"
+ @destinations = {}
+ @inventory = new Inventory()
+ @transitions = []
+ @visited = false
+
+ addTransition: (direction, toLocation, locked=false)->
+ @transitions[direction] = new Transition(direction, toLocation, locked)
+
+ addItem: (item)->
+ @inventory.add(item)
+ return this
+
+ describe: ->
+ result = [@name]
+
+ if not @visited
+ result.push("\n")
+ result.push(@description)
+
+ if not @inventory.isEmpty()
+ 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)->
+ @items.remove(item)
+ return this
+
+ toString: ->
+ return @name
+
+
+########################################################################################################################
+
+class ParseError extends Error
+
+
+########################################################################################################################
+
+class Parser
+
+ constructor: (@story)->
+ @aliases = {}
+ @directions = []
+ @verbs = {}
+ @fillerWords = new Set()
+
+ 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
+
+ # Private Methods ##############################################################################
+
+ _normalizeSentence: (sentence)->
+ if sentence.has(verb: 0, location: 1)
+ sentence.addWord(new WordToken("go", "verb"))
+ else if sentence.has(verb: 1, item: 0) and (sentence.verb is "take")
+ sentence.addWord(new WordToken("all", "item"))
+ else if sentence.has(verb: 1, item: 0) and (sentence.verb is "drop")
+ sentence.addWord(new WordToken("all", "item"))
+
+ 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.values()
+ 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]))
+ 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 rawWord is direction
+ candidates.add(transition.toLocation)
+ else 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
+
+ constructor: (@story)->
+ @inventory = new Inventory()
+ @verbs = {}
+
+ addVerb: (verb, onVerb)->
+ @verbs[verb] = onVerb
+ @story.parser.addVerb(verb)
+
+ enact: (sentence)->
+ onVerb = @verbs[sentence.verb]
+ if onVerb
+ onVerb(sentence)
+ else
+ throw new ParseError("I'm not sure how to #{sentence.verb}, to be honest.")
+
+ move: (location)->
+ transition = @story.currentLocation.getTransitionTo(location)
+ if not transition
+ throw new ParseError("You can't get to #{location.name} from here.")
+ else if transition.locked
+ throw new ParseError(transition.lockDescription)
+
+ @story.arrive(location)
+
+
+########################################################################################################################
+
+class Sentence
+
+ constructor: ->
+ @tokens = item: [], location: [], verb: []
+
+ 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(", ")}]" +
+ "}"
+ )
+
+ Object.defineProperties @prototype,
+ "item":
+ get: -> return @tokens.item[0].referant
+ "location":
+ get: -> return @tokens.location[0].referant
+ "verb":
+ get: -> return @tokens.verb[0].rawText
+
+
+########################################################################################################################
+
+class Story
+
+ constructor: (@title)->
+ @currentLocation = null
+ @items = []
+ @locations = []
+ @log = new GameLog()
+ @parser = new Parser(this)
+ @player = new Player(this)
+
+ @_configure()
+
+ # Configuration Methods ###########3############################################################
+
+ addItem: (name)->
+ item = new Item(name)
+ @items.push(item)
+ return item
+
+ addLocation: (name)->
+ location = new Location(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
+
+ begin: ->
+ @log.writeln(@title)
+ @log.writeln()
+ @arrive(@currentLocation)
+
+ interpret: (userInput)->
+ try
+ @log.echoInput(userInput)
+ sentence = @parser.interpret(userInput)
+ @player.enact(sentence)
+ catch e
+ if e instanceof ParseError
+ @log.writeln(e.message)
+ else
+ throw e
+
+ # Private Methods ##############################################################################
+
+ _configure: ->
+ @parser.addDirections(
+ "north", "northeast", "east", "southeast", "south", "southwest", "west", "northwest", "up", "down"
+ )
+ @parser.addFillerWords("of", "the", "a", "an", "to")
+ @parser.addAliases({
+ "d": "down",
+ "e": "east",
+ "everything": "all",
+ "g": "go"
+ "n": "north",
+ "ne": "northeast",
+ "nw": "northwest",
+ "s": "south",
+ "se": "southeast",
+ "sw": "southwest",
+ "u": "up",
+ "w": "west",
+ })
+
+ player = @player
+ @player.addVerb "go", (sentence)->
+ if sentence.has(location: 1)
+ player.move(sentence.location)
+ else
+ throw new ParseError("I'm not sure where you want to go...")
+
+
+########################################################################################################################
+
+class Transition
+
+ constructor: (@direction, @toLocation, @locked=false)->
+ # 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
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..1a77aed
--- /dev/null
+++ b/index.html
@@ -0,0 +1,17 @@
+
+
+
+