| <!DOCTYPE html> |
| <html lang="zh-CN"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>Take a Break - Snake Game</title> |
| <style> |
| * { |
| margin: 0; |
| padding: 0; |
| box-sizing: border-box; |
| } |
| |
| body { |
| font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; |
| background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%); |
| min-height: 100vh; |
| display: flex; |
| flex-direction: column; |
| justify-content: center; |
| align-items: center; |
| overflow: hidden; |
| position: relative; |
| } |
| |
| |
| .particles { |
| position: fixed; |
| top: 0; |
| left: 0; |
| width: 100%; |
| height: 100%; |
| pointer-events: none; |
| overflow: hidden; |
| z-index: 0; |
| } |
| |
| .particle { |
| position: absolute; |
| width: 4px; |
| height: 4px; |
| background: rgba(255, 255, 255, 0.3); |
| border-radius: 50%; |
| animation: float 15s infinite; |
| } |
| |
| @keyframes float { |
| 0%, 100% { |
| transform: translateY(100vh) rotate(0deg); |
| opacity: 0; |
| } |
| 10% { |
| opacity: 1; |
| } |
| 90% { |
| opacity: 1; |
| } |
| 100% { |
| transform: translateY(-100vh) rotate(720deg); |
| opacity: 0; |
| } |
| } |
| |
| |
| .container { |
| position: relative; |
| z-index: 1; |
| text-align: center; |
| padding: 20px; |
| } |
| |
| |
| .header { |
| margin-bottom: 20px; |
| } |
| |
| .header h1 { |
| color: #fff; |
| font-size: 2.5em; |
| margin-bottom: 10px; |
| text-shadow: 0 0 20px rgba(0, 255, 255, 0.5); |
| background: linear-gradient(90deg, #00ffff, #ff00ff, #ffff00); |
| -webkit-background-clip: text; |
| -webkit-text-fill-color: transparent; |
| background-clip: text; |
| animation: glow 2s ease-in-out infinite alternate; |
| } |
| |
| @keyframes glow { |
| from { |
| filter: drop-shadow(0 0 10px rgba(0, 255, 255, 0.5)); |
| } |
| to { |
| filter: drop-shadow(0 0 20px rgba(255, 0, 255, 0.5)); |
| } |
| } |
| |
| .header p { |
| color: rgba(255, 255, 255, 0.7); |
| font-size: 1.1em; |
| } |
| |
| |
| .game-wrapper { |
| display: inline-block; |
| background: rgba(0, 0, 0, 0.6); |
| border-radius: 20px; |
| padding: 20px; |
| box-shadow: |
| 0 0 50px rgba(0, 255, 255, 0.2), |
| inset 0 0 50px rgba(0, 0, 0, 0.5); |
| border: 2px solid rgba(0, 255, 255, 0.3); |
| } |
| |
| .score-board { |
| display: flex; |
| justify-content: space-around; |
| margin-bottom: 15px; |
| padding: 10px 15px; |
| background: rgba(255, 255, 255, 0.1); |
| border-radius: 10px; |
| color: #fff; |
| font-size: 1.1em; |
| font-weight: bold; |
| gap: 15px; |
| } |
| |
| .score-item { |
| display: flex; |
| align-items: center; |
| gap: 10px; |
| } |
| |
| .score-label { |
| color: rgba(255, 255, 255, 0.6); |
| font-size: 0.8em; |
| } |
| |
| .score-value { |
| color: #00ffff; |
| font-size: 1.3em; |
| text-shadow: 0 0 10px rgba(0, 255, 255, 0.5); |
| } |
| |
| #gameCanvas { |
| border-radius: 10px; |
| box-shadow: 0 0 30px rgba(0, 0, 0, 0.5); |
| background: #0a0a0a; |
| } |
| |
| |
| .controls { |
| margin-top: 20px; |
| display: flex; |
| gap: 15px; |
| justify-content: center; |
| flex-wrap: wrap; |
| } |
| |
| .btn { |
| padding: 12px 30px; |
| border: none; |
| border-radius: 25px; |
| font-size: 1em; |
| font-weight: bold; |
| cursor: pointer; |
| transition: all 0.3s ease; |
| text-transform: uppercase; |
| letter-spacing: 1px; |
| } |
| |
| .btn-primary { |
| background: linear-gradient(135deg, #00ffff 0%, #0080ff 100%); |
| color: #000; |
| box-shadow: 0 4px 15px rgba(0, 255, 255, 0.4); |
| } |
| |
| .btn-primary:hover { |
| transform: translateY(-2px); |
| box-shadow: 0 6px 25px rgba(0, 255, 255, 0.6); |
| } |
| |
| .btn-secondary { |
| background: rgba(255, 255, 255, 0.1); |
| color: #fff; |
| border: 2px solid rgba(255, 255, 255, 0.3); |
| } |
| |
| .btn-secondary:hover { |
| background: rgba(255, 255, 255, 0.2); |
| border-color: rgba(255, 255, 255, 0.5); |
| } |
| |
| |
| .mobile-controls { |
| display: none; |
| margin-top: 20px; |
| } |
| |
| .d-pad { |
| display: grid; |
| grid-template-columns: repeat(3, 60px); |
| grid-template-rows: repeat(3, 60px); |
| gap: 5px; |
| justify-content: center; |
| } |
| |
| .d-btn { |
| background: rgba(255, 255, 255, 0.1); |
| border: 2px solid rgba(0, 255, 255, 0.3); |
| border-radius: 10px; |
| color: #fff; |
| font-size: 1.5em; |
| cursor: pointer; |
| transition: all 0.2s; |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| } |
| |
| .d-btn:hover, .d-btn:active { |
| background: rgba(0, 255, 255, 0.3); |
| border-color: #00ffff; |
| } |
| |
| .d-btn:nth-child(2) { grid-column: 2; grid-row: 1; } |
| .d-btn:nth-child(4) { grid-column: 1; grid-row: 2; } |
| .d-btn:nth-child(5) { grid-column: 2; grid-row: 2; background: rgba(0, 255, 255, 0.2); } |
| .d-btn:nth-child(6) { grid-column: 3; grid-row: 2; } |
| .d-btn:nth-child(8) { grid-column: 2; grid-row: 3; } |
| |
| |
| .tips { |
| margin-top: 20px; |
| color: rgba(255, 255, 255, 0.5); |
| font-size: 0.9em; |
| } |
| |
| .tips kbd { |
| background: rgba(255, 255, 255, 0.2); |
| padding: 2px 8px; |
| border-radius: 4px; |
| font-family: monospace; |
| } |
| |
| |
| .game-over { |
| position: absolute; |
| top: 50%; |
| left: 50%; |
| transform: translate(-50%, -50%); |
| background: rgba(0, 0, 0, 0.9); |
| padding: 40px; |
| border-radius: 20px; |
| text-align: center; |
| display: none; |
| border: 2px solid #ff0066; |
| box-shadow: 0 0 50px rgba(255, 0, 102, 0.5); |
| } |
| |
| .game-over h2 { |
| color: #ff0066; |
| font-size: 2em; |
| margin-bottom: 20px; |
| text-shadow: 0 0 20px rgba(255, 0, 102, 0.5); |
| } |
| |
| .game-over .final-score { |
| color: #fff; |
| font-size: 1.5em; |
| margin-bottom: 20px; |
| } |
| |
| |
| @media (max-width: 600px) { |
| .header h1 { |
| font-size: 1.8em; |
| } |
| |
| #gameCanvas { |
| width: 100%; |
| max-width: 350px; |
| height: auto; |
| } |
| |
| .mobile-controls { |
| display: block; |
| } |
| |
| .tips { |
| display: none; |
| } |
| } |
| |
| |
| .login-link { |
| margin-top: 30px; |
| color: rgba(255, 255, 255, 0.5); |
| } |
| |
| .login-link a { |
| color: #00ffff; |
| text-decoration: none; |
| border-bottom: 1px dashed #00ffff; |
| transition: all 0.3s; |
| } |
| |
| .login-link a:hover { |
| color: #fff; |
| border-bottom-style: solid; |
| } |
| </style> |
| </head> |
| <body> |
| |
| <div class="particles" id="particles"></div> |
| |
| <div class="container"> |
| <div class="header"> |
| <h1>🐍 Snake Game</h1> |
| <p>Take a break and enjoy a game!</p> |
| </div> |
| |
| <div class="game-wrapper"> |
| <div class="score-board"> |
| <div class="score-item"> |
| <span class="score-label">SCORE</span> |
| <span class="score-value" id="score">0</span> |
| </div> |
| <div class="score-item" id="modeIndicator"> |
| <span class="score-label" style="color: #ff00ff; text-shadow: 0 0 10px rgba(255, 0, 255, 0.5);">DEMO MODE</span> |
| </div> |
| <div class="score-item"> |
| <span class="score-label">HIGH SCORE</span> |
| <span class="score-value" id="highScore">0</span> |
| </div> |
| </div> |
| |
| <canvas id="gameCanvas" width="400" height="400"></canvas> |
| |
| <div class="game-over" id="gameOver"> |
| <h2>GAME OVER</h2> |
| <div class="final-score">Final Score: <span id="finalScore">0</span></div> |
| <button class="btn btn-primary" onclick="startGame()">PLAY AGAIN</button> |
| </div> |
| |
| <div class="controls"> |
| <button class="btn btn-primary" id="startBtn" onclick="startGame()">CLICK TO PLAY</button> |
| <button class="btn btn-secondary" onclick="pauseGame()" id="pauseBtn" disabled>PAUSE</button> |
| </div> |
| |
| |
| <div class="mobile-controls"> |
| <div class="d-pad"> |
| <div class="d-btn" onclick="changeDirection('up')">▲</div> |
| <div class="d-btn" onclick="changeDirection('left')">◀</div> |
| <div class="d-btn">●</div> |
| <div class="d-btn" onclick="changeDirection('right')">▶</div> |
| <div class="d-btn" onclick="changeDirection('down')">▼</div> |
| </div> |
| </div> |
| |
| <div class="tips"> |
| Use <kbd>↑</kbd> <kbd>↓</kbd> <kbd>←</kbd> <kbd>→</kbd> or <kbd>W</kbd> <kbd>S</kbd> <kbd>A</kbd> <kbd>D</kbd> to control |
| </div> |
| </div> |
| |
| <div class="login-link"> |
| Need to manage your server? <a href="/login">Login here</a> |
| </div> |
| </div> |
|
|
| <script> |
| |
| const canvas = document.getElementById('gameCanvas'); |
| const ctx = canvas.getContext('2d'); |
| const gridSize = 20; |
| const tileCount = canvas.width / gridSize; |
| |
| let snake = []; |
| let food = {}; |
| let direction = 'right'; |
| let nextDirection = 'right'; |
| let score = 0; |
| let highScore = localStorage.getItem('snakeHighScore') || 0; |
| let gameLoop = null; |
| let isPaused = false; |
| let isGameOver = false; |
| let gameSpeed = 100; |
| let isDemoMode = true; |
| let isUserMode = false; |
| |
| |
| document.getElementById('highScore').textContent = highScore; |
| |
| |
| function createParticles() { |
| const container = document.getElementById('particles'); |
| for (let i = 0; i < 50; i++) { |
| const particle = document.createElement('div'); |
| particle.className = 'particle'; |
| particle.style.left = Math.random() * 100 + '%'; |
| particle.style.animationDelay = Math.random() * 15 + 's'; |
| particle.style.animationDuration = (10 + Math.random() * 10) + 's'; |
| container.appendChild(particle); |
| } |
| } |
| |
| |
| function initGame() { |
| snake = [ |
| { x: 5, y: 10 }, |
| { x: 4, y: 10 }, |
| { x: 3, y: 10 } |
| ]; |
| direction = 'right'; |
| nextDirection = 'right'; |
| score = 0; |
| isGameOver = false; |
| isPaused = false; |
| gameSpeed = 100; |
| document.getElementById('score').textContent = score; |
| document.getElementById('gameOver').style.display = 'none'; |
| if (isUserMode) { |
| document.getElementById('startBtn').textContent = 'RESTART'; |
| } else { |
| document.getElementById('startBtn').textContent = 'CLICK TO PLAY'; |
| } |
| spawnFood(); |
| } |
| |
| |
| function getAIDirection() { |
| const head = snake[0]; |
| let bestDirection = direction; |
| let minDistance = Infinity; |
| |
| const directions = ['up', 'down', 'left', 'right']; |
| const opposites = { 'up': 'down', 'down': 'up', 'left': 'right', 'right': 'left' }; |
| |
| for (const dir of directions) { |
| |
| if (opposites[dir] === direction) continue; |
| |
| let nextX = head.x; |
| let nextY = head.y; |
| |
| switch(dir) { |
| case 'up': nextY--; break; |
| case 'down': nextY++; break; |
| case 'left': nextX--; break; |
| case 'right': nextX++; break; |
| } |
| |
| |
| if (nextX < 0 || nextX >= tileCount || nextY < 0 || nextY >= tileCount) continue; |
| if (isSnakeAt(nextX, nextY)) continue; |
| |
| |
| const distance = Math.abs(nextX - food.x) + Math.abs(nextY - food.y); |
| |
| if (distance < minDistance) { |
| minDistance = distance; |
| bestDirection = dir; |
| } |
| } |
| |
| return bestDirection; |
| } |
| |
| |
| function spawnFood() { |
| do { |
| food = { |
| x: Math.floor(Math.random() * tileCount), |
| y: Math.floor(Math.random() * tileCount) |
| }; |
| } while (isSnakeAt(food.x, food.y)); |
| } |
| |
| |
| function isSnakeAt(x, y) { |
| return snake.some(segment => segment.x === x && segment.y === y); |
| } |
| |
| |
| function draw() { |
| |
| ctx.fillStyle = '#0a0a0a'; |
| ctx.fillRect(0, 0, canvas.width, canvas.height); |
| |
| |
| ctx.strokeStyle = 'rgba(0, 255, 255, 0.05)'; |
| ctx.lineWidth = 1; |
| for (let i = 0; i <= tileCount; i++) { |
| ctx.beginPath(); |
| ctx.moveTo(i * gridSize, 0); |
| ctx.lineTo(i * gridSize, canvas.height); |
| ctx.stroke(); |
| ctx.beginPath(); |
| ctx.moveTo(0, i * gridSize); |
| ctx.lineTo(canvas.width, i * gridSize); |
| ctx.stroke(); |
| } |
| |
| |
| const foodX = food.x * gridSize; |
| const foodY = food.y * gridSize; |
| |
| |
| const gradient = ctx.createRadialGradient( |
| foodX + gridSize/2, foodY + gridSize/2, 0, |
| foodX + gridSize/2, foodY + gridSize/2, gridSize |
| ); |
| gradient.addColorStop(0, 'rgba(255, 0, 102, 0.8)'); |
| gradient.addColorStop(1, 'rgba(255, 0, 102, 0)'); |
| ctx.fillStyle = gradient; |
| ctx.fillRect(foodX - gridSize/2, foodY - gridSize/2, gridSize * 2, gridSize * 2); |
| |
| |
| ctx.fillStyle = '#ff0066'; |
| ctx.shadowColor = '#ff0066'; |
| ctx.shadowBlur = 20; |
| ctx.fillRect(foodX + 2, foodY + 2, gridSize - 4, gridSize - 4); |
| ctx.shadowBlur = 0; |
| |
| |
| snake.forEach((segment, index) => { |
| const x = segment.x * gridSize; |
| const y = segment.y * gridSize; |
| |
| if (index === 0) { |
| |
| ctx.fillStyle = '#00ffff'; |
| ctx.shadowColor = '#00ffff'; |
| ctx.shadowBlur = 15; |
| } else { |
| |
| const ratio = index / snake.length; |
| const r = Math.floor(0 * (1 - ratio) + 0 * ratio); |
| const g = Math.floor(255 * (1 - ratio) + 100 * ratio); |
| const b = Math.floor(255 * (1 - ratio) + 200 * ratio); |
| ctx.fillStyle = `rgb(${r}, ${g}, ${b})`; |
| ctx.shadowColor = ctx.fillStyle; |
| ctx.shadowBlur = 10; |
| } |
| |
| ctx.fillRect(x + 1, y + 1, gridSize - 2, gridSize - 2); |
| |
| |
| if (index === 0) { |
| ctx.fillStyle = '#000'; |
| ctx.shadowBlur = 0; |
| const eyeSize = 3; |
| let eye1X, eye1Y, eye2X, eye2Y; |
| |
| switch(direction) { |
| case 'up': |
| eye1X = x + 5; eye1Y = y + 5; |
| eye2X = x + 14; eye2Y = y + 5; |
| break; |
| case 'down': |
| eye1X = x + 5; eye1Y = y + 12; |
| eye2X = x + 14; eye2Y = y + 12; |
| break; |
| case 'left': |
| eye1X = x + 5; eye1Y = y + 5; |
| eye2X = x + 5; eye2Y = y + 12; |
| break; |
| case 'right': |
| eye1X = x + 12; eye1Y = y + 5; |
| eye2X = x + 12; eye2Y = y + 12; |
| break; |
| } |
| ctx.fillRect(eye1X, eye1Y, eyeSize, eyeSize); |
| ctx.fillRect(eye2X, eye2Y, eyeSize, eyeSize); |
| } |
| }); |
| ctx.shadowBlur = 0; |
| } |
| |
| |
| function update() { |
| if (isPaused || isGameOver) return; |
| |
| |
| if (isDemoMode && !isUserMode) { |
| nextDirection = getAIDirection(); |
| } |
| |
| direction = nextDirection; |
| |
| const head = { ...snake[0] }; |
| |
| switch(direction) { |
| case 'up': head.y--; break; |
| case 'down': head.y++; break; |
| case 'left': head.x--; break; |
| case 'right': head.x++; break; |
| } |
| |
| |
| if (head.x < 0 || head.x >= tileCount || head.y < 0 || head.y >= tileCount) { |
| if (isDemoMode && !isUserMode) { |
| |
| initGame(); |
| return; |
| } |
| gameOver(); |
| return; |
| } |
| |
| if (isSnakeAt(head.x, head.y)) { |
| if (isDemoMode && !isUserMode) { |
| |
| initGame(); |
| return; |
| } |
| gameOver(); |
| return; |
| } |
| |
| snake.unshift(head); |
| |
| |
| if (head.x === food.x && head.y === food.y) { |
| score += 10; |
| document.getElementById('score').textContent = score; |
| |
| |
| if (score > highScore) { |
| highScore = score; |
| localStorage.setItem('snakeHighScore', highScore); |
| document.getElementById('highScore').textContent = highScore; |
| } |
| |
| |
| if (gameSpeed > 50) { |
| gameSpeed -= 2; |
| clearInterval(gameLoop); |
| gameLoop = setInterval(gameStep, gameSpeed); |
| } |
| |
| spawnFood(); |
| } else { |
| snake.pop(); |
| } |
| } |
| |
| |
| function gameStep() { |
| update(); |
| draw(); |
| } |
| |
| |
| function gameOver() { |
| isGameOver = true; |
| clearInterval(gameLoop); |
| document.getElementById('finalScore').textContent = score; |
| document.getElementById('gameOver').style.display = 'block'; |
| } |
| |
| |
| function switchToUserMode() { |
| if (!isUserMode) { |
| isUserMode = true; |
| isDemoMode = false; |
| document.getElementById('startBtn').textContent = 'RESTART'; |
| document.getElementById('pauseBtn').disabled = false; |
| |
| const modeIndicator = document.getElementById('modeIndicator'); |
| if (modeIndicator) { |
| modeIndicator.innerHTML = '<span class="score-label" style="color: #00ff00; text-shadow: 0 0 10px rgba(0, 255, 0, 0.5);">USER MODE</span>'; |
| } |
| |
| showModeIndicator('USER MODE'); |
| } |
| } |
| |
| |
| function showModeIndicator(text) { |
| const indicator = document.createElement('div'); |
| indicator.style.cssText = ` |
| position: absolute; |
| top: 50%; |
| left: 50%; |
| transform: translate(-50%, -50%); |
| background: rgba(0, 255, 255, 0.9); |
| color: #000; |
| padding: 15px 30px; |
| border-radius: 10px; |
| font-weight: bold; |
| font-size: 1.2em; |
| z-index: 100; |
| animation: fadeInOut 1.5s ease-in-out; |
| `; |
| indicator.textContent = text; |
| document.querySelector('.game-wrapper').appendChild(indicator); |
| setTimeout(() => indicator.remove(), 1500); |
| } |
| |
| |
| const style = document.createElement('style'); |
| style.textContent = ` |
| @keyframes fadeInOut { |
| 0% { opacity: 0; transform: translate(-50%, -50%) scale(0.8); } |
| 50% { opacity: 1; transform: translate(-50%, -50%) scale(1); } |
| 100% { opacity: 0; transform: translate(-50%, -50%) scale(0.8); } |
| } |
| `; |
| document.head.appendChild(style); |
| |
| |
| function startGame() { |
| clearInterval(gameLoop); |
| switchToUserMode(); |
| initGame(); |
| draw(); |
| gameLoop = setInterval(gameStep, gameSpeed); |
| } |
| |
| |
| function pauseGame() { |
| if (isGameOver) return; |
| isPaused = !isPaused; |
| event.target.textContent = isPaused ? 'RESUME' : 'PAUSE'; |
| } |
| |
| |
| function changeDirection(newDirection) { |
| |
| switchToUserMode(); |
| |
| const opposites = { |
| 'up': 'down', |
| 'down': 'up', |
| 'left': 'right', |
| 'right': 'left' |
| }; |
| |
| if (opposites[newDirection] !== direction) { |
| nextDirection = newDirection; |
| } |
| } |
| |
| |
| document.addEventListener('keydown', (e) => { |
| switch(e.key) { |
| case 'ArrowUp': |
| case 'w': |
| case 'W': |
| e.preventDefault(); |
| changeDirection('up'); |
| break; |
| case 'ArrowDown': |
| case 's': |
| case 'S': |
| e.preventDefault(); |
| changeDirection('down'); |
| break; |
| case 'ArrowLeft': |
| case 'a': |
| case 'A': |
| e.preventDefault(); |
| changeDirection('left'); |
| break; |
| case 'ArrowRight': |
| case 'd': |
| case 'D': |
| e.preventDefault(); |
| changeDirection('right'); |
| break; |
| case ' ': |
| e.preventDefault(); |
| pauseGame(); |
| break; |
| } |
| }); |
| |
| |
| document.addEventListener('touchmove', (e) => { |
| e.preventDefault(); |
| }, { passive: false }); |
| |
| |
| canvas.addEventListener('click', () => { |
| switchToUserMode(); |
| }); |
| |
| |
| document.querySelectorAll('.d-btn').forEach(btn => { |
| btn.addEventListener('click', () => { |
| switchToUserMode(); |
| }); |
| }); |
| |
| |
| createParticles(); |
| initGame(); |
| draw(); |
| gameLoop = setInterval(gameStep, gameSpeed); |
| </script> |
| </body> |
| </html> |
|
|