A fun game created with AI
Created by FunInventor8509 • November 20, 2025
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<title>Alpine Racer - Ski Slalom</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
-webkit-tap-highlight-color: transparent;
}
body {
margin: 0;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
background: linear-gradient(to bottom, #1a2a4a 0%, #2d4a6f 100%);
font-family: 'Arial', sans-serif;
overflow: hidden;
touch-action: manipulation;
}
#gameContainer {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
height: 100vh;
max-width: 800px;
}
canvas {
border: 3px solid #fff;
background: #e8f4f8;
box-shadow: 0 0 20px rgba(0,0,0,0.5);
touch-action: none;
}
#ui {
position: absolute;
top: 10px;
left: 10px;
right: 10px;
color: white;
font-size: clamp(14px, 3vw, 18px);
text-shadow: 2px 2px 4px rgba(0,0,0,0.8);
pointer-events: none;
z-index: 10;
}
.stat {
background: rgba(0,0,0,0.6);
padding: 8px 12px;
border-radius: 8px;
margin-bottom: 5px;
display: inline-block;
margin-right: 10px;
}
#controls {
position: absolute;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
color: white;
font-size: clamp(12px, 2.5vw, 14px);
text-align: center;
background: rgba(0,0,0,0.6);
padding: 10px 20px;
border-radius: 10px;
pointer-events: none;
}
#gameOver {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(0,0,0,0.9);
padding: 30px;
border-radius: 15px;
text-align: center;
color: white;
display: none;
z-index: 100;
}
#gameOver h2 {
font-size: clamp(24px, 5vw, 36px);
margin-bottom: 20px;
color: #4af;
}
#gameOver p {
font-size: clamp(16px, 3vw, 20px);
margin: 10px 0;
}
#restartBtn {
margin-top: 20px;
padding: 15px 40px;
font-size: clamp(16px, 3vw, 20px);
background: #4af;
border: none;
border-radius: 10px;
color: white;
cursor: pointer;
font-weight: bold;
touch-action: manipulation;
}
#restartBtn:active {
background: #39e;
transform: scale(0.95);
}
/* Mobile optimization CSS */
body {
margin: 0;
padding: 0;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
background: #000;
font-family: Arial, sans-serif;
overflow: hidden; /* Prevent scrolling on mobile */
/* Safe area insets for iPhone notch/home indicator */
padding-top: env(safe-area-inset-top);
padding-bottom: env(safe-area-inset-bottom);
padding-left: env(safe-area-inset-left);
padding-right: env(safe-area-inset-right);
/* Prevent text selection and long-press menus on mobile */
-webkit-user-select: none;
user-select: none;
-webkit-touch-callout: none;
/* Disable tap highlight on mobile */
-webkit-tap-highlight-color: transparent;
/* Optimize for touch */
touch-action: manipulation;
}
#game {
border: 2px solid #fff;
max-width: 100vw;
max-height: 100vh;
display: block;
touch-action: none; /* Prevent default touch behaviors on canvas */
}
/* UI elements should be positioned safely */
.game-ui {
position: absolute;
color: #fff;
font-size: clamp(14px, 3vw, 24px); /* Responsive font size */
z-index: 10;
pointer-events: none; /* UI doesn't interfere with touch controls */
}
.score {
top: max(10px, env(safe-area-inset-top) + 10px);
left: max(10px, env(safe-area-inset-left) + 10px);
}
.lives {
top: max(10px, env(safe-area-inset-top) + 10px);
right: max(10px, env(safe-area-inset-right) + 10px);
}
</style>
</head>
<body>
<div id="gameContainer">
<div id="ui">
<div class="stat">⏱️ Time: <span id="time">0.0</span>s</div>
<div class="stat">🎯 Gates: <span id="gates">0</span>/<span id="totalGates">20</span></div>
<div class="stat">💨 Speed: <span id="speed">0</span></div>
<div class="stat">❤️ Lives: <span id="lives">3</span></div>
</div>
<canvas id="game"></canvas>
<div id="controls">🎮 Desktop: Arrow Keys / WASD to steer & brake | 📱 Mobile: Swipe to turn</div>
<div id="gameOver">
<h2 id="resultTitle">Race Complete!</h2>
<p id="finalTime"></p>
<p id="finalGates"></p>
<p id="finalScore"></p>
<button id="restartBtn">Race Again</button>
</div>
</div>
<script>
window.addEventListener('load', function() {
try {
const canvas = document.getElementById('game');
if (!canvas) { alert('Canvas not found'); return; }
const ctx = canvas.getContext('2d');
if (!ctx) { alert('Canvas context failed'); return; }
// Responsive canvas setup
function resizeCanvas() {
const container = document.getElementById('gameContainer');
const maxWidth = Math.min(800, window.innerWidth - 20);
const maxHeight = window.innerHeight - 100;
const size = Math.min(maxWidth, maxHeight * 0.7);
canvas.style.width = size + 'px';
canvas.style.height = (size * 1.2) + 'px';
const dpr = window.devicePixelRatio || 1;
canvas.width = 800;
canvas.height = 960;
}
resizeCanvas();
window.addEventListener('resize', resizeCanvas);
window.addEventListener('orientationchange', () => setTimeout(resizeCanvas, 100));
// Game variables
const WIDTH = 800;
const HEIGHT = 960;
let gameRunning = true;
let gameTime = 0;
let lastFrameTime = Date.now();
// Player
const player = {
x: WIDTH / 2,
y: HEIGHT - 150,
width: 30,
height: 40,
speed: 0,
maxSpeed: 15,
acceleration: 0.3,
friction: 0.98,
turnSpeed: 0,
maxTurnSpeed: 8,
angle: 0,
lives: 3,
invincible: 0,
carving: false
};
// Course
let scrollSpeed = 5;
let gatesCleared = 0;
const totalGates = 20;
let courseOffset = 0;
// Gates
const gates = [];
const gateWidth = 150;
const gateSpacing = 300;
// Obstacles
const obstacles = [];
const trees = [];
// Particles
const snowParticles = [];
const sprayParticles = [];
// Weather
let snowIntensity = 0.3;
// Keyboard
const keys = {};
window.addEventListener('keydown', (e) => {
keys[e.key] = true;
if(['ArrowUp','ArrowDown','ArrowLeft','ArrowRight',' '].includes(e.key)) {
e.preventDefault();
}
});
window.addEventListener('keyup', (e) => {
keys[e.key] = false;
});
// Touch controls
let touchStartX = 0;
let touchStartY = 0;
let touchActive = false;
canvas.addEventListener('touchstart', (e) => {
e.preventDefault();
touchStartX = e.touches[0].clientX;
touchStartY = e.touches[0].clientY;
touchActive = true;
}, { passive: false });
canvas.addEventListener('touchmove', (e) => {
e.preventDefault();
if (touchActive) {
const deltaX = e.touches[0].clientX - touchStartX;
if (Math.abs(deltaX) > 10) {
if (deltaX > 0) {
keys['ArrowRight'] = true;
keys['ArrowLeft'] = false;
} else {
keys['ArrowLeft'] = true;
keys['ArrowRight'] = false;
}
}
}
}, { passive: false });
canvas.addEventListener('touchend', (e) => {
e.preventDefault();
touchActive = false;
keys['ArrowLeft'] = false;
keys['ArrowRight'] = false;
}, { passive: false });
// Initialize course
function initCourse() {
gates.length = 0;
obstacles.length = 0;
trees.length = 0;
// Create gates
for (let i = 0; i < totalGates; i++) {
const y = -200 - (i * gateSpacing);
const side = i % 2 === 0 ? -1 : 1;
const offset = side * (Math.random() * 100 + 50);
gates.push({
x: WIDTH / 2 + offset,
y: y,
width: gateWidth,
cleared: false,
side: side,
type: Math.random() > 0.7 ? 'jump' : 'normal'
});
}
// Create trees along sides
for (let i = 0; i < 50; i++) {
const side = Math.random() > 0.5 ? 1 : -1;
trees.push({
x: side > 0 ? WIDTH - 80 + Math.random() * 60 : 20 + Math.random() * 60,
y: -Math.random() * totalGates * gateSpacing,
size: 30 + Math.random() * 20
});
}
// Create obstacles
for (let i = 0; i < 15; i++) {
obstacles.push({
x: 150 + Math.random() * (WIDTH - 300),
y: -Math.random() * totalGates * gateSpacing * 0.8,
width: 40,
height: 40,
type: 'rock'
});
}
// Initialize snow particles
for (let i = 0; i < 100; i++) {
snowParticles.push({
x: Math.random() * WIDTH,
y: Math.random() * HEIGHT,
size: Math.random() * 3 + 1,
speed: Math.random() * 2 + 1
});
}
}
// Draw functions
function drawSnow() {
ctx.fillStyle = 'rgba(255, 255, 255, 0.8)';
snowParticles.forEach(p => {
ctx.beginPath();
ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
ctx.fill();
});
}
function drawTrees() {
trees.forEach(tree => {
const screenY = tree.y + courseOffset;
if (screenY > -100 && screenY < HEIGHT + 100) {
// Tree trunk
ctx.fillStyle = '#4a3520';
ctx.fillRect(tree.x - 8, screenY, 16, tree.size);
// Tree top
ctx.fillStyle = '#1a5c3a';
ctx.beginPath();
ctx.moveTo(tree.x, screenY - tree.size * 0.5);
ctx.lineTo(tree.x - tree.size * 0.6, screenY + tree.size * 0.3);
ctx.lineTo(tree.x + tree.size * 0.6, screenY + tree.size * 0.3);
ctx.closePath();
ctx.fill();
}
});
}
function drawGate(gate) {
const screenY = gate.y + courseOffset;
if (screenY > -100 && screenY < HEIGHT + 100) {
const leftX = gate.x - gate.width / 2;
const rightX = gate.x + gate.width / 2;
// Gate poles
const poleColor = gate.cleared ? '#4a4a4a' : (gate.side > 0 ? '#ff3333' : '#3333ff');
ctx.fillStyle = poleColor;
// Left pole
ctx.fillRect(leftX - 10, screenY - 60, 20, 80);
ctx.fillStyle = gate.cleared ? '#666' : '#ffff00';
ctx.fillRect(leftX - 15, screenY - 65, 30, 15);
// Right pole
ctx.fillStyle = poleColor;
ctx.fillRect(rightX - 10, screenY - 60, 20, 80);
ctx.fillStyle = gate.cleared ? '#666' : '#ffff00';
ctx.fillRect(rightX - 15, screenY - 65, 30, 15);
// Jump ramp indicator
if (gate.type === 'jump' && !gate.cleared) {
ctx.fillStyle = '#ffa500';
ctx.font = 'bold 24px Arial';
ctx.textAlign = 'center';
ctx.fillText('🚀', gate.x, screenY - 80);
}
}
}
function drawObstacle(obs) {
const screenY = obs.y + courseOffset;
if (screenY > -100 && screenY < HEIGHT + 100) {
ctx.fillStyle = '#666';
ctx.beginPath();
ctx.arc(obs.x, screenY, obs.width / 2, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#888';
ctx.beginPath();
ctx.arc(obs.x - 5, screenY - 5, obs.width / 3, 0, Math.PI * 2);
ctx.fill();
}
}
function drawPlayer() {
ctx.save();
ctx.translate(player.x, player.y);
ctx.rotate(player.angle);
// Skier body
if (player.invincible > 0 && Math.floor(Date.now() / 100) % 2 === 0) {
ctx.globalAlpha = 0.5;
}
// Skis
ctx.fillStyle = '#ff6600';
ctx.fillRect(-18, 10, 12, 30);
ctx.fillRect(6, 10, 12, 30);
// Body
ctx.fillStyle = '#0066cc';
ctx.fillRect(-12, -10, 24, 25);
// Head
ctx.fillStyle = '#ffcc99';
ctx.beginPath();
ctx.arc(0, -20, 10, 0, Math.PI * 2);
ctx.fill();
// Helmet
ctx.fillStyle = '#cc0000';
ctx.beginPath();
ctx.arc(0, -23, 11, Math.PI, Math.PI * 2);
ctx.fill();
// Poles
ctx.strokeStyle = '#333';
ctx.lineWidth = 3;
ctx.beginPath();
ctx.moveTo(-15, 0);
ctx.lineTo(-20, 20);
ctx.moveTo(15, 0);
ctx.lineTo(20, 20);
ctx.stroke();
ctx.restore();
// Spray particles when carving
if (player.carving && Math.abs(player.turnSpeed) > 3) {
for (let i = 0; i < 3; i++) {
sprayParticles.push({
x: player.x + (Math.random() - 0.5) * 40,
y: player.y + 20,
vx: (Math.random() - 0.5) * 4 - player.turnSpeed * 0.5,
vy: Math.random() * 2 + 2,
life: 30,
size: Math.random() * 4 + 2
});
}
}
}
function drawSpray() {
sprayParticles.forEach((p, index) => {
ctx.fillStyle = `rgba(255, 255, 255, ${p.life / 30})`;
ctx.beginPath();
ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
ctx.fill();
});
}
// Update functions
function updatePlayer() {
// Steering
if (keys['ArrowLeft'] || keys['a'] || keys['A']) {
player.turnSpeed = Math.max(player.turnSpeed - 0.5, -player.maxTurnSpeed);
player.carving = true;
} else if (keys['ArrowRight'] || keys['d'] || keys['D']) {
player.turnSpeed = Math.min(player.turnSpeed + 0.5, player.maxTurnSpeed);
player.carving = true;
} else {
player.turnSpeed *= 0.9;
player.carving = false;
}
// Braking
if (keys['ArrowDown'] || keys['s'] || keys['S']) {
player.speed *= 0.95;
} else {
player.speed = Math.min(player.speed + player.acceleration, player.maxSpeed);
}
// Apply movement
player.x += player.turnSpeed;
player.angle = player.turnSpeed * 0.05;
// Boundaries
player.x = Math.max(50, Math.min(WIDTH - 50, player.x));
// Update invincibility
if (player.invincible > 0) player.invincible--;
}
function updateCourse() {
courseOffset += scrollSpeed + player.speed * 0.5;
// Update snow
snowParticles.forEach(p => {
p.y += p.speed + scrollSpeed * 0.5;
if (p.y > HEIGHT) {
p.y = -10;
p.x = Math.random() * WIDTH;
}
});
// Update spray
for (let i = sprayParticles.length - 1; i >= 0; i--) {
const p = sprayParticles[i];
p.x += p.vx;
p.y += p.vy;
p.life--;
if (p.life <= 0) {
sprayParticles.splice(i, 1);
}
}
}
function checkCollisions() {
// Check gates
gates.forEach(gate => {
if (!gate.cleared) {
const screenY = gate.y + courseOffset;
if (screenY > player.y - 50 && screenY < player.y + 50) {
const leftX = gate.x - gate.width / 2;
const rightX = gate.x + gate.width / 2;
if (player.x > leftX && player.x < rightX) {
gate.cleared = true;
gatesCleared++;
if (gate.type === 'jump') {
player.speed = Math.min(player.speed + 3, player.maxSpeed + 5);
}
} else if (screenY > player.y) {
// Missed gate
if (player.invincible === 0) {
player.lives--;
player.invincible = 60;
if (player.lives <= 0) {
endGame(false);
}
}
}
}
}
});
// Check obstacles
if (player.invincible === 0) {
obstacles.forEach(obs => {
const screenY = obs.y + courseOffset;
if (screenY > player.y - 30 && screenY < player.y + 30) {
const dx = player.x - obs.x;
const dy = player.y - screenY;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist < 35) {
player.lives--;
player.invincible = 60;
player.speed *= 0.5;
if (player.lives <= 0) {
endGame(false);
}
}
}
});
}
// Check finish
if (gatesCleared >= totalGates) {
endGame(true);
}
}
function endGame(completed) {
gameRunning = false;
const gameOverDiv = document.getElementById('gameOver');
const resultTitle = document.getElementById('resultTitle');
const finalTime = document.getElementById('finalTime');
const finalGates = document.getElementById('finalGates');
const finalScore = document.getElementById('finalScore');
if (completed) {
resultTitle.textContent = '🏆 Race Complete!';
resultTitle.style.color = '#4af';
} else {
resultTitle.textContent = '💥 Race Over';
resultTitle.style.color = '#f44';
}
finalTime.textContent = `⏱️ Time: ${gameTime.toFixed(1)}s`;
finalGates.textContent = `🎯 Gates Cleared: ${gatesCleared}/${totalGates}`;
const score = Math.floor((gatesCleared * 1000) - (gameTime * 10) + (player.lives * 500));
finalScore.textContent = `⭐ Score: ${Math.max(0, score)}`;
gameOverDiv.style.display = 'block';
}
function updateUI() {
document.getElementById('time').textContent = gameTime.toFixed(1);
document.getElementById('gates').textContent = gatesCleared;
document.getElementById('totalGates').textContent = totalGates;
document.getElementById('speed').textContent = Math.floor(player.speed * 10);
document.getElementById('lives').textContent = player.lives;
}
function gameLoop() {
if (!gameRunning) return;
const now = Date.now();
const deltaTime = (now - lastFrameTime) / 1000;
lastFrameTime = now;
gameTime += deltaTime;
// Clear canvas
ctx.fillStyle = '#e8f4f8';
ctx.fillRect(0, 0, WIDTH, HEIGHT);
// Draw course boundaries
ctx.fillStyle = '#c8e0e8';
ctx.fillRect(0, 0, 100, HEIGHT);
ctx.fillRect(WIDTH - 100, 0, 100, HEIGHT);
// Update
updatePlayer();
updateCourse();
checkCollisions();
// Draw
drawTrees();
obstacles.forEach(drawObstacle);
gates.forEach(drawGate);
drawSpray();
drawPlayer();
drawSnow();
// Update UI
updateUI();
requestAnimationFrame(gameLoop);
}
// Restart button
document.getElementById('restartBtn').addEventListener('click', function() {
document.getElementById('gameOver').style.display = 'none';
gameRunning = true;
gameTime = 0;
gatesCleared = 0;
courseOffset = 0;
player.x = WIDTH / 2;
player.speed = 0;
player.turnSpeed = 0;
player.lives = 3;
player.invincible = 0;
sprayParticles.length = 0;
lastFrameTime = Date.now();
initCourse();
gameLoop();
});
// Start game
initCourse();
gameLoop();
} catch(e) {
document.body.innerHTML = '<div style="color:white;padding:20px;text-align:center;">Game error: ' + e.message + '<br><br>Please refresh the page.</div>';
console.error(e);
}
});
</script>
</body>
</html>
Install GB3A on your device for a better experience!