Movin' On Down the Road

After contemplating the sweet user interface and very significant underlying limitations of Undum, I wrote a couple of blog posts:

midiguru.wordpress.com/stuck-in-lodi-again
midiguru.wordpress.com/look-through-telescope

I’d like to invite comments on them, either on my blog or here. This is not about any specific Other Development System, it’s a sort of informal manifesto for a development system that doesn’t yet exist.

It hasn’t escaped my attention that there are a few people who enjoy developing alternate authoring systems. Most of the latter are, by their own creators’ admission, not as capable as Inform or TADS. It seems to me that what we need is, on the contrary, something better.

Better? Yes, better. Both I7 and T3 are wonderful, full-featured systems, as long as you’re satisfied to write games that accept user input via the command prompt – a user interface that was state-of-the-art in 1982. However fond some of us may be of the Command Line Interface, it seems pretty clear that it’s a giant stumbling block for the potential audience for our games. These posts dissect that problem in some detail, and speculate about some possible solutions.

You can code graphical user interfaces with TADS. See attached “game” (not really a game, just a demonstration).
graphicalBIV.zip (932 KB)

True enough. As I noted in the other thread, however, the documentation on the Web UI gives the author no advice or guidance on how to do so. In addition, I would note that your demonstration is still a text-based game. I know you’re fond of the CLI, Nikos – and indeed, for some purposes it may be the best interface. Even so, your demonstration is a long way from anything resembling the nice text output and clickable interface found in Undum … and I would have NO idea, based on the T3 documentation, how to leap over that chasm and produce such an interface using T3.

Btw, it’s not mine. It’s this:

ifdb.tads.org/viewgame?id=e2qi61r56mtufr6m

I attached a compiled game here since only source code is provided by the author.

Um, so, I’ve spent the last year working full time on exactly this, so forgive me for banging my drum again.

Quest is a fully web-based IF platform. You can customise the player UI. For example, here is a demo of a split-screen game with two command prompts: play.textadventures.co.uk/v5/Pla … lves.quest

Wiki page with more detail on interfacing between Quest and HTML/JavaScript: quest5.net/wiki/Using_Javascript

Here is a blog post all about using hyperlinks, which is default functionality: textadventures.co.uk/blog/20 … -the-verb/

… and how hyperlinks work in a game that runs in a smartphone app: textadventures.co.uk/blog/20 … d-android/

Oh and you don’t even need to install anything to create a game. Write a text adventure on your iPad if you want: textadventures.co.uk/blog/20 … r-browser/

Now, maybe in your vision there are certain things that could be made easier for an author to do, but the fact is, the platform is there and I think it’s the best base for the kinds of functionality you’re describing.

And it’s all open source: quest.codeplex.com/

Got to be easier than building something very similar from scratch?

Is it the interface, though, or the fact that it’s text-based games period? While the command prompt in text adventures was a “state of the art” awhile back, so were text adventures. On the other hand, is the interface really the stumbling block? Is there evidence that people would like the text adventure experience if it just didn’t have that command prompt thing?

I’m not asking this in a leading way. I still have a soft spot for text adventures; probably always will. But I wonder if removing the command prompt is really the problem. I see the command prompt as potentially allowing people to be very expressive – but, of course, that means the system has to recognize a good amount of commands (including variations). There’s a good book called “Build Awesome Command-Line Applications in Ruby” and while it has nothing to do with text adventures it was an interesting look at how to make command prompts more friendly and more amenable to people of varying abilities or tolerances.

I don’t know. I guess I just see the fact that text adventures are text adventures as being a bigger stumbling block than the fact that they happen to use a command prompt. I actually see the command prompt as the one thing that makes them unique. Take that away and I basically have a hypertexted set of pages or a choose your own adventure. Even all the gloss of a surrounding interface may not make all that much difference. Or would it? I don’t know? Maybe a good developer competition in the IF community would be to build a better mousetrap – or at least the start of one. Developers are already doing this, I realize. But use the “competition” as a way to also have design sessions and collaboration.

Just thinking off the cuff here. Great topic and interesting posts on your blog.

Also: Quest looks really interesting. I’ll definitely be taking a look.

This is a valid question, certainly! I don’t have an answer. I think the only way to know for sure is to create some games that are text-based but have a different user command input structure, and then see how people react to them.

At a base level, yes, you could end up with a CYOA. If that’s what you want to write, you can do it already with Undum, straight out of the box. (Or straight off of the download, I suppose.) But I’m pretty sure there’s a large gray area between CYOA and puzzly command-line games, and we don’t yet know what may be hiding in that gray area. (Grues? You never know.)

For those who worry that providing a pop-up menu of commands would remove anything resembling a puzzle, it would be perfectly feasible (given the right development tools) to generate pop-up menus of commands with a text input field at the bottom of the menu. If ‘pick up’, ‘look at’, ‘put in’, ‘open’, ‘close’, and ‘drop’ don’t provide the action you want, just type ‘eat’ or ‘knock on’. The player would still have, I think, a feeling of agency. And I’m sure there are other kinds of UI that could accomplish something similar.

No problem. Thanks for the reminder! Your object-sensitive pop-up of commands (upcoming in Beta 3, but not yet released?) is exactly the kind of thing I’m envisioning. Of course, I’m also thinking about restyling the visual – the blue underlined links are kinda not too attractive. But I wouldn’t be surprised to learn that Quest can do all that too, with something as simple as a style tag.

Clearly, I need to look more closely at Quest. I think what has put me off, up to now, is that the Quest games I’ve looked at (and I am explicitly excluding from this comment the Quest game currently entered in the Spring Thing, because I’m not allowed to comment on that!) have mostly tended to lack a certain polish.

Since I’ve proposed to Ben Cressey that we retool the opening of “Mrs. Pepper’s Nasty Secret” using the TADS 3 WebUI framework, perhaps I ought to give myself the same challenge with respect to porting it to Quest – do the port of that same game opening and then try customizing the UI.

That blog post is from last year - Quest 5.0 was released after I wrote that (latest version is 5.2 Beta).

Yep, the link colour is an option, and when embedding HTML you could include some CSS to change the “cmdlink” style (I think, off the top of my head)

Certainly I’d agree with that. It’s a chicken and egg problem really, Quest 5 is a pretty new system so there’s not a great deal of released games that really showcase its features - and most of the games on the site are written for Quest 4 and earlier (which was really an entirely separate system).

Sounds great - let me know if I can help with anything.

Probably not at the moment … unless there’s a downloadable version of the manual that I don’t know about.

I’ve just installed Quest 5.1 on my old WinXP laptop, a machine that lacks wireless connectivity. I’d kind of like to be able to sit in my easy chair and experiment with Quest, but to do that I have to have the Ethernet cable plugged in so I can read the wiki, and that’s a bit awkward. Is there a downloadable manual? I’m not seeing it on the main page of the wiki.

Converting a wiki to a downloadable document was one of those things I thought would be an easily solved problem by now, but apparently not - everything I’ve seen involves having to install umpteen plugins and various other bits of software requiring root access on the web server. Bleugh.

The wiki is powered by MediaWiki which fortunately provides a pretty good “printable version” of a page, and in response the same question a few weeks ago I went through and manually made a PDF version of the tutorial.

