|
|
<!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> |