The problem is that sometimes a wrong tile moves in a diagonal direction. This is a sliding puzzle. So tiles can move only horizontally or vertically.
Same tile cannot move in a row. For example if tile moves left, the next tile move cannot be to right. This is checked before swapping.
The goal is to move tile one time when a shuffle-button is clicked. First I check the neighbors next to the empty tile. Then I pick a random neighbor and swap it. I am not sure if I am swapping correctly or if there is an error in getNeighbors-function. Any advice?
let movesEl = document.getElementById("moves");
let emptyIndEl = document.querySelector(".empty-index");
let neighborEl = document.querySelector(".neighbor");
let dirEl = document.querySelector(".direction");
let neighborsEl = document.querySelector(".neighbors");
let curEmptyIndEl = document.querySelector(".new-empty-index");
let mapEl = document.querySelector(".new-map");
class Game {
constructor() {
this.parentEl = document.querySelector('#app');
this.puzzleRows = 3;
this.puzzleCols = 4;
this.width = 300;
this.height = 300;
this.cells = [];
this.map = [0,1,2,3,4,5,6,7,8,9,10,11];
this.shuffling = false;
this.moves = 0;
this.prevDir = null;
// events
this.onFinished = () => {};
this.onSwap = () => {};
}
init() {
this.el = this.createWrapper();
this.parentEl.appendChild(this.el);
this.parentEl.style.height = this.height + 'px';
this.setMap();
}
createWrapper() {
const div = document.createElement('div');
div.classList.add('puzzle-cells');
div.style.position = 'relative';
div.style.margin = ' 0 auto';
return div;
}
restart(){
this.stateMoves(0)
this.map = [0,1,2,3,4,5,6,7,8,9,10,11];
for(let i = 0; i < this.map.length; i++){
if(this.cells[i].index != this.map[i]){
this.swapCells(i, this.findPosition(this.map[i]))
}
}
}
stateMoves(num){
this.moves = num ;
movesEl.innerText = num;
}
setMap() {
for (let i = 0; i < this.puzzleRows * this.puzzleCols; i++) {
this.cells.push(new Cell(this, i));
}
for(let i = 0; i < this.map.length; i++){
if(this.cells[i].index != this.map[i]){
this.swapCells(i, this.findPosition(this.map[i]))
}
}
this.map = this.getMap();
}
shuffleTiles() {
// Find the index of the empty tile
const emptyIndex = this.findEmpty();
emptyIndEl.innerText = emptyIndex;
console.log('emptyIndex ', emptyIndex)
// Get the neighbors of the empty tile
// contains index and directions [10, 'left']
const neighbors = this.getNeighbors(emptyIndex);
// Select random neighbor data
const rand = Math.floor(Math.random() * neighbors.length);
let neighbor = neighbors[rand];
// Prevent moving same tile twice in a row
// Get new neighbor if directions matches
if(this.prevDir == neighbor[1]){
neighbors.splice(rand, 1);
const r = Math.floor(Math.random() * neighbors.length);
neighbor = neighbors[r];
}
neighborEl.innerText = neighbor[0];
console.log('selected neigbor ind', neighbor[0])
// Store direction
this.statePrevDir(neighbor[1]);
dirEl.innerText = this.prevDir;
console.log('prevDir ', this.prevDir)
neighborsEl.innerText = JSON.stringify(this.getNeighbors(neighbor[0]));
console.log('current neighbors', JSON.stringify(this.getNeighbors(neighbor[0])))
// Swap the empty tile with the selected neighbor
this.map[emptyIndex] = this.map[neighbor[0]];
this.map[neighbor[0]] = 11; // The empty tile now occupies the neighbor's position
curEmptyIndEl.innerText = this.map[emptyIndex];
console.log('swapp ', this.map[emptyIndex])
console.log('current emptyIndex ', this.map[emptyIndex])
// Update html
this.swapCells(this.map[emptyIndex], this.findPosition(this.map[neighbor[0]]))
// Update moves html
this.moves++;
this.stateMoves(this.moves);
mapEl.innerText = JSON.stringify(this.map);
console.log('updated map', JSON.stringify(this.map))
}
statePrevDir(dir){
// Store direction that cannot be used for the next move.
switch(dir){
case 'left':
this.prevDir = 'right';
break;
case 'right':
this.prevDir = 'left';
break;
case 'top':
this.prevDir = 'bottom';
break;
case 'bottom':
this.prevDir = 'top';
break;
}
}
getNeighbors(emptyIndex) {
// The grid
// 0 1 2
// 3 4 5
// 6 7 8
// 9 10 11
const adjacentPieces = [];
const row = Math.floor(emptyIndex / 3); // 0-3
const col = emptyIndex % 3; // 0-2
// Left neighbor
if (col > 0)
adjacentPieces.push([emptyIndex - 1, 'left']);
// Right neighbor
if (col < 2)
adjacentPieces.push([emptyIndex + 1, 'right']);
// Top neighbor
if (row > 0)
adjacentPieces.push([emptyIndex - 3, 'top']);
// Bottom neighbor
if (row < 3)
adjacentPieces.push([emptyIndex + 3, 'bottom']);
return adjacentPieces;
}
swapCells(i, j, animate) {
this.cells[i].setPosition(j, animate, i);
this.cells[j].setPosition(i);
[this.cells[i], this.cells[j]] = [this.cells[j], this.cells[i]];
}
getMap(){
const arr = [];
const list = [];
for(let i = 0; i < this.cells.length; i++){
const j = this.cells[i].index;
let obj = {i: i, j: j}
list.push(j);
arr.push(obj)
}
return list;
}
findEmptyIndexFromBottom(arr){
// Find empty index position
let index = '';
for(let i = this.dimmension - 1; i >= 0; i--){
for (let j = this.dimmension - 1; j >= 0; j--){
if(arr[i][j] == 0){
index = this.dimmension - i;
}
}
}
return index;
}
findPosition(ind) {
return this.cells.findIndex(cell => cell.index === ind);
}
findEmpty() {
return this.cells.findIndex(cell => cell.isEmpty);
}
}
// ============================
// CELL
//============================
class Cell {
constructor(puzzle, ind) {
this.isEmpty = false;
this.index = ind;
this.puzzle = puzzle;
this.width = this.puzzle.width / this.puzzle.puzzleRows;
this.height = this.puzzle.height / this.puzzle.puzzleCols;
this.el = this.createTile();
puzzle.el.appendChild(this.el);
if (this.index === this.puzzle.puzzleRows * this.puzzle.puzzleCols - 1) {
this.isEmpty = true;
return;
}
this.tileNum(this.index);
this.setPosition(this.index);
}
createTile() {
const div = document.createElement('div');
div.style.backgroundSize = `${Math.floor(this.puzzle.width)}px ${Math.floor(this.puzzle.height)}px`;
div.style.position = 'absolute';
div.classList.add('puzzle-block');
div.onclick = () => {
const currentCellIndex = this.puzzle.findPosition(this.index);
const emptyCellIndex = this.puzzle.findEmpty();
const {x, y} = this.getXY(currentCellIndex);
const {x: emptyX, y: emptyY} = this.getXY(emptyCellIndex);
if ((x === emptyX || y === emptyY) && (Math.abs(x - emptyX) === 1 || Math.abs(y - emptyY) === 1)) {
if (this.puzzle.onSwap && typeof this.puzzle.onSwap === 'function') {
this.puzzle.onSwap.call(this)
this.puzzle.moves++
this.puzzle.stateMoves(this.puzzle.moves);
}
this.puzzle.swapCells(currentCellIndex, emptyCellIndex, true);
}
};
return div;
}
tileNum(ind){
const {x, y} = this.getXY(this.index);
const left = Math.floor(this.width * x);
const top = Math.floor(this.height * y);
this.el.style.width = `${Math.floor(this.width - 3)}px`;
this.el.style.height = `${Math.floor(this.height - 3)}px`;
this.el.style.backgroundPosition = `-${left}px -${top}px`;
// number
const div = document.createElement("div");
div.classList.add("num");
div.style.width = `${Math.floor(this.width - 3)}px`;
div.style.height = `${Math.floor(this.height - 3)}px`;
div.innerText = ind;
this.el.appendChild(div);
}
setPosition(destinationIndex, animate, currentIndex) {
const {left, top} = this.getPositionFromIndex(destinationIndex);
const {left: currentLeft, top: currentTop} = this.getPositionFromIndex(currentIndex);
if (animate) {
if (left !== currentLeft) {
this.animate('left', currentLeft, left);
} else if (top !== currentTop) {
this.animate('top', currentTop, top);
}
} else {
this.el.style.left = `${Math.floor(left)}px`;
this.el.style.top = `${Math.floor(top)}px`;
}
}
animate(position, currentPosition, destination) {
const animationDuration = 50;
const frameRate = 5;
let step = frameRate * Math.abs((destination - currentPosition)) / animationDuration;
let id = setInterval(() => {
if (currentPosition < destination) {
currentPosition = Math.min(destination, currentPosition + step);
if (currentPosition >= destination) {
clearInterval(id)
}
} else {
currentPosition = Math.max(destination, currentPosition - step);
if (currentPosition <= destination) {
clearInterval(id)
}
}
this.el.style[position] = currentPosition + 'px';
}, frameRate)
}
getPositionFromIndex(index) {
const {x, y} = this.getXY(index);
return {
left: this.width * x,
top: this.height * y
}
}
getXY(index) {
return {
x: index % this.puzzle.puzzleRows,
y: Math.floor(index / this.puzzle.puzzleRows)
}
}
}
// Puzzle object
const game = new Game();
game.init();
document.querySelector('.restart').addEventListener('click', function(){
game.restart();
})
document.querySelector('.shuffle').onclick =()=>{
game.shuffleTiles();
}
body{
padding: 0;
margin: 0;
overflow: hidden;
}
.container{
padding-top: 10px;
display: flex;
justify-content: center;
gap: 15px;
}
.app{
width: 300px;
}
.num{
display: flex;
justify-content: center;
align-items: center;
background-color: #f2f2f2;
}
.footer{
display: flex;
justify-content: center;
flex-direction: column;
gap: 10px;
width: fit-content;
margin: 0 auto;
padding-top: 15px;
}
.values{
padding-top: 15px;
display: flex;
flex-direction: column;
gap: 5px;
width: fit-content;
margin: 0 auto;
justify-content: center;
}
.values span{
font-weight: bold;
text-transform: uppercase;
}
.values div{
display: flex;
flex-direction: column;
gap: 3px;
}
<div class="container">
<div id="app" class="app"></div>
</div>
<div class="footer">
<button class="shuffle">shuffle</button>
<button class="restart">restart</button>
<span id="moves"></span>
</div>
<div class="values">
<div>Empty index: <span class="empty-index"></span></div>
<div>Selected neighbor: <span class="neighbor"></span></div>
<div>Prevented direction: <span class="direction"></span></div>
<div>Current neighbors: <span class="neighbors"></span></div>
<div>Empty index after swap: <span class="new-empty-index"></span></div>
<div>Map after swap: <span class="new-map"></span></div>
</div>