I’ve just uploaded it here: quest5.net/tutorial.pdf

It would be nice to have a way of converting the entire wiki including the reference pages. Should be simple enough to spider them, getting the printable versions and then amalgamate them all. So if you need more pages let me know and maybe I’ll see what I can come up with.

I found this:

rdouglasjohnson.com/auntsandbutlers/

It looks like someone at one point had tried going a JavaScript only route. Maybe everyone but me knew of that “versificator” engine but it was new to me.

I might as well post a screenshot of a game created through a ‘graphical’ interactive fiction system I’m making in Python. It seems to match up to your design goals quite well. My plan is to release an editor and a game made with said editor in a span of a year. I’ll try and elaborate on this tommorow…it is getting kind of late and I need to get some shut-eye. ^^

I don’t think the Aunts and Butlers engine has ever been ‘released’. The author has used it for at least a couple of games though as I recall.

Well, it’s the nature of Javascript that the whole thing is client-side. If you load the game into your browser, you can save the whole thing as code and study it. If he’s using any 3rd-party libraries (which I don’t recall) you can enter the script links into your browser and do the same thing, or download them directly from the source of the source.

It’s released. Quoth the .js file:

 *   The VERSIFICATOR text adventure engine for Javascript
 *   by Robin Johnson, www.versificator.co.uk
 *   version 0.4, October 2006
 *
 *   (c) 2003-06 Robin Johnson, rj@robinjohnson.f9.co.uk
 *
 *   "Share my technology. Hands off my art."
 *
 *   Copy and distribute freely, preserving this license.
 *

http://rdouglasjohnson.com/auntsandbutlers/versif04.js

Conrad.

ps - the whole thing, for prosperity:

[rant][code]
/*
*

  • The VERSIFICATOR text adventure engine for Javascript
  • by Robin Johnson, www.versificator.co.uk
  • version 0.4, October 2006
  • © 2003-06 Robin Johnson, rj@robinjohnson.f9.co.uk
  • “Share my technology. Hands off my art.”
  • Copy and distribute freely, preserving this license.
  • You may distribute modified copies of this file, but please
  • make it clear that you have done so, with script comments.
  • This does NOT apply to the game data files, which may be
  • distributed in unmodified form only.

*/

/*

  • Game state is kept in two ways: in an encoded string, for writing to cookies
  • and UNDO history; and in a hash, for reading.
  • When the state is changed, the string and hash are both changed; when the
  • state is read - which is much more common - only the hash is read.
  • The hash is synchronised with the string after an UNDO or RESTORE.
    */
    Game_state = ‘’;
    StateHash = new Object;

last_cmd = ‘’;

Undo_states = new Array();
Undo_states.length = 10; // number of UNDO states to keep in memory
for(var i=0; i < Undo_states.length; ++i)
Undo_states[i] = ‘’;

function push_undo_state(new_state)
{
for(var i = Undo_states.length - 1; i > 0; --i)
Undo_states[i] = Undo_states[i - 1];

Undo_states[0] = new_state;

}
function pop_undo_state()
{
var old_state = Undo_states[0];
for(var i = 0; i < Undo_states.length - 1; ++i)
Undo_states[i] = Undo_states[i + 1];
Undo_states[Undo_states.length - 1] = ‘’;

return old_state;

}

var CommandHistory = new Array();
CommandHistory.length = 10;
for(var i=0; i < CommandHistory.length; ++i)
CommandHistory[i] = ‘’;
var CommandHistoryPointer = 1; // 0 is always empty
function push_command_history(new_command)
{
if(new_command != ‘’)
{
for(var i = CommandHistory.length - 1; i > 1; --i)
CommandHistory[i] = CommandHistory[i - 1];

	CommandHistory[1] = new_command;
}

}

function upKey()
{
if(CommandHistoryPointer < (CommandHistory.length - 1))
{
if(CommandHistory[CommandHistoryPointer + 1] != ‘’)
++CommandHistoryPointer;
}

document.getElementById('textIn').value = CommandHistory[CommandHistoryPointer];

}
function downKey()
{
if(CommandHistoryPointer > 0)
–CommandHistoryPointer;

document.getElementById('textIn').value = CommandHistory[CommandHistoryPointer];

}

TRANSCRIPT = ‘’;

var WINNER = false;

ALPHANUMERICS = ‘abcdefghijklmnopqrstuvwxyz1234567890’;

DEBUG = false;

// I really need an overhaul of the parser and action catcher

// pronouns
it = 0;
him = 0;

// set to cause of death, upon death
// (in some nested function calls, death must be checked for
// again ‘outside the well’)
DEATHSTRING = ‘’;

MAX_TOKENS = 8;
Token = new Object;
for(var i=1; i <= MAX_TOKENS; ++i)
Token[i]=’’;
Token_str = ‘’;

