JIFFEE Source Code

"Code Is Fact, Design Is Hope."
- Jamie Dinkelacker

jiffee-checks.js
jiffee-controller.js
jiffee-cookies.js
jiffee-display.js
jiffee-encoding.js
jiffee-engine.js
jiffee-help.js
jiffee-parser-en-us.js
jiffee-persistence.js
jiffee-persons.js
jiffee-places.js
jiffee-priorities.js
jiffee-props.js
jiffee-scheduler.js
jiffee-score.js
jiffee-sha.js
jiffee-traits.js
jiffee-verbs.js

jiffee-checks.js

// JIFFEE - JavaScript Interactive Fiction Framework for Education and Entertainment
// Copyright (c) 2008 by Michael S. Kenniston.  All rights reserved.
// Full documentation is at http://jiffeegames.com

// jiffee-checks.js

// set up a proper namespace to avoid naming conflicts
var com;
if (!com) {com = {};}
if (!com.jiffeegames) {com.jiffeegames = {};}

/**
A dummy constructor, so that jsdoc can figure out that this is a class.
@constructor
@class This class defines some checking routines that help with
error handling.  These are class static routines, so there is no need
to ever create a "Checks" object.  The normal way to use these routines is:
<pre>
com.jiffeegames.Checks.validate(arguments);
...
com.jiffeegames.Checks.error(arguments, 'ABCNNN', 'message');
</pre>
It is really important to use these methods,
and to provide good explanatory messages,
because JIFFEE is intended for use by novices who don't have a clue about
programming or about JavaScript.  Without clear and specific diagnostics,
they won't stand a chance of getting their games to work correctly.
In order for these to work optimally, you must give all functions,
even "anonymous" ones, a good name, e.g.
<pre>
Wizard.prototype.shazam = function shazam(arg1, arg2, ..., argN) {
  var checks = com.jiffeegames.Checks;
  checks.validate(arguments);
  ...
  if (today() === 'tuesday') {
      checks.error(arguments,
                   'WIZ007',
                   'Magic spells are not allowed on Tuesdays.');
    }
  ...
  };
</pre>
In the rest of JIFFEE we stick to a convention of packaging things as objects
and passing them to constructors of other objects.  This use of dependency
injection makes it easier to test and isolate problems.  This pseudo-class
deliberately violates that convention, because following it would lead to
chicken-and-egg problems if a Checks argument to a constructor was missing.
*/

com.jiffeegames.Checks = function Checks() {};

/**
Throw an error with a formatted diagnostic message.
@param {Arguments} parentArgs The "arguments" taken
directly from the calling function.  This is used to create a nicely
formatted error message which includes the name and all arguments of
the calling function.
@param {String} code An arbitrary code which will be set as the
"jiffeeCode" property of the thrown error.  This can then be used by unit
tests to verify that the correct error-checking was executed.
@param {String} msg An explanatory message.  This will be included as part of
the "message" property of the the thrown error.
@returns Never returns (always throws).
@type void
@throws Error with a jiffeeCode and a nicely formatted message.
*/

com.jiffeegames.Checks.error =
  function error(parentArgs, code, msg) {
    com.jiffeegames.Checks.validate(arguments);
    var funcName = parentArgs.callee.toString().match(/function (\w*)/);
    if (funcName && funcName[1]) {
      funcName = funcName[1];
    } else {
      funcName = 'anonymous';
    }
    var argList = '';
    var alen = parentArgs.length;
    for (var adex = 0; adex < alen; adex++) {
      if (adex > 0) {
        argList += ', ';
      }
      var arg = parentArgs[adex];
      if (typeof arg === 'string') {
        argList += '"' + arg + '"';
      } else {
        argList += arg;
      }
    }
    // compatibility minefield:
    // extra ".toString()" after "msg" is a workaround for an IE6 weirdness
    var e = new Error(msg.toString() + '\n\nLast function called was: ' +
                      funcName + '(' + argList + ')');
    e.jiffeeCode = code;
    throw e;
  };

/**
Verify that the number of arguments provided matches the number expected
by the function definition.  This checks only the number of arguments;
if you need to check types you must do that with separate "if" statements.
@param {Arguments} parentArgs - The "arguments" taken
directly from the calling function, and passed on to "error" to
allow nice formatting of diagnostic messages.
@type void
@throws Error with a jiffeeCode, if the number of arguments is wrong.
*/

com.jiffeegames.Checks.validate =
  function validate(parentArgs) {
    if (arguments.length !== 1) {
      com.jiffeegames.Checks.error(
          arguments,
          'J001',
          'illegal to call with ' + arguments.length + ' arguments');
    }
    var actual = parentArgs.length;
    if (actual === parentArgs.callee.length) {
      return;
    }
    com.jiffeegames.Checks.error(
        parentArgs,
        'J002',
        'illegal to call with ' + actual + ' arguments');
  };

/**
Verify that the function is called only during the init phase.
@type void
@throws Error with a jiffeeCode, if the phase is wrong.
*/

com.jiffeegames.Checks.initOnly =
  function initOnly(parentArgs, started) {
    if (arguments.length !== 2) {
      com.jiffeegames.Checks.error(
          arguments,
          'J089',
          'illegal to call with ' + arguments.length + ' arguments');
    }
    if (!started) {
      return;
    }
    com.jiffeegames.Checks.error(
        parentArgs,
        'J090',
        'illegal to call after game has started');
  };

/**
Verify that the function is called only during the running phase.
@type void
@throws Error with a jiffeeCode, if the phase is wrong.
*/

com.jiffeegames.Checks.runningOnly =
  function runningOnly(parentArgs, started) {
    if (arguments.length !== 2) {
      com.jiffeegames.Checks.error(
          arguments,
          'J091',
          'illegal to call with ' + arguments.length + ' arguments');
    }
    if (started) {
      return;
    }
    com.jiffeegames.Checks.error(
        parentArgs,
        'J092',
        'illegal to call before game has started');
  };

/**
Format an error message for HTML.  This is intended for use at the very
top-level, where all errors are caught and displayed to the user.
@param {Error} e - The error object to be interpreted.
@type void
*/

com.jiffeegames.Checks.formatErrorInHTML =
  function formatErrorInHTML(e) {
    var res = "<font color=\"red\">JIFFEE PROGRAM ERROR";
    if (e.jiffeeCode) {
      res += " (" + e.jiffeeCode + ")";
    } else {
      res += " (JavaScript syntax error)";
    }
    var message = e.message;
    if (!message) {
      message = "no error message found";
    }
    res += ":<br><br>" + message;
    if (message.indexOf("is not a constructor") >= 0) {  // for FireFox
      res += "<br><br>This often means your quotation marks don't match,";
      res += " so be sure to check<br>";
      res += "your \"double\" and 'single' quotes to make sure they";
      res += " all appear in matched pairs.";
    }
    if (e.jiffeeCode) {
      res += "<br><br>For more information <a href=\"http://jiffeegames.com/";
      res += "jiffee-errors.html#" + e.jiffeeCode + "\">click here<\/a>.";
    } else {
      res += "<br><br>Check FireFox's \"Tools/JavaScript Console\" or ";
      res += "\"Tools/Error Console\" for<br>";
      res += "more information about this probable syntax error.";
      res += " To get helpful hints, <a href=\"http://jiffeegames.com/";
      res += "syntax-errors.html\">click here<\/a>.";
    }
    res += "<\/font>";
    return res;
  };

jiffee-controller.js

// JIFFEE - JavaScript Interactive Fiction Framework for Education and Entertainment
// Copyright (c) 2008 by Michael S. Kenniston.  All rights reserved.
// Full documentation is at http://jiffeegames.com

// jiffee-controller.js

// set up a proper namespace to avoid naming conflicts
var com;
if (!com) {com = {};}
if (!com.jiffeegames) {com.jiffeegames = {};}

/**
Create a Controller object.  The normal call is
<pre>
var controller = com.jiffeegames.Controller();
</pre>
@class A Controller object coordinates the other main objects in a game.
Its basic function is to accept input from the user,
perform all the actions appropriate to that input, and
assemble the output.
@constructor
*/

com.jiffeegames.Controller =
  function Controller() {
    com.jiffeegames.Checks.validate(arguments);
  };

com.jiffeegames.Controller.prototype.IMPLEMENTS = 'com.jiffeegames.Controller';
com.jiffeegames.Controller.prototype.USES = [
    'com.jiffeegames.Display',
    'com.jiffeegames.Parser',
    'com.jiffeegames.Scheduler',
    'com.jiffeegames.Traits'
    ];

com.jiffeegames.Controller.prototype.init =
  function init(locate, language) {
    var checks = com.jiffeegames.Checks;
    var controller = this;
    checks.validate(arguments);
    var display = locate('com.jiffeegames.Display');
    var parser = locate('com.jiffeegames.Parser');
    var scheduler = locate('com.jiffeegames.Scheduler');
    var traits = locate('com.jiffeegames.Traits');

/**
 * Take a single line of raw input from the user, execute the command(s)
 * it contains, and return the output string that should be displayed.
 * @param {String} raw The raw input from the user.
 * @returns The output which results from running the command.
 * @type String
 */

com.jiffeegames.Controller.prototype.run =
  function run(raw) {
    checks.validate(arguments);
    parser.notifyUserInput(raw);
    var command;
    while ((command = parser.getCommand()) !== undefined) {
      if (command.raw.length > 0) {
        display.show('CONTROLLER-ECHO-CMD', {literal:command.raw});
      } else {
        display.show('CONTROLLER-EMPTY-PARA');
      }

      traits.set('game', 'phase', 'begin-command');
      this.drainScheduler_();

      command.eventType = 'directive';
      scheduler.notifyEvent(command);
      this.drainScheduler_();

      traits.set('game', 'phase', 'end-command');
      this.drainScheduler_();
    }
    return display.getAndClearOutput();
  };

/**
Do all the work on the rules, one event at a time in FIFO order.
For each event, examine all the rules that match that event,
in priority-then-LIFO order, and perform the action associated with
each rule.
As soon as one of the actions outputs something or changes the
state (value of a trait), that event is done and all remaining
rules are skipped.
This normal processing order can be overridden if an action
throws a string, in which case the following occurs:
<ul>
<li>Processing of that rule stops.  In particular, if a list or nested list
of actions is being processed, the entire list is aborted at that point.
<li>If the string is "done", all processing of that event stops
(even if no output or change has occurred).
<li>If the string is "not done", processing continues with the next rule
(even if output or a change has occurred).
<li>If the string is "auto", this rule is done butprocessing will continue
to the next rule dependent on the usual output-or-change-of-state rule.
</ul>
No matter what happens with the rules of one event, when that event is
done then processing always continues on normally to the following events.
@type void
*/

com.jiffeegames.Controller.prototype.drainScheduler_ =
  function drainScheduler_() {
    checks.validate(arguments);
    var event = scheduler.nextEvent();
    while (event !== undefined) {
      var action = scheduler.nextAction();
      if (action === undefined) {
        event = scheduler.nextEvent();
        continue;
      }
      try {
        this.performAndCheckForChanges(action, event);
        // nothing changed, so continue to next rule/action
      } catch(e) {
        if (e === 'event done') {
          // continue to first rule/action of next event
          event = scheduler.nextEvent();
        } else if (e === 'event not done') {
          // something may have changed, but continue to next action anyway
        } else {
          throw e;  // this exception is indicating a real problem
        }
      }
    }
  };

/**
Start all the modules that need starting, in the correct order.
@type void
@private
*/

com.jiffeegames.Controller.prototype.start =
  function start() {
    checks.validate(arguments);
    checks.initOnly(arguments, this.isStarted_);
    traits.set('game', 'phase', 'initialize-game');
    this.drainScheduler_();
    this.isStarted_ = true;
  };

/**
Perform an action.
<ul>
<li>
If the action is a function, it will be called (as a member of the
main game object).
<li>
If the action is the name of a location (i.e.
a noun with is-location === true), the player will be moved there.
<li>
If the action is any other string, it will be displayed with show().
</ul>
@param {String_or_Function} action The action to be peformed.
@param {Event} event The event that matched this action.
@type void
*/

com.jiffeegames.Controller.prototype.perform =
  function perform(action, event) {
    checks.validate(arguments);

    var plen = this.performers_.length;
    for (var pdex = 0; pdex < plen; pdex++) {
      if (this.performers_[pdex](action, event)) {
        return;
      }
    }
    checks.error(arguments,
                 'J052',
                 'action is the wrong type to be performed');
  };

/**
Perform an action, and throw 'event done' if there are any changes
in state or any output done.
@param {String_or_Function} action The action to be peformed.
@param {Event} event The event that matched this action.
@type void
*/

com.jiffeegames.Controller.prototype.performAndCheckForChanges =
  function performAndCheckForChanges(action, event) {
    checks.validate(arguments);

    var displaySeq = display.getSeq();
    var traitsSeq = traits.getSeq();
    try {
      this.perform(action, event);
    } catch (e) {
      if (e !== 'rule done') {
        throw e;
      }
      // This rule is done, but we still want to check for changes to see
      // if the event is also done.
    }
    // If anything changed, we're done with this event.
    if ((traits.getSeq() !== traitsSeq) ||
        (display.getSeq() !== displaySeq)) {
      throw 'event done';
    }
  };

/**
Add a function to perform a type of object.
@param {function} func A function which tries to "perform" an object.
The performer function should return true if the object is the right type and the perform succeeds, or false if another performer should be tried.
Since "func" will later be called as a free function, it should normally
be a closure so that it will have access to any objects it may need.
@type void
*/

com.jiffeegames.Controller.prototype.addPerformer =
  function addPerformer(func) {
    checks.validate(arguments);
    this.performers_.unshift(func);
  };

/**
Perform a list, by performing each element of the list in order.
@returns true if the object is the right type and the perform succeeds,
false if another performer should be tried.
@type boolean
@private
*/

com.jiffeegames.Controller.prototype.performList_ =
  function performList_(action, event) {
    checks.validate(arguments);

    if (action instanceof Array) {
      for (var i = 0; i < action.length; i++) {
        this.perform(action[i], event);
      }
      return true;
    }
    return false;
  };

/**
Perform a function, by executing it.
@returns true if the object is the right type and the perform succeeds,
false if another performer should be tried.
@type boolean
@private
*/

com.jiffeegames.Controller.prototype.performFunction_ =
  function performFunction_(action, event) {
    checks.validate(arguments);

    if (typeof action === 'function') {
      action(event);
      return true;
    }
    return false;
  };

/**
Perform a string, by passing it to show().
@returns true if the object is the right type and the perform succeeds,
false if another performer should be tried.
@type boolean
@private
*/

com.jiffeegames.Controller.prototype.performString_ =
  function performString_(action, event) {
    checks.validate(arguments);
    if (typeof action === 'string') {
      display.show(action);
      return true;
    }
    return false;
  };

// continuation of init()
    this.performers_ = [];
    this.isStarted_ = false;

    // Performers are called as free functions with no "this", so we need to
    // create a closure here that allows access back to the controller object
    // via "this" inside the function.

    this.addPerformer(function(action, event){ 
                          return controller.performList_(action, event);});
    this.addPerformer(function(action, event){
                          return controller.performFunction_(action, event);});
    this.addPerformer(function(action, event){
                          return controller.performString_(action, event);});
    traits.addNoun('game');
    traits.addEnum('phase', 'initialize-game', 'begin-command', 'end-command');
    traits.set('game', 'phase', 'end-command');

    // language-independent formatting
    display.translateTo('CONTROLLER-ECHO-CMD', '<p><em>${1}</em></p>');
    display.translateTo('CONTROLLER-EMPTY-PARA', '<p><em>&nbsp;</em></p>');
  };

jiffee-cookies.js

// JIFFEE - JavaScript Interactive Fiction Framework for Education and Entertainment
// Copyright (c) 2008 by Michael S. Kenniston.  All rights reserved.
// Full documentation is at http://jiffeegames.com

// jiffee-cookies.js

// set up a proper namespace to avoid naming conflicts
var com;
if (!com) {com = {};}
if (!com.jiffeegames) {com.jiffeegames = {};}

/*global document */

/**
Create a Cookies object.  The normal call is
<pre>
var cookies = new com.jiffeegames.Cookies();
</pre>
You normally only need a single Cookies object.
@constructor
@requires com.jiffeegames.Checks
@class A Cookies object encapsulates all the cookies in a browser.
In JIFFEE we use a cookie value to store the encoded state of the game,
and we use the game fingerprint as part of the cookie-name.
*/

com.jiffeegames.Cookies =
  function Cookies() {
    com.jiffeegames.Checks.validate(arguments);
  };

com.jiffeegames.Cookies.prototype.IMPLEMENTS = 'com.jiffeegames.Cookies';
com.jiffeegames.Cookies.prototype.USES = [];

com.jiffeegames.Cookies.prototype.init =
  function init(locate, language) {
    var checks = com.jiffeegames.Checks;
    var cookies = this;
    checks.validate(arguments);

/**
Get the characters that can be used in cookie values.
These are all the ASCII printable characters, with the following exceptions:
<ul>
<li>
double-quote and backslash - to avoid confusion with quoted or escaped strings
<li>
space, comma, semicolon - used by cookie formatting, as per the standard
</ul>
This lets us utilize aproximately 6.5 bits out of every byte.
These are deliberately not in ASCII order to provide added obfuscation.
@returns A list of all the legal cookie characters.
@type String
*/

com.jiffeegames.Cookies.prototype.legalChars =
  function legalChars() {
    return "aAbBcCdDeEfFgGhHiIjJkKlLmMnNoOpPqQrRsStTuUvVwWxXyYzZ" +
           "0123456789!@#$%^&*()-_+=[]{}|:'.<>/?~`";
  };

/**
Retrieve the value of a named cookie from the browser.
@param {String} cName The name of the cookie whose value should be retrieved.
@returns The value of the cookie, or undefined if no such cookie exists.
@type String
*/

com.jiffeegames.Cookies.prototype.get =
  function get(cName) {
    checks.validate(arguments);
    var pos = cName.length + 1;
    var allCookies = document.cookie.split(/; */);
    var clen = allCookies.length;
    for (var cdex = 0; cdex < clen; cdex++) {
      var c = allCookies[cdex];
      if (c.substring(0, pos) === cName + '=') {
        var result = c.substring(pos);
        if (result) {
          return result;
        }
        return undefined;
      }
    }
    return undefined;
  };

/**
Set the value of a browser cookie.
@param {String} cName The name of the cookie.
@param {String} value The value of the cookie.  This must already be legally
encoded, using only allowed characters.
@param {Number} daysToLive The lifetime of the cookie.
@type void
@throws Error if the value is too long.
*/

com.jiffeegames.Cookies.prototype.set =
  function set(cName, value, daysToLive) {
    checks.validate(arguments);
    var assignment = cName + '=' + value;
    if (assignment.length > 4096) {
      checks.error(arguments,
                   'J004',
                   'cookie string too long(' + assignment.length + ')');
    }
    var date = new Date();
    date.setTime(date.getTime() + 1000*3600*24*daysToLive);
    // You _must_ use the old "expires=" instead of the new "max-age="
    // in order to maintain browser compatibility, e.g. with IE7.
    // This is a particularly nasty bug, because it only shows up when
    // the browser is restarted, which means that a normal jsunit test
    // cannot detect it.
    document.cookie = assignment + '; expires=' + date.toGMTString();
  };

/**
Remove a browser cookie.
@param {String} cName The name of the cookie to be removed.
@type void
*/

com.jiffeegames.Cookies.prototype.remove =
  function remove(cName) {
    checks.validate(arguments);
    document.cookie = cName + '=; expires=0';
    // paranoia check
    if (this.get(cName)) {
      checks.error(arguments, 'J026',
                  'internal error - cookie could not be removed');
    }
  };

/**
Test whether cookies are enabled.
Note that if this returns false, then get/set/remove() will be no-ops.
@returns true if cookies are working, else false.
@type Boolean
*/

com.jiffeegames.Cookies.prototype.areEnabled =
  function areEnabled() {
    checks.validate(arguments);
    var n = 'testWhetherCookiesAreEnabled';
    var v = 'yesTheyCertainlyAre';
    this.set(n, v, 2);
    var res = this.get(n);
    this.remove(n);
    return res === v;
  };

/**
Clear all cookies in this domain.
This function is intended only for testing, where it is needed as a work-around
for the IE bug that limits a domain to a single cookie of maximum 4 KB size.
If you call if from normal code, you can wipe out the state of unrelated games.
@type void
*/

com.jiffeegames.Cookies.prototype.clearAll_ =
  function clearAll_() {
    var cNames = [];
    var allCookies = document.cookie.split(/; */);
    var clen = allCookies.length;
    var cdex;
    for (cdex = 0; cdex < clen; cdex++) {
      var c = allCookies[cdex];
      cNames[cNames.length] = c.split('=')[0];
    }
    for (cdex = 0; cdex < cNames.length; cdex++) {
      document.cookie = cNames[cdex] + '=; expires=0';
    }
  };

// continuation of init()
  };

jiffee-display.js

// JIFFEE - JavaScript Interactive Fiction Framework for Education and Entertainment
// Copyright (c) 2008 by Michael S. Kenniston.  All rights reserved.
// Full documentation is at http://jiffeegames.com

// jiffee-display.js

// set up a proper namespace to avoid naming conflicts
var com;
if (!com) {com = {};}
if (!com.jiffeegames) {com.jiffeegames = {};}

/**
Create a Display object.  The normal call is
<pre>
var display = com.jiffeegames.Display();
</pre>
@class A Display object manages the output to the display.
It accepts output from the rest of the program (usually in
units of complete sentences), 
translates them as needed,
sanitizes any user-input that is being re-displayed,
and accumulates all the output in a buffer until it is needed.
@constructor
*/

