Chotto.app
Login
Sign Up
ES
App Name
kuyawa / hanoi
App Icon in SVG
<svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"> <defs> <!-- Vibrant App Background Gradient --> <linearGradient id="bgGradient" x1="0%" y1="0%" x2="100%" y2="100%"> <stop offset="0%" stop-color="#7C3AED" /> <!-- Vibrant Purple --> <stop offset="100%" stop-color="#4338CA" /> <!-- Deep Indigo --> </linearGradient> <!-- Drop Shadow for 3D Depth --> <filter id="shadow" x="-20%" y="-20%" width="140%" height="140%"> <feDropShadow dx="0" dy="8" stdDeviation="8" flood-color="#000000" flood-opacity="0.35"/> </filter> <!-- Glossy Overlay for Disks --> <linearGradient id="diskGloss" x1="0%" y1="0%" x2="0%" y2="100%"> <stop offset="0%" stop-color="#ffffff" stop-opacity="0.4" /> <stop offset="45%" stop-color="#ffffff" stop-opacity="0.1" /> <stop offset="50%" stop-color="#000000" stop-opacity="0.05" /> <stop offset="100%" stop-color="#000000" stop-opacity="0.3" /> </linearGradient> </defs> <!-- App Icon Rounded Rectangle Background --> <rect width="512" height="512" rx="115" fill="url(#bgGradient)" /> <!-- Base and Pegs (Sleek Silver/Slate) --> <g filter="url(#shadow)"> <!-- Base Platform --> <rect x="66" y="390" width="380" height="24" rx="12" fill="#E2E8F0" /> <!-- Peg 1 (Left) --> <rect x="118" y="160" width="16" height="240" rx="8" fill="#E2E8F0" /> <!-- Peg 2 (Center) --> <rect x="248" y="160" width="16" height="240" rx="8" fill="#E2E8F0" /> <!-- Peg 3 (Right) --> <rect x="378" y="160" width="16" height="240" rx="8" fill="#E2E8F0" /> </g> <!-- Gameplay Elements (Disks) --> <g filter="url(#shadow)"> <!-- Peg 1 Disks (Left side) --> <!-- Disk 4 (Largest - Red) --> <rect x="61" y="354" width="130" height="36" rx="18" fill="#EF4444" /> <rect x="61" y="354" width="130" height="36" rx="18" fill="url(#diskGloss)" /> <!-- Disk 3 (Large - Orange) --> <rect x="76" y="318" width="100" height="36" rx="18" fill="#F97316" /> <rect x="76" y="318" width="100" height="36" rx="18" fill="url(#diskGloss)" /> <!-- Peg 2 Disk (Center side) --> <!-- Disk 2 (Medium - Yellow) --> <rect x="216" y="354" width="80" height="36" rx="18" fill="#EAB308" /> <rect x="216" y="354" width="80" height="36" rx="18" fill="url(#diskGloss)" /> <!-- Peg 3 (Right side) - Moving Disk --> <!-- Subtle dashed motion line dropping down to Peg 3 --> <line x1="386" y1="180" x2="386" y2="280" stroke="#E2E8F0" stroke-width="6" stroke-dasharray="10 10" stroke-linecap="round" opacity="0.5" /> <!-- Disk 1 (Smallest - Green) floating mid-air above Peg 3 --> <rect x="358" y="130" width="56" height="36" rx="18" fill="#22C55E" /> <rect x="358" y="130" width="56" height="36" rx="18" fill="url(#diskGloss)" /> </g> </svg>
HTML Content
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" /> <title>Tower of Hanoi · toy colors</title> <style> * { box-sizing: border-box; margin: 0; padding: 0; } body { background: radial-gradient(circle at 20% 20%, #151e2a, #0a0e14); min-height: 100vh; display: flex; justify-content: center; align-items: center; font-family: 'Segoe UI', system-ui, -apple-system, 'Helvetica Neue', sans-serif; padding: 1.2rem; margin: 0; } .game-container { background: rgba(18, 25, 35, 0.8); backdrop-filter: blur(6px); border-radius: 56px; padding: 2rem 2rem 2.5rem; max-width: 820px; width: 100%; border: 1px solid rgba(255, 255, 255, 0.04); box-shadow: 0 30px 60px -12px rgba(0, 0, 0, 0.9), inset 0 1px 2px rgba(255, 255, 255, 0.05); transition: all 0.2s; } /* header */ .header { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 0.8rem 1rem; margin-bottom: 1.8rem; } .title-group { display: flex; align-items: center; gap: 0.8rem; } .title { font-size: 2.2rem; font-weight: 700; letter-spacing: -0.5px; background: linear-gradient(145deg, #f0e6c5, #d4c9a8); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; text-shadow: 0 2px 12px rgba(212, 201, 168, 0.15); } .badge { background: rgba(255, 215, 120, 0.12); border: 1px solid rgba(255, 215, 120, 0.15); border-radius: 40px; padding: 0.2rem 1rem; font-size: 0.8rem; font-weight: 600; color: #d4c9a8; letter-spacing: 0.8px; backdrop-filter: blur(2px); } .stats { display: flex; align-items: center; gap: 1.5rem; background: rgba(0, 0, 0, 0.3); padding: 0.3rem 1.2rem 0.3rem 1.8rem; border-radius: 60px; border: 1px solid rgba(255, 255, 255, 0.03); } .stat-item { display: flex; align-items: baseline; gap: 0.3rem; } .stat-label { font-size: 0.7rem; text-transform: uppercase; letter-spacing: 1.5px; color: #8492a8; font-weight: 500; } .stat-value { font-size: 1.6rem; font-weight: 700; color: #eef3fc; line-height: 1.2; min-width: 2rem; text-align: center; } /* controls */ .controls { display: flex; flex-wrap: wrap; justify-content: space-between; align-items: center; gap: 0.8rem 1.2rem; margin-bottom: 1.8rem; } .control-group { display: flex; flex-wrap: wrap; align-items: center; gap: 0.6rem; } .btn { background: rgba(30, 40, 55, 0.7); backdrop-filter: blur(2px); border: 1px solid rgba(255, 255, 255, 0.05); color: #dce4f0; font-weight: 600; font-size: 0.9rem; padding: 0.5rem 1.2rem; border-radius: 40px; cursor: pointer; transition: 0.2s ease; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4); display: inline-flex; align-items: center; gap: 6px; letter-spacing: 0.3px; } .btn:hover { background: rgba(50, 65, 88, 0.85); color: white; transform: scale(0.96); border-color: rgba(255, 215, 120, 0.2); } .btn:active { transform: scale(0.92); } .btn-primary { background: #c9b07a; color: #141c26; border-color: #dac28e; font-weight: 700; box-shadow: 0 4px 14px rgba(201, 176, 122, 0.2); } .btn-primary:hover { background: #dcc28a; color: #0b1016; border-color: #e8d09a; } .btn-icon { font-size: 1.2rem; line-height: 1; } select { background: rgba(20, 28, 40, 0.8); border: 1px solid rgba(255, 255, 255, 0.08); color: #dce4f0; padding: 0.5rem 1.2rem; border-radius: 40px; font-weight: 600; font-size: 0.9rem; cursor: pointer; backdrop-filter: blur(2px); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); outline: none; transition: 0.2s; } select:hover { background: rgba(40, 54, 74, 0.9); border-color: rgba(255, 215, 120, 0.2); } select option { background: #1a2533; } /* board */ .board-wrapper { background: rgba(6, 10, 16, 0.5); border-radius: 48px; padding: 1.5rem 1rem 1rem; box-shadow: inset 0 8px 18px rgba(0, 0, 0, 0.7), 0 2px 6px rgba(0, 0, 0, 0.3); border: 1px solid rgba(255, 255, 255, 0.02); position: relative; } .board { display: flex; justify-content: space-around; align-items: flex-end; gap: 0.4rem; min-height: 340px; padding: 0.8rem 0.2rem 0.2rem; position: relative; } .peg-wrapper { display: flex; flex-direction: column; align-items: center; flex: 1; position: relative; cursor: pointer; transition: 0.15s; padding: 0 0.2rem; } .peg-wrapper.selected { transform: translateY(-6px); } .peg-wrapper.selected .peg-base { box-shadow: 0 0 20px rgba(201, 176, 122, 0.3); } .peg-label { font-size: 0.7rem; color: #5f7188; font-weight: 600; letter-spacing: 2px; margin-top: 0.6rem; opacity: 0.5; transition: 0.2s; } .peg-wrapper.selected .peg-label { color: #d4c9a8; opacity: 1; } .peg-rod { width: 8px; height: 120px; background: linear-gradient(180deg, #8a7a5e, #4d4232); border-radius: 20px 20px 4px 4px; box-shadow: inset -2px -4px 6px rgba(0,0,0,0.6), 0 4px 8px rgba(0,0,0,0.3); position: relative; z-index: 1; transition: height 0.2s; } .peg-base { width: 100%; max-width: 140px; height: 14px; background: linear-gradient(180deg, #564b3a, #3d3427); border-radius: 40px; box-shadow: 0 6px 12px rgba(0,0,0,0.6), inset 0 2px 4px rgba(255,215,140,0.08); margin-top: -4px; transition: 0.2s; } .disc-container { display: flex; flex-direction: column-reverse; align-items: center; position: absolute; bottom: 14px; left: 0; right: 0; z-index: 2; pointer-events: none; gap: 4px; } .disc { height: 24px; border-radius: 40px; box-shadow: 0 4px 8px rgba(0,0,0,0.5), inset 0 2px 6px rgba(255, 255, 255, 0.25); transition: all 0.2s cubic-bezier(0.34, 1.2, 0.64, 1); pointer-events: auto; cursor: grab; border: 1px solid rgba(255, 255, 255, 0.15); min-width: 20px; width: 100%; max-width: 120px; } .disc:active { cursor: grabbing; } .disc.selected-disc { transform: scale(1.04) translateY(-8px); box-shadow: 0 12px 28px rgba(255, 215, 0, 0.35), inset 0 2px 8px rgba(255, 255, 255, 0.4); border-color: #fff8e0; } /* 🎨 PRIMARY COLOR TOY DISCS */ .disc-1 { background: radial-gradient(ellipse at 35% 30%, #ff6b6b, #c0392b); } /* Red */ .disc-2 { background: radial-gradient(ellipse at 35% 30%, #ff9f43, #e67e22); } /* Orange */ .disc-3 { background: radial-gradient(ellipse at 35% 30%, #feca57, #f39c12); } /* Yellow */ .disc-4 { background: radial-gradient(ellipse at 35% 30%, #55efc4, #00b894); } /* Mint / Teal */ .disc-5 { background: radial-gradient(ellipse at 35% 30%, #74b9ff, #0984e3); } /* Blue */ .disc-6 { background: radial-gradient(ellipse at 35% 30%, #a29bfe, #6c5ce7); } /* Purple */ .disc-7 { background: radial-gradient(ellipse at 35% 30%, #fd79a8, #e84393); } /* Pink */ .disc-8 { background: radial-gradient(ellipse at 35% 30%, #81ecec, #00cec9); } /* Cyan */ /* win overlay */ .win-overlay { display: none; position: absolute; inset: 0; background: rgba(0, 0, 0, 0.7); backdrop-filter: blur(8px); border-radius: 48px; flex-direction: column; justify-content: center; align-items: center; z-index: 20; color: white; padding: 2rem; text-align: center; } .win-overlay.show { display: flex; } .win-overlay h2 { font-size: 3rem; font-weight: 700; background: linear-gradient(135deg, #f6e9a6, #e8d07a); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; margin-bottom: 0.2rem; } .win-overlay p { opacity: 0.7; font-size: 1.1rem; margin-bottom: 1.8rem; } /* responsive */ @media (max-width: 600px) { .game-container { padding: 1.2rem; border-radius: 32px; } .title { font-size: 1.6rem; } .stats { padding: 0.2rem 0.8rem 0.2rem 1.2rem; gap: 0.8rem; } .stat-value { font-size: 1.2rem; min-width: 1.4rem; } .board { min-height: 260px; } .peg-rod { height: 80px; } .disc { height: 20px; } .btn { padding: 0.3rem 1rem; font-size: 0.8rem; } .controls { flex-direction: column; align-items: stretch; } .control-group { justify-content: center; } .board-wrapper { padding: 1rem 0.2rem 0.5rem; } } @media (max-width: 420px) { .disc { height: 16px; } .peg-rod { height: 60px; } .board { min-height: 200px; } .header { flex-direction: column; align-items: flex-start; } } .footer-tip { margin-top: 1.2rem; text-align: center; color: #4d5f74; font-size: 0.75rem; letter-spacing: 0.8px; opacity: 0.5; } </style> </head> <body> <div class="game-container"> <div class="header"> <div class="title-group"> <span class="title">⌗ Hanoi</span> <span class="badge">toy</span> </div> <div class="stats"> <div class="stat-item"><span class="stat-label">Moves</span><span class="stat-value" id="moveCounter">0</span></div> <div class="stat-item"><span class="stat-label">Min</span><span class="stat-value" id="minMoves">7</span></div> </div> </div> <div class="controls"> <div class="control-group"> <select id="diskSelector"> <option value="3">3 discs</option> <option value="4">4 discs</option> <option value="5" selected>5 discs</option> <option value="6">6 discs</option> <option value="7">7 discs</option> </select> <button class="btn" id="resetBtn"><span class="btn-icon">↻</span> Reset</button> <button class="btn" id="undoBtn"><span class="btn-icon">↩</span> Undo</button> </div> <button class="btn btn-primary" id="solveBtn">✨ Solve</button> </div> <div class="board-wrapper"> <div class="board" id="boardContainer"> <!-- pegs will be injected --> </div> <div class="win-overlay" id="winOverlay"> <h2>✦ Solved ✦</h2> <p>completed in <span id="winMoves">0</span> moves</p> <button class="btn btn-primary" id="winResetBtn" style="padding:0.7rem 2.4rem;">Play again</button> </div> </div> <div class="footer-tip">click a peg to select top disc · click another peg to move</div> </div> <script> (function() { // ---- state ---- const PEGS = 3; let disks = 5; let towers = [[], [], []]; let moves = 0; let selectedPeg = null; // index of peg whose top disc is selected let history = []; // for undo: store { towers, moves } let gameWon = false; let solving = false; let solveTimer = null; // DOM refs const boardEl = document.getElementById('boardContainer'); const moveCounter = document.getElementById('moveCounter'); const minMovesEl = document.getElementById('minMoves'); const winOverlay = document.getElementById('winOverlay'); const winMoves = document.getElementById('winMoves'); const diskSelector = document.getElementById('diskSelector'); const resetBtn = document.getElementById('resetBtn'); const undoBtn = document.getElementById('undoBtn'); const solveBtn = document.getElementById('solveBtn'); const winResetBtn = document.getElementById('winResetBtn'); // ---- helpers ---- function getMinMoves(n) { return Math.pow(2, n) - 1; } function cloneTowers(t) { return t.map(peg => [...peg]); } function saveState() { history.push({ towers: cloneTowers(towers), moves: moves }); if (history.length > 30) history.shift(); } // ---- game logic ---- function initGame(diskCount) { disks = diskCount || disks; towers = [ [], [], [] ]; for (let i = disks; i >= 1; i--) towers[0].push(i); moves = 0; selectedPeg = null; gameWon = false; solving = false; if (solveTimer) { clearTimeout(solveTimer); solveTimer = null; } history = []; updateUI(); winOverlay.classList.remove('show'); minMovesEl.textContent = getMinMoves(disks); diskSelector.value = disks; } // move disc from peg 'from' to peg 'to' (returns true if success) function moveDisc(from, to) { if (from === to) return false; if (towers[from].length === 0) return false; const topFrom = towers[from][towers[from].length - 1]; if (towers[to].length > 0) { const topTo = towers[to][towers[to].length - 1]; if (topFrom > topTo) return false; } // save state before move saveState(); // move const disc = towers[from].pop(); towers[to].push(disc); moves++; selectedPeg = null; updateUI(); checkWin(); return true; } function checkWin() { if (towers[2].length === disks) { gameWon = true; winMoves.textContent = moves; winOverlay.classList.add('show'); return true; } return false; } // ---- UI render ---- function updateUI() { moveCounter.textContent = moves; // render pegs boardEl.innerHTML = ''; const pegLabels = ['A', 'B', 'C']; for (let p = 0; p < PEGS; p++) { const wrapper = document.createElement('div'); wrapper.className = 'peg-wrapper'; if (selectedPeg === p) wrapper.classList.add('selected'); wrapper.dataset.peg = p; // rod const rod = document.createElement('div'); rod.className = 'peg-rod'; wrapper.appendChild(rod); // disc container (absolute) const discContainer = document.createElement('div'); discContainer.className = 'disc-container'; // add discs from bottom to top (towers[p] is bottom->top) const pegDiscs = towers[p]; for (let i = 0; i < pegDiscs.length; i++) { const discSize = pegDiscs[i]; const discEl = document.createElement('div'); discEl.className = `disc disc-${discSize}`; // width proportional: from 30% to 90% of container const widthPercent = 25 + (discSize / disks) * 55; discEl.style.width = `${Math.min(widthPercent, 94)}%`; discEl.style.maxWidth = '120px'; discEl.dataset.discSize = discSize; discEl.dataset.peg = p; // if this disc is top and selected peg is this peg, highlight if (selectedPeg === p && i === pegDiscs.length - 1) { discEl.classList.add('selected-disc'); } discContainer.appendChild(discEl); } wrapper.appendChild(discContainer); // base const base = document.createElement('div'); base.className = 'peg-base'; wrapper.appendChild(base); // label const label = document.createElement('div'); label.className = 'peg-label'; label.textContent = pegLabels[p]; wrapper.appendChild(label); // click handler on wrapper wrapper.addEventListener('click', (e) => { e.stopPropagation(); if (solving || gameWon) return; const pegIndex = parseInt(wrapper.dataset.peg); handlePegClick(pegIndex); }); boardEl.appendChild(wrapper); } // update min moves minMovesEl.textContent = getMinMoves(disks); } // ---- click handler ---- function handlePegClick(pegIndex) { if (solving || gameWon) return; // if no peg selected: select if peg has discs if (selectedPeg === null) { if (towers[pegIndex].length > 0) { selectedPeg = pegIndex; updateUI(); } return; } // if same peg: deselect if (selectedPeg === pegIndex) { selectedPeg = null; updateUI(); return; } // try move from selectedPeg to pegIndex const from = selectedPeg; const to = pegIndex; const success = moveDisc(from, to); if (!success) { // invalid move: deselect selectedPeg = null; updateUI(); } // else moveDisc already updates UI and clears selectedPeg } // ---- undo ---- function undoMove() { if (solving || gameWon) return; if (history.length === 0) return; const prev = history.pop(); towers = prev.towers; moves = prev.moves; selectedPeg = null; updateUI(); winOverlay.classList.remove('show'); gameWon = false; } // ---- solve (recursive with animation) ---- function solveHanoi() { if (solving) return; if (gameWon) return; if (towers[2].length === disks) { return; } history = []; solving = true; const solution = []; function generate(n, from, to, aux) { if (n === 0) return; generate(n-1, from, aux, to); solution.push({ from, to }); generate(n-1, aux, to, from); } generate(disks, 0, 2, 1); // reset game state const diskCount = disks; towers = [ [], [], [] ]; for (let i = diskCount; i >= 1; i--) towers[0].push(i); moves = 0; selectedPeg = null; gameWon = false; winOverlay.classList.remove('show'); updateUI(); let step = 0; function doStep() { if (step >= solution.length) { solving = false; checkWin(); return; } const { from, to } = solution[step]; const disc = towers[from].pop(); towers[to].push(disc); moves++; step++; updateUI(); if (towers[2].length === disks) { solving = false; checkWin(); return; } solveTimer = setTimeout(doStep, 180); } solveTimer = setTimeout(doStep, 200); } // ---- reset ---- function resetGame() { if (solveTimer) { clearTimeout(solveTimer); solveTimer = null; } solving = false; initGame(disks); } // ---- event listeners ---- diskSelector.addEventListener('change', (e) => { const val = parseInt(e.target.value); if (val !== disks) { if (solveTimer) { clearTimeout(solveTimer); solveTimer = null; } solving = false; initGame(val); } }); resetBtn.addEventListener('click', resetGame); undoBtn.addEventListener('click', undoMove); solveBtn.addEventListener('click', solveHanoi); winResetBtn.addEventListener('click', () => { winOverlay.classList.remove('show'); resetGame(); }); // ---- start ---- initGame(5); })(); </script> </body> </html>
Cancel