diff --git a/.gitignore b/.gitignore
index dc82df65..a023f536 100644
--- a/.gitignore
+++ b/.gitignore
@@ -11,4 +11,5 @@ iphone/www
*/.DS_Store
.DS_Store
*.svn
-randomtest-output.txt
\ No newline at end of file
+randomtest-output.txt
+node_modules/**
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 00000000..aa3ec8d8
--- /dev/null
+++ b/README.md
@@ -0,0 +1,52 @@
+# ChoiceScript #
+
+[ChoiceScript](http://www.choiceofgames.com/blog/choicescript-intro/) is a small language written by Dan Fabulich for running multiple choice games.
+
+This is a fork of that project that attempts to bring easier development (especially if you are from a Mac/Unix dev background) to the language.
+
+## Installation ##
+
+Provided you have [npm](http://npmjs.org/) on your system, run:
+
+ $ npm install -g choicescript
+
+That's it.
+
+## Usage ##
+
+To create a new game simply run:
+
+ $ choicescript new MyAwesomeGame intro_scene,ending
+
+This will create the following project in your current directory (you can omit `intro_scene,ending`, some sample vignettes will be generated for you):
+
+ MyAwesomeGame/
+ |- game.js
+ `- scenes/
+ |- intro_scene.txt
+ |- ending.txt
+ `- choicescript_stats.txt
+
+You can open up your folder and write your game. To run it in a browser use:
+
+ $ choicescript server
+
+in the project directory. This will (by default) start your game at http://localhost:3030/.
+
+To test that your game works you can use two types of automated testing:
+
+ $ choicescript quicktest
+ $ choicescript randomtest
+
+This runs them both:
+
+ $ choicescript test
+
+Finally remember that you can get help on all the various command and options by typing:
+
+ $ choicescript -h
+
+or for any command:
+
+ $ choicescript COMMAND -h
+
diff --git a/archive.bat b/archive.bat
deleted file mode 100644
index ec779805..00000000
--- a/archive.bat
+++ /dev/null
@@ -1,7 +0,0 @@
-del choicescript.zip
-rm -rf choicescript
-call git archive --format=zip --prefix=choicescript/ HEAD index.html *.js scenes todo.txt > choicescript.zip
-mkdir choicescript
-call git log -1 > choicescript\revision.txt
-c:\Progra~1\7-Zip\7z.exe a choicescript.zip choicescript\*
-rm -rf choicescript
\ No newline at end of file
diff --git a/autotest.bat b/autotest.bat
deleted file mode 100644
index 463b8739..00000000
--- a/autotest.bat
+++ /dev/null
@@ -1,18 +0,0 @@
-
-::Copyright 2010 by Dan Fabulich.
-::
-::Dan Fabulich licenses this file to you under the
-::ChoiceScript License, Version 1.0 (the "License"); you may
-::not use this file except in compliance with the License.
-::You may obtain a copy of the License at
-::
-:: http://www.choiceofgames.com/LICENSE-1.0.txt
-::
-::See the License for the specific language governing
-::permissions and limitations under the License.
-::
-::Unless required by applicable law or agreed to in writing,
-::software distributed under the License is distributed on an
-::"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
-::either express or implied.
-java -jar js.jar -w -opt -1 -debug autotest.js %*
diff --git a/autotestdebug.bat b/autotestdebug.bat
deleted file mode 100644
index 00b152e5..00000000
--- a/autotestdebug.bat
+++ /dev/null
@@ -1,18 +0,0 @@
-
-::Copyright 2010 by Dan Fabulich.
-::
-::Dan Fabulich licenses this file to you under the
-::ChoiceScript License, Version 1.0 (the "License"); you may
-::not use this file except in compliance with the License.
-::You may obtain a copy of the License at
-::
-:: http://www.choiceofgames.com/LICENSE-1.0.txt
-::
-::See the License for the specific language governing
-::permissions and limitations under the License.
-::
-::Unless required by applicable law or agreed to in writing,
-::software distributed under the License is distributed on an
-::"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
-::either express or implied.
-java -cp js.jar org.mozilla.javascript.tools.debugger.Main autotest.js %*
diff --git a/build.xml b/build.xml
deleted file mode 100644
index 9c3595f8..00000000
--- a/build.xml
+++ /dev/null
@@ -1,164 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
- window.version="${revision}"
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Executing randomtest, writing to ${output}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/editor/embeddable-autotester.js b/editor/embeddable-autotester.js
index 6fc1c7f6..846b9a97 100644
--- a/editor/embeddable-autotester.js
+++ b/editor/embeddable-autotester.js
@@ -1,6 +1,8 @@
function autotester(sceneText, nav, sceneName) {
function log(msg) {
- if (typeof(console) != "undefined") console.log(msg)
+ if(!program.quiet) {
+ if (typeof(console) != "undefined") console.log(msg)
+ }
}
var coverage = [];
diff --git a/generator.js b/generator.js
deleted file mode 100644
index b77b496e..00000000
--- a/generator.js
+++ /dev/null
@@ -1,27 +0,0 @@
-var dir = arguments[0] || "web/mygame/scenes";
-load("web/scene.js");
-load("web/util.js");
-load("headless.js");
-
-var list = new java.io.File(dir).listFiles();
-
-var i = list.length;
-while (i--) {
- if (!/\.txt$/.test(list[i].getName())) continue;
- var inputMod = list[i].lastModified();
- var outputMod = new java.io.File(list[i].getAbsolutePath()+".js").lastModified();
- if (inputMod <= outputMod) {
- print(list[i] + " up to date");
- continue;
- }
- print(list[i]);
- var str = slurpFile(list[i]);
- var scene = new Scene();
- scene.loadLines(str);
-
- var writer = new java.io.BufferedWriter(new java.io.OutputStreamWriter(new java.io.FileOutputStream(list[i].getAbsolutePath()+".js"), "UTF-8"));
- writer.write("window.stats.scene.loadLinesFast(" + scene.temps.choice_crc + ", " + toJson(scene.lines)+ ", " + toJson(scene.labels) + ");");
-
- writer.close();
-}
-
diff --git a/js.jar b/js.jar
deleted file mode 100644
index 878b0d94..00000000
Binary files a/js.jar and /dev/null differ
diff --git a/autotest.js b/lib/autotest.js
similarity index 71%
rename from autotest.js
rename to lib/autotest.js
index 61ed5da4..505b34ff 100644
--- a/autotest.js
+++ b/lib/autotest.js
@@ -22,21 +22,23 @@ var gameName = "mygame";
if (typeof java == "undefined") {
var fs = require('fs');
var path = require('path');
- eval(fs.readFileSync("web/scene.js", "utf-8"));
- eval(fs.readFileSync("web/navigator.js", "utf-8"));
- eval(fs.readFileSync("web/util.js", "utf-8"));
- eval(fs.readFileSync("headless.js", "utf-8"));
- eval(fs.readFileSync("web/"+gameName+"/"+"mygame.js", "utf-8"));
- eval(fs.readFileSync("editor/embeddable-autotester.js", "utf-8"));
+ eval(fs.readFileSync(__dirname + "/../public/scene.js", "utf-8"));
+ eval(fs.readFileSync(__dirname + "/../public/navigator.js", "utf-8"));
+ eval(fs.readFileSync(__dirname + "/../public/util.js", "utf-8"));
+ eval(fs.readFileSync(__dirname + "/headless.js", "utf-8"));
+ //eval(fs.readFileSync(__dirname + "/../public/"+gameName+"/"+"mygame.js", "utf-8"));
+ eval(fs.readFileSync("./game.js", "utf-8"));
+
+ eval(fs.readFileSync(__dirname + "/../editor/embeddable-autotester.js", "utf-8"));
function print(str) {
console.log(str);
}
} else {
- load("web/scene.js");
- load("web/navigator.js");
- load("web/util.js");
- load("headless.js");
- load("web/"+gameName+"/"+"mygame.js");
+ load(__dirname + "/../public/scene.js");
+ load(__dirname + "/../public/navigator.js");
+ load(__dirname + "/../public/util.js");
+ load(__dirname + "headless.js");
+ load(__dirname + "/../public/"+gameName+"/"+"mygame.js");
load("editor/embeddable-autotester.js");
if (typeof(console) == "undefined") console = {log: print};
}
@@ -48,14 +50,14 @@ var sceneList = [];
function debughelp() {
debugger;
}
-var list;
-if (isRhino) {
- list = arguments;
-} else {
- list = process.argv;
- list.shift();
- list.shift();
-}
+//var list = [];
+// if (isRhino) {
+// list = arguments;
+// } else {
+// list = process.argv;
+// list.shift();
+// list.shift();
+// }
if (!list.length || (list.length == 1 && !list[0])) {
list = [];
var name = nav.getStartupScene();
@@ -63,7 +65,7 @@ if (!list.length || (list.length == 1 && !list[0])) {
list.push(name);
name = nav.nextSceneName(name);
}
- if (fileExists("web/"+gameName+"/scenes/choicescript_stats.txt")) {
+ if (fileExists("./scenes/choicescript_stats.txt")) {
list.push("choicescript_stats");
}
}
@@ -72,7 +74,7 @@ var uncoveredScenes = [];
var uncovered;
function verifyFileName(name) {
- var filePath = "web/"+gameName+"/scenes/"+name+".txt";
+ var filePath = "./scenes/"+name+".txt";
if (!fileExists(filePath)) throw new Error("File does not exist: " + name);
if (isRhino) {
var file = new java.io.File(filePath);
@@ -98,11 +100,11 @@ Scene.prototype.verifyFileName = function commandLineVerifyFileName(name) {
Scene.prototype.conflictingOptions = function() {};
for (var i = 0; i < list.length; i++) {
- print(list[i]);
+ if(!program.quiet) print(list[i]);
if (isRhino) java.lang.Thread.sleep(100); // sleep to allow print statements to flush :-(
try {
verifyFileName(list[i]);
- var sceneText = slurpFile("web/"+gameName+"/scenes/"+list[i]+".txt", true /*throwOnError*/);
+ var sceneText = slurpFile("./scenes/"+list[i]+".txt", true /*throwOnError*/);
uncovered = autotester(sceneText, nav, list[i])[1];
} catch (e) {
print("QUICKTEST FAILED\n");
@@ -110,7 +112,8 @@ for (var i = 0; i < list.length; i++) {
if (isRhino) {
java.lang.System.exit(1);
} else {
- process.exit(1);
+ throw new Error("Quicktest failed");
+ //process.exit(1);
}
}
if (uncovered) {
diff --git a/lib/cli.js b/lib/cli.js
new file mode 100755
index 00000000..0d8aef41
--- /dev/null
+++ b/lib/cli.js
@@ -0,0 +1,241 @@
+#!/usr/bin/env node
+
+/**
+ * Module dependencies.
+ */
+
+var program = require('commander');
+
+var fs = require('fs');
+
+var exec = require('child_process').exec;
+
+program
+ .version(JSON.parse(fs.readFileSync(
+ require.main.filename.match(/^(.+)\/.+$/)[1] + '/../package.json')).version)
+ .option('-q, --quiet', "supress output");
+
+
+function game_js(scenes, stats) {
+
+ var txt = "// Specify the list of scenes here, separated by commas, with no final comma\n\nnav = new SceneNavigator([\n ";
+ txt += scenes.map(function(scene){return '"'+scene+'"';}).join("\n ,");
+ txt += "\n]);\n\n// Specify the default starting stats here\n\nstats = {\n ";
+ keylist = stats.join(": 50\n ,") + ": 50\n";
+ txt += keylist;
+ txt += "};\n\n// Specify the stats to use in debug mode\n\ndebugStats = {\n ";
+ txt += keylist;
+ txt += "};\n\n// or just use defaults\n// debugStats = stats";
+ return txt;
+}
+
+/* Guestimates a reasonable variable name */
+function varname(name) {
+ return name.toLowerCase().replace(/\s+/g, "_");
+}
+
+program
+ .command("foo [bar...]")
+ .action(function() {
+ console.log(program.args)
+ });
+
+program
+ .command("new [comma,seperated,list,of,scene,names]")
+ .description("Create a new ChoiceScript project named NAME.\nYou may optionally pass a list of scene names to be generated.")
+ .option("--no-stats", "Run --no-stats to skip generating choicescript_stats.txt", true)
+ .option("--stats-list ", "Generate these stats (quoting the argument is recommended). `stat` is the name of the stat, type can be `percent` (default), `text` or `opposed_pair(NAME)`", function(arg) {
+
+ out = {};
+ arg.split(/\s*,\s*/).forEach(function(stat) {
+ stat = stat.split(/\s*\:\s*/);
+ if (stat[1]) {
+ if(m = stat[1].match(/opposed_pair\(\s*(.+)\s*\)/)) {
+ out[stat[0]] = {
+ type: "opposed_pair",
+ opposed_to: m[1]
+ }
+ } else {
+ out[stat[0]] = {type: stat[1]};
+ }
+ } else {
+ out[stat[0]] = {type: "percent"};
+ }
+ });
+ return out;
+ })
+ .action(function(name, scenes, opts) {
+ path = "./" + name;
+ fs.mkdirSync(path, 0777);
+ path += "/";
+
+ if(scenes) {
+ scenes = scenes.split(",");
+ } else {
+ scenes = ["startup", "ending"];
+ }
+ stats = [];
+ if (opts.statsList) {
+ for(key in opts.statsList) {
+ key = varname(key);
+ stats.push(key);
+ }
+ } else if (opts.stats) {
+ stats = ["leadership", "strength"];
+ }
+ fs.writeFileSync(path + "game.js", game_js(scenes, stats));
+ path += "scenes/";
+ fs.mkdirSync(path, 0777);
+ first = scenes.shift();
+ exec("touch " + scenes.map(function(scene){return path + scene + ".txt"}).join(' '), function(){});
+ fs.writeFileSync(path + first + ".txt", "Welcome to your very first ChoiceScript game!\n\nCopyright 2010 by Dan Fabulich.\n\nDan Fabulich licenses this file to you under the\nChoiceScript License, Version 1.0 (the \"License\"); you may\nnot use this file except in compliance with the License. \nYou may obtain a copy of the License at\n\n*link http://www.choiceofgames.com/LICENSE-1.0.txt\n\nSee the License for the specific language governing\npermissions and limitations under the License.\n\nUnless required by applicable law or agreed to in writing,\nsoftware distributed under the License is distributed on an\n\"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,\neither express or implied.\n\n*page_break\n\nYour majesty, your people are starving in the streets, and threaten revolution.\nOur enemies to the west are weak, but they threaten soon to invade. What will you do?\n\n*choice\n #Make pre-emptive war on the western lands.\n If you can seize their territory, your kingdom will flourish. But your army's\n morale is low and the kingdom's armory is empty. How will you win the war?\n *choice\n #Drive the peasants like slaves; if we work hard enough, we'll win.\n Unfortunately, morale doesn't work like that. Your army soon turns against you\n and the kingdom falls to the western barbarians.\n *finish\n #Appoint charismatic knights and give them land, peasants, and resources.\n Your majesty's people are eminently resourceful. Your knights win the day,\n but take care: they may soon demand a convention of parliament.\n *finish\n #Steal food and weapons from the enemy in the dead of night.\n A cunning plan. Soon your army is a match for the westerners; they choose\n not to invade for now, but how long can your majesty postpone the inevitable?\n *finish\n #Beat swords to plowshares and trade food to the westerners for protection.\n The westerners have you at the point of a sword. They demand unfair terms\n from you.\n *choice\n #Accept the terms for now.\n Eventually, the barbarian westerners conquer you anyway, destroying their\n bread basket, and the entire region starves.\n *finish\n #Threaten to salt our fields if they don't offer better terms.\n They blink. Your majesty gets a fair price for wheat.\n *finish\n #Abdicate the throne. I have clearly mismanaged this kingdom!\n The kingdom descends into chaos, but you manage to escape with your own hide.\n Perhaps in time you can return to restore order to this fair land.\n *finish");
+
+
+ if(opts.stats) {
+ var txt = "This is a stats screen!\n\n*stat_chart\n";
+ statsList = opts.statsList || {Leadership: {type: "percentage"}, Strength: {type: "opposed_pair", opposed_to: "Weakness"}};
+ for(key in statsList) {
+ var name = varname(key);
+ var stat = statsList[key];
+ txt += "\n " + stat.type + " ";
+ if (name == key.toLowerCase()) {
+ txt += key;
+ } else if(stat.type == "opposed_pair") {
+ txt += name + "\n " + key;
+ } else {
+ txt += name + " " + key;
+ }
+ if(stat.type == "opposed_pair") {
+ txt += "\n " + stat.opposed_to;
+ }
+ }
+ fs.writeFileSync(path + "choicescript_stats.txt", txt);
+ }
+ if(!program.quiet) {
+ console.log("Your game has been generated. You can now run it with `choicescript server`.");
+ }
+ });
+
+program
+ .command("server")
+ .description("Start your game.")
+ .option('-p, --port ', "What port should the server run on", 3030)
+ .option('--open', "Open in the browser (supported only on OSX)")
+ .action(function(opts) {
+ var SceneNavigator = function(){};
+ var express = require("express");
+ var app = express.createServer(); //program.port
+ app.set('view options', {
+ layout: false
+ });
+
+ //app.engine('html', require('ejs').renderFile);
+ app.get('/', function(req, res){
+ eval(fs.readFileSync("game.js", "utf-8"));
+ gameOptions.title = gameOptions["title"] || "My First ChoiceScript Game";
+ gameOptions.showStats = gameOptions.showStats || false;
+ gameOptions.email = gameOptions.email ||"support@choiceofgames.com";
+ var path = require.main.filename.split('/');
+ path.pop();
+ path.push('index.ejs')
+ res.render(path.join('/'), gameOptions)
+ });
+ app.use(express.static(__dirname + '/../public'));
+ app.use(express.static(fs.realpathSync('.')));
+
+ app.listen(opts.port);
+ console.log("Your game is running on http://localhost:" + opts.port + "/");
+ if (opts.open) exec("open http://localhost:" + opts.port + "/", function() {});
+ });
+
+program
+ .command("quicktest [FILES]")
+ .description("Run a test that will methodically attempt to try every possible option.")
+ .option("-w, --watch", "Watch the directory of FILE for changes and run tests automatically")
+ .action(function(files, opts) {
+ var list = [];
+ if(files) list = files.split(",");
+ // Massive watch functionallity
+ if(opts.watch) {
+ // Check if fs.watch is available
+ if(typeof fs.watch === 'function') {
+ if (list.length > 0) {
+ list.forEach(function(l) {
+ fs.watch("./scenes/" + l + ".txt", function() {
+ try {
+ eval(fs.readFileSync(__dirname + "/autotest.js", "utf-8"));
+ } catch(e) {}
+ });
+ });
+ } else {
+ function parseDir(f) {
+ fs.readdirSync(f).forEach(function(p) {
+ p = f + "/" + p;
+ if (fs.statSync(p).isFile()) {
+ fs.watch(p, function() {
+ try {
+ eval(fs.readFileSync(__dirname + "/autotest.js", "utf-8"));
+ } catch(e) {}
+ });
+ } else if(fs.statSync(p).isDirectory()) {
+ parseDir(p);
+ }
+ });
+ }
+ parseDir(fs.realpathSync("."));
+ }
+ } else {
+ process.stderr.write("fs.watch is not supported by your OS.");
+ process.exit(-1);
+ }
+ } // end --watch support
+ try {
+ eval(fs.readFileSync(__dirname + "/autotest.js", "utf-8"));
+ } catch(e) {}
+ });
+
+program
+ .command("randomtest [iterations] [random-seed]")
+ .description("Run a test that will randomly step trough your game and measure coverage of every line.")
+ .action(function(iterations, randomSeed) {
+ if (iterations == null) {
+ iterations = 10000;
+ }
+ if (randomSeed == null) {
+ randomSeed = 1;
+ }
+ eval(fs.readFileSync(__dirname + "/randomtest.js", "utf-8"));
+ });
+
+program
+ .command("test")
+ .description("Run both quicktest and randomtest")
+ .action(function() {
+ var iterations = 10000;
+ var randomSeed = 1;
+ eval(fs.readFileSync(__dirname + "/randomtest.js", "utf-8"));
+ eval(fs.readFileSync(__dirname + "/autotest.js", "utf-8"));
+ });
+
+program
+ .command("play")
+ .description("Play on the command line")
+ .option('-i, --input ', 'The input sequence')
+ .action(function(opts) {
+ if(opts.input)
+ choiceInput = opts.input.split(',');
+ else
+ choiceInput = [];
+ eval(fs.readFileSync(__dirname + "/cliplay.js", "utf-8"));
+ });
+
+program.on("--help", function() {
+ console.log(" For more information on command line usage type `man choicescript`\n For more information on ChoiceScript syntax type `man choicescript-syntax`.");
+});
+
+program
+ .parse(process.argv);
+
+if (program.args.length == 0) {
+ console.log("Usage: " + program.name + " "+ program.usage() + "\n Type `" + program.name + " --help` for more information.");
+}
\ No newline at end of file
diff --git a/lib/cliplay.js b/lib/cliplay.js
new file mode 100644
index 00000000..cceb3c30
--- /dev/null
+++ b/lib/cliplay.js
@@ -0,0 +1,840 @@
+var gameName = "mygame";
+var args;
+if (typeof java == "undefined") {
+ // args = process.argv;
+ // args.shift();
+ // args.shift();
+ var fs = require('fs');
+ var path = require('path');
+ eval(fs.readFileSync(__dirname + "/../public/scene.js", "utf-8"));
+ eval(fs.readFileSync(__dirname + "/../public/navigator.js", "utf-8"));
+ eval(fs.readFileSync(__dirname + "/../public/util.js", "utf-8"));
+ //eval(fs.readFileSync(__dirname + "/headless.js", "utf-8"));
+ eval(fs.readFileSync("./game.js", "utf-8"));
+ function print(str) {
+ console.log(str);
+ }
+} else {
+ args = arguments;
+ load(__dirname + "/../public/scene.js");
+ load(__dirname + "/../public/navigator.js");
+ load(__dirname + "/../public/util.js");
+ load(__dirname + "./headless.js");
+ load("game.js");
+}
+//if (args[1]) gameName = args[1];
+
+//var randomSeed = args[2] || 1;
+
+function initStore() { return false; }
+headless = true;
+doneLoading = function() {}
+
+var choiceStack = [];
+
+
+function listenForKey() {
+ var ke = new (require('events').EventEmitter);
+ var keypress = require('keypress');
+ keypress(process.stdin);
+ var self = this;
+ // listen for the "keypress" event
+
+ process.stdin.on('keypress', function (ch, key) {
+ //console.log(ch, typeof ch, key);
+ if (key && key.ctrl && key.name == 'c') {
+ process.stdin.pause();
+ return;
+ }
+ if (key) {
+ ke.emit(key.name);
+ } else if(typeof(key) == 'undefined' && ch) {
+ ke.emit(ch);
+ }
+ ke.emit('keypress', ch, key);
+ });
+ ke.setMaxListeners(50);
+ process.stdin.setRawMode(true);
+ process.stdin.resume();
+
+
+ return ke;
+}
+
+var keyEmitter = listenForKey();
+
+function slurpFile(name, throwOnError) {
+ return slurpFileLines(name, throwOnError).join('\n');
+}
+
+function slurpFileLines(name, throwOnError) {
+ if (typeof java != "undefined") {
+ var lines = [];
+ var reader = new java.io.BufferedReader(new java.io.InputStreamReader(new java.io.FileInputStream(name), "UTF-8"));
+ var line;
+ for (var i = 0; line = reader.readLine(); i++) {
+ if (i == 0 && line.charCodeAt(0) == 65279) line = line.substring(1);
+ if (throwOnError) {
+ var invalidCharacter = line.match(/^(.*)\ufffd/);
+ if (invalidCharacter) throw new Error("line " + (i+1) + ": invalid character. Is this text Unicode?\n" + invalidCharacter[0]);
+ }
+ lines.push(line);
+ }
+ return lines;
+ } else {
+ var blob = fs.readFileSync(name, "utf-8");
+ var lines = blob.split(/\r?\n/);
+ var firstLine = lines[0]
+ // strip byte order mark
+ if (firstLine.charCodeAt(0) == 65279) lines[0] = firstLine.substring(1);
+ if (throwOnError) {
+ for (var i = 0; i < lines.length; i++) {
+ var line = lines[i];
+ var invalidCharacter = line.match(/^(.*)\ufffd/);
+ if (invalidCharacter) throw new Error("line " + (i+1) + ": invalid character. Is this text Unicode?\n" + invalidCharacter[0]);
+ }
+ }
+ return lines;
+ }
+}
+
+Scene.prototype.ending = function ending() {
+ process.exit();
+}
+
+Scene.prototype.renderOptions = function renderOptions(groups, options, callback) {
+
+ this.paragraph();
+
+ var self = this;
+
+
+ if (!options) throw new Error(this.lineMsg()+"undefined options");
+ if (!options.length) throw new Error(this.lineMsg()+"no options");
+ // global num will be used to assign accessKeys to the options
+ var globalNum = 1;
+ var currentOptions = options;
+
+ for (var groupNum = 0; groupNum < groups.length; groupNum++) {
+ var group = groups[groupNum];
+ if (group) {
+ var textBuilder = ["Select "];
+ textBuilder.push(/^[aeiou]/i.test(group)?"an ":"a ");
+ textBuilder.push(group);
+ textBuilder.push(":");
+
+ println(textBuilder.join());
+ }
+ var checked = null;
+ for (var optionNum = 0; optionNum < currentOptions.length; optionNum++) {
+ var option = currentOptions[optionNum];
+ if (!checked && !option.unselectable) checked = option;
+ var isLast = (optionNum == currentOptions.length - 1);
+ //this.printRadioButton(div, group, option, optionNum, globalNum++, isLast, checked == option);
+ (function(globalNum, groupNum, option) {
+ keyEmitter.once(globalNum.toString(), function() {
+ var variable = "choice_" + (groupNum+1);
+ self.temps[variable] = null;
+ self.setVar(variable, option.name);
+ if (!callback) callback = self.standardResolution;
+ callback.call(self, option);
+ });
+ self.printLine("["+ globalNum +"] " + option.name + "\n");
+ })(globalNum++, groupNum, option);
+
+
+
+ }
+ // for rendering, the first options' suboptions should be as good as any other
+ currentOptions = currentOptions[0].suboptions;
+ }
+}
+
+main = {};
+
+function log(msg, always) {
+ if(always || !program.quiet)
+ print(msg);
+}
+
+// var printed = [];
+// printx = println = function printx(msg, parent) {
+// //printed.push(msg);
+// }
+
+
+slurps = {}
+function slurpFileCached(name) {
+ if (!slurps[name]) slurps[name] = slurpFile(name);
+ return slurps[name];
+}
+
+function debughelp() {
+ debugger;
+}
+
+function noop() {}
+//Scene.prototype.page_break = noop;
+//Scene.prototype.printLine = noop;
+// Scene.prototype.subscribe = noop;
+Scene.prototype.save = function(callback) {
+ //console.log(callback.toString());
+ if (callback) callback.call();
+};
+Scene.prototype.stat_chart = function() {
+ this.parseStatChart();
+}
+
+Scene.prototype.save_game = function(data) {
+ var stack = this.tokenizeExpr(data);
+ var result = this.evaluateExpr(stack);
+}
+
+// Scene.prototype.restore_game = function() {};
+
+crc32 = noop;
+
+// parsedLines = {};
+// Scene.prototype.oldLoadLines = Scene.prototype.loadLines;
+// Scene.prototype.loadLines = function cached_loadLines(str) {
+// var parsed = parsedLines[str];
+// if (parsed) {
+// this.labels = parsed.labels;
+// this.lines = parsed.lines;
+// return;
+// } else {
+// this.oldLoadLines(str);
+// parsedLines[str] = {labels: this.labels, lines: this.lines};
+// }
+// }
+
+// cachedNonBlankLines = {};
+// Scene.prototype.oldNextNonBlankLine = Scene.prototype.nextNonBlankLine;
+//
+// Scene.prototype.nextNonBlankLine = function cached_nextNonBlankLine(includingThisOne) {
+// var key = this.name+" "+this.lineNum +""+ !!includingThisOne;
+// var cached = cachedNonBlankLines[key];
+// if (cached) {
+// return cached;
+// }
+// cached = this.oldNextNonBlankLine(includingThisOne);
+// cachedNonBlankLines[key] = cached;
+// return cached;
+// }
+
+cachedTokenizedExpressions = {};
+// Scene.prototype.oldTokenizeExpr = Scene.prototype.tokenizeExpr;
+// Scene.prototype.tokenizeExpr = function cached_tokenizeExpr(str) {
+// var cached = cachedTokenizedExpressions[str];
+// if (cached) return cloneStack(cached);
+// cached = this.oldTokenizeExpr(str);
+// cachedTokenizedExpressions[str] = cloneStack(cached);
+// return cached;
+// function cloneStack(stack) {
+// var twin = new Array(stack.length);
+// var i = stack.length;
+// while (i--) {
+// twin[i] = stack[i];
+// }
+// return twin;
+// }
+// }
+
+
+// Scene.prototype.ending = function () {
+// this.reset();
+// this.finished = true;
+// }
+
+// Scene.prototype.restart = Scene.prototype.ending;
+
+// Scene.prototype.input_text = function(variable) {
+// this.setVar(variable, "blah blah");
+// }
+
+// Scene.prototype.input_number = function(data) {
+// this.rand(data);
+// }
+
+// Scene.prototype.finish = function random_finish() {
+// var nextSceneName = this.nav && nav.nextSceneName(this.name);
+// this.finished = true;
+// // if there are no more scenes, then just halt
+// if (!nextSceneName) {
+// return;
+// }
+// var scene = new Scene(nextSceneName, this.stats, this.nav, this.debugMode);
+// scene.resetPage();
+// }
+
+
+// Scene.prototype.choice = function choice(data, fakeChoice) {
+// var groups = ["choice"];
+// if (data) groups = data.split(/ /);
+// var choiceLine = this.lineNum;
+// var options = this.parseOptions(this.indent, groups);
+// var flattenedOptions = [];
+// flattenOptions(flattenedOptions, options);
+//
+// var index = randomIndex(flattenedOptions.length);
+//
+// var item = flattenedOptions[index];
+// if (fakeChoice) this.temps.fakeChoiceEnd = this.lineNum;
+// this.getFormValue = function(name) {return item[name];}
+//
+// log(this.name + " " + (choiceLine+1)+'#'+(index+1)+' ('+item.ultimateOption.line+')');
+// var self = this;
+// timeout = function() {self.resolveChoice(options, groups);}
+// this.finished = true;
+//
+// function flattenOptions(list, options, flattenedOption) {
+// if (!flattenedOption) flattenedOption = {};
+// for (var i = 0; i < options.length; i++) {
+// var option = options[i];
+// flattenedOption[option.group] = i;
+// if (option.suboptions) {
+// flattenOptions(list, option.suboptions, flattenedOption);
+// } else {
+// flattenedOption.ultimateOption = option;
+// if (!option.unselectable) list.push(dojoClone(flattenedOption));
+// }
+// }
+// }
+//
+// function dojoClone(/*anything*/ o){
+// // summary:
+// // Clones objects (including DOM nodes) and all children.
+// // Warning: do not clone cyclic structures.
+// if(!o){ return o; }
+// if(o instanceof Array || typeof o == "array"){
+// var r = [];
+// for(var i = 0; i < o.length; ++i){
+// r.push(dojoClone(o[i]));
+// }
+// return r; // Array
+// }
+// if(typeof o != "object" && typeof o != "function"){
+// return o; /*anything*/
+// }
+// if(o.nodeType && o.cloneNode){ // isNode
+// return o.cloneNode(true); // Node
+// }
+// if(o instanceof Date){
+// return new Date(o.getTime()); // Date
+// }
+// // Generic objects
+// r = new o.constructor(); // specific to dojo.declare()'d classes!
+// for(i in o){
+// if(!(i in r) || r[i] != o[i]){
+// r[i] = dojoClone(o[i]);
+// }
+// }
+// return r; // Object
+// }
+//
+// }
+
+ Scene.prototype.loadScene = function loadScene() {
+ var file = slurpFileCached('./scenes/'+this.name+'.txt');
+ this.loadLines(file);
+ this.loaded = true;
+ if (this.executing) {
+ this.execute();
+ }
+ }
+
+clearScreen();
+ var coverage = {};
+var sceneNames = [];
+
+ Scene.prototype.rollbackLineCoverage = function(lineNum) {
+ if (!lineNum) lineNum = this.lineNum;
+ coverage[this.name][lineNum]--;
+ }
+
+ try {
+ Scene.prototype.__defineGetter__("lineNum", function() { return this._lineNum; });
+ Scene.prototype.__defineSetter__("lineNum", function(val) {
+ var sceneCoverage;
+ if (!coverage[this.name]) {
+ sceneNames.push(this.name);
+ coverage[this.name] = [];
+ }
+ sceneCoverage = coverage[this.name];
+
+ if (sceneCoverage[val]) {
+ sceneCoverage[val]++;
+ } else {
+ sceneCoverage[val] = 1;
+ }
+ this._lineNum = val;
+ });
+ } catch (e) {
+ // IE doesn't support getters/setters; no coverage for you!
+ }
+
+nav.setStartingStatsClone(stats);
+
+// for (i = 0; i < iterations; i++) {
+ // log("*****" + i);
+ timeout = null;
+ var scene = new Scene(nav.getStartupScene(), stats, nav, false);
+ try {
+ scene.execute();
+ for (var i = choiceInput.length - 1; i >= 0; i--){
+ keyEmitter.emit(choiceInput[i]);
+ };
+ while (timeout) {
+ var fn = timeout;
+ timeout = null;
+ fn();
+ }
+ } catch (e) {
+ print("RANDOMTEST FAILED\n");
+ print(e);
+ if (typeof java != "undefined") {
+ java.lang.System.exit(1);
+ } else {
+ process.exit(1);
+ }
+ }
+ //nav.resetStats(stats);
+// }
+
+// for (i = 0; i < sceneNames.length; i++) {
+ // var sceneName = sceneNames[i];
+ var sceneLines = slurpFileLines('./scenes/'+sceneNames[0]+'.txt');
+ //var sceneCoverage = coverage[sceneName];
+ // for (var j = 0; j < sceneCoverage.length; j++) {
+ // log(sceneName + " "+ (sceneCoverage[j] || 0) + ": " + sceneLines[j], true);
+ // }
+// }
+// log("RANDOMTEST PASSED", true);
+
+
+
+
+
+// define cli UI
+
+/*
+ * Copyright 2010 by Dan Fabulich.
+ *
+ * Dan Fabulich licenses this file to you under the
+ * ChoiceScript License, Version 1.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.choiceofgames.com/LICENSE-1.0.txt
+ *
+ * See the License for the specific language governing
+ * permissions and limitations under the License.
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
+ * either express or implied.
+ */
+
+
+function printx(msg, parent) {
+ if (msg == null) return;
+ if (msg === "") return;
+ process.stdout.write(msg);
+}
+
+function println(msg, parent) {
+ //if (!parent) parent = document.getElementById('text');
+ console.log(msg);
+ // var br = global.document.createElement("br");
+ // parent.appendChild(br);
+}
+
+
+function showStats() {
+ if (global.showingStatsAlready) return;
+ global.showingStatsAlready = true;
+
+ var currentScene = global.stats.scene;
+
+ var scene = new Scene("choicescript_stats", global.stats, this.nav);
+ scene.save = function(callback) {if (callback) callback.call(scene);}; // Don't save state in stats screen, issue #70
+ // TODO ban *choice/*page_break/etc. in stats screen
+ scene.finish = scene.autofinish = function(buttonName) {
+ this.finished = true;
+ this.paragraph();
+ // var p = document.createElement("p");
+ // var restartLink = document.createElement("a");
+ // restartLink.setAttribute("style", "text-decoration: underline; cursor: pointer; text-align: left");
+ // restartLink.onclick = function() {
+ // if (global.confirm("Restart your game? Did you click that intentionally?")) {
+ // global.showingStatsAlready = false;
+ // document.getElementById("statsButton").style.display = "inline";
+ // clearCookie(function() {
+ // global.nav.resetStats(global.stats);
+ // clearScreen(restoreGame);
+ // }, "");
+ // }
+ // return false;
+ // }
+ // restartLink.innerHTML = "Start Over from the Beginning";
+ // p.appendChild(restartLink);
+ // var text = document.getElementById('text');
+ // text.appendChild(p);
+ //
+ // printButton(buttonName || "Next", main, false, function() {
+ // global.stats.scene = currentScene;
+ // global.showingStatsAlready = false;
+ // document.getElementById("statsButton").style.display = "inline";
+ // clearScreen(loadAndRestoreGame);
+ // });
+ }
+ scene.execute();
+}
+
+function callIos(scheme, path) {}
+
+function asyncAlert(message, callback) {
+ if (false/*global.isIosApp*/) {
+ // TODO asyncAlert
+ global.alertCallback = callback;
+ callIos("alert", message)
+ } else {
+ setTimeout(function() {
+ console.log(message);
+ if (callback) callback();
+ }, 0);
+ }
+}
+
+function clearScreen(code) {
+ console.log('clearScreen');
+ process.stdout.write('\u001B[2J\u001B[0;0f');
+ if(code) code();
+}
+
+function safeSubmit(code) {
+ return function safelySubmitted() {
+ safeCall(code);
+ return false;
+ }
+}
+
+function startLoading() {
+ // var loading = document.getElementById('loading');
+ // if (!loading) {
+ // loading = document.createElement('div');
+ // loading.setAttribute("id", "loading");
+ // loading.innerHTML = "Loading...

";
+ // main.appendChild(loading);
+ // }
+}
+
+function doneLoading() {
+ // var loading = document.getElementById('loading');
+ // if (loading) loading.parentNode.removeChild(loading);
+ // // TODO update header?
+}
+
+function setClass(element, classString) {
+ // element.setAttribute("class", classString);
+ // element.setAttribute("className", classString);
+}
+
+function printFooter() {
+ // var footer = document.getElementById('footer');
+ // We could put anything we want in the footer here, but perhaps we should avoid it.
+ // setTimeout(function() {callIos("curl");}, 0);
+}
+
+function printImage(source, alignment) {
+ // var img = document.createElement("img");
+ // img.src = source;
+ // setClass(img, "align"+alignment);
+ // document.getElementById("text").appendChild(img);
+}
+
+function moreGames() {
+
+}
+
+function printShareLinks(target, now) {}
+function subscribe() {}
+
+// Callback expects a map from product ids to booleans
+function checkPurchase(products, callback) {
+}
+
+function isRestorePurchasesSupported() {
+}
+
+function restorePurchases(callback) {
+}
+// Callback expects a localized string, or "", or "free", or "guess"
+function getPrice(product, callback) {
+}
+// Callback expects no args, but should only be called on success
+function purchase(product, callback) {
+}
+
+function isFullScreenAdvertisingSupported() {
+ return false;
+}
+
+function showFullScreenAdvertisement(callback) {
+}
+
+function showTicker(target, endTimeInSeconds, finishedCallback, skipCallback) {
+ if (!target) target = document.getElementById('text');
+ var div = document.createElement("div");
+ target.appendChild(div);
+ var timerDisplay = document.createElement("div");
+ div.appendChild(timerDisplay);
+ var timer;
+
+ var defaultStatsButtonDisplay = document.getElementById("statsButton").style.display;
+ document.getElementById("statsButton").style.display = "none";
+
+
+ if (endTimeInSeconds > Math.floor(new Date().getTime() / 1000)) {
+ if (global.isAndroidApp) {
+ notificationBridge.scheduleNotification(endTimeInSeconds);
+ } else if (global.isIosApp) {
+ callIos("schedulenotification", endTimeInSeconds);
+ }
+ }
+
+ function cleanUpTicker() {
+ global.tickerRunning = false;
+ if (global.isAndroidApp) {
+ notificationBridge.cancelNotification();
+ } else if (global.isIosApp) {
+ callIos("cancelnotifications");
+ }
+ clearInterval(timer);
+ document.getElementById("statsButton").style.display = defaultStatsButtonDisplay;
+ }
+
+ function formatSecondsRemaining(secondsRemaining, forceMinutes) {
+ if (!forceMinutes && secondsRemaining < 60) {
+ return ""+secondsRemaining+"s";
+ } else {
+ var minutesRemaining = Math.floor(secondsRemaining / 60);
+ if (minutesRemaining < 60) {
+ var remainderSeconds = secondsRemaining - minutesRemaining * 60;
+ return ""+minutesRemaining+"m " + formatSecondsRemaining(remainderSeconds);
+ } else {
+ var hoursRemaining = Math.floor(secondsRemaining / 3600);
+ var remainderSeconds = secondsRemaining - hoursRemaining * 3600;
+ return ""+hoursRemaining+"h " + formatSecondsRemaining(remainderSeconds, true);
+ }
+ }
+ }
+
+ function tick() {
+ global.tickerRunning = true;
+ var tickerStillVisible = div.parentNode && div.parentNode.parentNode;
+ if (!tickerStillVisible) {
+ cleanUpTicker();
+ return;
+ }
+ var nowInSeconds = Math.floor(new Date().getTime() / 1000);
+ var secondsRemaining = endTimeInSeconds - nowInSeconds;
+ if (secondsRemaining >= 0) {
+ timerDisplay.innerHTML = "" + formatSecondsRemaining(secondsRemaining) + " seconds remaining";
+ } else {
+ cleanUpTicker();
+ div.innerHTML = "0s remaining";
+ if (finishedCallback) finishedCallback();
+ }
+ }
+
+ timer = setInterval(tick, 1000);
+ tick();
+
+ if (skipCallback) skipCallback(div, function() {
+ endTimeInSeconds = 0;
+ tick();
+ });
+}
+
+
+
+
+
+
+
+function printButton(name, parent, isSubmit, code) {
+ println(name);
+ //console.log(code.toString());
+ if(code) keyEmitter.once('enter', code);
+
+ // make `process.stdin` begin emitting "keypress" events
+
+ //return button;
+}
+
+function printLink(target, href, anchorText) {
+ // if (!target) target = document.getElementById('text');
+ // var link = document.createElement("a");
+ // link.setAttribute("href", href);
+ // link.appendChild(document.createTextNode(anchorText));
+ // target.appendChild(link);
+ print(anchorText);
+}
+
+function printInput(target, inputType, callback, minimum, maximum, step) {
+ printx("> ");
+ var inp = "";
+ var ignore = true;
+ function ls(ch, key) {
+ if(key && key.name == 'enter') {
+ keyEmitter.removeListener('keypress', ls);
+ callback(inp);
+ } else if(key && key.name == 'backspace') {
+ inp = inp.slice(0,inp.length-1);
+ printx("\b \b");
+ } else if(!ignore) {
+ printx(ch);
+ inp += ch;
+ } else {
+ ignore = false;
+ }
+ }
+ keyEmitter.on('keypress', ls);
+}
+
+function promptEmailAddress(target, defaultEmail, callback) {
+ // if (!target) target = document.getElementById('text');
+ // var form = document.createElement("form");
+ // var self = this;
+ // form.action="#";
+ //
+ // var message = document.createElement("div");
+ // message.style.color = "red";
+ // message.style.fontWeight = "bold";
+ // form.appendChild(message);
+ //
+ // var input = document.createElement("input");
+ // // This can fail on IE
+ // try { input.type="email"; } catch (e) {}
+ // input.name="email";
+ // input.value=defaultEmail;
+ // input.setAttribute("style", "font-size: 25px; width: 90%;");
+ // form.appendChild(input);
+ // target.appendChild(form);
+ // println("", form);
+ // println("", form);
+ // printButton("Next", form, true);
+ //
+ // printButton("Cancel", target, false, function() {
+ // callback(true);
+ // });
+ //
+ // form.onsubmit = function(e) {
+ // preventDefault(e);
+ // safeCall(this, function() {
+ // var email = trim(input.value);
+ // if (!/^\S+@\S+\.\S+$/.test(email)) {
+ // var messageText = document.createTextNode("Sorry, \""+email+"\" is not an email address. Please type your email address again.");
+ // message.innerHTML = "";
+ // message.appendChild(messageText);
+ // } else {
+ // recordEmail(email, function() {
+ // callback(false, email);
+ // });
+ // }
+ // });
+ // };
+ //
+ // setTimeout(function() {callIos("curl");}, 0);
+}
+
+function preventDefault(event) {
+ // if (!event) event = global.event;
+ // if (event.preventDefault) {
+ // event.preventDefault();
+ // } else {
+ // event.returnValue = false;
+ // }
+}
+
+function getPassword(target, code) {
+ // if (!target) target = document.getElementById('text');
+ // var textArea = document.createElement("textarea");
+ // textArea.cols = 41;
+ // textArea.rows = 30;
+ // setClass(textArea, "savePassword");
+ // target.appendChild(textArea);
+ // println("", target);
+ // printButton("Next", target, false, function() {
+ // code(false, textArea.value);
+ // });
+ //
+ // printButton("Cancel", target, false, function() {
+ // code(true);
+ // });
+}
+
+function showPassword(target, password) {
+ // if (!target) target = document.getElementById('text');
+ //
+ // var textBuffer = [];
+ // var colWidth = 40;
+ // for (var i = 0; i < password.length; i++) {
+ // textBuffer.push(password.charAt(i));
+ // if ((i + 1) % colWidth == 0) {
+ // textBuffer.push('\n');
+ // }
+ // }
+ // password = "----- BEGIN PASSWORD -----\n" + textBuffer.join('') + "\n----- END PASSWORD -----";
+ //
+ // var shouldButton = isMobile;
+ // if (shouldButton) {
+ // var button = printButton("Email My Password to Me", target, false,
+ // function() {
+ // safeCall(self, function() {
+ // if (isWeb) {
+ // // TODO more reliable system
+ // }
+ // global.location.href = "mailto:?subject=Save%20this%20password&body=" + encodeURIComponent(password);
+ // });
+ // }
+ // );
+ // setClass(button, "");
+ // }
+ //
+ // var shouldTextArea = !isMobile;
+ // if (shouldTextArea) {
+ // var textArea = document.createElement("textarea");
+ // textArea.cols = colWidth + 1;
+ // textArea.rows = 30;
+ // setClass(textArea, "savePassword");
+ //
+ // textArea.setAttribute("readonly", true);
+ // textArea.onclick = function() {textArea.select();}
+ // textArea.value = (password);
+ // target.appendChild(textArea);
+ // }
+}
+
+// global.isWebOS = /webOS/.test(navigator.userAgent);
+// global.isMobile = isWebOS || /Mobile/.test(navigator.userAgent);
+// global.isFile = /^file:/.test(global.location.href);
+// global.isWeb = /^https?:/.test(global.location.href);
+// global.isSafari = /Safari/.test(navigator.userAgent);
+// global.isIE = /MSIE/.test(navigator.userAgent);
+// global.isIPad = /iPad/.test(navigator.userAgent);
+// global.isKindleFire = /Kindle Fire/.test(navigator.userAgent);
+
+global.loadTime = new Date().getTime();
+
+
+
+// if ( document.addEventListener ) {
+// document.addEventListener( "DOMContentLoaded", global.onload, false );
+// }
+
+
+
+
diff --git a/generatorNode.js b/lib/generatorNode.js
similarity index 100%
rename from generatorNode.js
rename to lib/generatorNode.js
diff --git a/headless.js b/lib/headless.js
similarity index 100%
rename from headless.js
rename to lib/headless.js
diff --git a/web/mygame/index.html b/lib/index.ejs
similarity index 87%
rename from web/mygame/index.html
rename to lib/index.ejs
index 556c772e..d549a381 100644
--- a/web/mygame/index.html
+++ b/lib/index.ejs
@@ -23,27 +23,27 @@
-Multiple Choice Example Game | My First ChoiceScript Game
+<%= title %>
-
-
-
-
-
+
+
+
+
+
-
-
+
+
-
+