iT邦幫忙

2025 iThome 鐵人賽

DAY 19
2

主題

在上一篇文章中,我們已經完成了踩地雷的基礎功能,包括地圖生成、點擊格子與遊戲結束判斷。
今天,我要帶大家實作 重新開始功能

🎯 主要邏輯

重新開始功能需要支援:

  • 玩家點擊 UI 上的 Restart 按鈕 → 遊戲重新開始。

  • 重新開始後:

    1. 地圖重新生成。
    2. 遊戲狀態回到「進行中」。
    3. 旗子數量全部重置。

Ebiten 遊戲邏輯實作

繪製重新開始的 icon button

var buttonRectRelativePos = image.Rect(0, 0, 32, 32) // 一個方格大小的 button
// drawButtonWithIcon - 繪製 buttonIcon
func (g *GameLayout) drawButtonWithIcon(screen *ebiten.Image, emojiIcon string) {
	vector.DrawFilledRect(screen,
		float32((ScreenWidth-1.5*gridSize)/2+buttonRectRelativePos.Min.X),
		float32(buttonRectRelativePos.Min.Y),
		float32(buttonRectRelativePos.Dx()+0.5*gridSize),
		float32(buttonRectRelativePos.Dy()+3),
		color.RGBA{120, 120, 120, 255},
		true,
	)
	vector.DrawFilledCircle(screen, ScreenWidth/2, gridSize/2, 16,
		color.RGBA{180, 180, 0, 255},
		true,
	)
	emojiValue := emojiIcon
	emojiXPos := (ScreenWidth) / 2
	emojiYPos := PaddingY
	emojiOpts := &text.DrawOptions{}
	emojiOpts.ColorScale.ScaleWithColor(getTileColor(IsButtonIcon))
	emojiOpts.PrimaryAlign = text.AlignCenter
	emojiOpts.SecondaryAlign = text.AlignCenter
	emojiOpts.GeoM.Translate(float64(emojiXPos), float64(emojiYPos))
	text.Draw(screen, emojiValue, &text.GoTextFace{
		Source: emojiFaceSource,
		Size:   32,
	}, emojiOpts)
}

繪製結果

https://ithelp.ithome.com.tw/upload/images/20250831/20111580fUg93cFIPs.png

重新開始邏輯

// Restart - 重新建立 Game 狀態
func (g *GameLayout) Restart() {
	g.gameInstance = game.NewGame(Rows, Cols, MineCounts)
}

偵測鍵盤去處理重新處理的邏輯

// 偵測 restart icon 有被點擊
	if ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft) {
		xPos, yPos := ebiten.CursorPosition()
		if xPos >= ((ScreenWidth-1.5*gridSize)/2+buttonRectRelativePos.Min.X) &&
			xPos <= (ScreenWidth)/2+buttonRectRelativePos.Max.X+0.5*gridSize &&
			yPos >= buttonRectRelativePos.Min.Y &&
			yPos <= buttonRectRelativePos.Max.Y+3 {
			g.Restart()
		}
	}

給 gemini 產生的 prompt

gemini -p "請根據我目前的變動,幫我撰寫出不需要使用ebiten library 的 unit test 來驗證 game restart的遊戲狀態跟遊戲剛開始是一樣的"

gemini 給出的建議

我看到您在 internal/layout/layout.go 中新增了一個 Restart 方法,這個方法會透過呼叫 game.NewGame()
  來建立一個新的遊戲實例。


  為了要驗證遊戲重新開始後的狀態與初始狀態相同,並且不使用 ebiten 函式庫,我將會直接在 internal/game
  package 中撰寫一個新的 unit test。


  這個測試會模擬以下情境:
   1. 建立一個遊戲。
   2. 改變遊戲狀態 (例如,揭開一個格子)。
   3. 建立一個新的遊戲 (模擬 Restart)。
   4. 比較這兩個遊戲的初始狀態是否一致。

根據 gemini 的建議所以產生出來的測試

func TestGameRestart(t *testing.T) {
	const (
		rows      = 5
		cols      = 5
		mineCount = 5
	)

	predicableMineShuffler := func(coords []coord) {
		// not shuffler
	}
	newGameWithPredictableMines := func() *Game {
		game := NewGame(rows, cols, mineCount)
		game.Board.minePositionShuffler = predicableMineShuffler
		game.Board.cells = make([][]*Cell, rows)
		for r := range game.Board.cells {
			game.Board.cells[r] = make([]*Cell, cols)
			for c := range game.Board.cells[r] {
				game.Board.cells[r][c] = &Cell{}
			}
		}
		game.Board.mineCoords = []coord{}
		game.Board.PlaceMines(mineCount)
		game.Board.CalculateAdjacentMines()
		return game
	}
	// 1. create an initial game state for comparison
	gameInitial := newGameWithPredictableMines()

	// 2. create another game to manipulate
	gamePlaying := newGameWithPredictableMines()

	// 3. Change the state of gamePlaying
	// Let's reveal a safe cell (a cell that is not a mine)
	// With our predictable shuffler, mines are at (0,0), (0,1), (0,2), (0,3), (0,4)
	// Let's reveal cell (4,4) which is safe
	gamePlaying.Board.Reveal(4, 4)

	assert.NotEqual(t, gameInitial.Board.cells, gamePlaying.Board.cells)

	// 5 Simulate a restart by create a new game
	restartedGame := NewGame(rows, cols, mineCount)
	restartedGame.Board.minePositionShuffler = predicableMineShuffler
	restartedGame.Board.minePositionShuffler = predicableMineShuffler
	restartedGame.Board.cells = make([][]*Cell, rows)
	for r := range restartedGame.Board.cells {
		restartedGame.Board.cells[r] = make([]*Cell, cols)
		for c := range restartedGame.Board.cells[r] {
			restartedGame.Board.cells[r][c] = &Cell{}
		}
	}
	restartedGame.Board.mineCoords = []coord{}
	restartedGame.Board.PlaceMines(mineCount)
	restartedGame.Board.CalculateAdjacentMines()

	// verify that the restarted game state is identical to intial game state
	assert.Equal(t, gameInitial.Board.cells, restartedGame.Board.cells)
	assert.Equal(t, gameInitial.IsGameOver, restartedGame.IsGameOver)
	assert.Equal(t, gameInitial.Board.GetRemainingFlags(), restartedGame.Board.GetRemainingFlags())
}

在這個測試,我透過操作遊戲狀態來驗正,我的 restart 實做
因為把狀態跟畫面呈現分開,因此可以很方便的測試

執行畫面

遊戲結束 
https://ithelp.ithome.com.tw/upload/images/20250901/20111580CwrCcVJSKZ.png
點擊icon button 之後
https://ithelp.ithome.com.tw/upload/images/20250901/2011158022HOJEWgiT.png
遊戲勝利
https://ithelp.ithome.com.tw/upload/images/20250901/20111580p14VP0Mxbq.png
點擊icon button 之後
https://ithelp.ithome.com.tw/upload/images/20250901/2011158022HOJEWgiT.png

github action 測試結果

https://github.com/leetcode-golang-classroom/mine-sweeper-game/actions/runs/17359685757

🚀 結語

今天我們完成了:

  • 🎮 UI 按鈕的重新開始功能。
  • 🧪 單元測試驗證遊戲是否正確重置。
  • 🤖 使用 Gemini CLI Prompt 輔助設計測試。

明日預計

下一步,我會帶大家做 遊戲時間計數器 讓遊戲更完整!


上一篇
踩地雷遊戲:勝負判斷邏輯
下一篇
踩地雷遊戲:UI 顯示計時器
系列文
在 ai 時代 gopher 遊戲開發者的 30 天自我養成20
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言