The code below implements a simple “Sokoban” game in JavaScript. To see it in action, copy it into an .html file and open that file in a web browser that runs JavaScript. Or, for an online version, visit https://thiscouldbebetter.neocities.org/sokoban.html
The player is represented by the cyan square, and the gray squares represent walls. The player can be moved around with the W, A, S, and D keys. The goal of the game is to push the sliders (brown squares) onto the targets (green squares). The sliders can only be pushed, not pulled, and they can only move if there’s not a wall or another slider in the way. If a slider gets pushed against the outside wall, it’s stuck there, since the player can’t get behind it to push it off. For more information about Sokoban, see its Wikipedia article.
<html> <body> <div id="divMain"></div> <script type="text/javascript"> // main function SlidingPuzzleGame() { this.main = function() { var mapTerrains = [ // name, symbol, color, isPassable, isMovable new MapTerrain("Goal", "+", "LightGreen", true, false), new MapTerrain("Floor", ".", "Black", true, false), new MapTerrain("Player", "p", "Cyan", false, true), new MapTerrain("Slider", "o", "Brown", false, true), new MapTerrain("Wall", "x", "LightGray", false, false), ]; var level0 = new Level ( "Level 0", new Map ( mapTerrains, [ "xxxxxxxxxxxx", "x..........x", "x.p........x", "x.....x....x", "x.....x..+.x", "x.....x....x", "x..o..x..+.x", "x.....x....x", "x..o..x....x", "x..........x", "x..........x", "xxxxxxxxxxxx", ] ) ); Globals.Instance.initialize ( new Coords(120, 120), // viewSizeInPixels level0 ); } } // extensions function ArrayExtensions() { // do nothing } { Array.prototype.addLookups = function(keyName) { for (var i = 0; i < this.length; i++) { var item = this[i]; var key = item[keyName]; this[key] = item; } } } // classes function Coords(x, y) { this.x = x; this.y = y; } { Coords.prototype.add = function(other) { this.x += other.x; this.y += other.y; return this; } Coords.prototype.clone = function() { return new Coords(this.x, this.y); } Coords.prototype.divide = function(other) { this.x /= other.x; this.y /= other.y; return this; } Coords.prototype.equals = function(other) { var returnValue = ( this.x == other.x && this.y == other.y ); return returnValue; } Coords.prototype.multiply = function(other) { this.x *= other.x; this.y *= other.y; return this; } Coords.prototype.overwriteWith = function(other) { this.x = other.x; this.y = other.y; return this; } } function DisplayHelper() { // do nothing } { DisplayHelper.prototype.clear = function() { this.graphics.fillStyle = "White"; this.graphics.fillRect ( 0, 0, this.viewSizeInPixels.x, this.viewSizeInPixels.y ); this.graphics.strokeStyle = "Gray"; this.graphics.strokeRect ( 0, 0, this.viewSizeInPixels.x, this.viewSizeInPixels.y ); } DisplayHelper.prototype.drawLevel = function(level) { this.clear(); this.drawMap(level.map); } DisplayHelper.prototype.drawMap = function(map) { var mapSizeInCells = map.sizeInCells; var mapCellSizeInPixels = this.viewSizeInPixels.clone().divide ( mapSizeInCells ); var cellPos = new Coords(0, 0); var drawPos = new Coords(0, 0); for (var y = 0; y < mapSizeInCells.y; y++) { cellPos.y = y; for (var x = 0; x < mapSizeInCells.x; x++) { cellPos.x = x; var cellToDraw = map.cellAtPos(cellPos); if (cellToDraw.color != null) { drawPos.overwriteWith ( cellPos ).multiply ( mapCellSizeInPixels ); this.graphics.fillStyle = cellToDraw.color; this.graphics.fillRect ( drawPos.x, drawPos.y, mapCellSizeInPixels.x, mapCellSizeInPixels.y ); } } } var colorSlider = map.terrains["Slider"].color; for (var i = 0; i < map.sliders.length; i++) { var slider = map.sliders[i]; drawPos.overwriteWith ( slider.pos ).multiply ( mapCellSizeInPixels ); this.graphics.fillStyle = colorSlider; this.graphics.fillRect ( drawPos.x, drawPos.y, mapCellSizeInPixels.x, mapCellSizeInPixels.y ); } var colorPlayer = map.terrains["Player"].color; drawPos.overwriteWith ( map.player.pos ).multiply ( mapCellSizeInPixels ); this.graphics.fillStyle = colorPlayer; this.graphics.fillRect ( drawPos.x, drawPos.y, mapCellSizeInPixels.x, mapCellSizeInPixels.y ); } DisplayHelper.prototype.initialize = function(viewSizeInPixels) { this.viewSizeInPixels = viewSizeInPixels; var canvas = document.createElement("canvas"); canvas.width = viewSizeInPixels.x; canvas.height = viewSizeInPixels.y; this.graphics = canvas.getContext("2d"); var divMain = document.getElementById("divMain"); divMain.appendChild(canvas); } } function Globals() { // do nothing } { Globals.Instance = new Globals(); Globals.prototype.initialize = function(viewSizeInPixels, level) { this.level = level; this.displayHelper = new DisplayHelper(); this.displayHelper.initialize(viewSizeInPixels); this.timer = setInterval ( this.handleEventTimerTick.bind(this), 150 // millisecondsPerTimerTick ); this.inputHelper = new InputHelper(); this.inputHelper.initialize(); } // events Globals.prototype.handleEventTimerTick = function() { this.level.updateForTimerTick(); } } function InputHelper() { // do nothing } { InputHelper.prototype.finalize = function() { this.keyCodePressed = null; document.body.onkeydown = null; document.body.onkeyup = null; } InputHelper.prototype.initialize = function() { document.body.onkeydown = this.handleEventKeyDown.bind(this); document.body.onkeyup = this.handleEventKeyUp.bind(this); } // events InputHelper.prototype.handleEventKeyDown = function(event) { this.keyCodePressed = event.keyCode; } InputHelper.prototype.handleEventKeyUp = function(event) { this.keyCodePressed = null; } } function Level(name, map) { this.name = name; this.map = map; } { Level.prototype.updateForTimerTick = function() { Globals.Instance.displayHelper.drawLevel(this); var inputHelper = Globals.Instance.inputHelper; if (inputHelper.keyCodePressed == 65) // a { this.updateForTimerTick_PlayerMove ( new Coords(-1, 0) ); } else if (inputHelper.keyCodePressed == 68) // d { this.updateForTimerTick_PlayerMove ( new Coords(1, 0) ); } else if (inputHelper.keyCodePressed == 83) // s { this.updateForTimerTick_PlayerMove ( new Coords(0, 1) ); } else if (inputHelper.keyCodePressed == 87) // w { this.updateForTimerTick_PlayerMove ( new Coords(0, -1) ); } } Level.prototype.updateForTimerTick_PlayerMove = function(directionToMove) { var playerToMove = this.map.player; var playerPosNext = playerToMove.pos.clone().add ( directionToMove ); var map = this.map; var cellAtPlayerPosNext = map.cellAtPos(playerPosNext); if (cellAtPlayerPosNext.isPassable == true) { var sliderAtPlayerPosNext = map.sliderAtPos(playerPosNext); if (sliderAtPlayerPosNext == null) { playerToMove.pos.add(directionToMove); } else { var canSliderSlide = true; var sliderPosNext = playerPosNext.clone().add ( directionToMove ); var cellAtSliderPosNext = map.cellAtPos(sliderPosNext); if (cellAtSliderPosNext.isPassable == false) { canSliderSlide = false; } else { var sliderOtherAtSliderPosNext = this.map.sliderAtPos ( sliderPosNext ); if (sliderOtherAtSliderPosNext != null) { canSliderSlide = false; } } if (canSliderSlide == true) { playerToMove.pos.add(directionToMove); sliderAtPlayerPosNext.pos.add(directionToMove); if (cellAtSliderPosNext.name == "Goal") { this.victoryCheck(); } } } } } Level.prototype.victoryCheck = function() { var areAllGoalCellsOccupiedBySliders = true; var map = this.map; var terrainGoal = map.terrains["Goal"]; var cellPos = new Coords(0, 0); for (var y = 0; y < map.sizeInCells.y; y++) { cellPos.y = y; for (var x = 0; x < map.sizeInCells.x; x++) { cellPos.x = x; var terrainAtPos = map.cellAtPos(cellPos); if (terrainAtPos == terrainGoal) { var sliderAtPos = map.sliderAtPos(cellPos); if (sliderAtPos == null) { areAllGoalCellsOccupiedBySliders = false; y = map.sizeInCells.y; break; } } } } if (areAllGoalCellsOccupiedBySliders == true) { Globals.Instance.inputHelper.finalize(); Globals.Instance.displayHelper.drawLevel(this); var divWinMessage = document.createElement("div"); divWinMessage.innerHTML = "You win!"; document.body.appendChild(divWinMessage); } } } function Map(terrains, cellsAsStrings) { this.terrains = terrains; this.terrains.addLookups("name"); this.terrains.addLookups("symbol"); this.cellsAsStrings = cellsAsStrings; this.sizeInCells = new Coords ( this.cellsAsStrings[0].length, this.cellsAsStrings.length ); this.movablesInitialize(); } { Map.prototype.cellAtPos = function(cellPos) { var terrainSymbol = this.cellsAsStrings[cellPos.y].charAt(cellPos.x); var terrain = this.terrains[terrainSymbol]; return terrain; } Map.prototype.movablesInitialize = function() { this.sliders = []; var terrainSymbolForFloor = this.terrains["Floor"].symbol; var cellPos = new Coords(0, 0); for (var y = 0; y < this.sizeInCells.y; y++) { cellPos.y = y; var cellRowAsString = this.cellsAsStrings[y]; for (var x = 0; x < this.sizeInCells.x; x++) { cellPos.x = x; var terrainSymbol = cellRowAsString.charAt(x); var terrain = this.terrains[terrainSymbol]; if (terrain.isMovable == true) { cellRowAsString = cellRowAsString.substr(0, x) + terrainSymbolForFloor + cellRowAsString.substr(x + 1) } if (terrain.name == "Player") { this.player = new Player(cellPos.clone()); } else if (terrain.name == "Slider") { var slider = new Slider(cellPos.clone()); this.sliders.push(slider); } } this.cellsAsStrings[y] = cellRowAsString; } } Map.prototype.sliderAtPos = function(cellPos) { var returnValue = null; for (var i = 0; i < this.sliders.length; i++) { var slider = this.sliders[i]; if (slider.pos.equals(cellPos) == true) { returnValue = slider; break; } } return returnValue; } } function MapTerrain(name, symbol, color, isPassable, isMovable) { this.name = name; this.symbol = symbol; this.color = color; this.isPassable = isPassable; this.isMovable = isMovable; } function Player(pos) { this.pos = pos; } function Slider(pos) { this.pos = pos; } // run new SlidingPuzzleGame().main(); </script> </body> </html>
Hello, I also coded a sokoban and try to explain how to do this in my blog. My code use text files for the leves and a engine I created to move the sokokban. I hope it will interest you.