/**
* jSokoban
* Nicholas Wright (http://www.nicholaswright.org/games/jsokoban)
* Created 06 / 22 / 2008
*
* Minicosmos levels from http://members.aol.com/SokobanMac/levels/minicosmosText.html
* Sasquatch IV levels from http://members.aol.com/SokobanMac/levels/sasquatch4Text.html
*/
var DefaultMaps = [
// 'EASY'
// Minicosmos levels
' ##### \n' +
'### # \n' +
'# $ # ##\n' +
'# # . #\n' +
'# # #\n' +
'##$#. #\n' +
' #@ ###\n' +
' ##### ',
' ##### \n' +
'### # \n' +
'# $ # ##\n' +
'# # . #\n' +
'# . # #\n' +
'##$#.$ #\n' +
' #@ ###\n' +
' ##### ',
' ####\n' +
'##### #\n' +
'# $ #\n' +
'# .# #\n' +
'## ## ##\n' +
'# #\n' +
'# @# #\n' +
'# #####\n' +
'#### ',
' ####\n' +
'##### #\n' +
'# $ #\n' +
'# *.# #\n' +
'## ## ##\n' +
'# #\n' +
'# @# #\n' +
'# #####\n' +
'#### ',
' ####\n' +
'##### #\n' +
'# * #\n' +
'# *.# #\n' +
'## ## ##\n' +
'# $ #\n' +
'# @# #\n' +
'# #####\n' +
'#### ',
' ##### \n' +
' # ## \n' +
'## #$ ##\n' +
'# $ #\n' +
'#. .# #\n' +
'### @ ##\n' +
' # # # \n' +
' # # \n' +
' ##### ',
' ##### \n' +
' # ## \n' +
'##.#$ ##\n' +
'# $ #\n' +
'#. .#$ #\n' +
'### @ ##\n' +
' # # # \n' +
' # # \n' +
' ##### ',
' ##### \n' +
' # # \n' +
'##$# ###\n' +
'# $@ #\n' +
'# # # #\n' +
'# #. . #\n' +
'# ####\n' +
'##### ',
// 'HARD'
// sasquatch 4 Level 6
' #### \n' +
'### # \n' +
'# ..# ####### \n' +
'# #..# # ####\n' +
'# #. ### $ #\n' +
'# #. # $ $ $$ #\n' +
'# # @ ### $## #\n' +
'# #####\n' +
'## ######### \n' +
' #### ',
// sasquatch 4 Level 7
' ############### \n' +
'## $. .$ ##\n' +
'# # ####### # #\n' +
'# # # #\n' +
'# .***$#$***. #\n' +
'### # ###\n' +
' # ####@#### # \n' +
' # # \n' +
' ############# ',
// sasquatch 4 Level 8
'#######################\n' +
'# # # # #\n' +
'# $@$$ # $ # .. ..#\n' +
'## ## ### ### ### ## ##\n' +
' # # # # # \n' +
' # # # # # # # \n' +
' # ################# # \n' +
' # # \n' +
' ##################### ',
// sasquatch 4 Level 9
'########### \n' +
'#@ # # # \n' +
'# $#$ $# \n' +
'## #..# # \n' +
' # #..# # \n' +
' # #..# ##\n' +
' #$ $#$ #\n' +
' # # # #\n' +
' ###########',
// sasquatch 4 Level 10
'###### \n' +
'# # \n' +
'# .$ # \n' +
'# ** # \n' +
'##$. # \n' +
' # ####\n' +
' # ## #\n' +
' # # #\n' +
' # #\n' +
' #.**$@#\n' +
' # # #\n' +
' #######',
// sasquatch 4 Level 11
' ########\n' +
' ##.... @#\n' +
' # # . #\n' +
'## # # ##\n' +
'# #$ # # \n' +
'# $ # ##\n' +
'###$ ## #\n' +
' # $$ #\n' +
' # # #\n' +
' ########',
// sasquatch 4 Level 12
' ###### \n' +
' # # \n' +
' # ### \n' +
' ##*# # \n' +
'## . ## # \n' +
'# # ##\n' +
'# #.# $ #\n' +
'# $.###$ #\n' +
'### ## #\n' +
' # $$@#\n' +
' ##..## #\n' +
' # #####\n' +
' # # \n' +
' ##### ',
// sasquatch 4 Level 13
' #### \n' +
' ##### # \n' +
'###. #$ ## \n' +
'# * .*.# \n' +
'# $.$ #$ # \n' +
'### ### # \n' +
' # ### ###\n' +
' # $# $.$ #\n' +
' #.*.@ * #\n' +
' ## $# .###\n' +
' # ##### \n' +
' #### ',
// sasquatch 4 Level 14
' ###### \n' +
' # #####\n' +
' # $ *# #\n' +
' # * * $ #\n' +
'###* . * #\n' +
'# * .@. * #\n' +
'# * . *###\n' +
'# $ * * # \n' +
'# #* $ # \n' +
'##### # \n' +
' ###### ',
// sasquatch 4 Level 15
'###########\n' +
'# * #\n' +
'# $$ ## $ #\n' +
'# $..#$$ #\n' +
'# ##*.*. #\n' +
'#*#..@..#*#\n' +
'# .*.*## #\n' +
'# $$#..$ #\n' +
'# $ ## $$ #\n' +
'# * #\n' +
'###########',
// sasquatch 4 Level 16
'#############\n' +
'# $ . #\n' +
'#.$ $### *$ #\n' +
'# ** ## .* #\n' +
'# .$#..$ $ #\n' +
'# # .$.$### #\n' +
'#$##..@..##$#\n' +
'# ###$.$. # #\n' +
'# $ $..#$. #\n' +
'# *. ## ** #\n' +
'# $* ###$ $.#\n' +
'# . $ #\n' +
'#############',
// sasquatch 4 Level 17
'###############\n' +
'# #\n' +
'# $.$.$.$.$.$ #\n' +
'# .$.$.#.$.$. #\n' +
'# $.$.$ $.$.$ #\n' +
'# .$.$.#.$.$. #\n' +
'# $.$.$ $.$.$ #\n' +
'# .# # @ # #. #\n' +
'# $.$.$ $.$.$ #\n' +
'# .$.$.#.$.$. #\n' +
'# $.$.$ $.$.$ #\n' +
'# .$.$.#.$.$. #\n' +
'# $.$.$.$.$.$ #\n' +
'# #\n' +
'###############',
// sasquatch 4 Level 18
' ############### \n' +
'## # # # ##\n' +
'# **.. ..** #\n' +
'# * $$$ * #\n' +
'##* .### ###. *##\n' +
'# * ## $@$ ## * #\n' +
'# . # # . #\n' +
'# .$#$ ### $#$. #\n' +
'## $ # # $ ##\n' +
'# .$#$ ### $#$. #\n' +
'# . # # . #\n' +
'# * ## $ $ ## * #\n' +
'##* .### ###. *##\n' +
'# * $$$ * #\n' +
'# **.. ..** #\n' +
'## # # # ##\n' +
' ############### ',
// sasquatch 4 Level 19
' ######### \n' +
' # * # \n' +
' # ## ## # \n' +
' # * * # \n' +
'### # ###\n' +
'# .$#$. #\n' +
'# # @ # #\n' +
'# .$#$. #\n' +
'### # ###\n' +
' ####### ',
// sasquatch 4 Level 20
'#### \n' +
'# ######### \n' +
'# ## # \n' +
'# $$$# # \n' +
'##...# #$$$# \n' +
' #...# #...# \n' +
' #$$$ #...##\n' +
' # ##$$$ #\n' +
' ####### @ #\n' +
' ######',
// sasquatch 4 Level 21
' ######### \n' +
' ## # # \n' +
'## $# # #$###\n' +
'# #. .# #\n' +
'# $ *.@.* $ #\n' +
'# #. .# #\n' +
'###$# # #$ ##\n' +
' # # ## \n' +
' ######### ',
// sasquatch 4 Level 22
' #### #### \n' +
' # #### #### \n' +
'### $ $ # \n' +
'# # $ *...##\n' +
'# $ # #### #.. #\n' +
'## # #\n' +
' # $$ $$@ #...##\n' +
' # # ####### \n' +
' #### # # \n' +
' ##### ',
// sasquatch 4 Level 23
' #### \n' +
' ###### # \n' +
' ## $ $ # \n' +
'## $ #$ # \n' +
'# $ # $# \n' +
'# # ## ##\n' +
'###$ ..# #\n' +
' # #*...@#\n' +
' # ..####\n' +
' # #### \n' +
' #### '
];
function Piece( elTD ) {
this.elTD = elTD;
this.setCoords();
}
Piece.prototype = {
setCoords: function() {
var aryCoords = this.elTD.id.split( "-" );
this.x = parseInt( aryCoords.first() );
this.y = parseInt( aryCoords.last() );
},
addClass: function( strClass ) {
Element.addClassName( this.elTD, strClass );
},
removeClass: function( strClass ) {
Element.removeClassName( this.elTD, strClass );
},
canBePushed: function() {
return this.elTD.hasClassName( 'ruby' );
},
isBlocked: function() {
return ![ "", "storage" ].include( this.elTD.className );
}
}
function MapGeneration( strMap ) {
this.hshClassMap = $H( {
" ": "",
"#": "wall",
"@": "player",
"$": "ruby",
".": "storage",
"*": "storage ruby",
"+": "storage player"
} );
this.elBoardContainer = $( 'jsokoban' );
this.elMapEditorInput = $( 'input-map' );
this.aryValidTiles = this.hshClassMap.keys();
this.intCurrentMap = null;
this.setup();
this.generate();
}
MapGeneration.prototype = {
aryNodes: [],
hshGameBoard: {},
setup: function( strMap ) {
this.aryRows = this.loadSuppliedMapOrRandom( strMap );
this.elMapEditorInput.value = this.aryRows.join( "\n" );
this.intRows = this.aryRows.length;
this.intCols = this.aryRows.first().length;
},
reset: function( strMap ) {
this.aryNodes.each( Element.remove );
this.aryNodes.clear();
this.setup( strMap );
this.generate();
},
loadSuppliedMapOrRandom: function( strMap ) {
if ( typeof( strMap ) == 'undefined' ) {
strMap = this.randomMap();
}
return strMap.split( '\n' )
},
randomMap: function() {
var intIndex = Math.floor( Math.random() * DefaultMaps.length );
this.intCurrentMap = intIndex;
return DefaultMaps[ intIndex ];
},
fatal: function( strMessage ) {
strMessage += '\n\nLoading a random map...';
alert( strMessage );
this.reset();
return -1;
},
createElement: function( elParent, strTagName, hshAttributes ) {
var elNewNode = document.createElement( strTagName );
$H( hshAttributes ).each( function( aryValues ) {
elNewNode.setAttribute( aryValues.first(), aryValues.last() );
} );
// push references to the beginning of the nodes list so we delete children first
this.aryNodes.unshift( elNewNode );
elParent.appendChild( elNewNode );
return elNewNode;
},
generateColID: function( intRow, intCol ) {
return intRow + '-' + intCol;
},
findSquare: function( intRow, intCol ) {
return this.hshGameBoard[ this.generateColID( intRow, intCol ) ];
},
checkForWin: function() {
return $$( '.storage' ).all( function( el ) {
return el.hasClassName( 'ruby' );
} );
},
generate: function() {
var elCurrentTable = this.createElement( this.elBoardContainer, 'table', { 'id': 'jsokoban-board', 'cellspacing': '0' } );
var elCurrentTbody = this.createElement( elCurrentTable, 'tbody', {} );
var intRow = 0;
var context = this;
this.aryRows.each( function( strRow ) {
var intCol = 0;
var elCurrentTr = context.createElement( elCurrentTbody, 'tr', {} );
var aryCols = strRow.toArray();
if ( aryCols.length != context.intCols ) {
context.fatal( "Your map must form a box! This means the columns and rows may not be staggered." );
}
aryCols.each( function( strCol ) {
var strTileClasses = context.hshClassMap[ strCol ];
if ( strTileClasses != null ) {
var strCurrentId = context.generateColID( intRow, intCol );
var elCurrentTd = context.createElement( elCurrentTr, 'td', { 'id': strCurrentId, 'class': strTileClasses } );
context.hshGameBoard[ strCurrentId ] = new Piece( elCurrentTd );
} else {
context.fatal( "Your map may only contain valid tiles characters!" );
}
intCol++;
} );
intRow++;
} );
return this.hshGameBoard;
}
}
function Player( board ) {
this.board = board;
this.setup();
}
Player.prototype = {
setup: function() {
this.intPushes = 0;
this.intMoves = 0;
this.findPlayer();
},
reset: function() {
this.setup();
$( 'moves' ).innerHTML = this.intMoves;
$( 'pushes' ).innerHTML = this.intPushes;
},
findPlayer: function() {
var playerFound = $$( '.player' ).first();
if ( playerFound ) {
var aryCoords = playerFound.id.split( "-" );
this.row = parseInt( aryCoords.first() );
this.col = parseInt( aryCoords.last() );
}
},
withinBounds: function( intRow, intCol ) {
if ( ( intRow < 0 ) ||
( intRow > ( this.board.intRows - 1 ) ) ||
( intCol < 0 ) ||
( intCol > ( this.board.intCols - 1 ) ) ) {
return false;
}
return this.board.findSquare( intRow, intCol );
},
incrementMoves: function() {
this.intMoves++;
$( 'moves' ).innerHTML = this.intMoves;
},
incrementPushes: function() {
this.intPushes++;
$( 'pushes' ).innerHTML = this.intPushes;
},
checkForWin: function() {
if ( this.board.checkForWin() ) {
var strMsg = "Congratulations! You've managed to solve this puzzle!\n\n" +
"Press OK to try another, or hit Cancel to stay where you are.\n\n" +
"If you stay where you are, click 'Restart Map' and beat it with less pushes than before!";
if ( confirm( strMsg ) ) {
this.board.reset();
this.reset();
}
}
},
pushRuby: function( neighbors ) {
neighbors.near.removeClass( 'ruby' );
neighbors.far.addClass( 'ruby' );
this.incrementPushes();
},
updatePlayerPosition: function( neighbor ) {
this.board.findSquare( this.row, this.col ).removeClass( 'player' );
neighbor.addClass( 'player' );
this.row = neighbor.x;
this.col = neighbor.y;
this.incrementMoves();
},
move: function( strDirection ) {
var neighbors = this.neighbors( strDirection );
if ( !neighbors.near ) {
return false;
}
if ( neighbors.near.isBlocked() ) {
if ( ( neighbors.near.canBePushed() ) && ( !neighbors.far.isBlocked() ) ) {
this.pushRuby( neighbors );
this.updatePlayerPosition( neighbors.near );
this.checkForWin();
}
} else {
this.updatePlayerPosition( neighbors.near );
}
},
constructNeighborsHash: function( intNearRow, intNearCol, intFarRow, intFarCol ) {
return { near: this.withinBounds( this.row + intNearRow, this.col + intNearCol ), far: this.withinBounds( this.row + intFarRow, this.col + intFarCol ) };
},
neighbors: function( strDirection ) {
if ( strDirection == "left" ) {
return this.constructNeighborsHash( 0, -1, 0, -2 );
} else if ( strDirection == "right" ) {
return this.constructNeighborsHash( 0, 1, 0, 2 );
} else if ( strDirection == "up" ) {
return this.constructNeighborsHash( -1, 0, -2, 0 );
} else if ( strDirection == "down" ) {
return this.constructNeighborsHash( 1, 0, 2, 0 );
}
},
handleKeyPress: function( event ) {
var keyCode = event.keyCode;
if ( ( keyCode >= 37 ) || ( keyCode <= 40 ) ) {
// left 37
if ( keyCode == 37 ) {
this.move( "left" );
// up 38
} else if ( keyCode == 38 ) {
this.move( "up" );
// right 39
} else if ( keyCode == 39 ) {
this.move( "right" );
// down 40
} else if ( keyCode == 40 ) {
this.move( "down" );
}
}
},
handleKeyDepress: function( event ) {
}
};
function setupEventHandlers( player, board ) {
var hideMapEditor = function() {
$( 'edit-this-map' ).innerHTML = 'Edit This Map';
$( 'map-editor' ).hide();
}
Event.observe( $( 'edit-this-map' ), 'click', function( event ) {
var strLinkText = 'Edit This Map';
this.innerHTML = ( this.innerHTML == strLinkText ) ? 'Cancel Editing Map' : strLinkText;
$( 'jsokoban-board' ).toggle();
$( 'map-editor' ).toggle();
} );
Event.observe( $( 'load-map' ), 'click', function( event ) {
hideMapEditor();
board.reset( $( 'input-map' ).value.gsub( /\r/, '' ) );
player.reset();
} );
Event.observe( $( 'retry-map' ), 'click', function( event ) {
hideMapEditor();
board.reset( $( 'input-map' ).value.gsub( /\r/, '' ) );
player.reset();
} );
Event.observe( $( 'randomize-map' ), 'click', function( event ) {
event.preventDefault();
hideMapEditor();
board.reset();
player.reset();
} );
Event.observe( document, 'keydown', function( event ) {
event.preventDefault();
player.handleKeyPress( event );
} );
Event.observe( document, 'keyup', function( event ) {
player.handleKeyDepress( event );
} );
}
Objective
Push all of the rubies onto the yellow storage squares in as few moves as possible.
Instructions
Control the little man with the arrow keys and have him push the red rubies onto the yellow storage squares.
The green squares represent walls. Note that you are only able to push rubies, and if there is something behind them you will not be strong enough to push them.
Avoid getting rubies stuck in corners or against walls where you will not be able to get them out and the game will be left in a stalemate.
If you manage to get the game stuck in a stalemate, simply click the 'Random Map' link to load a new map at random, or 'Restart Map' to reload the map currently in play.
Using The Map Editor
At any time you can click the 'Edit This Map' link to pull up the map editor. This will allow you to simply modify the map you're currently on, or create your own from scratch.
The map editor only understands a few 'tile' characters which are shown below in double quotes:
" " - A blank space, represents an empty part of the floor.
"#" - Represents a wall.
"@" - Represets a player on the empty part of the floor..
"$" - Represents a ruby on an empty part of the floor.
"." - Represents an empty storage square.
"*" - Represents a storage square with a ruby on it.
"+" - Represents a storage square with a player on it.
Note that while you may use as many combinations of these tiles as you like, you must remember that your map has to have the same number of tiles on every line of the map or you will generate an error alert. That is, each row must contain the exact same number of columns as the row preceding it.
Have fun!