iT邦幫忙

2025 iThome 鐵人賽

DAY 5
2

🎯 今日目標

今天我們要完成 2048 遊戲的核心動作之一——滑動合併,並先以「左滑」為例實作。
我們會先在 CLI 環境驗證邏輯,等確定正確後,再整合到 Ebiten 畫面更新邏輯。

1️⃣ 左滑的遊戲規則回顧

在 2048 中,「左滑」時每一行數字會:

  1. 去掉空格:將所有非零的數字往左靠。
  2. 合併相同數字:從左到右檢查,相同的數字合併成它們的和(例如 2 + 2 → 4),並將該格右邊設為 0。
  3. 再次去掉空格:確保合併後的數字依然緊靠左邊。

範例:

初始行: [2, 0, 2, 4]
去掉空格: [2, 2, 4, 0]
合併後: [4, 0, 4, 0]
再去空格: [4, 4, 0, 0]

2️⃣ 資料結構回顧

我們的盤面在 Day 3 已經用 二維整數陣列 表示:

var board = [4][4]int{
    {2, 0, 2, 4},
    {0, 4, 4, 0},
    {2, 2, 0, 2},
    {0, 0, 0, 2},
}

3️⃣ 核心邏輯設計

我們先實作一個可以處理「單行左滑」的函式,然後讓它去處理整個盤面的每一行。

單行左滑函式

// slideAndMergeLeft - 向左滑動並且合併
func (g *Game) slideAndMergeLeft(row []int) []int {
	// 1 去掉空格
	filtered := make([]int, 0, len(row))
	for _, v := range row {
		if v != 0 {
			filtered = append(filtered, v)
		}
	}

	// 假設沒有空格
	if len(filtered) == 0 {
		return row
	}

	// 2 合併相鄰相同數字
	for i := 0; i < len(filtered)-1; i++ {
		if filtered[i] == filtered[i+1] {
			filtered[i] *= 2
			filtered[i+1] = 0
			i++ // 跳過剛合併的數字
		}
	}

	// 3 再次去掉空格
	result := make([]int, 0, len(row))
	for _, v := range filtered {
		if v != 0 {
			result = append(result, v)
		}
	}

	// 4 補充剩下的空格為 0
	for len(result) < len(row) {
		result = append(result, 0)
	}

	return result
}


4️⃣ 套用到整個盤面

// moveLeft - 整個 board 同時左移
func (g *Game) moveLeft() {
	for r := 0; r < sideSize; r++ {
		g.board[r] = g.slideAndMergeLeft(g.board[r][:])
	}
}

5️⃣ 測試案例範例

func TestGameMoveLeft(t *testing.T) {
	type field struct {
		board [][]int
	}
	tests := []struct {
		name  string
		input field
		want  [][]int
	}{
		{
			name: "case1: 單行多次合併",
			input: field{
				board: [][]int{
					{4, 4, 4, 4},
					{2, 2, 0, 0},
					{2, 0, 2, 0},
					{8, 0, 0, 8},
				},
			},
			want: [][]int{
				{8, 8, 0, 0},
				{4, 0, 0, 0},
				{4, 0, 0, 0},
				{16, 0, 0, 0},
			},
		},
		{
			name: "case2: 新生成的數字不參與當回合合併",
			input: field{
				board: [][]int{
					{2, 2, 4, 8},
					{0, 0, 0, 0},
					{4, 4, 8, 8},
					{2, 2, 2, 2},
				},
			},
			want: [][]int{
				{4, 4, 8, 0},
				{0, 0, 0, 0},
				{8, 16, 0, 0},
				{4, 4, 0, 0},
			},
		},
		{
			name: "case3: 無合併,只有移動",
			input: field{
				board: [][]int{
					{2, 4, 8, 16},
					{0, 2, 0, 4},
					{8, 0, 4, 0},
					{2, 4, 2, 4},
				},
			},
			want: [][]int{
				{2, 4, 8, 16},
				{2, 4, 0, 0},
				{8, 4, 0, 0},
				{2, 4, 2, 4},
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			game := NewGame()
			game.Init(tt.input.board, nil, nil)
			// 模擬左移
			game.moveLeft()
			assert.Equal(t, tt.want, game.board)
		})
	}
}

透過 gemini 產生測試案例的 prompt

gemini -p "你是一位 Golang 與 2048 遊戲邏輯驗證專家。
現在我提供一個 4x4 遊戲盤面,請你模擬 2048 的『左滑』規則,並驗證我程式輸出的結果是否正確。

【2048 左滑規則】:
1. 所有數字向左靠,移除空格(0)。
2. 相鄰且相同的數字會合併成它們的和,並將該格右邊的數字設為 0。
3. 合併後再次移除空格(0),並在右側補 0 至長度一致。
4. 同一回合中,每個數字最多只能被合併一次。

【輸入範例】
初始盤面:
[2 0 2 4]
[0 4 4 0]
[2 2 0 2]
[0 0 0 2]

我的程式輸出:
[4 4 0 0]
[8 0 0 0]
[4 2 0 0]
[2 0 0 0]

請你依規則驗證我的輸出是否正確。
如果正確,請回覆『驗證通過』並簡述理由。
如果錯誤,請指出錯誤的行與正確的期望結果,並解釋錯誤原因。

請同時產生三組額外的測資(初始盤面與正確輸出),方便我繼續測試程式。"

gemini 所產生的測試資料

三組測資,涵蓋了一些常見的邊界情況,供您繼續測試。

測資 1:測試單行多次合併

  • 初始盤面:
    [4 4 4 4]
    [2 2 0 0]
    [2 0 2 0]
    [8 0 0 8]
    
  • 正確輸出:
    [8 8 0 0]
    [4 0 0 0]
    [4 0 0 0]
    [16 0 0 0]
    

測資 2:測試新生成的數字不參與當回合合併

  • 初始盤面:
    [2 2 4 8]
    [0 0 0 0]
    [4 4 8 8]
    [2 2 2 2]
    
  • 正確輸出:
    [4 4 8 0]
    [0 0 0 0]
    [8 16 0 0]
    [4 4 0 0]
    

測資 3:測試無合併,僅移動

  • 初始盤面:
    [2 4 8 16]
    [0 2 0 4]
    [8 0 4 0]
    [2 4 2 4]
    
  • 正確輸出:
    [2 4 8 16]
    [2 4 0 0]
    [8 4 0 0]
    [2 4 2 4]
    

驗證測試案例

https://github.com/leetcode-golang-classroom/2048-game/actions/runs/17046494123/job/48323566098

✅ 今日驗收條件

  • 合併邏輯與 2048 規則一致,且不會多次合併同一數字。
  • 盤面更新正確,空格補零。

📌 明日預告(Day 6)

明天我們會 將左滑邏輯延伸到四個方向(上、右、下),並探討如何透過矩陣轉置與反轉,讓同一個函式處理所有方向的滑動,避免重複寫邏輯。


上一篇
2048 遊戲: 隨機新增數字邏輯實作
下一篇
2048 遊戲: 實作其他方向滑動(上、右、下)
系列文
在 ai 時代 gopher 遊戲開發者的 30 天自我養成23
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言