com.jiffeegames.Display =
  function Display() {
    com.jiffeegames.Checks.validate(arguments);
  };

com.jiffeegames.Display.prototype.IMPLEMENTS = 'com.jiffeegames.Display';
com.jiffeegames.Display.prototype.USES = [];

com.jiffeegames.Display.prototype.init =
  function init(locate, language) {
    var checks = com.jiffeegames.Checks;
    var display = this;
    checks.validate(arguments);

/**
Return the current change sequence number.
@type int
*/

com.jiffeegames.Display.prototype.getSeq =
  function getSeq() {
    checks.validate(arguments);
    return this.seq_;
  };

/**
Display a formatted string to the user.
Consecutive calls to this method will be handled efficiently,
because the final string concatenation is not done until you actually
retrieve the entire results.  Example:
<pre>
display.show('The ${1} has no effect on the ${2}.', 'wand', 'mouse');
</pre>
which will output
<pre>
The wand has no effect on the mouse.
</pre>
It is important to use this function, rather than building up strings
"by hand" using concatenation,
since this method allows much easier translation into other languages.
@param {String} template The basic form of the string to be displayed.
@param {String} args Zero through nine arguments,
which will be substituted for ${1} through ${9} in the template.
@type void
*/

com.jiffeegames.Display.prototype.show =
  function show(template, args) {
    if (arguments.length < 1 || arguments.length > 10) {
      checks.validate(arguments);
    }

    var result = this.translate_(template);
    for (var i = 1; i <= 9; i++) {
      var tag = '${' + String(i) + '}';
      var pos = result.indexOf(tag);
      if (i < arguments.length) {
        if (pos >= 0) {
          result = result.replace(tag, this.translate_(arguments[i]));
        } else {
          checks.error(arguments,
                      'J055',
                      'you have an argument but no ' + tag + ' to match');
        }
      } else {
        if (pos >= 0) {
          checks.error(arguments,
                      'J071',
                      'you have a ' + tag + ' but no argument to match');
        }
      }
    }

    if (result.length > 0 ) {
      this.outputBuffer_.push(result);
      this.seq_++;
    }
  };

/**
Erase the last thing shown.
@type void
*/

com.jiffeegames.Display.prototype.erase =
  function erase() {
    checks.validate(arguments);
    this.outputBuffer_.pop();
  };

/**
Get the entire string which should be displayed to the user.
This is the concatenation of everything which has been
passed to "show()".
@returns Everything that should be displayed, as one long string.
@type String
*/

com.jiffeegames.Display.prototype.getAndClearOutput =
  function getAndClearOutput() {
    checks.validate(arguments);
    var result = this.outputBuffer_.join('');
    this.outputBuffer_ = [];
    this.seq_++;
    return result;
  };

/**
Sanitize a string to make it safe for output by removing any HTML markup.
This needs to be called whenever user input is displayed on the screen,
so that the user cannot accidentally or deliberately mess up the
display formatting by typing in HTML.
@param {String} str The string to be sanitized.
@returns The santized version of the input string.
@type String
@private
*/

com.jiffeegames.Display.prototype.sanitize_ =
  function sanitize_(str) {
    checks.validate(arguments);
    if (typeof str !== 'string') {
      checks.error(arguments, 'J053', 'argument must be a string');
    }

    str = str.replace(/&/g, '&amp;');
    str = str.replace(/</g, '&lt;');
    str = str.replace(/>/g, '&gt;');
    return str;
  };

/**
Define an output translation.
@param {String} from The output in internal/canonical form.
@param {String} to The translation of the output into the target language.
If "null", then translation does not alter this string.
@type void
@throws Error with a jiffeeCode if the args are bad.
*/

com.jiffeegames.Display.prototype.translateTo =
  function translateTo(from, to) {
    checks.validate(arguments);
    if (to === null) {
      to = from;
    }
    if (typeof from !== 'string' || typeof to !== 'string') {
      checks.error(arguments, 'J060', 'both arguments must be strings');
    }
    if (from in this.map_) {
      checks.error(arguments,
                   'J061',
                   'A translation is already defined for this output.'); 
    }
    this.map_[from] = to;
  };

/**
Translate a single string
from internal/canonical form into the target language.
If no translation is defined, it returns the input unchanged.
@param {String} from The desired output in internal form.
If the argument is of the form {literal:somestring}, then no
translation is performed, but the string will be sanitized.
@returns The translation of the output into the target language.
@type String
@throws Error with a jiffeeCode if the args are bad.
@private
*/

com.jiffeegames.Display.prototype.translate_ =
  function translate_(from) {
    checks.validate(arguments);
    if (typeof from !== 'string') {
      if (typeof from.literal === 'string') {
        return this.sanitize_(from.literal);
      }
      checks.error(arguments, 'J064', 'argument must be a string');
    }
    if (from === '') {
      return '';
    }
    var res = this.map_[from];
    if (res) {
      return res;
    }
    if (this.strict_) {
      checks.error(arguments,
                   'J065',
                   'no translation defined for this output');
    }
    return from;
  };

/**
Define the desired action if a string has no translation specified.
The default is strict=false, because even though displaying an untranslated
string to the player is very bad form, it's still a lot better than having the
game crash.
<p>
This setting is intended for use by game authors during testing:  When you
write a game that is intended to be friendly to translators, you should
set strict=true, so that you will get an error whenever you try to output a
string which has no translation.  If you only speak English it's perfectly
fine to translate English-to-English;
the idea is just to ensure that there is a translateTo() call for every
output string.
<p>
This will then make it much easier for somebody else, who is perhaps not
a good programmer but who is fluent in the languages involved, to actually
do the translation in a straightforward and reliable way.
@param {Boolean} strict true if undefined translations should cause an error.
@type void
*/

com.jiffeegames.Display.prototype.setStrictTranslation =
  function setStrictTranslation(strict) {
    checks.validate(arguments);
    if (typeof strict !== 'boolean') {
      checks.error(arguments, 'J057', 'argument must be boolean');
    }
    this.strict_ = strict;
  };

/**
Compare two translation tables for consistency.
Assume the translation table in this Display object is correct,
and compare it to the translation table in the "other" Display object.
Throw an exception if they don't have exactly the same keys (i.e. "from"
values), otherwise return silently.
<p>
This is intended to be used by game translators.
If you have one translation (e.g. English) that has been thoroughly tested
to make sure it includes all the needed strings, you can use this function to
quickly compare it to a different version of your game (e.g. Spanish) without
having to repeat all the tests just to find missing entries in the other table.
@param {Display} other Another display object.
@type void
*/

com.jiffeegames.Display.prototype.compareTranslations =
  function compareTranslations(other) {
    checks.validate(arguments);
    var master = this.map_;
    var slave = other.map_;
    var diffs = [];

    var key;
    for (key in master) {
      if (!(key in slave)) {
        diffs.push('missing key "' + key + '"');
      }
    }
    for (key in slave) {
      if (!(key in master)) {
        diffs.push('extra key "' + key + '"');
      }
    }

    if (diffs.length > 0) {
      checks.error(arguments,
                   'J048',
                   'Translation tables differ:\n' + diffs.join('\n'));
    }
  };

// continuation of init()

    this.map_ = {};
    this.outputBuffer_ = [];
    this.strict_ = false;
    this.seq_ = 0;

    this.translateTo('<p>', null);
    this.translateTo('</p>', null);
    this.translateTo('<br>', null);
  };

jiffee-encoding.js

// JIFFEE - JavaScript Interactive Fiction Framework for Education and Entertainment
// Copyright (c) 2008 by Michael S. Kenniston.  All rights reserved.
// Full documentation is at http://jiffeegames.com

// jiffee-encoding.js

// set up a proper namespace to avoid naming conflicts
var com;
if (!com) {com = {};}
if (!com.jiffeegames) {com.jiffeegames = {};}

/**
Create an empty Encoding object.  The normal call is
<pre>
var encoding = new com.jiffeegames.Encoding();
</pre>
@constructor
@requires com.jiffeegames.Checks
@class An Encoding object very efficiently encodes the entire state
of the game as a string, or decodes it from a string.
The model used is very general, so there should be no need to change this
code for any specific game.  It basically takes the whole string and 
breaks it up into 4-char 'chunks', each of which is interpreted
as a base-90 integer.  Each of these integers in turn is interpreted as a
mixed-base integer, where each 'place' stores the value of one variable.
Note that "variable" here means a named value being stored in the encoding,
which is not the same as a JavaScript "var".
<br><br>
The model:
<br><br>
The state is divided into 'chunks', each of which is a roughly 26-bit
integer which holds the encoded value of one or more variables.
Each chunk has both a 'chunkAsNum' and a 'chunkAsStr' which both hold the
same number and are kept in sync.
<br>
The chunkAsStr is just a base-90 representation of the integer.
<br>
The chunkAsNum is the same value, but stored as a 64-bit JavaScript
floating point number.
<br>
The integer represents the variable values in mixed-base format, e.g. if:
<ul>
<li>variable #0 ranges from 0-6
<li>variable #1 from 0-2
<li>variable #2 from 0-100
</ul>
then:
<ul>
<li>variable #0 is in the 'one's' place
<li>variable #1 is in the 'seven's' place
<li>variable #2 is in the 'twenty-one's' place (7*3)
</ul>
This scheme maximizes the amount of information you can represent in the
string, which is restricted to printable characters and thus has only about
6.5 bits per byte available for real data.  We stick to ASCII characters,
because Unicode would have even worse efficiency once the UTF-8 encoding
rules are taken into account.  (This deliberately reflects the limitations
of cookies, so that these strings may easily be stored inside a cookie.)
<br><br>
This means that the state of each variable in the game is stored in multiple
places, all of which are kept synchronized:
<ul>
<li>this.chunkAsStr_, an array of 4-char strings, one per chunk
<li>this.chunkAsNum_, an array of JavaScript numbers, one per chunk
<li>this.value_, the individual variable values
</ul>
Of course the variable values are also stored on disk (in cookies) and
in the jiffee state-management code, but that is outside the scope of this file.
<br><br>
The standard index names used are:
<ul>
<li>chunkNum, the index of a chunk
<li>varNum, the index of a variable in this.value_, this.range_
</ul>
*/

com.jiffeegames.Encoding =
  function Encoding() {
    com.jiffeegames.Checks.validate(arguments);
  };

com.jiffeegames.Encoding.prototype.IMPLEMENTS = 'com.jiffeegames.Encoding';
com.jiffeegames.Encoding.prototype.USES = [
    'com.jiffeegames.Cookies'
    ];

com.jiffeegames.Encoding.prototype.init =
  function init(locate, language) {
    var checks = com.jiffeegames.Checks;
    var encoding = this;
    checks.validate(arguments);
    var cookies = locate('com.jiffeegames.Cookies');

/**
Do an integer division.
@param {integer} a The dividend.
@param {integer} b The divisor.
@returns The quotient, truncated to an integer.
@type integer
*/

com.jiffeegames.Encoding.prototype.integerDivide_ =
  function integerDivide_(a, b) {
    return (a / b) | 0;  // force 32-bit int rep to fake integer division
  };

/**
Add another variable to the encoding.  Note that the noun and trait names
are used only to compute a fingerprint for the encoding;
they are not actually stored anywhere.
@param {String} noun The name of the noun which owns the trait.
@param {String} trait The name of the trait whose value is being encoded.
@param {integer} maxVal The maximum allowed value of the trait.
@returns The index of the new variable within the Encoding.
This index must be used to refer to the variable in get/set calls.
@type integer
*/

com.jiffeegames.Encoding.prototype.addVar =
  function addVar(noun, trait, maxVal) {
    checks.validate(arguments);
    if (maxVal < 1) {
      checks.error(arguments, 'J007', 'smallest allowable maximum is 1');
    }
    if (maxVal >= this.maxRange_) {
      checks.error(arguments, 'J008',
                  'largest allowable maximum is ' + (this.maxRange_-1));
    }
    if (this.isStarted_) {
      checks.error(arguments, 'J009', 'called after chunks were built');
    }

    var varNum = this.value_.length;
    this.range_.push(maxVal + 1);  // number of values is max+1 to include zero
    this.value_.push(0);

    // Accumulate a string descriptor, which is converted into a SHA-224 hash
    // once we're done.  The provides protection against a game trying to
    // restore state from a different game, or from an obsolete cookie
    // from an old version of the same game.

    this.fingerprint_.push(noun);
    this.fingerprint_.push(trait);
    this.fingerprint_.push(maxVal);

    return varNum; // use this number to refer to this variable in the future
  };

/**
Allocate all the variables to "chunks".
Do this after adding all variables but before touching the encodingString.
This is not a normal start() routine because it needs to be called *after*
traits starts.
@type void
*/

com.jiffeegames.Encoding.prototype.startManually =
  function startManually() {
    checks.validate(arguments);
    if (this.isStarted_) {
      checks.error(arguments, 'J010', 'tried to build chunks twice.');
    }

    this.chunkAsNum_ = [];          // map chunkNum to int
    this.chunkAsStr_ = [];          // map chunkNum to string
    this.varsInChunk_ = [];         // map chunkNum to list of varNum
    this.chunkNumFromVarNum_ = [];  // map varNum to chunkNum

    // Sort variables, biggest first, then do a first-fit into chunkAsNum.
    // This does not guarantee optimum packing, but it does make the 'maximum
    // amount of state that will fit' less sensitive to trivial
    // changes in a game.

    var tlist = [];
    var vlen = this.value_.length;
    for (var vdex = 0; vdex < vlen; vdex++) {
      tlist.push(vdex);
    }
    var enc = this; // for closure of 'sorter' function
    var sorter = function(a, b) {
      var res = enc.range_[b] - enc.range_[a];
      if (res !== 0) {
        return res;
      }
      return a - b;  // make this a stable sort, to simplify testing
    };
    tlist.sort(sorter);

    var remains = [];
    var tlen = tlist.length;
    var varNum;
    for (var tdex = 0; tdex < tlen; tdex++) {
      varNum = tlist[tdex];
      var r = this.range_[varNum];
      // look for an existing chunk with enough room for this variable
      var chunkNum;
      var nChunks = remains.length;
      for (chunkNum = 0; chunkNum < nChunks; chunkNum++) {
        if (r <= remains[chunkNum]) {
          break;
        }
      }
      if (chunkNum === this.varsInChunk_.length) {
        // no room, so create a new empty chunk
        remains.push(this.maxRange_);
        this.chunkAsNum_.push(0);
        this.chunkAsStr_.push('aaaa');
        this.varsInChunk_.push([]);
      }

      // finally, assign the variable to a chunk
      this.varsInChunk_[chunkNum].push(varNum);
      this.chunkNumFromVarNum_[varNum] = chunkNum;
      remains[chunkNum] = this.integerDivide_(remains[chunkNum], r);
    }

    // now that chunks are built, prevent any more variables from being added
    this.isStarted_ = true;

    // some variables may have had values set before chunks were built,
    // so set them all now to initialize c.chunkAsStr and c.chunkAsNum
    vlen = this.value_.length;
    for (varNum = 0; varNum < vlen; varNum++) {
      var newVal = this.value_[varNum];
      if (newVal !== 0 ) {
        this.setVar(varNum, newVal);
      }
    }

    // Compute fingerprint, based on names of all noun/trait/maxval values.
    var shaObj = new jsSHA(this.fingerprint_.join('/'));
    // If the fingerprint algorithm changes, change this to invalidate
    // any existing (and thus obsolete) cookies in players' browsers.
    var fpVersion = 'A';
    this.fingerprint_ = shaObj.getHash('SHA-224', 'HEX') + fpVersion;
  };

/**
Get the hash code (fingerprint) associated with this encoding.
@returns The fingerprint.
@type String
*/

com.jiffeegames.Encoding.prototype.getFingerprint =
  function getFingerprint() {
    checks.validate(arguments);
    if (!this.isStarted_) {
      checks.error(arguments, 'J011', 'called before chunks were built');
    }
    return this.fingerprint_;
  };

/**
Get the string that encodes the entire game state.
@returns The string that encodes the state.
@type String
*/

com.jiffeegames.Encoding.prototype.getString =
  function getString() {
    checks.validate(arguments);
    if (! this.isStarted_) {
      checks.error(arguments, 'J012', 'called before chunks were built');
    }
    return this.chunkAsStr_.join('');
  };

/**
Change the state of the whole game as needed to match an encoded string.
@param {String} str The string that encodes a state.
@type void
*/

com.jiffeegames.Encoding.prototype.setString =
  function setString(str) {
    checks.validate(arguments);
    if (! this.isStarted_) {
      checks.error(arguments, 'J013', 'called before chunks were built');
    }
    if (typeof str !== 'string') {
      checks.error(arguments, 'J074', 'argument must be a string');
    }
    if (str.length !== 4 * this.chunkAsNum_.length) {
      checks.error(arguments,
                   'J014',
                   'string should have length ' +
                       (4 * this.chunkAsStr_.length) +
                       ' but had length ' + str.length);
    }

    var clen = this.chunkAsNum_.length;
    for (var chunkNum = 0; chunkNum < clen; chunkNum++) {
      // split and store string representation
      var ss = str.substr(4*chunkNum, 4);
      this.chunkAsStr_[chunkNum] = ss;

      // make chunkAsNum match chunkAsStr
      var n = this.intFromChar_[ss.charAt(0)];
      n = n * this.base_ + this.intFromChar_[ss.charAt(1)];
      n = n * this.base_ + this.intFromChar_[ss.charAt(2)];
      n = n * this.base_ + this.intFromChar_[ss.charAt(3)];
      this.chunkAsNum_[chunkNum] = n;

      // make value match chunkAsNum
      var tlist = this.varsInChunk_[chunkNum];
      for (var tdex = tlist.length - 1; tdex >= 0; tdex--) {
        var varNum = tlist[tdex];
        this.value_[varNum] = n % this.range_[varNum];
        n = this.integerDivide_(n, this.range_[varNum]);
      }
    }
  };

/**
Set the value of a single variable in the Encoding.
@param {integer} varNum The index of the variable in the encoding.
@param {integer} newVal The new value of the variable.
@type void
*/

com.jiffeegames.Encoding.prototype.setVar =
  function setVar(varNum, newVal) {
    checks.validate(arguments);
    if (varNum < 0 || varNum >= this.value_.length) {
      checks.error(arguments, 'J016', 'illegal varNum');
    }
    if (newVal < 0) {
      checks.error(arguments, 'J017', 'smallest allowed value is zero');
    }
    if (newVal >= this.range_[varNum]) {
      checks.error(arguments,
                  'J018',
                  'largest allowed value is ' + (this.range_[varNum] - 1));
    }

    this.value_[varNum] = newVal;
    if (! this.isStarted_) {
      return;
    }

    // make c.chunkAsNum match c.value_
    // for simplicity, just re-encode the whole int
    var chunkNum = this.chunkNumFromVarNum_[varNum];
    var n = 0;
    var tlist = this.varsInChunk_[chunkNum];
    var tlen = tlist.length;
    for (var tdex = 0; tdex < tlen; tdex++) {
      n = n * this.range_[tlist[tdex]] + this.value_[tlist[tdex]];
    }
    this.chunkAsNum_[chunkNum] = n;

    // make c.chunkAsStr match c.chunkAsNum
    var c3 = this.charFromInt_.charAt(n % this.base_);
    n = this.integerDivide_(n, this.base_);
    var c2 = this.charFromInt_.charAt(n % this.base_);
    n = this.integerDivide_(n, this.base_);
    var c1 = this.charFromInt_.charAt(n % this.base_);
    n = this.integerDivide_(n, this.base_);
    var c0 = this.charFromInt_.charAt(n);
    this.chunkAsStr_[chunkNum] = c0 + c1 + c2 + c3;
  };

/**
Read the value of a single encoded variable.
@param {integer} varNum The index of the variable within an Encoding.
@returns The value of the variable.
@type integer
*/

com.jiffeegames.Encoding.prototype.getVar =
  function getVar(varNum) {
    checks.validate(arguments);
    if (varNum < 0 || varNum >= this.value_.length) {
      checks.error(arguments, 'J019', 'illegal varNum');
    }

    return this.value_[varNum];
  };

// contination of init()

    this.charFromInt_ = cookies.legalChars();
    this.base_ = this.charFromInt_.length;
    this.maxRange_ = this.base_ * this.base_ *
                      this.base_ * this.base_;
    this.intFromChar_ = {};
    for (var codePoint = 0; codePoint < this.base_; codePoint++) {
      this.intFromChar_[this.charFromInt_.charAt(codePoint)] = codePoint;
    }
    this.range_ = [];  // index by varNum
    this.value_ = [];  // index by varNum
    this.isStarted_ = false;
    this.fingerprint_ = [];
  };

jiffee-engine.js

// JIFFEE - JavaScript Interactive Fiction Framework for Education and Entertainment
// Copyright (c) 2008 by Michael S. Kenniston.  All rights reserved.
// Full documentation is at http://jiffeegames.com

// jiffee-engine.js

// set up a proper namespace to avoid naming conflicts
var com;
if (!com) {com = {};}
if (!com.jiffeegames) {com.jiffeegames = {};}

/**
Create an Engine object.  A typical use is:
<pre>
var engine = new com.jiffeegames.Engine();
engine.addStdLibs('en-us');
engine.add(new com.jiffeegames.Module1());
engine.add(new com.jiffeegames.Module2());
engine.init('en-us');
</pre>
@class An Engine object is the glue for all the other modules in the game.
It takes a list of modules which have been created but not yet plugged-in
or initialized, figures out the right order to add them, then plugs in and
initializes each one.  As it initializes the individual modules, it gives
each one a customized service locator that it can use to link to any
other modules that it depends on.
@constructor
*/

