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> |