// obey a command, after it has been tokenised
function obey()
{
IS_METACOMMAND = false;

// set undo state, except for certain meta-commands
if(gs('gameover')!=1 && !(Token[1]=='load'||Token[1]=='delete'||
     Token[1]=='dir'||Token[1]=='restart'||Token[1]=='help'||Token[1]=='undo'||Token[1]=='transcript'))
{
	push_undo_state(Game_state);
	update_status();
}

if(Token[1]=='transcript')
{
	IS_METACOMMAND = true;
	show_transcript();
}

else if(Token_str=='restart.game')
{
	IS_METACOMMAND = true;
	start();
}

else if(Token[1]=='load')
{
	IS_METACOMMAND = true;
	load(Token[2]);
}
	
else if(Token[1]=='delete')
{
	IS_METACOMMAND = true;
	delete_cookie(Token[2]);
}
	
else if(Token[1]=='dir')
{
	IS_METACOMMAND = true;
	list_cookies()
}

else if(Token[1]=='restart')
{
	IS_METACOMMAND = true;
	say('Type RESTART GAME to begin a new game.');
}

else if(Token[1]=='help')
{
	IS_METACOMMAND = true;
	game_help();
}

else if(Token[1]=='undo')
{
	var Undo_state = pop_undo_state();
	IS_METACOMMAND = true;
	if(Undo_state=='')
		say('Can\'t undo, sorry.')
	else
	{
		Game_state = Undo_state;
		setGameHash()
		say('Undone.');
		update_status();
	}
}

else if(gs('gameover')==1)
{
	say('Your game is over.\n\

RESTART GAME, LOAD, or UNDO might be good commands to try now.’);
}

else if(Token_str=='')
{
	if(last_cmd!='')
		say('Sorry, I didn\'t understand that.');
	else
		say(PARDON);
	return;
}

else if(Token[1]=='save')
{
	IS_METACOMMAND = true;
	save(Token[2]);
}

else if(Token[1]=='score')
{
	IS_METACOMMAND = true;
	say('You have completed ' + sc_percent() + '% of this adventure.' +
	  score_rating());
}

else if(Token[1]=='verbose')
{
	IS_METACOMMAND = true;
	
	sgs('VRBS', 1);
	
	say('Maximum verbosity on.');
}
else if(Token[1]=='terse')
{
	IS_METACOMMAND = true;
	
	sgs('VRBS', 0);
	say('Verbosity off.');
}

else if(Token[1]=='wait')
{
	IS_METACOMMAND = false;
	say('Time passes...');
}

// special things that happen in the presence of certain characters, if they're awake
else if(is_personname(Token[2]) && personloc(eval(Token[2]))==heroloc() && eval(Token[2]).reactions.indexOf(Token[1]+'::')!=-1)
{
	if(asleep(eval(Token[2])) && Token[1]!='fight')
	{
		say(capitalise(the_person(eval(Token[2]))) + ' is fast asleep.');
	}
	else
	{
		var reactions = eval(Token[2]).reactions.split('/');
		for(var i=0;i<reactions.length;++i)
			if(reactions[i].split('::')[0]==Token[1])
			{
				say(reactions[i].split('::')[1]);
				break;
			}
	}
	
}

// special things that happen in certain places OR
// things that happen in the presence of characters, but aren't actions done TO
// the character
else if(special())
	{} // ok

else if(Token_str=='look')
	look()

else if(Token[1]=='look')
	look_at_tkn(Token[2])

// 'get all', 'drop all'
else if(Token[1]=='take' && Token[2]=='all')
{
	var thingsInAll = false;
	for(var i=1; i<=NUM_THNG; ++i)
		if(thingloc(Thing[i])==heroloc())
		{
			if(thingsInAll) say('\n');
			thingsInAll = true;
			take_command('take ' + Thing[i].name, true);
		}
	
	if(!thingsInAll) say('There\'s nothing here that you can obviously take.');
}
else if(Token[1]=='drop' && Token[2]=='all')
{
	var thingsInAll = false;
	for(var i=1; i<=NUM_THNG; ++i)
		if(in_inv(Thing[i]))
		{
			if(thingsInAll) say('\n');
			thingsInAll = true;
			take_command('drop ' + Thing[i].name, true);
		}
	if(!thingsInAll) say('You\'re not carrying anything.');
}

else if(Token[1]=='drop' &&
	!(
		is_thingname(Token[2]) &&
	 	('/' + eval(Token[2]).uses).indexOf('/drop::')!=-1 &&
	 	in_inv(eval(Token[2]))
	)
)
{
	if(is_thingname(Token[2]))
		drop(eval(Token[2]))
	else if(Token[2])
		say('You\'re not carrying that!')
	else
		say('I\'m not sure what you want to drop.');
}

else if(Token[1]=='take' &&
  !(is_thingname(Token[2]) && ('/' + eval(Token[2]).uses).indexOf('/take::')!=-1)
)
{
	if(is_thingname(Token[2]))
		take(eval(Token[2]))
	else if(Token[2])
	{
		say('Sorry, you can\'t get that.')
	}
	else
		say('I\'m not sure what you want to pick up.');
}

else if(is_thingname(Token[2]) && eval(Token[2]).uses!='')
{
	if(in_inv(eval(Token[2])) || thingloc(eval(Token[2]))==heroloc())
	{
		var uses = eval(Token[2]).uses.split('/');
		for(var i=0;i<uses.length;++i)
		{
			if(uses[i].split('::')[0]==Token[1])
			{
				say(uses[i].split('::')[1]);
				return;
			}

		}
		default_use(Token[1],eval(Token[2]));
		return;
	}
	else if(Token[1]=='take')
		say('You can\'t see that here.');

// say(‘You can’t see the ’ + Token[2] + ’ here.’)
else
say('You haven’t got the ’ + Token[2] + ‘.’);
}

else if(Token[1]=='inventory'||Token_str=='look.inventory')
	list_inv()

else if(is_personname(Token[2]) && heroloc()==personloc(eval(Token[2])) && asleep(eval(Token[2])) && Token[1]!='wake')
{
	say(capitalise(the_person(eval(Token[2]))) + ' is fast asleep.');
}

// semi-mimic Infocom's "marvin, give me the hammer" syntax
// by interpreting this as talking to the NPC
else if(is_personname(Token[1]))
{
	var person = eval(Token[1]);
	if(personloc(person)!=heroloc())
		person_isnt_here(person)
	else if(!Token[2])
		say('I don\'t understand what you want to do to ' +
			  the_person(person) + '.')
	else
	{
		for (var i = MAX_TOKENS; i > 1; --i)
			Token[i] = Token[i - 1];
		Token[1] = 'talk';
		talk_to(eval(Token[2]));
		
		Token_str = '';
		for(var i=1; i<=MAX_TOKENS; ++i)
		{
			Token_str += Token[i];
			if(i < MAX_TOKENS)	Token_str += '.';
		}
	}
}

else if(Token[1]=='talk' && is_personname(Token[2]))
		talk_to(eval(Token[2]))
else if(Token[1]=='talk' && Token[2]=='self')
{
		say('You talk to yourself for a little while, but the \

conversation soon peters out.’);
}

// give present to someone
else if(Token[1]=='give')
{
	if(is_personname(Token[2])&&is_thingname(Token[3]))
		present(eval(Token[2]),eval(Token[3]))
	else if(is_thingname(Token[2])&&is_personname(Token[3]))
		present(eval(Token[3]),eval(Token[2]))
	else
		say('I\'m not sure what you want to show to whom.');
}

else if(is_personname(Token[2]))
{
	var person = eval(Token[2]);
	if(personloc(person)!=heroloc())
		person_isnt_here(person)
	else
	{
		if(default_react(Token[1],person))
			{} // all well and good
		else
			say('You can\'t do that to ' + the_person(person) + '.');
	}
}

else if(is_thingname(Token[2]))
{
	if(in_inv(eval(Token[2])) || thingloc(eval(Token[2]))==heroloc())
	{
		default_use(Token[1],eval(Token[2]));
		return;
	}
		
	else
		say('You haven\'t got the ' + Token[2] + '.');
}

else if(Token_str=='open.door')
	say('No need for that, just tell me what directions you want to move in.')

else if(('.'+DIRECTIONS+'.').indexOf('.'+Token[1]+'.')!=-1)
{
	move();
}

// all 'failure' cases come at the end

else if(Token[1]=='fight')
{
	if(Token[2])
		say('Why? What has it ever done to you?')
	else
		say('I\'m not sure what you want to attack.')
}

else if(Token[1]=='talk')
	say('No one takes any notice.')

else
	say('Sorry, you can\'t do that.');

}

// called when you attempt to foo something that isn’t SPECIALLY fooed
function default_use(verb,th)
{
if(Token[3] && verb==‘wear’) // things like “put hat on signpost” fail
{
say(‘Sorry, you can’t do that.’);
}
else if(verb==‘wear’)
wear(th);
else if(verb==‘remove’)
unwear(th);
else if(verb==‘give’)
{
if(is_personname(Token[3]))
present(eval(Token[3]),eval(Token[2]))
else
say(‘I’m not sure what you want to show to whom.’);
}
else if(verb==‘eat’)
say(‘I don’t think the ’ + th.name + ’ would be very tasty.’);
else if(verb==‘talk’)
say('The ’ + th.name + ’ ’ + pldo(th) + ‘n’t seem to be very talkative.’);
else if(verb==‘smell’)
say('The ’ + th.name + ’ ’ + pldo(th) + ‘n’t smell very interesting.’);
else if(verb==‘kiss’)
say(‘I don’t think you and the ’ + th.name + ’ are close enough for that.’);
else if(verb==‘fight’)
say('You have no animosity towards the ’ + th.name + ‘.’);
else if(verb==‘wave’)
say(‘Waving the ’ + th.name + ’ about has no useful effect.’);
else
say('You can’t do that with the ’ + th.name + ‘.’);
}

function talk_to(person)
{
if(personloc(person)==heroloc())
{
// talk to Person

	var say_default = true;
	
	// talk about something in particular...
	for(var j=3; j<=MAX_TOKENS; ++j)
	{
		if(person.subjects.indexOf(Token[j] + '::')!=-1)
		{
			for(var i=0;i<person.subjects.split('/').length;++i)
				if(person.subjects.split('/')[i].split('::')[0]==Token[j])
				{
					say(person.subjects.split('/')[i].split('::')[1]);
					say_default = false;
				}
			
			if(!say_default) break;
		}
	}
	
	// ...or say one of this Person's default sayings
	if(say_default)
		say(person.talks.split('/')[pick(person.talks.split('/').length)]);
}
else
	person_isnt_here(person);

}

function present(person,thing)
{
if(worn(thing))
{
say(‘You’ll have to take the ’ + thing.name + ’ off first.’);
return;
}

if(personloc(person)!=heroloc())
{
	say('You can\'t see ' + the_person(person) + ' here.');
	return;
}
if(asleep(person))
{
	say(capitalise(the_person(person)) + ' is fast asleep.');
	return;
}
if(!in_inv(thing))
{
	say('You\'re not carrying the ' + thing.name + '.');
	return;
}

var presents = person.presents.split('/');
for(var i=0;i<presents.length;++i)
{
	if(presents[i].split('::')[0]==thing.name)
	{
		say(presents[i].split('::')[1]);
		return;
	}
}

// try talking to the NPC about the thing instead
if(('/' + person.subjects).indexOf(thing.name + '::')!=-1)
{
	for(var i=0;i<person.subjects.split('/').length;++i)
		if(person.subjects.split('/')[i].split('::')[0]==thing.name)
		{
			say(person.subjects.split('/')[i].split('::')[1]);
			say_default = false;
		}

	return;
}

say(capitalise(the_person(person)) +
  ' takes no notice of the ' + thing.name + '.');

}

function special()
{
if(heroloc().special!=’’)
{
var specials = heroloc().special.split(’/’);

	for(var i=0;i<specials.length;++i)
	{	
		if((Token_str+'.').indexOf((specials[i].split('::')[0])+'.')==0 &&
		  // horrid fudge to stop 'in' catching 'inventory'
		  (Token[1]!='inventory' && specials[i].split('::')[0].indexOf('inventory')!=0))
		{
			say(specials[i].split('::')[1]);
			return(true);
		}
	}
}

for(var p=1; p<=NUM_CHRS; ++p) if(Person[p].special && personloc(Person[p]) == heroloc())
{
	var specials = Person[p].special.split('/');
	for(var i=0;i<specials.length;++i)
	{	
		if((Token_str+'.').indexOf((specials[i].split('::')[0])+'.')==0 &&
		  (Token[1]!='inventory' && specials[i].split('::')[0].indexOf('inventory')!=0))
		{
			say(specials[i].split('::')[1]);
			return(true);
		}
	}
}

if(anywhere_special())
	return(true);

return(false);

}

function sightsee(sight)
{
var sights = heroloc().sights.split(’/’);
for(var i=0;i<sights.length;++i)
{
if((’.’ + sights[i].split(’::’)[0] + ‘.’).indexOf(sight)>0)
{
it = sight;
say(sights[i].split(’::’)[1]);
return;
}
}
for(var p=1; p<=NUM_CHRS; ++p) if(Person[p].sights && personloc(Person[p])==heroloc())
{
var sights = Person[p].sights.split(’/’);
for(var i=0;i<sights.length;++i)
{
if(sights[i].split(’::’)[0]==sight)
{
it = sight;
say(sights[i].split(’::’)[1]);
return;
}
}
}

say('You can\'t see that here.');

}

function thingsee(th)
{
if(!in_inv(th) && thingloc(th)!=heroloc())
say(‘You can’t see that here.’);
// say(‘You can’t see the ’ + th.name + ’ here.’);
else
say(th.description);
}

function personsee(person)
{
if(personloc(person)==heroloc())
say(person.description)
else
person_isnt_here(person);
}

function person_isnt_here(person)
{
say(‘You can’t see ’ + the_person(person) + ’ here.’);
}

function set_personloc(person,place)
{
sgs(‘cl_’ + person.id, place.id);
}

function personloc(person)
{
return Place[gs(‘cl_’ + person.id)];
}

function is_personname(name)
{
for(var i=1;i<=NUM_CHRS;++i)
if(Person[i].name==name)
return(true);

// failed to find it
return(false);

}

function the_person(person)
{
return ((person.pname ? ‘’ : 'the ') + person.fullname);
}

// only rudimentarily implemented -
// in particular, it’ll give odd results if there is an alternative,
// indirect route from fromPlace to toPlace.
// toPlace is assumed to be heroloc()
function person_follow(person, fromPlace, toPlace)
{
if(personloc(person)==fromPlace)
{
set_personloc(person, toPlace);
say(’\n’ + capitalise(the_person(person)) + ’ follows you.’);
}
}

function set_thingloc(thing,place)
{
sgs(‘tl_’ + thing.id, place.id);
}

function thingloc(thing)
{
return Place[gs(‘tl_’ + thing.id)];
}

function is_thingname(name)
{
for(var i=1;i<=NUM_THNG;++i)
if(Thing[i].name==name)
return(true);

// failed to find it
return(false);

}

function say(txt)
{
if(!txt) return;

var SCROLL_INC = 300;

if(txt.charAt(0)=='*')
{
	eval(txt.substring(1,txt.length));
	return;
}

if(txt.charAt(0)=='=')
	txt = '\"' + txt.substring(1,txt.length) + '\"';

// allow nested expressions in all say() strings
if(txt.indexOf('[[') != -1)
{
	var openC = txt.indexOf('[[') ;
	var closeC = txt.indexOf(']]') ;
	say(txt.substring(0, openC)) ;
	
	var evalStr = txt.substring(2 + openC, closeC);
	
	say( eval(evalStr) );
	
	say(txt.substring(2 + closeC, txt.length));

	return;
}

txt = txt.split('\n').join('<br/>');

var HTMLOut = document.getElementById('outDiv').innerHTML;

// HTMLOut = HTMLOut.replace(‘xOutEndx’, ‘xx’);

HTMLOut += txt;		// + '<a name="#xOutEndx"> </a>';

TRANSCRIPT += txt;

var MAX_LENGTH = 2200;
  
if(HTMLOut.length > MAX_LENGTH)
	HTMLOut = HTMLOut.substring(HTMLOut.length - MAX_LENGTH, HTMLOut.length) ;
  
document.getElementById('outDiv').innerHTML = HTMLOut;

document.getElementById('outDiv').scrollTop += SCROLL_INC;

document.getElementById('textIn').focus();

}

function show_transcript()
{
document.getElementById(‘outDiv’).innerHTML = TRANSCRIPT;
}

function list_exits()
{
// quick and ugly fix
if(!heroloc())
set_heroloc(START_LOC);

if(!heroloc().exits) // no exits
	return('\nThere are no exits.');

var exits = heroloc().exits.split('.');
for(var i=0;i<exits.length;++i)
	exits[i] = exits[i].substring(0,exits[i].indexOf('::'));

if(exits.length==1) // one exit
	return('\nAn exit leads ' + exits[0] + '.');

var exlist = '\nExits are ';
for(var i=0;i<exits.length-1;++i)
	exlist += exits[i] + ', '

exlist = exlist.substring(0,exlist.lastIndexOf(',')) +
  ' and ' + exits[exits.length-1] + '.';

return(exlist);

}

function list_persons()
{
var person_list = ‘’;

for(var i=1;i<=NUM_CHRS;++i)
{
	if(personloc(Person[i])==heroloc())
	{
		person_list += '\n';
		
		var tocap = true;
		
		if(Person[i].ishere != '' && !asleep(Person[i]))
		{
			person_list += Person[i].ishere;
		}
		else 
		{
			if(!Person[i].pname)
			{
				person_list += 'A';
				if('aeiouAEIOU'.indexOf(Person[i].fullname.charAt(0))!=-1)
					person_list += 'n';
				person_list += ' ';
				tocap = false;
			}
		
			person_list += (tocap ? capitalise(Person[i].fullname) : Person[i].fullname) + ' ' +
			  (asleep(Person[i]) ? 'is here, fast asleep.' : 'is here');
		}
	}
}

return(person_list);

}

function list_things()
{
var thing_list = ‘’;

for(var i=1;i<=NUM_THNG;++i)
{
	if(thingloc(Thing[i])==heroloc())
	{
		if(thing_list!='')
			thing_list += ', ';
		thing_list += Thing[i].fullname;
	}
}

if(thing_list != '')
	thing_list = 'You can also see ' + thing_list + '.';

if(thing_list.indexOf(',')!=-1)
	thing_list = thing_list.substring(0,thing_list.lastIndexOf(',')) +
	  ', and' + thing_list.substring(thing_list.lastIndexOf(',')+1,
	  thing_list.length);

if(thing_list!='')
	thing_list = '\n' + thing_list;
return(thing_list);

}

function take(th)
{
if(in_inv(th))
{
say('You are already carrying the ’ + th.name + ‘.’);
return;
}

if(thingloc(th)!=heroloc())
{
	say('You can\'t see that here.');

// say(‘You can’t see the ’ + th.name + ’ here.’);
return;
}

set_thingloc(th,nowhere);
give_hero(th);
say('Okay. You have taken the ' + th.name + '.');

}

function give_hero(th)
{
set_thingloc(th,nowhere);
sgs(‘I_’ + th.id, 1);
return(true);
}

function drop(th)
{
if(worn(th))
{
say('You can’t drop the ’ + th.name + ’ - you’re wearing ’ +
(th.plural ? ‘them!’ : ‘it!’));
return(false);
}

if(!in_inv(th))
{
	say('You are not carrying the ' + th.name + '.');
	return(false);
}

if(Token[3]=='in' && Token[4]) // 'put X in Y', not implemented
{
	say('That would be pointless.');
	return false;
}

sgs('I_' + th.id, 0);
set_thingloc(th,heroloc());
say('Okay. You have dropped the ' + th.name + '.');
return(true);

}

function in_inv(th)
{
if(gs(‘I_’ + th.id)==1)
return true
else
return false;
}

function take_away(th)
{
// stop thing being worn, quietly
sgs(‘wrn_’ + th.id, 0);

set_thingloc(th,nowhere);

sgs('I_' + th.id, 0);

return(false);

}

function list_inv()
{
var inv_list = ‘’;

for(var i=1;i<=NUM_THNG;++i)
	if(in_inv(Thing[i]))
	{
		if(inv_list!='')
			inv_list += ', ';
		inv_list += Thing[i].fullname;
		if(worn(Thing[i]))
			inv_list += ' (which you are wearing)';
	}

if(inv_list=='')
{
	say('You are not carrying anything.');
	return;
}

if(inv_list.indexOf(',')!=-1)
	inv_list = inv_list.substring(0,inv_list.lastIndexOf(',')) +
	  ', and' + inv_list.substring(inv_list.lastIndexOf(',')+1,
	  inv_list.length);

say('You are carrying ' + inv_list + '.');

}

function move()
{
if(Token[1]==’’)
{
// if((’.’ + DIRECTIONS + ‘.’).indexOf(’.’ + Token[2] + ‘.’)==-1)
say(‘Which way?’)
// else
// {
// Token[1] = Token[2];
// obey();
// }

	return;
}

if(!heroloc().exits)
{
	say('There are no exits from here.');
	return;
}

var moved = false;
if(heroloc().exits!='')
{
	var exits = heroloc().exits.split('.')
	for(var i=0;i<exits.length;++i)
		if(exits[i].split('::')[0]==Token[1])
		{

// say(’\nToken[1] : ’ + Token[1]);
// say(’\nexits[i].split(’::’)[1] : ’ + exits[i].split(’::’)[1] + ‘\n’);
set_heroloc(eval(exits[i].split(’::’)[1]));
moved = true;
}
}
if(heroloc().hExits!=’’ && !moved)
{
var hExits = heroloc().hExits.split(’.’)
for(var i=0; i<hExits.length; ++i)
{
if(hExits[i].split(’::’)[0]==Token[1])
{
Token[1] = hExits[i].split(’::’)[1];
move();
return;
}
}
}
if(!moved)
say(‘You can’t see a way ’ + Token[1] +
(Token[1]==‘out’ ? ‘wards’ : ‘’) + // “can’t see a way out” is confusing
’ from here.’)
else
look();
}

function verbose()
{
return (gs(‘VRBS’) == 1);
}
function visited(pl)
{
return ( gs(‘V’ + pl.id) == 1 );
}
function setVisited(pl)
{
sgs(‘V’ + pl.id, 1);
}

function look()
{

// some games may have mazes. These are defined by a place name (internal name)
// that begins with 'maze'. In mazes, terse/verbose has no effect.
// This is a horrible piece of coding, but hey, I'm not getting paid for this.

var showDesc = ( verbose() || Token[1] == 'look' || !visited(heroloc()) || 
heroloc().name.indexOf('maze')==0);

say('<font color=\"' + HIGHLIGHT_COLOUR + '\"><b>' + heroloc().fullname + '</b></font>');

if(showDesc) { say('\n' + heroloc().description ) } ;

say(list_persons());
say(list_things());
say(heroloc().append); // always gets said; if you don't want this use [[eval nesting]]
					   // in the place description.
say(anywhere_append());
if(!winner)
	say(list_exits());

setVisited(heroloc());

update_status();

if(gs('gameover')==1)
	die(DEATHSTRING);

}

function look_at_tkn(tkn)
{
if(tkn==‘out’)
say(list_exits().substring(1,list_exits().length));

else if(is_personname(tkn)) // look at character
	personsee(eval(tkn))

else if(is_sighthere(tkn)) // look at sight
	sightsee(tkn)

// look at something that you can see anywhere
else if(anywhere_sights.indexOf('/' + tkn + '::')!=-1)
{
	var sights = anywhere_sights.split('/')
	for(var i=0;i<sights.length;++i)
		if(sights[i].split('::')[0]==tkn)
		{
			say(sights[i].split('::')[1]);
			break;
		}
}

else if(is_thingname(tkn))
	thingsee(eval(tkn));

else if(tkn!='')
	say('Nothing special.');
	
else
	look();

}

function update_status()
{
var txt = heroloc().fullname;
if(gs(‘gameover’)==1)
{
if(winner)
txt = ‘GAME COMPLETE!’
else
txt = ‘GAME OVER’;
}
/* var n = 48 - txt.length;
for(var i=0;i<n;++i)
txt += ’ ';
var sc = sc_percent();
txt += 'SCORE: ’ + ((sc < 100) ? ’ ’ : ‘’) + ((sc < 10) ? ’ ’ : ‘’) +
sc_percent() + ‘%’;
*/
document.getElementById(‘placeLabel’).innerHTML = (gs(‘gameover’)==1) ?
‘Game over’ : heroloc().fullname ;

document.getElementById('scoreLabel').innerHTML = sc_percent() + '%' ;

}

function sc_percent()
{
var sc = parseInt(gs(‘sc’));

if(sc==MAX_SCORE-1)
	return(99)
	// (a) so that the player will know there is only one more thing to do;
	// (b) so it won't get rounded to 100.

else
	return(Math.round((sc/MAX_SCORE)*100));

}

function heroloc()
{
return(Place[gs(‘hl’)]);
}

function set_heroloc(loc)
{
sgs(‘hl’,loc.id);
update_status();
}

function is_sighthere(sight)
{
if ((’/’ + heroloc().sights).indexOf(’/’+sight+’::’)!=-1) {it = ‘’ + sight; return true;}
for(var p=1;p<=NUM_CHRS;++p)
if(Person[p].sights && personloc(Person[p])==heroloc() &&
(’/’ + Person[p].sights).indexOf(’/’+sight+’::’)!=-1)
{it = ‘’ + sight; return true;}

// don't change "it" after all

}

function inc_score()
{
sgs(‘sc’,parseInt(gs(‘sc’))+1);
update_status();
}

// Initialise game

function start()
{
winner = false;

sgs('gameover',0);
sgs('sc',0);

Game_state = '';
setGameHash();
document.getElementById('outDiv').innerHTML = INTRO.split('\n').join('<br/>');
TRANSCRIPT = INTRO.split('\n').join('<br/>');

document.getElementById('textIn').value = '';
document.getElementById('textIn').focus();

set_gameflags();

for(var i=1;i<=NUM_CHRS;++i)
	set_personloc(Person[i],Person[i].firstplace);

set_heroloc(START_LOC);

for(var i=1;i<=NUM_THNG;++i)
{
	set_thingloc(Thing[i],Thing[i].firstplace);
	sgs('I_' + i, 0);
}

look();
initialiseGame();

}

function die(msg, youHaveDied)
{
// alert('die: ’ + msg + ‘\n\n’ + youHaveDied + ‘\n\n’ + gs(‘gameover’));

if(gs('gameover')!=1)
{
	DEATHSTRING = msg;
	sgs('gameover',1);
	say(msg +
  '\n\n*** ' + (youHaveDied ? youHaveDied : '[[winner && !(youHaveDied) ? "You completed the game!" : "You have died"]]') + ' ***\n' +
  'You completed ' + sc_percent() + '% of this adventure.' + score_rating()
	  );
	update_status();
}

}

/*
*

  • tokeniser and pseudo-NLP
  • (actually all it does is skip unrecognised words sometimes,
  • and merge similar words together sometimes -
  • this can make it seem to understand surprisingly clever
  • sentences, but it’s just as much at home with
  • ADVENT style pseudo-English)

*/

var IS_METACOMMAND = false;

function take_command(command, suppressEcho)
{
if(!suppressEcho)
say(’\n\n> ’ + command.replace(’\n’, ‘’) + ‘\n’);

if(!suppressEcho)
{
	push_command_history(command.replace('\n', ''));
	CommandHistoryPointer = 0;
}

/*

  • This can be rather slow, and in extreme cases might cause nastiness

  • with the player entering a new command while an old one is being

  • executed. With the lack of a proper verb model, it splits things like

  • SAY “HELLO AUNT. COME IN.” And I’m not convinced anyone really wants

  • it. Something similar will be included in a future release.

  • if(command.indexOf(’.’)!=-1)
    {
    var cArray = command.split(’.’);
    for(var i=0; i<cArray.length; ++i)
    {
    if(gs(‘gameover’)!=1 && cArray[i]!=’’)
    {
    if(i > 0)
    say(’\n\n’);
    take_command(cArray[i], true);
    }
    }
    return;
    }
    */

    // debugging aid - hide in live release
    if(false && command.charAt(0)==’*’)
    {
    eval(command.substring(1,command.length));
    return;
    }

    for(var i=1;i<=MAX_TOKENS;++i)
    Token[i]=’’;

    tokenise(command);
    set_pronouns();
    if(DEBUG)
    alert('Token_str is ’ + Token_str);

// not good, as some unrecognised commands will still be ignored
// with no message
//
// if(Token_str==’’ && last_cmd!=’’)
// say(‘Sorry, I didn’t understand that command.’);
// else
// obey();

IS_METACOMMAND = false;
obey();

//alert(IS_METACOMMAND);

// special things to do
if((!IS_METACOMMAND) && gs('gameover')!=1)
	anywhere_do();

// mannerisms of any characters that happen to be present
for(var i = 1; i <= NUM_CHRS; ++i)
{
	if(gs('gameover')!=1 && Person[i].mannerisms != '' &&
	   personloc(Person[i]) == heroloc() &&
	   pick(MANNER_FREQ) == 0)
	{
		var mannersA = Person[i].mannerisms.split('/');
		say('\n' + mannersA[pick(mannersA.length)]);
	}
}

}

function set_pronouns()
{
for(var i=1;i<=MAX_TOKENS;++i)
if(is_thingname(Token[i]))
{
it = eval(Token[i]);
break;
}
for(var i=1;i<=MAX_TOKENS;++i)
if(is_personname(Token[i]))
{
him = eval(Token[i]);
break;
}
}

function tokenise(command)
{
command = command.toLowerCase();

// change all non-alphanumeric characters to spaces
var newCommand = '';
for(var i=0; i < command.length; ++i)
{
	newCommand += ALPHANUMERICS.indexOf(command.charAt(i))==-1 ? ' ' :
				  command.charAt(i)
}
command = newCommand;

while(ALPHANUMERICS.indexOf(command.charAt(command.length-1))==-1)
	command = command.substring(0,command.length-1);
while(ALPHANUMERICS.indexOf(command.charAt(0))==-1 && command.length>0)
	command = command.substring(1,command.length);
	
last_cmd = command;

var tkn_ptr = 1;
var done=false;
while(!done && tkn_ptr<=MAX_TOKENS)
{

	while(ALPHANUMERICS.indexOf(command.charAt(0))==-1)
	{
		command=command.substring(1,command.length);
	}

	if(command=='')
		done=true;

	var this_token = '';

	if(command.indexOf(' ')==-1)
	{
		this_token = command;
		done = true;
	}
	else
	{
		this_token = command.substring(0,command.indexOf(' '));
		command=command.substring(command.indexOf(' ')+1,command.length);
	}
	
	var sensical = false;
	for(var i=1;i<=NUM_SYNS;++i)
		if(this_token!=''&&Synonyms[i].indexOf('.'+this_token+'.')!=-1)
		{
			sensical = true;
			this_token = Synonyms[i].substring(0,Synonyms[i].indexOf('.'));
		}
	
	// another try for plurals
	if(!sensical && this_token.length > 3 && this_token.charAt(this_token.length-1)=='s')
	{
		this_token = this_token.substring(0,this_token.length-1);
		for(var i=1;i<=NUM_SYNS;++i)
			if(this_token!=''&&Synonyms[i].indexOf('.'+this_token+'.')!=-1)
			{
				sensical = true;
				this_token = Synonyms[i].substring(0,Synonyms[i].indexOf('.'));
			}
	}

	// substitute nouns for pronouns
	if(this_token=='it')
	{
		if(it==0)
			sensical = false
		else if(it.fullname) // not if "it" is a string (i.e. a sight)
		{

			say('([[it.fullname]])\n');

			this_token = it.name;
		}
		else // "it" is the name of some scenery
		{
			say('(the ' + it + ')\n');
			this_token = it;
		}
	}
	else if(this_token=='him')
	{
		if(him==0)
			sensical = false
		else
			this_token = him.name;
	}

	// ignore token if it the same as the last one
	if(tkn_ptr > 1 && this_token==Token[tkn_ptr-1])
		sensical = false;
		
	// special - ignore 'up' after 'pick'...
	if(tkn_ptr > 1 && this_token=='up' && Token[tkn_ptr-1]=='take')
		sensical = false
	// ...'down' after 'put'...
	else if(tkn_ptr > 1 && this_token=='down' && Token[tkn_ptr-1]=='drop')
		sensical = false
	// ... 'up' after 'wake'...
	else if(tkn_ptr > 1 && this_token=='up' && Token[tkn_ptr-1]=='wake')
		sensical = false;
	// ... 'up' after 'fill'...
	else if(tkn_ptr > 1 && this_token=='up' && Token[tkn_ptr-1]=='fill')
		sensical = false;
	// ... 'out' after 'empty'
	else if(tkn_ptr > 1 && this_token=='out' && Token[tkn_ptr-1]=='empty')
		sensical = false;

	// run primary compass directions together into secondary ones
	if(tkn_ptr > 1 && this_token=='east' && Token[tkn_ptr-1]=='north')
	{
		Token[tkn_ptr-1] = 'northeast';
		sensical = false;
	}
	else if(tkn_ptr > 1 && this_token=='west' && Token[tkn_ptr-1]=='north')
	{
		Token[tkn_ptr-1] = 'northwest';
		sensical = false;
	}
	else if(tkn_ptr > 1 && this_token=='east' && Token[tkn_ptr-1]=='south')
	{
		Token[tkn_ptr-1] = 'southeast';
		sensical = false;
	}
	else if(tkn_ptr > 1 && this_token=='west' && Token[tkn_ptr-1]=='south')
	{
		Token[tkn_ptr-1] = 'southwest';
		sensical = false;
	}
	// 'hold on' to 'take'...
	else if(tkn_ptr > 1 && this_token=='on' && Token[tkn_ptr-1]=='wear')
	{
		Token[tkn_ptr-1] = 'take';
		sensical = false;
	}
	// change 'put on' to 'wear'...
	else if(tkn_ptr > 1 && this_token=='on' && Token[tkn_ptr-1]=='drop')
	{
		Token[tkn_ptr-1] = 'wear';
		sensical = false;
	}
	// 'put X on' to 'wear X'
	else if(tkn_ptr > 2 && this_token=='on' && Token[tkn_ptr-2]=='drop')
	{
		Token[tkn_ptr-2] = 'wear';
		sensical = false;
	}
	// ...'take off' to 'remove'...
	else if(tkn_ptr > 1 && this_token=='off' && Token[tkn_ptr-1]=='take')
	{
		Token[tkn_ptr-1] = 'remove';
		sensical = false;
	}
	// 'take X off' to 'remove X'
	else if(tkn_ptr > 2 && this_token=='off' && Token[tkn_ptr-2]=='take')
	{
		Token[tkn_ptr-2] = 'remove';
		sensical = false;
	}
	// ...'look in' to 'open'...
	else if(tkn_ptr > 1 && this_token=='in' && Token[tkn_ptr-1]=='look')
	{
		Token[tkn_ptr-1] = 'open';
		sensical = false;
	}
	// ...'move <direction>' to <direction>
	else if(tkn_ptr > 1 && DIRECTIONS.indexOf(this_token+'.')!=-1 && Token[tkn_ptr-1]=='move')
	{
		Token[tkn_ptr - 1] = this_token;
		sensical = false;
	}

	// special - accept anything as token 2 if token 1 is load or save
	if(tkn_ptr==2 && (Token[1]=='save'||Token[1]=='load'||Token[1]=='delete'))
		sensical = true;

	// ignore certain words, and all words of three letters or less that haven't
	// been recognised already
	// always ignore an unrecognised word if the last word was also unrecognised.
	if(	(!sensical && this_token.length <= 3) ||
		('.' + WORDS_TO_IGNORE + '.').indexOf('.' + this_token + '.')!=-1
	  )
	{
		sensical = false;
	}
	else if(!sensical && tkn_ptr > 1 && Token[tkn_ptr - 1] != 'xxxxx')
	{
		// this token is meaningless, but should be treated as a word IF IT'S LAST.
		this_token = 'xxxxx';

		// due to the Eliza effect, we get a more effective-seeming parser
		// if we DON'T complain about unrecognised words.

// say(’[I don\‘t know the word "’ + this_token + ‘]’);

		sensical = true;
	}

	// ignore last token if it was a 'meaningless' word (Eliza)
	if(sensical && Token[tkn_ptr - 1]=='xxxxx')
		tkn_ptr -= 1;

	if(sensical)
	{
		Token[tkn_ptr++] = this_token;
		if(is_sighthere(this_token))
			it = this_token;
	}


}

Token_str = '';
for(var i=1;i<=MAX_TOKENS;++i)
{
	if(Token[i]!='')
		Token_str += Token[i]+'.';
}

while(Token_str.charAt(Token_str.length-1)=='.')
{
	Token_str=Token_str.substring(0,Token_str.length-1);
}

}

// random number 0 to n-1
function pick(n)
{
return(Math.floor(Math.random()*n));
}

// capitalise ‘text’ to ‘Text’
function capitalise(txt)
{
if(txt==’’)
return(’’)
else
return(txt.charAt(0).toUpperCase() + txt.substring(1,txt.length));
}

// wear and remove wearable things
function wear(th)
{
if(Token[3]) // kludge - flow ends up here after ‘PUT X ON Y’, (if X is wearable) which isn’t implemented
{
say(‘Sorry, you can’t do that.’);
return false;
}

if(!in_inv(th))
{
	say('You are not carrying the ' + th.name + '.');
	return(false);
}

if(!th.wearable)
{
	say('You can\'t wear the ' + th.name + '!')
	return(false);
}

if(worn(th))
{
	say('You are already wearing the ' + th.name + '.');
	return(true);
}

sgs('wrn_' + th.id,1);
say('Okay. You are wearing the ' + th.name + '.');
return(true);

}

function unwear(th)
{
if(!in_inv(th))
{
say('You are not even carrying the ’ + th.name + ‘!’);
return false;
}

if(!(th.wearable))
{
	say('You can\'t wear or remove the ' + th.name + '.');
	return false;
}

if(!worn(th))
{
	say('You are not wearing the ' + th.name + '.');
	return false;
}
	
sgs('wrn_' + th.id,0);
say('Okay. You are no longer wearing the ' + th.name + '.');
return(true);

}

// is th being worn?
function worn(th)
{
return(gs(‘wrn_’ + th.id)==1);
}

// send a person to sleep
function sleep(ch)
{
sgs(‘slp_’ + ch.id, 1);
}

function unsleep(ch)
{
sgs(‘slp_’ + ch.id, 0);
}

function asleep(ch)
{
return(gs(‘slp_’ + ch.id)==1)
}

// first word of a string
function firstword(str)
{
return(str.split(’ ')[0]);
}

/*

  • game state handler
  • Game state is kept in TWO places: a string, Game_state, and a hash, StateHash.
  • When writing, which happens less often than reading, the string and the hash are
  • BOTH changed.
  • When reading, only the string is read.
  • The string is what gets written to saved-game cookies and undo history.
  • After an UNDO or RESTORE, the hash is updated from the string.

*/

function gs(name)
{
// restore from string only
if(StateHash[name])
return StateHash[name]
else
return 0;

// var states = Game_state.split(’.’);
// for(var i=0;i<states.length;++i)
// if(states[i].split(’-’)[0]==name)
// return(states[i].split(’-’)[1]);
//
// return(0);
}

function sgs(name,value)
{
// store in hash and string
if(value==0 || value==‘0’)
delete StateHash[name]
else
StateHash[name] = value;

var states = Game_state.split('.');
for(var i=0;i<states.length;++i)
	if(states[i].split('-')[0]==name)
	{
		states[i]='';
	}

Game_state = states.join('.');

if(value==0)
	return;

if(Game_state.indexOf('..')!=-1)
	Game_state = Game_state.substring(0,Game_state.indexOf('..')) +
	  Game_state.substring(Game_state.indexOf('..')+1,Game_state.length);

Game_state = name + '-' + value + '.' + Game_state;

}

function setGameHash()
{
StateHash = new Object;

// rebuild the state hash from the Game_state string.
// This must be called after a RESTORE or UNDO.
var hashEntries = Game_state.split('.')
for(var i=0; i < hashEntries.length; ++i)
{
	var thisEntry = hashEntries[i].split('-');
	var hashName = thisEntry[0];
	var hashValue = thisEntry[1];
	
	if(!( hashValue == 0 || hashValue == '0' ))
		StateHash[hashName] = hashValue;
}

}

/*
*

  • save and load to cookies

*/

// todo - allow player to list and delete SGM cookies

function save(name)
{
if(name==’’)
{
say(‘Please name your cookie - type SAVE (NAME).’);
return;
}

//alert(';expires=' + new Date( new Date().setYear(1 + new Date().getFullYear()) ).toGMTString());
document.cookie = 'SGM_' + GAME_ID + '_' + name.toUpperCase() + '=' + Game_state + ';expires=' + new Date( new Date().setYear(1 + new Date().getFullYear()) ).toGMTString() + ';' ;
say('Game saved to cookie ' + name.toUpperCase() + '.\n\

Type RESTORE ’ + name.toUpperCase() + ’ to carry on from this point.’ +
(
(num_cookies() > 4) ? ‘\n\nWARNING: On some browsers (including
MS Internet Explorer), odd things
start to happen if you save more than about four cookies, including
loss of all cookies and the ability to save new ones.
You might want to delete some. (Type DIR to see a list of cookies.)’
: ‘’
)
);
}

function num_cookies()
{
if(!document.cookie)
return(0);
var n = 0;
var cookies = (document.cookie.split(’; '));
for(var i=0;i<cookies.length;++i)
{
if(cookies[i].indexOf(‘SGM_’ + GAME_ID)==0)
++n;
}
return(n);
}

function delete_cookie(name)
{
if(name==’’)
{
say(‘Please name your cookie - type DELETE (NAME).\n
(Type DIR to see a list of cookies.)’);
return;
}

if(!document.cookie)
{
	say('No cookies found, sorry.');
	return;
}

var foundit = false;

var cookies = document.cookie.split(';');
for(var i=0;i<cookies.length;++i)
{
	if(cookies[i].indexOf('SGM_' + GAME_ID + '_' + name.toUpperCase() + '=')!=-1)
	{
		foundit = true;
		break;
	}
}

if(!foundit)
{
	say('Cookie ' + name.toUpperCase() + ' not found, sorry.\n\

(Type DIR to see a list of cookies.)’);
}
else
{
rmck(‘SGM_’ + GAME_ID + ‘_’ + name.toUpperCase());
say(‘Cookie ’ + name.toUpperCase() + ’ deleted.’);
}
}

function load(name)
{
if(name==’’)
{
say(‘Please name your cookie - type RESTORE (NAME).\n
(Type DIR to see a list of cookies.)’);
return;
}

if(!ck('SGM_' + GAME_ID + '_' + name.toUpperCase()))
{
	say ('Cookie ' + name.toUpperCase() + ' not found, sorry.\n\

(Type DIR to see a list of cookies.)’);
return;
}

Game_state = ck('SGM_' + GAME_ID + '_' + name.toUpperCase());
setGameHash();
say('Restored game from cookie ' + name.toUpperCase() + '.');
update_status();

}

function list_cookies()
{
var txt = ‘’;

var cookies = document.cookie.split('; ');
for (var i=0;i<cookies.length;++i)
	if(cookies[i].split('=')[0].indexOf('SGM_' + GAME_ID)==0)
		txt += '\n' + cookies[i].split('=')[0].substring(8,cookies[i].split('=')[0].length);

if(txt=='')
	say('No cookies found.')
else
	say('Cookies found:' + txt);

}

/*
*

  • cookie handler
  • cribbed from any JS textbook

*/

function ck(name)
{
var cookies = document.cookie.split(’; ‘);
for(i=0;i<cookies.length;++i)
if(cookies[i].split(’=’)[0]==name)
return(cookies[i].split(’=’)[1]);

return(0);

}

function sck(name,val)
{
document.cookie = name + ‘=’ + val ;
}

function rmck(name)
{
var expires = new Date;
expires.setDate(expires.getDate() - 1);

document.cookie = name + '=;expires=' +
  expires.toGMTString();

}

[/code][/rant]