com.jiffeegames.Engine =
  function Engine() {
    this.checks = com.jiffeegames.Checks;
    this.checks.validate(arguments);
    this.pendingModules_ = [];
    this.acceptedModules_ = {};
    this.startOrder_ = [];
    this.started_ = false;
  };

/**
Add a module to the engine.
@param {Module} module - A newly created but not-yet-initialized module.
*/

com.jiffeegames.Engine.prototype.add =
  function add(module) {
    this.checks.validate(arguments);
    this.pendingModules_.push(module);
    return module;
  };

/**
Add all the standard modules to the engine.
This is just a shortcut to save you typing 
(and to make sure you don't forget any module names)
in the normal case where you
want to make all the standard modules supplied by JIFFEE available to
your game.
*/

com.jiffeegames.Engine.prototype.addStdLibs =
  function addStdLibs(lang) {
    this.checks.validate(arguments);
    this.add(new com.jiffeegames.Controller());
    this.add(new com.jiffeegames.Cookies());
    this.add(new com.jiffeegames.Display());
    this.add(new com.jiffeegames.Encoding());
    this.add(new com.jiffeegames.Help());
    this.add(new com.jiffeegames.Parser[lang]());
    this.add(new com.jiffeegames.Persistence());
    this.add(new com.jiffeegames.Persons());
    this.add(new com.jiffeegames.Places());
    this.add(new com.jiffeegames.Priorities());
    this.add(new com.jiffeegames.Props());
    this.add(new com.jiffeegames.Scheduler());
    this.add(new com.jiffeegames.Score());
    this.add(new com.jiffeegames.Traits());
    this.add(new com.jiffeegames.Verbs());
  };

/**
Initialize the engine, and all the modules in it.
This method checks the dependencies of all the modules which have been
added, finds a feasible start-up order, then plugs-in and initializes
all the modules in that order.
Although it is normally called only once, after all the modules have been
added, it is also legal to add more modules and then call init() again.
@param {String} language The target language, e.g. 'en-us' or 'zh-cn'.
*/

com.jiffeegames.Engine.prototype.init =
  function init(language) {
    this.checks.validate(arguments);
    this.language_ = language;

    var failed;
    var pending = this.pendingModules_;
    while (true) {
      var succeeded = 0;
      failed = 0;
      for (var m = 0; m < pending.length; m++) {
        var mod = pending[m];
        if (!mod) {
          continue;
        }
        if (this.plugInModule_(mod)) {
          pending[m] = undefined;
          this.startOrder_.push(mod);
          succeeded++;
        } else {
          failed++;
        }
      }
      if (succeeded === 0) {
        break;
      }
    }
    if (failed > 0) {
      this.reportProblems_();
    }
  };

/*
Generate a detailed report on why the engine failed to initialize.
This lists each unfilled dependency of each module that it could not
initialize.
**/

com.jiffeegames.Engine.prototype.reportProblems_ =
  function reportProblems_() {
    this.checks.validate(arguments);

    var message = 'The following modules failed to initialize:';
    var pending = this.pendingModules_;
    for (var m = 0; m < pending.length; m++) {
      var mod = pending[m];
      if (!mod) {
        continue;
      }
      message += '\n' + mod.IMPLEMENTS + ' (needs:';
      var depNames = mod.USES;
      for (var d = 0; d < depNames.length; d++) {
        var depName = depNames[d];
        if (!(depName in this.acceptedModules_)) {
          message += ' ' + depName;
        }
      }
      message += ')';
    }
    this.checks.error(arguments, 'J059', message);
  };

/**
Try to plug a module into the engine.
@returns true if it succeeds, false if doesn't.
<ul>
<li>If all this module's dependencies are already plugged-in, 
plug in this module, initialize it, and return true.
<li>If one or more of this module's dependencies are not yet plugged-in,
return false.
<li>If an error is detected which cannot be resolved by continuing
to plug in other modules, throw an error.
</ul>
@param {Module} module - the module to be plugged in.
*/

com.jiffeegames.Engine.prototype.plugInModule_ =
  function plugInModule_(module) {
    this.checks.validate(arguments);

    var thisModName = module.IMPLEMENTS;
    if (thisModName === undefined) {
      this.checks.error(arguments, 'J066', 'module has no IMPLEMENTS');
    }
    if (thisModName in this.acceptedModules_) {
      this.checks.error(arguments, 'J062', 'duplicated module ' + thisModName);
    }
    var depNames = module.USES;
    if (depNames === undefined) {
      this.checks.error(arguments,
                        'J076',
                        'module ' + thisModName + ' has no USES');
    }
    var svcLocator = {};
    for (var d = 0; d < depNames.length; d++) {
      var interfaceName = depNames[d];
      if (!(interfaceName in this.acceptedModules_)) {
        return false;
      }
      svcLocator[interfaceName] = this.acceptedModules_[interfaceName];
    }
    this.acceptedModules_[thisModName] = module;
    var checks = this.checks;
    // "locate()" is a closure whose job is to be passed into the module
    // and saved by it.  A separate closure is constructed for each module
    // that is plugged-in.
    function locate(interfaceName) {
      var result = svcLocator[interfaceName];
      if (result) {
        return result;
      }
      checks.error(arguments,
                   'J077',
                   'module ' + thisModName + ' cannot locate ' + interfaceName);
    }
    module.init(locate, this.language_);
    return true;
  };

/*
Start all modules, in the correct bottom-up order.
This is normally called only by run(), but for testing it is handy to
make it be a separate function.
@void
**/

com.jiffeegames.Engine.prototype.start_ =
  function start_() {
    this.checks.validate(arguments);
    this.checks.initOnly(arguments, this.started_);

    this.controller = this.acceptedModules_['com.jiffeegames.Controller'];
    if (!this.controller) {
      this.checks.error(arguments, 'J087', 'The Engine has no controller');
    }
    for (var i = 0; i < this.startOrder_.length; i++) {
      var mod = this.startOrder_[i];
      if (mod.start !== undefined) {
        mod.start();
      }
    }
    this.started_ = true;
  };

/*
Run a command.  This just passes the argument through to the Controller
object, which does all the real work.  The Engine has a run() routine for
only two reasons:  to hide the internal table of modules, and to provide
a trigger for automatically calling start() at the right time.
@param {String} cmd - the command to run.
@void
**/

com.jiffeegames.Engine.prototype.run =
  function run(cmd) {
    this.checks.validate(arguments);
    if (!this.started_) {
      this.start_();
    }
    return this.controller.run(cmd);
  };

jiffee-help.js

// JIFFEE - JavaScript Interactive Fiction Framework for Education and Entertainment
// Copyright (c) 2008 by Michael S. Kenniston.  All rights reserved.
// Full documentation is at http://jiffeegames.com

// jiffee-help.js

// set up a proper namespace to avoid naming conflicts
var com;
if (!com) {com = {};}
if (!com.jiffeegames) {com.jiffeegames = {};}

/**
Create a Help object.  The typical call is:
<pre>
var help = new com.jiffeegames.Help();
</pre>
@class A Help object defines the command handlers that
handle "help" and unrecognized commands.
Once you create the object, the only thing you need to use it
for is to call the "addXxx" methods from other modules.
*/

com.jiffeegames.Help =
  function Help() {
    com.jiffeegames.Checks.validate(arguments);
  };

com.jiffeegames.Help.prototype.IMPLEMENTS = 'com.jiffeegames.Help';
com.jiffeegames.Help.prototype.USES = [
    'com.jiffeegames.Display',
    'com.jiffeegames.Parser',
    'com.jiffeegames.Priorities',
    'com.jiffeegames.Scheduler',
    'com.jiffeegames.Traits'
    ];

com.jiffeegames.Help.prototype.init =
  function init(locate, language) {
    var checks = com.jiffeegames.Checks;
    var help = this;
    checks.validate(arguments);

    var display = locate('com.jiffeegames.Display');
    var parser = locate('com.jiffeegames.Parser');
    var priorities = locate('com.jiffeegames.Priorities');
    var scheduler = locate('com.jiffeegames.Scheduler');
    var traits = locate('com.jiffeegames.Traits');

/**
Format a row of the summary table.
@returns The formatted HTML string for one row.
@type String
@private
*/

com.jiffeegames.Help.prototype.formatRow_ =
  function formatRow_(cmd, meaning) {
    checks.validate(arguments);
    // cmd = translate(cmd)
    // meaning = translate(meaning)
    return '<tr><td>' + cmd + '</td><td>' + meaning + '</td></tr>';
  };

/**
Add a row to the summary table that will be displayed by the "help" command.
@param {String} cmd The string to display in the first column ("command").
@param {String} meaning The string to display in the second column ("what it means").
@type void
*/

com.jiffeegames.Help.prototype.addSummary =
  function addSummary(cmd, meaning) {
    checks.validate(arguments);
    this.summaries_.push(this.formatRow_(cmd, meaning));
  };

/**
Command handler for any user command that is not recognized by some
other handler.  Displays an appropriate error message.
@param {Object} event The event that triggered the call.
@type void
@private
*/

com.jiffeegames.Help.prototype.doUnrecognizedCmd_ =
  function doUnrecognizedCmd_(event) {
    checks.validate(arguments);
    var common = '<br><br>For a list of common commands, type "help".';
    if (event.verb === 'BAD-GRAMMAR') {
      if (event.hasOwnProperty('word')) {
        display.show('HELP-BAD-WORD', event.word);
        display.show('HELP-COMMON');
      } else {
        display.show('HELP-BAD-MEANING');
        display.show('HELP-COMMON');
      }
    } else if (event.direct &&
      traits.isNoun(event.direct) &&
      traits.get(event.direct, 'location') !==
                   traits.get('player', 'location')) {
      display.show('HELP-SEE-NONE-HERE', event.direct);
    } else {
      display.show('HELP-DONT-KNOW-HOW', event.raw);
      display.show('HELP-COMMON');
    }
  };

/**
Command handler for the "help" command.
Displays a summary table of all available commands and what they do.
@param {Object} event The event that triggered the call.
@type void
@private
*/

com.jiffeegames.Help.prototype.doHelpCmd_ =
  function doHelpCmd_(event) {
    checks.validate(arguments);
    display.show('<table border=1 cellpadding=5>');
    display.show(this.formatRow_(
        '<strong>Command You Type</strong>',
        '<strong>What It Means</strong>'));
    display.show(this.summaries_.join(''));
    display.show(this.formatRow_(
        'h, help', 'show this help message'));
    display.show(this.formatRow_(
        'other phrases', 'as appropriate for the situation'));
    display.show('</table>');
  };

/**
Set the default language.
@type void
@member com.jiffeegames.Help
*/
com.jiffeegames.Help["en-us"] =
  function Help_en_us() {
    checks.validate(arguments);
    var d = display;
    d.t = d.translateTo;

    d.t('HELP-BAD-WORD', 'I don\'t recognize the word "${1}."');
    d.t('HELP-BAD-MEANING',
        'I recognize your words, but I don\'t understand what you mean.');
    d.t('HELP-COMMON', '<br><br>For a list of common commands, type "help".');
    d.t('HELP-SEE-NONE-HERE', 'I see no ${1} here.');
    d.t('HELP-DONT-KNOW-HOW', 'I don\'t know how to "${1}" here.');

    parser.addVerb('help', 'help');
    parser.addVerb('help', 'h');
    parser.addVerb('help', '?');
  };

/**
Set the language to Spanish.
@type void
@member com.jiffeegames.Help
*/
// TODO(msk): fix this
com.jiffeegames.Help.es = 
  function Help_es() {
    checks.validate(arguments);
    var d = display;
    d.t = d.translateTo;

    d.t('HELP-BAD-WORD', 
        // 'I don\'t recognize the word "${1}."');
        'No reconocer la palabra "${1}".');
    d.t('HELP-BAD-MEANING',
        // 'I recognize your words, but I don\'t understand what you mean.');
        'Reconozco tus palabras, pero yo no entiendo lo que quieres decir.');
    d.t('HELP-COMMON',
        // '<br><br>For a list of common commands, type "help".');
        '<br><br>Para obtener una lista de comandos comunes, escriba "ayuda".');
    d.t('HELP-SEE-NONE-HERE', // 'I see no ${1} here.');
        'No veo ningún ${1} aquí.');
    d.t('HELP-DONT-KNOW-HOW',
        // 'I don\'t know how to "${1}" here.');
        'Yo no sé cómo "${1}" aquí." here.');

    parser.addVerb('help', 'help');
    parser.addVerb('help', 'h');
    parser.addVerb('help', '?');
  };

// continuation of init()

    this.summaries_ = [];

    priorities.add('normal', 'unrecognized');
    // We avoid using Verbs.add() here to avoid circular dependencies.
    var rule = scheduler.addRule('directive');
    rule.setPriority('unrecognized');
    rule.setAction(function(event){help.doUnrecognizedCmd_(event);});

    rule = scheduler.addRule('directive');
    rule.addExactMatch('verb', 'help');
    rule.setAction(function(event){help.doHelpCmd_(event);});

    var langDefs = com.jiffeegames.Help[language];
    if (!langDefs) {
      checks.error(arguments, 'J063',
                   'Help has no support for language ' + language);
    }
    langDefs.call(this);
  };

jiffee-parser-en-us.js

// JIFFEE - JavaScript Interactive Fiction Framework for Education and Entertainment
// Copyright (c) 2008-2009 by Michael S. Kenniston.  All rights reserved.
// Full documentation is at http://jiffeegames.com

// jiffee-parser-en-us.js

// set up a proper namespace to avoid naming conflicts
var com;
if (!com) {com = {};}
if (!com.jiffeegames) {com.jiffeegames = {};}
if (!com.jiffeegames.Parser) {/** @namespace */ com.jiffeegames.Parser = {};}

/**
Create a Parser object.  The normal call is
<pre>
var parser = new com.jiffeegames.Parser['en-us']();
</pre>
There is a different parser for each language, but you only use one of
them in any given game so they all use the same dependency name.
This makes for less code churn when you switch languages, since all you
have to do is to select the one you want as you initialize the Engine.
@constructor
@requires com.jiffeegames.Checks
@class A Parse object parses user input and translates
it into canonical form.
*/

com.jiffeegames.Parser_en_us =
  function Parser_en_us() {
    com.jiffeegames.Checks.validate(arguments);
  };
com.jiffeegames.Parser['en-us'] = com.jiffeegames.Parser_en_us;

com.jiffeegames.Parser_en_us.prototype.IMPLEMENTS = 'com.jiffeegames.Parser';
com.jiffeegames.Parser_en_us.prototype.USES = [
    'com.jiffeegames.Display',
    'com.jiffeegames.Scheduler',
    'com.jiffeegames.Traits'
    ];

com.jiffeegames.Parser_en_us.prototype.init =
  function init(locate, language) {
    var checks = com.jiffeegames.Checks;
    var parser = this;
    checks.validate(arguments);
    var display = locate('com.jiffeegames.Display');
    var scheduler = locate('com.jiffeegames.Scheduler');
    var traits = locate('com.jiffeegames.Traits');

com.jiffeegames.Parser_en_us.prototype.setStrictTranslation =
  function setStrictTranslation(strict) {
    checks.validate(arguments);
    checks.initOnly(arguments, this.started_);
    if (typeof strict !== 'boolean') {
      checks.error(arguments, 'J058', 'argument must be boolean');
    }
    this.strict_ = strict;
    display.setStrictTranslation(strict);
  };

/**
Tell the parser about a possible input token.
@param {String} tok The token that may appear in user input.
@returns The tokenValue of this token.
@type Integer
*/

com.jiffeegames.Parser_en_us.prototype.addToken_ =
  function addToken_(tok) {
    checks.validate(arguments);
    checks.initOnly(arguments, this.started_);

    if (this.tokenCodes_.hasOwnProperty(tok)) {
      return this.tokenCodes_[tok];  // addToken_ is idempotent
    }
    this.tokenCodes_[tok] = this.nextCode_;
    return this.nextCode_++;
  };

/**
Ask the parser for the code (number) of a given token.
@param {String} tok The token that appeared in user input.
@returns The code number of the token, or undefined if the token is
not recognized.
@type int
*/

com.jiffeegames.Parser_en_us.prototype.tokenValue_ =
  function tokenValue_(tok) {
    checks.validate(arguments);
    if (this.tokenCodes_.hasOwnProperty(tok)) {
      return this.tokenCodes_[tok];
    }
    return undefined;
  };

/**
Convert an input line into a list of words.
@param {String} raw The input line, as typed by the user.
@returns A list of strings, one for each word or known punctuation mark.
@type {String}
*/

com.jiffeegames.Parser_en_us.prototype.wordify_ =
  function wordify_(raw) {
    checks.validate(arguments);
    raw = raw.replace(/[,.?!]/g, ' $& ');  // space-delimit known punctuation
    raw = raw.replace(/^\s+|\s+$/g, '');  // strip leading or trailing spaces
    if (raw === '') {return [];}
    var list = raw.split(/\s+/);  // convert into words and punctuation marks
    return list;
  };

/**
Convert an input line into a list of token codes.
@param {String} raw The input line, as typed by the user.
@returns A list of tokens, each of which is an integer code.
If a word is unrecognized, its token code is -1 and its value
is placed in res.word.  (If more than one is unrecognized, the
value of the first one is placed in res.word.)
@type {List of Integer}
*/

com.jiffeegames.Parser_en_us.prototype.tokenize =
  function tokenize(raw) {
    checks.validate(arguments);
    var words = this.wordify_(raw);
    var res = [];
    for (var i = 0; i < words.length; i++) {
      var code = this.tokenValue_(words[i]);
      if (code === undefined) {
        res.push(-1);
        if (!res.hasOwnProperty('word')) {
          res.word = words[i];
        }
      } else {
        res.push(code);
      }
    }
    return res;
  };

/**
Add a word or phrase to a list of known phrases.
@param {Hash} hash The set of known phrases to be augmented.
@param {String} internal The internal name.
@param {String} external The external form of the word or phrase.
@type void
*/

com.jiffeegames.Parser_en_us.prototype.addPhrase_ =
  function addPhrase_(hash, internal, external) {
    checks.validate(arguments);
    var words = this.wordify_(external.toLowerCase());
    for (var i = 0; i < words.length; i++) {
      this.addToken_(words[i]);
    }
    var tokens = this.tokenize(external);
    var list = hash[tokens[0]];
    if (list === undefined) {
      list = [];
      hash[tokens[0]] = list;
    }
    list.push(new this.Pattern(internal, tokens));
  };

/**
Add a word or phrase (external form) to the list of known nouns.
Example:
<pre>
parser.addNoun('candy', 'candy', 'sweet', 'peppermint', 'peppermint stick');
</pre>
@param {String} internal The name of the noun, in author language.
@param {String} external One or more forms of the noun, in player language.
@type void
*/

com.jiffeegames.Parser_en_us.prototype.addNoun =
  function addNoun(internal, external) {
    if (arguments.length < 2) {
      checks.error(arguments, 'J082', 'must have at least 2 arguments');
    }
    checks.initOnly(arguments, this.started_);

    for (var i = 1; i < arguments.length; i++) {
      this.addPhrase_(this.nouns_, internal, arguments[i]);
    }
  };

/**
Add a word or phrase (external form) to the list of known verbs.
<pre>
parser.addVerb('sing', 'sing', 'hum', 'whistle');
</pre>
@param {String} internal The name of the verb, in author language.
@param {String} external One or more forms of the verb, in player language.
@type void
*/

com.jiffeegames.Parser_en_us.prototype.addVerb =
  function addVerb(internal) {
    if (arguments.length < 2) {
      checks.error(arguments, 'J088', 'must have at least 2 arguments');
    }
    checks.initOnly(arguments, this.started_);

    for (var i = 1; i < arguments.length; i++) {
      this.addPhrase_(this.verbs_, internal, arguments[i]);
    }
  };

// TODO:  Fix this awful temporary kludge for spanish-out/english-in.
com.jiffeegames.Parser.es = com.jiffeegames.Parser['en-us'];

/**
Parse a simple command of the form "<verb> [<noun>]" and convert it to
a Command.
This will be made much more sophisticated later.
@param {String} raw The raw input from the user.
@returns A list of the Commands that result (currently always 0 or 1).
If there is an unknown word,
the "word" property will be set to the unknown word.
If the grammar cannot be parsed, the verb will be "BAD-GRAMMAR".
In each case the "raw" property will be set to the original input.
@type {Array of Command}
*/

com.jiffeegames.Parser_en_us.prototype.parseCommands =
  function parseCommands(raw) {
    checks.validate(arguments);

    // replace() then split() as separate steps to avoid IE bug
    raw = raw.toLowerCase().replace(/,(\s+then)?/, ',').split(/,/);
    var res = [];
    for (var i = 0; i < raw.length; i++) {
      res = res.concat(this.parseOneCommand_(raw[i]));
    }    
    return res;
  };

/**
Parse a single command that has already been split.
*/

com.jiffeegames.Parser_en_us.prototype.parseOneCommand_ =
  function parseOneCommand_(raw) {
    checks.validate(arguments);

    var command = {raw: raw};  // TODO(msk): fix this for "a, b, then c"
    var tokens0 = this.tokenize(raw);
    // Ignore optional trailing period, but only if there is at least one real word.
    var tlen = tokens0.length;
    if (tlen > 1 && tokens0[tlen - 1] === this.tokenValue_('.')) {
      tokens0.pop();
    }

    if (tokens0.length < 1) {
      command.verb = 'EMPTY';
      return [command];
    }
    if (tokens0.hasOwnProperty('word')) {
      command.word = tokens0.word;
    }
    if (tokens0[0] === -1) {
      command.verb = 'BAD-GRAMMAR';
      return [command];
    }

    // Try all the promising verbs (those that begin with the right token).
    var verbs = this.verbs_[tokens0[0]];
    if (verbs === undefined) {
      command.verb = 'BAD-GRAMMAR';
      return [command];
    }
    for (var v = verbs.length - 1; v >= 0; v--) {
      var vPattern = verbs[v];
      var tokens1 = vPattern.match(tokens0);
      if (tokens1 === undefined) {continue;}
      command.verb = vPattern.internal;
      if (tokens1.length === 0) {
        // matched <verb>
        return [command];
      }
      if ((command.verb === 'save') || (command.verb === 'restore')) {
        if ((tokens1.length === 1) && (tokens1[0] === -1)) {
          command.direct = 'LITERAL';
          return [command];        
        }
      } else if (this.articles_[tokens1[0]]) {
        tokens1.shift();
      }
      var nouns = this.nouns_[tokens1[0]];
      if (nouns === undefined) {continue;}
      for (var n = nouns.length - 1; n >= 0; n--) {
        var nPattern = nouns[n];
        var tokens2 = nPattern.match(tokens1);
        if (tokens2 === undefined) {continue;}
        command.direct = nPattern.internal;
        if (tokens2.length === 0) {
          // matched <verb> <noun>
          return [command];
        }
      }
    }
    command.verb = 'BAD-GRAMMAR';
    return [command];
  };

/**
Tell the parser that we've received input from the user.
We immediately parse it into commands and store them for later execution.
This is where "do A, then do B" gets parsed and split into multiple commands.
@param {String} raw The raw input typed by the user.
@type void
*/

com.jiffeegames.Parser_en_us.prototype.notifyUserInput =
  function notifyUserInput(raw) {
    checks.validate(arguments);
    checks.runningOnly(arguments, this.started_);

    if (this.commands_.length > 0) {
      checks.error(arguments,
                   'J050',
                   'there are still ' + this.commands_.length +
                       ' more commands left to process with getCommand');
    }
    this.commands_ = this.parseCommands(raw);
  };

/**
Start this module.
@type void
*/

com.jiffeegames.Parser_en_us.prototype.start =
  function start() {
      this.addDefaults_();
      this.started_ = true;
  };

/**
Add a translation for every verb and noun that does not already have one.
Example call:
<pre>
parser.addDefaults_();
</pre>
@type void
*/

com.jiffeegames.Parser_en_us.prototype.addDefaults_ =
  function addDefaults_() {
    checks.validate(arguments);
    checks.initOnly(arguments, this.started_);
    // Add a translation for every verb that doesn't have one.
    var schedVerbs = scheduler.getAllVerbs();
    for (vKey in this.verbs_) {
      if (this.verbs_.hasOwnProperty(vKey)) {
        var vlist = this.verbs_[vKey];
        var vlen = vlist.length;
        for (var vdex = 0; vdex < vlen; vdex++) {
          var parserVerb = vlist[vdex].internal;
          delete schedVerbs[parserVerb];
        }
      }
    }
    for (var newVerb in schedVerbs) {
      if (schedVerbs.hasOwnProperty(newVerb)) {
        if (this.strict_) {
          checks.error(arguments, 'J093',
                       'parser.addVerb() never called for verb ' + newVerb);
        }
        this.addVerb(newVerb, newVerb);
      }
    }

    // Add a translation for every thing that doesn't have one.
    var traitsNouns = traits.getAllThings();
    for (var nKey in this.nouns_) {
      if (this.nouns_.hasOwnProperty(nKey)) {
        var nlist = this.nouns_[nKey];
        var nlen = nlist.length;
        for (var ndex = 0; ndex < nlen; ndex++) {
          var parserNoun = nlist[ndex].internal;
          delete traitsNouns[parserNoun];
        }
      }
    }
    for (var newNoun in traitsNouns) {
      if (traitsNouns.hasOwnProperty(newNoun)) {
        if (this.strict_) {
          checks.error(arguments, 'J094',
                       'parser.addNoun() never called for noun ' + newNoun);
        }
        this.addNoun(newNoun, newNoun);
      }
    }

  };

/**
Retrieve the next command typed by the user.
The original user input gets split into simple commands,
and this method retrieves a single one of those commands.
@returns A single command object, or undefined if there are no commands
left to process.
@type {Command}
*/

com.jiffeegames.Parser_en_us.prototype.getCommand =
  function getCommand() {
    checks.validate(arguments);
    return this.commands_.shift();
  };

// continuation of init()

    if ((language !== 'es') &&  // hack!!!
        (language.substring(0,2) !== 'en')) {
      checks.error(arguments, 'J084',
                   'Parser for en-us does not support language ' + language);
    }

    this.tokenCodes_ = {};
    this.nextCode_ = 0;
    this.nouns_ = {};
    this.verbs_ = {};
    this.articles_ = {};
    this.commands_ = [];
    this.strict_ = false;
    this.started_ = false;

    this.addToken_(',');
    this.addToken_('.');
    this.addToken_('?');
    this.addToken_('!');

    this.articles_[this.addToken_('a')] = true;
    this.articles_[this.addToken_('an')] = true;
    this.articles_[this.addToken_('any')] = true;
    this.articles_[this.addToken_('some')] = true;
    this.articles_[this.addToken_('the')] = true;

    scheduler.addLegalProperty('verb');
    scheduler.addLegalProperty('raw');
    scheduler.addLegalProperty('direct');
    scheduler.addLegalProperty('word');

    // Set up a type to hold token-pattern information.
    this.Pattern = function Pattern(internal, tokens) {
      this.internal = internal;
      this.tokens = tokens;
    };

    // If pattern matches, remove matched tokens and return new list of tokens.
    // If no match, return undefined.
    this.Pattern.prototype.match =
      function match(inTokens) {
        var inLen = inTokens.length;
        var inPtr = 0;
        var patTokens = this.tokens;
        var patLen = patTokens.length;
        var patPtr = 0;
        while (true) {
          if (patPtr >= patLen) {return inTokens.slice(inPtr);}
          if (inPtr >= inLen) {return undefined; }
          if (inTokens[inPtr] !== patTokens[patPtr]) {return undefined;}
          ++inPtr;
          ++patPtr;
        }
      };
  };

