iT邦幫忙

2025 iThome 鐵人賽

DAY 18
1

前言

勝負判斷是 Minesweeper 的核心流程之一:當玩家不小心踩到地雷時應立即結束遊戲;當玩家揭開了所有非地雷格子時則判定勝利。本篇以現有的資料結構(CellBoardGame)為基礎,示範實作、解釋關鍵邏輯、並提供可測試的範例與驗收項目。

今日重點(摘要)

  • 失敗判定:玩家揭開(reveal)到 IsMine == trueIsGameOver = true
  • 勝利判定:當「剩餘需要揭開的安全格子數 (Remaining)」變為 0,或遍歷確認所有非雷格皆為 RevealedIsPlayerWin = true
  • 在 UI 層顯示對應提示(灰色遮罩 + 文字提示 )。

設計要點(程式流程)

  1. 初始化
    • Remaining = Rows*Cols - mineCount(開始時尚未揭開的安全格子數)。
  2. RevealCell(row,col)
    • 若該格已揭開或已插旗 → 不處理(返回)。
    • 如果 IsMine == true:把該格設為 IsRevealed,把 Game.IsGameOver = true,並結束流程(可顯示所有地雷)。
    • 否則揭開該格:IsRevealed = trueRemaining--
    • 如果該格 AdjacentMines == 0 → 執行 flood-fill(BFS/DFS)展開相鄰安全格(同時減少 Remaining)。
    • 每次揭開後呼叫 CheckIsPlayerWin() 判斷是否勝利。
  3. CheckIsPlayerWin
    • Remaining == 0IsPlayerWin = true(視需求:IsPlayerWin 會鎖定遊戲輸入)。
    • (備註)也可實作「旗子全部正確標記」的判定,但主流與簡潔做法仍以 Remaining==0 為標準。

核心程式邏輯

  1. 控制畫面更新的邏輯判斷
func (g *GameLayout) Update() error {
	// 當狀態為遊戲結束
	if g.gameInstance.IsGameOver || g.gameInstance.IsPlayerWin {
		return nil
	}
	// 偵測 mouse 左鍵 click 事件
	if ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft) {
		g.handlePositionClickEvent(func(row, col int) {
			// 檢查是否踩到地雷
			if g.gameInstance.Board.GetCell(row, col).IsMine {
				g.gameInstance.IsGameOver = true
			}
			// 執行 Flood Fill - 更新踩到之後的更新
			g.gameInstance.Board.Reveal(row, col)
			// 檢查是否達到勝利條件
			if !g.gameInstance.IsGameOver {
				g.gameInstance.IsPlayerWin = g.gameInstance.Board.CheckIsPlayerWin()
			}
		})
	}
	// 偵測 mouse 右鍵 click 事件
	if inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonRight) {
		// 標記該位置格子
		g.handlePositionClickEvent(func(row, col int) {
			g.gameInstance.Board.ToggleFlag(row, col)
		})
	}
	return nil
}
  1. 判斷遊戲勝利的條件
// CheckIsPlayerWin - 檢查是否所有該非地雷的格子都有被掀開
func (board *Board) CheckIsPlayerWin() bool {
	return board.remainingUnRevealedCells == 0
}
  1. 當點到地雷時,顯示所有其他的地雷
// revealMines - 顯示所有 Mines
func (board *Board) revealMines() {
	for _, mineCoord := range board.mineCoords {
		if !board.cells[mineCoord.Row][mineCoord.Col].Flagged &&
			!board.cells[mineCoord.Row][mineCoord.Col].Revealed {
			board.cells[mineCoord.Row][mineCoord.Col].Revealed = true
		}
		if board.cells[mineCoord.Row][mineCoord.Col].Flagged {
			board.cells[mineCoord.Row][mineCoord.Col].Revealed = true
		}
	}
}
  1. 在 UnReveal 加入更新 remainingUnRevealedCells 的邏輯
remainingUnRevealedCells--
  1. 更新 GameOver 的畫面顯示
// drawCoverOnGameOver - 畫出無法操作的灰色遮罩
func (g *GameLayout) drawCoverOnGameOver(screen *ebiten.Image) {
	w, h := ScreenWidth, ScreenHeight-PanelHeight
	vector.DrawFilledRect(
		screen,
		0, PanelHeight, // x, y
		float32(w), float32(h), // width, height
		color.RGBA{0, 0, 0, 128}, // 半透明黑色 (128 = 約 50% 透明)
		false,
	)
}

// getColorStatus - 根據 IsGameOver 與 IsPlayerWin 來找出對 message, bgColor
func (g *GameLayout) getColorStatus() (string, color.RGBA) {
	bgColor := color.RGBA{100, 100, 0x10, 0xFF}
	status := "playing"
	if g.gameInstance.IsGameOver {
		status = "game over"
		bgColor = color.RGBA{150, 0, 0x10, 0xFF}
	}
	if g.gameInstance.IsPlayerWin {
		status = "you win"
		bgColor = color.RGBA{200, 200, 0, 0xFF}
	}
	return status, bgColor
}

執行結果

  1. 踩到地雷

https://ithelp.ithome.com.tw/upload/images/20250830/20111580oFcg0jgDtp.png

  1. 勝利
    https://ithelp.ithome.com.tw/upload/images/20250830/20111580493QHm3YQ5.png

本日收穫

  • 完成 Minesweeper 勝負判斷核心:揭雷即敗、揭開所有非雷即勝。
  • 學會如何把 Remaining 與 reveal 流程綁定,並使用 BFS 做 flood-fill 展開。
  • 為 UI 顯示勝敗訊息與後續 Restart 機制打好基礎。

明日預告

  • 滑鼠與鍵盤事件處理整合 — 重新開始功能與 UI 按鈕互動。

上一篇
踩地雷遊戲:旗子標記功能
下一篇
踩地雷遊戲:重新開始功能與 UI 按鈕互動
系列文
在 ai 時代 gopher 遊戲開發者的 30 天自我養成20
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言