Snow is falling and everyone in town wants a hot cocoa ☕. Read each customer's order, tap to add the right number of marshmallows 🍡 and candy canes 🍬, then press Serve before the timer runs out. On a keyboard, M adds a marshmallow, C a candy cane, Backspace empties the cup and Enter serves. A wrong cup or a customer who waits too long costs you a life, and after three mistakes the café closes. Orders get bigger and the queue moves faster as you go.
How many hot cocoas can you serve before closing time?
.cocoa-game {
text-align: center;
}
.cocoa-game .instructions {
max-width: 500px;
margin: 0 auto 15px;
font-size: 1em;
color: #555;
}
.cocoa-info {
margin-bottom: 10px;
font-size: 1.2em;
}
.cocoa-counter {
width: 100%;
max-width: 340px;
margin: 0 auto;
padding: 14px;
box-sizing: border-box;
border: 3px solid #6d3b1f;
border-radius: 12px;
background: linear-gradient(#fff4e6, #f3d9bd);
touch-action: manipulation;
user-select: none;
-webkit-user-select: none;
}
.cocoa-order {
min-height: 2.6em;
font-size: 1em;
font-weight: bold;
color: #4e2a12;
}
.cocoa-timer {
height: 10px;
margin: 8px 0 12px;
border-radius: 5px;
background: #e8c9a6;
overflow: hidden;
}
.cocoa-timer-bar {
height: 100%;
width: 100%;
background: #43a047;
}
.cocoa-cup {
min-height: 40px;
font-size: 30px;
line-height: 1.2;
word-break: break-all;
}
.cocoa-buttons {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
margin-top: 10px;
}
.cocoa-btn {
padding: 12px 4px;
font-size: 0.9em;
font-weight: bold;
border: none;
border-radius: 8px;
background: #8d5524;
color: #fff;
cursor: pointer;
-webkit-tap-highlight-color: transparent;
}
.cocoa-btn:disabled {
background: #c9b29b;
cursor: default;
}
.cocoa-btn.cocoa-reset {
background: #a1887f;
}
.cocoa-btn.cocoa-serve {
background: #2e7d32;
}
.cocoa-btn:disabled.cocoa-reset,
.cocoa-btn:disabled.cocoa-serve {
background: #c9b29b;
}
.cocoa-message {
margin-top: 15px;
font-weight: bold;
min-height: 1.4em;
}
.cocoa-game .start-btn {
margin-top: 15px;
padding: 10px 20px;
font-size: 1em;
background-color: #6d3b1f;
color: #fff;
font-weight: bold;
border: none;
border-radius: 5px;
cursor: pointer;
}
.cocoa-game .start-btn:hover {
background-color: #8d5524;
}
.cocoa-game .start-btn:disabled {
background-color: #ccc;
cursor: default;
}
<div class="cocoa-game">
<h1>☕ Hot Cocoa Rush ❄️</h1>
<div class="instructions">
Read each order, add the right number of marshmallows 🍡 and candy canes 🍬, then press Serve before the timer runs out. Three mistakes and the café closes!
</div>
<div class="cocoa-info">Served: <span id="cocoaScore">0</span> | Lives: <span id="cocoaLives">❤️❤️❤️</span> | Best: <span id="cocoaBest">0</span></div>
<div class="cocoa-counter">
<div class="cocoa-order" id="cocoaOrder">Waiting for customers…</div>
<div class="cocoa-timer"><div class="cocoa-timer-bar" id="cocoaTimer"></div></div>
<div class="cocoa-cup" id="cocoaCup">☕</div>
<div class="cocoa-buttons">
<button class="cocoa-btn" id="cocoaMallow" aria-label="Add a marshmallow" disabled>🍡 +1</button>
<button class="cocoa-btn" id="cocoaCane" aria-label="Add a candy cane" disabled>🍬 +1</button>
<button class="cocoa-btn cocoa-reset" id="cocoaEmpty" aria-label="Empty the cup" disabled>↺ Empty</button>
<button class="cocoa-btn cocoa-serve" id="cocoaServe" disabled>Serve ✓</button>
</div>
</div>
<div class="cocoa-message" id="cocoaMessage">Press "Start" to open the café!</div>
<button class="start-btn" id="cocoaStart">Start</button>
</div>
const orderDisplay = document.getElementById('cocoaOrder');
const timerBar = document.getElementById('cocoaTimer');
const cupDisplay = document.getElementById('cocoaCup');
const mallowButton = document.getElementById('cocoaMallow');
const caneButton = document.getElementById('cocoaCane');
const emptyButton = document.getElementById('cocoaEmpty');
const serveButton = document.getElementById('cocoaServe');
const scoreDisplay = document.getElementById('cocoaScore');
const livesDisplay = document.getElementById('cocoaLives');
const bestDisplay = document.getElementById('cocoaBest');
const message = document.getElementById('cocoaMessage');
const startButton = document.getElementById('cocoaStart');
const MAX_LIVES = 3;
const CUSTOMERS = ['🧑', '👩', '👨', '👧', '🧓', '👦', '⛄'];
let score, lives, running, order, cup, timeLeft, timeTotal, lastTime, animationId, best;
best = 0;
try {
best = parseInt(localStorage.getItem('hotCocoaRushBest'), 10) || 0;
} catch (e) {
best = 0;
}
bestDisplay.textContent = best;
// 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 setButtons(enabled) {
[mallowButton, caneButton, emptyButton, serveButton].forEach(function (b) {
b.disabled = !enabled;
});
}
function repeat(symbol, count) {
let text = '';
for (let i = 0; i < count; i++) {
text += symbol;
}
return text;
}
function showCup() {
cupDisplay.textContent = '☕' + repeat('🍡', cup.mallows) + repeat('🍬', cup.canes);
}
function randomInt(min, max) {
return min + Math.floor(Math.random() * (max - min + 1));
}
function newOrder() {
const level = score;
const maxMallows = Math.min(6, 2 + Math.floor(level / 3));
order = {
customer: CUSTOMERS[Math.floor(Math.random() * CUSTOMERS.length)],
mallows: randomInt(level < 2 ? 1 : 0, maxMallows),
canes: level < 4 ? 0 : randomInt(0, Math.min(4, 1 + Math.floor((level - 4) / 4)))
};
if (order.mallows === 0 && order.canes === 0) {
order.mallows = 1;
}
const parts = [];
if (order.mallows > 0) {
parts.push(order.mallows + ' marshmallow' + (order.mallows === 1 ? '' : 's'));
}
if (order.canes > 0) {
parts.push(order.canes + ' candy cane' + (order.canes === 1 ? '' : 's'));
}
orderDisplay.textContent = order.customer + ' "Hot cocoa with ' + parts.join(' and ') + ', please!"';
cup = { mallows: 0, canes: 0 };
showCup();
timeTotal = Math.max(2.5, 7 - score * 0.2);
timeLeft = timeTotal;
}
function mistake(text) {
lives -= 1;
showLives();
message.textContent = text;
if (lives <= 0) {
endGame();
} else {
newOrder();
}
}
function serve() {
if (!running) {
return;
}
if (cup.mallows === order.mallows && cup.canes === order.canes) {
setScore(score + 1);
message.textContent = score % 5 === 0 ? score + ' cocoas served! The queue is growing… ☕' : 'Perfect, thank you! ☕';
newOrder();
} else {
mistake('Oops, that is not what they ordered! 😕');
}
}
function loop(time) {
if (!running) {
return;
}
const dt = Math.min(0.05, (time - lastTime) / 1000);
lastTime = time;
timeLeft -= dt;
const ratio = Math.max(0, timeLeft / timeTotal);
timerBar.style.width = (ratio * 100) + '%';
timerBar.style.background = ratio > 0.5 ? '#43a047' : (ratio > 0.25 ? '#fb8c00' : '#e53935');
if (timeLeft <= 0) {
mistake('Too slow, the customer left! ⏰');
}
if (running) {
animationId = requestAnimationFrame(loop);
}
}
function startGame() {
setScore(0);
lives = MAX_LIVES;
showLives();
running = true;
startButton.disabled = true;
setButtons(true);
setShareButtons('none');
message.textContent = 'The café is open! ☕';
newOrder();
lastTime = performance.now();
animationId = requestAnimationFrame(loop);
}
function endGame() {
running = false;
cancelAnimationFrame(animationId);
setButtons(false);
timerBar.style.width = '0%';
orderDisplay.textContent = 'The café is closed for today.';
if (score > best) {
best = score;
bestDisplay.textContent = best;
try {
localStorage.setItem('hotCocoaRushBest', String(best));
} catch (e) {
// ignore storage errors
}
message.textContent = 'Café closed! New best: ' + score + ' cocoas served! 🏆';
} else {
message.textContent = 'Café closed! You served ' + score + ' cocoas. ☕';
}
startButton.disabled = false;
startButton.textContent = 'Play again';
setShareButtons('flex');
}
function onPress(button, action) {
button.addEventListener('touchstart', function (e) {
e.preventDefault();
if (!button.disabled) {
action();
}
}, { passive: false });
button.addEventListener('click', function () {
action();
});
}
onPress(mallowButton, function () {
if (running && cup.mallows < 9) {
cup.mallows += 1;
showCup();
}
});
onPress(caneButton, function () {
if (running && cup.canes < 9) {
cup.canes += 1;
showCup();
}
});
onPress(emptyButton, function () {
if (running) {
cup = { mallows: 0, canes: 0 };
showCup();
}
});
onPress(serveButton, serve);
document.addEventListener('keydown', function (e) {
if (!running) {
return;
}
if (e.key === 'm' || e.key === 'M') {
cup.mallows = Math.min(9, cup.mallows + 1);
showCup();
} else if (e.key === 'c' || e.key === 'C') {
cup.canes = Math.min(9, cup.canes + 1);
showCup();
} else if (e.key === 'Backspace') {
e.preventDefault();
cup = { mallows: 0, canes: 0 };
showCup();
} else if (e.key === 'Enter') {
e.preventDefault();
serve();
}
});
lives = MAX_LIVES;
showLives();
startButton.addEventListener('click', startGame);