用Python實現2️⃣0️⃣4️⃣8️⃣ 好玩的方塊消除遊戲

更新於 發佈於 閱讀時間約 31 分鐘
2048遊戲畫面

2048遊戲畫面

🎮 Python 2048 遊戲教學:
從零開始打造好玩的方塊消除遊戲!

哈囉,同學們!今天我們要一起用 Python 寫一個超經典的遊戲--2048!
而且這個遊戲可以直接在Command line(也就是命令提示字元、Console)玩!

在這篇教學哩,會一步步帶你理解遊戲邏輯,還有怎麼撰寫對應的程式程式碼,讓你自己也能做出來,甚至還能自己改寫成其他類似的方塊消除遊戲喔!


🎯 遊戲目標與規則

目標:把數字方塊合併到 2048 ,一但有一個數字抵達2048,即宣告遊戲獲勝!

規則

2048 是一款很有趣的益智遊戲,玩法很簡單:

  • 你有一個4x4的方格 🟦
  • 每次可以用W、A、S、D,把所有方塊往一個方向推 ⬅️⬆️➡️⬇️
  • 相同數字的方塊碰到一起會合併,數字會變成兩倍 ✖️2
  • 每移動一次,空白地方會隨機出現一個2或4 🎲
  • 目標是合成一個數字是2048的方塊 🏆
  • 如果沒有空格也不能合併了,遊戲就結束囉!💀

📝我們要怎麼做呢? (功能分析與拆解)

們會分成幾個小步驟:

  1. 建立4x4的遊戲板和分數 🧮
  2. 顯示每一回合的遊戲畫面 👀
  3. 處理方塊移動和合併 🔄
  4. 隨機新增數字方塊 🎲
  5. 判斷2048遊戲是否結束Gameover,或者宣告勝利 Victory🏁
  6. 讀取玩家的鍵盤輸入 ⌨️
  7. 主遊戲迴圈 🔁

1. 建立遊戲板和分數 🟦

我們先用一個4x4的二維陣列(在Python,就是2D list)來代表4x4的遊戲板,裡面放數字,用0代表空格,其餘的數字就對應到合成出來的方塊。
遊戲開始時,我們會隨機放兩個初始數字,初始數字有可能是2或4。

def initialize_game():
grid = [[0]*4 for _ in range(4)] # 建立4x4的遊戲板
add_new_tile(grid) # 隨機放置一個初始數字
add_new_tile(grid) # 再次隨機放置一個初始數字
score = 0 # 開局分數從0開始計分​
return grid, score

2. 顯示遊戲畫面 👀

每回合移動後,我們要把當下的遊戲板和分數顯示在螢幕上
讓玩家知道現在的遊戲狀況。

import os

def display_grid(grid, score):
os.system('clear' if os.name == 'posix' else 'cls') # 清除畫面(Linux/Mac用clear,Windows用cls)
print(f"2048 遊戲!目前分數:{score} 🏅")
print("-"*25)
for row in grid: # 印出4x4遊戲版的每一條row​
print("|", end="")
for num in row:
if num == 0: # 0 代表空位
print(" ", end="|")
else: # 非0 代表隊應的方塊2, 4, 8, 16, ...​
print(f"{num:5d}", end="|")

print("\n" + "-"*25) # 每一列的分隔線 -----
print("請用 W/A/S/D 移動,Q 離開遊戲 ⌨️")

3. 方塊移動和合併的邏輯 2️⃣2️⃣ ➡️ 4️⃣

這是遊戲的重點!我們需要:

  • 把所有非零數字推向同一邊(壓縮)🧲
  • 相鄰相同的數字合併,並加上對應的分數(例如 2, 2 合併得4,加4分)
  • 再次壓縮,讓空格都在一邊

我們先寫個小函式幫忙「壓縮」:

def compress(line):

new_line = [num for num in line if num != 0] # 把非零數字挑出來
new_line += [0] * (4 - len(new_line)) # 補零,對其到4個格子的寬度

return new_line # 回傳朝牆壁壓縮後的結果


接著,寫相鄰方塊數字相同的合併function:

def merge(line):
score_add = 0
for i in range(3):

# 相鄰兩個方塊數字相同的時候,進行合併
if line[i] != 0 and line[i] == line[i+1]:
line[i] *= 2
line[i+1] = 0
score_add += line[i] # 加分!🎉

return line, score_add


最後,根據W/A/S/D的方向移動整個數字方塊:

def move(grid, direction, score):

moved = False # 判斷是否至少有一塊方塊被移動過​
score_add = 0 # 這次移動,帶來的分數​
original = [row[:] for row in grid] # 複製一份原本的遊戲版

if direction == 'left':
for i in range(4):
line = compress(grid[i])
line, add = merge(line)
line = compress(line)
grid[i] = line
score_add += add

elif direction == 'right':
for i in range(4):
line = grid[i][::-1]
line = compress(line)
line, add = merge(line)
line = compress(line)
grid[i] = line[::-1]
score_add += add

elif direction == 'up':
for j in range(4):
line = [grid[i][j] for i in range(4)]
line = compress(line)
line, add = merge(line)
line = compress(line)
for i in range(4):
grid[i][j] = line[i]
score_add += add

elif direction == 'down':
for j in range(4):
line = [grid[i][j] for i in range(4)][::-1]
line = compress(line)
line, add = merge(line)
line = compress(line)
line = line[::-1]
for i in range(4):
grid[i][j] = line[i]
score_add += add

# 判斷有沒有動過
moved = any(grid[i][j] != original[i][j] for i in range(4) for j in range(4))
return moved, score + score_add

4. 新增隨機方塊 2️⃣ 🎲 4️⃣

