🐍 Snake Ladder
← Back

Snakes & Ladders

Roll the dice, climb ladders, dodge snakes!
First to 100 wins the game.

🧑 You 0
🏁 100
0 🤖 CPU
🎲
🎲 Roll the dice to start!

🎉 You Win!

const SNAKES = { 99:54, 95:75, 93:73, 87:24, 64:60, 62:19, 54:34, 17:7, 98:79, 85:55, 68:32, 48:11, 28:8, 16:6 }; const LADDERS = { 4:14, 9:31, 21:42, 28:84, 36:44, 51:67, 71:91, 80:100, 15:78, 41:56, 61:84, 76:97 }; const DICE_FACES = ['⚀','⚁','⚂','⚃','⚄','⚅']; function getBoardLabel(pos) { if (LADDERS[pos]) return '🪜'; if (SNAKES[pos]) return '🐍'; return ''; } let state = {}; let youMarker, cpuMarker; let boardRectCache = null; function getStreak() { return JSON.parse(localStorage.getItem('sl_streak') || '0'); } function setStreak(n) { localStorage.setItem('sl_streak', JSON.stringify(n)); } function renderStreak() { const s = getStreak(); document.getElementById('streakDisplay').textContent = s > 0 ? '🔥 Win streak: ' + s : ''; } function showSetup() { document.querySelectorAll('.screen').forEach(s => s.classList.remove('active')); document.getElementById('setupScreen').classList.add('active'); renderStreak(); } function buildBoard() { const board = document.getElementById('board'); board.innerHTML = ''; const cells = []; for (let r = 0; r < 10; r++) { const rowStart = r * 10 + 1; const nums = []; for (let c = 0; c < 10; c++) { nums.push(r % 2 === 0 ? rowStart + c : rowStart + 9 - c); } cells.push(...nums); } cells.reverse().forEach(n => { const cell = document.createElement('div'); cell.className = 'cell' + (LADDERS[n] ? ' ladder' : '') + (SNAKES[n] ? ' snake' : '') + (n === 1 ? ' start' : ''); cell.id = 'cell-' + n; cell.innerHTML = '' + n + '' + getBoardLabel(n); board.appendChild(cell); }); boardRectCache = null; } function getCellPos(cellNum) { const cell = document.getElementById('cell-' + cellNum); if (!cell) return { x:0, y:0 }; const board = document.getElementById('board'); const br = board.getBoundingClientRect(); const cr = cell.getBoundingClientRect(); return { x: cr.left - br.left + cr.width / 2, y: cr.top - br.top + cr.height / 2 }; } function movePiece(marker, cellNum, smooth) { const pos = getCellPos(cellNum); const size = marker.offsetWidth || 14; const tx = pos.x - size / 2; const ty = pos.y - size / 2; if (!smooth) marker.style.transition = 'none'; marker.style.transform = 'translate(' + tx + 'px,' + ty + 'px)'; if (!smooth) { marker.offsetHeight; marker.style.transition = 'transform 0.08s cubic-bezier(0.34, 1.56, 0.64, 1)'; } } function updatePiecePosition(player, cellNum) { const marker = player === 'you' ? youMarker : cpuMarker; if (!marker) return; movePiece(marker, cellNum, true); const both = document.querySelector('.piece.both'); if (both) { const other = player === 'you' ? cpuMarker : youMarker; const oNum = player === 'you' ? state.cpu : state.you; if (oNum === cellNum) { both.style.display = 'block'; marker.style.display = 'none'; other.style.display = 'none'; movePiece(both, cellNum, true); } } } function refreshPieces() { document.querySelectorAll('.piece').forEach(p => p.remove()); youMarker = null; cpuMarker = null; if (!state || state.done) return; const board = document.getElementById('board'); const wrap = document.getElementById('boardWrap'); youMarker = document.createElement('div'); youMarker.className = 'piece you'; wrap.appendChild(youMarker); movePiece(youMarker, state.you || 1, false); cpuMarker = document.createElement('div'); cpuMarker.className = 'piece cpu'; wrap.appendChild(cpuMarker); movePiece(cpuMarker, state.cpu || 1, false); if ((state.you || 1) === (state.cpu || 1)) { const both = document.createElement('div'); both.className = 'piece both'; wrap.appendChild(both); youMarker.style.display = 'none'; cpuMarker.style.display = 'none'; movePiece(both, state.you || 1, false); } } function rebuildPieces() { document.querySelectorAll('.piece').forEach(p => p.remove()); youMarker = null; cpuMarker = null; const boardWrap = document.getElementById('boardWrap'); youMarker = document.createElement('div'); youMarker.className = 'piece you'; boardWrap.appendChild(youMarker); cpuMarker = document.createElement('div'); cpuMarker.className = 'piece cpu'; boardWrap.appendChild(cpuMarker); } function placeMarker(pos, player) { if (state.done) return; updatePiecePosition(player, pos); if (player === 'you') { document.getElementById('youPos').textContent = pos; } else { document.getElementById('cpuPos').textContent = pos; } } function startGame() { state = { you: 1, cpu: 1, turn: 'you', done: false }; buildBoard(); rebuildPieces(); movePiece(youMarker, 1, false); movePiece(cpuMarker, 1, false); document.getElementById('youPos').textContent = '1'; document.getElementById('cpuPos').textContent = '1'; document.getElementById('youBadge').className = 'player-badge active-you'; document.getElementById('cpuBadge').className = 'player-badge'; document.getElementById('dice').textContent = '🎲'; document.getElementById('msgArea').textContent = '🎲 Your turn! Roll the dice!'; document.getElementById('msgArea').className = 'msg neutral'; document.getElementById('rollBtn').disabled = false; document.getElementById('boardWrap').classList.remove('board-shake'); document.querySelectorAll('.screen').forEach(s => s.classList.remove('active')); document.getElementById('gameScreen').classList.add('active'); } function rollDice() { return Math.floor(Math.random() * 6) + 1; } function animateDice(value, cb) { const el = document.getElementById('dice'); el.classList.remove('rolling', 'shake'); el.offsetHeight; el.classList.add('rolling'); let i = 0; const interval = setInterval(() => { el.textContent = DICE_FACES[Math.floor(Math.random() * 6)]; if (++i > 8) { clearInterval(interval); el.textContent = DICE_FACES[value - 1]; el.classList.remove('rolling'); el.classList.add('shake'); setTimeout(() => { el.classList.remove('shake'); if (cb) cb(); }, 250); } }, 50); } function sleep(ms) { return new Promise(r => setTimeout(r, ms)); } async function animateMove(player, from, to, _label) { const marker = player === 'you' ? youMarker : cpuMarker; const step = from < to ? 1 : -1; for (let p = from + step; step > 0 ? p <= to : p >= to; p += step) { if (state.done) return; movePiece(marker, p, true); if (player === 'you') document.getElementById('youPos').textContent = p; else document.getElementById('cpuPos').textContent = p; await sleep(70); } } async function animateSnake(player, from, to) { const marker = player === 'you' ? youMarker : cpuMarker; const wrap = document.getElementById('boardWrap'); const el = document.getElementById('msgArea'); el.className = 'msg snake-anim'; el.textContent = '🐍 Snake! ' + from + ' → ' + to; document.getElementById('cell-' + from).classList.add('highlight'); wrap.classList.remove('board-shake'); wrap.offsetHeight; wrap.classList.add('board-shake'); movePiece(marker, to, false); if (player === 'you') document.getElementById('youPos').textContent = to; else document.getElementById('cpuPos').textContent = to; await sleep(400); wrap.classList.remove('board-shake'); document.getElementById('cell-' + from).classList.remove('highlight'); } async function animateLadder(player, from, to) { const marker = player === 'you' ? youMarker : cpuMarker; const el = document.getElementById('msgArea'); el.className = 'msg ladder-anim'; el.textContent = '🪜 Ladder! ' + from + ' → ' + to; document.getElementById('cell-' + from).classList.add('highlight'); movePiece(marker, to, false); if (player === 'you') document.getElementById('youPos').textContent = to; else document.getElementById('cpuPos').textContent = to; await sleep(350); document.getElementById('cell-' + from).classList.remove('highlight'); } function playTone(freq, dur) { try { const ctx = new (window.AudioContext || window.webkitAudioContext)(); const osc = ctx.createOscillator(); const gain = ctx.createGain(); osc.connect(gain); gain.connect(ctx.destination); osc.frequency.value = freq; gain.gain.value = 0.08; gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + dur); osc.start(ctx.currentTime); osc.stop(ctx.currentTime + dur); } catch (e) { /* audio unavailable */ } } function playSound(name) { const sounds = { roll: [440, 0.15], move: [520, 0.08], ladder: [660, 0.25], snake: [220, 0.3], win: [880, 0.4], lose: [330, 0.3], }; const s = sounds[name]; if (s) playTone(s[0], s[1]); } async function yourTurn() { if (state.done) return; document.getElementById('rollBtn').disabled = true; state.turn = 'you'; document.getElementById('youBadge').className = 'player-badge active-you'; document.getElementById('cpuBadge').className = 'player-badge'; const roll = rollDice(); playSound('roll'); await new Promise(cb => animateDice(roll, cb)); let newPos = state.you + roll; if (newPos > 100) { document.getElementById('msgArea').textContent = '🙅 Need exactly 100! Rolled ' + roll + ' — stay at ' + state.you; document.getElementById('msgArea').className = 'msg fail'; document.getElementById('rollBtn').disabled = false; return; } await animateMove('you', state.you, newPos); playSound('move'); state.you = newPos; if (LADDERS[newPos]) { const dest = LADDERS[newPos]; playSound('ladder'); await animateLadder('you', newPos, dest); state.you = dest; } else if (SNAKES[newPos]) { const dest = SNAKES[newPos]; playSound('snake'); await animateSnake('you', newPos, dest); state.you = dest; } document.getElementById('youPos').textContent = state.you; if (state.you === 100) { playSound('win'); endGame('you'); return; } await sleep(400); cpuTurn(); } async function cpuTurn() { if (state.done) return; state.turn = 'cpu'; document.getElementById('youBadge').className = 'player-badge'; document.getElementById('cpuBadge').className = 'player-badge active-cpu'; const roll = rollDice(); playSound('roll'); await new Promise(cb => animateDice(roll, cb)); let newPos = state.cpu + roll; if (newPos > 100) { document.getElementById('msgArea').textContent = '🤖 CPU rolled ' + roll + ' — stays at ' + state.cpu; document.getElementById('msgArea').className = 'msg info'; state.turn = 'you'; document.getElementById('youBadge').className = 'player-badge active-you'; document.getElementById('cpuBadge').className = 'player-badge'; document.getElementById('rollBtn').disabled = false; return; } await animateMove('cpu', state.cpu, newPos); state.cpu = newPos; if (LADDERS[newPos]) { const dest = LADDERS[newPos]; playSound('ladder'); await animateLadder('cpu', newPos, dest); state.cpu = dest; } else if (SNAKES[newPos]) { const dest = SNAKES[newPos]; playSound('snake'); await animateSnake('cpu', newPos, dest); state.cpu = dest; } document.getElementById('cpuPos').textContent = state.cpu; if (state.cpu === 100) { playSound('lose'); endGame('cpu'); return; } state.turn = 'you'; document.getElementById('youBadge').className = 'player-badge active-you'; document.getElementById('cpuBadge').className = 'player-badge'; document.getElementById('rollBtn').disabled = false; document.getElementById('msgArea').textContent = '🎲 Your turn! Roll!'; document.getElementById('msgArea').className = 'msg neutral'; } function endGame(winner) { state.done = true; document.getElementById('rollBtn').disabled = true; let streak = getStreak(); if (winner === 'you') { streak++; setStreak(streak); } else { setStreak(0); } document.querySelectorAll('.screen').forEach(s => s.classList.remove('active')); document.getElementById('resultScreen').classList.add('active'); if (winner === 'you') { document.getElementById('resultTitle').textContent = '🎉 You Win!'; document.getElementById('resultDetail').textContent = 'Reached 100! CPU was at ' + state.cpu + '. Streak: ' + streak + ' 🔥'; launchConfetti(); } else { document.getElementById('resultTitle').textContent = '😞 CPU Wins'; document.getElementById('resultDetail').textContent = 'CPU reached 100. You were at ' + state.you + '.'; } } // ── Confetti ── function launchConfetti() { const canvas = document.getElementById('confettiCanvas'); const ctx = canvas.getContext('2d'); canvas.width = window.innerWidth; canvas.height = window.innerHeight; const pieces = []; for (let i = 0; i < 120; i++) { const colors = ['#fbbf24','#f97316','#ec4899','#8b5cf6','#3b82f6','#4ade80','#f87171']; pieces.push({ x: canvas.width * Math.random(), y: canvas.height * Math.random() - canvas.height, w: 4 + Math.random() * 4, h: 4 + Math.random() * 6, color: colors[Math.floor(Math.random() * colors.length)], vx: (Math.random() - 0.5) * 4, vy: 2 + Math.random() * 3, rot: Math.random() * 360, rv: (Math.random() - 0.5) * 8, gravity: 0.06 + Math.random() * 0.04, }); } let frame; function draw() { ctx.clearRect(0, 0, canvas.width, canvas.height); let alive = false; for (const p of pieces) { p.x += p.vx; p.vy += p.gravity; p.y += p.vy; p.rot += p.rv; if (p.y < canvas.height + 20) { alive = true; ctx.save(); ctx.translate(p.x, p.y); ctx.rotate(p.rot * Math.PI / 180); ctx.fillStyle = p.color; ctx.fillRect(-p.w / 2, -p.h / 2, p.w, p.h); ctx.restore(); } } if (alive) frame = requestAnimationFrame(draw); } cancelAnimationFrame(frame); draw(); } renderStreak();