| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>Ball Sort Puzzle</title> |
| <style> |
| * { |
| margin: 0; |
| padding: 0; |
| box-sizing: border-box; |
| font-family: 'Arial', sans-serif; |
| } |
| |
| body { |
| background: linear-gradient(135deg, #1a2a6c, #b21f1f, #fdbb2d); |
| height: 100vh; |
| display: flex; |
| flex-direction: column; |
| align-items: center; |
| justify-content: center; |
| color: white; |
| overflow: hidden; |
| } |
| |
| .game-container { |
| display: flex; |
| flex-direction: column; |
| align-items: center; |
| width: 100%; |
| max-width: 800px; |
| padding: 20px; |
| } |
| |
| h1 { |
| margin-bottom: 20px; |
| font-size: 2.5rem; |
| text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5); |
| text-align: center; |
| } |
| |
| .tubes-container { |
| display: flex; |
| flex-wrap: wrap; |
| justify-content: center; |
| gap: 20px; |
| width: 100%; |
| margin: 30px 0; |
| } |
| |
| .tube { |
| width: 60px; |
| height: 200px; |
| background-color: rgba(255, 255, 255, 0.1); |
| border-radius: 5px 5px 0 0; |
| position: relative; |
| cursor: pointer; |
| border: 2px solid rgba(255, 255, 255, 0.3); |
| display: flex; |
| flex-direction: column-reverse; |
| align-items: center; |
| transition: transform 0.2s; |
| margin-bottom: 40px; |
| } |
| |
| .tube::after { |
| content: ''; |
| position: absolute; |
| bottom: -20px; |
| left: -2px; |
| right: -2px; |
| height: 20px; |
| background-color: rgba(255, 255, 255, 0.1); |
| border-radius: 0 0 5px 5px; |
| border: 2px solid rgba(255, 255, 255, 0.3); |
| } |
| |
| .tube:hover { |
| transform: translateY(-5px); |
| } |
| |
| .tube.selected { |
| transform: scale(1.05); |
| box-shadow: 0 0 15px rgba(255, 255, 255, 0.7); |
| } |
| |
| .ball { |
| width: 50px; |
| height: 50px; |
| border-radius: 50%; |
| margin-top: 5px; |
| transition: all 0.3s ease; |
| position: relative; |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| font-weight: bold; |
| color: rgba(0, 0, 0, 0.5); |
| box-shadow: inset -5px -5px 10px rgba(0, 0, 0, 0.2); |
| } |
| |
| .ball::after { |
| content: ''; |
| position: absolute; |
| top: 10px; |
| left: 10px; |
| width: 15px; |
| height: 15px; |
| border-radius: 50%; |
| background-color: rgba(255, 255, 255, 0.4); |
| } |
| |
| .controls { |
| display: flex; |
| gap: 20px; |
| margin-top: 20px; |
| } |
| |
| button { |
| padding: 10px 20px; |
| font-size: 1rem; |
| background-color: rgba(255, 255, 255, 0.2); |
| color: white; |
| border: 2px solid white; |
| border-radius: 5px; |
| cursor: pointer; |
| transition: all 0.3s; |
| text-transform: uppercase; |
| letter-spacing: 1px; |
| font-weight: bold; |
| } |
| |
| button:hover { |
| background-color: rgba(255, 255, 255, 0.4); |
| transform: translateY(-2px); |
| } |
| |
| .level-info { |
| margin-top: 20px; |
| font-size: 1.2rem; |
| text-align: center; |
| } |
| |
| .message { |
| position: fixed; |
| top: 50%; |
| left: 50%; |
| transform: translate(-50%, -50%); |
| background-color: rgba(0, 0, 0, 0.8); |
| color: white; |
| padding: 30px 50px; |
| border-radius: 10px; |
| font-size: 2rem; |
| display: none; |
| z-index: 100; |
| text-align: center; |
| box-shadow: 0 0 20px rgba(0, 0, 0, 0.5); |
| } |
| |
| .message button { |
| margin-top: 20px; |
| display: block; |
| width: 100%; |
| } |
| |
| .move-counter { |
| font-size: 1.2rem; |
| margin-bottom: 20px; |
| background-color: rgba(0, 0, 0, 0.3); |
| padding: 10px 20px; |
| border-radius: 5px; |
| } |
| |
| @media (max-width: 600px) { |
| .tube { |
| width: 50px; |
| height: 180px; |
| } |
| |
| .ball { |
| width: 45px; |
| height: 45px; |
| } |
| |
| h1 { |
| font-size: 1.8rem; |
| } |
| } |
| |
| .floating-balls { |
| position: absolute; |
| top: 0; |
| left: 0; |
| width: 100%; |
| height: 100%; |
| pointer-events: none; |
| z-index: -1; |
| } |
| |
| .floating-ball { |
| position: absolute; |
| border-radius: 50%; |
| opacity: 0.3; |
| animation: float 15s infinite linear; |
| } |
| |
| @keyframes float { |
| 0% { |
| transform: translate(0, 0) rotate(0deg); |
| } |
| 25% { |
| transform: translate(100px, 50px) rotate(90deg); |
| } |
| 50% { |
| transform: translate(200px, -50px) rotate(180deg); |
| } |
| 75% { |
| transform: translate(100px, 50px) rotate(270deg); |
| } |
| 100% { |
| transform: translate(0, 0) rotate(360deg); |
| } |
| } |
| </style> |
| </head> |
| <body> |
| <div class="floating-balls" id="floatingBalls"></div> |
| |
| <div class="game-container"> |
| <h1>Ball Sort Puzzle</h1> |
| <div class="move-counter">Moves: <span id="moves">0</span></div> |
| <div class="tubes-container" id="tubes"></div> |
| <div class="level-info">Level: <span id="level">1</span></div> |
| <div class="controls"> |
| <button id="btnNewGame">New Game</button> |
| <button id="btnUndo">Undo</button> |
| </div> |
| </div> |
|
|
| <div class="message" id="winMessage"> |
| Level Complete! |
| <button id="btnNextLevel">Next Level</button> |
| </div> |
|
|
| <script> |
| |
| const gameState = { |
| tubes: [], |
| selectedTube: null, |
| moves: 0, |
| level: 1, |
| moveHistory: [] |
| }; |
| |
| |
| const colors = [ |
| '#FF5252', '#FF4081', '#E040FB', '#7C4DFF', |
| '#536DFE', '#448AFF', '#40C4FF', '#18FFFF', |
| '#64FFDA', '#69F0AE', '#B2FF59', '#EEFF41', |
| '#FFFF00', '#FFD740', '#FFAB40', '#FF6E40' |
| ]; |
| |
| |
| const tubesContainer = document.getElementById('tubes'); |
| const movesDisplay = document.getElementById('moves'); |
| const levelDisplay = document.getElementById('level'); |
| const btnNewGame = document.getElementById('btnNewGame'); |
| const btnUndo = document.getElementById('btnUndo'); |
| const winMessage = document.getElementById('winMessage'); |
| const btnNextLevel = document.getElementById('btnNextLevel'); |
| const floatingBalls = document.getElementById('floatingBalls'); |
| |
| |
| function createFloatingBalls() { |
| floatingBalls.innerHTML = ''; |
| for (let i = 0; i < 20; i++) { |
| const ball = document.createElement('div'); |
| ball.className = 'floating-ball'; |
| const size = Math.random() * 100 + 50; |
| ball.style.width = `${size}px`; |
| ball.style.height = `${size}px`; |
| ball.style.background = colors[Math.floor(Math.random() * colors.length)]; |
| ball.style.left = `${Math.random() * 100}%`; |
| ball.style.top = `${Math.random() * 100}%`; |
| ball.style.animationDuration = `${Math.random() * 20 + 10}s`; |
| floatingBalls.appendChild(ball); |
| } |
| } |
| |
| |
| function initGame() { |
| gameState.tubes = []; |
| gameState.selectedTube = null; |
| gameState.moves = 0; |
| gameState.moveHistory = []; |
| |
| movesDisplay.textContent = gameState.moves; |
| levelDisplay.textContent = gameState.level; |
| |
| tubesContainer.innerHTML = ''; |
| const tubeCount = Math.min(3 + gameState.level, 8); |
| const colorCount = Math.min(tubeCount - 2, colors.length); |
| |
| |
| const selectedColors = colors.slice(0, colorCount); |
| const allBalls = []; |
| |
| |
| selectedColors.forEach(color => { |
| for (let i = 0; i < 4; i++) { |
| allBalls.push(color); |
| } |
| }); |
| |
| |
| shuffleArray(allBalls); |
| |
| |
| const ballsPerTube = allBalls.length / (tubeCount - 2); |
| for (let i = 0; i < tubeCount - 2; i++) { |
| const tubeBalls = []; |
| for (let j = 0; j < ballsPerTube; j++) { |
| tubeBalls.push(allBalls[i * ballsPerTube + j]); |
| } |
| gameState.tubes.push(tubeBalls); |
| } |
| |
| |
| for (let i = 0; i < 2; i++) { |
| gameState.tubes.push([]); |
| } |
| |
| renderTubes(); |
| } |
| |
| |
| function shuffleArray(array) { |
| for (let i = array.length - 1; i > 0; i--) { |
| const j = Math.floor(Math.random() * (i + 1)); |
| [array[i], array[j]] = [array[j], array[i]]; |
| } |
| } |
| |
| |
| function renderTubes() { |
| tubesContainer.innerHTML = ''; |
| gameState.tubes.forEach((tubeBalls, index) => { |
| const tubeElement = document.createElement('div'); |
| tubeElement.className = 'tube'; |
| if (gameState.selectedTube === index) { |
| tubeElement.classList.add('selected'); |
| } |
| |
| tubeElement.addEventListener('click', () => handleTubeClick(index)); |
| |
| tubeBalls.forEach((ballColor, ballIndex) => { |
| const ballElement = document.createElement('div'); |
| ballElement.className = 'ball'; |
| ballElement.style.backgroundColor = ballColor; |
| tubeElement.appendChild(ballElement); |
| }); |
| |
| tubesContainer.appendChild(tubeElement); |
| }); |
| } |
| |
| |
| function handleTubeClick(tubeIndex) { |
| if (gameState.selectedTube === null) { |
| |
| if (gameState.tubes[tubeIndex].length > 0) { |
| gameState.selectedTube = tubeIndex; |
| renderTubes(); |
| } |
| } else { |
| |
| if (gameState.selectedTube === tubeIndex) { |
| |
| gameState.selectedTube = null; |
| renderTubes(); |
| } else { |
| moveBall(gameState.selectedTube, tubeIndex); |
| } |
| } |
| } |
| |
| |
| function moveBall(fromTubeIndex, toTubeIndex) { |
| const fromTube = gameState.tubes[fromTubeIndex]; |
| const toTube = gameState.tubes[toTubeIndex]; |
| |
| if (fromTube.length === 0) { |
| gameState.selectedTube = null; |
| renderTubes(); |
| return; |
| } |
| |
| |
| const topBallColor = fromTube[fromTube.length - 1]; |
| |
| |
| |
| |
| if (toTube.length === 0 || |
| (toTube[toTube.length - 1] === topBallColor && toTube.length < 4)) { |
| |
| |
| const prevState = JSON.parse(JSON.stringify(gameState.tubes)); |
| gameState.moveHistory.push(prevState); |
| |
| |
| let ballsToMove = 0; |
| for (let i = fromTube.length - 1; i >= 0; i--) { |
| if (fromTube[i] === topBallColor && (toTube.length + ballsToMove) < 4) { |
| ballsToMove++; |
| } else { |
| break; |
| } |
| } |
| |
| |
| const movedBalls = fromTube.splice(fromTube.length - ballsToMove, ballsToMove); |
| toTube.push(...movedBalls); |
| |
| gameState.moves++; |
| movesDisplay.textContent = gameState.moves; |
| |
| |
| if (checkWin()) { |
| showWinMessage(); |
| } |
| |
| gameState.selectedTube = null; |
| renderTubes(); |
| } else { |
| gameState.selectedTube = null; |
| renderTubes(); |
| } |
| } |
| |
| |
| function checkWin() { |
| return gameState.tubes.every(tube => { |
| if (tube.length === 0) return true; |
| if (tube.length < 4) return false; |
| const firstColor = tube[0]; |
| return tube.every(ball => ball === firstColor); |
| }); |
| } |
| |
| |
| function showWinMessage() { |
| winMessage.style.display = 'block'; |
| } |
| |
| |
| function undoMove() { |
| if (gameState.moveHistory.length > 0) { |
| gameState.tubes = gameState.moveHistory.pop(); |
| gameState.moves++; |
| movesDisplay.textContent = gameState.moves; |
| gameState.selectedTube = null; |
| renderTubes(); |
| } |
| } |
| |
| |
| function nextLevel() { |
| gameState.level++; |
| winMessage.style.display = 'none'; |
| initGame(); |
| } |
| |
| |
| btnNewGame.addEventListener('click', initGame); |
| btnUndo.addEventListener('click', undoMove); |
| btnNextLevel.addEventListener('click', nextLevel); |
| |
| |
| createFloatingBalls(); |
| initGame(); |
| |
| |
| document.addEventListener('keydown', (e) => { |
| if (e.key === 'Escape') { |
| gameState.selectedTube = null; |
| renderTubes(); |
| } else if (e.key === 'z' && (e.ctrlKey || e.metaKey)) { |
| undoMove(); |
| } |
| }); |
| </script> |
| <p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <a href="https://enzostvs-deepsite.hf.space" style="color: #fff;" target="_blank" >DeepSite</a> <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;"></p></body> |
| </html> |