Christmas Eve is getting close and one of Santa's reindeer 🦌 is racing across the snowy fields to collect the last presents. Tap the left or right side of the field, swipe, or use the arrow keys to switch between the three lanes. Every present 🎁 you grab scores a point, but a single bump into a snowman, a fir tree or a rock ends the run. The snow keeps falling and the reindeer runs faster and faster as you go. How many presents can you deliver before Christmas?
.deer-game {
text-align: center;
}
.deer-game .instructions {
max-width: 500px;
margin: 0 auto 15px;
font-size: 1em;
color: #555;
}
.deer-info {
margin-bottom: 10px;
font-size: 1.2em;
}
#deerCanvas {
display: block;
margin: 0 auto;
max-width: 100%;
border: 3px solid #c62828;
border-radius: 10px;
background: #e8f1fb;
touch-action: none;
cursor: pointer;
}
.deer-message {
margin-top: 15px;
font-weight: bold;
}
.deer-game .start-btn {
margin-top: 15px;
padding: 10px 20px;
font-size: 1em;
background-color: #c62828;
color: #fff;
font-weight: bold;
border: none;
border-radius: 5px;
cursor: pointer;
}
.deer-game .start-btn:hover {
background-color: #e53935;
}
.deer-game .start-btn:disabled {
background-color: #ccc;
cursor: not-allowed;
}
<div class="deer-game">
<h1>🦌 Reindeer Rush 🎄</h1>
<div class="instructions">
Tap the left or right side, swipe, or use the arrow keys to switch lanes. Grab the presents and dodge the snowmen and trees!
</div>
<div class="deer-info">Score: <span id="deerScore">0</span> | Best: <span id="deerBest">0</span></div>
<canvas id="deerCanvas" width="300" height="420"></canvas>
<div class="deer-message" id="deerMessage">Press "Start" to help Santa's reindeer!</div>
<button class="start-btn" id="deerStart">Start</button>
</div>
const canvas = document.getElementById('deerCanvas');
const ctx = canvas.getContext('2d');
const scoreDisplay = document.getElementById('deerScore');
const bestDisplay = document.getElementById('deerBest');
const message = document.getElementById('deerMessage');
const startButton = document.getElementById('deerStart');
const hiddenScore = document.getElementById('hiddenScore');
const W = canvas.width;
const H = canvas.height;
const LANES = 3;
const LANE_W = W / LANES;
const PLAYER_Y = H - 60;
const SIZE = 34;
const OBSTACLES = ['⛄', '🌲', '🪨'];
let lane, playerX, items, score, running, waiting, lastTime, animationId, spawnTimer, elapsed, snow, best;
best = 0;
try {
best = parseInt(localStorage.getItem('reindeerRushBest'), 10) || 0;
} catch (e) {
best = 0;
}
bestDisplay.textContent = best;
snow = [];
for (let i = 0; i < 40; i++) {
snow.push({ x: Math.random() * W, y: Math.random() * H, r: 1 + Math.random() * 2, s: 0.4 + Math.random() * 0.8 });
}
function laneCenter(l) {
return LANE_W * l + LANE_W / 2;
}
function reset() {
lane = 1;
playerX = laneCenter(lane);
items = [];
score = 0;
spawnTimer = 0;
elapsed = 0;
scoreDisplay.textContent = 0;
hiddenScore.value = 0;
}
function move(direction) {
if (!running) {
return;
}
if (waiting) {
// The first move starts the run
waiting = false;
message.textContent = 'Run, reindeer, run!';
lastTime = performance.now();
}
lane = Math.max(0, Math.min(LANES - 1, lane + direction));
}
function speed() {
// Pixels per 1/60s step, ramps up gradually
return 3 + Math.min(elapsed / 600, 4);
}
function spawn() {
const blocked = Math.floor(Math.random() * LANES);
items.push({ lane: blocked, y: -SIZE, type: 'obstacle', icon: OBSTACLES[Math.floor(Math.random() * OBSTACLES.length)] });
// Sometimes a second obstacle, but always leave one lane free
if (elapsed > 900 && Math.random() < 0.35) {
const second = (blocked + 1 + Math.floor(Math.random() * 2)) % LANES;
items.push({ lane: second, y: -SIZE, type: 'obstacle', icon: OBSTACLES[Math.floor(Math.random() * OBSTACLES.length)] });
} else if (Math.random() < 0.6) {
let giftLane = Math.floor(Math.random() * LANES);
if (giftLane === blocked) {
giftLane = (giftLane + 1) % LANES;
}
items.push({ lane: giftLane, y: -SIZE - 90, type: 'gift', icon: '🎁' });
}
}
function drawBackground(step) {
ctx.fillStyle = '#e8f1fb';
ctx.fillRect(0, 0, W, H);
ctx.fillStyle = '#d3e3f3';
for (let l = 1; l < LANES; l++) {
ctx.fillRect(LANE_W * l - 2, 0, 4, H);
}
ctx.fillStyle = '#b7cde3';
snow.forEach(function (f) {
f.y += f.s * step;
if (f.y > H) {
f.y = -4;
f.x = Math.random() * W;
}
ctx.beginPath();
ctx.arc(f.x, f.y, f.r, 0, Math.PI * 2);
ctx.fill();
});
}
function drawEmoji(icon, x, y, size) {
ctx.font = size + 'px serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(icon, x, y);
}
function draw(step) {
drawBackground(step);
items.forEach(function (item) {
drawEmoji(item.icon, laneCenter(item.lane), item.y, SIZE);
});
drawEmoji('🦌', playerX, PLAYER_Y, SIZE + 6);
}
function endGame() {
running = false;
cancelAnimationFrame(animationId);
let text = 'Bump! The reindeer slipped. Final score: ' + score;
if (score > best) {
best = score;
bestDisplay.textContent = best;
text += ' - new best!';
try {
localStorage.setItem('reindeerRushBest', String(best));
} catch (e) {
// Storage unavailable, keep the best score for this visit only
}
}
message.textContent = text;
startButton.disabled = false;
document.querySelector('.share-buttons').style.display = 'flex'; // Show share buttons
}
function loop(now) {
if (waiting) {
playerX = laneCenter(lane);
draw(0.5);
animationId = requestAnimationFrame(loop);
return;
}
// Frame-rate independent step (1 = one frame at 60fps)
const step = Math.min((now - lastTime) / (1000 / 60), 3);
lastTime = now;
elapsed += step;
// Slide smoothly towards the chosen lane
const target = laneCenter(lane);
playerX += (target - playerX) * Math.min(1, 0.35 * step);
spawnTimer -= step;
if (spawnTimer <= 0) {
spawn();
spawnTimer = Math.max(26, 60 - elapsed / 60);
}
const v = speed();
items.forEach(function (item) {
item.y += v * step;
});
for (let i = items.length - 1; i >= 0; i--) {
const item = items[i];
const sameLane = Math.abs(laneCenter(item.lane) - playerX) < LANE_W / 2 - 12;
const close = Math.abs(item.y - PLAYER_Y) < SIZE - 8;
if (sameLane && close) {
if (item.type === 'gift') {
items.splice(i, 1);
score += 1;
scoreDisplay.textContent = score;
hiddenScore.value = score; // Update hidden score field
continue;
}
draw(step);
endGame();
return;
}
if (item.y > H + SIZE) {
items.splice(i, 1);
}
}
draw(step);
animationId = requestAnimationFrame(loop);
}
function startGame() {
reset();
running = true;
waiting = true;
startButton.disabled = true;
message.textContent = 'Tap a side, swipe or press an arrow key to start!';
document.querySelector('.share-buttons').style.display = 'none';
animationId = requestAnimationFrame(loop);
}
let touchStartX = null;
canvas.addEventListener('mousedown', function (event) {
const rect = canvas.getBoundingClientRect();
move(event.clientX - rect.left < rect.width / 2 ? -1 : 1);
});
canvas.addEventListener('touchstart', function (event) {
event.preventDefault();
touchStartX = event.touches[0].clientX;
}, { passive: false });
canvas.addEventListener('touchend', function (event) {
event.preventDefault();
if (touchStartX === null) {
return;
}
const endX = event.changedTouches[0].clientX;
const rect = canvas.getBoundingClientRect();
if (Math.abs(endX - touchStartX) > 30) {
move(endX > touchStartX ? 1 : -1); // Swipe
} else {
move(endX - rect.left < rect.width / 2 ? -1 : 1); // Tap a side
}
touchStartX = null;
}, { passive: false });
document.addEventListener('keydown', function (event) {
if (!running) {
return;
}
if (event.code === 'ArrowLeft' || event.code === 'KeyA') {
event.preventDefault();
move(-1);
} else if (event.code === 'ArrowRight' || event.code === 'KeyD') {
event.preventDefault();
move(1);
}
});
startButton.addEventListener('click', startGame);
running = false;
waiting = false;
reset();
draw(0);