jiffee-persistence.js

// JIFFEE - JavaScript Interactive Fiction Framework for Education and Entertainment
// Copyright (c) 2008 by Michael S. Kenniston.  All rights reserved.
// Full documentation is at http://jiffeegames.com

// jiffee-persistence.js

// set up a proper namespace to avoid naming conflicts
var com;
if (!com) {com = {};}
if (!com.jiffeegames) {com.jiffeegames = {};}

/**
Create a Persistence object.  The typical call is:
<pre>
var persistence = new com.jiffeegames.Persistence();
</pre>
@class A Persistence object defines and implements the functions
and commands used to resume, save, restore, or restart a game:
<ul>
<li>If you stop the browser, then come back to the game later, it will
automatically remember where you were
so you can resume the game from where you left off.
<li>If you "save" a game, you can get back to that state
later with a "restore" command.
<li>If you want to start all over, you can "restart" from the beginning.
</ul>
*/

com.jiffeegames.Persistence =
  function Persistence() {
    com.jiffeegames.Checks.validate(arguments);
  };

com.jiffeegames.Persistence.prototype.IMPLEMENTS = 'com.jiffeegames.Persistence';
com.jiffeegames.Persistence.prototype.USES = [
    'com.jiffeegames.Cookies',
    'com.jiffeegames.Display',
    'com.jiffeegames.Encoding',
    'com.jiffeegames.Help',
    'com.jiffeegames.Parser',
    'com.jiffeegames.Scheduler',
    'com.jiffeegames.Traits',
    'com.jiffeegames.Verbs'
    ];

com.jiffeegames.Persistence.prototype.init =
  function init(locate, language) {
    var checks = com.jiffeegames.Checks;
    var persistence = this;
    checks.validate(arguments);

    var cookies = locate('com.jiffeegames.Cookies');
    var display = locate('com.jiffeegames.Display');
    var encoding = locate('com.jiffeegames.Encoding');
    var help = locate('com.jiffeegames.Help');
    var parser = locate('com.jiffeegames.Parser');
    var scheduler = locate('com.jiffeegames.Scheduler');
    var traits = locate('com.jiffeegames.Traits');
    var verbs = locate('com.jiffeegames.Verbs');

/**
Check to see if a potential cookie-name is legal.
@param {String} cName The name to check.
@returns true if the name is legal
@type {boolean}
@private
*/

com.jiffeegames.Persistence.prototype.isLegalName_ =
  function isLegalName_(cName) {
    checks.validate(arguments);

    var pos = cName.search(/[^a-zA-Z0-9.]/);
    if (pos >= 0) {
      display.show('PERSISTENCE-ILLEGAL-CHAR',
                   {literal:cName},
                   {literal:cName.substr(pos, 1)});
      return false;
    }
    if (cName.length > this.MAX_NAME_LENGTH) {
      display.show('PERSISTENCE-NAME-TOO-LONG', {literal:cName});
      return false;
    }
    return true;
  };

/**
Command handler for the "save" command.
Saves the complete state of the game in a cookie with the given name.
The fingerprint is appended to the name internally to avoid version
clashes.
@param {Object} event The event that triggered the call.
@type void
@private
*/

com.jiffeegames.Persistence.prototype.doSaveCmd_ =
  function doSaveCmd_(event) {
    checks.validate(arguments);
    if (!cookies.areEnabled()) {
      display.show('PERSISTENCE-NO-COOKIES');
      return;
    }
    var fingerprint = encoding.getFingerprint();
    var arg = event.direct;
    if (arg === 'LITERAL') {
      arg = event.word;
    }
    if (!this.isLegalName_(arg)) {
      return;
    }
    var cName = arg + '.' + fingerprint;
    var cValue = encoding.getString();
    cookies.set(cName, cValue, 366);
    if (cookies.get(cName) !== cValue) {  // paranoia check
      checks.error(arguments, 'J073',
                   'internal cookie error - inconsistent save');
    }
    display.show('PERSISTENCE-GAME-SAVED', {literal:arg});
  };

/**
Restore state.
Does the work of restoring a state to all the slots and getting
the game ready to roll again.
@param {String} state the state encoded as a string
@type void
@private
*/

com.jiffeegames.Persistence.prototype.restoreState_ =
  function restoreState_(state) {
    checks.validate(arguments);
    encoding.setString(state);
    traits.getVarsFromEncoding();
  };

/**
Command handler for the "restore" command.
Restores the complete state of the game from a cookie with the given name.
The fingerprint is appended to the name internally to avoid version
clashes.
@param {Object} task The event that triggered the call.
@type void
@private
*/

com.jiffeegames.Persistence.prototype.doRestoreCmd_ =
  function doRestoreCmd_(event) {
    checks.validate(arguments);
    if (!cookies.areEnabled()) {
      display.show('PERSISTENCE-NO-COOKIES');
      return;
    }
    var fingerprint = encoding.getFingerprint();
    var arg = event.direct;
    if (arg === 'LITERAL') {
      arg = event.word;
    }
    if (!this.isLegalName_(arg)) {
      return;
    }
    var cName = arg + '.' + fingerprint;
    var cValue = cookies.get(cName);
    if (!cValue) {
      display.show('PERSISTENCE-NO-SAVED', {literal:arg});
      return;
    }
    display.show('PERSISTENCE-GAME-RESTORED', {literal:arg});
    this.restoreState_(cValue);
    scheduler.notifyEvent({eventType:'directive', verb:'where am i', raw:''});
  };

/**
Handler for saving complete state after each move, so that a game
can be resumed later.
The state is saved in the default cookie.
@type void
@private
*/

com.jiffeegames.Persistence.prototype.doSaveEveryTurn_ =
  function doSaveEveryTurn_(event) {
    checks.validate(arguments);
    var cName = this.DEFAULT_NAME + '.' + encoding.getFingerprint();
    var cValue = encoding.getString();
    cookies.set(cName, cValue, 366);
    throw 'event not done';  // don't count this as command processing
  };

/**
Command handler for beginning of game.
This saves the initial state (to enable restarts), and
automatically resumes any previous game (by restoring from the default
cookie).
@type void
@private
*/

com.jiffeegames.Persistence.prototype.doInitialState_ =
  function doInitialState_(event) {
    checks.validate(arguments);
    if (this.initialState_) {
      return;  // execute only on the first call
    }

    this.initialState_ = encoding.getString();  // for restarts
    var cName = this.DEFAULT_NAME + '.' + encoding.getFingerprint();
    var cValue = cookies.get(cName);
    if (!cValue) {
      return;  // no saved state
    }
    display.getAndClearOutput();  // replace old nbsp with our message
    display.show('PERSISTENCE-RESUMING-PREVIOUS');
    this.restoreState_(cValue);
    throw 'event not done';  // don't count this as command processing
  };

/**
Command handler for the "restart" command.
This resets the state back to the initial state, i.e. returns
the player to the beginning of the game.
@param {Object} event The event that triggered the call.
@type void
@private
*/

com.jiffeegames.Persistence.prototype.doRestartCmd_ =
  function doRestartCmd_(event) {
    checks.validate(arguments);
    this.restoreState_(this.initialState_);
    scheduler.notifyEvent({eventType:'directive', verb:'where am i', raw:''});
    throw 'event done';
  };

/**
Set the default language.
@type void
@member com.jiffeegames.Persistence
*/

com.jiffeegames.Persistence['en-us'] =
  function Persistence_en_us() {
    checks.validate(arguments);
    var d = display;
    d.t = d.translateTo;

    d.t('PERSISTENCE-ILLEGAL-CHAR', '"${1}" contains a character "${2}" that is illegal in the name of a saved game.');
    d.t('PERSISTENCE-NAME-TOO-LONG', '"${1}" is too long to be a legal name for a saved game.');
    d.t('PERSISTENCE-NO-COOKIES', 'Cookies are not enabled in your browser.');
    d.t('PERSISTENCE-GAME-SAVED', 'Game saved as "${1}".');
    d.t('PERSISTENCE-NO-SAVED', 'There is no saved value named "${1}".');
    d.t('PERSISTENCE-GAME-RESTORED', '<p>Game restored from "${1}".</p>');
    d.t('PERSISTENCE-RESUMING-PREVIOUS', '<p>Resuming previous game ...<br>To start a new game, type "restart".</p>');

    parser.addVerb('save', 'save', 'save as');
    parser.addVerb('restore', 'restore', 'restore from');
    parser.addVerb('restart', 'restart', 'restart game', 'restart the game',
                   'q', 'quit');
  };

/**
Set the language to Spanish.
@type void
@member com.jiffeegames.Persistence
*/
com.jiffeegames.Persistence.es =
  function Persistence_es() {
    checks.validate(arguments);
    var d = display;
    d.t = d.translateTo;

    d.t('PERSISTENCE-ILLEGAL-CHAR', // "${1}" contains a character "${2}" that is illegal in the name of a saved game.');
'"${1}" contiene un carácter "${2}", que es ilegal en el nombre de una partida guardada.');
    d.t('PERSISTENCE-NAME-TOO-LONG', // '"${1}" is too long to be a legal name for a saved game.');
'"${1}" es demasiado larga para ser un nombre legal para una partida guardada.');
    d.t('PERSISTENCE-NO-COOKIES', // 'Cookies are not enabled in your browser.');
'No están habilitadas las cookies en tu navegador.');
    d.t('PERSISTENCE-GAME-SAVED', // 'Game saved as "${1}".');
'Juego guardado como "${1}".');
    d.t('PERSISTENCE-NO-SAVED', // There is no saved value named "${1}".');
'Pero no hay ningún valor guardado el nombre "${1}".');
    d.t('PERSISTENCE-GAME-RESTORED', // '<p>Game restored from "${1}".</p>')'
'<p>Juego restaurada de "${1}".');
    d.t('PERSISTENCE-RESUMING-PREVIOUS', // '<p>Resuming previous game ...<br>To start a new game, type "restart".</p>');
'<p>La reanudación de juego anterior ... <br> Para iniciar un nuevo juego, tipo "reiniciar".</p>');

    parser.addVerb('save', 'save', 'save as');
    parser.addVerb('restore', 'restore', 'restore from');
    parser.addVerb('restart', 'restart', 'restart game', 'restart the game',
                   'q', 'quit');
  };

// continuation of init()

    this.initialState_ = undefined;
    // Use a maximum-length default name, so that if the state is too
    // big for a cookie the game will blow up immediately (fail-fast).
    // Don't begin with $ (to avoid Opera bug).
    this.DEFAULT_NAME = 'current.state.$$$$$$';
    this.MAX_NAME_LENGTH = this.DEFAULT_NAME.length;

    var rule = scheduler.addRule('change');
    rule.addExactMatch('noun', 'game');
    rule.addExactMatch('trait', 'phase');
    rule.addExactMatch('newVal', 'initialize-game');
    rule.setAction(
        function(event){persistence.doInitialState_(event);});

    rule = scheduler.addRule('change');
    rule.addExactMatch('noun', 'game');
    rule.addExactMatch('trait', 'phase');
    rule.addExactMatch('newVal', 'end-command');
    rule.setAction(
        function(event){persistence.doSaveEveryTurn_(event);});

    verbs.describe('save', '*',
                   function(event){persistence.doSaveCmd_(event);});
    help.addSummary('save &lt;name&gt;',
                    'save the current state of the game as &lt;name&gt;');

    verbs.describe('restore', '*',
                   function(event){persistence.doRestoreCmd_(event);});
    help.addSummary('restore &lt;name&gt;',
                    'restore the state of the game saved as &lt;name&gt;');

    verbs.describe('restart', '',
                   function(event){persistence.doRestartCmd_(event);});
    help.addSummary('restart',
                    'restart the game from the beginning');

    var langDefs = com.jiffeegames.Persistence[language];
    if (!langDefs) {
      checks.error(arguments, 'J072',
                   'Persistence has no support for language ' + language);
    }
    langDefs.call(this);
  };

jiffee-persons.js

// JIFFEE - JavaScript Interactive Fiction Framework for Education and Entertainment
// Copyright (c) 2008 by Michael S. Kenniston.  All rights reserved.
// Full documentation is at http://jiffeegames.com

// jiffee-persons.js

// set up a proper namespace to avoid naming conflicts
var com;
if (!com) {com = {};}
if (!com.jiffeegames) {com.jiffeegames = {};}

/**
Create a Persons object.  The typical call is:
<pre>
var persons = new com.jiffeegames.Persons();
</pre>
@class A Persons object defines the basic commands to create person
objects, and automatically sets up the 'player' person.
*/

com.jiffeegames.Persons =
  function Persons() {
    com.jiffeegames.Checks.validate(arguments);
  };

com.jiffeegames.Persons.prototype.IMPLEMENTS = 'com.jiffeegames.Persons';
com.jiffeegames.Persons.prototype.USES = [
    'com.jiffeegames.Traits',
    'com.jiffeegames.Verbs'
    ];

com.jiffeegames.Persons.prototype.init =
  function init(locate, language) {
    var checks = com.jiffeegames.Checks;
    var persons = this;
    checks.validate(arguments);
    var traits = locate('com.jiffeegames.Traits');
    // verbs is an implicit dependency, not used directly
    var verbs = locate('com.jiffeegames.Verbs');

/**
Create a noun of type "person" with the given name.
@param {String} person The name to use.
@type void
*/

com.jiffeegames.Persons.prototype.add =
  function add(person) {
    checks.validate(arguments);
    traits.addNoun(person);
    traits.freeze(person, 'is-person', true);
  };

// continuation of init()

    traits.addEnum('is-person', false, true);
    traits.freeze('player', 'is-person', true);  // player created by Verbs()
  };

jiffee-places.js

// JIFFEE - JavaScript Interactive Fiction Framework for Education and Entertainment
// Copyright (c) 2008 by Michael S. Kenniston.  All rights reserved.
// Full documentation is at http://jiffeegames.com

// jiffee-places.js

// set up a proper namespace to avoid naming conflicts
var com;
if (!com) {com = {};}
if (!com.jiffeegames) {com.jiffeegames = {};}

/**
Create a Places object.
The typical call is:
<pre>
var places = new com.jiffeegames.Places();
</pre>
@class A Places object defines the API to create, describe, and connect
places, as well as the commands used to look at them and
to move around among them.
@requires com.jiffeegames.Persons
*/

com.jiffeegames.Places =
  function Places() {
    com.jiffeegames.Checks.validate(arguments);
  };

com.jiffeegames.Places.prototype.IMPLEMENTS = 'com.jiffeegames.Places';
com.jiffeegames.Places.prototype.USES = [
    'com.jiffeegames.Controller',
    'com.jiffeegames.Display',
    'com.jiffeegames.Help',
    'com.jiffeegames.Parser',
    'com.jiffeegames.Priorities',
    'com.jiffeegames.Scheduler',
    'com.jiffeegames.Traits',
    'com.jiffeegames.Verbs'
    ];

