How to make a solvable slide puzzle in HTML?

Question: So yesterday I was thinking, why not make a slide puzzle game in HTML (with no <canvas>es)? So I picked the result image that shows when it’s complete, I made the tiles sort randomly, now the only thing is left: Solving, which I thought was easy, but… Wasn’t. So I’m asking your help and, how can one program such tricky thing?

Repl link: https://replit.com/@RixTheTyrunt/games?v=1

<!DOCTYPE html>
<html>
	<head>
		<meta name="viewport" content="width=device-width">
		<title>Games</title>
		<link href="/futureDarkness.css" rel="stylesheet">
		<style>
			div.puzzle {
				display: inline-block;
				border: 4px outset #c0c0c0;
				border-radius: 8px;
				background-color: #404040;
				padding: 16px 16px 0;
			}

			div.puzzle div.pictures {
				display: inline-block;
				border: 2px inset #c0c0c0;
				border-radius: 4px;
				background-color: #404040;
				padding: 4px 4px 0;
			}

			div.puzzle div.pictures div {
				margin-top: -4px;
			}

			div.puzzle div.pictures div:first-child {
				margin: 0;
			}
		</style>
	</head>
	<body>
		<h1><small><a href="/">Games</a> &gt;&nbsp;</small>Slide puzzle</h1>
		<p><strong>INSTRUCTIONS</strong>: Tap to switch tiles between the selected tile and the empty one</p>
		<center>
			<div class="puzzle">
				<div class="pictures">
					<div><img src="1.png"><img src="2.png"><img src="empty.png"></div>
					<div><img src="3.png"><img src="4.png"><img src="5.png"></div>
					<div><img src="6.png"><img src="7.png"><img src="8.png"></div>
				</div>
				<p><i>Slide puzzle by <a href="https://pokeheroes.com/userprofile?name=RixTheTyrunt">@RixTheTyrunt</a><br>Picture by <a href="https://pokeheroes.com/userprofile?name=RoarArtist">@RoarArtist</a></i></p>
			</div>
		</center>
		<script src="https://peanut.rixthetyrunt.repl.co/0.1.0/peanut.js"></script>
		<script>
correctTiles = [["1", "2", "empty"], ["3", "4", "5"], ["6", "7", "8"]]
function genTiles() {
	var tiles = correctTiles
	var count = 0
	while (count < 3) {
		tiles[count] = tiles[count].sort(function() {return Math.random() - 0.5})
		count += 1
	}
	tiles = tiles.sort(function() {return Math.random() - 0.5})
	return tiles
}
newTiles = genTiles()

for (window.count in newTiles) {
	document.querySelector("div.pictures div:nth-child(" + (parseInt(window.count) + 1) + ")").innerHTML = ""
	for (window.tileNum in newTiles[window.count]) {
		document.querySelector("div.pictures div:nth-child(" + (parseInt(window.count) + 1) + ")").appendChild(document.createElement("img"))
		document.querySelector("div.pictures div:nth-child(" + (parseInt(window.count) + 1) + ") img:nth-child(" + (parseInt(window.tileNum) + 1) + ")").setAttribute("src", newTiles[window.count][window.tileNum] + ".png")
	}
}
delete window.count
delete window.tileNum
		</script>
	</body>
</html>

You seem to manage rearranging the tiles fine, just switch the positions in the array based on a click event (make sure the tile can slide into the empty space) and then rearrange the tiles again.

Why not set an id to each tile and acces them with respective ids instead?
Also, to get the position of tile, you may assign them a class like pos, like this:

for (window.tileNum in newTiles[window.count]) {
    const el = document.querySelector("div.pictures div:nth-child(" + (parseInt(window.count) + 1) + ") img:nth-child(" + (parseInt(window.tileNum) + 1) + ")");
    
    el.appendChild(document.createElement("img"))
    el.setAttribute("src", newTiles[window.count][window.tileNum] + ".png")
    el.setAttribute("class", "pos")
	}

Then make an array to know the position of elements:

var newPos = [];
function getPos() {
   for(let k=1; k<4; k++) {
     if(newPos.length==9) var newPos=[];
     let useless = [];

     for(let i=1; i<4; i++) {
useless.push(document.getElementsByClassName("pos")[i*k-1].getAttribute("src").slice(0,-4));
      }
     newPos.push(useless);
   }
}

Call this function everytime someone presses the key to change position, (or slides if you’re making it mobile friendly). Then just check for the tiles and when if(newPos==correctTiles), you win :trophy:!!

1 Like

Heya, @Pro0grammer!

I replaced it so that it would use while instead of for and also made it support Peanut, I got:

newPos = []
function getPos() {
	var count1 = 0
	while (count1 < 4) {
		if (newPos.length == 9) {
			newPos = []
		}
		var useless = []
		var count2 = 0
		while (count2 < 4) {
			useless.push(peanut().dom.getFromClass("tile")[count2 * count1 - 1].getAttribute("src").slice(0, -4))
			count2 += 1
		}
		newPos.push(useless)
		count1 += 1
	}
}

But when I click a tile nearby the empty tile, it throws me an error:

Uncaught TypeError: Cannot read properties of undefined (reading 'getAttribute')
	at HTMLImageElement.getPos ((index):84:71)

You forgot to add pos class to each image element as I stated. Like:
<img src="your png" class="pos"></img>

I changed it so that the class would be tile instead of pos (because it would lack readability or something), here’s my full JavaScript:

correctTiles = [["1", "2", "empty"], ["3", "4", "5"], ["6", "7", "8"]]
function genTiles() {
	var tiles = correctTiles
	var count = 0
	while (count < 3) {
		tiles[count] = tiles[count].sort(function() {return Math.random() - 0.5})
		count += 1
	}
	tiles = tiles.sort(function() {return Math.random() - 0.5})
	return tiles
}
newTiles = genTiles()

