🛒 Black Friday Cart Catch 🏷️

Drag your finger or mouse, or use the arrow keys, to move the cart. Catch the falling deals and dodge the "sold out" tags!
Deals: 0  |  Lives: ❤️❤️❤️  |  Best: 0
Press "Start", then move the cart to catch the deals!

The Black Friday sale has started and deals are raining from the shelves 🏷️. Drag your finger or your mouse, or use the arrow keys, to move your shopping cart 🛒 and catch the phones, sneakers, headphones and gifts as they fall. Every deal you catch scores a point, but watch out for the red ❌ "sold out" tags: catch three of them and the sale is over for you. The deals fall faster and the sold out tags get more common as the sale goes on.

How many deals can you fit in your cart before Black Friday ends?

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

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

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

#cartCanvas {
    display: block;
    margin: 0 auto;
    width: 100%;
    max-width: 300px;
    height: auto;
    border: 3px solid #111;
    border-radius: 10px;
    background: #1f1f2e;
    touch-action: none;
    cursor: pointer;
}

.cart-message {
    margin-top: 15px;
    font-weight: bold;
    min-height: 1.4em;
}

.cart-game .start-btn {
    margin-top: 15px;
    padding: 10px 20px;
    font-size: 1em;
    background-color: #e53935;
    color: #fff;
    font-weight: bold;
    border: none;
    border-radius: 5px;
    cursor: pointer;
}

.cart-game .start-btn:hover {
    background-color: #ef5350;
}

.cart-game .start-btn:disabled {
    background-color: #ccc;
    cursor: default;
}
HTML
<div class="cart-game">
    <h1>🛒 Black Friday Cart Catch 🏷️</h1>
    <div class="instructions">
        Drag your finger or mouse, or use the arrow keys, to move the cart. Catch the falling deals and dodge the "sold out" tags!
    </div>
    <div class="cart-info">Deals: <span id="cartScore">0</span> &nbsp;|&nbsp; Lives: <span id="cartLives">❤️❤️❤️</span> &nbsp;|&nbsp; Best: <span id="cartBest">0</span></div>
    <canvas id="cartCanvas" width="300" height="400"></canvas>
    <div class="cart-message" id="cartMessage">Press "Start", then move the cart to catch the deals!</div>
    <button class="start-btn" id="cartStart">Start</button>
</div>
JS
const canvas = document.getElementById('cartCanvas');
const ctx = canvas.getContext('2d');
const scoreDisplay = document.getElementById('cartScore');
const livesDisplay = document.getElementById('cartLives');
const bestDisplay = document.getElementById('cartBest');
const message = document.getElementById('cartMessage');
const startButton = document.getElementById('cartStart');

const W = canvas.width;
const H = canvas.height;
const CART_W = 56;
const CART_Y = H - 50;
const ITEM_SIZE = 30;
const DEALS = ['📱', '🎧', '👟', '🎮', '⌚', '🧸', '💻', '🎁'];
const MAX_LIVES = 3;

let cartX, targetX, items, score, lives, running, lastTime, animationId, spawnTimer, elapsed, best, keys;

best = 0;
try {
    best = parseInt(localStorage.getItem('cartCatchBest'), 10) || 0;
} catch (e) {
    best = 0;
}
bestDisplay.textContent = best;
keys = { left: false, right: false };

// 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;
    }
}

function showLives() {
    let text = '';
    for (let i = 0; i < MAX_LIVES; i++) {
        text += i < lives ? '❤️' : '🤍';
    }
    livesDisplay.textContent = text;
}

function reset() {
    cartX = W / 2;
    targetX = W / 2;
    items = [];
    elapsed = 0;
    spawnTimer = 0.5;
    lives = MAX_LIVES;
    setScore(0);
    showLives();
}

function fallSpeed() {
    return 120 + elapsed * 4;
}

function spawnDelay() {
    return Math.max(0.35, 0.9 - elapsed * 0.012);
}

function spawn() {
    const soldOutChance = Math.min(0.4, 0.15 + elapsed * 0.005);
    const soldOut = Math.random() < soldOutChance;
    items.push({
        x: ITEM_SIZE / 2 + Math.random() * (W - ITEM_SIZE),
        y: -ITEM_SIZE,
        soldOut: soldOut,
        emoji: soldOut ? '❌' : DEALS[Math.floor(Math.random() * DEALS.length)],
        speed: fallSpeed() * (0.85 + Math.random() * 0.3)
    });
}

