The code below implements a simple sliding tile puzzle in JavaScript. To see it in action, copy it into an .html file and open that file in a web browser that runs JavaScript.
Use the arrow keys to move the cursor around and the Enter key to swap the empty cell with the selected neighbor. The goal is to put the tiles in the correct order from their randomized starting positions, though currently the program doesn’t detect the victory condition anyway.
<html> <body> <script type="text/javascript"> // main function main() { var display = new Display ( new Coords(100, 100), // sizeInPixels 10, // fontHeightInPixels "White", // colorBack "Gray" // colorFore ); var grid = new Grid ( new Coords(4, 4) ).randomize(); Globals.Instance.initialize ( display, grid ); } // 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.divide = function(other) { this.x /= other.x; this.y /= other.y; return this; } Coords.prototype.divideScalar = function(scalar) { this.x /= scalar; this.y /= scalar; return this; } Coords.prototype.clone = function() { return new Coords(this.x, this.y); } Coords.prototype.multiply = function(other) { this.x *= other.x; this.y *= other.y; return this; } Coords.prototype.magnitude = function() { return Math.sqrt(this.x * this.x + this.y * this.y); } Coords.prototype.overwriteWith = function(other) { this.x = other.x; this.y = other.y; return this; } Coords.prototype.subtract = function(other) { this.x -= other.x; this.y -= other.y; return this; } Coords.prototype.trimToRangeMax = function(max) { if (this.x < 0) { this.x = 0; } else if (this.x > max.x) { this.x = max.x; } if (this.y < 0) { this.y = 0; } else if (this.y > max.y) { this.y = max.y; } return this; } } function Display(sizeInPixels, fontHeightInPixels, colorBack, colorFore) { this.sizeInPixels = sizeInPixels; this.fontHeightInPixels = fontHeightInPixels; this.colorBack = colorBack; this.colorFore = colorFore; } { Display.prototype.initialize = function() { var canvas = document.createElement("canvas"); canvas.width = this.sizeInPixels.x; canvas.height = this.sizeInPixels.y; this.graphics = canvas.getContext("2d"); this.graphics.font = this.fontHeightInPixels + "px sans-serif"; document.body.appendChild(canvas); } // drawing Display.prototype.clear = function() { this.drawRectangle ( new Coords(0, 0), this.sizeInPixels, this.colorBack, this.colorFore ); } Display.prototype.drawRectangle = function(pos, size, colorFill, colorBorder) { if (colorFill != null) { this.graphics.fillStyle = colorFill; this.graphics.fillRect ( pos.x, pos.y, size.x, size.y ); } if (colorBorder != null) { this.graphics.strokeStyle = colorBorder; this.graphics.strokeRect ( pos.x, pos.y, size.x, size.y ); } } Display.prototype.drawText = function(text, pos, color) { this.graphics.fillStyle = color; this.graphics.fillText ( text, pos.x, pos.y + this.fontHeightInPixels ); } } function Globals() { // do nothing } { Globals.Instance = new Globals(); Globals.prototype.initialize = function(display, grid) { this.display = display; this.grid = grid; this.display.initialize(); this.grid.drawToDisplay(this.display); this.inputHelper = new InputHelper(); this.inputHelper.initialize(); } } function Grid(sizeInCells) { this.sizeInCells = sizeInCells; this.cells = []; this.cursorPos = new Coords(0, 0); } { Grid.prototype.cellAtPosGet = function(cellPos) { var cellIndex = this.indexOfCellAtPos(cellPos); var cellValue = this.cells[cellIndex]; return cellValue } Grid.prototype.cellAtPosSet = function(cellPos, valueToSet) { var cellIndex = this.indexOfCellAtPos(cellPos); this.cells[cellIndex] = valueToSet; } Grid.prototype.cursorMove = function(direction) { this.cursorPos.add ( direction ).trimToRangeMax ( this.sizeInCells ); } Grid.prototype.indexOfCellAtPos = function(cellPos) { return cellPos.y * this.sizeInCells.x + cellPos.x; } Grid.prototype.openCellPos = function() { var cellPos = new Coords(); for (var y = 0; y < this.sizeInCells.y; y++) { cellPos.y = y; var cellValue = null; for (var x = 0; x < this.sizeInCells.x; x++) { cellPos.x = x; cellValue = this.cellAtPosGet(cellPos); if (cellValue == null) { break; } } if (cellValue == null) { break; } } return cellPos; } Grid.prototype.randomize = function() { var numberOfCells = this.sizeInCells.x * this.sizeInCells.y; for (var i = 0; i < numberOfCells; i++) { this.cells[i] = null; } for (var i = 0; i < numberOfCells - 1; i++) { var cellIndex = Math.floor ( Math.random() * numberOfCells ); while (this.cells[cellIndex] != null) { cellIndex++; if (cellIndex >= numberOfCells) { cellIndex = 0; } } this.cells[cellIndex] = i; } return this; } Grid.prototype.slideAtCursorIfPossible = function() { var openCellPos = this.openCellPos(); var displacement = openCellPos.clone().subtract ( this.cursorPos ); var distance = displacement.magnitude(); if (distance == 1) { var cellValueToSlide = this.cellAtPosGet(this.cursorPos); this.cellAtPosSet(this.cursorPos, null); this.cellAtPosSet(openCellPos, cellValueToSlide); } } // drawable Grid.prototype.drawToDisplay = function(display) { var cellSizeInPixels = display.sizeInPixels.clone().divide ( this.sizeInCells ); var cellSizeInPixelsHalf = cellSizeInPixels.clone().divideScalar(2); var cellPos = new Coords(); var drawPos = new Coords(); var cellInde; var cellValue; for (var y = 0; y < this.sizeInCells.y; y++) { cellPos.y = y; for (var x = 0; x < this.sizeInCells.x; x++) { cellPos.x = x; cellValue = this.cellAtPosGet(cellPos); if (cellValue == null) { cellValue = ""; } drawPos.overwriteWith ( cellPos ).multiply ( cellSizeInPixels ); display.drawRectangle ( drawPos, cellSizeInPixels, display.colorBack, // fill display.colorFore // border ); drawPos.add ( cellSizeInPixelsHalf ); display.drawText ( "" + cellValue, drawPos, display.colorFore ); } } drawPos.overwriteWith ( this.cursorPos ).multiply ( cellSizeInPixels ); display.drawRectangle ( drawPos, cellSizeInPixels, display.colorFore, // fill display.colorBack // border ); drawPos.add ( cellSizeInPixelsHalf ); cellValue = this.cellAtPosGet(this.cursorPos); if (cellValue == null) { cellValue = ""; } display.drawText ( "" + cellValue, drawPos, display.colorBack ); } } function InputHelper() { // do nothing } { InputHelper.prototype.initialize = function() { document.body.onkeydown = this.handleEventKeyDown.bind(this); } // events InputHelper.prototype.handleEventKeyDown = function(event) { var keyPressed = event.key; var grid = Globals.Instance.grid; if (keyPressed == "ArrowDown") { grid.cursorMove(new Coords(0, 1)); } else if (keyPressed == "ArrowLeft") { grid.cursorMove(new Coords(-1, 0)); } else if (keyPressed == "ArrowRight") { grid.cursorMove(new Coords(1, 0)); } else if (keyPressed == "ArrowUp") { grid.cursorMove(new Coords(0, -1)); } else if (keyPressed == "Enter") { grid.slideAtCursorIfPossible(); } grid.drawToDisplay(Globals.Instance.display); } } // run main(); </script> </body> </html>