for (window.count in newTiles) {
	document.querySelector("div.pictures div:nth-child(" + (parseInt(window.count) + 1) + ")").innerHTML = ""
	for (window.tileNum in newTiles[window.count]) {
		document.querySelector("div.pictures div:nth-child(" + (parseInt(window.count) + 1) + ")").appendChild(document.createElement("img"))
		document.querySelector("div.pictures div:nth-child(" + (parseInt(window.count) + 1) + ") img:nth-child(" + (parseInt(window.tileNum) + 1) + ")").setAttribute("class", "tile")
		window.tileElems = peanut().dom.getFromClass("tile")
		window.tileElems[window.tileElems.length - 1].setAttribute("src", newTiles[window.count][window.tileNum] + ".png")
	}
}
delete window.count
delete window.tileNum
delete window.tileElems

newPos = []
function getPos() {
	var count1 = 0
	while (count1 < 4) {
		if (newPos.length == 9) {
			newPos = []
		}
		var useless = []
		var count2 = 0
		while (count2 < 4) {
			useless.push(peanut().dom.getFromClass("tile")[count2 * count1 - 1].getAttribute("src").slice(0, -4))
			count2 += 1
		}
		newPos.push(useless)
		count1 += 1
	}
}

for (tile of peanut().dom.getFromClass("tile")) {
	tile.addEventListener("mouseup", getPos)
}

Then you might need to add class tile.

Please inform me if it still does not work.

But I did add the line to add the class:

document.querySelector("div.pictures div:nth-child(" + (parseInt(window.count) + 1) + ") img:nth-child(" + (parseInt(window.tileNum) + 1) + ")").setAttribute("class", "tile")

Let me check your code, a min.

OHH, I get it now! I accidentally set the count variables to 0 instead of 1, I’ll fix it now

It shouldn’t have created that error though, does the error disappear after that?

It does! But it doesn’t change the shown tiles…

Since it’s a while...loop and not for...loop, I believe that you should set values to 0 first and condition should be replaced by 3. I mean:

var count1 = 0;
	while (count1 < 3) ....

@RixTheTyrunt, Does this solve your issue?

I fixed the issue myself. The only thing left is showing it to the user

As you said earlier, I added an event listener that whenever the user clicks it executes getPos
Here’s how I did it:

for (tile of peanut().dom.getFromClass("tile")) {
	tile.addEventListener("mouseup", getPos)
}

I also added an if (newPos == correctTiles) statement after the while (count1 < 4) loop, and it executes peanut().dialogs.notify("You won!")

Mouse up works but it would be complex and you still need to add support for mobile differently, I would rather recommend adding a support for keydown function. When user presses arrow key, you check for the position fo empty tile and update it accordingly. Just my personal opinion, whatever works for you is fine :smiley:

It’s alrighty, you can show me code for how would you implement it, I’ll do the same, just for mouseup events :sunglasses:

//When you need to move upwards
function upKeyPressed() {
   
   //User tried moving up when empty tile was in 3rd row, just ignore em
   if(!newPos[2].contains("empty")){
      if(newPos[1].contains("empty"){
         const emptyPos = newPos[1].indexOf("empty");
         newPos[1][emptyPos] = newPos[2][emptyPos];
         newPos[2][emptyPos] = "empty";

         document.getElementsByClassName("tile")[6+emptyPos].setAttribute("src", (6+emptyPos)+".png" );
         document.getElementsByClassName("tile")[3+emptyPos].setAttribute("src", "empty.png");
      } else{
         const emptyPos = newPos[0].indexOf("empty");
         newPos[0][emptyPos] = newPos[1][emptyPos];
         newPos[1][emptyPos] = "empty";

         document.getElementsByClassName("tile")[emptyPos].setAttribute("src", (emptyPos)+".png" );
         document.getElementsByClassName("tile")[3+emptyPos].setAttribute("src", "empty.png");
      }
   }
}

This is when user presses up key, you can do similar for other keys by imagining positions :+1:

1 Like

My code for the getPos function and a keypress event are here:

newPos = []
function getPos() {
	var count1 = 1
	while (count1 < 4) {
		if (newPos.length == 9) {
			newPos = []
		}
		var useless = []
		var count2 = 1
		while (count2 < 4) {
			useless.push(peanut().dom.getFromClass("tile")[count2 * count1 - 1].getAttribute("src").slice(0, -4))
			count2 += 1
		}
		newPos.push(useless)
		count1 += 1
	}
}

document.addEventListener("keypress", function() {
	getPos()
	if (!newPos[2].contains("empty")) {
		if (newPos[1].contains("empty")) {
			const emptyPos = newPos[1].indexOf("empty")
			newPos[1][emptyPos] = newPos[2][emptyPos]
			newPos[2][emptyPos] = "empty"
			peanut().getFromClass("tile")[6 + emptyPos].setAttribute("src", (6 + emptyPos) + ".png")
			peanut().getFromClass("tile")[3 + emptyPos].setAttribute("src", "empty.png")
		} else {
			var emptyPos = newPos[0].indexOf("empty")
			newPos[0][emptyPos] = newPos[1][emptyPos]
			newPos[1][emptyPos] = "empty"
			peanut().getFromClass("tile")[emptyPos].setAttribute("src", (emptyPos) + ".png")
			peanut().getFromClass("tile")[3 + emptyPos].setAttribute("src", "empty.png")
		}
	}
	if (newPos == correctTiles) {
		peanut().dialogs.notify("You won!")
	}
})

Is this right? Because it does nothing