
<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` }">
<div
v-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>
<div
class="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">
<button
class="control-btn up"
@click="setDirection(0, -1)"
@touchstart.prevent="setDirection(0, -1)"
>
↑
</button>
</div>
<div class="control-row middle-row">
<button
class="control-btn left"
@click="setDirection(-1, 0)"
@touchstart.prevent="setDirection(-1, 0)"
>
←
</button>
<button
class="control-btn down"
@click="setDirection(0, 1)"
@touchstart.prevent="setDirection(0, 1)"
>
↓
</button>
<button
class="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>