It's Thanksgiving and the whole feast has to fit on one plate 🍽️! Pumpkin pie, cornbread, cranberry sauce, roast turkey and stuffing slide in one after another: tap, click or press Space to drop each layer onto the pile. Anything hanging over the edge falls off, so the pile gets narrower with every sloppy drop, and the layers slide faster as it grows. Land a perfect drop to keep the full width. How high can you stack the feast before it topples?
.stk-game {
text-align: center;
}
.stk-game .instructions {
max-width: 500px;
margin: 0 auto 15px;
font-size: 1em;
color: #555;
}
.stk-info {
display: flex;
justify-content: center;
gap: 24px;
margin-bottom: 10px;
font-size: 1.2em;
}
#stkCanvas {
display: block;
margin: 0 auto;
max-width: 100%;
border: 3px solid #b5651d;
border-radius: 10px;
background: #fdf3e1;
touch-action: manipulation;
cursor: pointer;
}
.stk-message {
margin-top: 15px;
font-weight: bold;
min-height: 1.5em;
}
.stk-btn {
margin-top: 15px;
padding: 10px 20px;
font-size: 1em;
background-color: #b5651d;
color: #fff;
font-weight: bold;
border: none;
border-radius: 5px;
cursor: pointer;
}
.stk-btn:hover {
background-color: #c97a2f;
}
.stk-btn:disabled {
background-color: #ccc;
color: #666;
cursor: not-allowed;
}
<div class="stk-game">
<h1>🦃 Thanksgiving Stack 🥧</h1>
<div class="instructions">
Tap, click or press Space to drop each layer of the feast onto the pile. Anything hanging over the edge falls off, so time it right! A perfect drop keeps the layer at full width.
</div>
<div class="stk-info">
<span>Score: <span id="stkScore">0</span></span>
<span>Best: <span id="stkBest">0</span></span>
</div>
<canvas id="stkCanvas" width="320" height="480"></canvas>
<div class="stk-message" id="stkMessage">Press "Start" to set the Thanksgiving table...</div>
<button class="stk-btn" id="stkStart">Start</button>
</div>
const stkCanvas = document.getElementById('stkCanvas');
const stkCtx = stkCanvas.getContext('2d');
const stkScoreEl = document.getElementById('stkScore');
const stkBestEl = document.getElementById('stkBest');
const stkMessageEl = document.getElementById('stkMessage');
const stkStartBtn = document.getElementById('stkStart');
const stkHiddenScore = document.getElementById('hiddenScore');
const STK_W = stkCanvas.width;
const STK_H = stkCanvas.height;
const STK_LAYER_H = 26;
const STK_START_WIDTH = 200;
const STK_BASE_Y = STK_H - 40; // top of the serving plate
const STK_PERFECT_MARGIN = 4; // px of tolerance for a perfect drop
const STK_START_SPEED = 120; // px per second
const STK_SPEED_STEP = 9; // extra px/s per layer
const STK_FEAST = [
{name: 'Pumpkin pie', color: '#e07b2a', crust: '#c9a26b'},
{name: 'Cornbread', color: '#f2c14e', crust: '#d9a531'},
{name: 'Cranberry sauce', color: '#b3263a', crust: '#8c1d2d'},
{name: 'Mashed potatoes', color: '#f4ead2', crust: '#d8c9a3'},
{name: 'Roast turkey', color: '#a8612d', crust: '#7d4520'},
{name: 'Green beans', color: '#5f9e3f', crust: '#467a2d'},
{name: 'Stuffing', color: '#9b7a4e', crust: '#765b38'},
{name: 'Sweet potatoes', color: '#d9622b', crust: '#a84a20'}
];
let stkStack = []; // placed layers: {x, w, feast}
let stkMoving = null; // {x, w, dir, feast}
let stkFalling = []; // cut-off pieces: {x, y, w, vy, feast}
let stkScore = 0;
let stkSpeed = STK_START_SPEED;
let stkRunning = false;
let stkCameraY = 0; // how far the view has scrolled up
let stkLastTime = 0;
let stkFrame = null;
function stkReadBest() {
try {
return parseInt(localStorage.getItem('thanksgivingStackBest') || '0', 10) || 0;
} catch (e) {
return 0;
}
}
function stkSaveBest(value) {
try {
localStorage.setItem('thanksgivingStackBest', String(value));
} catch (e) {
// storage blocked: the best score only lasts for this visit
}
}
let stkBest = stkReadBest();
stkBestEl.textContent = stkBest;
// Screen y of the top of layer number `index` (0 = first layer on the plate).
function stkLayerY(index) {
return STK_BASE_Y - (index + 1) * STK_LAYER_H + stkCameraY;
}
function stkNewMovingLayer() {
const top = stkStack[stkStack.length - 1];
const fromLeft = stkStack.length % 2 === 0;
stkMoving = {
x: fromLeft ? -top.w : STK_W,
w: top.w,
dir: fromLeft ? 1 : -1,
feast: STK_FEAST[stkStack.length % STK_FEAST.length]
};
}
function stkReset() {
stkStack = [{x: (STK_W - STK_START_WIDTH) / 2, w: STK_START_WIDTH, feast: STK_FEAST[0]}];
stkFalling = [];
stkScore = 0;
stkSpeed = STK_START_SPEED;
stkCameraY = 0;
stkScoreEl.textContent = '0';
stkHiddenScore.value = 0;
stkNewMovingLayer();
}
function stkDrop() {
if (!stkRunning || !stkMoving) {
return;
}
const top = stkStack[stkStack.length - 1];
const left = Math.max(stkMoving.x, top.x);
const right = Math.min(stkMoving.x + stkMoving.w, top.x + top.w);
const overlap = right - left;
const index = stkStack.length;
if (overlap <= 0) {
stkFalling.push({x: stkMoving.x, y: stkLayerY(index), w: stkMoving.w, vy: 0, feast: stkMoving.feast});
stkMoving = null;
stkEndGame();
return;
}
if (Math.abs(stkMoving.x - top.x) <= STK_PERFECT_MARGIN) {
// Perfect drop: snap onto the layer below and keep the full width.
stkStack.push({x: top.x, w: top.w, feast: stkMoving.feast});
stkMessageEl.textContent = '✨ Perfect! ' + stkMoving.feast.name;
} else {
// Whatever hangs over the edge falls off.
if (stkMoving.x < top.x) {
stkFalling.push({x: stkMoving.x, y: stkLayerY(index), w: top.x - stkMoving.x, vy: 0, feast: stkMoving.feast});
} else {
stkFalling.push({x: right, y: stkLayerY(index), w: stkMoving.x + stkMoving.w - right, vy: 0, feast: stkMoving.feast});
}
stkStack.push({x: left, w: overlap, feast: stkMoving.feast});
stkMessageEl.textContent = stkMoving.feast.name + ' served!';
}
stkScore++;
stkScoreEl.textContent = stkScore;
stkHiddenScore.value = stkScore; // Update hidden score field
stkSpeed += STK_SPEED_STEP;
stkNewMovingLayer();
}
function stkUpdate(dt) {
if (stkMoving) {
stkMoving.x += stkMoving.dir * stkSpeed * dt;
// Bounce at the edges so the layer keeps sliding until dropped.
if (stkMoving.x + stkMoving.w > STK_W + 40 && stkMoving.dir > 0) {
stkMoving.dir = -1;
} else if (stkMoving.x < -40 && stkMoving.dir < 0) {
stkMoving.dir = 1;
}
}
// Scroll the view up once the pile passes the middle of the canvas.
const targetCamera = Math.max(0, (stkStack.length + 1) * STK_LAYER_H - STK_H / 2);
stkCameraY += (targetCamera - stkCameraY) * Math.min(1, dt * 6);
stkFalling.forEach(function (piece) {
piece.vy += 900 * dt;
piece.y += piece.vy * dt;
});
stkFalling = stkFalling.filter(function (piece) {
return piece.y < STK_H + 100;
});
}
function stkDrawLayer(x, y, w, feast) {
stkCtx.fillStyle = feast.color;
stkCtx.fillRect(x, y, w, STK_LAYER_H - 2);
stkCtx.fillStyle = feast.crust;
stkCtx.fillRect(x, y + STK_LAYER_H - 8, w, 6);
}
function stkDraw() {
stkCtx.fillStyle = '#fdf3e1';
stkCtx.fillRect(0, 0, STK_W, STK_H);
// Serving plate.
stkCtx.fillStyle = '#e9e4da';
stkCtx.fillRect(20, STK_BASE_Y + stkCameraY, STK_W - 40, 14);
stkCtx.fillStyle = '#cfc6b6';
stkCtx.fillRect(40, STK_BASE_Y + 14 + stkCameraY, STK_W - 80, 8);
stkStack.forEach(function (layer, index) {
stkDrawLayer(layer.x, stkLayerY(index), layer.w, layer.feast);
});
if (stkMoving) {
stkDrawLayer(stkMoving.x, stkLayerY(stkStack.length), stkMoving.w, stkMoving.feast);
}
stkFalling.forEach(function (piece) {
stkDrawLayer(piece.x, piece.y, piece.w, piece.feast);
});
if (stkMoving) {
stkCtx.fillStyle = '#7a4a1e';
stkCtx.font = 'bold 14px sans-serif';
stkCtx.textAlign = 'center';
stkCtx.fillText(stkMoving.feast.name, STK_W / 2, 24);
}
}
// Real elapsed time drives movement, whatever the frame rate.
function stkLoop(now) {
const dt = Math.min(0.05, (now - stkLastTime) / 1000);
stkLastTime = now;
stkUpdate(dt);
stkDraw();
if (stkRunning || stkFalling.length > 0) {
stkFrame = requestAnimationFrame(stkLoop);
}
}
function stkStartGame() {
cancelAnimationFrame(stkFrame);
stkReset();
stkRunning = true;
stkStartBtn.disabled = true;
stkMessageEl.textContent = 'Drop the ' + stkMoving.feast.name.toLowerCase() + '!';
document.querySelector('.share-buttons').style.display = 'none';
stkLastTime = performance.now();
stkFrame = requestAnimationFrame(stkLoop);
}
function stkEndGame() {
stkRunning = false;
stkStartBtn.disabled = false;
stkStartBtn.textContent = 'Play again';
let text = '🍂 The feast toppled! You stacked ' + stkScore + ' layer' + (stkScore === 1 ? '' : 's') + '.';
if (stkScore > stkBest) {
stkBest = stkScore;
stkSaveBest(stkBest);
stkBestEl.textContent = stkBest;
text += ' 🎉 New best score!';
}
stkMessageEl.textContent = text;
document.querySelector('.share-buttons').style.display = 'flex'; // Show share buttons
}
stkCanvas.addEventListener('mousedown', stkDrop);
stkCanvas.addEventListener('touchstart', function (event) {
event.preventDefault();
stkDrop();
}, {passive: false});
document.addEventListener('keydown', function (event) {
if (event.code === 'Space' && stkRunning) {
event.preventDefault();
stkDrop();
}
});
stkStartBtn.addEventListener('click', stkStartGame);
stkReset();
stkMoving = null;
stkDraw();