Skip to content

Instantly share code, notes, and snippets.

@brunnerh
Last active September 24, 2018 18:09
Show Gist options
  • Save brunnerh/2c4f6e01df945e77b931860a48bda218 to your computer and use it in GitHub Desktop.
Save brunnerh/2c4f6e01df945e77b931860a48bda218 to your computer and use it in GitHub Desktop.
coin-hexagon-puzzle
<!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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment