A tower stacking game where every technical decision is slightly dumb.
Fun constraint: no
canvas, no in-game SVG, no text, no JS global state for the game. Everything you see is built with plaindivelements, CSS transforms, CSS animations and the state is all derived from the DOM structure and CSS selectors.
Each block is actually three div: a top, a right side and a left side. To make the whole thing look 3D, the entire stack is rotated with rotateX and rotateZ. Then each block is pushed "upward" using translateZ based on its position in the stack — block 0 sits at the bottom, block 5 floats higher up, and so on. And what's cool is that we have a bunch of CSS variables to control the size of the blocks, the rotation angles, the sliding distance, and the animation speed.
.block {
--z: calc(var(--i) * var(--stack-step));
transform: rotateX(var(--rotate-x)) rotateZ(var(--rotate-z))
translateX(calc(var(--ox) + var(--slide-x)))
translateY(calc(var(--oy) + var(--slide-y))) translateZ(var(--z));
}
.block .top {
inset: 0;
}
.block .front-right {
top: 100%;
height: var(--block-h);
transform-origin: top;
transform: rotateX(-90deg);
}
.block .front-left {
width: var(--block-h);
height: var(--bh);
transform-origin: left;
transform: rotateY(90deg);
}No grid elements, no SVG lines. The floor is a single div with two linear-gradient backgrounds stacked on top of each other, one draws the vertical lines, the other draws the horizontal ones. background-size controls the spacing.
.grid {
width: calc(var(--block-l) * 9);
height: calc(var(--block-w) * 9);
background-image:
linear-gradient(to right, rgba(255, 255, 255, 0.4) 2px, transparent 0px),
linear-gradient(to bottom, rgba(255, 255, 255, 0.4) 2px, transparent 0px);
background-size: var(--block-l) var(--block-w);
background-position: center center;
}Blocks alternate between sliding left-right and front-back. Instead of toggling this in JavaScript, the game uses CSS :has() to check whether the tower currently has an odd or even number of blocks, then applies the matching animation.
.tower:has(.block:last-child:nth-child(odd)) .block:first-child {
animation: first-block-y var(--block-slide-speed) ease-in-out infinite
alternate;
}
.tower:has(.block:last-child:nth-child(even)) .block:first-child {
animation: first-block-x var(--block-slide-speed) ease-in-out infinite
alternate;
}Android makes the vibration easy with navigator.vibrate(). But iOS doesn't support the Vibration API. iPhones vibrate when you tap a checkbox, so we simply dispatch a click event on a hidden checkbox to trigger the vibration.
const triggerHaptic = (isIOS, times = 1, durationMs = 0, delayMs = 0) => {
const trigger = document.getElementById("haptic-trigger");
if (isIOS) {
for (let pulse = 0; pulse < times; pulse += 1) {
setTimeout(() => {
trigger.dispatchEvent(new MouseEvent("click", { bubbles: true }));
}, pulse * delayMs);
}
return;
}
if ("vibrate" in navigator) {
const pattern = [];
for (let pulse = 0; pulse < times; pulse += 1)
pattern.push(durationMs, delayMs);
pattern.pop();
navigator.vibrate(pattern);
}
};The favicon updates to show your current score in a seven-segment display style
On the JS side every time the score changes, we swap the favicon URL to point at a PHP script with the new score.
const updateFavicon = (score) => {
const faviconLink = document.querySelector('link[rel="icon"]');
const faviconUrl = new URL("favicon.php", window.location.href);
faviconUrl.searchParams.set("score", String(Math.max(0, Math.floor(score))));
faviconLink.href = faviconUrl.toString();
};On the PHP side, the script reads the score, draws seven-segment digit shapes, and returns an SVG image on the fly.
<?php
header('Content-Type: image/svg+xml; charset=UTF-8');
header('Cache-Control: no-store, max-age=0');
$score = (string) (int) ($_GET['score'] ?? 0);
$digits = str_split($score);
echo '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 48">';
// render white seven-segment bars per digit
echo '</svg>';- GitHub Pages: Just fork this repo and enable GitHub Pages in the repository settings. Note that the dynamic favicon generation won't work since GitHub Pages doesn't support PHP.
- PHP Hosting: For full functionality, deploy to any hosting that supports PHP.
- Local: Use Docker to spin up a quick PHP server with the project directory mounted:
docker run -it --rm -p 8080:80 -v "$PWD":/var/www/html php:8.2-apacheContributions are welcome! If you'd like to improve the project, whether it's a weird CSS trick or just making it more absurd, feel free to open a PR.
This project is licensed under the MIT License - see the LICENSE file for details.