com.jiffeegames.Places.prototype.init =
  function init(locate, language) {
    var checks = com.jiffeegames.Checks;
    var places = this;
    checks.validate(arguments);

    var controller = locate('com.jiffeegames.Controller');
    var display = locate('com.jiffeegames.Display');
    var help = locate('com.jiffeegames.Help');
    var parser = locate('com.jiffeegames.Parser');
    var priorities = locate('com.jiffeegames.Priorities');
    var scheduler = locate('com.jiffeegames.Scheduler');
    var traits = locate('com.jiffeegames.Traits');
    var verbs = locate('com.jiffeegames.Verbs');

/**
Create a noun of type "place" with the given name.
@param {String} place The name to use.
@type void
*/

com.jiffeegames.Places.prototype.add =
  function add(place) {
    checks.validate(arguments);
    traits.addNoun(place);
    traits.freeze(place, 'is-place', true);
    traits.set(place, 'has-been-visited', false);
    if (traits.get('player', 'location') === '') {
      traits.set('player', 'location', place);
    }
  };

/**
Connect two places, in both directions.
The typical call is:
<pre>
places.connect("dining room", "north", "kitchen");
</pre>
which places the dining room north of the kitchen.
@param {String} place The name of the place you are positioning.
@param {String} direction The position of the place, stated as a
direction relative to the base location.
@param {String} base The name of the base location.
@type void
*/

com.jiffeegames.Places.prototype.connect =
  function connect(place, direction, base) {
    checks.validate(arguments);
    if (!traits.get(place, 'is-place')) {
      checks.error(arguments, 'J067', place + ' is not a place');
    }
    if (!traits.get(base, 'is-place')) {
      checks.error(arguments, 'J067', base + ' is not a place');
    }
    var reverseDir = this.REVERSE_DIRS_[direction];
    if (!reverseDir) {
      checks.error(arguments, 'J025', direction + ' is not a direction');
    }
    traits.freeze(place, reverseDir, 'MOVE TO ' + base);
    traits.freeze(base, direction, 'MOVE TO ' + place);
  };

/**
Create a new place and give it a long description.
The typical call is:
<pre>
places.describe("porch", 
                "You're on the porch.",
                "You are standing on a spacious porch blah blah");
</pre>
@param {String} pName The name of the place you are describing.
@param {String} whereAmI (optional) The response to "where am i".
@param {String} lookAround The long description of the place.
@type void
*/

com.jiffeegames.Places.prototype.describe =
  function describe(pName, whereAmI, lookAround) {
    if (arguments.length < 2 || arguments.length > 3) {
      checks.error(arguments, 'J027', '2 or 3 arguments required');
    }
    this.add(pName);
    if (arguments.length === 3) {
      traits.freeze(pName, 'where am i', arguments[1]);
      traits.freeze(pName, 'look around', arguments[2]);
    } else {
      traits.freeze(pName, 'look around', arguments[1]);
    }
  };

/**
Return the player's current location.
@returns The name of the Place where the player is now.
@type String
*/

com.jiffeegames.Places.prototype.here =
  function here() {
    checks.validate(arguments);
    var res = traits.get('player', 'location');
    if (!res) {
      checks.error(arguments, 'J054', 'player has no location');
    }
    return res;
  };

/**
Command handler for the "look" command.
Does a long detailed look around the current location.
@param {Object} event The event that triggered the call.
@type void
@private
*/

com.jiffeegames.Places.prototype.doLookAroundCmd_ =
  function doLookAroundCmd_(event) {
    checks.validate(arguments);
    var here = this.here();
    var override = traits.get(here, 'look around');
    if (override !== undefined) {
      display.show('<p>');
      try {
        controller.performAndCheckForChanges(override, event);
      } catch(e) {
        display.show('</p>');
        traits.set(here, 'has-been-visited', true);
        throw e;
      }
      display.erase();  // override did nothing, so we don't want leading '<p>'
    }
    traits.set(here, 'has-been-visited', true);
    this.doWhereAmICmd_(event);
  };

/**
Command handler for the "where" command.
Shows a quick, terse indication of where the player is.
@param {Object} task The event that triggered the call.
@type void
@private
*/

com.jiffeegames.Places.prototype.doWhereAmICmd_ =
  function doWhereAmICmd_(event) {
    checks.validate(arguments);
    var here = this.here();
    if (!traits.get(here, 'has-been-visited')) {
      this.doLookAroundCmd_(event);
      return;
    }
    display.show('<p>');
    var override = traits.get(here, 'where am i');
    if (override !== undefined) {
      try {
        controller.performAndCheckForChanges(override, event);
      } catch (e) {
        display.show('</p>');
        throw e;
      }
    }
    display.show('PLACES-THIS-IS-HERE', here);
    display.show('</p>');
  };

/**
Handle actions of the form "MOVE thing-name TO place-name", where thing-name
defaults to "player" if not specified, and place-name can be "HERE".
@returns true if this performer handles this action.
@type boolean
@private
*/

com.jiffeegames.Places.prototype.performMoveTo_ =
  function performMoveTo_(action, event) {
    checks.validate(arguments);
    // Determine whether the action is a legal MOVE TO action.
    if (typeof action !== 'string') {
      return false;
    }
    var res = action.match(/MOVE\s(\s*|\s*\S.+\s)TO\s(.*)/);
    if (! res) {
      return false;  // not a MOVE TO
    }
    var thing = res[1].replace(/^\s+|\s+$/g, '');  // strip surrounding spaces
    var place = res[2].replace(/^\s+|\s+$/g, '');
    if (thing === '') {
      thing = 'player';
    } else if (thing === 'DIRECT OBJECT') {
      thing = event.direct;
      if (!thing) {
        checks.error(arguments, 'J098',
                     'there is no direct object for "' + action + '"' );
      }
    }
    if (place === 'HERE') {
      place = this.here();
    }
    if (!traits.get(thing, 'is-prop') && thing !== 'player') {
      checks.error(arguments, 'J015', thing + ' is not a prop or player');
    }
    if (!traits.get(place, 'is-place')) {
      checks.error(arguments, 'J051', place + ' is not a place');
    }
    traits.set(thing, 'location', place);
    return true;
  };

/**
Command handler for the "go" command.
Moves the player in a compass direction, or up/down/inside/outside.
@param {Object} event The event that triggered the call.
@type void
@private
*/

com.jiffeegames.Places.prototype.doGoCmd_ =
  function doGoCmd_(event) {
    checks.validate(arguments);
    var dir = event.verb;
    if (!(dir in this.REVERSE_DIRS_)) {
      display.show('PLACES-BAD-DIR', dir);
      return;
    }
    var override = traits.get(this.here(), dir);
    if (override !== undefined) {
      controller.performAndCheckForChanges(override, event);
    }
    display.show('PLACES-NO-WAY-MOVE', dir);
  };

/**
Set the default language.
@type void
@member com.jiffeegames.Places
*/
com.jiffeegames.Places['en-us'] =
  function Places_en_us() {
    checks.validate(arguments);
    var d = display;
    d.t = d.translateTo;

    d.t('PLACES-THIS-IS-HERE', 'This is the ${1}.');
    d.t('PLACES-BAD-DIR', '${1} is not a direction that I understand.');
    d.t('PLACES-NO-WAY-MOVE', 'I see no way to move ${1} from here.');

    parser.addVerb('where am i', 'where', 'where am i');
    parser.addVerb('look around', 'l', 'look', 'look around');
    parser.addVerb('north', 'n', 'north',
                   'go n', 'go north', 'move n', 'move north');
    parser.addVerb('northeast', 'ne', 'northeast',
                   'go ne', 'go northeast', 'move ne', 'move northeast');
    parser.addVerb('east', 'e', 'east',
                   'go e', 'go east', 'move e', 'move east');
    parser.addVerb('southeast', 'se', 'southeast',
                   'go se', 'go southeast', 'move se', 'move southeast');
    parser.addVerb('south', 's', 'south',
                   'go s', 'go south', 'move s', 'move south');
    parser.addVerb('southwest', 'sw', 'southwest',
                   'go sw', 'go southwest', 'move sw', 'move southwest');
    parser.addVerb('west', 'w', 'west',
                   'go w', 'go west', 'move w', 'move west');
    parser.addVerb('northwest', 'nw', 'northwest',
                   'go nw', 'go northwest', 'move nw', 'move northwest');
    parser.addVerb('up', 'u', 'up',
                   'go u', 'go up', 'move u', 'move up');
    parser.addVerb('down', 'd', 'down',
                   'go d', 'go down', 'move d', 'move down');
    parser.addVerb('inside', 'in', 'inside',
                   'go in', 'go inside', 'move in', 'move inside');
    parser.addVerb('outside', 'out', 'outside',
                   'go out', 'go outside', 'move out', 'move outside');
  };

/**
Set the language to Spanish.
@type void
@member com.jiffeegames.Places
*/
com.jiffeegames.Places.es =
  function Places_es() {
    checks.validate(arguments);
    var d = display;
    d.t = d.translateTo;

    d.t('PLACES-THIS-IS-HERE', // 'This is the ${1}.');
'Este es el ${1}.');
    d.t('PLACES-BAD-DIR', // '${1} is not a direction that I understand');
'${1} no es una dirección que lo entiendo.');
    d.t('PLACES-NO-WAY-MOVE', // 'I see no way to move ${1} from here.');
'No veo ninguna manera de mover ${1} de aquí.');

    parser.addVerb('where am i', 'where', 'where am i');
    parser.addVerb('look around', 'l', 'look', 'look around');
    parser.addVerb('north', 'n', 'north',
                   'go n', 'go north', 'move n', 'move north');
    parser.addVerb('northeast', 'ne', 'northeast',
                   'go ne', 'go northeast', 'move ne', 'move northeast');
    parser.addVerb('east', 'e', 'east',
                   'go e', 'go east', 'move e', 'move east');
    parser.addVerb('southeast', 'se', 'southeast',
                   'go se', 'go southeast', 'move se', 'move southeast');
    parser.addVerb('south', 's', 'south',
                   'go s', 'go south', 'move s', 'move southeast');
    parser.addVerb('southeast', 'sw', 'southwest',
                   'go sw', 'go southwest', 'move sw', 'move southwest');
    parser.addVerb('west', 'w', 'west',
                   'go w', 'go west', 'move w', 'move west');
    parser.addVerb('northwest', 'nw', 'northwest',
                   'go nw', 'go northwest', 'move nw', 'move northwest');
    parser.addVerb('up', 'u', 'up',
                   'go u', 'go up', 'move u', 'move up');
    parser.addVerb('down', 'd', 'down',
                   'go d', 'go down', 'move d', 'move down');
    parser.addVerb('inside', 'in', 'inside',
                   'go in', 'go inside', 'move in', 'move inside');
    parser.addVerb('outside', 'out', 'outside',
                   'go out', 'go outside', 'move out', 'move outside');
  };

// continuation of init()

    traits.addEnum('is-place', false, true);
    traits.addEnum('has-been-visited', false, true);
    traits.set('player', 'location', '');
    controller.addPerformer(function(action, event){
                            return places.performMoveTo_(action, event);});
    priorities.add('look-places', 'normal');

    traits.addArb('where am i');
    var rule = scheduler.addRule('directive');
    rule.addExactMatch('verb', 'where am i');
    rule.addRejected('direct');
    rule.setPriority('look-places');
    rule.setAction(function(event){places.doWhereAmICmd_(event);});

    rule = scheduler.addRule('directive');
    rule.addExactMatch('verb', 'EMPTY');
    rule.addRejected('direct');
    rule.setPriority('look-places');
    rule.setAction(function(event){places.doWhereAmICmd_(event);});

    help.addSummary('where, ENTER',
                    'quick look around');

    rule = scheduler.addRule('change');
    rule.addExactMatch('noun', 'player');
    rule.addExactMatch('trait', 'location');
    rule.setPriority('look-places');
    rule.setAction(
        function(event){places.doWhereAmICmd_(event);});

    traits.addArb('look around');
    rule = scheduler.addRule('directive');
    rule.addExactMatch('verb', 'look around');
    rule.addRejected('direct');
    rule.setPriority('look-places');
    rule.setAction(function(event){places.doLookAroundCmd_(event);});
    help.addSummary('l, look',
                    'look (around you)');

    help.addSummary('n, north, go n, go north', 'move north');
    help.addSummary('go ne, e, se, s, sw, w, nw', 'move that direction');
    help.addSummary('u, up, go up', 'move up');
    help.addSummary('d, down, go down', 'move down');
    help.addSummary('in, out, go inside, go outside',
                    'move inside or outside');
    traits.addArb('north');
    traits.addArb('northeast');
    traits.addArb('east');
    traits.addArb('southeast');
    traits.addArb('south');
    traits.addArb('southwest');
    traits.addArb('west');
    traits.addArb('northwest');
    traits.addArb('up');
    traits.addArb('down');
    traits.addArb('inside');
    traits.addArb('outside');
    verbs.describe('north', '',
                   function(event){places.doGoCmd_(event);});
    verbs.describe('northeast', '',
                   function(event){places.doGoCmd_(event);});
    verbs.describe('east', '',
                   function(event){places.doGoCmd_(event);});
    verbs.describe('southeast', '',
                   function(event){places.doGoCmd_(event);});
    verbs.describe('south', '',
                   function(event){places.doGoCmd_(event);});
    verbs.describe('southwest', '',
                   function(event){places.doGoCmd_(event);});
    verbs.describe('west', '',
                   function(event){places.doGoCmd_(event);});
    verbs.describe('northwest', '',
                   function(event){places.doGoCmd_(event);});
    verbs.describe('up', '',
                   function(event){places.doGoCmd_(event);});
    verbs.describe('down', '',
                   function(event){places.doGoCmd_(event);});
    verbs.describe('inside', '',
                   function(event){places.doGoCmd_(event);});
    verbs.describe('outside', '',
                   function(event){places.doGoCmd_(event);});

    this.REVERSE_DIRS_ = {};
    this.REVERSE_DIRS_.north = 'south';
    this.REVERSE_DIRS_.northeast = 'southwest';
    this.REVERSE_DIRS_.east = 'west';
    this.REVERSE_DIRS_.southeast = 'northwest';
    this.REVERSE_DIRS_.south = 'north';
    this.REVERSE_DIRS_.southwest = 'northeast';
    this.REVERSE_DIRS_.west = 'east';
    this.REVERSE_DIRS_.northwest = 'southeast';
    this.REVERSE_DIRS_.up = 'down';
    this.REVERSE_DIRS_.down = 'up';
    this.REVERSE_DIRS_.inside = 'outside';
    this.REVERSE_DIRS_.outside = 'inside';

    var langDefs = com.jiffeegames.Places[language];
    if (!langDefs) {
      checks.error(arguments, 'J069',
                   'Places has no support for language ' + language);
    }
    langDefs.call(this);
  };

jiffee-priorities.js

// JIFFEE - JavaScript Interactive Fiction Framework for Education and Entertainment
// Copyright (c) 2008 by Michael S. Kenniston.  All rights reserved.
// Full documentation is at http://jiffeegames.com

// jiffee-priorities.js

// set up a proper namespace to avoid naming conflicts
var com;
if (!com) {com = {};}
if (!com.jiffeegames) {com.jiffeegames = {};}

/**
Create an empty Priorities object.  The normal call is
<pre>
var priority = new com.jiffeegames.Priorities();
</pre>
@class A Priorities object keeps track of named priority levels,
especially their order.
*/

com.jiffeegames.Priorities =
  function Priorities() {
    com.jiffeegames.Checks.validate(arguments);
  };

com.jiffeegames.Priorities.prototype.IMPLEMENTS = 'com.jiffeegames.Priorities';
com.jiffeegames.Priorities.prototype.USES = [];

com.jiffeegames.Priorities.prototype.init = 
  function init(locate, language) {
    var checks = com.jiffeegames.Checks;
    var priorities = this;
    checks.validate(arguments);

/**
Add a new name to the list of priorities that may be used, and
make it come before or after an existing priority.
Exactly one of "before" and "after" should already be defined.
The one which is not defined will be ordered before or after the one
that is already defined.  The priority "normal" is
already defined and acts as the base for all others.
@param {String} before The priority which comes first.
@param {String} after The priority which comes second.
@type void
*/

com.jiffeegames.Priorities.prototype.add =
  function add(before, after) {
    checks.validate(arguments);
    if (this.isStarted_) {
      checks.error(arguments, 'J005', 'priorities are already started');
    }
    if (typeof before !== 'string' || typeof after !== 'string' ||
        before.length < 1 || after.length < 1) {
      checks.error(arguments, 'J003', 'args must be non-empty strings');
    }

    var insertBefore;
    var anchor;
    var insert;
    if (before in this.validNames_) {
      if (after in this.validNames_) {
        checks.error(arguments, 'J006', 'both are already defined');
      } else {
        anchor = before;
        insert = after;
        insertBefore = false;
      }
    } else {
      if (after in this.validNames_) {
        anchor = after;
        insert = before;
        insertBefore = true;
      } else {
        checks.error(arguments, 'J020', 'neither is already defined');
      }
    }

    this.validNames_[insert] = true;
    var plen = this.orderedList_.length;
    for (var pdex = 0; pdex < plen; pdex++) {
      if (this.orderedList_[pdex] === anchor) {
        var pos = pdex + (insertBefore ? 0 : 1);
        this.orderedList_.splice(pos, 0, insert);
        break;
      }
    }
  };

/**
Freeze the set of strings that may be used as priorities,
by not allowing any more add() calls.
This lets all priorities be resolved into integers that are guaranteed not to change.
@type void
*/

com.jiffeegames.Priorities.prototype.start =
  function start() {
    checks.validate(arguments);
    if (this.isStarted_) {
      checks.error(arguments, 'J022', 'priorities already started');
    }

    this.isStarted_ = true;
  };

/**
Resolve a priority into its value.
@param {String} str The name of the priority to be resolved.
@returns The original string (before priorities are started),
or an integer that is guaranteed not to change (after priorities are started).
@type String or integer
*/

com.jiffeegames.Priorities.prototype.resolve =
  function resolve(str) {
    checks.validate(arguments);
    if (! (str in this.validNames_)) {
      checks.error(arguments, 'J021', 'no such priority is defined');
    }

    if (! this.isStarted_) {
      return str;
    }
    var plen = this.orderedList_.length;
    for (var pdex = 0; pdex < plen; pdex++) {
      if (this.orderedList_[pdex] === str) {
        return pdex;
      }
    }
  };

// continuation of init()

    this.isStarted_ = false;
    this.orderedList_ = ['normal'];
    this.validNames_ = {normal:true};
  };

jiffee-props.js

// JIFFEE - JavaScript Interactive Fiction Framework for Education and Entertainment
// Copyright (c) 2008 by Michael S. Kenniston.  All rights reserved.
// Full documentation is at http://jiffeegames.com

// jiffee-props.js

// set up a proper namespace to avoid naming conflicts
var com;
if (!com) {com = {};}
if (!com.jiffeegames) {com.jiffeegames = {};}

/**
Create a Props object.  The typical call is:
<pre>
var props = new com.jiffeegames.Props();
</pre>
@class A Props object defines the basic commands to
create and manipulate props, i.e. portable things.
*/

com.jiffeegames.Props =
  function Props() {
    com.jiffeegames.Checks.validate(arguments);
  };

com.jiffeegames.Props.prototype.IMPLEMENTS = 'com.jiffeegames.Props';
com.jiffeegames.Props.prototype.USES = [
    'com.jiffeegames.Controller',
    'com.jiffeegames.Display',
    'com.jiffeegames.Help',
    'com.jiffeegames.Parser',
    'com.jiffeegames.Places',
    'com.jiffeegames.Scheduler',
    'com.jiffeegames.Traits',
    'com.jiffeegames.Verbs'
    ];

com.jiffeegames.Props.prototype.init =
  function init(locate, language) {
    var checks = com.jiffeegames.Checks;
    var props = this;
    checks.validate(arguments);

    var controller = locate('com.jiffeegames.Controller');
    var display = locate('com.jiffeegames.Display');
    var help = locate('com.jiffeegames.Help');
    var parser = locate('com.jiffeegames.Parser');
    var places = locate('com.jiffeegames.Places');
    var scheduler = locate('com.jiffeegames.Scheduler');
    var traits = locate('com.jiffeegames.Traits');
    var verbs = locate('com.jiffeegames.Verbs');

/**
Create a noun of type "prop" with the given name.
@param {String} name The name to use.
@type void
*/

com.jiffeegames.Props.prototype.add =
  function add(pName) {
    checks.validate(arguments);
    traits.addNoun(pName);
    traits.freeze(pName, 'is-prop', true);
  };

/**
Create a new prop and give it a long description and initial location.
The typical call is:
<pre>
props.describe("ball", "grassy field",
"This is a standard-sized black-and-white soccer ball,
and it feels like it is pumped up to the regulation pressure.");
</pre>
@param {String} name The name of the prop you are describing.
@param {String} location The initial location of the prop.
@param {String} lookAround (optional) The string that describes the prop when present.
@param {String} inventory (optional) The string that describes the prop when carried.
@param {String} examine The string the describes the prop when examined.
@type void
*/

com.jiffeegames.Props.prototype.describe =
  function describe(pName, location, lookAround, inventory, examine) {
    if (arguments.length !== 3 && arguments.length !== 5) {
      checks.error(arguments, 'J078', '2 or 3 arguments required');
    }
    this.add(pName);
    if (arguments.length === 3) {
      traits.set(pName, 'location', arguments[1]);
      traits.freeze(pName, 'examine', arguments[2]);
    } else {
      traits.set(pName, 'location', arguments[1]);
      traits.freeze(pName, 'look around', arguments[2]);
      traits.freeze(pName, 'inventory', arguments[3]);
      traits.freeze(pName, 'examine', arguments[4]);
    }
  };

/**
Test a prop to see if it is being carried.
@param {String} prop The name of the prop being tested.
@returns true if the prop is being carried, else returns false.
@type bool
*/

com.jiffeegames.Props.prototype.carried =
  function carried(prop) {
    checks.validate(arguments);
    return traits.get(prop, 'location') === 'player';
  };

/**
Test a prop to see if it is present.
@param {String} prop The name of the prop being tested.
@returns true if the prop is present, else returns false.
@type bool
*/

com.jiffeegames.Props.prototype.present =
  function present(prop) {
    checks.validate(arguments);
    var loc = traits.get(prop, 'location');
    return traits.get(prop, 'location') === places.here();
  };

/**
Test a prop to see if it is reachable.
@param {String} prop The name of the prop being tested.
@returns true if the prop is reachable, else returns false.
@type bool
*/

com.jiffeegames.Props.prototype.reachable =
  function reachable(prop) {
    checks.validate(arguments);
    var loc = traits.get(prop, 'location');
    return (loc === 'player') || (loc === places.here());
  };

/**
Decide how to describe a prop in the current location.
@returns The string which should be displayed.
@type void
@private
*/

com.jiffeegames.Props.prototype.showHereDesc_ =
  function showHereDesc_(prop) {
    checks.validate(arguments);
    var override = traits.get(prop, 'look around');
    if (override !== undefined) {
      try {
        controller.performAndCheckForChanges(override, null);
      } catch(e) {
        if ((e === 'event done') || (e === 'event not done')) {
          return;  // this is called inside loops where we should NOT terminate
        }
        throw e;
      }
    }
    display.show('PROPS-I-SEE-HERE', prop);
  };

/**
Decide how to describe the player carrying a prop.
@returns The string which should be displayed.
@type String
@private
*/

com.jiffeegames.Props.prototype.showInventory_ =
  function showInventory_(prop) {
    checks.validate(arguments);
    var override = traits.get(prop, 'inventory');
    if (override !== undefined) {
      try {
        controller.performAndCheckForChanges(override, null);
      } catch(e) {
        if ((e === 'event done') || (e === 'event not done')) {
          return;  // this is called inside loops where we should NOT terminate
        }
        throw e;
      }
    }
    display.show('PROPS-ARE-CARRYING', prop);
  };

/**
Display a list of the items which are present (but not carried) in the current location.
@type void
@private
*/

com.jiffeegames.Props.prototype.doLookCmd_ =
  function doLookCmd_(event) {
    checks.validate(arguments);
    var nounSet = traits.findNouns('location', places.here());
    var found = false;
    for (var noun in nounSet) {
      if (nounSet.hasOwnProperty(noun) && traits.get(noun, 'is-prop')) {
        if (!found) {
          display.show("<p>");
          found = true;
        } else {
          display.show("<br>");
        }
        this.showHereDesc_(noun);
      }
    }
    if (found) {
      display.show("</p>");
    }
    throw 'event done';  // even if there is nothing to show
  };

/**
Command handler for the "examine" command.
Displays a detailed description of the specified item.
@param {Object} event The event that triggered the call.
@type void
@private
*/

com.jiffeegames.Props.prototype.doExamineCmd_ =
  function doExamineCmd_(event) {
    checks.validate(arguments);
    var noun = event.direct;
    if (noun === 'LITERAL') {
      return;
    }
    if (!this.reachable(noun)) {
      display.show('PROPS-SEE-NONE-HERE', noun);
      throw 'event done';
    }
    display.show('<p>');
    var override = traits.get(noun, 'examine');
    if (override !== undefined) {
      try {
        controller.performAndCheckForChanges(override, event);
      } catch(e) {
        display.show('</p>');
        throw e;
      }
    }
    display.show('PROPS-NOTHING-REMARKABLE', noun);
    display.show('</p>');      
  };

/**
Command handler for the "get" command.
Picks up the specified item, if possible, and displays confirmation.
@param {Object} event The event that triggered the call.
@type void
@private
*/

com.jiffeegames.Props.prototype.doGetCmd_ =
  function doGetCmd_(event) {
    checks.validate(arguments);
    var prop = event.direct;
    if (prop === 'LITERAL') {
      return;
    }
    var loc = traits.get(prop, 'location');
    if (loc === 'player') {
      display.show('PROPS-ALREADY-CARRYING', prop);
      throw 'event done';
    }
    if (loc !== places.here()) {
      display.show('PROPS-SEE-NONE-HERE', prop);
      throw 'event done';
    }
    var override = traits.get(prop, 'get');
    if (override !== undefined) {
      controller.performAndCheckForChanges(override, event);
    }
    display.show('<p>');
    traits.set(prop, 'location', 'player');
    this.showInventory_(prop);
    display.show('</p>');
  };

/**
Command handler for the "put" command.
Puts down the specified item, if possible, and displays confirmation.
@param {Object} event The event that triggered the call.
@type void
@private
*/

com.jiffeegames.Props.prototype.doPutCmd_ =
  function doPutCmd_(event) {
    checks.validate(arguments);
    var prop = event.direct;
    if (prop === 'LITERAL') {
      return;
    }
    if (!this.carried(prop)) {
      display.show('PROPS-NOT-CARRYING', prop);
      throw 'event done';
    }
    var override = traits.get(prop, 'put');
    if (override !== undefined) {
      controller.performAndCheckForChanges(override, event);
    }
    display.show('<p>');
    traits.set(prop, 'location', places.here());
    this.showHereDesc_(prop);
    display.show('</p>');
  };

/**
Command handler for the "put" command.
Displays an inventory of all the props that the player is currently carrying.
@param {Object} event The event that triggered the call.
@type void
@private
*/

com.jiffeegames.Props.prototype.doInventoryCmd_ =
  function doInventoryCmd_(event) {
    checks.validate(arguments);
    display.show('<p>');
    var nounSet = traits.findNouns('location', 'player');
    var found = false;
    for (var noun in nounSet) {
      if (nounSet.hasOwnProperty(noun)) {
        if (found) {
          display.show("<br>");
        }
        found = true;
        this.showInventory_(noun);
      }
    }
    if (!found) {
      display.show('PROPS-NOT-CARRYING-ANYTHING');
    }
    display.show('</p>');
  };

/**
Define the English input and output.
@type void
@member com.jiffeegames.Props
*/
com.jiffeegames.Props['en-us'] =
  function Props_en_us() {
    checks.validate(arguments);
    var d = display;
    d.t = d.translateTo;

    d.t('PROPS-I-SEE-HERE', 'I see the ${1} here.');
    d.t('PROPS-ARE-CARRYING', 'You are carrying the ${1}.');
    d.t('PROPS-NOTHING-REMARKABLE',
        'There\'s nothing remarkable about it; this is just an ordinary ${1}.');
    d.t('PROPS-NOT-CARRYING-ANYTHING', 'You\'re not carrying anything.');
    d.t('PROPS-SEE-NONE-HERE', 'I see no ${1} here.');
    d.t('PROPS-NOT-CARRYING', 'You\'re not carrying the ${1}.');
    d.t('PROPS-ALREADY-CARRYING', 'You\'re already carrying the ${1}.');

    parser.addVerb('examine', 'x', 'examine', 'look at', 'check out');
    parser.addVerb('get', 'get', 'take', 'grab', 'pick up');
    parser.addVerb('put', 'put', 'put down', 'drop', 'release',
                   'set', 'set down');
    parser.addVerb('inventory', 'i', 'inv', 'inventory', 'show inventory');
  };

/**
Define the Spanish input and output.
@type void
@member com.jiffeegames.Props
*/
com.jiffeegames.Props.es =
  function Props_es() {
    checks.validate(arguments);
    var d = display;
    d.t = d.translateTo;

    d.t('PROPS-I-SEE-HERE', // 'I see the ${1} here.');
        'Veo la ${1} aquí.');
    d.t('PROPS-ARE-CARRYING', // 'You are carrying the ${1}.');
        'Que están llevando a la ${1}.');
    d.t('PROPS-NOTHING-REMARKABLE',
        // 'There\'s nothing remarkable about it; this is just an ordinary ${1}.');
        'No hay nada notable sobre él, es sólo una corriente ${1}.');
    d.t('PROPS-NOT-CARRYING-ANYTHING', // 'You\'re not carrying anything.');
        'Usted no llevar nada.');
    d.t('PROPS-SEE-NONE-HERE', // 'I see no ${1} here.');
        'No veo ningún ${1} aquí.');
    d.t('PROPS-NOT-CARRYING', // 'You\'re not carrying the ${1}.');
        'Usted no está llevando el ${1}.');
    d.t('PROPS-ALREADY-CARRYING', // 'You\'re already carrying the ${1}.');
        'Usted ya está llevando el ${1}.');

    parser.addVerb('examine', 'x', 'examine', 'look at', 'check out');
    parser.addVerb('get', 'get', 'take', 'grab', 'pick up');
    parser.addVerb('put', 'put', 'put down', 'drop', 'release',
                   'set', 'set down');
    parser.addVerb('inventory', 'i', 'inv', 'inventory', 'show inventory');
  };

// continuation of init()

    traits.addEnum('is-prop', false, true);
    traits.addArb('examine');

    verbs.describe('examine', '*',
                   function(event){props.doExamineCmd_(event);});
    help.addSummary('x, examine &lt;a thing&gt;', 
                    'look at an object in detail');

    traits.addArb('get');
    verbs.describe('get', '*',
                   function(event){props.doGetCmd_(event);});
    help.addSummary('get, take, pick up &lt;a thing&gt;',
                    'pick up an object');
  
    traits.addArb('put');
    verbs.describe('put', '*',
                   function(event){props.doPutCmd_(event);});
    help.addSummary('put, drop &lt;a thing&gt;',
                    'set down an object');

    traits.addArb('inventory');
    var rule = scheduler.addRule('directive');
    rule.addExactMatch('verb', 'inventory');
    rule.addRejected('direct');
    rule.setPriority('look-places');
    rule.setAction(function(event){props.doInventoryCmd_(event);});
    help.addSummary('i, inv, inventory',
                    'show what you\'re carrying');

    // The normal "look around" code handles the WHERE/LOOK/change event and
    // the controller thinks that command is done so it does a nextEvent().
    // In order to get our code (that shows what objects are present) to execute
    // _after_ the default code, we need to make it be a separate command.

    rule = scheduler.addRule('directive');
    rule.addExactMatch('verb', 'where am i');
    rule.addRejected('direct');
    rule.setPriority('look-places');
    rule.setAction(function(event){scheduler.notifyEvent(
                       {eventType:'directive',
                        verb:'PROPS-LOOK', raw:event.raw});});

    rule = scheduler.addRule('directive');
    rule.addExactMatch('verb', 'EMPTY');
    rule.addRejected('direct');
    rule.setPriority('look-places');
    rule.setAction(function(event){scheduler.notifyEvent(
                       {eventType:'directive',
                        verb:'PROPS-LOOK', raw:event.raw});});

    rule = scheduler.addRule('directive');
    rule.addExactMatch('verb', 'look around');
    rule.addRejected('direct');
    rule.setPriority('look-places');
    rule.setAction(function(event){scheduler.notifyEvent(
                       {eventType:'directive',
                        verb:'PROPS-LOOK', raw:event.raw});});

    rule = scheduler.addRule('change');
    rule.addExactMatch('noun', 'player');
    rule.addExactMatch('trait', 'location');
    rule.setPriority('look-places');
    rule.setAction(function(event){scheduler.notifyEvent(
        {eventType:'directive',
         verb:'PROPS-LOOK', raw:'[show props in new location]'});});

    verbs.describe('PROPS-LOOK', '',
                   function(event){props.doLookCmd_(event);});

    var langDefs = com.jiffeegames.Props[language];
    if (!langDefs) {
      checks.error(arguments, 'J070',
                   'Props has no support for language ' + language);
    }
    langDefs.call(this);
  };

jiffee-scheduler.js

// JIFFEE - JavaScript Interactive Fiction Framework for Education and Entertainment
// Copyright (c) 2008 by Michael S. Kenniston.  All rights reserved.
// Full documentation is at http://jiffeegames.com

// jiffee-scheduler.js

// set up a proper namespace to avoid naming conflicts
var com;
if (!com) {com = {};}
if (!com.jiffeegames) {com.jiffeegames = {};}

/**
Create a Scheduler object.  The normal call is
<pre>
var scheduler = new com.jiffeegames.Scheduler();
</pre>
@constructor
@requires com.jiffeegames.Checks
@requires com.jiffeegames.Priorities
@class A Scheduler object maintains the queue of pending events and the
rules that tell how to respond to them.
It contains the following:
<ul>
<li>The master rule-list, which contains all the known rules about how
to respond to events.
<li>The event-queue, which is a FIFO queue of events which have
occurred but have not been processed.
</ul>
When events occure, call <em>notifyEvent()</em> to inform the Scheduler module.
To retrieve data, call
<em>nextEvent()</em> to get the next pending event,
and call <em>nextAction()</em> repeatedly
to get the actions that respond to that event.
*/

com.jiffeegames.Scheduler =
  function Scheduler() {
    com.jiffeegames.Checks.validate(arguments);
  };

com.jiffeegames.Scheduler.prototype.IMPLEMENTS = 'com.jiffeegames.Scheduler';
com.jiffeegames.Scheduler.prototype.USES = [
  'com.jiffeegames.Priorities'
  ];

com.jiffeegames.Scheduler.prototype.init =
  function init(locate, language) {
    var checks = com.jiffeegames.Checks;
    var scheduler = this;
    checks.validate(arguments);
    var priorities = locate('com.jiffeegames.Priorities');

/**
Tell the Scheduler that priorities may now be resolved.
Map all preliminary priorities into their corresponding integers.
Hereafter, priorities will be mapped to integers as soon as we see them.
@type void
*/

com.jiffeegames.Scheduler.prototype.start =
  function start() {
    this.started_ = true;
    for (var i = 0; i < this.allRules_.length; i++) {
      rule = this.allRules_[i];
      rule.priority = priorities.resolve(rule.priority);
    }
    this.allRules_.sort(this.sortByPriority_);
  };

/**
Compare the "priority" attributes of two objects.
@private
@param a One of the objects.
@param b The other object.
@returns The difference between their priorities.
@type float
*/

com.jiffeegames.Scheduler.prototype.sortByPriority_ =
  function sortByPriority_(a, b) {
    // reverse sense of comparison so getNextTask() can pop() earliest
    return (b.priority + b.subPriority) - (a.priority + a.subPriority);
  };

/**
Tell the Scheduler module that an event occurred.
An event is an object with an "eventType" field
<ul>
<li>If the eventType is "change", the other fields will include
"noun", "trait", "oldVal", "newVal".
<li>If the eventType is "directive", the other fields will include
"raw", "verb", and optionally "word", "direct", "indirect", and propositions.
</ul>
@param {Event} event the event that occurred
@type void
*/

com.jiffeegames.Scheduler.prototype.notifyEvent =
  function notifyEvent(event) {
    checks.validate(arguments);
    for (var p in event) {
      if (event.hasOwnProperty(p)) {
        this.checkPropertyName_(p);
      }
    }
    if (this.started_) {
      this.eventQueue_.push(event);
    }
  };

/**
Get the next event from the event queue.
Events are returned in chronological order, i.e. it is a FIFO queue.
Getting an event flushes any leftover actions from the previous event.
@returns the next event to be processed,
or undefined if there are no more events left.
@type Event
*/

com.jiffeegames.Scheduler.prototype.nextEvent =
  function nextEvent() {
    checks.validate(arguments);
    this.currentEvent_ = this.eventQueue_.shift();
    if (this.currentEvent_) {
      this.lastRuleNum_ = this.allRules_.length;
    } else {
      this.lastRuleNum_ = 0;
    }
    return this.currentEvent_;  // undefined if none left
  };

/**
For debugging only.
@returns A string description of a given rule number.
@type String
*/

com.jiffeegames.Scheduler.prototype.dumpRule_ =
  function dumpRule_(num) {
    var rule = this.allRules_[num];
    var res = 'Rule #' + num + ': ';
    for (var trait in rule.exactMatch) {
      if (rule.exactMatch.hasOwnProperty(trait)) {
        res += '<' + trait + ':' + rule.exactMatch[trait] + '>';
      }
    }
    var i;
    for (i = 0; i < rule.present.length; i++) {
      res += '<required:' + rule.present[i] + '>';
    }
    for (i = 0; i < rule.notPresent.length; i++) {
      res += '<rejected:' + rule.notPresent[i] + '>';
    }
    res += '<priority:' + rule.priority + '>';
    res += '<action: ' + rule.action + '>';
    return res;
  };

/**
Get the next action which should be performed for the current event.
Actions are returned in priority order,
and within a given priority they are returned in LIFO order (most
recently specified rule takes priority).
@returns the next action to be processed,
or undefined if there are no more actions left.
@type Action
*/

com.jiffeegames.Scheduler.prototype.nextAction =
  function nextAction() {
    checks.validate(arguments);
    while (this.lastRuleNum_ > 0) {
      this.lastRuleNum_ -= 1;
      var rule = this.allRules_[this.lastRuleNum_];
      if (rule.match(this.currentEvent_)) {
        return rule.action;
      }
    }
    return undefined;
  };

/**
Add a rule to the game, and return a reference to it.
@param {String} eventType The type of event to which this rule will apply.
@type Rule
*/

com.jiffeegames.Scheduler.prototype.addRule =
  function addRule(eventType) {
    checks.validate(arguments);
    checks.initOnly(arguments, this.started_);
    var rule = new com.jiffeegames.Scheduler.Rule();
    rule.exactMatch.eventType = eventType;
    rule.subPriority = this.whenSeq_;
    this.allRules_.push(rule);
    this.whenSeq_ -= 0.000001;
    if (this.whenSeq_ <= -1.0) {
      checks.error(arguments, 'J056', 'too many calls to addRule()');
    }
    return rule;
  };

/**
Return a hash of all the verbs that we know about.
Example call:
<pre>
var schedVerbs = scheduler.getAllVerbs();
</pre>
@returns A hash of all the verbs we know about.
@type {Hash of String}
*/

com.jiffeegames.Scheduler.prototype.getAllVerbs =
  function getAllVerbs() {
    checks.validate(arguments);
    var verbs = {};
    var rlen = this.allRules_.length;
    for (var rdex = 0; rdex < rlen; rdex++) {
      var verb = this.allRules_[rdex].exactMatch.verb;
      if (verb !== undefined) {
        verbs[verb] = true;
      }
    }
    return verbs;
  };

/**
Specify that a property name is legal for use in events and rules.
Example call:
<pre>
scheduler.addLegalProperty("oldVal");
</pre>
The purpose of this function is to allow JIFFEE to detect when you misspell a
property name later, so that it can throw an exception with a useful message
instead of silently failing to match in the intended way.
@type void
*/

com.jiffeegames.Scheduler.prototype.addLegalProperty =
  function addLegalProperty(propertyName) {
    checks.validate(arguments);
    checks.initOnly(arguments, this.started_);
    if (typeof propertyName !== 'string') {
      checks.error(arguments, 'J023', 'property name must be a string');
    }
    this.legalPropertyNames_[propertyName] = true;
  };

/**
Check that a property name is legal, and throw an exception if it isn't.
Example call:
<pre>
this.checkPropertyName("foo");
</pre>
@type void
*/

com.jiffeegames.Scheduler.prototype.checkPropertyName_ =
  function checkPropertyName_(propertyName) {
    checks.validate(arguments);
    if (this.legalPropertyNames_[propertyName] !== true) {
      checks.error(arguments, 'J024',
                   '"' + propertyName + '" is not a legal property name');
    }
  };

// continuation of init()

    this.started_ = false;
    this.whenSeq_ = 0.0;  // used to sort w/in a given priority

    this.eventQueue_ = [];
    this.allRules_ = [];
    this.currentEvent_ = undefined;
    this.lastRuleNum_ = 0;
    this.legalPropertyNames_ = {};
    this.addLegalProperty('eventType');
    // All the other properties are added by the modules that use/define them.
    // In general, these are the Traits and Parser modules.

/**
@class
@constructor
*/

    com.jiffeegames.Scheduler.Rule =
      function Rule() {
        checks.validate(arguments);
        this.present = [];
        this.notPresent = [];
        this.exactMatch = {};
        this.priority = 'normal';
        this.action = '';
      };

/**
Try to match a rule to an event.
Example call:
<pre>
if (rule.match(event)) ...
</pre>
@returns true if it matches, false if it does not.
@type Rule
*/

    com.jiffeegames.Scheduler.Rule.prototype.match =
      function match(event) {
        checks.validate(arguments);
        for (var prop in this.exactMatch) {
          if (this.exactMatch.hasOwnProperty(prop)) {
            if (this.exactMatch[prop] !== event[prop]) {
              return false;
            }
          }
        }
        var i;
        for (i = 0; i < this.present.length; i++) {
          if (!event.hasOwnProperty(this.present[i])) {
            return false;
          }
        }
        for (i = 0; i < this.notPresent.length; i++) {
          if (event.hasOwnProperty(this.notPresent[i]))  {
            return false;
          }
        }
        return true;
      };

/**
Specify that a rule only matches if a property in the event
has a specific value.
Example call:
<pre>
rule.addExactMatch('verb', 'jump');
</pre>
@returns The rule, to allow call-chaining.
@type Rule
*/

    com.jiffeegames.Scheduler.Rule.prototype.addExactMatch =
      function addExactMatch(prop, value) {
        checks.validate(arguments);
        scheduler.checkPropertyName_(prop);
        this.exactMatch[prop] = value;
        return this;
      };

/**
Specify that a rule only matches if a property IS present in the event.
Example call:
<pre>
rule.addRequired('direct');  // verb must be transitive
</pre>
@returns The rule, to allow call-chaining.
@type Rule
*/

    com.jiffeegames.Scheduler.Rule.prototype.addRequired =
      function addRequired(prop) {
        checks.validate(arguments);
        scheduler.checkPropertyName_(prop);
        this.present.push(prop);
        return this;
      };

/**
Specify that a rule only matches if a property is NOT present in the event.
Example call:
<pre>
rule.addRejected('direct');  // must be an intransitive verb
</pre>
@returns The rule, to allow call-chaining.
@type Rule
*/

    com.jiffeegames.Scheduler.Rule.prototype.addRejected =
      function addRejected(prop) {
        checks.validate(arguments);
        scheduler.checkPropertyName_(prop);
        this.notPresent.push(prop);
        return this;
      };

/**
Set the priority at which a rule will try to match.
Example call:
<pre>
rule.setPriority('before-jumping');
</pre>
@returns The rule, to allow call-chaining.
@type Rule
*/

    com.jiffeegames.Scheduler.Rule.prototype.setPriority =
      function setPriority(priority) {
        checks.validate(arguments);
        this.priority = priority;
        return this;
      };

/**
Specify the action that should be performed if a rule matches.
Example call:
<pre>
rule.setAction("A bright flash momentarily blinds you.");
</pre>
@returns The rule, to allow call-chaining.
@type Rule
*/

    com.jiffeegames.Scheduler.Rule.prototype.setAction =
      function setAction(action) {
        checks.validate(arguments);
        this.action = action;
        return this;
      };
  };

