Created
December 20, 2023 16:53
-
-
Save daveknights/556b0e56d4afbcd3b7d1b789c248266d to your computer and use it in GitHub Desktop.
Santa sliding puzzle.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
: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; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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