// Emacs settings: -*- mode: Fundamental; tab-width: 4; -*-//////////////////////////////////////////////////////////////////////////////                                                                        //// Sokoban in Javascript                                                  ////                                                                        //// Copyright (c) 2009, Andrew Birrell                                     ////                                                                        ////////////////////////////////////////////////////////////////////////////////// Global state//var levelsTxt = "";     // text description of current collectionvar levelsName = "";    // name of current collectionvar levels = null;      // current set of levels, as line arraysvar levelsDone = null;  // levels completed in current collectionvar curLevel = -1;      // index into "levels"var savedlevel = -1;    // level with state saved in a cookievar width = 0;          // total columnsvar height = 0;         // total rowsvar scale = 1;          // pixels per column/rowvar targets = null;     // square numbers of target squaresvar initBoxes = null;   // square numbers of initial box positionsvar initMan = -1;       // square number of initial man positionvar floorState = null;  // what's in each floor squarevar floorWall = "wall";var floorFree = "passage";var floorTarget = "target";var floorWater = "water";var thingState = null;  // what's on floor squares, not including the manvar thingWall = "#";var thingFree = "-";var thingBox = "$";var manSq = -1;         // square number for the man who's doing the workvar selectedBox = -1;   // square with box that we want to movevar moves = 0;var pushes = 0;var neighbourCache = null; // optimizes "findPath" a lot// Configuration options, initialized from the HTML in "init"var speed = 9999;       // animation delay in msec// Constants to optimize the choice of image tilesvar modulus = 5;        // textures repeat after "modulus" rows or columnsvar granularity = 8;    // scale factor is multiple of "granularity"var maxScale = 48;      // max pixels per squarevar minScale = 16;       // min pixels per squarevar blankUrl = "blank.png";//// Graphics//function thingUrl(sq) {	// Return the URL for image in the "things" layer at given square,	// assuming square is passage or target.	return (sq == selectedBox ? "skin/selectedBox.png" :				(thingState[sq] == thingBox ? "skin/box.png" :					blankUrl));}function updateBoxImg(sq) {	// Place image in "things" layer: box, selected box, or blank	document.getElementById("things" + sq).src = thingUrl(sq);}function selectBox(sq) {	// flag this as the box we want to move, or -1 to deselect	var wasSelected = selectedBox;	selectedBox = sq;	if (wasSelected >= 0) updateBoxImg(wasSelected);	if (selectedBox >= 0) updateBoxImg(selectedBox);}function moveBox(a, b) {	// Jump the box at "a" to "b".	if (b == manSq) alert("Bug: moveBox, trampling man");	if (thingState[a] != thingBox) alert("Bug: moveBox, not there");	if (a != b) {		if (thingState[b] != thingFree) alert("Bug: moveBox, occupied");		thingState[a] = thingFree;		thingState[b] = thingBox;		updateBoxImg(a);		updateBoxImg(b);	}}function moveMan(sq) {	// Jump the man to the given square, assumed to be vacant	// Use -1 to make man disappear	if (sq >= 0 && thingState[sq] != thingFree) alert("Bug: bad moveMan");	if (manSq >= 0) {		var elt = document.getElementById("things" + manSq);		elt.src = blankUrl;	}	if (sq >= 0) {		var elt = document.getElementById("things" + sq);		elt.src = "skin/man.png";	}	manSq = sq;}function floorUrl(sq, row, col) {	// Return the URL for given floor square, based on floorState,	// whihc is assumed to not be floorWater	var sqState = floorState[sq];	var parity =		(Math.floor(col / modulus) + Math.floor(row / modulus)) % 2;	return "image.php?img=" + sqState +		"&w=" + scale +		"&h=" + scale +		"&x=" + ((col % modulus) * scale) +		"&y=" + ((row % modulus) * scale) +		"&r=" + (parity == 0 ? 0 : 90);}function highlightUrl(sq, row, col) {	// Return the URL for highlight on given square.	//	// Server-side, this is done by compositing quadrants to create an	// entire square's highlights; otherwise, allowing for inside corners,	// there would be too many different images.  Composites are cached.	//	// Each corner is specified by [0..4], indicating:	//    0 ... both adjacent squares are empty	//    1, 3, or 4 ... square ccw from corner is occupied	//    2, 3, or 4 ... square cw from corner is occupied	// In cases 0, 1 and 2, the diagonally adjacent square is	// unspecified.  In 3 it's empty, producing an inside corner, and in 4	// it's occupied, producing a flat corner.	//	// To minimize the number of distinct images, we always create a	// 128x128 highlight, and let the browser do the scaling (unlike the	// other images, there's no texture scaling to worry about).	//	var tl = false;	var tr = false;	var bl = false;	var br = false;	var above = false;	var left = false;	var below = false;	var right = false;	if (row > 0) {		above = (floorState[sq-width] == floorWall);		if (above) {			if (col > 0) tl = (floorState[sq-width-1] == floorWall);			if (col < width-1) tr = (floorState[sq-width+1] == floorWall);		}	}	if (row < height-1) {		below = (floorState[sq+width] == floorWall);		if (below) {			if (col > 0) bl = (floorState[sq+width-1] == floorWall);			if (col < width-1) br = (floorState[sq+width+1] == floorWall);		}	}	if (col > 0) left = (floorState[sq-1] == floorWall);	if (col < width-1) right = (floorState[sq+1] == floorWall);	var tln = 0;	var trn = 0;	var bln = 0;	var brn = 0;	if (above) {		tln += 2;		trn += 1;	}	if (left) {		tln += 1;		bln += 2;	}	if (below) {		bln += 1;		brn += 2;	}	if (right) {		trn += 2;		brn += 1;	}	if (tln == 3 && tl) tln += 1;	if (trn == 3 && tr) trn += 1;	if (bln == 3 && bl) bln += 1;	if (brn == 3 && br) brn += 1;	return "image.php?img=highlights" +		"&tl=" + tln +		"&tr=" + trn +		"&bl=" + bln +		"&br=" + brn;}function createImages() {	// Create table of images for the board, appropriately scaled	var things = "";	for (var row = 0; row < height; row++) {		if (row > 0) things += "<BR>";		for (var col = 0; col < width; col++) {			var sq = row * width + col;			var fState = floorState[sq];			var color = (fState == floorWall ? "#d0aea0" :				(fState == floorFree ? "#c18456" : "#402d1f"));			things += "<IMG\nID=things" + sq +				" WIDTH=" + scale + " HEIGHT=" + scale +				(fState == floorWater ? "" :					" STYlE=\"background-color: " + color +					"; background-image: url(" +					floorUrl(sq, row, col) + ")\"") +				" SRC=\"" +				(fState == floorWater ? blankUrl :					(fState == floorWall ? highlightUrl(sq, row, col) :						thingUrl(sq))) +				"\">";		}	}	var thingsElt = document.getElementById("things");	thingsElt.innerHTML = things;	thingsElt.style.width = "" + (width * scale) + "px";	xableControl("magnify", scale < maxScale);	xableControl("shrink", scale > minScale);	showHideInline("manImg", manSq >= 0);	moveMan(manSq);}function boundScale(targetScale) {	// Return scale bounded by [minScale, maxScale]	return Math.min(maxScale, Math.max(minScale, targetScale));}function scaleFromWindow() {	// Return scale based on current window size.	// Also updates size of dialog backdrops.	var wSize = windowSize();	var tablePos = getElementPos(document.getElementById("board"))	var maxWidth = wSize.x - 2 * tablePos.x;	var maxHeight = wSize.y - tablePos.y - 16;	document.getElementById("help").style.width = "" + maxWidth + "px";	document.getElementById("help").style.height = "" + maxHeight + "px";	document.getElementById("design").style.width = "" + maxWidth + "px";	document.getElementById("design").style.height = "" + maxHeight + "px";	var xScale =		granularity * Math.floor(maxWidth / (granularity * width));	var yScale =		granularity * Math.floor(maxHeight / (granularity * height));	return boundScale(Math.min(xScale, yScale));}function doResize() {	var newScale = scaleFromWindow();	if (newScale != scale) {		scale = newScale;		createImages();	}}function changeScale(delta) {	// Change current scale by delta increments/decrements of "granularity".	// Assumes "maxScale" is multiple of "granularity", but not "minScale".	scale = boundScale(		(Math.floor(scale/granularity) + delta) * granularity);	createImages();	return false;}function showScore() {	// Display the current ratings	document.getElementById("score").innerHTML =		"Moves: " + moves + ",&nbsp;&nbsp;pushes: " + pushes;}function setSokoCookie(key, value) {	// Set a persistent cookie	var date = new Date();	date.setTime(date.getTime() + (365.25*24*60*60*1000));	deleteCookie(key); // delete archaic path-less cookie, if any	setCookie(key, value, date, window.location.pathname);}function deleteSokoCookie(key) {	// Delete a cookie	deleteCookie(key); // delete archaic path-less cookie, if any	deleteCookie(key, window.location.pathname);}function saveState() {	// Save current board state in a cookie.	if (levelsName && curLevel >= 0) {		var boxState = new Array();		for (var i = 0; i < width * height; i++) {			if (thingState[i] == thingBox) boxState.push(i);		}		var undoState = new Array();		for (var i = 0; i < undoList.length; i++) {			var u = undoList[i];			undoState.push(u.a + " " + u.b + " " + u.manSq +							" " + u.moves + " " + u.pushes);		}		for (var i = 0; i < redoList.length; i++) {			var u = redoList[i];			undoState.push(u.a + " " + u.b + " " + u.manSq +							" " + u.moves + " " + u.pushes);		}		var parts = new Array();		parts.push(manSq);		parts.push(boxState.join(","));		parts.push(moves);		parts.push(pushes);		parts.push(undoList.length);		parts.push(undoState.join(","));		parts.push(curLevel);		var savedState =  parts.join("/");		setSokoCookie("sokoState-" + levelsName, savedState);		savedLevel = curLevel;	}}function saveDone() {	// Save levelsDone into a cookie	if (levelsName) {		var doneList = new Array();		for (var i = 0; i < levelsDone.length; i++) {			if (levelsDone[i]) {				var start = i;				while (levelsDone[i+1]) i++;				doneList.push(i == start ? i : "" + start + "-" + i);			}		}		if (doneList.length == 0) {			deleteSokoCookie("sokoDone-" + levelsName);		} else {			setSokoCookie("sokoDone-" + levelsName, doneList.join(","));		}	}}function placeBoxes(boxList) {	// Place boxes in thingState according to "boxList".	// Assumes there are no existing boxes in thingState	for (var i = 0; i < boxList.length; i++) {		thingState[boxList[i]] = thingBox;	}}function terminateLevel() {	// Dispose of state related to existing level, if any	selectBox(-1);	manCurRoute = null;	boxCurRoute = null;	eraseUndo();	pushes = 0;	moves = 0;	showScore();}function startLevel(level) {	// Start given level of current collection	//	if (level < 0 || level >= levels.length) return false;//	window.onresize = doResize;	terminateLevel();	curLevel = level;	setOption("levels", curLevel);	xableControl("nextLevel", curLevel+1 < levels.length);	xableControl("prevLevel", curLevel > 0);	xableControl("nextUnfinished", false);	for (var i = curLevel+1; i < levels.length; i++) {		if (!levelsDone[i]) {			xableControl("nextUnfinished", true);			break;		}	}	if (levelsName) {		setSokoCookie("sokoCollection", levelsName);		setSokoCookie("sokoLevel-" + levelsName, "" + curLevel);	}		// Create floorState, initBoxes and initMan from level description	// Set thingState to thingFree or thingWall as appropriate	var levelLines = levels[level];	width = 1;	for (var row = 0; row < levelLines.length; row++) {		width = Math.max(width, levelLines[row].length);	}	height = Math.max(1, levelLines.length);	initBoxes = new Array();	floorState = new Array();	thingState = new Array();	neighbourCache = new Array();	targets = new Array();	for (var row = 0; row < levelLines.length; row++) {		var line = levelLines[row];		var col;		for (col = 0; col < line.length; col++) {			var sq = row * width + col;			var c = line.charAt(col);			thingState[sq] = thingFree;			if (c == "-" || c == " " || c == "_") {				floorState[sq] = floorFree;			} else if (c == "#") {				floorState[sq] = floorWall;				thingState[sq] = thingWall;			} else if (c == "." || c == "o") {				floorState[sq] = floorTarget;				targets.push(sq);			} else if (c == "$" || c == "b") {				floorState[sq] = floorFree;				initBoxes.push(sq);			} else if (c == "*" || c == "B") {				floorState[sq] = floorTarget;				initBoxes.push(sq);				targets.push(sq);			} else if (c == "@" || c == "p") {				floorState[sq] = floorFree;				initMan = sq;			} else if (c == "+" || c == "P") {				floorState[sq] = floorTarget;				targets.push(sq);				initMan = sq;			}		}		for (j = col; j < width; j++) {			var sq = row * width + j;			floorState[sq] = floorWater;			thingState[sq] = thingWall;		}	}	// Turn external non-wall squares into water, using a flood-fill	// algorithm (of course).	// Also floods the passages if the external walls have a leak.	var wTouch = new Array();	var wQueue = new Array();	for (var row = 1; row < height-1; row++) {		var left = row * width;		wQueue.push(left);		wQueue.push(left+width-1);	}	for (var col = 0; col < width; col++) wQueue.push(col);	var botLeft = (levelLines.length-1) * width;	for (var col = 0; col < width; col++) wQueue.push(botLeft + col);	while (wQueue.length > 0) {		var sq = wQueue.shift();		if (!wTouch[sq]) {			wTouch[sq] = true;			if (floorState[sq] != floorWall) {				floorState[sq] = floorWater;				thingState[sq] = thingWall;			}			if (floorState[sq] == floorWater) {				var n = neighbours(sq);				for (var i = 0; i < 4; i++) {					if (n[i] >= 0) wQueue.push(n[i]);				}			}		}	}	// Set thingState for non-walls, and manSq, from one of:	//    saved state, done position, initial state	var savedState = getCookie("sokoState-" + levelsName);	var savedParts;	if (savedState) {		savedParts = savedState.split("/");		savedLevel = parseInt(savedParts.pop());	}	if (savedState && savedLevel == curLevel) {		var undoStateStr = savedParts.pop();		var undoState = (undoStateStr == "" ? new Array() :												undoStateStr.split(","));		var undoLength = parseInt(savedParts.pop());		pushes = parseInt(savedParts.pop());		moves = parseInt(savedParts.pop());		var boxState = savedParts.pop().split(",");		manSq = parseInt(savedParts.pop());		placeBoxes(boxState);		for (var i = 0; i < undoState.length; i++) {			var undoParts = undoState[i].split(" ");			var action = new UndoState(							parseInt(undoParts[0]), parseInt(undoParts[1]));			action.manSq = parseInt(undoParts[2]);			action.moves = parseInt(undoParts[3]);			action.pushes = parseInt(undoParts[4]);			if (i < undoLength) {				undoList.push(action);			} else {				redoList.push(action);			}		}		if (!levelsDone[curLevel] && allTargetsDone()) {			levelsDone[curLevel] = true;			saveDone();			// ... presumably we crashed before calling saveDone		}		if (levelsDone[curLevel]) {			if (redoList.length > 0) alert("Bug: excess redoList");			manSq = -1;		}		showScore();		xableControl("restart", undoList.length > 0);		xableControl("undo", undoList.length > 0);		xableControl("redo", redoList.length > 0);	} else if (levelsDone[curLevel]) {		for (var i = 0; i < width * height; i++) {			if (floorState[i] == floorTarget) thingState[i] = thingBox;		}		manSq = -1;		xableControl("restart", true);	} else {		placeBoxes(initBoxes);		manSq = initMan;	}		// Update the display	scale = scaleFromWindow();	createImages();	return false;}function selectLevel() {	// Action in the "levels" selector	var levelNum = parseInt(getOption("levels"));	return (levelNum != curLevel && startLevel(levelNum));}function nextUnfinished() {	// Go to the next unfinished level, if any	for (var i = curLevel+1; i < levels.length; i++) {		if (!levelsDone[i]) {			startLevel(i);			break;		}	}}function neighbours(sq) {	// Returns an array containing the neighbouring squares of "sq",	// excluding walls.	//	// There are 4 entries, one for each side.  Entry is >= 0 if there	// is an in-bounds neighbouring square there.	//	// Index 3-i is the neighbour opposite to index i.	//	var c = neighbourCache[sq];	if (c) return c;	var n = new Array();	var row = Math.floor(sq/width);	var col = sq - row * width;	n[0] = (row > 0 ? sq-width : -1);	n[3] = (row < height-1 ? sq+width : -1);	n[1] = (col > 0 ? sq-1 : -1);	n[2] = (col < width-1 ? sq+1 : -1);	for (var i = 0; i < 4; i++) {		if (floorState[n[i]] == floorWall) n[i] = -1;	}	neighbourCache[sq] = n;	return n;}//// Interaction//function allTargetsDone() {	// Return true iff there are no empty target squares	if (targets) {		for (var i = 0; i < targets.length; i++) {			if (thingState[targets[i]] == thingFree) return false;		}	}	return true;}function findPath(a, b, justChecking) {	// Find shortest legal path for a man at "a" moving to "b", b!=a.	// Says nothing about the contents of "a" or "b".	//	// If "justChecking", returns length of the path, or 0; otherwise	// returns an array containing the path (with "b" but not "a"), or null.	//	// Uses the simplest version of Dijkstra's shortest-path algorithm.	// The graph nodes are positions of the man, and there's an arc to	// another node iff the other node is empty and adjacent.	//	// "srce" is indexed by node and contains a node one closer	// on the shortest path to this node from "a".  (Proof by induction.)	//	// "thisGen" is set of nodes at the current distance from "a";	// "nextGen" is nodes one hop further away.	// Dijkstra used a FIFO, but separate arrays are easier in Javascript.	//	var srce = new Array();	var thisGen = new Array();	var nextGen = new Array();	if (a >= 0) {		srce[a] = a;		thisGen.push(a);	}	while (true) {		if (thisGen.length == 0) {			// no more nodes at this distance			if (nextGen.length > 0) {				var temp = thisGen;				thisGen = nextGen;				nextGen = temp;			} else {				return (justChecking ? 0 : null);			}		}		var sPos = thisGen.pop();		var n = neighbours(sPos);		for (var i = 0; i < 4; i++) {			var neighbour = n[i];			if (neighbour < 0 || srce[neighbour] || srce[neighbour] === 0) {				// out of bounds or already in play: skip			} else if (neighbour == b) {				// success: build the route and exit.				if (justChecking) {					var len = 1;					var pos = sPos;					while (pos != a) {						len++;						pos = srce[pos];					}					return len;				} else {					var route = new Array();					route.push(neighbour);					var pos = sPos;					while (pos != a) {						route.push(pos);						pos = srce[pos];					}					return route.reverse();				}			} else if (thingState[neighbour] == thingFree) {				srce[neighbour] = sPos;				nextGen.push(neighbour);			}		}	}}function oppositeNeighbour(sq, neighbour) {	// If "neighbour" is adjacent to "sq", return square on the opposite	// side; else return -1.	// Doesn't do bounds checking (there's a perimeter wall).	return (neighbour == sq - 1 ? sq + 1 :				(neighbour == sq + 1 ? sq - 1 :					(neighbour == sq-width ? sq + width :						(neighbour == sq+width ? sq - width : -1))));}function findBoxPath(a, b) {	// Find the shortest path to push a box from "a" to "b", from "manSq".	// Assumes there's a box at "a", and fails if there's one already at "b"	//	// Like "findPath", this uses Dijkstra's algorithm, but the graph is	// rather different.  The nodes are a box position combined with a	// man position.  There's an arc to another node iff one of:	//   a) the other node is the same box position, with a man position	//      that's reachable from this node's man position (i.e. we can	//      move the man to it), or	//   b) this node's man is adjacent to this node's box, and the other	//      node's box position is adjacent to this node's box position and	//      opposite to this node's man position (i.e. we can push to it).	//	// This supports paths where we must push a box through a narrow	// passage then back again in order to get the man around to the other	// side of the box.  The simpler notion of a graph of merely box	// positions doesn't handle that case.	//	// "srce" is indexed by node and contains a node one earlier	// on the shortest path to this node from "a".  (Proof by induction.)	//	// "srceMoves" is indexed by node, and contains the number of man moves	// to reach the node.  This lets us choose among the shortest paths the	// one with the least number of man moves.	//	// "thisGen" is set of nodes at the current distance from "a";	// "nextGen" is nodes one hop further away.	// Dijkstra used a FIFO, but separate arrays are easier in Javascript.	//	var srce = new Array();	var srcePushes = new Array();	var srceMoves = new Array();	var pushes = -1;	var thisGen = new Array();	var nextGen = new Array();	var posExp = /^([^-]*)-.*$/;	var manExp = /^[^-]*-(.*)$/;	var startNode = "" + a + "-" + manSq;	var finalNode = null;	srce[startNode] = "origin";	srcePushes[startNode] = 0;	srceMoves[startNode] = 0;	nextGen.push(startNode);	// During the algorithm, we pretend our box is at the current node,	// not at "a".  This gets undone before returning.	thingState[a] = thingFree;	while (true) {		if (thisGen.length == 0) {			// no more nodes at this distance			if (finalNode) {				// success: construct and return the route				var route = new Array();				route.push(b);				var node = finalNode;				while (true) {					var nodePos = parseInt(node.replace(posExp, "$1"));					if (nodePos != route[route.length-1]) {						route.push(nodePos);					}					if (node == startNode) break;					node = srce[node];				}				// Remove "a" from the route				route.pop();				thingState[a] = thingBox;				return route.reverse();			} else if (nextGen.length == 0) {				// failure				thingState[a] = thingBox;				return null;			}			var temp = thisGen;			thisGen = nextGen;			nextGen = temp;			pushes++;			// Before considering any of thisGen, add on adjacent squares			// reachable by the man.			var genLen = thisGen.length; // thisGen changes during the loop			for (var i = 0; i < genLen; i++) {				var xNode = thisGen[i];				var xPos = parseInt(xNode.replace(posExp, "$1"));				var xMan = parseInt(xNode.replace(manExp, "$1"));				var xLen = srceMoves[xNode];				thingState[xPos] = thingBox;				var n = neighbours(xPos);				for (var j = 0; j < 4; j++) {					var neighbour = n[j];					if (neighbour >= 0 && neighbour != xMan &&									thingState[neighbour] == thingFree) {						var oppSq = n[3-j];						if (oppSq >= 0 && thingState[oppSq] == thingFree) {							var nNode = "" + xPos + "-" + neighbour;							var nextLen = findPath(xMan, neighbour, true);							var len = xLen + nextLen;							var missing = (!srce[nNode]);							if (nextLen > 0 && (missing ||									(pushes == srcePushes[nNode] &&										len < srceMoves[nNode]))) {								if (missing) {									thisGen.push(nNode);									srcePushes[nNode] = pushes;								}								srce[nNode] = xNode;								srceMoves[nNode] = len;							}						}					}				}				thingState[xPos] = thingFree;			}		}		var thisNode = thisGen.pop();		var thisPos = parseInt(thisNode.replace(posExp, "$1"));		var thisMan = parseInt(thisNode.replace(manExp, "$1"));		var pushDest = oppositeNeighbour(thisPos, thisMan);		if (pushDest >= 0 && thingState[pushDest] == thingFree) {			var destNode = "" + pushDest + "-" + thisPos;			var len = srceMoves[thisNode] + 1;			if (pushDest == b) {				// Got there; but we need to finish this generation in case				// there's a path with fewer man moves.				// Note that since we're not going to do another generation,				// finalNode, if any, necessarily has the same number of				// pushes as us.				if (!finalNode || len < srceMoves[finalNode]) {					finalNode = destNode;					srce[destNode] = thisNode;					srceMoves[destNode] = len;				}			} else {				var missing = (!srce[destNode]);				if (missing || ((pushes+1) == srcePushes[destNode] &&											len < srceMoves[destNode])) {					if (missing) {						nextGen.push(destNode);						srcePushes[destNode] = pushes + 1;					}					srce[destNode] = thisNode;					srceMoves[destNode] = len;				}			}		}	}	alert("Bug: findBoxPath drop through");}var manCurRoute = null; // route for man-move animationvar boxCurRoute = null; // route for box-move animationvar boxCurSq = -1;      // box location for next animation movefunction routeMan() {	// If speed == 0, jump man to the end of manCurRoute, and do the score	// accounting.  Doesn't schedule anything else in this case.	//	// If speed != 0, move man one step along boxCurRoute. If there's more	// route, schedule next step; otherwise schedule a call of routeBox.	//	if (manCurRoute) {		if (manCurRoute.length == 0) alert("Bug: empty man route");		if (manCurRoute[0] == -1) moves--; // move to -1 is free		if (speed == 0) {			moves += manCurRoute.length;			moveMan(manCurRoute.pop());			manCurRoute = null;		} else {			moves++;			moveMan(manCurRoute.shift());			if (manCurRoute.length > 0) {				setTimeout(routeMan, speed);			} else {				manCurRoute = null;				if (boxCurRoute) setTimeout(routeBox, speed);			}		}		showScore();	}}function routeBox() {	// If speed == 0, move box from boxCurSq to end of boxCurRoute, and man	// to the appropriate final position, doing all the score accounting.	//	// If speed != 0, initiate a step in moving from boxCurSq along	// boxCurRoute; this might move the box, or just route the man to the	// appropriate pushing spot (in which case routeMan will schedule	// another call of routeBox when the man arrives).	//	// The outer "while" handles iteration when speed==0, as well as	// abandoning a deferred task if boxCurRoute gets cancelled (by undo,	// etc).	//	while (boxCurRoute) {		if (boxCurRoute.length == 0) {			alert("Bug: empty box route");			boxCurRoute = null;		}		var pos = boxCurRoute[0];		var opposite = oppositeNeighbour(boxCurSq, pos);		if (opposite < 0) {			alert("Bug: next pos not adjacent in routeBox");			boxCurRoute = null;		} else if (opposite == manSq) {			boxCurRoute.shift();			selectBox(-1);			moveBox(boxCurSq, pos);			moveMan(boxCurSq);			boxCurSq = pos;			pushes++;			moves++;			showScore();			if (boxCurRoute.length > 0) {				if (speed != 0) {					setTimeout(routeBox, speed);					break;				} // else iterate around our outer loop			} else {				saveState();				boxCurRoute = null; // also terminates our outer loop				if (allTargetsDone()) {					levelsDone[curLevel] = true;					saveDone();					manCurRoute = new Array();					manCurRoute.push(-1);					if (speed == 0) {						routeMan();					} else {						setTimeout(routeMan, 150);					}				}				if (redoList.length > 0) xableControl("redo", true);			}		} else {			manCurRoute = findPath(manSq, opposite, false);			if (!manCurRoute) {				alert("Bug: no route for man from " + manSq +						" to " + opposite);				boxCurRoute = null;			} else {				routeMan();			}			if (speed != 0) break; // else iterate for next box move		}	}}function sqClick(event) {	// Respond to mouse click in a square	if (!event) event = window.event; // IE versus the rest	var eltPos = getElementPos(document.getElementById("things"));	var mouse = getMousePos(event);	var x = mouse.x - eltPos.x;	var y = mouse.y - eltPos.y;	var row = Math.floor(y / scale);	var col = Math.floor(x / scale);	if (row >= 0 && row < height && col >= 0 && col < width) {		var sq = row * width + col;		var thing = thingState[sq];		if (manCurRoute || boxCurRoute) {			// Ignore clicks during the animation		} else if (thing == thingBox) {			if (selectedBox < 0 && savedLevel >= 0 &&						savedLevel != curLevel && !levelsDone[savedLevel]) {				alert("Warning: if you make a move in this level,\ you will abandon the game in progress at level " + (savedLevel+1));			}			selectBox(sq == selectedBox ? -1 : sq);		} else if (selectedBox >= 0) {			if (thing == thingFree) {				boxCurRoute = findBoxPath(selectedBox, sq);				boxCurSq = selectedBox;				if (boxCurRoute) {					record(selectedBox, sq);					routeBox();				} else {					selectBox(-1);				}			} else {				selectBox(-1);			}		} else if (manSq != sq && thing == thingFree) {			manCurRoute = findPath(manSq, sq, false);			if (manCurRoute) routeMan();		}	}	return false;}function showHideDlog(id, show) {	// Show or hide a dialog (e.g. help or design)	showHide("normalControls", !show);	showHide("things", !show);	if (manSq >= 0) showHide("manImg", !show);	showHide(id, show);}function showHideDesign(show) {	showHideDlog("design", show);	showHide("designControls1", show);}function startDesign() {	// From playing, go to design edit dialog	showHideDesign(true);	document.getElementById("designContents").value =										levels[curLevel].join("\n");	return false;}function playDesign() {	// From design edit dialog, start play	showHideDesign(false); // display board before calling "adoptCollection"	showHideInline("designControls2", true);	showHideInline("collections", false);	adoptCollection(document.getElementById("designContents").value);	return false;}function perhapsAbandonDesignEdit() {	// From design edit, ask for confirmation	showHide("designControls1", false);	showHide("designControls4", true);	return false;}function dontAbandonDesignEdit() {	// Confirmation denied, return to design edit	showHide("designControls1", true);	showHide("designControls4", false);	return false;}function perhapsAbandonDesign() {	// From design play, ask for confirmation	showHideInline("designControls2", false);	showHideInline("designControls3", true);	return false;}function dontAbandonDesign() {	// Confirmation denied, return to design play	showHideInline("designControls2", true);	showHideInline("designControls3", false);	return false;}function abandonDesign() {	// Resume normal service	// Works from normal design play, or from confirmation state,	// or from design edit state	showHideDesign(false);	showHideInline("designControls2", false);	showHideInline("designControls3", false);	showHide("designControls4", false);	showHideInline("collections", true);	initSelCollection();	return false;}function showHideHelp(show) {	showHideDlog("help", show);	showHide("helpControls", show);	return false;}function changeSpeed() {	// change in the "speeds" menu	var newS = parseInt(getOption("speeds"));	if (newS != speed) {		speed = newS;		setSokoCookie("sokoSpeed", "" + speed);	}	return false;}function showHideHistory(show) {	// Show or hide history dialog	showHideDlog("history", show);	showHide("historyControls", show);}function computeHistory() {	// Return text describing the current history	var undoState = new Array();	for (var i = 0; i < undoList.length; i++) {		var item = undoList[i];		undoState.push("" + item.a + "-" + item.b);	}	if (redoList.length > 0) {		undoState.push("Redo");		for (var i = redoList.length-1; i >= 0; i--) {			var item = redoList[i];			undoState.push("" + item.a + "-" + item.b);		}	}	return undoState.join("\n");}function startHistory() {	document.getElementById("historyContents").value = computeHistory();	showHideHistory(true);	return false;}function doneHistory() {	// Exit from history dialog	showHideHistory(false); // display board before calling "restart"	var s = document.getElementById("historyContents").value;	s = s.replace(/\r\n/g, "\n");	s = s.replace(/\r/g, "\n");	if (s != computeHistory()) {		undoList.length = 0;		redoList.length = 0;		var moves = s.split("\n");		for (var i = moves.length-1; i >= 0; i--) {			var move = moves[i];			if (move.match(/^\s*[0-9]+\s*-\s*[0-9]+\s*$/)) {				var ab = move.split("-");				redoList.push(					new UndoState(parseInt(ab[0]), parseInt(ab[1])));			} else if (move != "Redo" && move != "") {				alert("Syntax error in the history list");				return false;			}		}		restart();	}	return false;}function showCookies() {	var cc = document.cookie.split(";").sort();	var s = "" + cc.length + " cookies, " +				document.cookie.length + " chars\n";	s += "level: " + getCookie("sokoLevel-" + levelsName) + "\n";	var done = getCookie("sokoDone-" + levelsName);	if (done) {		s += "done: " + done + "\n";	} else {		s += "no levels done\n";	}	var state = getCookie("sokoState-" + levelsName);	if (state) {		var stateParts = state.split("/");		s += "saved level: " + stateParts.pop();		var undoState = stateParts.pop();		if (undoState) {			s += ", " + undoState.split(",").length + " moves";		}		s += "\n";	} else {		s += "no saved state\n";	}	for (var i = 0; i < cc.length; i++) {		var kv = cc[i].split("=");		var key = kv[0].replace(/^\s*(.*)$/, "$1");		var value = getCookie(key);		s += key + "=" + value.substr(0, 20) +			(value.length > 20 ? "..." : "") + "\n";	}	alert(s);	return false;}//// Undo/redo//var undoList = new Array();var redoList = new Array();function UndoState(a, b) {	// Constructor to undo moving box from "a" to "b"	this.a = a;	this.b = b;	this.manSq = manSq;	this.moves = moves;	this.pushes = pushes;}function record(a, b) {	// Record action in undo state	undoList.push(new UndoState(a, b));	redoList.length = 0;	xableControl("restart", true);	xableControl("undo", true);	xableControl("redo", false);}function eraseUndo() {	// Erase undo state at start or end of a level	undoList.length = 0;	redoList.length = 0;	xableControl("restart", false);	xableControl("undo", false);	xableControl("redo", false);}function restart() {	// Restart the current level	while (undoList.length > 0) redoList.push(undoList.pop());	var myRedo = redoList;	redoList = new Array(); // keep terminateLevel out of myRedo	terminateLevel();	if (levelsDone[curLevel]) {		levelsDone[curLevel] = false;		saveDone();	}	for (var sq = 0; sq < width * height; sq++) {		if (thingState[sq] == thingBox) {			thingState[sq] = thingFree;			updateBoxImg(sq);		}	}	moveMan(-1)	// Now set up the initial position, plus our saved redo list	placeBoxes(initBoxes);	for (var i = 0; i < initBoxes.length; i++) {		updateBoxImg(initBoxes[i]);	}	moveMan(initMan);	if (myRedo.length > 0) {		// Note: this might implicitly abandon another saved level. That		// happens only in response to editting a history in a non-saved		// level.		redoList = myRedo;		saveState();	} else if (savedLevel == curLevel) {		deleteSokoCookie("sokoState-" + levelsName);		savedLevel = -1;	}	xableControl("redo", redoList.length > 0);}function undo() {	// Undo most recent box move (no animation)	if (undoList.length > 0) {		var action = undoList.pop();		redoList.push(action);		xableControl("redo", true);		if (undoList.length == 0) {			xableControl("restart", false);			xableControl("undo", false);		}		if (levelsDone[curLevel]) {			levelsDone[curLevel] = false;			saveDone();		}		selectBox(-1);		if (manSq == action.a) moveMan(-1); // don't trample him		moveBox((boxCurRoute ? boxCurSq : action.b), action.a);		moveMan(action.manSq);		manCurRoute = null;		boxCurRoute = null;		moves = action.moves;		pushes = action.pushes;		showScore();		saveState();	}	return false;}function redo() {	// Redo an undone box move, with animation	if (redoList.length > 0) {		var action = redoList.pop();		if (thingState[action.a] != thingBox ||				thingState[action.b] != thingFree ||				action.a == action.b ||				!(boxCurRoute = findBoxPath(action.a, action.b))) {			alert("Incorrect redo entry " + action.a + " to " +					action.b);			redoList.push(action);		} else {			undoList.push(new UndoState(action.a, action.b));			xableControl("restart", true);			xableControl("undo", true);			if (redoList.length == 0) xableControl("redo", false);			xableControl("redo", false); // during routeBox			selectBox(action.a);			manCurRoute = null;			boxCurSq = action.a;			var savedSpeed = speed;			speed = 0;			routeBox(); // also saves the state			speed = savedSpeed;		}	}	return false;}function wheelMove(delta) {	// Respond to mouse wheel action with undo/redo instead of scrolling	if (delta < 0) {		undo();	} else if (!manCurRoute && !boxCurRoute) {		redo();	}	return false;}//// Initialization//function LevelRequest(collection) {	// Constructor for reading a level set via initiateXMLHttp	this.collection = collection;	this.url = "levels/" + collection + ".txt";	initiateXMLHttp(this);}LevelRequest.prototype.handleFailure = function() {	alert("Failed to read levels for \"" + this.collection + "\"");}function adoptCollection(s) {	s = s.replace(/\r\n/g, "\n");	s = s.replace(/\r/g, "\n");	levelsTxt = s.replace(/\|/g, "\n");	var lines = levelsTxt.split("\n");	var i = 0;	levels = new Array();	while (i < lines.length) {		while (i < lines.length && !lines[i].match(/ *#/)) i++;		if (i < lines.length) {			var levelLines = new Array();			while (i < lines.length && lines[i].match(/ *#/)) {				levelLines[levelLines.length] = lines[i];				i++;			}			levels[levels.length] = levelLines;		}	}	truncateOptions("levels", 0);	var lSelect = document.getElementById("levels");	for (i = 0; i < levels.length; i++) {		appendOption(lSelect, "Level " + (i+1), i);	}	lSelect.disabled = (levels.length <= 1);	levelsDone = new Array(levels.length);	curLevel = -1;	savedLevel = -1;	if (levelsName) {		var oldDone = getCookie("sokoDone-" + levelsName);		if (oldDone) {			var doneList = oldDone.split(",");			for (var i = 0; i < doneList.length; i++) {				var ab = doneList[i].split("-");				if (ab.length == 1) ab[1] = ab[0];				ab[0] = parseInt(ab[0]);				ab[1] = parseInt(ab[1]);				for (var j = ab[0]; j <= ab[1]; j++) levelsDone[j] = true;			}		}		var oldLevel = getCookie("sokoLevel-" + levelsName);		if (oldLevel && setOption("levels", oldLevel)) {			startLevel(parseInt(oldLevel));		} else {			startLevel(0);		}	} else {		startLevel(0);	}}LevelRequest.prototype.handleResult = function(xmlhttp) {	adoptCollection(xmlhttp.responseText);}function selCollection() {	// Switch to a collection based on the selector	var collectionName = getOption("collections");	if (collectionName == levelsName) {	} else if (collectionName == "design") {		levelsName = null;		startDesign();	} else {		levelsName = collectionName;		new LevelRequest(collectionName);	}	return false;}function initSelCollection() {	// Switch to a collection based on a cookie, or force 0	var oldC = getCookie("sokoCollection");	if (oldC) {		setOption("collections", oldC);	} else {		setOptSelection("collections", 0);	}	selCollection();}function init() {	addMouseWheel("board", wheelMove);	var oldS = getCookie("sokoSpeed");	if (oldS) setOption("speeds", oldS);	speed = parseInt(getOption("speeds"));	if (navigator.userAgent.match(/MSIE 6/)) {		alert("This program doesn't work properly with Internet Explorer\ version 6, because the program uses translucent PNG files.\ You should upgrade to a more modern browser.\ IE 7 or IE 8 are fine, as are Firefox and Safari."); 	}	initSelCollection();}