test-v2 / index.html
mrwhy06's picture
Add 2 files
70c07a8 verified
<!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;
position: relative;
}
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, box-shadow 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);
}
.tube.valid-move {
animation: pulse 1.5s infinite;
box-shadow: 0 0 15px rgba(0, 255, 0, 0.7);
}
@keyframes pulse {
0% { box-shadow: 0 0 10px rgba(0, 255, 0, 0.7); }
50% { box-shadow: 0 0 20px rgba(0, 255, 0, 0.9); }
100% { box-shadow: 0 0 10px rgba(0, 255, 0, 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;
}
.hint-container {
position: absolute;
top: 20px;
right: 20px;
display: flex;
flex-direction: column;
gap: 10px;
}
.hint-count {
font-size: 0.9rem;
background-color: rgba(0, 0, 0, 0.3);
padding: 5px 10px;
border-radius: 5px;
text-align: center;
}
@media (max-width: 600px) {
.tube {
width: 50px;
height: 180px;
}
.ball {
width: 45px;
height: 45px;
}
h1 {
font-size: 1.8rem;
}
.hint-container {
position: static;
margin-bottom: 10px;
}
}
.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">
<div class="hint-container">
<button id="btnHint">Hint</button>
<div class="hint-count">Hints left: <span id="hintCount">3</span></div>
</div>
<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>
// Game state
const gameState = {
tubes: [],
selectedTube: null,
moves: 0,
level: 1,
moveHistory: [],
hintsUsed: 0,
maxHints: 3
};
// Colors
const colors = [
'#FF5252', '#FF4081', '#E040FB', '#7C4DFF',
'#536DFE', '#448AFF', '#40C4FF', '#18FFFF',
'#64FFDA', '#69F0AE', '#B2FF59', '#EEFF41',
'#FFFF00', '#FFD740', '#FFAB40', '#FF6E40'
];
// DOM elements
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 btnHint = document.getElementById('btnHint');
const hintCountDisplay = document.getElementById('hintCount');
const winMessage = document.getElementById('winMessage');
const btnNextLevel = document.getElementById('btnNextLevel');
const floatingBalls = document.getElementById('floatingBalls');
// Initialize floating background balls
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);
}
}
// Initialize game
function initGame() {
gameState.tubes = [];
gameState.selectedTube = null;
gameState.moves = 0;
gameState.moveHistory = [];
gameState.hintsUsed = 0;
movesDisplay.textContent = gameState.moves;
levelDisplay.textContent = gameState.level;
hintCountDisplay.textContent = gameState.maxHints - gameState.hintsUsed;
tubesContainer.innerHTML = '';
const tubeCount = Math.min(3 + gameState.level, 8);
const colorCount = Math.min(tubeCount - 2, colors.length);
// Create color set
const selectedColors = colors.slice(0, colorCount);
const allBalls = [];
// Create 4 balls for each color
selectedColors.forEach(color => {
for (let i = 0; i < 4; i++) {
allBalls.push(color);
}
});
// Shuffle balls
shuffleArray(allBalls);
// Distribute balls to tubes (first tubeCount - 2 tubes)
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);
}
// Add empty tubes (2)
for (let i = 0; i < 2; i++) {
gameState.tubes.push([]);
}
renderTubes();
}
// Helper function to shuffle an array
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]];
}
}
// Render tubes and balls
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 => {
const ballElement = document.createElement('div');
ballElement.className = 'ball';
ballElement.style.backgroundColor = ballColor;
tubeElement.appendChild(ballElement);
});
tubesContainer.appendChild(tubeElement);
});
}
// Show hint by highlighting valid move targets
function showHint() {
// Check if hints are available
if (gameState.hintsUsed >= gameState.maxHints) {
alert(`No hints left! You've used all ${gameState.maxHints} hints.`);
return;
}
gameState.hintsUsed++;
hintCountDisplay.textContent = gameState.maxHints - gameState.hintsUsed;
// First look for tubes where we can make progress (destination tubes)
for (let i = 0; i < gameState.tubes.length; i++) {
const tube = gameState.tubes[i];
if (tube.length > 0) {
const topColor = tube[tube.length - 1];
let count = 0;
// Count how many balls of same color are stacked
for (let j = tube.length - 1; j >= 0; j--) {
if (tube[j] === topColor) {
count++;
} else {
break;
}
}
// Find potential destination tubes
const potentialTargets = [];
for (let j = 0; j < gameState.tubes.length; j++) {
if (i === j) continue;
const targetTube = gameState.tubes[j];
// Empty tube is always valid
if (targetTube.length === 0) {
potentialTargets.push(j);
}
// Tube with same color top ball and space available
else if (targetTube[targetTube.length - 1] === topColor &&
(targetTube.length + count) <= 4) {
potentialTargets.push(j);
}
}
// If we found valid targets, highlight them
if (potentialTargets.length > 0) {
// Highlight source tube
gameState.selectedTube = i;
renderTubes();
// Highlight target tubes
const tubeElements = document.querySelectorAll('.tube');
potentialTargets.forEach(targetIndex => {
tubeElements[targetIndex].classList.add('valid-move');
});
// Remove highlights after 3 seconds
setTimeout(() => {
gameState.selectedTube = null;
renderTubes();
const validMoves = document.querySelectorAll('.valid-move');
validMoves.forEach(tube => {
tube.classList.remove('valid-move');
});
}, 3000);
return; // Show just one hint at a time
}
}
}
}
// Handle tube click
function handleTubeClick(tubeIndex) {
if (gameState.selectedTube === null) {
// Select a tube if it has balls
if (gameState.tubes[tubeIndex].length > 0) {
gameState.selectedTube = tubeIndex;
renderTubes();
}
} else {
// Try to move ball from selected tube to clicked tube
if (gameState.selectedTube === tubeIndex) {
// Deselect if same tube clicked
gameState.selectedTube = null;
renderTubes();
} else {
moveBall(gameState.selectedTube, tubeIndex);
}
}
}
// Animate moving the top ball from one tube to another
function animateBallMove(fromTubeIndex, toTubeIndex, ballColor) {
// Get source tube element and its last ball element
const fromTubeElement = tubesContainer.children[fromTubeIndex];
const ballElement = fromTubeElement.lastElementChild;
if (!ballElement) return;
// Get destination tube element
const toTubeElement = tubesContainer.children[toTubeIndex];
// Get bounding rectangles for start and target positions
const fromRect = ballElement.getBoundingClientRect();
const toRect = toTubeElement.getBoundingClientRect();
// Determine target position.
// We use the number of balls already in the destination to offset vertically.
const targetBallIndex = gameState.tubes[toTubeIndex].length;
const ballHeight = ballElement.offsetHeight;
// Adjust target position: center the ball horizontally and stack from the bottom
const targetX = toRect.left + toRect.width / 2 - fromRect.width / 2;
const targetY = toRect.bottom - (targetBallIndex + 1) * (ballHeight + 5);
// Create a clone of the ball element for animation
const clone = ballElement.cloneNode(true);
clone.style.position = 'absolute';
clone.style.left = fromRect.left + 'px';
clone.style.top = fromRect.top + 'px';
clone.style.margin = '0';
clone.style.transition = 'all 0.5s ease-in-out';
document.body.appendChild(clone);
// Hide the original ball temporarily
ballElement.style.visibility = 'hidden';
// Start animation on next frame
requestAnimationFrame(() => {
clone.style.left = targetX + 'px';
clone.style.top = targetY + 'px';
});
// When animation completes, update the game state and re-render
clone.addEventListener('transitionend', () => {
clone.remove();
// Determine how many balls of the same color to move (if they are stacked)
let ballsToMove = 0;
const fromTube = gameState.tubes[fromTubeIndex];
for (let i = fromTube.length - 1; i >= 0; i--) {
if (fromTube[i] === ballColor && (gameState.tubes[toTubeIndex].length + ballsToMove) < 4) {
ballsToMove++;
} else {
break;
}
}
// Save current state for undo
const prevState = JSON.parse(JSON.stringify(gameState.tubes));
gameState.moveHistory.push(prevState);
// Move the balls in game state
const movedBalls = gameState.tubes[fromTubeIndex].splice(fromTube.length - ballsToMove, ballsToMove);
gameState.tubes[toTubeIndex].push(...movedBalls);
gameState.moves++;
movesDisplay.textContent = gameState.moves;
gameState.selectedTube = null;
renderTubes();
// Check win condition after state update
if (checkWin()) {
showWinMessage();
}
});
}
// Move ball from one tube to another (with animation)
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];
// Move is valid if destination is empty OR has the same color and is not full
if (toTube.length === 0 || (toTube[toTube.length - 1] === topBallColor && toTube.length < 4)) {
// Start the animation; state will update once the animation completes
animateBallMove(fromTubeIndex, toTubeIndex, topBallColor);
} else {
gameState.selectedTube = null;
renderTubes();
}
}
// Check if all tubes are complete (all balls of same color or empty)
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);
});
}
// Show win message
function showWinMessage() {
winMessage.style.display = 'block';
}
// Undo last move
function undoMove() {
if (gameState.moveHistory.length > 0) {
gameState.tubes = gameState.moveHistory.pop();
gameState.moves++;
movesDisplay.textContent = gameState.moves;
gameState.selectedTube = null;
renderTubes();
}
}
// Next level
function nextLevel() {
gameState.level++;
winMessage.style.display = 'none';
initGame();
}
// Event listeners
btnNewGame.addEventListener('click', initGame);
btnUndo.addEventListener('click', undoMove);
btnHint.addEventListener('click', showHint);
btnNextLevel.addEventListener('click', nextLevel);
// Initialize
createFloatingBalls();
initGame();
// Add keyboard controls
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
gameState.selectedTube = null;
renderTubes();
} else if (e.key === 'z' && (e.ctrlKey || e.metaKey)) {
undoMove();
} else if (e.key === 'h') {
showHint();
}
});
</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>