🥧 Pie Stack 🦃

Tap, click or press Space to drop each pumpkin pie on the stack. Only the part that lands on the pie below stays, so time it well!
Pies: 0  |  Best: 0
Press "Start" and build the tallest Thanksgiving pie tower!

The Thanksgiving table is set and the pumpkin pies keep coming 🥧. Each pie slides from side to side above your tower: tap, click or press Space to drop it. Only the part that lands on the pie below stays, and the rest falls off, so every miss makes the next pie smaller. Land it right on top for a perfect drop and a dollop of whipped cream. The pies speed up as the tower grows, and the game ends when a pie misses the stack completely.

How many pies can you stack before dinner is served?

Here is the code
CSS
.pie-game {
    text-align: center;
}

.pie-game .instructions {
    max-width: 500px;
    margin: 0 auto 15px;
    font-size: 1em;
    color: #555;
}

.pie-info {
    margin-bottom: 10px;
    font-size: 1.1em;
}

.pie-canvas {
    display: block;
    width: 100%;
    max-width: 320px;
    height: auto;
    margin: 0 auto;
    border: 3px solid #8d4a14;
    border-radius: 12px;
    background: #fff4e0;
    touch-action: manipulation;
    user-select: none;
    -webkit-user-select: none;
    cursor: pointer;
}

.pie-message {
    min-height: 1.5em;
    margin: 12px auto;
    max-width: 320px;
    font-size: 1.05em;
}

.pie-game .start-btn {
    padding: 10px 28px;
    border: none;
    border-radius: 8px;
    background: #d2691e;
    color: #fff;
    font-size: 1.1em;
    font-weight: bold;
    cursor: pointer;
}

.pie-game .start-btn:hover {
    background: #b8571a;
}
HTML
<div class="pie-game">
    <h1>🥧 Pie Stack 🦃</h1>
    <div class="instructions">
        Tap, click or press Space to drop each pumpkin pie on the stack. Only the part that lands on the pie below stays, so time it well!
    </div>
    <div class="pie-info">Pies: <span id="pieScore">0</span> &nbsp;|&nbsp; Best: <span id="pieBest">0</span></div>
    <canvas class="pie-canvas" id="pieCanvas" width="320" height="480" aria-label="Pie stacking game area"></canvas>
    <div class="pie-message" id="pieMessage">Press "Start" and build the tallest Thanksgiving pie tower!</div>
    <button class="start-btn" id="pieStart">Start</button>
</div>
JS
const canvas = document.getElementById('pieCanvas');
const ctx = canvas.getContext('2d');
const scoreDisplay = document.getElementById('pieScore');
const bestDisplay = document.getElementById('pieBest');
const message = document.getElementById('pieMessage');
const startButton = document.getElementById('pieStart');

const W = 320;
const H = 480;
const LAYER = 24;
const BASE_WIDTH = 190;
const PERFECT = 5;
const START_SPEED = 120;

let stack, mover, pieces, score, best, running, waiting, speed, cameraY, targetY, lastTime, animationId;

best = 0;
try {
    best = parseInt(localStorage.getItem('pieStackBest'), 10) || 0;
} catch (e) {
    best = 0;
}
bestDisplay.textContent = best;

// Sharper drawing on high-density screens; game logic stays in 320 x 480 units.
const ratio = window.devicePixelRatio || 1;
canvas.width = W * ratio;
canvas.height = H * ratio;
ctx.setTransform(ratio, 0, 0, ratio, 0, 0);

// The share buttons are rendered after this script, so look them up when needed.
function setShareButtons(display) {
    const shareButtons = document.querySelector('.share-buttons');
    if (shareButtons) {
        shareButtons.style.display = display;
    }
}

function setScore(value) {
    score = value;
    scoreDisplay.textContent = score;
    const hiddenScore = document.getElementById('hiddenScore');
    if (hiddenScore) {
        hiddenScore.value = score;
    }
}

// Screen y of the top of layer n (layer 0 is the plate).
function layerY(n) {
    return H - 40 - (n + 1) * LAYER + cameraY;
}

function newMover() {
    const top = stack[stack.length - 1];
    const fromLeft = stack.length % 2 === 0;
    mover = {
        x: fromLeft ? -top.w : W,
        w: top.w,
        dir: fromLeft ? 1 : -1
    };
}

function reset() {
    stack = [{ x: (W - BASE_WIDTH) / 2, w: BASE_WIDTH, perfect: false }];
    pieces = [];
    speed = START_SPEED;
    cameraY = 0;
    targetY = 0;
    setScore(0);
    newMover();
}

function drawPie(x, y, w, perfect) {
    const r = Math.min(8, w / 3);
    // crust
    ctx.fillStyle = '#e3b26b';
    roundRect(x, y + 6, w, LAYER - 6, r);
    // pumpkin filling
    ctx.fillStyle = '#d2691e';
    roundRect(x + 3, y + 2, Math.max(0, w - 6), LAYER - 12, r);
    // crimped edge
    ctx.fillStyle = '#c9954f';
    for (let i = x + 6; i < x + w - 4; i += 12) {
        ctx.beginPath();
        ctx.arc(i, y + LAYER - 2, 3, 0, Math.PI * 2);
        ctx.fill();
    }
    if (perfect && w > 20) {
        ctx.fillStyle = '#fffdf7';
        ctx.beginPath();
        ctx.arc(x + w / 2, y + 2, 6, 0, Math.PI * 2);
        ctx.fill();
    }
}

