Skip to content

Instantly share code, notes, and snippets.

@daveknights
Created December 20, 2023 16:53
Show Gist options
  • Save daveknights/556b0e56d4afbcd3b7d1b789c248266d to your computer and use it in GitHub Desktop.
Save daveknights/556b0e56d4afbcd3b7d1b789c248266d to your computer and use it in GitHub Desktop.
Santa sliding puzzle.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Sliding Santa Puzzle</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div class="container">
<p class="moves">Moves: <span class="move-count"></span></p>
<div class="puzzle-grid"></div>
<button class="start-puzzle">Shuffle to start</button>
<div class="message">Please complete the move</div>
</div>
<script src="script.js" type="module"></script>
</body>
</html>
import tileData from "./tileData.js";
const moveCount = document.querySelector('.move-count');
const puzzleGrid = document.querySelector('.puzzle-grid');
const startButton = document.querySelector('.start-puzzle');
const message = document.querySelector('.message');
const tiles = [];
let squaresArray = [...Array(8).keys()].map(x => ++x);
let playing = false;
let targetTile = null;
let moveComplete = true;
let blankSquare = '9';
let moving = false;
let moves = 0;
let mousePosition = {};
let offset = [];
let minX, maxX, minY, maxY, leftVal, topVal;
const appendTiles = () => {
let i = 0;
for (const tile of tiles) {
tile.className = ''
tile.className = `pos-${squaresArray[i]}`;
puzzleGrid.appendChild(tile);
i++;
}
};
const createTiles = () => {
for (const sq of squaresArray) {
const tile = document.createElement('div');
tile.id = `tile-${sq}`;
tiles.push(tile);
}
appendTiles();
}
const shuffle = () => {
moves = 0;
moveCount.textContent = moves;
playing = true;
startButton.classList.add('playing');
squaresArray = squaresArray
.map(value => ({ value, sort: Math.random() }))
.sort((a, b) => a.sort - b.sort)
.map(({ value }) => value).slice(0, squaresArray.length);
let inversions = 0;
// Had to look this bit up, calculates inversion value to sort the array
for(let i = 0; i < squaresArray.length - 1; i++) {
for(let j = i + 1; j < squaresArray.length; j++) {
if(squaresArray[i] > squaresArray[j]) inversions++;
}
}
// Check if puzzle is solvable
// Otherwise reshuffle
if (inversions % 2 === 1) {
shuffle();
} else {
puzzleGrid.innerHTML = '';
appendTiles();
}
}
const canMove = () => blankSquare in tileData[targetTile.className];
const dragTile = e => {
if (!targetTile) {
return;
}
const direction = tileData[targetTile.className][blankSquare].direction;
const currentStyles = window.getComputedStyle(targetTile);
minX = tileData[targetTile.className][blankSquare].minX;
maxX = tileData[targetTile.className][blankSquare].maxX;
minY = tileData[targetTile.className][blankSquare].minY;
maxY = tileData[targetTile.className][blankSquare].maxY;
leftVal = parseInt(currentStyles.getPropertyValue('left').replace('px', ''));
topVal = parseInt(currentStyles.getPropertyValue('top').replace('px', ''));
moving = true;
mousePosition = {
x : e.clientX,
y : e.clientY
};
if (direction === 'left' || direction === 'right') {
const currentXPos = mousePosition.x + offset[0];
if((leftVal > minX && leftVal > currentXPos) || (leftVal < maxX && leftVal < currentXPos)) {
targetTile.style.left = `${currentXPos}px`;
}
if (leftVal < minX) {
targetTile.style.left = `${minX}px`;
} else if (leftVal > maxX) {
targetTile.style.left = `${maxX}px`;
}
} else {
const currentYPos = mousePosition.y + offset[1];
if(topVal > minY && topVal > currentYPos || topVal < maxY && topVal < currentYPos) {
targetTile.style.top = `${currentYPos}px`;
}
if (topVal < minY) {
targetTile.style.top = `${minY}px`;
} else if (topVal > maxY) {
targetTile.style.top = `${maxY}px`;
}
}
};
const selectSquare = e => {
if (!playing || !e.target.className.startsWith('pos-')) return;
moveComplete && (targetTile = document.querySelector(`#${e.target.id}`));
if(canMove()) {
targetTile.addEventListener('mousemove', dragTile);
offset = [
targetTile.offsetLeft - e.clientX,
targetTile.offsetTop - e.clientY
];
} else {
targetTile = null;
}
};
const checkOrder = () => {
const tileOrder = document.querySelectorAll('.puzzle-grid div');
for (const tile of tileOrder) {
const tileId = parseInt(tile.id.replace('tile-', ''));
const tileClass = parseInt(tile.className.replace('pos-', ''));
if (tileId != tileClass) return;
};
playing = false;
startButton.classList.remove('playing');
squaresArray = [...Array(8).keys()].map(x => ++x)
};
const deselectSquare = () => {
if(moving === false && moveComplete === true) {
targetTile = null;
return;
}
if (targetTile) {
targetTile.removeEventListener('mousemove', dragTile);
message.classList.contains('show') && message.classList.remove('show');
if (leftVal === minX || leftVal === maxX || topVal === minY || topVal === maxY) {
const newPos = blankSquare;
blankSquare = targetTile.className.slice(-1);
targetTile.className = `pos-${newPos}`;
moveComplete = true;
moveCount.textContent = ++moves;
checkOrder();
} else {
moveComplete = false;
message.classList.add('show');
}
}
moving = false;
}
const init = () => {
createTiles();
startButton.addEventListener('click', shuffle);
puzzleGrid.addEventListener('mousedown', selectSquare);
puzzleGrid.addEventListener('mouseup', deselectSquare);
};
window.addEventListener('load', init);
:root {
--green: 44,88,51;
--red: #fe0000;
--tile-size: 60px;
--tile-offset: calc(var(--tile-size) / 2);
@media (min-width: 768px) {
--tile-size: 120px;
}
}
* {
box-sizing: border-box;
}
body {
display: flex;
font-family: Arial, Helvetica, sans-serif;
justify-content: center;
margin: 0;
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
}
.container {
align-items: center;
display: flex;
flex-direction: column;
max-width: 440px;
}
.moves {
color: rgb(var(--green));
font-size: 1.5rem;
font-weight: bold;
margin-bottom: 1rem;
}
.puzzle-grid {
border: solid 40px var(--red);
border-radius: 20px;
height: calc((var(--tile-size) * 3) + 80px);
margin-bottom: 10px;
position: relative;
width: calc((var(--tile-size) * 3) + 80px);
}
.puzzle-grid > div {
background-image: url('./santa-claus-dt.webp');
background-repeat: no-repeat;
border: solid 1px #eee;
cursor: pointer;
height: var(--tile-size);
position: absolute;
width: var(--tile-size);
}
#tile-1 {
background-position: left top;
}
#tile-2 {
background-position: center top;
}
#tile-3 {
background-position: right top;
}
#tile-4 {
background-position: left center;
}
#tile-5 {
background-position: center;
}
#tile-6 {
background-position: right center;
}
#tile-7 {
background-position: left bottom;
}
#tile-8 {
background-position: center bottom;
}
.pos-1 {
left: 0;
top: 0;
}
.pos-2 {
left: calc(50% - var(--tile-offset));
top: 0;
}
.pos-3 {
right: 0;
top: 0;
}
.pos-4 {
left: 0;
top: calc(50% - var(--tile-offset));
}
.pos-5 {
left: calc(50% - var(--tile-offset));
top: calc(50% - var(--tile-offset));
}
.pos-6 {
right: 0;
top: calc(50% - var(--tile-offset));;
}
.pos-7 {
bottom: 0;
left: 0;
}
.pos-8 {
bottom: 0;
left: calc(50% - var(--tile-offset));
}
.pos-9 {
bottom: 0;
right: 0;
}
.start-puzzle {
background: rgba(var(--green), 0.2);
border: solid 2px rgb(var(--green));
border-radius: 5px;
box-shadow: 0 1px 7px #aaa;
color: rgb(var(--green));
cursor: pointer;
font-size: 1.25rem;
font-weight: bold;
letter-spacing: 0.5px;
padding-block: 10px;
width: 240px;
}
.start-puzzle:focus {
box-shadow: none;
font-size: 1.2rem;
height: 50px;
width: 230px;
}
.message {
background: rgba(var(--green), 0.2);
border: solid 2px rgb(var(--green));
border-radius: 5px;
color: var(--red);
display: none;
font-size: 1.75rem;
padding: 20px;
text-align: center;
width: 100%;
}
.show {
display: block;
}
.playing {
display: none;
}
const tileData = {
'pos-1': {
'2': {
direction: 'right',
maxX: 120,
},
'4': {
direction: 'down',
maxY: 120,
},
},
'pos-2': {
'1': {
direction: 'left',
minX: 0,
},
'3': {
direction: 'right',
maxX: 240,
},
'5': {
direction: 'down',
maxY: 120,
}
},
'pos-3': {
'2': {
direction: 'left',
minX: 120,
},
'6': {
direction: 'down',
maxY: 120,
},
},
'pos-4': {
'1': {
direction: 'up',
minY: 0,
},
'5': {
direction: 'right',
maxX: 120,
},
'7': {
direction: 'down',
maxY: 240,
}
},
'pos-5': {
'2': {
direction: 'up',
minY: 0,
},
'4': {
direction: 'left',
minX: 0,
},
'6': {
direction: 'right',
maxX: 240,
},
'8': {
direction: 'down',
maxY: 240,
}
},
'pos-6': {
'3': {
direction: 'up',
minY: 0,
},
'5': {
direction: 'left',
minX: 120,
},
'9': {
direction: 'down',
maxY: 240,
}
},
'pos-7': {
'4': {
direction: 'up',
minY: 120,
},
'8': {
direction: 'right',
maxX: 120,
},
},
'pos-8': {
'5': {
direction: 'up',
minY: 120,
},
'7': {
direction: 'left',
minX: 0,
},
'9': {
direction: 'right',
maxX: 240,
}
},
'pos-9': {
'6': {
direction: 'up',
minY: 120,
},
'8': {
direction: 'left',
minX: 120,
},
},
}
export default tileData;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment