161 lines
3.8 KiB
Vue
161 lines
3.8 KiB
Vue
|
|
<script setup>
|
|||
|
|
// 迷宫探险:随机生成迷宫(深度优先挖墙),限时走到终点,越快分越高
|
|||
|
|
import { ref, onMounted, onBeforeUnmount } from 'vue'
|
|||
|
|
import { Keys, palette, randInt } from '../engine'
|
|||
|
|
|
|||
|
|
const emit = defineEmits(['score', 'end'])
|
|||
|
|
const canvas = ref(null)
|
|||
|
|
const N = 21 // 迷宫尺寸(奇数)
|
|||
|
|
const CELL = 26
|
|||
|
|
const SIZE = N * CELL
|
|||
|
|
let ctx
|
|||
|
|
let keys
|
|||
|
|
let maze = [] // 1=墙 0=路
|
|||
|
|
let player = { x: 1, y: 1 }
|
|||
|
|
let level = 1
|
|||
|
|
let score = 0
|
|||
|
|
let timeLeft = 60
|
|||
|
|
let countTimer = 0
|
|||
|
|
let playing = false
|
|||
|
|
|
|||
|
|
// 深度优先挖墙生成迷宫
|
|||
|
|
function generate() {
|
|||
|
|
maze = Array.from({ length: N }, () => Array(N).fill(1))
|
|||
|
|
const stack = [[1, 1]]
|
|||
|
|
maze[1][1] = 0
|
|||
|
|
while (stack.length) {
|
|||
|
|
const [cx, cy] = stack[stack.length - 1]
|
|||
|
|
// 随机方向找未访问的相邻格(步长2)
|
|||
|
|
const dirs = [[2, 0], [-2, 0], [0, 2], [0, -2]].sort(() => Math.random() - 0.5)
|
|||
|
|
let moved = false
|
|||
|
|
for (const [dx, dy] of dirs) {
|
|||
|
|
const nx = cx + dx
|
|||
|
|
const ny = cy + dy
|
|||
|
|
if (nx > 0 && nx < N - 1 && ny > 0 && ny < N - 1 && maze[ny][nx] === 1) {
|
|||
|
|
maze[ny][nx] = 0
|
|||
|
|
maze[cy + dy / 2][cx + dx / 2] = 0 // 打通中间墙
|
|||
|
|
stack.push([nx, ny])
|
|||
|
|
moved = true
|
|||
|
|
break
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
if (!moved) stack.pop()
|
|||
|
|
}
|
|||
|
|
// 终点保证可达(右下角)
|
|||
|
|
maze[N - 2][N - 2] = 0
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function move(dx, dy) {
|
|||
|
|
if (!playing) return
|
|||
|
|
const nx = player.x + dx
|
|||
|
|
const ny = player.y + dy
|
|||
|
|
if (maze[ny]?.[nx] === 0) {
|
|||
|
|
player.x = nx
|
|||
|
|
player.y = ny
|
|||
|
|
draw()
|
|||
|
|
// 到达终点:加分并进入下一关(迷宫重新生成,时间+20)
|
|||
|
|
if (nx === N - 2 && ny === N - 2) {
|
|||
|
|
score += 100 + timeLeft
|
|||
|
|
timeLeft = Math.min(90, timeLeft + 20)
|
|||
|
|
level++
|
|||
|
|
emit('score', score)
|
|||
|
|
generate()
|
|||
|
|
player = { x: 1, y: 1 }
|
|||
|
|
draw()
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function tick() {
|
|||
|
|
timeLeft--
|
|||
|
|
draw()
|
|||
|
|
if (timeLeft <= 0) {
|
|||
|
|
playing = false
|
|||
|
|
clearInterval(countTimer)
|
|||
|
|
emit('end', { score })
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function draw() {
|
|||
|
|
const p = palette()
|
|||
|
|
ctx.fillStyle = p.bg
|
|||
|
|
ctx.fillRect(0, 0, SIZE, SIZE)
|
|||
|
|
for (let y = 0; y < N; y++) {
|
|||
|
|
for (let x = 0; x < N; x++) {
|
|||
|
|
if (maze[y][x] === 1) {
|
|||
|
|
ctx.fillStyle = p.panel
|
|||
|
|
ctx.fillRect(x * CELL, y * CELL, CELL, CELL)
|
|||
|
|
ctx.strokeStyle = p.border
|
|||
|
|
ctx.strokeRect(x * CELL + 0.5, y * CELL + 0.5, CELL - 1, CELL - 1)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
// 终点
|
|||
|
|
ctx.fillStyle = '#4ade80'
|
|||
|
|
ctx.beginPath()
|
|||
|
|
ctx.roundRect((N - 2) * CELL + 4, (N - 2) * CELL + 4, CELL - 8, CELL - 8, 6)
|
|||
|
|
ctx.fill()
|
|||
|
|
// 玩家
|
|||
|
|
ctx.font = `${CELL - 4}px serif`
|
|||
|
|
ctx.textAlign = 'center'
|
|||
|
|
ctx.textBaseline = 'middle'
|
|||
|
|
ctx.fillText('🐭', player.x * CELL + CELL / 2, player.y * CELL + CELL / 2 + 1)
|
|||
|
|
// 顶部信息
|
|||
|
|
ctx.fillStyle = p.text
|
|||
|
|
ctx.font = 'bold 14px sans-serif'
|
|||
|
|
ctx.textAlign = 'left'
|
|||
|
|
ctx.textBaseline = 'top'
|
|||
|
|
ctx.fillText(`第 ${level} 关 ⏱ ${timeLeft}s`, 6, 4)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ---- 对外接口 ----
|
|||
|
|
function start() {
|
|||
|
|
generate()
|
|||
|
|
player = { x: 1, y: 1 }
|
|||
|
|
level = 1
|
|||
|
|
score = 0
|
|||
|
|
timeLeft = 60
|
|||
|
|
playing = true
|
|||
|
|
emit('score', 0)
|
|||
|
|
clearInterval(countTimer)
|
|||
|
|
countTimer = setInterval(tick, 1000)
|
|||
|
|
draw()
|
|||
|
|
}
|
|||
|
|
function stop() {
|
|||
|
|
clearInterval(countTimer)
|
|||
|
|
keys?.detach()
|
|||
|
|
}
|
|||
|
|
// 时间延长卡:+30 秒
|
|||
|
|
function useProp(code) {
|
|||
|
|
if (code === 'time_extend' && playing) {
|
|||
|
|
timeLeft += 30
|
|||
|
|
draw()
|
|||
|
|
return true
|
|||
|
|
}
|
|||
|
|
return false
|
|||
|
|
}
|
|||
|
|
defineExpose({ start, stop, useProp })
|
|||
|
|
|
|||
|
|
onMounted(() => {
|
|||
|
|
ctx = canvas.value.getContext('2d')
|
|||
|
|
keys = new Keys()
|
|||
|
|
keys.attach()
|
|||
|
|
keys.onPress((k) => {
|
|||
|
|
const map = {
|
|||
|
|
ArrowUp: [0, -1], w: [0, -1],
|
|||
|
|
ArrowDown: [0, 1], s: [0, 1],
|
|||
|
|
ArrowLeft: [-1, 0], a: [-1, 0],
|
|||
|
|
ArrowRight: [1, 0], d: [1, 0],
|
|||
|
|
}
|
|||
|
|
if (map[k]) move(...map[k])
|
|||
|
|
})
|
|||
|
|
generate()
|
|||
|
|
draw()
|
|||
|
|
})
|
|||
|
|
onBeforeUnmount(stop)
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<template>
|
|||
|
|
<canvas ref="canvas" :width="SIZE" :height="SIZE"></canvas>
|
|||
|
|
</template>
|