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>
|