/*
 *
 *   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.
 *
 *   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('<font color="' + INPUT_COLOUR + '">\n\n&gt; <b>' + command.replace('\n', '') + '</b></font>\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();
}

