initial commit

This commit is contained in:
Rudis Muiznieks 2014-07-01 18:59:41 -05:00
parent 9f71f899b5
commit e5b9e87735
11 changed files with 637 additions and 0 deletions

3
.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
*.swp
bin/
node_modules/

72
Gruntfile.coffee Normal file
View file

@ -0,0 +1,72 @@
module.exports = (grunt) ->
pkg = require './package.json'
grunt.initConfig
pkg: pkg
coffee:
compile:
options:
join: true
bare: true
files: [
'bin/ficdown.js': ['src/*.coffee']
]
stylus:
compile:
options:
compress: true
expand: true
files: [
'bin/example/player.css': ['src/example/*.styl']
]
uglify:
js:
files:
'bin/ficdown.min.js': [
'bin/ficdown.js'
]
copy:
static:
files: [
expand: true
flatten: true
src: ['src/*.html']
dest: 'bin/'
]
example:
files: [
expand: true
flatten: true
src: ['src/example/*.html', 'src/example/*.png', 'src/example/*.md']
dest: 'bin/example/'
]
watch:
js:
files: ['src/**/*.coffee']
tasks: ['build:js']
css:
files: ['src/**/*.styl']
tasks: ['stylus:compile']
static:
files: ['src/**/*.html','src/**/*.js','src/**/*.md']
tasks: ['copy:static', 'copy:example']
for name of pkg.devDependencies when name.substring(0, 6) is 'grunt-'
grunt.loadNpmTasks name
grunt.registerTask 'build:js', [
'coffee:compile'
'uglify:js'
]
grunt.registerTask 'default', [
'coffee:compile'
'uglify:js'
'stylus:compile'
'copy:static'
'copy:example'
]

15
package.json Normal file
View file

@ -0,0 +1,15 @@
{
"name": "ficdown.js",
"version": "0.0.1",
"description": "A parser and player for Interactive Fiction written in Ficdown",
"dependencies": {},
"devDependencies": {
"coffee-script": "^1.7.1",
"grunt": "^0.4.5",
"grunt-contrib-coffee": "^0.10.1",
"grunt-contrib-uglify": "^0.5.0",
"grunt-contrib-copy": "^0.5.0",
"grunt-contrib-watch": "^0.6.1",
"grunt-contrib-stylus": "^0.17.0"
}
}

26
src/example/index.html Normal file
View file

@ -0,0 +1,26 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>The Robot King</title>
<meta charset="utf-8">
<link href="http://fonts.googleapis.com/css?family=Maven+Pro:400,700" rel="stylesheet">
<link rel="stylesheet" href="player.css">
</head>
<body>
<div id="container" class="container">
<div id="main">
</div>
</div>
<script src="http://code.jquery.com/jquery-2.1.1.min.js"></script>
<script src="http://cdnjs.cloudflare.com/ajax/libs/pagedown/1.0/Markdown.Converter.min.js"></script>
<script src="http://cdnjs.cloudflare.com/ajax/libs/pagedown/1.0/Markdown.Sanitizer.min.js"></script>
<script src="../ficdown.min.js"></script>
<script>
$.get('story.md', function(data){
story = parseText(data);
player = new Player(story, 'main');
player.play();
}, 'text');
</script>
</body>
</html>

39
src/example/player.styl Normal file
View file

@ -0,0 +1,39 @@
html, body
height 100%
body
font-family 'Maven Pro', sans-serif
background url('science.png') fixed repeat
html, body, h1, h2, p, ul, div
margin 0
padding 0
ul
list-style-type circle
margin-left 1.33em
#container
background-color rgba(255,255,255,0.95)
max-width 800px
margin 0 auto
height 100%
overflow-y scroll
overflow-x hidden
#main
position relative
padding 40px 60px
p, h1, h2
margin 1em 0
a
color #c00
text-decoration none
&.disabled
color #999
&.chosen
color #000
&:hover
color #000
&:hover
text-decoration underline
color #f00
&.disabled
text-decoration none
color #999
cursor not-allowed

BIN
src/example/science.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 KiB

178
src/example/story.md Normal file
View file