每次移動成功後,我們會在空格隨機放一個新的數字方塊
有可能是2(90%機率,或者是4(10%機率)。

def add_new_tile(grid):

empty = [(i,j) for i in range(4) for j in range(4) if grid[i][j] == 0]

if empty:
i,j = random.choice(empty)

# 在空格隨機放一個新的數字方塊,
# 有可能是2(90%機率,或者是4(10%機率)。
grid[i][j] = 2 if random.random() < 0.9 else 4

5. 判斷是否遊戲結束(Gameover),或者宣告勝利(Victory) 🏁

  • 如果有成功合成出一個方塊是2048,那就贏了 🏆
  • 如果沒有剩餘空格,而且也不能合併方塊,就輸了 😭
def has_won(grid):

# 如果有成功合成出一個方塊是2048,那就贏了 🏆
return any(2048 in row for row in grid)

def has_moves_left(grid):
for i in range(4):
for j in range(4):
if grid[i][j] == 0:
return True
if j < 3 and grid[i][j] == grid[i][j+1]:
return True
if i < 3 and grid[i][j] == grid[i+1][j]:
return True

# 如果沒有剩餘空格,而且也不能合併方塊,就輸了 😭
return False

6. 讀取玩家輸入 ⌨️

因為我們在Command line玩,我們用不同OS的鍵盤讀取function方式來取得玩家的選擇方向(W/A/S/D):

def get_key():

    """獲取鍵盤輸入"""
    try:

        import msvcrt  # Windows
        return msvcrt.getch().decode('utf-8',errors='ignore').lower()

    except ImportError:

        try:
            import termios, tty, sys  # Unix/Linux/MacOS
            fd = sys.stdin.fileno()
            old_settings = termios.tcgetattr(fd)

            try:
                tty.setraw(sys.stdin.fileno())
                ch = sys.stdin.read(1).lower()

            finally:
                termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
            return ch

        except:
            # 如果以上方法都失敗,使用input函數
            key = input("輸入移動方向 (W/A/S/D): ").lower()
            return key[0] if key else 'w'

7. 主遊戲迴圈 🔁

現在,把所有已經實現的模組組合在一起,就大功告成囉!

def main():
grid, score = initialize_game()

while True:
display_grid(grid, score)

if has_won(grid):
print("太棒了!你贏了,成功合成出2048!🏆")
break

if not has_moves_left(grid):
print("遊戲結束,沒有更多移動了!😢")
break
key = get_key()

if key == 'q':
print("遊戲結束,掰掰!👋")
break

directions = {'w':'up', 'a':'left', 's':'down', 'd':'right'}
if key in directions:
moved, score = move(grid, directions[key], score)
if moved:
add_new_tile(grid)


最後,記得補上entry point程式進入點的呼叫:

if __name__ == "__main__":
try:
main()
except KeyboardInterrupt:
print("\n遊戲中斷囉!🛑")

完整程式碼

import random
import os

def initialize_game():
"""初始化遊戲網格和分數"""
grid = [[0 for _ in range(4)] for _ in range(4)]
score = 0
# 隨機添加兩個初始方塊
add_new_tile(grid)
add_new_tile(grid)
return grid, score

def display_grid(grid, score):
"""顯示遊戲網格和分數"""
os.system('clear' if os.name == 'posix' else 'cls') # 清除螢幕
print(f"2048遊戲 - 分數: {score}")
print("-" * 25)
for row in grid:
print("|", end="")
for cell in row:
if cell == 0:
print(" ", end="|")
else:
print(f"{cell:5d}", end="|")
print("\n" + "-" * 25)
print("使用 W/A/S/D 控制移動,Q 退出")

def add_new_tile(grid):
"""在空白處隨機添加一個新方塊(值為2或4)"""
empty_cells = []
for i in range(4):
for j in range(4):
if grid[i][j] == 0:
empty_cells.append((i, j))

if empty_cells:
i, j = random.choice(empty_cells)
grid[i][j] = 2 if random.random() < 0.9 else 4
return True
return False

def compress(line):
"""移除所有零並將非零元素移到開頭"""
new_line = [cell for cell in line if cell != 0]
new_line = new_line + [0] * (4 - len(new_line))
return new_line

def merge(line):
"""合併相鄰的相同數字"""
score_addition = 0
for i in range(3):
if line[i] != 0 and line[i] == line[i + 1]:
line[i] *= 2
score_addition += line[i]
line[i + 1] = 0
return line, score_addition

def move(grid, direction, score):
"""根據方向移動所有方塊"""
# 保存原始網格以檢查是否有移動
original_grid = [row[:] for row in grid]
total_score_addition = 0

# 根據方向處理每行/列
if direction == 'left':
for i in range(4):
# 壓縮,移除所有零
grid[i] = compress(grid[i])
# 合併
grid[i], score_add = merge(grid[i])
# 再次壓縮,移除合併後產生的零
grid[i] = compress(grid[i])
total_score_addition += score_add

elif direction == 'right':
for i in range(4):
# 反轉
grid[i].reverse()
# 壓縮
grid[i] = compress(grid[i])
# 合併
grid[i], score_add = merge(grid[i])
# 再次壓縮
grid[i] = compress(grid[i])
# 再次反轉
grid[i].reverse()
total_score_addition += score_add

elif direction == 'up':
for j in range(4):
# 提取列
column = [grid[i][j] for i in range(4)]
# 壓縮
column = compress(column)
# 合併
column, score_add = merge(column)
# 再次壓縮
column = compress(column)
# 更新網格
for i in range(4):
grid[i][j] = column[i]
total_score_addition += score_add

elif direction == 'down':
for j in range(4):
# 提取列
column = [grid[i][j] for i in range(4)]
# 反轉
column.reverse()
# 壓縮
column = compress(column)
# 合併
column, score_add = merge(column)
# 再次壓縮
column = compress(column)
# 反轉
column.reverse()
# 更新網格
for i in range(4):
grid[i][j] = column[i]
total_score_addition += score_add

# 檢查是否有移動
moved = any(original_grid[i][j] != grid[i][j] for i in range(4) for j in range(4))

return moved, score + total_score_addition

def has_won(grid):
"""檢查是否已達到2048"""
return any(any(cell >= 2048 for cell in row) for row in grid)

def has_moves_left(grid):
"""檢查是否還有可能的移動"""
# 檢查是否有空白格子
if any(0 in row for row in grid):
return True

# 檢查是否有相鄰的相同數字
for i in range(4):
for j in range(4):
val = grid[i][j]
# 檢查右邊
if j < 3 and grid[i][j+1] == val:
return True
# 檢查下邊
if i < 3 and grid[i+1][j] == val:
return True

return False

def get_key():
"""獲取按鍵輸入"""
try:
import msvcrt # Windows
return msvcrt.getch().decode('utf-8',errors='ignore').lower()
except ImportError:
try:
import termios, tty, sys # Unix/Linux/MacOS
fd = sys.stdin.fileno()
old_settings = termios.tcgetattr(fd)
try:
tty.setraw(sys.stdin.fileno())
ch = sys.stdin.read(1).lower()
finally:
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
return ch
except:
# 如果以上方法都失敗,使用input函數
key = input("輸入移動方向 (W/A/S/D): ").lower()
return key[0] if key else 'w'

def main():
"""遊戲主函數"""
grid, score = initialize_game()

while True:
display_grid(grid, score)

# 檢查遊戲是否結束
if has_won(grid):
print("恭喜,你贏了!")
break

if not has_moves_left(grid):
print("遊戲結束!沒有更多可能的移動。")
break

# 獲取用戶輸入
key = get_key()

if key == 'q':
print("遊戲結束!")
break

direction = {
'w': 'up',
'a': 'left',
's': 'down',
'd': 'right'
}.get(key)

if direction:
moved, score = move(grid, direction, score)

if moved:
add_new_tile(grid)

if __name__ == "__main__":
try:
main()
except KeyboardInterrupt:
print("\n遊戲被中斷")


遊戲畫面

raw-image

點這行 線上執行 與 試玩(在新的頁面按Run開始玩)


小結 與 延伸思考 🎉

到這邊,我們就完成了一個可以在Command line玩的2048遊戲啦!

你可以試著自己動手寫看看,玩玩看,甚至改改程式碼,像是改變遊戲大小、加上顏色、或是改變數字規則,讓遊戲更有趣!✨

如果你有任何問題,歡迎在下方留言提問!寫出一個小遊戲其實很有成就感的!💪

祝福各位收穫滿滿,玩得開心!😊🎉

留言
avatar-img
留言分享你的想法!
avatar-img
小松鼠的演算法樂園
95會員
427內容數
由有業界實戰經驗的演算法工程師, 手把手教你建立解題的框架, 一步步寫出高效、清晰易懂的解題答案。 著重在讓讀者啟發思考、理解演算法,熟悉常見的演算法模板。 深入淺出地介紹題目背後所使用的演算法意義,融會貫通演算法與資料結構的應用。 在幾個經典的題目融入一道題目的多種解法,或者同一招解不同的題目,擴展廣度,並加深印象。
2025/05/04
今天我們來學一個有趣的小專案: 自己用 Python 寫一個「剪刀、石頭、布」猜拳遊戲! 學完這個,你就不只是學程式設計, 還能順便和電腦玩幾把猜拳,重拾兒時回憶喔!😄
Thumbnail
2025/05/04
今天我們來學一個有趣的小專案: 自己用 Python 寫一個「剪刀、石頭、布」猜拳遊戲! 學完這個,你就不只是學程式設計, 還能順便和電腦玩幾把猜拳,重拾兒時回憶喔!😄
Thumbnail
2024/10/10
從Python 內建deque資料結構的角度切入, 同時了解deque 與 FIFO Queue相關的function用法。 collections.deque是一種兩端點皆可進出的雙端佇列 在兩端點高效地在O(1)常數時間內添加和刪除元素。 這使得deque非常適合實現FIFO Queue
Thumbnail
2024/10/10
從Python 內建deque資料結構的角度切入, 同時了解deque 與 FIFO Queue相關的function用法。 collections.deque是一種兩端點皆可進出的雙端佇列 在兩端點高效地在O(1)常數時間內添加和刪除元素。 這使得deque非常適合實現FIFO Queue
Thumbnail
2024/09/27
井字遊戲(OOXX)的遊戲描述 Tic Tac Toe(井字遊戲)是經典的雙人棋盤遊戲,在一個3x3的方格中進行。 每回合兩個玩家輪流選一個位置,先讓自己的符號(是 X 或 O)在 水平線、垂直線或對角線上連成一線的玩家宣告獲勝。
Thumbnail
2024/09/27
井字遊戲(OOXX)的遊戲描述 Tic Tac Toe(井字遊戲)是經典的雙人棋盤遊戲,在一個3x3的方格中進行。 每回合兩個玩家輪流選一個位置,先讓自己的符號(是 X 或 O)在 水平線、垂直線或對角線上連成一線的玩家宣告獲勝。
Thumbnail
看更多
你可能也想看
Thumbnail
TOMICA第一波推出吉伊卡哇聯名小車車的時候馬上就被搶購一空,一直很扼腕當時沒有趕緊入手。前陣子閒來無事逛蝦皮,突然發現幾家商場都又開始重新上架,價格也都回到正常水準,估計是官方又再補了一批貨,想都沒想就立刻下單! 同文也跟大家分享近期蝦皮購物紀錄、好用推薦、蝦皮分潤計畫的聯盟行銷!
Thumbnail
TOMICA第一波推出吉伊卡哇聯名小車車的時候馬上就被搶購一空,一直很扼腕當時沒有趕緊入手。前陣子閒來無事逛蝦皮,突然發現幾家商場都又開始重新上架,價格也都回到正常水準,估計是官方又再補了一批貨,想都沒想就立刻下單! 同文也跟大家分享近期蝦皮購物紀錄、好用推薦、蝦皮分潤計畫的聯盟行銷!
Thumbnail
每年4月、5月都是最多稅要繳的月份,當然大部份的人都是有機會繳到「綜合所得稅」,只是相當相當多人還不知道,原來繳給政府的稅!可以透過一些有活動的銀行信用卡或電子支付來繳,從繳費中賺一點點小確幸!就是賺個1%~2%大家也是很開心的,因為你們把沒回饋變成有回饋,就是用卡的最高境界 所得稅線上申報
Thumbnail
每年4月、5月都是最多稅要繳的月份,當然大部份的人都是有機會繳到「綜合所得稅」,只是相當相當多人還不知道,原來繳給政府的稅!可以透過一些有活動的銀行信用卡或電子支付來繳,從繳費中賺一點點小確幸!就是賺個1%~2%大家也是很開心的,因為你們把沒回饋變成有回饋,就是用卡的最高境界 所得稅線上申報
Thumbnail
分享一個猜數字的遊戲題目,給予提示讓玩家找出正確的四位數密碼。
Thumbnail
分享一個猜數字的遊戲題目,給予提示讓玩家找出正確的四位數密碼。
Thumbnail
給定一個輸入陣列,每一個tuple代表節點之間了從屬關係。 請從從屬關係重建整顆二元樹,並且返回整顆二元樹的根結點。
Thumbnail
給定一個輸入陣列,每一個tuple代表節點之間了從屬關係。 請從從屬關係重建整顆二元樹,並且返回整顆二元樹的根結點。
Thumbnail
題目敘述 Combination Sum IV 給定一個輸入陣列nums,和目標值target,從nums裡面挑數字去湊出總和 = target,數字可以重複挑選。 請問有多少排列數可以湊出target? 註: 排列數的意思就是位置不同代表兩種不同的方法數。
Thumbnail
題目敘述 Combination Sum IV 給定一個輸入陣列nums,和目標值target,從nums裡面挑數字去湊出總和 = target,數字可以重複挑選。 請問有多少排列數可以湊出target? 註: 排列數的意思就是位置不同代表兩種不同的方法數。
Thumbnail
Leetcode 精選75題 題目與題解 熱門考點 目錄 (持續更新中) 建議從左側目錄 或者 按Ctrl+F輸入關鍵字進行搜尋
Thumbnail
Leetcode 精選75題 題目與題解 熱門考點 目錄 (持續更新中) 建議從左側目錄 或者 按Ctrl+F輸入關鍵字進行搜尋
Thumbnail
題目給定一個布林代數的二元樹,要求我們計算最後的結果。 葉子節點都是真假值 非葉子節點都是布林運算子
Thumbnail
題目給定一個布林代數的二元樹,要求我們計算最後的結果。 葉子節點都是真假值 非葉子節點都是布林運算子
Thumbnail
題目敘述 輸入給定一個二元的二維矩陣grid 每次可以翻轉一條row,讓每個元素的01反相。 也可以翻轉一條column,讓每個元素的01反相。 可以操作任意多次。 最後把每條row視為一條二進位表達式的數字,並且進行加總,得到最後的分數。 請問分數的最大值是多少? 原本的英文題目敘
Thumbnail
題目敘述 輸入給定一個二元的二維矩陣grid 每次可以翻轉一條row,讓每個元素的01反相。 也可以翻轉一條column,讓每個元素的01反相。 可以操作任意多次。 最後把每條row視為一條二進位表達式的數字,並且進行加總,得到最後的分數。 請問分數的最大值是多少? 原本的英文題目敘
Thumbnail
題目敘述 輸入給定一個鏈結串列的head node。 要求我們進行化簡,只要某個節點的右手邊存在比較大的節點,就刪除掉。 例如 5->2->13->3 5的右手邊有13,所以5刪除掉。 2的右手邊有13,所以2刪除掉。 13的右手邊沒有更大的節點,所以13留著。 3的右手邊沒有更大
Thumbnail
題目敘述 輸入給定一個鏈結串列的head node。 要求我們進行化簡,只要某個節點的右手邊存在比較大的節點,就刪除掉。 例如 5->2->13->3 5的右手邊有13,所以5刪除掉。 2的右手邊有13,所以2刪除掉。 13的右手邊沒有更大的節點,所以13留著。 3的右手邊沒有更大
Thumbnail
題目敘述 給定一個piles陣列,裡面對應到每堆石頭的數量。 Alice 和 Bob玩輪流取石頭的遊戲,總共有n堆石頭,每堆的石頭數量有多有少。 Alice先取,接著Bob,反覆交替,每回合輪到的人可以從當下的第一堆或者最後一堆,拿走那堆對應的石頭。 最後比誰拿到的石頭總數量比較多就獲勝。
Thumbnail
題目敘述 給定一個piles陣列,裡面對應到每堆石頭的數量。 Alice 和 Bob玩輪流取石頭的遊戲,總共有n堆石頭,每堆的石頭數量有多有少。 Alice先取,接著Bob,反覆交替,每回合輪到的人可以從當下的第一堆或者最後一堆,拿走那堆對應的石頭。 最後比誰拿到的石頭總數量比較多就獲勝。
Thumbnail
題目敘述 題目會給我們一個輸入陣列nums,每個元素值代表那個格子點可以跳躍的最大長度。 一開始從最左邊的格子點出發開始跳,請問可以成功抵達終點,也就是最右邊的格子點嗎? 如果可以,返回 True。 如果不行,返回False。 題目的原文敘述 測試範例 Example 1: In
Thumbnail
題目敘述 題目會給我們一個輸入陣列nums,每個元素值代表那個格子點可以跳躍的最大長度。 一開始從最左邊的格子點出發開始跳,請問可以成功抵達終點,也就是最右邊的格子點嗎? 如果可以,返回 True。 如果不行,返回False。 題目的原文敘述 測試範例 Example 1: In
追蹤感興趣的內容從 Google News 追蹤更多 vocus 的最新精選內容追蹤 Google News