Simple, hacked together implementation of this coin hexagon puzzle i saw in a Numberphile video.
View it via RawGit.
Simple, hacked together implementation of this coin hexagon puzzle i saw in a Numberphile video.
View it via RawGit.
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Coin Hexagon Puzzle</title> | |
<style> | |
html, | |
body { | |
margin: 0; | |
padding: 0; | |
height: 100%; | |
width: 100%; | |
background: #333; | |
color: #EEE; | |
user-select: none; | |
} | |
a { | |
color: goldenrod; | |
} | |
a:visited { | |
color: darkgoldenrod; | |
} | |
body { | |
display: flex; | |
flex-direction: column; | |
align-items: center; | |
} | |
.coin-container { | |
position: relative; | |
flex: 1 1 auto; | |
} | |
.coin { | |
width: 100px; | |
height: 100px; | |
border-radius: 100px; | |
background: goldenrod; | |
border: 5px solid darkgoldenrod; | |
will-change: transform; | |
cursor: move; | |
transition: all ease-in-out 0.3s; | |
} | |
.coin:hover { | |
background: hsl(43, 74%, 59%); | |
border-color: hsl(43, 89%, 48%); | |
} | |
.coin.moving { | |
transition-property: background-color, border-color; | |
} | |
.coin.legal-move { | |
background: rgb(91, 218, 32); | |
} | |
.coin.illegal-move { | |
background: rgb(218, 85, 32); | |
} | |
</style> | |
</head> | |
<body> | |
<div class="text"> | |
<a href="https://www.youtube.com/watch?v=_pP_C7HEy3g"> | |
<h1>Coin Hexagon Puzzle</h1> | |
</a> | |
<p> | |
<b>Goal:</b> Create a hexagonal shape. | |
</p> | |
<p> | |
<b>Rules:</b> | |
<ul> | |
<li>3 coin moves are allowed.</li> | |
<li>Coins may not push other coins while moving.</li> | |
<li>A coin may only be moved to a location where it touches two other coins.</li> | |
</ul> | |
</p> | |
<p> | |
<b>Moves:</b> | |
<span class="moves"></span> | |
<button class="reset">Reset</button> | |
</p> | |
</div> | |
<div class="coin-container"> | |
</div> | |
<script> | |
// @ts-check | |
(() => | |
{ | |
const container = document.querySelector(".coin-container"); | |
const movesSpan = document.querySelector(".moves"); | |
const state = (() => | |
{ | |
let _moves; | |
return { | |
set moves(val) | |
{ | |
_moves = val; | |
movesSpan.textContent = val.toString(); | |
}, | |
get moves() { return _moves; } | |
}; | |
})(); | |
state.moves = 0; | |
const coinRadius = 50 + 5; | |
const shiftDown = Math.sqrt(Math.pow(2 * coinRadius, 2) - Math.pow(coinRadius, 2)); | |
const bounds = container.getBoundingClientRect(); | |
const left = bounds.width / 2; | |
const top = bounds.height / 2; | |
const initialPositions = [ | |
{ x: left, y: top }, | |
{ x: left + coinRadius * 2, y: top }, | |
{ x: left + coinRadius * 4, y: top }, | |
{ x: left + coinRadius, y: top + shiftDown }, | |
{ x: left + coinRadius * 3, y: top + shiftDown }, | |
{ x: left + coinRadius * 5, y: top + shiftDown }, | |
].map(point => | |
{ | |
// Center coins | |
point.x -= (coinRadius * 7) / 2; | |
point.y -= (shiftDown + 2 * coinRadius) / 2; | |
return point; | |
}); | |
let reset; | |
const coins = initialPositions.map(point => | |
{ | |
/** @typedef {{x:number, y:number}} Point */ | |
/** @typedef {HTMLDivElement & {position: Point, startPosition: Point, dragStartPosition: Point, dragStartNeighbors: Coin[], update: () => void, state?: string, dragging: boolean, adjacentCoins: () => Coin[] }} Coin */ | |
/** @type {Coin} */ | |
// @ts-ignore | |
const coin = container.appendChild(document.createElement("div")); | |
coin.classList.add("coin"); | |
coin.style.position = "absolute"; | |
coin.dragging = false; | |
coin.startPosition = point; | |
coin.position = { ...point }; | |
coin.update = () => | |
{ | |
coin.style.transform = `translate(${coin.position.x}px, ${coin.position.y}px)`; | |
coin.classList.toggle("moving", coin.dragging); | |
coin.classList.toggle("illegal-move", coin.dragging && coin.state == "illegal"); | |
coin.classList.toggle("legal-move", coin.dragging && coin.state != "illegal"); | |
}; | |
const distance = (point1, point2) => | |
Math.sqrt(Math.pow(point2.x - point1.x, 2) + Math.pow(point2.y - point1.y, 2)); | |
coin.adjacentCoins = () => coins.filter(c2 => c2 != coin && distance(coin.position, c2.position) <= (coinRadius * 2) + 2); | |
coin.addEventListener("pointerdown", e => | |
{ | |
coin.setPointerCapture(e.pointerId); | |
coin.dragging = true; | |
coin.dragStartPosition = { ...coin.position }; | |
coin.dragStartNeighbors = coin.adjacentCoins(); | |
}); | |
coin.addEventListener("pointerup", e => | |
{ | |
if (coin.dragging) | |
{ | |
coin.releasePointerCapture(e.pointerId); | |
coin.dragging = false; | |
if (coin.state == "illegal") | |
{ | |
coin.position = coin.dragStartPosition; | |
} | |
else | |
{ | |
const currentNeighbors = coin.adjacentCoins(); | |
if (currentNeighbors.length != coin.dragStartNeighbors.length || | |
currentNeighbors.some(c => coin.dragStartNeighbors.indexOf(c) < 0)) | |
state.moves++; | |
} | |
coin.state = null; | |
coin.update(); | |
} | |
if (coins.every(c => c.adjacentCoins().length == 2)) | |
{ | |
if (state.moves == 3) | |
{ | |
alert("Success!"); | |
} | |
else | |
{ | |
if (confirm("Well done, but too many moves. Retry?")) | |
reset(); | |
} | |
} | |
}); | |
coin.addEventListener("pointermove", e => | |
{ | |
if (coin.dragging) | |
{ | |
const targetDestination = { | |
x: coin.position.x + e.movementX, | |
y: coin.position.y + e.movementY | |
}; | |
if (coins.every(c2 => c2 == coin || distance(targetDestination, c2.position) >= (coinRadius * 2) - 1)) | |
{ | |
coin.position = targetDestination; | |
} | |
const touchesCount = coin.adjacentCoins().length; | |
coin.state = touchesCount < 2 ? "illegal" : null; | |
coin.update(); | |
} | |
}); | |
coin.update(); | |
return coin; | |
}); | |
reset = () => | |
{ | |
coins.forEach(c => | |
{ | |
c.position = { ...c.startPosition }; | |
c.update(); | |
}); | |
state.moves = 0; | |
}; | |
document.querySelector(".reset").addEventListener("click", reset); | |
})() | |
</script> | |
</body> | |
</html> |