jiffee-score.js

// JIFFEE - JavaScript Interactive Fiction Framework for Education and Entertainment
// Copyright (c) 2008 by Michael S. Kenniston.  All rights reserved.
// Full documentation is at http://jiffeegames.com

// jiffee-score.js

// set up a proper namespace to avoid naming conflicts
var com;
if (!com) {com = {};}
if (!com.jiffeegames) {com.jiffeegames = {};}

/**
Create a Score object.  The normal call is
<pre>
var score = com.jiffeegames.Score();
</pre>
@class A Score object keeps track of the player's score,
and it announces when the player has won the game.
@constructor
*/

com.jiffeegames.Score =
  function Score() {
    com.jiffeegames.Checks.validate(arguments);
  };

com.jiffeegames.Score.prototype.IMPLEMENTS = 'com.jiffeegames.Score';
com.jiffeegames.Score.prototype.USES = [
    'com.jiffeegames.Display',
    'com.jiffeegames.Help',
    'com.jiffeegames.Parser',
    'com.jiffeegames.Places',
    'com.jiffeegames.Traits',
    'com.jiffeegames.Verbs'
    ];

com.jiffeegames.Score.prototype.init =
  function init(locate, language) {
    var checks = com.jiffeegames.Checks;
    var score = this;
    checks.validate(arguments);

    var display = locate('com.jiffeegames.Display');
    var help = locate('com.jiffeegames.Help');
    var parser = locate('com.jiffeegames.Parser');
    var places = locate('com.jiffeegames.Places');
    var traits = locate('com.jiffeegames.Traits');
    var verbs = locate('com.jiffeegames.Verbs');

/**
Display the current score to the user.
@param {Integer} current The player's current score.
@param {Integer} max The maximum possible (i.e. winning) score.
@type void
@private
*/

com.jiffeegames.Score.prototype.showScore_ =
  function showScore_(current, max) {
    checks.validate(arguments);
    display.show('SCORE-CURRENT-SCORE',
                 {literal:String(current)},
                 {literal:String(max)});
  };

/**
Set the maximum number of points which the player can score, i.e.
the number required to win the game.
@param {Integer} points The number of points needed to win the game.
*/

com.jiffeegames.Score.prototype.setMaximum =
  function setMaximum(points) {
    checks.validate(arguments);
    traits.addRange('maximum score', points);
    traits.freeze('game', 'maximum score', points);
    traits.addRange('current score', points);
    traits.set('game', 'current score', 0);
  };

/**
Define what message should be displayed when the player wins the game.
@param {String} message The message to be displayed.
@type void
*/

com.jiffeegames.Score.prototype.setWinningMessage =
  function setWinningMessage(message) {
    checks.validate(arguments);
    traits.set('you have won the game', 'look around', message);
  };

/**
Add points to the player's current score (or deduct points
if the increment is negative).
The player's score must stay between zero and the maximum at all times.
@param {Integer} points - The number of points to add to the score.
@type void
*/

com.jiffeegames.Score.prototype.add =
  function add(points) {
    checks.validate(arguments);
    var current = traits.get('game', 'current score');
    var max = traits.get('game', 'maximum score');
    current += points;
    if (current > max) {
      checks.error(arguments, 'J080',
                   'new score of ' + current +
                       ' exceeds maximum score of ' + max);
    }
    if (current < 0) {
      checks.error(arguments, 'J081',
                   'new score of ' + current + ' is less than zero');
    }
    traits.set('game', 'current score', current);
    this.showScore_(current, max);
    if (current === max) {
      traits.set('player', 'location', 'you have won the game');
    }
  };


/**
Set the default language.
@type void
@member com.jiffeegames.Score
*/
com.jiffeegames.Score['en-us'] =
  function Score_en_us() {
    checks.validate(arguments);
    var d = display;
    d.t = d.translateTo;

    d.t('SCORE-ALREADY-WON-GAME', "You've already won the game.");
    d.t('SCORE-CURRENT-SCORE', '<p>Your score is now ${1} out of a possible ${2}.</p>');

    parser.addVerb('score', 'score');
  };

/**
Set the language to Spanish.
@type void
@member com.jiffeegames.Score
*/
com.jiffeegames.Score.es =
  function Score_es() {
    checks.validate(arguments);
    var d = display;
    d.t = d.translateTo;

    d.t('SCORE-ALREADY-WON-GAME', // "You've already won the game.");
'Ya ha ganado el juego.');
    d.t('SCORE-CURRENT-SCORE', // '<p>Your score is now ${1} out of a possible ${2}.</p>');
'<p>Su puntuación es de ${1}, de un posible ${2}.</p>');

    parser.addVerb('score', 'score');
  };

// continuation of init()

    places.add('you have won the game');
    traits.freeze('you have won the game',
               'where am i',
               'SCORE-ALREADY-WON-GAME');

    verbs.describe('score', '',
                   function(event) {
                     score.showScore_(traits.get('game', 'current score'),
                                      traits.get('game', 'maximum score'));
                   });
    help.addSummary('score',
                    'display the current score');

    var langDefs = com.jiffeegames.Score[language];
    if (!langDefs) {
      checks.error(arguments, 'J083',
                   'Score has no support for language ' + language);
    }
    langDefs.call(this);
  };

jiffee-sha.js

// This is open-source code from http://jssha.sourceforge.net.
// This file includes only the parts needed by JIFFEE, not the whole package.
// This code is covered by its own copyright, not by the JIFFEE copyright.
/*
Copyright (c) 2008, Brian Turek
All rights reserved.

Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:

 * Redistributions of source code must retain the above copyright notice, this
   list of conditions and the following disclaimer.
 * Redistributions in binary form must reproduce the above copyright notice,
   this list of conditions and the following disclaimer in the documentation
   and/or other materials provided with the distribution.
 * The names of the contributors may not be used to endorse or promote products
   derived from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
OF THE POSSIBILITY OF SUCH DAMAGE.
*/

/* A JavaScript implementation of the SHA family of hashes, as defined in FIPS PUB 180-2
 * Version 1.11 Copyright Brian Turek 2008
 * Distributed under the BSD License
 * See http://jssha.sourceforge.net/ for more information
 *
 * Several functions taken from Paul Johnson
 */
 
function jsSHA(srcString){jsSHA.charSize=8;jsSHA.b64pad ="";jsSHA.hexCase=0;var sha224=null;var sha256=null;var str2binb=function(str){var bin=[];var mask =(1 << jsSHA.charSize)- 1;var length=str.length*jsSHA.charSize;for(var i=0;i<length;i += jsSHA.charSize){bin[i >> 5] |=(str.charCodeAt(i/jsSHA.charSize)& mask)<<(32-jsSHA.charSize-i%32);}return bin;};var strBinLen=srcString.length*jsSHA.charSize;var strToHash=str2binb(srcString);var binb2hex=function(binarray){var hex_tab=jsSHA.hexCase?"0123456789ABCDEF":"0123456789abcdef";var str="";var length=binarray.length*4;for(var i=0;i<length;i++){str += hex_tab.charAt((binarray[i >> 2] >>((3-i%4)* 8+4))& 0xF)+ hex_tab.charAt((binarray[i >> 2] >>((3-i%4)* 8))& 0xF);}return str;};var binb2b64=function(binarray){var tab="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";var str="";var length=binarray.length*4;for(var i=0;i<length;i += 3){var triplet =(((binarray[i >> 2] >> 8 *(3-i%4))& 0xFF)<< 16)|(((binarray[i+1 >> 2] >> 8 *(3 -(i+1)% 4))& 0xFF)<< 8)|((binarray[i+2 >> 2] >> 8 *(3 -(i+2)% 4))& 0xFF);for(var j=0;j<4;j++){if(i*8+j*6>binarray.length*32){str += jsSHA.b64pad;}else{str += tab.charAt((triplet >> 6 *(3-j))& 0x3F);}}}return str;};var rotr=function(x,n){if(n<32){return(x >>> n)|(x <<(32-n));}else{return x;}};var shr=function(x,n){if(n<32){return x >>> n;}else{return 0;}};var ch=function(x,y,z){return(x & y)^(~x & z);};var maj=function(x,y,z){return(x & y)^(x & z)^(y & z);};var sigma0=function(x){return rotr(x,2)^ rotr(x,13)^ rotr(x,22);};var sigma1=function(x){return rotr(x,6)^ rotr(x,11)^ rotr(x,25);};var gamma0=function(x){return rotr(x,7)^ rotr(x,18)^ shr(x,3);};var gamma1=function(x){return rotr(x,17)^ rotr(x,19)^ shr(x,10);};var safeAdd=function(x,y){var lsw =(x & 0xFFFF)+(y & 0xFFFF);var msw =(x >>> 16)+(y >>> 16)+(lsw >>> 16);return((msw & 0xFFFF)<< 16)|(lsw & 0xFFFF);};var coreSHA2=function(variant){var W=[];var a,b,c,d,e,f,g,h;var T1,T2;var H;var K=[0x428A2F98,0x71374491,0xB5C0FBCF,0xE9B5DBA5,0x3956C25B,0x59F111F1,0x923F82A4,0xAB1C5ED5,0xD807AA98,0x12835B01,0x243185BE,0x550C7DC3,0x72BE5D74,0x80DEB1FE,0x9BDC06A7,0xC19BF174,0xE49B69C1,0xEFBE4786,0x0FC19DC6,0x240CA1CC,0x2DE92C6F,0x4A7484AA,0x5CB0A9DC,0x76F988DA,0x983E5152,0xA831C66D,0xB00327C8,0xBF597FC7,0xC6E00BF3,0xD5A79147,0x06CA6351,0x14292967,0x27B70A85,0x2E1B2138,0x4D2C6DFC,0x53380D13,0x650A7354,0x766A0ABB,0x81C2C92E,0x92722C85,0xA2BFE8A1,0xA81A664B,0xC24B8B70,0xC76C51A3,0xD192E819,0xD6990624,0xF40E3585,0x106AA070,0x19A4C116,0x1E376C08,0x2748774C,0x34B0BCB5,0x391C0CB3,0x4ED8AA4A,0x5B9CCA4F,0x682E6FF3,0x748F82EE,0x78A5636F,0x84C87814,0x8CC70208,0x90BEFFFA,0xA4506CEB,0xBEF9A3F7,0xC67178F2];if(variant==="SHA-224"){H=[0xc1059ed8,0x367cd507,0x3070dd17,0xf70e5939,0xffc00b31,0x68581511,0x64f98fa7,0xbefa4fa4];}else{H=[0x6A09E667,0xBB67AE85,0x3C6EF372,0xA54FF53A,0x510E527F,0x9B05688C,0x1F83D9AB,0x5BE0CD19];}var message=strToHash.slice();message[strBinLen >> 5] |= 0x80 <<(24-strBinLen%32);message[((strBinLen+1+64 >> 9)<< 4)+ 15]=strBinLen;var appendedMessageLength=message.length;for(var i=0;i<appendedMessageLength;i += 16){a=H[0];b=H[1];c=H[2];d=H[3];e=H[4];f=H[5];g=H[6];h=H[7];for(var t=0;t<64;t++){if(t<16){W[t]=message[t+i];}else{W[t]=safeAdd(safeAdd(safeAdd(gamma1(W[t-2]),W[t-7]),gamma0(W[t-15])),W[t-16]);}T1=safeAdd(safeAdd(safeAdd(safeAdd(h,sigma1(e)),ch(e,f,g)),K[t]),W[t]);T2=safeAdd(sigma0(a),maj(a,b,c));h=g;g=f;f=e;e=safeAdd(d,T1);d=c;c=b;b=a;a=safeAdd(T1,T2);}H[0]=safeAdd(a,H[0]);H[1]=safeAdd(b,H[1]);H[2]=safeAdd(c,H[2]);H[3]=safeAdd(d,H[3]);H[4]=safeAdd(e,H[4]);H[5]=safeAdd(f,H[5]);H[6]=safeAdd(g,H[6]);H[7]=safeAdd(h,H[7]);}switch(variant){case "SHA-224":return[H[0],H[1],H[2],H[3],H[4],H[5],H[6]];case "SHA-256":return H;default:return [];}};this.getHash=function(variant,format){var formatFunc=null;switch(format){case "HEX":formatFunc=binb2hex;break;case "B64":formatFunc=binb2b64;break;default:return "FORMAT NOT RECOGNIZED";}switch(variant){case "SHA-224":if(sha224===null){sha224=coreSHA2(variant);}return formatFunc(sha224);case "SHA-256":if(sha256===null){sha256=coreSHA2(variant);}return formatFunc(sha256);default:return "HASH NOT RECOGNIZED";}};}

jiffee-traits.js

// JIFFEE - JavaScript Interactive Fiction Framework for Education and Entertainment
// Copyright (c) 2008 by Michael S. Kenniston.  All rights reserved.
// Full documentation is at http://jiffeegames.com

// jiffee-traits.js

// set up a proper namespace to avoid naming conflicts
var com;
if (!com) {com = {};}
if (!com.jiffeegames) {com.jiffeegames = {};}

/**
Create a Traits object.  The normal call is
<pre>
var traits = new com.jiffeegames.Traits();
</pre>
@constructor
@requires com.jiffeegames.Checks
@class A Traits object manages the state of the game, all of which is stored in noun/trait/value slots in a master table.
Rules about set/change/get:
<ul>
<li>Each noun/trait pair is called a "slot".
<li>To give a value to a slot you can call freeze() exactly once, or
you can call set() one or more times, but you cannot mix freeze() with set().
<li>freeze() must be done right after the noun is added.
<li>set() may not be used with an arbTrait.
<li>Every slot that is ever given a value by either freeze() or set(),
must be given a value before the game is started.
<li>If you get() a slot that has never been given a value, the result is undefined.
</ul>
The Traits object needs to know about the Encoding and Scheduler objects
so that it can notify them whenever the value of a slot changes.
*/

com.jiffeegames.Traits =
  function Traits() {
    com.jiffeegames.Checks.validate(arguments);
  };

com.jiffeegames.Traits.prototype.IMPLEMENTS = 'com.jiffeegames.Traits';
com.jiffeegames.Traits.prototype.USES = [
    'com.jiffeegames.Scheduler',
    'com.jiffeegames.Encoding'
    ];

com.jiffeegames.Traits.prototype.init =
  function init(locate, language) {
    var checks = com.jiffeegames.Checks;
    var traits = this;
    checks.validate(arguments);
    var scheduler = locate('com.jiffeegames.Scheduler');
    var encoding = locate('com.jiffeegames.Encoding');

/**
Return the current change sequence number.
@type int
*/

com.jiffeegames.Traits.prototype.getSeq =
  function getSeq() {
    checks.validate(arguments);
    return this.seq_;
  };

/**
Make a sure a new name is legal for a noun or a trait.
@param {String} str The name to be checked.
@type void
@throws Error if the name is not legal
@private
*/

com.jiffeegames.Traits.prototype.validateName_ =
  function validateName_(str) {
    if (typeof str !== 'string') {
      checks.error(arguments, 'J029',
                   'name must be a string');
    }
    if (str.length < 1) {
      checks.error(arguments, 'J030',
                   'name must contain at least one character');
    }
    var pos = str.search(/[^\-_a-zA-Z0-9 ']/);
    if (pos >= 0) {
      checks.error(arguments, 'J031',
                   'names may not contain special chars like "' +
                       str.charAt(pos) + '"');
    }
    if (this.isStarted_) {
      checks.error(arguments, 'J032',
                   'new names may only be defined before game starts');
    }
    if (this.nouns_[str] !== undefined) {
      checks.error(arguments, 'J033',
                   'that name is already in use for a noun');
    }
    if (this.traits_[str] !== undefined) {
      checks.error(arguments, 'J034',
                   'that name is already in use for a trait');
    }
  };

/**
Check whether a noun with the given name exists.
@param {String} name The name to check.
@returns true if the noun exists, else false.
@type boolean
*/

com.jiffeegames.Traits.prototype.isNoun =
  function isNoun(nounName) {
    return nounName in this.nouns_;
  };

/**
Check whether a trait with the given name exists.
@param {String} name The name to check.
@returns true if the trait exists, else false.
@type boolean
*/

com.jiffeegames.Traits.prototype.isTrait =
  function isTrait(traitName) {
    return traitName in this.traits_;
  };

/**
Create a noun with the given name.
@param {String} noun The name to use.
@type void
*/

com.jiffeegames.Traits.prototype.addNoun =
  function addNoun(noun) {
    checks.validate(arguments);
    this.validateName_(noun);

    this.nounNames_[this.numNouns_] = noun;
    this.nouns_[noun] = this.numNouns_++;
  };

/**
Add an "arbitrary" trait to the game.
The values of this trait are unrestricted (may be of arbitrary type),
but can only be set with "freeze()", never with "set()".
@param {String} trait The name of the trait.
@type void
*/

com.jiffeegames.Traits.prototype.addArb =
  function addArb(trait) {
    checks.validate(arguments);
    this.validateName_(trait);

    this.traits_[trait] = {type: 'arbitrary'};
  };

/**
Add a "reference" trait to the game.
The values of this trait are restricted to the names of nouns.
@param {String} trait The name of the trait.
@type void
*/

com.jiffeegames.Traits.prototype.addRef =
  function addRef(trait) {
    checks.validate(arguments);
    this.validateName_(trait);

    this.traits_[trait] = {type: 'ref'};
  };

/**
Add a "range" trait to the game.
The values of this trait are restricted to integers between [0 ... max].
@param {String} trait The name of the trait.
@param {integer} max The maximum allowed value (the minimum is always zero).
@type void
*/

com.jiffeegames.Traits.prototype.addRange =
  function addRange(trait, max) {
    checks.validate(arguments);
    this.validateName_(trait);

    this.traits_[trait] = {type: 'range', max: arguments[1]};
  };

/**
Find the index of a value in an Array.
@param {Array} arr The Array to be searched.
@param value The value to be searched for.
@returns The index within the Array, or undefined if "val" does not appear.
@type integer
@private
*/

com.jiffeegames.Traits.prototype.arrDex_ =
  function arrDex_(arr, value) {
    var alen = arr.length;
    for (var adex = 0; adex < alen; adex++) {
      if (arr[adex] === value) {
        return adex;
      }
    }
    return undefined;
  };

/**
Add an "enumeration" trait to the game.
The values of this trait are restricted to the specified (enumerated) values.
Example:
<pre>
traits.addEnum('precipitation', 'none', 'rain', 'sleet', 'snow', 'fog');
</pre>
@param {String} trait The name of the trait.
@param {anytype} values A list of the possible values
@type void
*/

com.jiffeegames.Traits.prototype.addEnum =
  function addEnum(trait, values) {
    if (arguments.length < 3) {
      checks.error(arguments, 'J035', 'needs at least 3 arguments');
    }
    this.validateName_(trait);

    var list = [];
    var alen = arguments.length;
    for (var adex = 1; adex < alen; adex++) {
      var val = arguments[adex];
      if (this.arrDex_(list, val) !== undefined) {
        checks.error(arguments, 'J036',
                     'value "' + val + '" occurs twice in list');
      }
      list[adex - 1] = val;
    }
    this.traits_[trait] = {type: 'enum', values: list};
  };

/**
Set the game State to "started".
This means that "freeze()" is no longer legal.
@type void
*/

com.jiffeegames.Traits.prototype.start =
  function start() {
    checks.validate(arguments);
    if (this.isStarted_) {
      checks.error(arguments, 'J037', 'game is already started');
    }

    this.addVarsToEncoding_();
    encoding.startManually();
    this.isStarted_ = true;
  };

/**
Create a new Slot object.
The real work is just to do error-checking on the names.
@param {String} noun The name of the noun.
@param {String} trait The name of the trait.
@returns The Slot object created.
@type Object
@private
*/

com.jiffeegames.Traits.prototype.addSlot_ =
  function addSlot_(noun, trait) {
    if (this.nouns_[noun] === undefined) {
      checks.error(arguments, 'J038', 'no such noun exists');
    }
    if (this.traits_[trait] === undefined) {
      checks.error(arguments, 'J039', 'no such trait exists');
    }
    return {};
  };

/**
Set the value of a slot.
Using this method freezes the value of the slot for all time.
@param {String} noun The name of the noun.
@param {String} trait The name of the trait.
@param value The value of the slot.
@type void
*/

com.jiffeegames.Traits.prototype.freeze =
  function freeze(noun, trait, value) {
    checks.validate(arguments);

    var key = noun + '/' + trait;
    var varDesc = this.slots_[key];
    if (varDesc) {
      checks.error(arguments, 'J040',
                   'you cannot freeze a noun/trait value that already exists');
    }
    if (this.isStarted_) {
      checks.error(arguments, 'J041',
                   'you cannot freeze a noun/trait value after the game starts');
    }

    varDesc = this.addSlot_(noun, trait);
    this.slots_[key] = varDesc;
    // N.B. do not set varDesc.vnum
    varDesc.value = value;
  };

/**
Set the value of a slot.
Using this method allows the value of the slot to change throughout the 
course of the game (unless the trait is Arbitrary, in which case set()
actually calls freeze().
@param {String} noun The name of the noun.
@param {String} trait The name of the trait.
@param value The value of the slot.
@type void
*/

com.jiffeegames.Traits.prototype.set =
  function set(noun, trait, value) {
    checks.validate(arguments);
    var traitDesc = this.traits_[trait];
    if (traitDesc && traitDesc.type === 'arbitrary') {
      return this.freeze(noun, trait, value);
    }
    var key = noun + '/' + trait;
    var varDesc = this.slots_[key];
    if (varDesc && varDesc.vnum === undefined) {
      checks.error(arguments, 'J042',
                   'set() cannot be called after freeze()');
    }
    if (this.isStarted_) {
      if (varDesc === undefined) {
        checks.error(arguments, 'J043',
                     'set() must be called once before the game starts');
      }
      if (varDesc.value === value) {
        return;  // no actual change
      }
      encoding.setVar(varDesc.vnum, this.asInt_(trait, value));
      scheduler.notifyEvent({
          eventType:'change',
          noun:noun,
          trait:trait,
          oldVal:varDesc.value,
          newVal:value});
      this.seq_++;
    } else {
      if (varDesc === undefined) {
        varDesc = this.addSlot_(noun, trait);
        this.slots_[key] = varDesc;
        varDesc.vnum = -1;
        this.asInt_(trait, value);  // for error checking on value
      }
    }
    varDesc.value = value;
  };

/**
Convert a trait value to the corresponding integer.
@param {String} trait The name of the trait.
@param value The value to be converted.
@returns The integer equivalent (which is what gets encoded into the cookie).
@type integer
@private
*/

com.jiffeegames.Traits.prototype.asInt_ =
  function asInt_(trait, value) {
    // precondition:  trait exists
    // precondition:  no more nouns will be created
    var traitDesc = this.traits_[trait];
    if (traitDesc.type === 'arbitrary') {
      return undefined;
    } else if (traitDesc.type === 'ref') {
      var nounNum = this.nouns_[value];
      if (nounNum === undefined) {
        checks.error(arguments, 'J044',
                     'refTrait value must be the name of a noun');
      }
      return nounNum;
    } else if (traitDesc.type === 'range') {
      if (typeof value !== 'number' ||  // only numerics values allowed
          value !== (value | 0) ||  // make sure it is integer, not float
          !(value >= 0) ||  // written strangely to make sure NaN works
          value > traitDesc.max) {
        checks.error(arguments, 'J045',
                     'rangeTrait value must be integer between zero and ' +
                         traitDesc.max);
      }
      return value;
    } else if (traitDesc.type === 'enum') {
      var vdex = this.arrDex_(traitDesc.values, value);
      if (vdex === undefined) {
        checks.error(arguments, 'J046',
                     'enumTrait value must be one of the enumerated values');
      }
      return vdex;
    }
  };

/**
For each slot that has been created, add a corresponding variable to the Encoding object and associate the variable number with the slot.
@type void
@private
*/

com.jiffeegames.Traits.prototype.addVarsToEncoding_ =
  function addVarsToEncoding_() {
    checks.validate(arguments);
    if (this.isStarted_) {
      checks.error(arguments, 'J047', 'game is already started');
    }

    for (var key in this.slots_) {
      if (this.slots_.hasOwnProperty(key)) {
        var pair = key.split('/');
        var noun = pair[0];
        var trait = pair[1];
        var varDesc = this.slots_[key];
        var traitDesc = this.traits_[trait];
        var intValue = this.asInt_(trait, varDesc.value);
        if (varDesc.vnum === undefined) {
          continue;  // fixed trait
        }
        if (traitDesc.type === 'ref') {
          varDesc.vnum = encoding.addVar(noun, trait, this.numNouns_ - 1);
        } else if (traitDesc.type === 'range') {
          varDesc.vnum = encoding.addVar(noun, trait, traitDesc.max);
        } else if (traitDesc.type === 'enum') {
          varDesc.vnum = encoding.addVar(noun,
                                         trait,
                                         traitDesc.values.length - 1);
        }
        encoding.setVar(varDesc.vnum, intValue);
      }
    }
  };

/**
Retrieve the values of all changeable slots from the encoding,
discarding the current slot values.  Do not notify the Scheduler
of the changes, because this is done only to implement
a "restore" or "restart".
@type void
*/

com.jiffeegames.Traits.prototype.getVarsFromEncoding =
  function getVarsFromEncoding() {
    checks.validate(arguments);
    if (!this.isStarted_) {
      checks.error(arguments, 'J075', 'game has not yet started');
    }

    for (var key in this.slots_) {
      if (this.slots_.hasOwnProperty(key)) {
        var pair = key.split('/');
        var trait = pair[1];
        var varDesc = this.slots_[key];
        if (varDesc.vnum === undefined) {
          continue;  // fixed trait
        }
        var traitDesc = this.traits_[trait];
        var vnum = varDesc.vnum;
        var intValue = encoding.getVar(vnum);
        if (traitDesc.type === 'ref') {
          varDesc.value = this.nounNames_[intValue];
        } else if (traitDesc.type === 'range') {
          varDesc.value = intValue;
        } else if (traitDesc.type === 'enum') {
          varDesc.value = traitDesc.values[intValue];
        }
      }
    }
  };

/**
Get the value of a slot.
@param {String} noun The name of the noun.
@param {String} trait The name of the trait.
@returns The value of that slot.
@type Any
*/

com.jiffeegames.Traits.prototype.get =
  function get(noun, trait) {
    checks.validate(arguments);

    var key = noun + '/' + trait;
    var varDesc = this.slots_[key];
    if (varDesc === undefined) {
      this.addSlot_(noun, trait);  // for error checking
      return undefined;
    }
    return varDesc.value;
  };

/**
Find the set of all nouns with a given trait/value pair.
@param {String} trait The name of the trait.
@param value The value you are looking for.
@returns A hash table of all the nouns which have what you're looking for.
@type HashTable_of_NounNames
*/

com.jiffeegames.Traits.prototype.findNouns =
  function findNouns(trait, value) {
    checks.validate(arguments);
    if (! (trait in this.traits_)) {
      checks.error(arguments, 'J049', 'no such trait');
    }

    var nounSet = {};
    for (var key in this.slots_) {
      if (this.slots_.hasOwnProperty(key)) {
        var pair = key.split('/');
        if (pair[1] === trait && this.slots_[key].value === value) {
          nounSet[pair[0]] = true;
        }
      }
    }
    return nounSet;
  };

/**
Find the set of all things.  Example call:
<pre>
var thingList = things.getAllThings();
</pre>
@returns A list of all the thing names.
@type {Hash of String}
*/

com.jiffeegames.Traits.prototype.getAllThings =
  function getAllThings() {
    checks.validate(arguments);

    var nounSet = {};
    for (var key in this.slots_) {
      if (this.slots_.hasOwnProperty(key)) {
        var pair = key.split('/');
        if (pair[1] === 'is-prop' && this.slots_[key].value === true) {
          nounSet[pair[0]] = true;
        }
      }
    }
    return nounSet;
  };

// continuation of init()

    this.isStarted_ = false;
    this.numNouns_ = 0;
    this.nouns_ = {};
    this.nounNames_ = {};
    this.traits_ = {};
    this.slots_ = {};
    this.seq_ = 0;

    this.nounNames_[this.numNouns_] = '';
    this.nouns_[''] = this.numNouns_++;

    scheduler.addLegalProperty('noun');
    scheduler.addLegalProperty('trait');
    scheduler.addLegalProperty('oldVal');
    scheduler.addLegalProperty('newVal');
  };

jiffee-verbs.js

// JIFFEE - JavaScript Interactive Fiction Framework for Education and Entertainment
// Copyright (c) 2008 by Michael S. Kenniston.  All rights reserved.
// Full documentation is at http://jiffeegames.com

// jiffee-verbs.js

// set up a proper namespace to avoid naming conflicts
var com;
if (!com) {com = {};}
if (!com.jiffeegames) {com.jiffeegames = {};}

/**
Create a Verbs object.  The typical call is:
<pre>
var verbs = new com.jiffeegames.Verbs();
</pre>
@constructor
@class A Verbs object defines and implements the creation of new verbs
with which the user can manipulate the game.
*/

com.jiffeegames.Verbs =
  function Verbs() {
    com.jiffeegames.Checks.validate(arguments);
  };

com.jiffeegames.Verbs.prototype.IMPLEMENTS = 'com.jiffeegames.Verbs';
com.jiffeegames.Verbs.prototype.USES = [
    'com.jiffeegames.Controller',
    'com.jiffeegames.Display',
    'com.jiffeegames.Scheduler',
    'com.jiffeegames.Traits'
    ];

com.jiffeegames.Verbs.prototype.init =
  function init(locate, language) {
    var checks = com.jiffeegames.Checks;
    var verbs = this;
    checks.validate(arguments);
    var controller = locate('com.jiffeegames.Controller');
    var display = locate('com.jiffeegames.Display');
    var scheduler = locate('com.jiffeegames.Scheduler');
    var traits = locate('com.jiffeegames.Traits');

/** Define the action which should be performed when a certain verb
is used in a certain context.  This is a convenience routine, which simply
creates and adds an appropriate rule to make the action occur at the desired
time.  It does not express the full generality possible with JIFFEE rules,
but it does provide enough power to get started writing interesting
games before learning to write JavaScript functions.  The full form is:
<pre>
verbs.describe(&lt;verb&gt;,
               &lt;noun&gt;|"*"|"",
               &lt;optional modifier clause&gt;, ...
               &lt;action&gt;
              );
where each &lt;modifier clause&gt; is one of:
               &lt;selection clause&gt;, ...
               &lt;override clause&gt;, ...
and each &lt;selection clause&gt; is one of:
               "&lt;preposition&gt; &lt;noun&gt;",
               "WHEN &lt;noun&gt; IS [NOT] CARRIED",
               "WHEN &lt;noun&gt; IS [NOT] HERE",
               "WHEN &lt;noun&gt; IS [NOT] REACHABLE",
               "WHEN &lt;noun&gt; IS [NOT] AT &lt;place&gt;",
and each &lt;override clause&gt; is one of:
               "DIRECT OBJECT OVERRIDES",
               "PLACE OVERRIDES",
and each &lt;noun&gt; can be any one of:
               the name of a noun,
               "player",
               "DIRECT OBJECT".
</pre>
and a typical call would be:
<pre>
verbs.describe("give", "box",
               "to dwarf",
               "WHEN box IS CARRIED",
               "WHEN PLAYER IS AT bridge",
               ["The troll winks and mutters a strange incantation.",
                "MOVE PLAYER TO forest"]
              );
</pre>
<ul>
<li>The first argument is the verb itself.  If this doesn't match, the rule
will not match and nothing will happen.
<li>The second argument is the direct object.  It must be the name of a
noun, "*" if you want to match on any noun, or "" if the verb is intransitive.
<li>A selection clause of the form "<preposition> <noun>" means that this rule
will only match if the command includes that preposition-noun combination.
<li>A selection clause of the form "WHEN <noun> IS CARRIED/HERE/REACHABLE" means
that this rule will match only when the specified noun is being carried by the player (or is in the same place as the player, or is either carried or in the same place).
<li>A selection clause of the form "WHEN <noun> IS AT <place>" means the rule will
only match when the specified noun is at the specified place.
<li>An override clause of the form "DIRECT OBJECT OVERRIDES" means that
if the noun which is specified as the direct object has a trait with the
same name as this verb, that trait's value will be taken as an override
and performed instead of the default action.  If the override doesn't do 
anything (no output and no state changes), the default action will also
be performed.
<li>An override clause of the form "PLACE OVERRIDES" means that if the place
which is the player's current location has a trait with the same name
as the this verb, that trait's value will be taken as an override.
If an override is specified for both DIRECT OBJECT and PLACE, they will be
checked in the order specified, and if the first does nothing then the second
will be performed (and if that does nothing, the default action will be
performed).
<li>The last argument is the action which should be performed.
The event passed to the action will be populated with:
<ul>
<li>event.eventType (always 'directive').
<li>event.verb (the name of the verb that was used)
<li>event.direct (the name of the direct object, if any)
<li>event.&lt;preposition&gt;, e.g. event.to, event.after, or event['with'],
i.e. the name of the noun that
appears after that preposition.  If the preposition happens to be a
reserved word like 'with', you must use the subscript notation
to reference it.
</ul>
</ul>
If any of the selection clauses does not match, this verb description does
nothing and
JIFFEE will keep looking for another rule or description that does match.
In particular,
if the player is for example <em>not</em> carrying a noun that a selection
clause must be carried,
no error message will be displayed.  If you want that, you have to specify
that explicitly with a separate call to verbs.describe().  Note, however,
that there are default actions programmed when the command includes the object
of a verb or preposition which is not present or carried.
@returns The rule that will match for this action.
@type Rule
*/

com.jiffeegames.Verbs.prototype.describe =
  function describe(verb, direct) {
    var nargs = arguments.length;
    if (nargs < 3) {
      checks.error(arguments, 'J085', 'at least 3 arguments are required');
    }
    var action = arguments[nargs-1];
    if (typeof verb !== 'string') {
      checks.error(arguments, 'J086', 'verb must be a string');
    }
    if (typeof direct !== 'string') {
      checks.error(arguments, 'J095', 'direct object must be a string');
    }

    var rule = scheduler.addRule('directive');
    rule.addExactMatch('verb', verb);
    if (direct === '') {
      rule.addRejected('direct');
    } else if (direct === '*') {
      rule.addRequired('direct');
    } else {
      rule.addExactMatch('direct', direct);
    }

    var modifiers = [];
    for (var modDex = 2; modDex < nargs - 1; modDex++) {
      var clause = arguments[modDex];
      if (typeof clause !== 'string') {
        checks.error(arguments, 'J096',
                     'modifier clause ' + modDex + ' must be a string');
      }
      var modifier = this.parseModifier_(clause);
      if (modifier.length > 1 && modifier[1] === 'DIRECT OBJECT' &&
          direct === '') {
        checks.error(arguments, 'J097',
                     'an intransitive verb has no DIRECT OBJECT');
      }
      modifiers.push(modifier);
      if (modifier[0] === 'DIRECT OBJECT OVERRIDES' ||
          modifier[0] === 'PLACE OVERRIDES') {
        if (!traits.isTrait(verb)) {
          traits.addArb(verb);
        }
      }
    }
    rule.setAction(function(event) {
      var overrides = verbs.checkModifiers_(event, modifiers);
      for (var i = 0; i < overrides.length; i++) {
        controller.performAndCheckForChanges(overrides[i], event);
      }
      controller.performAndCheckForChanges(action, event);
    });
    return rule;
  };

/**
Run through a modifier list.  If everything matches, return.
If anything fails to match, throw 'rule done' to force this rule to terminate.
Return a list of any applicable override actions.
@parameter {Array} list of modifier descriptors.
@type void
*/

com.jiffeegames.Verbs.prototype.checkModifiers_ =
  function checkModifiers_(event, modifiers) {
    var overrides = [];
    var override;
    var nounLoc;
    var playerLoc;
    var place;
    for (var i = 0; i < modifiers.length; i++ ) {
      var mod = modifiers[i];
      if (mod.length > 1 && mod[1] === 'DIRECT OBJECT') {
        mod[1] = event.direct;
      }

      if (mod[0] === 'DIRECT OBJECT OVERRIDES') {
        if (event.direct) {
          override = traits.get(event.direct, event.verb);
          if (override !== undefined) {
            overrides.push(override);
          }
        }

      } else if (mod[0] === 'PLACE OVERRIDES') {
        playerLoc = traits.get('player', 'location');
        override = traits.get(playerLoc, event.verb);
        if (override !== undefined) {
          overrides.push(override);
        }

      } else if (mod[0] === 'CARRIED') {
        nounLoc = traits.get(mod[1], 'location');
        if (nounLoc !== 'player') {
          throw 'rule done';
        }

      } else if (mod[0] === 'NOTCARRIED') {
        nounLoc = traits.get(mod[1], 'location');
        if (nounLoc === 'player') {
          throw 'rule done';
        }

      } else if (mod[0] === 'HERE') {
        playerLoc = traits.get('player', 'location');
        nounLoc = traits.get(mod[1], 'location');
        if (nounLoc !== playerLoc) {
          throw 'rule done';
        }

      } else if (mod[0] === 'NOTHERE') {
        playerLoc = traits.get('player', 'location');
        nounLoc = traits.get(mod[1], 'location');
        if (nounLoc === playerLoc) {
          throw 'rule done';
        }

      } else if (mod[0] === 'REACHABLE') {
        playerLoc = traits.get('player', 'location');
        nounLoc = traits.get(mod[1], 'location');
        if (nounLoc !== playerLoc && nounLoc !== 'player') {
          throw 'rule done';
        }

      } else if (mod[0] === 'NOTREACHABLE') {
        playerLoc = traits.get('player', 'location');
        nounLoc = traits.get(mod[1], 'location');
        if (nounLoc === playerLoc || nounLoc === 'player') {
          throw 'rule done';
        }

      } else if (mod[0] === 'AT') {
        nounLoc = traits.get(mod[1], 'location');
        place = mod[2];
        if (nounLoc !== place) {
          throw 'rule done';
        }

      } else if (mod[0] === 'NOTAT') {
        nounLoc = traits.get(mod[1], 'location');
        place = mod[2];
        if (nounLoc === place) {
          throw 'rule done';
        }

      } else {
        var prep = mod[0];
        var noun = mod[1];
        if (!event.hasOwnProperty(prep) || event[prep] !== noun) {
          throw 'rule done';
        }
      }
    }
    return overrides;
  };

/**
Helper function to parse one modifier clause.
<pre>
var res = this.parseModifier_(clause);
</pre>
@param {String} clause - the modifier clause in string form
@returns a list of [type-of-modifier, optional-arg1, optional-arg2]
@type Array
*/

com.jiffeegames.Verbs.prototype.parseModifier_ =
  function parseModifier_(clause) {
    checks.validate(arguments);
    // strip surrounding spaces
    function strip(c) {return c.replace(/^\s+|\s+$/g, '');}
    clause = strip(clause);
    var res;
    res = clause.match(/^DIRECT\s+OBJECT\s+OVERRIDES$/);
    if (res) {
      return ['DIRECT OBJECT OVERRIDES'];
    }
    res = clause.match(/^PLACE\s+OVERRIDES$/);
    if (res) {
      return ['PLACE OVERRIDES'];
    }
    res = clause.match(
        /^WHEN\s+(\S.*)\sIS(\s+|\s+NOT\s+)(CARRIED|HERE|REACHABLE)$/);
    var noun;
    var toggle;
    if (res) {
      noun = strip(res[1]);
      toggle = strip(res[2]);
      var which = res[3];
      return [toggle + which, noun];
    }
    res = clause.match(/^WHEN\s+(\S.*)\sIS(\s+|\s+NOT\s+)AT\s+(\S.*)$/);
    if (res) {
      noun = strip(res[1]);
      toggle = strip(res[2]);
      var place = strip(res[3]);
      return [toggle + 'AT', noun, place];
    }
    res = clause.match(/WHEN/);
    if (res) {
      checks.error(arguments, 'J099', 'invalid WHEN modifier clause');
    }
    res = clause.match(/OVERRIDES/);
    if (res) {
      checks.error(arguments, 'J100', 'invalid OVERRIDES modifier clause');
    }
    res = clause.match(/^(\S+)\s+(\S.*)$/);
    if (res) {
      var prep = strip(res[1]);
      noun = strip(res[2]);
      return ['PREP', prep, noun];
    }
    checks.error(arguments, 'J028', 'not a valid modifier clause');
  };

/**
Set the default language.
@type void
@member com.jiffeegames.Verbs
*/
com.jiffeegames.Verbs['en-us'] =
  function Verbs_en_us() {
    checks.validate(arguments);
    var d = display;
    d.t = d.translateTo;
  };

/**
Set the language to Spanish.
@type void
@member com.jiffeegames.Verbs
*/
com.jiffeegames.Verbs.es =
  function Verbs_es() {
    checks.validate(arguments);
    var d = display;
    d.t = d.translateTo;
  };

// continuation of init()

    // It is a bit tricky to break the cyclic logical dependency between
    // Verbs and Persons/Places.  To do it, the base noun and trait that
    // doVerb_ needs to use must be created here instead of inside
    // the Persons and Places modules where you would normally expect.

    traits.addNoun('player');
    traits.addRef('location');

    var langDefs = com.jiffeegames.Verbs[language];
    if (!langDefs) {
      checks.error(arguments, 'J068',
                   'Verbs has no support for language ' + language);
    }
    langDefs.call(this);
  };