A Sliding Tile Puzzle in JavaScript

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.

SlidingTilePuzzle.png


<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>

This entry was posted in Uncategorized and tagged , , . Bookmark the permalink.

Leave a comment