Add inital setup including 'go' command
This commit is contained in:
@@ -0,0 +1,2 @@
|
|||||||
|
.DS_Store
|
||||||
|
*.swp
|
||||||
@@ -0,0 +1,420 @@
|
|||||||
|
class GameLog
|
||||||
|
|
||||||
|
constructor: (onChange=(->))->
|
||||||
|
@content = ""
|
||||||
|
@onChange = onChange
|
||||||
|
|
||||||
|
echoInput: (text)->
|
||||||
|
@content += "<br>> " + text + "<br>"
|
||||||
|
@onChange(this)
|
||||||
|
|
||||||
|
writeln: (text="")->
|
||||||
|
@content += text.replace(/\n/g, "<br>") + "<br>"
|
||||||
|
@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
|
||||||
+17
@@ -0,0 +1,17 @@
|
|||||||
|
<html>
|
||||||
|
<link rel="stylesheet" type="text/css" href="styles.css"/>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="log">
|
||||||
|
<p class="log-text"></p>
|
||||||
|
</div>
|
||||||
|
<input class="entry" type="text"></input>
|
||||||
|
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
|
||||||
|
<script src="https://coffeescript.org/v2/browser-compiler/coffeescript.js"></script>
|
||||||
|
|
||||||
|
<script src="game_engine.coffee" type="text/coffeescript"></script>
|
||||||
|
<script src="story.coffee" type="text/coffeescript"></script>
|
||||||
|
<script src="run_game.coffee" type="text/coffeescript"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# Constants ############################################################################################################
|
||||||
|
|
||||||
|
RETURN_KEY = 13
|
||||||
|
|
||||||
|
|
||||||
|
# Global Data ##########################################################################################################
|
||||||
|
|
||||||
|
$(document).ready ->
|
||||||
|
$input = $("input.entry")
|
||||||
|
$input.on "keydown", ->
|
||||||
|
if event.which isnt RETURN_KEY then return
|
||||||
|
STORY.interpret($input.val())
|
||||||
|
$input.val("")
|
||||||
|
|
||||||
|
$log = $(".log")
|
||||||
|
$logText = $("p.log-text")
|
||||||
|
|
||||||
|
STORY.log.onChange = (log)->
|
||||||
|
$logText.html(log.content)
|
||||||
|
$log.scrollTop($log[0].scrollHeight)
|
||||||
|
|
||||||
|
STORY.begin()
|
||||||
|
$input.focus()
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
window.STORY = s = new Story("A Walk Through My House")
|
||||||
|
|
||||||
|
# Items ################################################################################################################
|
||||||
|
|
||||||
|
boxOfTile = s.addItem("box of tile")
|
||||||
|
boxOfTile.description =
|
||||||
|
"This appears to be a box of flooring tiles. The tiles appear to be a very light color of natural stone. It is
|
||||||
|
quite heavy."
|
||||||
|
|
||||||
|
hose = s.addItem("garden hose")
|
||||||
|
hose.description = "It's a pretty ordinary garden hose about 20' long."
|
||||||
|
|
||||||
|
# Locations ############################################################################################################
|
||||||
|
|
||||||
|
driveway = s.addLocation("Driveway")
|
||||||
|
driveway.description =
|
||||||
|
"The driveway extends slightly uphill a short way to the west back to the street. At this part, it's recessed a
|
||||||
|
little below the level of the lawn with a stone wall defining the boundary. To the east are three garage doors which
|
||||||
|
make up the entire side of the house. Above the doors are a few windows. The back yard is to the east, and the
|
||||||
|
front porch is to the south."
|
||||||
|
driveway.addItem(hose)
|
||||||
|
|
||||||
|
frontPorch = s.addLocation("Front Porch")
|
||||||
|
frontPorch.description =
|
||||||
|
"You're standing on the front porch. It's a wooden deck with a slight overhang above, and a railing all about. A
|
||||||
|
double door is in front of you with a fancy crystal inset in the windows. There's a light on inside the house, but
|
||||||
|
you can't make out any details."
|
||||||
|
frontPorch.addItem(boxOfTile)
|
||||||
|
|
||||||
|
# Configure Map ########################################################################################################
|
||||||
|
|
||||||
|
driveway.addTransition("southwest", frontPorch)
|
||||||
|
frontPorch.addTransition("northeast", driveway)
|
||||||
|
|
||||||
|
s.currentLocation = frontPorch
|
||||||
+42
@@ -0,0 +1,42 @@
|
|||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
font-size: 16px;
|
||||||
|
font-family: monospace;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.log {
|
||||||
|
background: black;
|
||||||
|
color: green;
|
||||||
|
height: 600px;
|
||||||
|
overflow-y: scroll;
|
||||||
|
padding: 0.25em 1em;
|
||||||
|
width: 800px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input.entry {
|
||||||
|
background: black;
|
||||||
|
border: none;
|
||||||
|
border-top: 1px solid gray;
|
||||||
|
color: green;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 16px;
|
||||||
|
margin: 0;
|
||||||
|
outline: none;
|
||||||
|
padding: 0.5em 1em;
|
||||||
|
width: 800px;
|
||||||
|
}
|
||||||
|
|
||||||
|
p.log-text {
|
||||||
|
}
|
||||||
|
|
||||||
|
p.placeholder {
|
||||||
|
color: gray;
|
||||||
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user