function roundRect(x, y, w, h, r) {
    if (w <= 0) {
        return;
    }
    ctx.beginPath();
    ctx.moveTo(x + r, y);
    ctx.lineTo(x + w - r, y);
    ctx.quadraticCurveTo(x + w, y, x + w, y + r);
    ctx.lineTo(x + w, y + h - r);
    ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
    ctx.lineTo(x + r, y + h);
    ctx.quadraticCurveTo(x, y + h, x, y + h - r);
    ctx.lineTo(x, y + r);
    ctx.quadraticCurveTo(x, y, x + r, y);
    ctx.fill();
}

function draw() {
    ctx.clearRect(0, 0, W, H);

    // autumn background: falling-leaf dots that move with the camera
    ctx.fillStyle = '#f6d7a7';
    for (let i = 0; i < 12; i++) {
        const lx = (i * 73) % W;
        const ly = ((i * 131) + cameraY * 0.3) % H;
        ctx.beginPath();
        ctx.arc(lx, ly, 4, 0, Math.PI * 2);
        ctx.fill();
    }

    // table and plate
    const plateY = H - 40 + cameraY;
    if (plateY < H) {
        ctx.fillStyle = '#8d4a14';
        ctx.fillRect(0, plateY + LAYER, W, H);
    }

    for (let i = 0; i < stack.length; i++) {
        const y = layerY(i - 1);
        if (y > -LAYER && y < H) {
            if (i === 0) {
                ctx.fillStyle = '#ffffff';
                roundRect(stack[0].x - 12, y + LAYER - 8, stack[0].w + 24, 10, 5);
            } else {
                drawPie(stack[i].x, y, stack[i].w, stack[i].perfect);
            }
        }
    }

    pieces.forEach(function (p) {
        drawPie(p.x, p.y, p.w, false);
    });

    if (running && mover) {
        drawPie(mover.x, layerY(stack.length - 1), mover.w, false);
    }
}

function drop() {
    if (!running) {
        return;
    }
    const top = stack[stack.length - 1];
    const left = Math.max(mover.x, top.x);
    const right = Math.min(mover.x + mover.w, top.x + top.w);
    const overlap = right - left;
    const y = layerY(stack.length - 1);

    if (overlap <= 0) {
        pieces.push({ x: mover.x, y: y, w: mover.w, vy: 0 });
        endGame();
        return;
    }

    let perfect = false;
    let x = left;
    let w = overlap;
    if (Math.abs(mover.x - top.x) <= PERFECT) {
        perfect = true;
        x = top.x;
        w = top.w;
        message.textContent = 'Perfect drop! Whipped cream on top. 🥧';
    } else {
        // the overhang falls off
        if (mover.x < top.x) {
            pieces.push({ x: mover.x, y: y, w: top.x - mover.x, vy: 0 });
        } else {
            pieces.push({ x: top.x + top.w, y: y, w: mover.x + mover.w - (top.x + top.w), vy: 0 });
        }
        message.textContent = 'Keep stacking!';
    }

    stack.push({ x: x, w: w, perfect: perfect });
    setScore(score + 1);
    speed = Math.min(START_SPEED + score * 9, 420);

    // keep the top of the tower in the upper half of the canvas
    const topY = H - 40 - (stack.length + 1) * LAYER;
    if (topY < H * 0.45) {
        targetY = H * 0.45 - topY;
    }

    newMover();
}

function endGame() {
    running = false;
    if (score > best) {
        best = score;
        try {
            localStorage.setItem('pieStackBest', String(best));
        } catch (e) {
            // storage unavailable: keep the best score for this visit only
        }
        bestDisplay.textContent = best;
        message.textContent = 'Missed! New record: ' + score + ' pies. 🦃 Press "Start" to try again.';
    } else {
        message.textContent = 'Missed! Your tower had ' + score + ' pies. Press "Start" to try again.';
    }
    startButton.style.display = 'inline-block';
    setShareButtons('flex');
}

function loop(time) {
    const dt = Math.min((time - lastTime) / 1000, 0.05);
    lastTime = time;

    if (running && !waiting) {
        mover.x += mover.dir * speed * dt;
        if (mover.x + mover.w > W + 10 && mover.dir > 0) {
            mover.dir = -1;
        } else if (mover.x < -10 && mover.dir < 0) {
            mover.dir = 1;
        }
    }

    cameraY += (targetY - cameraY) * Math.min(1, dt * 6);

    pieces.forEach(function (p) {
        p.vy += 900 * dt;
        p.y += p.vy * dt;
    });
    pieces = pieces.filter(function (p) {
        return p.y < H + 60;
    });

    draw();

    if (running || pieces.length) {
        animationId = requestAnimationFrame(loop);
    } else {
        animationId = null;
    }
}

function start() {
    reset();
    running = true;
    waiting = false;
    startButton.style.display = 'none';
    setShareButtons('none');
    message.textContent = 'Drop the pie when it is right above the stack!';
    lastTime = performance.now();
    if (!animationId) {
        animationId = requestAnimationFrame(loop);
    }
}

canvas.addEventListener('touchstart', function (e) {
    e.preventDefault();
    drop();
}, { passive: false });

canvas.addEventListener('mousedown', function () {
    drop();
});

document.addEventListener('keydown', function (e) {
    if ((e.code === 'Space' || e.key === ' ') && running) {
        e.preventDefault();
        drop();
    }
});

startButton.addEventListener('click', start);

reset();
running = false;
draw();