iT邦幫忙

2025 iThome 鐵人賽

DAY 16
2

主題

在前一天的進度中,我們已經完成了 基本格子繪製,能夠看到遊戲畫面上整齊的網格,並且維持未揭開狀態。今天我們要進一步實作 點擊揭開格子,並處理經典踩地雷中的一個重要功能:空白區域的自動擴散(Flood Fill)。

功能說明

  • 玩家點擊某個格子時:

    • 如果該格子是地雷 → 遊戲結束(未來處理)。
    • 如果該格子是數字(周圍地雷數量 > 0) → 顯示數字。
    • 如果該格子是空白(周圍沒有地雷) → 自動展開周圍相鄰的安全格子,直到邊界。

這裡的「自動展開」其實就是 Flood Fill 演算法,和影像處理中的「顏色填滿」非常類似。

實作思路

1. 狀態管理

  • 每個格子除了「是否有地雷」、「周圍地雷數」之外,還需要記錄「是否被揭開」。
  • 玩家點擊時,需要更新對應格子的狀態。

2. Flood Fill 實作

  • 當揭開一個格子,發現它周圍地雷數為 0,代表它是「空白格」。
  • 需要遞迴或 BFS/DFS,將周圍的格子也自動揭開。
  • 防止無限遞迴 → 需要判斷是否已經揭開過。

程式碼範例

以下是簡化過的 Ebiten 程式碼,示範如何實作點擊揭開與 Flood Fill:

點偵偵測

func (g *GameLayout) Update() error {
	// 偵測 mouse click 事件
	if ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft) {
		xPos, yPos := ebiten.CursorPosition()
		row := yPos / gridSize
		col := xPos / gridSize
		if row >= 0 && row < Rows && col >= 0 && col < Cols {
			// 執行 Flood Fill
			g.gameInstance.Board.Reveal(row, col)
		}
	}
	return nil
}

畫面更新

func (g *GameLayout) Draw(screen *ebiten.Image) {
	for row := 0; row < Rows; row++ {
		for col := 0; col < Cols; col++ {
			// 取出格子狀態
			cell := g.gameInstance.Board.GetCell(row, col)

			// 根據格子狀態,顯示對應的畫面
			// 當格子沒有被掀開時,畫出原本的灰階
			if !cell.Revealed {
				g.drawUnTouchCell(screen, row, col)
			} else {
				g.drawTouchCellBackground(screen, row, col)
				if cell.AdjacenetMines != 0 {
					g.drawTouchCellAdjacency(screen, row, col, cell.AdjacenetMines)
				}
				if cell.IsMine {
					g.drawTouchCellMine(screen, row, col)
				}
			}
		}
	}
}

Flood Fill 實做

// Reveal - 從 row, col 開始翻開周圍不是地雷,直到遇到非零的格子
func (board *Board) Reveal(row, col int) {
	// 超出邊界
	if row < 0 || row >= board.rows ||
		col < 0 || col >= board.cols {
		return
	}

	cell := board.cells[row][col]
	// 已經被揭開
	if cell.Revealed {
		return
	}

	// 標注該格已經被揭開
	board.cells[row][col].Revealed = true

	// 如果是空白格 (AdjacenetMines = 0, 且不是地雷)
	if !cell.IsMine && cell.AdjacenetMines == 0 {
		// 鄰近所有方向
		neighborDirections := [8]coord{
			{Row: -1, Col: -1}, {Row: -1, Col: 0}, {Row: -1, Col: 1},
			{Row: 0, Col: -1}, {Row: 0, Col: 1},
			{Row: 1, Col: -1}, {Row: 1, Col: 0}, {Row: 1, Col: 1},
		}
		for _, direction := range neighborDirections {
			neighborRow, neighborCol := row+direction.Row, col+direction.Col
			board.Reveal(neighborRow, neighborCol)
		}
	}
}

flood fill 解說

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

從按下的位置的格子開始
step1 首先檢查當下這個格子是否已經走訪過了
step2 當已經走訪過,則直接結束
step3 如果還沒走訪過,則標注為走否過
step4 如果這個格子是地雷則結束
step5 如果這個格子鄰近的地雷數超過 0 則結束
step6 從這個格子以鄰近的格子為當下位置 從 step1 開始執行

執行結果

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

github 驗證

https://github.com/leetcode-golang-classroom/mine-sweeper-game/actions/runs/17331520311/job/49208372938

驗收條件 ✅

  • 點擊格子後:

    • 該格子會正確顯示。
    • 空白格會觸發 Flood Fill,自動展開相鄰安全格。
    • 相鄰的數字格會正確顯示。

本日收穫 🎯

  • 理解了 格子揭開 的邏輯。
  • 實作了 Flood Fill,能夠展開空白區域。
  • 程式碼結構開始逐漸具備「踩地雷」的樣貌。

明日預告 🔮

明天我們將要實作 右鍵插旗功能,讓玩家可以標記地雷位置,進一步接近完整的踩地雷玩法。


上一篇
踩地雷遊戲: 使用 Ebiten 繪製踩地雷的基本格子
下一篇
踩地雷遊戲:旗子標記功能
系列文
在 ai 時代 gopher 遊戲開發者的 30 天自我養成20
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言