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