@ -0,0 +1,178 @@
# [The Robot King](/robot-cave)
An experiment in markdown-based interactive fiction by Rudis Muiznieks.
This story was written in [Ficdown](http://rdsm.ca/ficdown), a subset of [Markdown](http://daringfireball.net/projects/markdown/) syntax with specific conventions for creating state-aware interactive fiction.
*Release r2014-07-01*
## Robot Cave
> You wake up and emit a great robot yawn as you stretch your metallic arms. Today is the big day! You have to start your new job working for the Robot King at the Robot Palace!
You're in your cave and you can see [a peg by the door where you usually hang your raincoat|your raincoat hanging by the door](?raincoat).
Your cave only has one tiny window, and through it you can see [the sun shining through the clouds|that it's raining heavily outside](?stopped-raining).
**What do you want to do?**
- [Go outside and start walking to the palace.](/outside)
- [|Wait for it to stop raining.](?stopped-raining#stopped-raining)
- [|Put on your raincoat.](?raincoat#raincoat)
### Raincoat
You take your raincoat and put it on. It fits perfectly!
### Stopped Raining
It feels like hours, but it finally stops raining. You hope you won't be late for your new job!
## Outside
> You step through the door and feel the water flowing over your metal body. Brrr! That's cold! You start to think that maybe getting your raincoat would be a good idea. This is just the kind of rain that might turn you into a rusty robot statue if you stay in it too long.
You're standing on your front porch in the pouring rain. You need to get to the palace for your new job, but you don't want to rust!
**What will you do?**
- [Continue walking to the palace.](/rusted)
- [Go back into your cave.](/robot-cave)
## [Outside](?stopped-raining)
You step through the door and feel the early afternoon sun warming your metal body. It feels good, but you were supposed to start your new job early in the morning!
You run as fast as you can all the way to the Robot Palace, but it's already too late.
"You were supposed to be here first thing in the morning," says the palace guard. "We can't have sleepy-head robots working at the Robot Palace! Try finding a different job instead."
**You've been fired!**
## Rusted!
You start walking toward the Robot Palace in the rain. Who needs a raincoat anyway? As you move down the path, rust starts forming on your legs and knees so you have to walk slower. Eventually the rust gets so bad that you can't move anymore at all!
As your whole body rusts over, you wonder what you could have been thinking. Only a crazy robot would ever go out into the rain without a raincoat!
You will have a long time to think about your mistake while you wait for another robot to come and help scrape off all the rust so you can move again. Since you never made it to the palace for your new job, you'll probably be fired.
**You have turned into a rusty robot statue!**
## [Outside](?raincoat)
You head out the door and into the rain. It's a good thing you put on your raincoat, because it's just the kind of rain that would probably turn you into a rusty robot statue if you stayed in it for too long.
You follow the road by your house all the way through town until you reach the door to the Robot Palace.
The palace guard looks you up and down. "What do you want?" he asks.
**What will you tell him?**
- ["I'm the new janitor-bot!"](/palace-gate#new-job)
- ["I'd like a tour of the palace!"](/palace-gate)
## Palace Gate
The robot guard looks at you and [nods|frowns](?new-job). "[Oh yeah, they told me to expect you. You're supposed to be starting today right?|We don't do tours on weekdays. Hey, aren't you the new janitor-bot who's starting today?](?new-job)"
**How will you answer?**
- ["Yup!"](/palace-entrance)
- ["Nope!"](/back-to-bed)
## Back to Bed
The robot guard looks at you with a confused expression on his face, then stops paying attention to you.
I guess you decided that you don't want a new job today after all. You turn around and walk all the way back home, where you hop back into bed for a quick nap.
Without a job, you fail to earn any money and you can no longer afford fuel to power yourself.
**You run out of fuel and shut down forever!**
## Palace Entrance
> The robot guard nods and ushers you into the palace through the large front doors.
> "You'll want to report to the Master Janitor Robot downstairs. He'll give you your uniform and get you started," the guard says, then quickly leaves and shuts the doors behind him.
The palace entrance is one of the biggest rooms you've ever seen! There are statues of knight-robots and pictures of all of the old Robot Kings going back for centuries lining the walls. The picture closest to you is a picture of the current Robot King. He looks a lot like you!
There is a grand double staircase leading up to the throne room, a hallway straight ahead that leads to the living quarters, and a door to your left that says "Stairs."
**Where do you want to go?**
- [Go upstairs to the throne room.](#throne-room)
- [Go through the hall to the living quarters.](/living-quarters)
- [Go downstairs to see the Master Janitor Robot.](/palace-basement)
### Throne Room
You start to ascend the stairs, but then think better of it. You wouldn't know what to do if you ran into the Robot King up there anyway!
## Living Quarters
You walk into the hall that leads to the living quarters, and find a gate blocking your way. There is a robot scanner installed on the gate. I guess it only opens for robots who live or work here. Maybe the Master Janitor Robot will have a way for you to get through.
[Go back to the palace entrance.](/palace-entrance#tried-gate)
## Palace Basement
> You walk down three flights of stairs until you reach the basement. The staircase is dark, but the basement is even darker. It's a little scary! You hope you can get the information you need from the Master Janitor Robot and get out of here as quickly as possible.
You're standing in the basement where new employees can pick up their uniforms and learn what their jobs are for the day.
[The Master Janitor Robot is pacing back and forth here, muttering to himself.|There is a funny looking robot here pacing back and forth, muttering to himself. That must be the Master Janitor Robot. When he notices you, he stops muttering and stares at you with crazy eyes.](?talked-to-master)
**What will you do?**
- [Go back upstairs.](/palace-entrance)
- [Ask the Master Janitor Robot what he's muttering about.](#talked-to-master+muttering)
- [|Ask the Master Janitor Robot about your uniform.](?uniform#talked-to-master+uniform)
- [Ask the Master Janitor Robot about the gate upstairs.](?tried-gate#talked-to-master+about-the-gate)
- [Ask the Master Janitor Robot about your job.](?uniform#started-job)
### Muttering
"Muttering?" says the Master Janitor Robot. "Was I muttering? I hadn't noticed."
The Master Janitor Robot pauses thoughtfully for a moment, then resumes muttering.
### Uniform
The Master Janitor Robot's eyes light up a pleasant shade of blue. "Ahh, you must be the new janitor-bot starting today!" he says.
He walks to a box in the corner and pulls out a blue janitor's uniform, then hands it to you. You put it on.
### About the Gate
"Ahh, yes, the gate," says the Master Janitor Robot. "Quite a clever contraption. There's a scanner attached that looks for a special device that's sewn into the [uniform I gave you|uniform that employees here wear](?uniform). [As I said, you'll want to head up there now to start cleaning room 13.](?started-job)"
### Started Job
["Like I said before, your|"Ready to get going?" says the Master Janitor Robot. He continues before you have a chance to answer. "Good, good. Your](?started-job) first job will be to clean room 13 in the living quarters. That's where the Robot King keeps all of his spare robes and crowns. There's a janitor's closet right next to that room where you can get a mop to clean the floors, and a duster to dust off the crowns."
The Master Janitor Robot scratches his chin for a moment, then resumes pacing back and forth and muttering to himself.
## [Living Quarters](?uniform)
You head into the hallway that leads to the living quarters and come to a large gate. A scanner attached to the gate lights up and beeps a few times. After a moment, you hear a click and a soft hiss as the gate opens to let you pass. Once you walk through, the gate hisses and clicks shut behind you.
You notice with some alarm that there's no scanner on the inside of the gate. You don't know how to get back out!
[Continue...](/living-quarters-2)
## [Living Quarters 2]("Living Quarters")
That's when you realize that you never asked the Master Janitor Bot what your job here was. You just took your uniform and left!
**You have failed to perform your new job because you never found out what it was.**
## [Living Quarters 2](?started-job "Living Quarters")
That's no problem though, because you already know what your job is. You continue down the hall, looking at and passing all of the doors until you come to the one marked with a "13." Right next to it is another door labeled "Janitor's Closet."
You open the closet and grab the mop and duster. You're so excited! Your first day as a janitor working for a Robot King that looks just like you, and you are about to enter a room containing all of his spare robes and crowns. What fun!
**You have reached the end of the intro to The Robot King.**

76
src/parser.coffee Normal file
View file

@ -0,0 +1,76 @@
parseText = (text) ->
lines = text.split /\n|\r\n/
blocks = extractBlocks lines
story = parseBlocks blocks
getBlockType = (hashCount) ->
switch hashCount
when 1 then 'story'
when 2 then 'scene'
when 3 then 'action'
extractBlocks = (lines) ->
blocks = []
currentBlock = null
for line in lines
match = line.match /^(#{1,3})\s+([^#].*)$/
if match?
if currentBlock != null
blocks.push currentBlock
currentBlock =
type: getBlockType match[1].length
name: match[2]
lines: []
else
currentBlock.lines.push line
if currentBlock != null
blocks.push currentBlock
return blocks
parseBlocks = (blocks) ->
storyBlock = null
for block in blocks
if block.type == 'story'
storyBlock = block
break
storyName = matchAnchor storyBlock.name
storyHref = matchHref storyName.href
story =
name: storyName.text
description: trimText storyBlock.lines.join "\n"
firstScene: storyHref.target
scenes: {}
actions: {}
for block in blocks
switch block.type
when 'scene'
scene = blockToScene block
if !story.scenes[scene.key]
story.scenes[scene.key] = []
story.scenes[scene.key].push scene
when 'action'
action = blockToAction block
story.actions[action.state] = action
return story
blockToScene = (block) ->
sceneName = matchAnchor block.name
if sceneName?
title = if sceneName.title? then trimText sceneName.title else trimText sceneName.text
key = normalize sceneName.text
href = matchHref sceneName.href
if href?.conditions?
conditions = href.conditions.split '&'
else
title = trimText block.name
key = normalize block.name
scene =
name: title
key: key
description: trimText block.lines.join "\n"
conditions: if conditions? then conditions else null
blockToAction = (block) ->
action =
state: normalize block.name
description: trimText block.lines.join "\n"

108
src/player.coffee Normal file
View file

@ -0,0 +1,108 @@
class Player
constructor: (@story, @id) ->
@converter = new Markdown.Converter()
@container = $("##{@id}")
@container.addClass('ficdown').data 'player', this
@playerState = {}
@visitedScenes = {}
@currentScene = null
@moveCounter = 0
play: ->
@container.html @converter.makeHtml "##{story.name}\n\n#{story.description}\n\n[Click to start...](/#{story.firstScene})"
@wireLinks()
wireLinks: ->
@container.find('a:not(.disabled):not(.external)').each (i) ->
$this = $(this)
if !$this.attr('href').match(/^https?:\/\//)?
$this.click ->
$this.addClass 'chosen'
player = $this.parents('.ficdown').data 'player'
player.handleHref $this.attr 'href'
return false
else
$this.addClass 'external'
resolveDescription: (description) ->
for anchor in matchAnchors description
href = matchHref anchor.href
if href.conditions?
conditions = href.conditions.split '&'
satisfied = conditionsMet @playerState, conditions
alts = splitAltText anchor.text
replace = if satisfied then alts.passed else alts.failed
if !replace?
replace = ''
replace = replace.replace regexLib.escapeChar, ''
if replace == '' or (!href.toggles? and !href.target?)
description = description.replace anchor.anchor, replace
else
newAnchor = "[#{replace}](#{ if href.target? then "/#{href.target}" else "" }#{ if href.toggles? then "##{href.toggles}" else "" })"
description = description.replace anchor.anchor, newAnchor
description = description.replace regexLib.emptyListItem, ''
return description
disableOldLinks: ->
@container.find('a:not(.external)').each (i) ->
$this = $(this)
$this.addClass 'disabled'
$this.unbind 'click'
$this.click -> return false
processHtml: (scene, html) ->
temp = $('<div/>').append $.parseHTML html
if @visitedScenes[scene]
temp.find('blockquote').remove()
else
temp.find('blockquote').each (i) ->
$this = $(this)
$this.replaceWith $this.html()
return temp.html()
checkGameOver: ->
if @container.find('a:not(.disabled):not(.external)').length == 0
@container.append @converter.makeHtml '## The End\n\nYou have reached the end of this story. <a id="restart" href="">Click here</a> to start over.'
$('#restart').click ->
player = new Player @story, @id
$("##{@id}").data 'player', player
player.play()
handleHref: (href) ->
match = matchHref href
matchedScene = null
actions = []
if match?.toggles?
console.log "toggles: " + JSON.stringify match.toggles
toggles = match.toggles.split '+'
for toggle in toggles
if @story.actions[toggle]?
action = $.extend {}, @story.actions[toggle]
action.description = @resolveDescription action.description
actions.push action
@playerState[toggle] = true
if match?.target?
if @story.scenes[match.target]?
for scene in @story.scenes[match.target]
if conditionsMet @playerState, scene.conditions
if !matchedScene? or !scene.conditions? or !matchedScene.conditions? or scene.conditions.length > matchedScene.conditions.length
matchedScene = scene
if matchedScene?
@currentScene = matchedScene
newScene = $.extend {}, @currentScene
newScene.description = @resolveDescription newScene.description
@disableOldLinks()
newContent = "###{newScene.name}\n\n"
newContent += "#{action.description}\n\n" for action in actions
newContent += newScene.description
newHtml = @processHtml newScene.key, @converter.makeHtml newContent
@visitedScenes[newScene.key] = true
scrollId = "move-#{@moveCounter++}"
@container.append $('<span/>').attr 'id', scrollId
@container.append newHtml
@wireLinks()
@checkGameOver()
console.log "scrolling to #{scrollId}"
@container.parent('.container').animate
scrollTop: $("##{scrollId}").offset().top - @container.offset().top
, 1000

48
src/tests.html Normal file
View file

@ -0,0 +1,48 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Ficdown.js Tests</title>
<meta charset="utf-8">
</head>
<body>
<div>
<textarea style="height:200px;width:600px;" id="input"># [Test Story](/test-scene)
This is a test story by Rudis Muiznieks
Version 1
## Test Scene
This is a test scene.scene
- [This is an option.](/test-scene-2)
- [This is another option.](#test-condition)
### Test Condition
You have [|not](?test-condition) selected that option before.
## [Test Scene](?test-condition "Different Title")
This is the scene that matches a condition.
## Test Scene 2
This is a second scene</textarea>
<div><button onclick="doIt()">Do It</button></div>
<pre id="output">
</pre>
</div>
<script src="ficdown.js"></script>
<script>
function doIt(){
var storyText = document.getElementById('input').value;
var story = parseText(storyText);
document.getElementById('output').innerHTML = JSON.stringify(story,null,2);
}
doIt();
</script>
</body>
</html>

72
src/util.coffee Normal file
View file

@ -0,0 +1,72 @@
regexLib =
anchors: /(\[((?:[^\[\]]+|\[(?:[^\[\]]+|\[(?:[^\[\]]+|\[(?:[^\[\]]+|\[(?:[^\[\]]+|\[(?:[^\[\]]+|\[\])*\])*\])*\])*\])*\])*)\]\([ ]*((?:[^()\s]+|\((?:[^()\s]+|\((?:[^()\s]+|\((?:[^()\s]+|\((?:[^()\s]+|\((?:[^()\s]+|\(\))*\))*\))*\))*\))*\))*)[ ]*((['"])(.*?)\5[ ]*)?\))/gm
href: /^(?:\/([a-zA-Z](?:-?[a-zA-Z0-9])*))?(?:\?((?:[a-zA-Z](?:-?[a-zA-Z0-9])*)(?:&[a-zA-Z](?:-?[a-zA-Z0-9])*)*)?)?(?:#((?:[a-zA-Z](?:-?[a-zA-Z0-9])*)(?:\+[a-zA-Z](?:-?[a-zA-Z0-9])*)*))?$/
trim: /^\s+|\s+$/g
altText: /^((?:[^|\\]|\\.)*)(?:\|((?:[^|\\]|\\.)+))?$/
escapeChar: /\\(?=[^\\])/g
emptyListItem: /^\s*-\s*([\r\n]+|$)/gm
matchAnchor = (text) ->
re = new RegExp regexLib.anchors
match = re.exec text
if match?
result =
anchor: match[1]
text: match[2]
href: match[3]
title: match[6]
return result
return match
matchAnchors = (text) ->
re = new RegExp regexLib.anchors
anchors = []
while match = re.exec text
anchors.push
anchor: match[1]
text: match[2]
href: match[3]
title: match[6]
return anchors
trimText = (text) ->
text.replace regexLib.trim, ''
matchHref = (href) ->
re = new RegExp regexLib.href
match = re.exec href
if match?
result =
target: match[1]
conditions: match[2]
toggles: match[3]
return result
return match
normalize = (text) ->
text.toLowerCase().replace(/^\W+|\W+$/g, '').replace /\W+/g, '-'
conditionsMet = (state, conditions) ->
met = true
if conditions?
for condition in conditions
if !state[condition]?
met = false
break
return met
splitAltText = (text) ->
re = new RegExp regexLib.altText
match = re.exec text
if match?
result =
passed: match[1]
failed: match[2]
return result
return