function pointerToCanvasX(clientX) {
    const rect = canvas.getBoundingClientRect();
    return (clientX - rect.left) * (W / rect.width);
}

function update(dt) {
    elapsed += dt;

    if (keys.left) {
        targetX -= 320 * dt;
    }
    if (keys.right) {
        targetX += 320 * dt;
    }
    targetX = Math.max(CART_W / 2, Math.min(W - CART_W / 2, targetX));
    cartX += (targetX - cartX) * Math.min(1, dt * 18);

    spawnTimer -= dt;
    if (spawnTimer <= 0) {
        spawn();
        spawnTimer = spawnDelay();
    }

    for (let i = items.length - 1; i >= 0; i--) {
        const item = items[i];
        item.y += item.speed * dt;
        const caught = item.y + ITEM_SIZE / 2 >= CART_Y - 10 && item.y - ITEM_SIZE / 2 <= CART_Y + 20 &&
            Math.abs(item.x - cartX) < CART_W / 2 + ITEM_SIZE / 3;
        if (caught) {
            items.splice(i, 1);
            if (item.soldOut) {
                lives -= 1;
                showLives();
                message.textContent = 'Sold out! That one cost you a life. ❌';
                if (lives <= 0) {
                    endGame();
                    return;
                }
            } else {
                setScore(score + 1);
                message.textContent = score % 10 === 0 ? score + ' deals in the cart! 🛍️' : 'Deal! 🏷️';
            }
        } else if (item.y > H + ITEM_SIZE) {
            items.splice(i, 1);
        }
    }
}

function draw() {
    ctx.clearRect(0, 0, W, H);
    ctx.fillStyle = '#1f1f2e';
    ctx.fillRect(0, 0, W, H);

    ctx.fillStyle = '#e53935';
    ctx.font = 'bold 14px sans-serif';
    ctx.textAlign = 'center';
    ctx.fillText('BLACK FRIDAY SALE', W / 2, 24);

    ctx.textBaseline = 'middle';
    ctx.font = ITEM_SIZE + 'px serif';
    items.forEach(function (item) {
        ctx.fillText(item.emoji, item.x, item.y);
    });

    ctx.font = '44px serif';
    ctx.fillText('🛒', cartX, CART_Y);
    ctx.textBaseline = 'alphabetic';
}

function loop(time) {
    if (!running) {
        return;
    }
    const dt = Math.min(0.05, (time - lastTime) / 1000);
    lastTime = time;
    update(dt);
    if (running) {
        draw();
        animationId = requestAnimationFrame(loop);
    }
}

function startGame() {
    reset();
    running = true;
    startButton.disabled = true;
    setShareButtons('none');
    message.textContent = 'The sale is on! Catch the deals! 🏷️';
    lastTime = performance.now();
    animationId = requestAnimationFrame(loop);
}

function endGame() {
    running = false;
    cancelAnimationFrame(animationId);
    draw();
    if (score > best) {
        best = score;
        bestDisplay.textContent = best;
        try {
            localStorage.setItem('cartCatchBest', String(best));
        } catch (e) {
            // ignore storage errors
        }
        message.textContent = 'Sale over! New best: ' + score + ' deals! 🏆';
    } else {
        message.textContent = 'Sale over! You caught ' + score + ' deals. 🛒';
    }
    startButton.disabled = false;
    startButton.textContent = 'Play again';
    setShareButtons('flex');
}

canvas.addEventListener('touchstart', function (e) {
    e.preventDefault();
    targetX = pointerToCanvasX(e.touches[0].clientX);
}, { passive: false });
canvas.addEventListener('touchmove', function (e) {
    e.preventDefault();
    targetX = pointerToCanvasX(e.touches[0].clientX);
}, { passive: false });
canvas.addEventListener('mousemove', function (e) {
    if (running) {
        targetX = pointerToCanvasX(e.clientX);
    }
});
document.addEventListener('keydown', function (e) {
    if (!running) {
        return;
    }
    if (e.key === 'ArrowLeft') {
        keys.left = true;
        e.preventDefault();
    } else if (e.key === 'ArrowRight') {
        keys.right = true;
        e.preventDefault();
    }
});
document.addEventListener('keyup', function (e) {
    if (e.key === 'ArrowLeft') {
        keys.left = false;
    } else if (e.key === 'ArrowRight') {
        keys.right = false;
    }
});
startButton.addEventListener('click', startGame);

reset();
draw();