import { ref, reactive, onMounted, onUnmounted, computed } from "vue"
import { useHandleEvent } from "./useHandleEvent"
export function useMinesweeper(gameGridRef: Ref<HTMLElement>) {
const theme = ref({
title: "扫雷 - 经典版",
background: "#000000",
foreground: "#000000",
border: "#000000",
font: "16px/24px 'Microsoft YaHei', sans-serif"
})
// 游戏状态
const gameState = ref<"ready" | "playing" | "won" | "lost">("ready")
const gameTime = ref(0)
const timer = ref<NodeJS.Timeout | null>(null)
// const musicPlaying = ref(false)
const isResetting = ref(false)
const lastTapTime = ref(0)
const tapTimeout = ref<NodeJS.Timeout | null>(null)
// 游戏配置
const difficulty = ref<"beginner" | "intermediate" | "expert">("beginner")
const gridSize = reactive({ rows: 9, cols: 9 })
const mineCount = ref(10)
const remainingMines = ref(10)
const isMobile = ref(false)
const difficulties = [
{ name: "初级", key: "beginner" },
{ name: "中级", key: "intermediate" },
{ name: "高级", key: "expert" }
]
// 游戏数据
const grid = ref<any[][]>([])
const score = ref(0)
const highlightedCells = ref<{ row: number; col: number }[]>([])
const pressedCells = ref<{ row: number; col: number }[]>([])
// 弹窗控制
const showScoreboard = ref(false)
const showShareModal = ref(false)
const scoreboardType = ref<"beginner" | "intermediate" | "expert">("beginner")
const playerName = ref("玩家")
// 排行榜数据
const leaderboards = reactive({
beginner: [] as any[],
intermediate: [] as any[],
expert: [] as any[]
})
// 计算属性
const gameFace = computed(() => {
if (gameState.value === "won") {
return "😎"
}
if (gameState.value === "lost") {
return "😵"
}
return "😊"
})
const getScoreboardTitle = computed(() => {
return {
beginner: "初级排行榜",
intermediate: "中级排行榜",
expert: "高级排行榜"
}[scoreboardType.value]
})
const getScoreboard = computed(() => {
return leaderboards[scoreboardType.value]
})
const getDifficultyName = computed(() => {
return {
beginner: "初级",
intermediate: "中级",
expert: "高级"
}[difficulty.value]
})
// 检测移动设备
const detectMobile = () => {
return (
/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) ||
(navigator.platform === "MacIntel" && navigator.maxTouchPoints > 1)
)
}
// 初始化游戏
const init = () => {
// 检测设备类型
isMobile.value = detectMobile()
console.log("isMobile.value", isMobile.value, gameGridRef.value)
resetGame()
loadLeaderboard()
// 尝试自动播放音乐
// setTimeout(() => {
// toggleMusic()
// }, 1000)
}
// 重置游戏
const resetGame = () => {
gameState.value = "ready"
gameTime.value = 0
remainingMines.value = mineCount.value
score.value = 0
removeHighlight()
removePressed()
if (timer.value) {
clearInterval(timer.value)
timer.value = null
}
createGrid()
showScoreboard.value = false
showShareModal.value = false
isResetting.value = false
}
// 延迟重置游戏(带动画效果)
const delayedResetGame = () => {
isResetting.value = true
// 添加重置动画效果
for (let i = 0; i < gridSize.rows; i++) {
for (let j = 0; j < gridSize.cols; j++) {
grid.value[i][j].resetAnimating = true
}
}
// 1秒后重置游戏
setTimeout(() => {
resetGame()
}, 1000)
}
// 创建网格
const createGrid = () => {
grid.value = []
for (let i = 0; i < gridSize.rows; i++) {
const row = []
for (let j = 0; j < gridSize.cols; j++) {
row.push({
isMine: false, // 是否是雷
isRevealed: false, // 是否已经翻开
isFlagged: false, // 是否被标记为雷
isQuestion: false, // 是否被标记为问号
isHighlighted: false, // 是否被高亮显示
isPressed: false, // 是否被按下
resetAnimating: false, // 是否正在重置动画
number: 0 // 周围雷数量
})
}
grid.value.push(row)
}
console.log("grid.value", grid.value, gameGridRef.value)
}
// 设置难度
const setDifficulty = (level: "beginner" | "intermediate" | "expert") => {
difficulty.value = level
switch (level) {
case "beginner":
gridSize.rows = 9
gridSize.cols = 9
mineCount.value = 10
break
case "intermediate":
gridSize.rows = 16
gridSize.cols = 16
mineCount.value = 40
break
case "expert":
gridSize.rows = 16
gridSize.cols = 30
mineCount.value = 99
break
}
remainingMines.value = mineCount.value
resetGame()
}
// 开始游戏(第一次点击时)
const startGame = (firstClickRow: number, firstClickCol: number) => {
// 放置雷
placeMines(firstClickRow, firstClickCol)
// 计算每个格子的数字
calculateCellValues()
// 开始计时
gameState.value = "playing"
timer.value = setInterval(() => {
if (gameState.value === "playing") {
gameTime.value++
}
}, 1000)
}
// 放置雷(避开第一次点击的位置)
const placeMines = (firstClickRow: number, firstClickCol: number) => {
let minesPlaced = 0
while (minesPlaced < mineCount.value) {
const row = Math.floor(Math.random() * gridSize.rows)
const col = Math.floor(Math.random() * gridSize.cols)
// 确保第一次点击的位置不是雷
if (row === firstClickRow && col === firstClickCol) continue
if (!grid.value[row][col].isMine) {
grid.value[row][col].isMine = true
minesPlaced++
}
}
}
// 计算每个格子的数字(周围雷数量)
const calculateCellValues = () => {
for (let i = 0; i < gridSize.rows; i++) {
for (let j = 0; j < gridSize.cols; j++) {
if (!grid.value[i][j].isMine) {
grid.value[i][j].number = countAdjacentMines(i, j)
}
}
}
}
// 计算周围雷数量
const countAdjacentMines = (row: number, col: number) => {
let count = 0
for (let i = Math.max(0, row - 1); i <= Math.min(gridSize.rows - 1, row + 1); i++) {
for (let j = Math.max(0, col - 1); j <= Math.min(gridSize.cols - 1, col + 1); j++) {
if (i === row && j === col) continue
if (grid.value[i][j].isMine) {
count++
}
}
}
return count
}
// 统一处理单元格event事件
useHandleEvent(gameGridRef, (event) => {
const { type, cell, originalEvent } = event
const { row, col } = cell
const gridCell = grid.value[row][col]
console.log("type", type)
switch (type) {
case "down":
// 按下效果
gridCell.isPressed = true
break
case "up":
// 取消按下效果
gridCell.isPressed = false
removeHighlight()
removePressed()
break
case "longclick":
// 处理右键/长按事件
originalEvent.preventDefault()
if (!gridCell.isRevealed) {
// 循环切换标记状态:无标记 -> 旗帜 -> 问号 -> 无标记
if (!gridCell.isFlagged && !gridCell.isQuestion) {
gridCell.isFlagged = true
} else if (gridCell.isFlagged) {
gridCell.isFlagged = false
gridCell.isQuestion = true
} else {
gridCell.isQuestion = false
}
}
break
case "dblclick":
// 处理双击事件
if (gridCell.isRevealed && gridCell.number > 0) {
// 如果单元格已经翻开且有数字,尝试翻开周围的单元格
revealAdjacentCells(row, col)
}
break
case "click":
// 处理单击事件
if (!gridCell.isFlagged && !gridCell.isQuestion && !gridCell.isRevealed) {
// 翻开单元格
revealCell(row, col)
}
break
}
})
// 处理单元格点击
const handleCellClick = (row: number, col: number, event: MouseEvent) => {
if (gameState.value === "won" || gameState.value === "lost") return
const cell = grid.value[row][col]
// 在移动设备上,双击事件由handleDoubleClick处理
if (isMobile.value && cell.isRevealed && cell.number > 0) {
return
}
// 在PC上,左键点击翻开格子
if (!isMobile.value && event.button === 0 && !cell.isFlagged && !cell.isQuestion) {
revealCell(row, col)
}
}
// 处理单元格右键点击
const handleCellRightClick = (row: number, col: number) => {
if (gameState.value === "won" || gameState.value === "lost") return
// 在PC上,右键标记旗帜
if (!isMobile.value) {
toggleCellMark(row, col)
}
}
// 处理单元格双击
const handleCellDoubleClick = (row: number, col: number) => {
if (gameState.value === "won" || gameState.value === "lost") return
const cell = grid.value[row][col]
// 在移动设备上,双击标记旗帜
if (isMobile.value && !cell.isRevealed) {
toggleCellMark(row, col)
return
}
// 在PC上,双击已翻开的数字
if (cell.isRevealed && cell.number > 0) {
handleDoubleClickOnNumber(row, col)
}
}
// 处理单元格鼠标按下
const handleCellMouseDown = (row: number, col: number, event: MouseEvent) => {
if (gameState.value === "won" || gameState.value === "lost") return
const cell = grid.value[row][col]
// 如果是已翻开的数字,高亮周围未翻开的格子
if (cell.isRevealed && cell.number > 0 && event.button === 0) {
highlightAdjacentCells(row, col)
}
}
// 处理单元格鼠标释放
const handleCellMouseUp = () => {
removeHighlight()
removePressed()
}
// 处理单元格触摸开始
const handleCellTouchStart = (row: number, col: number, event: TouchEvent) => {
if (gameState.value === "won" || gameState.value === "lost") return
const cell = grid.value[row][col]
const currentTime = new Date().getTime()
const tapLength = currentTime - lastTapTime.value
// 清除之前的点击超时
if (tapTimeout.value) clearTimeout(tapTimeout.value)
// 如果是已翻开的数字,高亮周围未翻开的格子
if (cell.isRevealed && cell.number > 0) {
highlightAdjacentCells(row, col)
}
// 处理双击逻辑
if (tapLength < 300 && tapLength > 0) {
// 双击事件
toggleCellMark(row, col)
event.preventDefault()
} else {
// 设置单击超时
tapTimeout.value = setTimeout(() => {
// 单击事件
if (!cell.isFlagged && !cell.isQuestion) {
revealCell(row, col)
}
}, 300)
}
lastTapTime.value = currentTime
}
// 处理单元格触摸结束
const handleCellTouchEnd = () => {
removeHighlight()
removePressed()
}
// 处理双击数字
const handleDoubleClickOnNumber = (row: number, col: number) => {
// const cell = grid.value[row][col];
// 移除之前的高亮
removeHighlight()
// 检查周围是否有未标记的雷
let hasUnmarkedMine = false
for (let i = Math.max(0, row - 1); i <= Math.min(gridSize.rows - 1, row + 1); i++) {
for (let j = Math.max(0, col - 1); j <= Math.min(gridSize.cols - 1, col + 1); j++) {
if (i === row && j === col) continue
const adjCell = grid.value[i][j]
if (adjCell.isMine && !adjCell.isFlagged) {
hasUnmarkedMine = true
break
}
}
}
// 高亮周围未翻开的格子
for (let i = Math.max(0, row - 1); i <= Math.min(gridSize.rows - 1, row + 1); i++) {
for (let j = Math.max(0, col - 1); j <= Math.min(gridSize.cols - 1, col + 1); j++) {
if (i === row && j === col) continue
const adjCell = grid.value[i][j]
if (!adjCell.isRevealed && !adjCell.isFlagged && !adjCell.isQuestion) {
adjCell.isHighlighted = true
highlightedCells.value.push({ row: i, col: j })
// 如果有未标记的雷,模拟按下动作
if (hasUnmarkedMine) {
adjCell.isPressed = true
pressedCells.value.push({ row: i, col: j })
}
}
}
}
// 如果没有未标记的雷,翻开周围格子
if (!hasUnmarkedMine) {
return revealAdjacentCells(row, col)
}
// 设置定时器移除效果
setTimeout(() => {
removeHighlight()
removePressed()
}, 100)
}
// 高亮周围未翻开的格子
const highlightAdjacentCells = (row: number, col: number) => {
const cell = grid.value[row][col]
if (!cell.isRevealed || cell.number === 0) return
// 清除之前的高亮
removeHighlight()
// 高亮周围未翻开的格子
for (let i = Math.max(0, row - 1); i <= Math.min(gridSize.rows - 1, row + 1); i++) {
for (let j = Math.max(0, col - 1); j <= Math.min(gridSize.cols - 1, col + 1); j++) {
if (i === row && j === col) continue
const adjCell = grid.value[i][j]
console.log("adjCell: ", isMobile.value, adjCell.number, adjCell)
if (!adjCell.isRevealed && !adjCell.isFlagged && !adjCell.isQuestion) {
adjCell.isHighlighted = true
highlightedCells.value.push({ row: i, col: j })
// 在移动设备上,显示按下动画
if (isMobile.value) {
adjCell.isPressed = true
pressedCells.value.push({ row: i, col: j })
}
}
}
}
}
// 移除高亮
const removeHighlight = () => {
highlightedCells.value.forEach((pos) => {
if (grid.value[pos.row] && grid.value[pos.row][pos.col]) {
grid.value[pos.row][pos.col].isHighlighted = false
}
})
highlightedCells.value = []
}
// 移除按下状态
const removePressed = () => {
pressedCells.value.forEach((pos) => {
if (grid.value[pos.row] && grid.value[pos.row][pos.col]) {
grid.value[pos.row][pos.col].isPressed = false
}
})
pressedCells.value = []
}
// 切换单元格标记(旗子/问号)
const toggleCellMark = (row: number, col: number) => {
const cell = grid.value[row][col]
if (cell.isRevealed) return
if (!cell.isFlagged && !cell.isQuestion) {
cell.isFlagged = true
remainingMines.value--
if (cell.isMine) score.value += 10 // 正确标记雷得10分
} else if (cell.isFlagged) {
cell.isFlagged = false
cell.isQuestion = true
remainingMines.value++
} else if (cell.isQuestion) {
cell.isQuestion = false
}
}
// 揭示单元格
const revealCell = (row: number, col: number) => {
const cell = grid.value[row][col]
// 如果游戏还未开始(第一次点击)
if (gameState.value === "ready") {
return startGame(row, col)
}
if (cell.isRevealed || cell.isFlagged || cell.isQuestion) return
// 如果是雷,游戏结束
if (cell.isMine) {
cell.isRevealed = true
revealAllMines()
gameState.value = "lost"
if (timer.value) {
clearInterval(timer.value)
}
return
}
// 揭示当前单元格
cell.isRevealed = true
// 如果是空白单元格,递归揭示周围
if (cell.number === 0) {
revealAdjacentCells(row, col)
}
// 检查是否获胜
checkWinCondition()
}
// 揭示相邻单元格
const revealAdjacentCells = (row: number, col: number) => {
for (let i = Math.max(0, row - 1); i <= Math.min(gridSize.rows - 1, row + 1); i++) {
for (let j = Math.max(0, col - 1); j <= Math.min(gridSize.cols - 1, col + 1); j++) {
if (i === row && j === col) continue
const adjCell = grid.value[i][j]
if (!adjCell.isRevealed && !adjCell.isFlagged && !adjCell.isQuestion) {
revealCell(i, j)
}
}
}
}
// 揭示所有雷
const revealAllMines = () => {
for (let i = 0; i < gridSize.rows; i++) {
for (let j = 0; j < gridSize.cols; j++) {
const cell = grid.value[i][j]
if (cell.isMine) {
cell.isRevealed = true
}
}
}
}
// 检查获胜条件
const checkWinCondition = () => {
let unrevealedSafeCells = 0
for (let i = 0; i < gridSize.rows; i++) {
for (let j = 0; j < gridSize.cols; j++) {
const cell = grid.value[i][j]
if (!cell.isRevealed && !cell.isMine) {
unrevealedSafeCells++
}
}
}
if (unrevealedSafeCells === 0) {
gameState.value = "won"
if (timer.value) {
clearInterval(timer.value)
}
// 标记所有剩余雷
markAllRemainingMines()
}
}
// 标记所有剩余雷
const markAllRemainingMines = () => {
for (let i = 0; i < gridSize.rows; i++) {
for (let j = 0; j < gridSize.cols; j++) {
const cell = grid.value[i][j]
if (cell.isMine && !cell.isFlagged) {
cell.isFlagged = true
remainingMines.value--
score.value += 10 // 正确标记雷得10分
}
}
}
}
// 计算得分
const calculateScore = () => {
const baseScore = score.value
const timeBonus = Math.max(0, 1000 - gameTime.value * 10)
const difficultyBonus = {
beginner: 500,
intermediate: 1000,
expert: 2000
}[difficulty.value]
return baseScore + timeBonus + difficultyBonus
}
// 获取单元格显示内容
const getCellDisplay = (cell: any) => {
if (cell.isRevealed) {
// if (cell.isMine) return "💣";
if (cell.isMine) return ""
if (cell.number === 0) return ""
return cell.number
}
if (cell.isFlagged) return "🚩"
if (cell.isQuestion) return "?"
return ""
}
// // 切换音乐
// const toggleMusic = () => {
// const music = document.getElementById("backgroundMusic") as HTMLAudioElement
// if (musicPlaying.value) {
// music.pause()
// } else {
// music.play().catch(() => console.log("自动播放被阻止,请手动点击播放"))
// }
// musicPlaying.value = !musicPlaying.value
// }
// 加载排行榜
const loadLeaderboard = () => {
const savedLeaderboard = localStorage.getItem("minesweeperLeaderboard")
if (savedLeaderboard) {
Object.assign(leaderboards, JSON.parse(savedLeaderboard))
} else {
// 初始示例数据
leaderboards.beginner = [
{ player: "玩家1", time: 45, score: 850, date: "2023-06-15" },
{ player: "玩家2", time: 52, score: 820, date: "2023-06-14" }
]
leaderboards.intermediate = [
{ player: "高手1", time: 120, score: 1500, date: "2023-06-15" },
{ player: "高手2", time: 135, score: 1450, date: "2023-06-14" }
]
leaderboards.expert = [
{ player: "专家1", time: 240, score: 2500, date: "2023-06-15" },
{ player: "专家2", time: 260, score: 2400, date: "2023-06-14" }
]
saveLeaderboard()
}
}
// 保存排行榜
const saveLeaderboard = () => {
localStorage.setItem("minesweeperLeaderboard", JSON.stringify(leaderboards))
}
// 分享结果
const shareResult = () => {
const shareText = `我在${theme.value.title}游戏中取得了${calculateScore()}分!难度:${
getDifficultyName.value
},用时:${gameTime.value}秒`
if (navigator.share) {
navigator
.share({
title: "扫雷战绩分享",
text: shareText,
url: window.location.href
})
.catch(console.error)
} else {
alert(`分享内容: ${shareText}\n复制链接分享给朋友: ${window.location.href}`)
}
showShareModal.value = false
}
// 保存到排行榜
const saveToLeaderboard = () => {
const newEntry = {
player: playerName.value || "匿名玩家",
time: gameTime.value,
score: calculateScore(),
date: new Date().toISOString().split("T")[0]
}
// 添加到排行榜
leaderboards[difficulty.value].push(newEntry)
// 排序并保留前10名
leaderboards[difficulty.value].sort((a, b) => b.score - a.score)
leaderboards[difficulty.value] = leaderboards[difficulty.value].slice(0, 10)
// 保存到本地存储
saveLeaderboard()
alert("已保存到排行榜!")
showShareModal.value = false
showScoreboard.value = true
scoreboardType.value = difficulty.value
}
// 生命周期钩子
onMounted(() => {
init()
})
onUnmounted(() => {
if (timer.value) {
clearInterval(timer.value)
}
if (tapTimeout.value) {
clearTimeout(tapTimeout.value)
}
})
// 返回所有需要暴露的状态和方法
return {
// 状态
gameState,
gameTime,
// musicPlaying,
isResetting,
difficulty,
gridSize,
mineCount,
remainingMines,
isMobile,
grid,
score,
difficulties,
showScoreboard,
showShareModal,
scoreboardType,
playerName,
leaderboards,
// 计算属性
gameFace,
getScoreboardTitle,
getScoreboard,
getDifficultyName,
// 方法
removeHighlight,
setDifficulty,
handleCellClick,
handleCellRightClick,
handleCellDoubleClick,
handleCellMouseDown,
handleCellMouseUp,
handleCellTouchStart,
handleCellTouchEnd,
resetGame,
delayedResetGame,
toggleCellMark,
revealCell,
getCellDisplay,
// toggleMusic,
saveToLeaderboard,
calculateScore,
shareResult
}
}
请分析这段代码,帮我梳理并修改:单击或右键点击未翻开格子上依次为插旗/问号/取消,在数字上单击或双击如果范围内有超出的未翻开的格子则提醒,否则就递归翻开;在未翻开格子上双击则直接翻开;
最新发布