
<template><div class="snake-game"><div class="game-header"><h1>贪吃蛇</h1><div class="score-panel"><div class="current-score">分数: {{ currentScore }}</div><div class="high-score">最高分: {{ highScore }}</div></div></div><div class="game-area"><div class="game-board" :style="{ width: `${boardSize}px`, height: `${boardSize}px` }"><divv-for="(segment, index) in snake":key="index"class="snake-segment":class="{ 'snake-head': index === 0 }":style="{left: `${segment.x * cellSize}px`,top: `${segment.y * cellSize}px`,width: `${cellSize}px`,height: `${cellSize}px`}"></div><divclass="food"v-if="food":style="{left: `${food.x * cellSize}px`,top: `${food.y * cellSize}px`,width: `${cellSize}px`,height: `${cellSize}px`}"></div><div class="game-start" v-if="!isPlaying && !gameOver"><button @click="startGame" class="start-btn">开始游戏</button></div><div class="game-over" v-if="gameOver"><h2>游戏结束</h2><p>最终得分: {{ currentScore }}</p><button @click="startGame" class="restart-btn">再来一局</button></div></div></div><div class="control-pad" v-if="isPlaying || gameOver"><div class="control-row top-row"><buttonclass="control-btn up"@click="setDirection(0, -1)"@touchstart.prevent="setDirection(0, -1)">↑</button></div><div class="control-row middle-row"><buttonclass="control-btn left"@click="setDirection(-1, 0)"@touchstart.prevent="setDirection(-1, 0)">←</button><buttonclass="control-btn down"@click="setDirection(0, 1)"@touchstart.prevent="setDirection(0, 1)">↓</button><buttonclass="control-btn right"@click="setDirection(1, 0)"@touchstart.prevent="setDirection(1, 0)">→</button></div></div></div>
</template><script setup>
import { ref, onMounted, onUnmounted, computed } from 'vue';
const gridSize = 15;
const initialSpeed = 250;
const speedIncrease = 5;
const boardSize = computed(() => {return Math.min(window.innerWidth * 0.9, 400);
});
const cellSize = computed(() => {return boardSize.value / gridSize;
});
const snake = ref([{ x: 7, y: 7 }]);
const food = ref(null);
const direction = ref({ x: 1, y: 0 });
const nextDirection = ref({ ...direction.value });
const currentScore = ref(0);
const highScore = ref(0);
const isPlaying = ref(false);
const gameOver = ref(false);
const gameInterval = ref(null);
const startGame = () => {snake.value = [{ x: 7, y: 7 }];direction.value = { x: 1, y: 0 };nextDirection.value = { ...direction.value };currentScore.value = 0;gameOver.value = false;isPlaying.value = true;generateFood();if (gameInterval.value) clearInterval(gameInterval.value);gameInterval.value = setInterval(moveSnake, initialSpeed);
};
const generateFood = () => {let newFood;do {newFood = {x: Math.floor(Math.random() * gridSize), y: Math.floor(Math.random() * gridSize) };} while (snake.value.some(segment => segment.x === newFood.x && segment.y === newFood.y));food.value = newFood;
};
const moveSnake = () => {if (!isPlaying.value) return;direction.value = { ...nextDirection.value };const head = {x: snake.value[0].x + direction.value.x,y: snake.value[0].y + direction.value.y};if (checkCollision(head)) {endGame();return;}snake.value.unshift(head);if (head.x === food.value.x && head.y === food.value.y) {currentScore.value += 10;if (currentScore.value > highScore.value) {highScore.value = currentScore.value;localStorage.setItem('snakeHighScore', highScore.value);}generateFood(); adjustSpeed();} else {snake.value.pop(); }
};
const checkCollision = (head) => {if (head.x < 0 || head.x >= gridSize || head.y < 0 || head.y >= gridSize) {return true;}return snake.value.some((segment, index) => index !== 0 && segment.x === head.x && segment.y === head.y);
};
const adjustSpeed = () => {if (gameInterval.value) {clearInterval(gameInterval.value);const newSpeed = Math.max(initialSpeed - (currentScore.value / 10) * speedIncrease, 100);gameInterval.value = setInterval(moveSnake, newSpeed);}
};
const endGame = () => {isPlaying.value = false;gameOver.value = true;clearInterval(gameInterval.value);
};
const setDirection = (x, y) => {if ((direction.value.x === -x && direction.value.y === -y) || !isPlaying.value) {return;}nextDirection.value = { x, y };
};
const handleKeydown = (e) => {if (!isPlaying.value) return;switch(e.key) {case 'ArrowUp': setDirection(0, -1); break;case 'ArrowDown': setDirection(0, 1); break;case 'ArrowLeft': setDirection(-1, 0); break;case 'ArrowRight': setDirection(1, 0); break;}
};
const loadHighScore = () => {const saved = localStorage.getItem('snakeHighScore');if (saved) highScore.value = parseInt(saved);
};
onMounted(() => {loadHighScore();window.addEventListener('keydown', handleKeydown);
});onUnmounted(() => {window.removeEventListener('keydown', handleKeydown);if (gameInterval.value) clearInterval(gameInterval.value);
});
</script><style scoped>
.snake-game {display: flex;flex-direction: column;align-items: center;min-height: 100vh;background-color: #f0f2f5;padding: 20px 10px;box-sizing: border-box;font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.game-header {width: 100%;max-width: 400px;margin-bottom: 20px;text-align: center;
}.game-header h1 {color: #1a1a1a;margin: 0 0 15px 0;font-size: 28px;
}.score-panel {display: flex;justify-content: space-around;background-color: white;padding: 12px;border-radius: 10px;box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}.current-score, .high-score {font-size: 16px;font-weight: 500;color: #333;
}
.game-area {position: relative;margin-bottom: 30px;
}.game-board {position: relative; background-color: #e6f7ff;border: 2px solid #1890ff;border-radius: 8px;overflow: hidden;
}
.snake-segment, .food {position: absolute;box-sizing: border-box;
}.snake-segment {background-color: #52c41a;border-radius: 4px;transition: all 0.1s ease;
}.snake-head {background-color: #2e7d32;border: 2px solid #1b5e20;
}.food {background-color: #ff4d4f;border-radius: 50%;box-shadow: 0 0 0 2px rgba(255, 77, 79, 0.5) inset;
}
.game-start, .game-over {position: absolute;top: 0;left: 0;width: 100%;height: 100%;background-color: rgba(0, 0, 0, 0.7);display: flex;flex-direction: column;align-items: center;justify-content: center;border-radius: 6px;color: white;
}.game-over h2 {margin: 0 0 15px 0;font-size: 24px;
}.game-over p {margin: 0 0 20px 0;font-size: 18px;
}.start-btn, .restart-btn {background-color: #1890ff;color: white;border: none;border-radius: 6px;padding: 12px 24px;font-size: 16px;font-weight: 500;cursor: pointer;transition: background-color 0.2s;
}.start-btn:hover, .restart-btn:hover {background-color: #096dd9;
}.start-btn:active, .restart-btn:active {transform: scale(0.98);
}
.control-pad {width: 240px;height: 200px;position: relative;margin-top: auto;margin-bottom: 20px;
}.control-row {display: flex;justify-content: center;
}.top-row {margin-bottom: 10px;
}.middle-row {gap: 10px;
}.control-btn {width: 70px;height: 70px;border-radius: 10px;background-color: #1890ff;color: white;border: none;font-size: 20px;font-weight: bold;box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);cursor: pointer;display: flex;align-items: center;justify-content: center;touch-action: manipulation;-webkit-tap-highlight-color: transparent;
}.control-btn:active {transform: scale(0.95);background-color: #096dd9;box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
@media (max-width: 360px) {.control-pad {width: 200px;height: 170px;}.control-btn {width: 60px;height: 60px;font-size: 18px;}.game-header h1 {font-size: 24px;}
}
</style>