Snake / index.html
Moonfanz's picture
Update index.html
3259609 verified
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>贪吃蛇</title>
<link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap" rel="stylesheet">
<script src="https://cdn.tailwindcss.com"></script>
<style>
body {
font-family: 'Press Start 2P', cursive;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
background-color: #1a1a1a; /* 深色背景 */
color: #ffffff;
flex-direction: column; /* 垂直排列 */
padding: 20px;
box-sizing: border-box;
}
#gameCanvas {
background-color: #000000; /* 黑色画布 */
border: 5px solid #33ff33; /* 亮绿色边框 */
display: block; /* 移除默认的内联间距 */
max-width: 100%; /* 确保画布在小屏幕上不会溢出 */
height: auto; /* 保持宽高比 */
aspect-ratio: 1 / 1; /* 使画布保持正方形 */
image-rendering: pixelated; /* 像素化渲染,保持复古风格 */
box-shadow: 0 0 20px #33ff33; /* 添加辉光效果 */
margin-bottom: 20px; /* 画布下方间距 */
}
.game-controls {
text-align: center;
margin-top: 15px;
}
.game-button {
font-family: 'Press Start 2P', cursive;
background-color: #33ff33; /* 亮绿色 */
color: #000000; /* 黑色文字 */
border: 2px solid #000000;
padding: 10px 20px;
margin: 5px;
cursor: pointer;
text-transform: uppercase;
transition: background-color 0.3s, transform 0.1s;
box-shadow: 3px 3px 0 #118811; /* 添加阴影 */
}
.game-button:hover {
background-color: #11cc11; /* 深一点的绿色 */
}
.game-button:active {
transform: translate(2px, 2px);
box-shadow: 1px 1px 0 #118811;
}
#messageBox {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: rgba(0, 0, 0, 0.8);
color: #ff3333; /* 红色消息 */
padding: 30px;
border: 3px solid #ff3333;
border-radius: 10px; /* 圆角 */
display: none; /* 默认隐藏 */
text-align: center;
z-index: 10;
box-shadow: 0 0 15px #ff3333;
}
#messageText {
margin-bottom: 15px;
}
/* 响应式设计:小屏幕 */
@media (max-width: 600px) {
body {
padding: 10px;
}
#gameCanvas {
margin-bottom: 15px;
}
.game-button {
padding: 8px 15px;
font-size: 0.8em;
}
#messageBox {
padding: 20px;
font-size: 0.9em;
}
}
</style>
</head>
<body>
<h1 class="text-2xl mb-4 text-center">贪吃蛇</h1>
<div id="score" class="text-lg mb-2">分数: 0</div>
<canvas id="gameCanvas"></canvas>
<div class="game-controls">
<button id="resetButton" class="game-button">重新开始</button>
</div>
<div id="messageBox">
<div id="messageText">游戏结束!</div>
<button id="closeMessageButton" class="game-button" style="background-color: #ff3333; color: #000;">关闭</button>
</div>
<script>
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const scoreElement = document.getElementById('score');
const resetButton = document.getElementById('resetButton');
const messageBox = document.getElementById('messageBox');
const messageText = document.getElementById('messageText');
const closeMessageButton = document.getElementById('closeMessageButton');
// --- 游戏设置 ---
const grid = 20; // 每个格子的大小(像素)
const canvasSize = 400; // 画布大小(像素) - 确保是 grid 的倍数
canvas.width = canvasSize;
canvas.height = canvasSize;
// --- 游戏状态变量 ---
let snake; // 蛇的数组,每个元素是 {x, y}
let food; // 食物的位置 {x, y}
let score; // 分数
let direction; // 当前移动方向: 'up', 'down', 'left', 'right'
let nextDirection; // 玩家请求的下一个方向
let bufferedDirection; // 当与食物对齐时,缓存的下一个方向
let isAligned; // 蛇头是否与食物在同一行或同一列
let gameLoopTimeout; // 游戏循环的 Timeout ID
let isGameOver; // 游戏是否结束
// --- 初始化游戏 ---
function initGame() {
clearTimeout(gameLoopTimeout); // 清除之前的循环
hideMessage(); // 隐藏消息框
isGameOver = false;
// 初始化蛇的位置和方向
snake = [
{ x: Math.floor(canvasSize / grid / 2) * grid, y: Math.floor(canvasSize / grid / 2) * grid } // 初始在中间
];
direction = 'right'; // 初始向右移动
nextDirection = null; // 初始没有预定方向
bufferedDirection = null; // 初始没有缓存方向
isAligned = false; // 初始未对齐
// 初始化分数
score = 0;
scoreElement.textContent = `分数: ${score}`;
// 放置第一个食物
placeFood();
// 开始游戏循环
gameLoop();
}
// --- 放置食物 ---
function placeFood() {
let newFoodPosition;
do {
newFoodPosition = {
x: Math.floor(Math.random() * (canvasSize / grid)) * grid,
y: Math.floor(Math.random() * (canvasSize / grid)) * grid
};
} while (isFoodOnSnake(newFoodPosition)); // 确保食物不生成在蛇身上
food = newFoodPosition;
}
// --- 检查食物是否在蛇身上 ---
function isFoodOnSnake(position) {
for (let i = 0; i < snake.length; i++) {
if (snake[i].x === position.x && snake[i].y === position.y) {
return true;
}
}
return false;
}
// --- 游戏循环 ---
function gameLoop() {
if (isGameOver) {
return; // 如果游戏结束,停止循环
}
// 设置游戏速度(数字越小越快)
const speed = 150; // 毫秒
gameLoopTimeout = setTimeout(() => {
// 清除画布
ctx.clearRect(0, 0, canvasSize, canvasSize);
// 更新状态(移动,检查碰撞等)
update();
// 绘制游戏元素
draw();
// 继续下一帧
requestAnimationFrame(gameLoop); // 使用 requestAnimationFrame 替代 setTimeout 以获得更平滑的动画
}, speed);
}
// --- 更新游戏状态 ---
function update() {
// 检查蛇头是否与食物对齐
const head = snake[0];
isAligned = (head.x === food.x || head.y === food.y);
// --- 处理方向改变 ---
if (isAligned) {
// 如果对齐,并且玩家请求了新方向,则缓存它
if (nextDirection) {
// 检查缓存的方向是否与当前方向相反
if (!isOppositeDirection(nextDirection, direction)) {
bufferedDirection = nextDirection;
}
nextDirection = null; // 清除立即生效的请求
}
// 蛇继续按当前方向移动 (direction 不变)
} else {
// 如果不对齐
// 1. 检查是否有缓存的方向
if (bufferedDirection) {
// 检查缓存的方向是否与当前方向相反
if (!isOppositeDirection(bufferedDirection, direction)) {
direction = bufferedDirection; // 应用缓存的方向
}
bufferedDirection = null; // 清除缓存
nextDirection = null; // 也清除,避免冲突
}
// 2. 如果没有缓存,检查是否有新的请求方向
else if (nextDirection) {
// 检查请求的方向是否与当前方向相反
if (!isOppositeDirection(nextDirection, direction)) {
direction = nextDirection; // 应用请求的方向
}
nextDirection = null; // 清除请求
}
}
// --- 计算蛇头的新位置 ---
let newHead = { x: head.x, y: head.y };
switch (direction) {
case 'up': newHead.y -= grid; break;
case 'down': newHead.y += grid; break;
case 'left': newHead.x -= grid; break;
case 'right': newHead.x += grid; break;
}
// --- 检查碰撞 ---
// 1. 撞墙
if (newHead.x < 0 || newHead.x >= canvasSize || newHead.y < 0 || newHead.y >= canvasSize) {
gameOver("撞到墙了!");
return;
}
// 2. 撞自己
for (let i = 1; i < snake.length; i++) {
if (newHead.x === snake[i].x && newHead.y === snake[i].y) {
gameOver("撞到自己了!");
return;
}
}
// --- 移动蛇 ---
snake.unshift(newHead); // 在前面添加新头
// --- 检查是否“吃到”食物 ---
// 在这个版本中,蛇不会真的吃掉食物,只是触发食物重新生成
if (newHead.x === food.x && newHead.y === food.y) {
score++; // 增加分数
scoreElement.textContent = `分数: ${score}`;
placeFood(); // 重新放置食物
// 注意:蛇的尾巴不会移除,所以蛇会一直增长
} else {
// 如果没有吃到食物,移除尾巴 (正常移动)
// 如果想让蛇一直增长,注释掉下面这行
// snake.pop();
}
// 如果想让蛇一直增长,即使没碰到食物,就把 snake.pop() 移到上面 else 之外
// 如果只想在碰到食物时增长,保持现状即可
// --- 特殊规则:如果没碰到食物,移除尾巴 ---
// (这使得游戏更像传统贪吃蛇,但食物无法被“吃掉”来增长)
if (!(newHead.x === food.x && newHead.y === food.y)) {
snake.pop();
}
}
// --- 绘制游戏元素 ---
function draw() {
// 绘制蛇
ctx.fillStyle = '#33ff33'; // 蛇的颜色 (亮绿色)
snake.forEach(segment => {
ctx.fillRect(segment.x, segment.y, grid, grid);
ctx.strokeStyle = '#000000'; // 黑色边框,增加清晰度
ctx.strokeRect(segment.x, segment.y, grid, grid);
});
// 绘制食物
ctx.fillStyle = '#ff3333'; // 食物颜色 (红色)
ctx.fillRect(food.x, food.y, grid, grid);
ctx.strokeStyle = '#ffffff'; // 白色边框
ctx.strokeRect(food.x, food.y, grid, grid);
// 如果对齐,给蛇头或食物加点提示?(可选)
if (isAligned) {
ctx.fillStyle = 'rgba(255, 255, 0, 0.3)'; // 半透明黄色高亮
// 高亮蛇头和食物所在的行列
// ctx.fillRect(snake[0].x, 0, grid, canvasSize); // 高亮列
// ctx.fillRect(0, snake[0].y, canvasSize, grid); // 高亮行
// 或者只高亮蛇头
ctx.fillStyle = 'yellow';
ctx.fillRect(snake[0].x, snake[0].y, grid, grid);
}
}
// --- 检查是否是相反方向 ---
function isOppositeDirection(dir1, dir2) {
return (dir1 === 'up' && dir2 === 'down') ||
(dir1 === 'down' && dir2 === 'up') ||
(dir1 === 'left' && dir2 === 'right') ||
(dir1 === 'right' && dir2 === 'left');
}
// --- 处理键盘输入 ---
document.addEventListener('keydown', (e) => {
if (isGameOver) return; // 游戏结束时忽略输入
let requestedDir = null;
switch (e.key) {
case 'ArrowUp':
case 'w': // 添加 WASD 控制
requestedDir = 'up';
break;
case 'ArrowDown':
case 's':
requestedDir = 'down';
break;
case 'ArrowLeft':
case 'a':
requestedDir = 'left';
break;
case 'ArrowRight':
case 'd':
requestedDir = 'right';
break;
}
// 只有当请求的方向不是当前方向的直接反方向时,才设置 nextDirection
// 同时,也检查不能是 nextDirection 的反方向(防止快速连续按键导致180度转弯)
const effectiveCurrentDirection = bufferedDirection || direction; // 考虑缓存的方向
if (requestedDir &&
!isOppositeDirection(requestedDir, effectiveCurrentDirection) &&
(!nextDirection || !isOppositeDirection(requestedDir, nextDirection)))
{
nextDirection = requestedDir;
}
// 阻止方向键滚动页面
if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.key)) {
e.preventDefault();
}
});
// --- 游戏结束处理 ---
function gameOver(reason) {
isGameOver = true;
clearTimeout(gameLoopTimeout); // 停止游戏循环
showMessage(`游戏结束! ${reason}<br>最终分数: ${score}`);
}
// --- 显示消息框 ---
function showMessage(msg) {
messageText.innerHTML = msg; // 使用 innerHTML 以支持 <br>
messageBox.style.display = 'block';
}
// --- 隐藏消息框 ---
function hideMessage() {
messageBox.style.display = 'none';
}
// --- 事件监听器 ---
resetButton.addEventListener('click', initGame);
closeMessageButton.addEventListener('click', hideMessage);
// --- 初始启动游戏 ---
initGame();
</script>
</body>
</html>