贪吃蛇
demo
实现
规则:
- 地图大小24*24
- 初始蛇长度为3,位置随机;随机在空余位置生成一个食物
- 蛇定时步进一次,自动向“前”移动;可以上下左右操作蛇的移动方向,无法向“后”移动
- 吃掉食物后蛇长度加1,随机生成一个新食物,若没有空余位置则完成游戏
- 蛇头碰到地图边缘或身体时游戏结束
用一个24*24的二维数组(matrix)表示地图,每个元素代表地图中的一个位置,值为0表示空位,1表示蛇,2表示食物。
用一个列表(snake)表示蛇的当前位置,蛇头在最后。
js
function step(direction) {
if (direction === adverseDirection(prevArrow.value)) direction = prevArrow.value
else prevArrow.value = direction
const moveStep = {
ArrowRight: [0, 1],
ArrowLeft: [0, -1],
ArrowUp: [-1, 0],
ArrowDown: [1, 0],
}[direction]
const head = snake.value[snake.value.length - 1]
const newHead = [head[0] + moveStep[0], head[1] + moveStep[1]]
if(newHead[0] < 0 || newHead[0] >= GRID_SIZE || newHead[1] < 0 || newHead[1] >= GRID_SIZE || matrix.value[newHead[0]][newHead[1]] === 1) {
alert('Game Over!')
return
}
const eated = matrix.value[newHead[0]][newHead[1]] === 2
matrix.value[newHead[0]][newHead[1]] = 1
snake.value.push(newHead)
if(GRID_SIZE * GRID_SIZE === snake.value.length) {
alert('You Win!')
return
}
// 进食则生成新食物,否则尾巴前进一格
if(eated) {
try {
const [rowIdx, colIdx] = generateFood()
matrix.value[rowIdx][colIdx] = 2
} catch (error) {
alert('Game Error: ' + (error.message || 'Unknown Error'))
return
}
} else {
const tail = snake.value.shift()
matrix.value[tail[0]][tail[1]] = 0
}
}
// 生成食物,排除snake占据的位置,随机从其余位置选择一个
function generateFood() {
const emptyCount = GRID_SIZE * GRID_SIZE - snake.value.length;
if(emptyCount <= 0) throw new Error('Process Error!')
let randomNum = Math.floor(Math.random() * emptyCount) + 1
for(let rowIdx = 0; rowIdx < matrix.value.length; rowIdx++) {
for(let colIdx = 0; colIdx < matrix.value[rowIdx].length; colIdx++) {
const col = matrix.value[rowIdx][colIdx]
if(col > 0) continue
if(--randomNum === 0) return [rowIdx, colIdx]
}
}
}
function adverseDirection(direction) {
return {
ArrowRight: 'ArrowLeft',
ArrowLeft: 'ArrowRight',
ArrowUp: 'ArrowDown',
ArrowDown: 'ArrowUp',
}[direction]
}
源码
vue
<script setup type="ts">
import { ref, onMounted, onBeforeUnmount } from 'vue'
const GRID_SIZE = 24
const STEP_INTERVAL = 800
const visible = ref(false)
onMounted(() => {
visible.value = true
document.addEventListener('keyup', recodeDirectives)
})
onBeforeUnmount(() => {
document.removeEventListener('keyup', recodeDirectives)
})
const directions = ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight']
const status = ref('init')
const matrix = ref(Array.from({ length: GRID_SIZE }).map(() => Array.from({ length: GRID_SIZE }).fill(0)))
const snake = ref([]) // 蛇头在最后
/**
* 自动步进
* @param e
*/
let stepInterval
const prevArrow = ref()
const currArrows = ref([])
function recodeDirectives({ code }) {
if(status.value !== 'playing') return
// 忽略非方向按键事件
if (!directions.includes(code)) return
currArrows.value.push(code)
if(currArrows.value.length > 2) currArrows.value.shift()
}
function startInterval() {
if(stepInterval) return
stepInterval = setInterval(() => {
if(status.value !== 'playing') {
clearInterval(stepInterval)
stepInterval = null
return
}
step(currArrows.value.shift() || prevArrow.value)
}, STEP_INTERVAL)
}
function stopInterval() {
if(!stepInterval) return
clearInterval(stepInterval)
stepInterval = null
}
function initSnake(d) {
if(!d) d = directions[Math.floor(Math.random() * directions.length)]
prevArrow.value = d
currArrows.value = [d]
const midIdx = Math.floor(GRID_SIZE / 2 - 1)
let list = [] // 蛇头在最后
switch (d) {
case 'ArrowDown':
list = [[midIdx - 1, midIdx], [midIdx, midIdx]]
break
case 'ArrowUp':
list = [[midIdx + 1, midIdx], [midIdx, midIdx]]
break
case 'ArrowRight':
list = [[midIdx, midIdx - 1], [midIdx, midIdx]]
break
case 'ArrowLeft':
list = [[midIdx, midIdx + 1], [midIdx, midIdx]]
break
default: throw new Error('Invalid direction')
}
snake.value = []
list.forEach(([x, y]) => {
matrix.value[x][y] = 1
snake.value.push([x, y])
})
}
function init() {
if(!['init', 'gameover', 'win'].includes(status.value)) return
try {
matrix.value = Array.from({ length: GRID_SIZE }).map(() => Array.from({ length: GRID_SIZE }).fill(0))
initSnake()
// 初始食物位置
const [rowIdx, colIdx] = generateFood()
matrix.value[rowIdx][colIdx] = 2
status.value = 'init'
start()
} catch (error) {
console.error(error)
}
}
function start() {
if(!['init', 'paused'].includes(status.value)) return
status.value = 'playing'
document.addEventListener('keyup', recodeDirectives)
// 恢复自动步进
startInterval()
}
function pause() {
if(status.value !== 'playing') return
status.value = 'paused'
document.removeEventListener('keyup', recodeDirectives)
// 停止自动步进
stopInterval()
}
function stop(s) {
if(!['paused', 'playing'].includes(status.value) || !['gameover', 'win'].includes(s)) return
status.value = s
document.removeEventListener('keyup', recodeDirectives)
}
function step(direction) {
if(status.value !== 'playing') return
if (!['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(direction)) return
if (direction === adverseDirection(prevArrow.value)) direction = prevArrow.value
else prevArrow.value = direction
const moveStep = {
ArrowRight: [0, 1],
ArrowLeft: [0, -1],
ArrowUp: [-1, 0],
ArrowDown: [1, 0],
}[direction]
const head = snake.value[snake.value.length - 1]
const newHead = [head[0] + moveStep[0], head[1] + moveStep[1]]
if(newHead[0] < 0 || newHead[0] >= GRID_SIZE || newHead[1] < 0 || newHead[1] >= GRID_SIZE || matrix.value[newHead[0]][newHead[1]] === 1) {
stop('gameover')
alert('Game Over!')
return
}
const eated = matrix.value[newHead[0]][newHead[1]] === 2
matrix.value[newHead[0]][newHead[1]] = 1
snake.value.push(newHead)
if(GRID_SIZE * GRID_SIZE === snake.value.length) {
stop('win')
alert('You Win!')
return
}
// 进食则生成新食物,否则尾巴前进一格
if(eated) {
try {
const [rowIdx, colIdx] = generateFood()
matrix.value[rowIdx][colIdx] = 2
} catch (error) {
stop('gameover')
alert('Game Error: ' + (error.message || 'Unknown Error'))
return
}
} else {
const tail = snake.value.shift()
matrix.value[tail[0]][tail[1]] = 0
}
}
// 生成食物,排除snake占据的位置,随机从其余位置选择一个
function generateFood() {
const emptyCount = GRID_SIZE * GRID_SIZE - snake.value.length;
if(emptyCount <= 0) throw new Error('Process Error!')
let randomNum = Math.floor(Math.random() * emptyCount) + 1
for(let rowIdx = 0; rowIdx < matrix.value.length; rowIdx++) {
for(let colIdx = 0; colIdx < matrix.value[rowIdx].length; colIdx++) {
const col = matrix.value[rowIdx][colIdx]
if(col > 0) continue
if(--randomNum === 0) return [rowIdx, colIdx]
}
}
}
function adverseDirection(direction) {
return {
ArrowRight: 'ArrowLeft',
ArrowLeft: 'ArrowRight',
ArrowUp: 'ArrowDown',
ArrowDown: 'ArrowUp',
}[direction]
}
</script>
<template>
<div class="container">
<div v-show="visible" class="panel-wrap">
<div id="panel" :class="[status]">
<div v-if="['init', 'paused', 'gameover', 'win'].includes(status)" class="overlay">
<button v-if="status === 'init'" @click="init">开始</button>
<button v-if="status === 'paused'" @click="start">继续</button>
<button v-if="status === 'gameover'" @click="init">重新开始</button>
<button v-if="status === 'win'" @click="init">重新开始</button>
</div>
<template v-for="(row, rowIdx) in matrix">
<div v-for="(cell, colIdx) in row" :key="colIdx" class="cell-wrap">
<div
v-if="cell"
class="cell"
:class="{
body: cell === 1,
head: cell === 1 && snake[snake.length - 1][0] === rowIdx && snake[snake.length - 1][1] === colIdx,
food: cell === 2
}"
/>
</div>
</template>
</div>
</div>
<div class="menu">
<!-- <div>currArrows: {{ currArrows }}</div> -->
<button v-if="status === 'playing'" @click="pause">暂停</button>
<button v-if="['paused', 'playing'].includes(status)" @click="stop('gameover')">结束</button>
</div>
</div>
</template>
<style>
html,body {
margin: 0;
}
.container {
height: 100vh;
height: 100dvh;
padding: 1rem;
background: #fff;
box-sizing: border-box;
}
.panel-wrap {
display: flex;
justify-content: center;
align-items: center;
}
#panel {
--cellLen: clamp(10px, calc(min(90vw, 80vh) / 24), 36px);
position: relative;
display: grid;
grid-template-columns: repeat(v-bind(GRID_SIZE), var(--cellLen));
border: 1px solid rgba(0,0,0,0.4);
}
.overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
justify-content: center;
align-items: center;
background-color: rgba(0,0,0,0.3);
z-index: 10;
}
.cell-wrap {
display: flex;
justify-content: center;
align-items: center;
height: var(--cellLen);
width: var(--cellLen);
min-width: 0;
min-height: 0;
z-index: 2;
}
.cell {
height: 70%;
width: 70%;
cursor: pointer;
user-select: none;
}
.cell.body, .cell.head {
background-color: #1e1e1e;
box-shadow: 0 0 2px 1px rgba(0,0,0,0.4);
}
.cell.head {
height: 90%;
width: 90%;
border-radius: 2px;
}
.cell.food {
background-color: #0ff;
box-shadow: 0 0 2px 1px rgba(0,255,255,0.4);
}
.menu {
text-align: center;
margin-top: 1rem;
}
</style>