成品展示
具備基本的數據合並以及分數統計,不同數字的色塊不同
產生隨機數, 數據無法合並判定以及重新開始選項
同時可以判定游戲失敗條件
需求分析
- 完成基本數據合並算法
- 游戲結束條件
- 界面展示
- 重置按鈕
- 分數統計
代碼邏輯
頁面創建
展示數據
創建一個基本的數據結構地圖數據來保存各位置的數值
_map_data = [
[0, 0, 0, 0],
[0, 0, 0, 0],
[0, 0, 0, 0],
[0, 0, 0, 0]
]
展示圖形
利用 tkinter 通過遍歷地圖數據來循環創建標簽
創建時利用 tkinter 設置樣式以及顏色
同樣維護一個列表來填入每行的標簽
map_labels = [] for r in range(4): row = [] for c in range(len(_map_data[0])): value = _map_data[r][c] text = str(value) if value else '' label = Label(frame, text=text, width=4, height=2, font=("黑體", 30, "bold")) label.grid(row=r, column=c, padx=5, pady=5, sticky=N + E + W + S) row.append(label) map_labels.append(row)
色塊設置
不同數值的色塊以不同的顏色標識
# 設置游戲中每個數據對應色塊的顏色 mapcolor = { 0: ("#cdc1b4", "#776e65"), 2: ("#eee4da", "#776e65"), 4: ("#ede0c8", "#f9f6f2"), 8: ("#f2b179", "#f9f6f2"), 16: ("#f59563", "#f9f6f2"), 32: ("#f67c5f", "#f9f6f2"), 64: ("#f65e3b", "#f9f6f2"), 128: ("#edcf72", "#f9f6f2"), 256: ("#edcc61", "#f9f6f2"), 512: ("#e4c02a", "#f9f6f2"), 1024: ("#e2ba13", "#f9f6f2"), 2048: ("#ecc400", "#f9f6f2"), 4096: ("#ae84a8", "#f9f6f2"), 8192: ("#b06ca8", "#f9f6f2"), # ----其它顏色都與8192相同--------- 2 ** 14: ("#b06ca8", "#f9f6f2"), 2 ** 15: ("#b06ca8", "#f9f6f2"), 2 ** 16: ("#b06ca8", "#f9f6f2"), 2 ** 17: ("#b06ca8", "#f9f6f2"), 2 ** 18: ("#b06ca8", "#f9f6f2"), 2 ** 19: ("#b06ca8", "#f9f6f2"), 2 ** 20: ("#b06ca8", "#f9f6f2"), }
分數顯示
創建兩個標簽分別標識分數 , 以及數字
label = Label(frame, text='分數', font=("黑體", 30, "bold"), bg="#bbada0", fg="#eee4da") label.grid(row=4, column=0, padx=5, pady=5) label_score = Label(frame, text='0', font=("黑體", 30, "bold"), bg="#bbada0", fg="#ffffff") label_score.grid(row=4, columnspan=2, column=1, padx=5, pady=5)
重置按鈕
重置按鈕需要做到將游戲重置
即地圖數據還原以及分數重置
此部分需要設計相關 函數來負責重置以及刷新界面
def reset(): '''重新設置游戲數據,將地圖恢復為初始狀態,並加入兩個數據 2 作用初始狀態''' _map_data[:] = [] # _map_data.clear() _map_data.append([0, 0, 0, 0]) _map_data.append([0, 0, 0, 0]) _map_data.append([0, 0, 0, 0]) _map_data.append([0, 0, 0, 0]) # 在空白地圖上填充兩個2 fill2() fill2()
def reset_game(): reset() update_ui() restart_button = Button(frame, text='重新開始', font=("黑體", 16, "bold"), bg="#8f7a66", fg="#f9f6f2", command=reset_game) restart_button.grid(row=4, column=3, padx=5, pady=5)
計算邏輯
移動邏輯
移動的邏輯分為兩步
移動數字和合並數字
但是合並數字后又會發現存在空格, 因此需要第三步
def _left_move_number(line): '''左移一行數字,如果有數據移動則返回True,否則返回False: 如: line = [0, 2, 0, 8] 即表達如下一行: +---+---+---+---+ | 0 | 2 | 0 | 8 | <----向左移動 +---+---+---+---+ 此行數據需要左移三次: 第一次左移結果: +---+---+---+---+ | 2 | 0 | 8 | 0 | +---+---+---+---+ 第二次左移結果: +---+---+---+---+ | 2 | 8 | 0 | 0 | +---+---+---+---+ 第三次左移結果: +---+---+---+---+ | 2 | 8 | 0 | 0 | # 因為最左則為2,所以8不動 +---+---+---+---+ 最終結果: line = [4, 8, 0, 0] ''' moveflag = False # 是否移動的標識,先假設沒有移動 for _ in range(3): # 重復執行下面算法三次 for i in range(3): # i為索引 if 0 == line[i]: # 此處有空位,右側相鄰數字向左側移動,右側填空白 moveflag = True line[i] = line[i + 1] line[i + 1] = 0 return moveflag def _left_marge_number(line): '''向左側進行相同單元格合並,合並結果放在左側,右側補零 如: line = [2, 2, 4, 4] 即表達如下一行: +---+---+---+---+ | 2 | 2 | 4 | 4 | +---+---+---+---+ 全並后的結果為: +---+---+---+---+ | 4 | 0 | 8 | 0 | +---+---+---+---+ 最終結果: line = [4, 8, 8, 0] ''' for i in range(3): if line[i] == line[i + 1]: moveflag = True line[i] *= 2 # 左側翻倍 line[i + 1] = 0 # 右側歸零 def _left_move_aline(line): '''左移一行數據,如果有數據移動則返回True,否則返回False: 如: line = [2, 0, 2, 8] 即表達如下一行: +---+---+---+---+ | 2 | | 2 | 8 | <----向左移動 +---+---+---+---+ 左移算法分為三步: 1. 將所有數字向左移動來填補左側空格,即: +---+---+---+---+ | 2 | 2 | 8 | | +---+---+---+---+ 2. 判斷是否發生碰幢,如果兩個相臨且相等的數值則說明有碰撞需要合並, 合並結果靠左,右則填充空格 +---+---+---+---+ | 4 | | 8 | | +---+---+---+---+ 3. 再重復第一步,將所有數字向左移動來填補左側空格,即: +---+---+---+---+ | 4 | 8 | | | +---+---+---+---+ 最終結果: line = [4, 8, 0, 0] ''' moveflag = False if _left_move_number(line): moveflag = True if _left_marge_number(line): moveflag = True if _left_move_number(line): moveflag = True return moveflag
上下左右移動實現
基本實現了一個就可以全部實現了
本質本身就是列表 , 翻轉方向的就翻轉列表即可. 但是記得要再轉回來
上下的列表就是4個列表的同索引位置重新拼接列表.
同樣翻轉后在翻轉
def left(): """游戲左鍵按下時或向左滑動屏幕時的算法""" moveflag = False # moveflag 是否成功移動數字標志位,如果有移動則為真值,原地圖不變則為假值 # 將第一行都向左移動.如果有移動就返回True for line in _map_data: if _left_move_aline(line): moveflag = True return moveflag def right(): """游戲右鍵按下時或向右滑動屏幕時的算法 選將屏幕進行左右對調,對調后,原來的向右滑動即為現在的向左滑動 滑動完畢后,再次左右對調回來 """ # 左右對調 for r in _map_data: r.reverse() moveflag = left() # 向左滑動 # 再次左右對調 for r in _map_data: r.reverse() return moveflag def up(): """游戲上鍵按下時或向上滑動屏幕時的算法 先把每一列都自上而下放入一個列表中line中,然后執行向滑動, 滑動完成后再將新位置擺回到原來的一列中 """ moveflag = False line = [0, 0, 0, 0] # 先初始化一行,准備放入數據 for col in range(4): # 先取出每一列 # 把一列中的每一行數入放入到line中 for row in range(4): line[row] = _map_data[row][col] # 將當前列進行上移,即line 左移 if (_left_move_aline(line)): moveflag = True # 把左移后的 line中的數據填充回原來的一列 for row in range(4): _map_data[row][col] = line[row] return moveflag def down(): """游戲下鍵按下時或向下滑動屏幕時的算法 選將屏幕進行上下對調,對調后,原來的向下滑動即為現在的向上滑動 滑動完畢后,再次上下對調回來 """ _map_data.reverse() moveflag = up() # 上滑 _map_data.reverse() return moveflag
結束判定
def is_gameover(): """判斷游戲是否結束,如果結束返回True,否是返回False """ for r in _map_data: # 如果水平方向還有0,則游戲沒有結束 if r.count(0): return False # 水平方向如果有兩個相鄰的元素相同,應當是可以合並的,則游戲沒有結束 for i in range(3): if r[i] == r[i + 1]: return False for c in range(4): # 豎直方向如果有兩個相鄰的元素相同,應當可以合並的,則游戲沒有結束 for r in range(3): if _map_data[r][c] == _map_data[r + 1][c]: return False # 以上都沒有,則游戲結束 return True
分數統計
def get_score(): '''獲取游戲的分數,得分規則是每次有兩個數加在一起則生成相應的分數。 如 2 和 2 合並后得4分, 8 和 8 分並后得 16分. 根據一個大於2的數字就可以知道他共合並了多少次,可以直接算出分數: 如: 4 一定由兩個2合並,得4分 8 一定由兩個4合並,則計:8 + 4 + 4 得32分 ... 以此類推 ''' score = 0 for r in _map_data: for c in r: score += 0 if c < 4 else c * int((math.log(c, 2) - 1.0)) return score # 導入數學模塊
隨機數添加
隨機數的添加形式為 添加一個 2 到任意一個為 0 的位置
先進行一個 0 位置的數量統計
def get_space_count(): """獲取沒有數字的方格的數量,如果數量為0則說有無法填充新數據,游戲即將結束 """ count = 0 for r in _map_data: count += r.count(0) return count
利用定義偏移量來添加, 隨機 0~0位置統計個數, 然后選一個后循環+1偏移量到被選到數字進行復制為 2
def fill2(): '''填充2到空位置,如果填度成功返回True,如果已滿,則返回False''' blank_count = get_space_count() # 得到地圖上空白位置的個數 if 0 == blank_count: return False # 生成隨機位置, 如,當只有四個空時,則生成0~3的數,代表自左至右,自上而下的空位置 pos = random.randrange(0, blank_count) offset = 0 for row in _map_data: # row為行row for col in range(4): # col 為列,column if 0 == row[col]: if offset == pos: # 把2填充到第row行,第col列的位置,返回True row[col] = 2 return True offset += 1
鍵盤映射響應
keymap = { 'a': left, 'd': right, 'w': up, 's': down, 'Left': left, 'Right': right, 'Up': up, 'Down': down, 'q': root.quit, }
def on_key_down(event): '鍵盤按下處理函數' keysym = event.keysym if keysym in keymap: if keymap[keysym](): # 如果有數字移動 fill2() # 填充一個新的2 update_ui() if is_gameover(): mb = messagebox.askyesno( title="gameover", message="游戲結束!\n是否退出游戲!") if mb: root.quit() else: reset() update_ui()
# 設置焦點能接收按鍵事件 frame.focus_set() frame.bind("<Key>", on_key_down)
刷新界面
def update_ui(): '''刷新界面函數 根據計算出的f地圖數據,更新各個Label的設置 ''' for r in range(4): for c in range(len(_map_data[0])): number = _map_data[r][c] # 設置數字 label = map_labels[r][c] # 選中Lable控件 label['text'] = str(number) if number else '' label['bg'] = mapcolor[number][0] label['foreground'] = mapcolor[number][1] label_score['text'] = str(get_score()) # 重設置分數
全部代碼
"""2048游戲 本模塊已完整實現2048游戲的算法及分數的計算算法 本游戲的界面采用python 標准庫 tkinter 來實現 此界面的布局采用tkinter中的grid布局 """ import random import math import sys _map_data = [ [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0] ] # -------------------------以下為2048游戲的基本算法--------------------------- # 重置 def reset(): '''重新設置游戲數據,將地圖恢復為初始狀態,並加入兩個數據 2 作用初始狀態''' _map_data[:] = [] # _map_data.clear() _map_data.append([0, 0, 0, 0]) _map_data.append([0, 0, 0, 0]) _map_data.append([0, 0, 0, 0]) _map_data.append([0, 0, 0, 0]) # 在空白地圖上填充兩個2 fill2() fill2() # 獲取 0 個數 def get_space_count(): """獲取沒有數字的方格的數量,如果數量為0則說有無法填充新數據,游戲即將結束 """ count = 0 for r in _map_data: count += r.count(0) return count # 計算分數 def get_score(): '''獲取游戲的分數,得分規則是每次有兩個數加在一起則生成相應的分數。 如 2 和 2 合並后得4分, 8 和 8 分並后得 16分. 根據一個大於2的數字就可以知道他共合並了多少次,可以直接算出分數: 如: 4 一定由兩個2合並,得4分 8 一定由兩個4合並,則計:8 + 4 + 4 得32分 ... 以此類推 ''' score = 0 for r in _map_data: for c in r: score += 0 if c < 4 else c * int((math.log(c, 2) - 1.0)) return score # 導入數學模塊 # 隨機數生成 def fill2(): '''填充2到空位置,如果填度成功返回True,如果已滿,則返回False''' blank_count = get_space_count() # 得到地圖上空白位置的個數 if 0 == blank_count: return False # 生成隨機位置, 如,當只有四個空時,則生成0~3的數,代表自左至右,自上而下的空位置 pos = random.randrange(0, blank_count) offset = 0 for row in _map_data: # row為行row for col in range(4): # col 為列,column if 0 == row[col]: if offset == pos: # 把2填充到第row行,第col列的位置,返回True row[col] = 2 return True offset += 1 # 結束判定 def is_gameover(): """判斷游戲是否結束,如果結束返回True,否是返回False """ for r in _map_data: # 如果水平方向還有0,則游戲沒有結束 if r.count(0): return False # 水平方向如果有兩個相鄰的元素相同,應當是可以合並的,則游戲沒有結束 for i in range(3): if r[i] == r[i + 1]: return False for c in range(4): # 豎直方向如果有兩個相鄰的元素相同,應當可以合並的,則游戲沒有結束 for r in range(3): if _map_data[r][c] == _map_data[r + 1][c]: return False # 以上都沒有,則游戲結束 return True # 移動合並分數 def _left_move_number(line): '''左移一行數字,如果有數據移動則返回True,否則返回False: 如: line = [0, 2, 0, 8] 即表達如下一行: +---+---+---+---+ | 0 | 2 | 0 | 8 | <----向左移動 +---+---+---+---+ 此行數據需要左移三次: 第一次左移結果: +---+---+---+---+ | 2 | 0 | 8 | 0 | +---+---+---+---+ 第二次左移結果: +---+---+---+---+ | 2 | 8 | 0 | 0 | +---+---+---+---+ 第三次左移結果: +---+---+---+---+ | 2 | 8 | 0 | 0 | # 因為最左則為2,所以8不動 +---+---+---+---+ 最終結果: line = [4, 8, 0, 0] ''' moveflag = False # 是否移動的標識,先假設沒有移動 for _ in range(3): # 重復執行下面算法三次 for i in range(3): # i為索引 if 0 == line[i]: # 此處有空位,右側相鄰數字向左側移動,右側填空白 moveflag = True line[i] = line[i + 1] line[i + 1] = 0 return moveflag # 移動位置 def _left_marge_number(line): '''向左側進行相同單元格合並,合並結果放在左側,右側補零 如: line = [2, 2, 4, 4] 即表達如下一行: +---+---+---+---+ | 2 | 2 | 4 | 4 | +---+---+---+---+ 全並后的結果為: +---+---+---+---+ | 4 | 0 | 8 | 0 | +---+---+---+---+ 最終結果: line = [4, 8, 8, 0] ''' for i in range(3): if line[i] == line[i + 1]: moveflag = True line[i] *= 2 # 左側翻倍 line[i + 1] = 0 # 右側歸零 # 移動邏輯 def _left_move_aline(line): '''左移一行數據,如果有數據移動則返回True,否則返回False: 如: line = [2, 0, 2, 8] 即表達如下一行: +---+---+---+---+ | 2 | | 2 | 8 | <----向左移動 +---+---+---+---+ 左移算法分為三步: 1. 將所有數字向左移動來填補左側空格,即: +---+---+---+---+ | 2 | 2 | 8 | | +---+---+---+---+ 2. 判斷是否發生碰幢,如果兩個相臨且相等的數值則說明有碰撞需要合並, 合並結果靠左,右則填充空格 +---+---+---+---+ | 4 | | 8 | | +---+---+---+---+ 3. 再重復第一步,將所有數字向左移動來填補左側空格,即: +---+---+---+---+ | 4 | 8 | | | +---+---+---+---+ 最終結果: line = [4, 8, 0, 0] ''' moveflag = False if _left_move_number(line): moveflag = True if _left_marge_number(line): moveflag = True if _left_move_number(line): moveflag = True return moveflag def left(): """游戲左鍵按下時或向左滑動屏幕時的算法""" moveflag = False # moveflag 是否成功移動數字標志位,如果有移動則為真值,原地圖不變則為假值 # 將第一行都向左移動.如果有移動就返回True for line in _map_data: if _left_move_aline(line): moveflag = True return moveflag def right(): """游戲右鍵按下時或向右滑動屏幕時的算法 選將屏幕進行左右對調,對調后,原來的向右滑動即為現在的向左滑動 滑動完畢后,再次左右對調回來 """ # 左右對調 for r in _map_data: r.reverse() moveflag = left() # 向左滑動 # 再次左右對調 for r in _map_data: r.reverse() return moveflag def up(): """游戲上鍵按下時或向上滑動屏幕時的算法 先把每一列都自上而下放入一個列表中line中,然后執行向滑動, 滑動完成后再將新位置擺回到原來的一列中 """ moveflag = False line = [0, 0, 0, 0] # 先初始化一行,准備放入數據 for col in range(4): # 先取出每一列 # 把一列中的每一行數入放入到line中 for row in range(4): line[row] = _map_data[row][col] # 將當前列進行上移,即line 左移 if (_left_move_aline(line)): moveflag = True # 把左移后的 line中的數據填充回原來的一列 for row in range(4): _map_data[row][col] = line[row] return moveflag def down(): """游戲下鍵按下時或向下滑動屏幕時的算法 選將屏幕進行上下對調,對調后,原來的向下滑動即為現在的向上滑動 滑動完畢后,再次上下對調回來 """ _map_data.reverse() moveflag = up() # 上滑 _map_data.reverse() return moveflag # -------------------------以下為2048游戲的操作界面--------------------------- if (sys.version_info > (3, 0)): from tkinter import * from tkinter import messagebox else: from Tkinter import * def main(): reset() # 先重新設置游戲數據 root = Tk() # 創建tkinter窗口 root.title('2048游戲') # 設置標題文字 root.resizable(width=False, height=False) # 固定寬和高 # 以下是鍵盤映射 keymap = { 'a': left, 'd': right, 'w': up, 's': down, 'Left': left, 'Right': right, 'Up': up, 'Down': down, 'q': root.quit, } game_bg_color = "#bbada0" # 設置背景顏色 # 設置游戲中每個數據對應色塊的顏色 mapcolor = { 0: ("#cdc1b4", "#776e65"), 2: ("#eee4da", "#776e65"), 4: ("#ede0c8", "#f9f6f2"), 8: ("#f2b179", "#f9f6f2"), 16: ("#f59563", "#f9f6f2"), 32: ("#f67c5f", "#f9f6f2"), 64: ("#f65e3b", "#f9f6f2"), 128: ("#edcf72", "#f9f6f2"), 256: ("#edcc61", "#f9f6f2"), 512: ("#e4c02a", "#f9f6f2"), 1024: ("#e2ba13", "#f9f6f2"), 2048: ("#ecc400", "#f9f6f2"), 4096: ("#ae84a8", "#f9f6f2"), 8192: ("#b06ca8", "#f9f6f2"), # ----其它顏色都與8192相同--------- 2 ** 14: ("#b06ca8", "#f9f6f2"), 2 ** 15: ("#b06ca8", "#f9f6f2"), 2 ** 16: ("#b06ca8", "#f9f6f2"), 2 ** 17: ("#b06ca8", "#f9f6f2"), 2 ** 18: ("#b06ca8", "#f9f6f2"), 2 ** 19: ("#b06ca8", "#f9f6f2"), 2 ** 20: ("#b06ca8", "#f9f6f2"), } def on_key_down(event): '鍵盤按下處理函數' keysym = event.keysym if keysym in keymap: if keymap[keysym](): # 如果有數字移動 fill2() # 填充一個新的2 update_ui() if is_gameover(): mb = messagebox.askyesno( title="gameover", message="游戲結束!\n是否退出游戲!") if mb: root.quit() else: reset() update_ui() def update_ui(): '''刷新界面函數 根據計算出的f地圖數據,更新各個Label的設置 ''' for r in range(4): for c in range(len(_map_data[0])): number = _map_data[r][c] # 設置數字 label = map_labels[r][c] # 選中Lable控件 label['text'] = str(number) if number else '' label['bg'] = mapcolor[number][0] label['foreground'] = mapcolor[number][1] label_score['text'] = str(get_score()) # 重設置分數 # 創建一個frame窗口,此創建將容納全部的widget 部件 frame = Frame(root, bg=game_bg_color) frame.grid(sticky=N + E + W + S) # 設置焦點能接收按鍵事件 frame.focus_set() frame.bind("<Key>", on_key_down) # 初始化圖形界面 map_labels = [] for r in range(4): row = [] for c in range(len(_map_data[0])): value = _map_data[r][c] text = str(value) if value else '' label = Label(frame, text=text, width=4, height=2, font=("黑體", 30, "bold")) label.grid(row=r, column=c, padx=5, pady=5, sticky=N + E + W + S) row.append(label) map_labels.append(row) # 設置顯示分數的Lable label = Label(frame, text='分數', font=("黑體", 30, "bold"), bg="#bbada0", fg="#eee4da") label.grid(row=4, column=0, padx=5, pady=5) label_score = Label(frame, text='0', font=("黑體", 30, "bold"), bg="#bbada0", fg="#ffffff") label_score.grid(row=4, columnspan=2, column=1, padx=5, pady=5) # 以下設置重新開始按鈕 def reset_game(): reset() update_ui() restart_button = Button(frame, text='重新開始', font=("黑體", 16, "bold"), bg="#8f7a66", fg="#f9f6f2", command=reset_game) restart_button.grid(row=4, column=3, padx=5, pady=5) update_ui() # 更新界面 root.mainloop() # 進入tkinter主事件循環 main() # 啟動游戲