贪吃蛇游戏链接:Snake Game
2025.01.11:更新了游戏代码,用豆包生成游戏封面,增加游戏效果并push到github仓库
github仓库地址:theisness/snake-game: using html+css+js create a snake game, add background music, partical effect
AI大热,我借助cursor工具用一个晚上写了一个贪吃蛇游戏,这是我人生中第一个网页游戏!
游戏有三档难度,游戏开始后还有背景音乐。
为了给亲朋好友也能体验一下,我把它部署在自己的NAS中。
一、创建docker
docker compose:
version: '3.8'
services:
python-linux:
build: . # 使用当前目录下的 Dockerfile
container_name: python-linux
restart: unless-stopped
volumes:
- ./python39-app:/app
working_dir: /app
ports:
- "8000:8000"
environment:
- PYTHONUNBUFFERED=1
tty: true
stdin_open: true
command: python3 /app/hello.py # 容器启动后自动运行脚本
二、上传游戏代码
我的游戏代码如下:纯html代码,基于python做服务端:
from http.server import BaseHTTPRequestHandler, HTTPServer
import socket
import os
# from dotenv import load_dotenv
# Load environment variables
# load_dotenv()
# Define difficulty levels
DIFFICULTY = {
'easy': 10,
'normal': 25,
'hard': 50
}
class SimpleHTTPRequestHandler(BaseHTTPRequestHandler):
def do_GET(self):
self.send_response(200)
self.send_header('Content-type', 'text/html; charset=utf-8')
self.end_headers()
html_part1 = """
<!DOCTYPE html>
<html>
<head>
<title>Snake Game</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<style>
body {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
margin: 0;
background-color: #1a1a1a;
font-family: Arial, sans-serif;
}
canvas {
border: 2px solid #333;
border-radius: 8px;
display: none;
background-color: #fff;
box-shadow: 0 0 20px rgba(0,0,0,0.1);
max-width: 95vw; /* Limit width to 95% of viewport width */
height: auto; /* Maintain aspect ratio */
}
#startBtn {
padding: 15px 40px;
font-size: 24px;
cursor: pointer;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 25px;
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
transition: all 0.3s ease;
}
#startBtn:hover {
background-color: #45a049;
transform: translateY(-2px);
}
.game-container {
display: flex;
flex-direction: column;
align-items: center;
gap: 20px;
width: 100%;
padding: 10px;
box-sizing: border-box;
}
.controls {
display: grid;
grid-template-areas:
'. up .'
'left down right';
grid-template-columns: repeat(3, 60px);
gap: 10px;
margin-top: 20px;
}
.control-btn {
width: 60px;
height: 60px;
border: none;
border-radius: 50%;
background-color: #4CAF50;
color: white;
font-size: 24px;
cursor: pointer;
touch-action: manipulation;
user-select: none;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
}
.control-btn:active {
transform: translateY(2px);
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
}
.btn-up { grid-area: up; }
.btn-down { grid-area: down; }
.btn-left { grid-area: left; }
.btn-right { grid-area: right; }
@media (min-width: 768px) {
.controls {
display: none;
}
}
.author {
position: fixed;
bottom: 20px;
right: 20px;
color: #4CAF50;
font-size: 16px;
font-family: Arial, sans-serif;
text-shadow: 1px 1px 2px rgba(0,0,0,0.1);
}
.game-cover {
width: 280px;
height: 280px;
margin-bottom: 20px;
border-radius: 15px;
box-shadow: 0 8px 16px rgba(0,0,0,0.2);
object-fit: cover;
}
@media (max-width: 320px) {
.game-cover {
width: 95vw;
height: auto;
}
}
.difficulty-select {
display: flex;
gap: 15px;
margin: 20px 0;
justify-content: center;
}
.difficulty-btn {
padding: 10px 25px;
font-size: 18px;
cursor: pointer;
border: none;
border-radius: 15px;
color: white;
transition: all 0.3s ease;
}
.easy { background-color: #4CAF50; }
.normal { background-color: #FFA500; }
.hard { background-color: #FF4444; }
.difficulty-btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
}
.difficulty-btn.selected {
box-shadow: 0 0 0 3px #fff, 0 0 0 6px currentColor;
}
.music-control {
position: fixed;
top: 20px;
right: 20px;
background: rgba(76, 175, 80, 0.8);
padding: 10px;
border-radius: 50%;
cursor: pointer;
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 20px;
border: none;
transition: all 0.3s ease;
}
.music-control:hover {
transform: scale(1.1);
background: rgba(76, 175, 80, 1);
}
</style>
</head>
<body>
<audio id="bgMusic" loop>
<source src="https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3" type="audio/mp3">
Your browser does not support the audio element.
</audio>
<button class="music-control" id="musicToggle" onclick="toggleMusic()">🎵</button>
<div class="game-container">
<!-- from internet url image-->
<img src="https://cdn.pixabay.com/photo/2013/07/13/13/42/snake-161424_1280.png"
alt="Snake Game Cover"
class="game-cover"
id="gameCover"
style="max-width: 100%; height: auto;">
<div class="difficulty-select" id="difficultySelect">
<button class="difficulty-btn easy" onclick="selectDifficulty('easy')">Easy</button>
<button class="difficulty-btn normal selected" onclick="selectDifficulty('normal')">Normal</button>
<button class="difficulty-btn hard" onclick="selectDifficulty('hard')">Hard</button>
</div>
<button id="startBtn" onclick="startGame()">Start Game</button>
<canvas id="gameCanvas" width="320" height="320"></canvas>
<div class="controls">
<button class="control-btn btn-up" onclick="handleMobileControl('up')">up</button>
<button class="control-btn btn-left" onclick="handleMobileControl('left')">left</button>
<button class="control-btn btn-down" onclick="handleMobileControl('down')">down</button>
<button class="control-btn btn-right" onclick="handleMobileControl('right')">right</button>
</div>
</div>
<div class="author">Author: Pan</div>
<script>
let canvas, ctx, snake, food, direction, gameLoop;
let foodCount = 0;
let selectedDifficulty = 'normal';
const DIFFICULTY_SETTINGS = {
'easy': { foodTarget: 10, speed: 200 },
'normal': { foodTarget: 25, speed: 150 },
'hard': { foodTarget: 50, speed: 100 }
};
function selectDifficulty(difficulty) {
selectedDifficulty = difficulty;
// Update button styles
document.querySelectorAll('.difficulty-btn').forEach(btn => {
btn.classList.remove('selected');
});
document.querySelector(`.${difficulty}`).classList.add('selected');
}
function checkCollision(head) {
// Start from index 1 to skip the head itself
for (let i = 1; i < snake.length; i++) {
if (snake[i].x === head.x && snake[i].y === head.y) {
return true; // Collision detected
}
}
return false;
}
function drawSnakeSegment(x, y, isHead) {
ctx.save();
if (isHead) {
// Draw larger green head
let headGradient = ctx.createLinearGradient(x, y, x + 20, y + 20);
headGradient.addColorStop(0, '#69F0AE');
headGradient.addColorStop(1, '#388E3C');
ctx.fillStyle = headGradient;
roundedRect(x - 2, y - 2, 24, 24, 6);
// Add shine effect to head
ctx.fillStyle = 'rgba(255, 255, 255, 0.3)';
ctx.beginPath();
ctx.arc(x + 6, y + 6, 4, 0, Math.PI * 2);
ctx.fill();
} else {
// Draw thinner red body segments
let bodyGradient = ctx.createLinearGradient(x, y, x + 20, y + 20);
bodyGradient.addColorStop(0, '#FF5252');
bodyGradient.addColorStop(1, '#D32F2F');
ctx.fillStyle = bodyGradient;
roundedRect(x + 2, y + 2, 16, 16, 4);
}
ctx.restore();
}
// Helper function to draw rounded rectangles
function roundedRect(x, y, width, height, radius) {
ctx.beginPath();
ctx.moveTo(x + radius, y);
ctx.lineTo(x + width - radius, y);
ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
ctx.lineTo(x + width, y + height - radius);
ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
ctx.lineTo(x + radius, y + height);
ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
ctx.lineTo(x, y + radius);
ctx.quadraticCurveTo(x, y, x + radius, y);
ctx.closePath();
ctx.fill();
}
function handleMobileControl(dir) {
if (dir === 'up' && direction !== 'down') direction = 'up';
if (dir === 'down' && direction !== 'up') direction = 'down';
if (dir === 'left' && direction !== 'right') direction = 'left';
if (dir === 'right' && direction !== 'left') direction = 'right';
}
function generateFood() {
let newFood;
do {
newFood = {
x: Math.floor(Math.random() * (canvas.width/20)) * 20,
y: Math.floor(Math.random() * (canvas.height/20)) * 20
};
} while (isOnSnake(newFood)); // Keep generating until we find a free spot
return newFood;
}
function isOnSnake(pos) {
return snake.some(segment => segment.x === pos.x && segment.y === pos.y);
}
function startGame() {
document.getElementById('startBtn').style.display = 'none';
document.getElementById('gameCover').style.display = 'none';
document.getElementById('difficultySelect').style.display = 'none';
document.getElementById('gameCanvas').style.display = 'block';
canvas = document.getElementById('gameCanvas');
ctx = canvas.getContext('2d');
canvas.width = 400;
canvas.height = 400;
snake = [{x: 0, y: 0}];
food = generateFood(); // Use the new function instead of direct assignment
direction = 'right';
foodCount = 0;
if (gameLoop) clearInterval(gameLoop);
gameLoop = setInterval(update, DIFFICULTY_SETTINGS[selectedDifficulty].speed);
document.addEventListener('keydown', changeDirection);
// Start music when game starts
if (!isMusicPlaying) {
toggleMusic();
}
}
function update() {
const head = {x: snake[0].x, y: snake[0].y};
if (direction === 'right') head.x += 20;
if (direction === 'left') head.x -= 20;
if (direction === 'up') head.y -= 20;
if (direction === 'down') head.y += 20;
if (head.x < 0) head.x = canvas.width - 20;
if (head.x >= canvas.width) head.x = 0;
if (head.y < 0) head.y = canvas.height - 20;
if (head.y >= canvas.height) head.y = 0;
if (checkCollision(head)) {
gameOver();
return;
}
snake.unshift(head);
if (head.x === food.x && head.y === food.y) {
foodCount++;
if (foodCount >= DIFFICULTY_SETTINGS[selectedDifficulty].foodTarget) {
clearInterval(gameLoop);
alert('Congratulations! You won!');
document.getElementById('startBtn').style.display = 'block';
document.getElementById('difficultySelect').style.display = 'flex';
document.getElementById('gameCover').style.display = 'block';
document.getElementById('gameCanvas').style.display = 'none';
return;
}
food = generateFood(); // Use the new function here too
} else {
snake.pop();
}
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Draw snake with different head and body segments
snake.forEach((segment, index) => {
drawSnakeSegment(segment.x, segment.y, index === 0);
});
// Draw food
let foodGradient = ctx.createRadialGradient(
food.x + 10, food.y + 10, 2,
food.x + 10, food.y + 10, 10
);
foodGradient.addColorStop(0, '#FDD835');
foodGradient.addColorStop(1, '#F57F17');
ctx.fillStyle = foodGradient;
ctx.beginPath();
ctx.arc(food.x + 10, food.y + 10, 10, 0, Math.PI * 2);
ctx.fill();
}
function changeDirection(event) {
if (event.key === 'ArrowUp' && direction !== 'down') direction = 'up';
if (event.key === 'ArrowDown' && direction !== 'up') direction = 'down';
if (event.key === 'ArrowLeft' && direction !== 'right') direction = 'left';
if (event.key === 'ArrowRight' && direction !== 'left') direction = 'right';
}
// Update canvas size based on screen width
function resizeCanvas() {
const canvas = document.getElementById('gameCanvas');
const maxWidth = Math.min(320, window.innerWidth * 0.95);
canvas.style.width = maxWidth + 'px';
canvas.style.height = maxWidth + 'px';
}
// Add resize event listener
window.addEventListener('resize', resizeCanvas);
function gameOver() {
clearInterval(gameLoop);
// Stop music on game over
if (isMusicPlaying) {
toggleMusic();
}
alert('Game Over! Snake hit itself!');
document.getElementById('startBtn').style.display = 'block';
document.getElementById('difficultySelect').style.display = 'flex';
document.getElementById('gameCover').style.display = 'block'; // Show cover again
document.getElementById('gameCanvas').style.display = 'none';
}
let bgMusic = document.getElementById('bgMusic');
let isMusicPlaying = false;
function toggleMusic() {
const musicBtn = document.getElementById('musicToggle');
if (isMusicPlaying) {
bgMusic.pause();
// use emoji
musicBtn.textContent = '🔇';
} else {
bgMusic.play().catch(e => console.log('Audio play failed:', e));
// use emoji
musicBtn.textContent = '🔈';
}
isMusicPlaying = !isMusicPlaying;
}
</script>
</body>
</html>
"""
self.wfile.write(html_part1.encode())
def find_free_port():
# Try ports from 8080 to 8099
for port in range(8080, 8100):
try:
# Test if port is available
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind(('', port))
return port
except OSError:
continue
raise OSError("No free ports found in range 8080-8099")
def run(server_class=HTTPServer, handler_class=SimpleHTTPRequestHandler):
port = find_free_port()
server_address = ('', port)
try:
httpd = server_class(server_address, handler_class)
# set address reuse
httpd.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
print(f"Server started on port {port}")
httpd.serve_forever()
except KeyboardInterrupt:
print("\nShutting down server")
# close httpd right now
httpd.server_close()
httpd.socket.close()
if __name__ == "__main__":
run()
由于把app路径挂载在了docker中,只要把游戏代码放到app下就行。
三、开始游戏
访问NAS的8000端口,即可游玩。
成功!这样一个贪吃蛇游戏就部署好了。
花絮
粉丝开始玩贪吃蛇啦!直接上HARD难度!