本篇博文圍繞使用Python開發熱門游戲2048 GAME(命令行版本) 代碼未做任何優化(原生且隨意)、全程以面向過程、MVC的設計思想為主、開發環境是Ubuntu系統下的Pycharm 2048是我很久以前學習Python過程中的一個作業,接下來直入正題—— 一、瞭解游戲 1. 介紹 《2048 ...
本篇博文圍繞使用Python開發熱門游戲2048 GAME(命令行版本)
代碼未做任何優化(原生且隨意)、全程以面向過程、MVC的設計思想為主、開發環境是Ubuntu系統下的Pycharm
2048是我很久以前學習Python過程中的一個作業,接下來直入正題——
一、瞭解游戲
1. 介紹
《2048》是一款單人線上和移動端游戲,由19歲的義大利人Gabriele Cirulli於2014年3月開發。游戲任務是在一個網格上滑動小方塊來進行組合,直到形成一個帶有有數字2048的方塊(來源:維基百科)
2. 玩法規則
- 通過方向鍵讓方塊整體上下左右移動
- 如果兩個帶有相同數字的方塊在移動中碰撞,則它們會相加合併為一個新方塊
- 每次出現方塊移動時,都會有一個值為2或者4的新方塊出現
- 初始開局時,4*4的方塊,隨機2個方塊賦值2或者4
- 其中所出現的數字都是2的冪,2,4,8,16......
二、MVC設計
Model:無
View:終端界面(有時間再研究一下pyQt),列印二維列表,輸入輸出控制
Controller:二維列表-矩陣、數據控制、上下左右操作、計分機制、方塊合併處理等等
三、核心函數
通過觀察游戲界面,可知數據由二維數組(線性代數--方陣)存儲,將上圖映射到如下代碼:
source = [
[0, 0, 0, 0],
[0, 0, 2, 2],
[0, 0, 0, 0],
[0, 0, 0, 0]
]
通過玩法規則第1條可知,方向鍵上下左右移動,四種移動必然存在相似的操作,祁天暄講師通過分析向左移動來書寫後續代碼,我這裡也會以向左移動來分析如何寫後續的代碼。
[0, 0, 2, 2]
取上述一行,按左方向鍵移動後,可以看到兩個方塊2持續左移(如果左邊還有非0的方塊,那麼就會頂住該非0的方塊),然後相撞變成方塊4,因為有方塊移動,所以隨機挑選一個方塊0進行填充成2(不限於當前行,也可能發生在其他行):
本行規律:
[0, 0, 2, 2] >發生滑動> [2, 2, 0, 0] >相等相撞求和> [4, 0, 0, 0] >隨機填充> [4, 0, 0, 2]
經過多局游戲結合上方的規律,可以得知以下規律:
[0, 0, 2, 2] >發生滑動> [2, 2, 0, 0] >相等相撞求和> [4, 0, 0, 0] >隨機填充> [4, 0, 0, 2]
[4, 0, 2, 2] >發生滑動> [4, 2, 2, 0] >相等相撞求和> [4, 4, 0, 0] >隨機填充> [4, 4, 0, 4]
[4, 4, 2, 2] >發生滑動,不動> [4, 4, 2, 2] >相等相撞求和> [8, 4, 0, 0] >隨機填充> [4, 0, 0, 2]
1. 滑動處理
發生滑動的環節,可以得出一個規律,有0在非0元素的前方則必滑動,否則不動
[0, 0, 2, 2] >發生滑動> [2, 2, 0, 0]
[4, 0, 2, 2] >發生滑動> [4, 2, 2, 0]
[4, 4, 2, 2] >發生滑動,不動> [4, 4, 2, 2]
所以此處構造一個zero_to_end函數,功能就是將0移至末尾處,並保持非0元素應有的順序,先看一下中規中矩的方式(冒泡式的移動,時間複雜度較高):
def zero_to_end(list_data):
for i in range(3, -1, -1):
for j in range(i):
if list_data[j] == 0:
list_data[j], list_data[j + 1] = list_data[j + 1], list_data[j]
再看一下另一種寫法(採用該函數,時間複雜度為O(n)):
def zero_to_end(list_data):
"""
重排序函數(核心演算法)
非0元素移至最前(保持順序),0元素移至最後,充當中間人處理列表的角色
:param list_data: list 一維列表
:return: None
"""
for i in range(3, -1, -1):
if not list_data[i]:
del list_data[i]
list_data.append(0)
2. 相等相撞求和
經上一函數,每一行列表都被處理成:若幹非0元素有序在前,若幹0元素在後。
[2, 2, 0, 0] >相等相撞求和> [4, 0, 0, 0]
[4, 2, 2, 0] >相等相撞求和> [4, 4, 0, 0]
[4, 4, 2, 2] >相等相撞求和> [8, 4, 0, 0]
相等相撞求和這個過程肯定要統一函數處理,增加復用性,因此需要詳細拆分該流程的細節:
[4, 2, 2, 0]
如果第1個元素等==第2個元素:
則第1個元素 + 第2個元素,並賦給第1個元素的位置
刪除第2個元素
末尾追加一個0
[4, 2, 2, 0]
如果第2個元素==第3個元素(符合條件)
則第2個元素 + 第3個元素,並賦給第2個元素的位置[4, 4, 2, 0]
刪除第3個元素[4, 2, 0]
末尾追加一個0[4, 2, 0, 0]
[4, 2, 0, 0]
如果第3個元素==第4個元素
則第3個元素 + 第4個元素,並賦給第3個元素的位置
刪除第4個元素
末尾追加一個0
也就是說,相鄰且相等的兩個元素相加,應賦值給前方位置的元素,然後刪除後方位置的元素,刪除了一個,肯定還要湊回去的,根據游戲規則,補0即可
其實上方的邏輯還可以進行優化,當檢測到當前位置的元素為0時,直接打斷迴圈即可(因為已經被zero_to_end函數處理過了),封裝成merge_single函數如下:
def merge_single(list_data):
"""
合併元素函數(核心演算法)
重排序後,左邊兩個相鄰相同的非0元素相加,後方補0,並加分(可diy)
如果兩個相鄰的元素不同或者為0,則不做其他操作
:param list_data: list 一維列表
:return: None
"""
zero_to_end(list_data) # 處理一維列表
for i in range(3):
if list_data[i] == 0: break # 檢測到當前位置為0,後方就不管了,直接打斷
if list_data[i] == list_data[i + 1]:
list_data[i] *= 2 # 等價於 += list_data[i + 1]
del list_data[i + 1] # 刪除 [i + 1]位置的元素
list_data.append(0) # 補0
有點不太放心,放一條數據進行測試:
[4, 4, 2, 2] >i = 0,相加> [8, 4, 2, 2] >刪除i + 1位置> [8, 2, 2] >補0> [8, 2, 2, 0]
>迴圈結束第1次,i = 1,又相加> [8, 4, 2, 0] >又刪除i + 1位置> [8, 4, 0] >又補0> [8, 4, 0, 0]
> 迴圈結束第2次,i = 2,發現是0,直接跳出迴圈
3. 隨機填充
玩法規則第3條,當游戲中,發生方塊滑動時,在0元素區域隨機抽取一個位置,隨機賦值2或者4。
因此,先定義全局變數,一個存2和4的元組,通過random模塊實現隨機索引獲取2或者4。
random_tuple = (2, 4) # 初始添加的值、移動時添加的值
通過while迴圈不斷尋找隨機方格,直到發現該方格存儲0,那麼該方格將被賦予新值。
def random_site():
"""
隨機填充0元素函數(非核心)
隨機挑選0元素的位置,進行隨機填充random_list中的任意一個元素
可通過增刪改變random_list中的元素,從而影響到隨機填充的數字
:return: None
"""
random_list_len = len(random_tuple)
while True:
x = random.randint(0, 3)
y = random.randint(0, 3)
if after_source[x][y] == 0:
after_source[x][y] = random_tuple[random.randint(0, random_list_len - 1)]
break
四、附加功能函數
通過以上三步,成功的完成了2048的核心功能,接下來逐一部署2048的初始化、游戲操作、用戶操作、列印等等函數。
1. 初始數據和矩陣比較
構造游戲初始數據,以全局變數表示:
score = 0 # 初始分數,後續累加即可
source = [
[0, 0, 0, 0],
[0, 0, 0, 0],
[0, 0, 0, 0],
[0, 0, 0, 0]
]
單純的一個source表示數據可能還不夠,我構造了兩個4*4的矩陣,分別命名為before_source和after_source:
before_source = [
[0, 0, 0, 0],
[0, 0, 0, 0],
[0, 0, 0, 0],
[0, 0, 0, 0]
] # 操作前的矩陣
after_source = [
[0, 0, 0, 0],
[0, 0, 0, 0],
[0, 0, 0, 0],
[0, 0, 0, 0]
] # 操作後的矩陣(當前列印的矩陣)
這兩個矩陣,用於用戶操作前的一個比較(後臺比較),假設用戶進行左向移動,那麼移動前的數據傳給before_source,移動後(即每一行調用merge_single函數後)的數據傳給after_source,after_source才是用戶需要看的,兩者在後臺進行比較後,倘若不同,則說明發生了“方塊移動”,那麼就要調用隨機填充的函數,如果相同,說明沒有方塊滑動,則不可隨機填充。假如對以下的數據發起左移操作後,是不存在元素移動的,即不會調用隨機填充:
[
[4, 0, 0, 0],
[0, 0, 0, 0],
[2, 4, 2, 0],
[8, 2, 0, 0]
]
根據上述分析,構造比較函數compare_matrix:
def compare_matrix():
"""
二維數組比較
操作前後的二維數組(矩陣)進行比較
如果不相等,說明有元素可移動,當移動時調用random_site()函數
"""
if not (before_source == after_source):
random_site()
2. 矩陣數據列印
每次執行完移動操作後(無論是上下還是左右),肯定都要反饋給用戶數據界面,因此需要構造列印矩陣的函數:
def print_list():
"""列印游戲過程中必看的矩陣信息"""
for single_list in after_source:
print(single_list)
3. main入口(框架搭建)
調用程式總該需要一個入口,構造main函數。
迴圈開始階段,通過global關鍵字操作全局變數before_source,此處需要註意:應使用深拷貝(需要導入copy模塊),將當前的數據拷貝給before_source,如果採取淺拷貝,操作after_source後,before_source也會跟隨變化,這樣就導致before_source恆等於after_source。
上下左右以input輸入(w 、s、a、d)來進行移動,n表示主動認輸,q表示退出游戲。
每次執行完移動操作後,都需要進行反饋數據界面,所以迴圈末尾需要調用print_list函數和列印當前分數:
def main():
"""程式入口:初始化 + 輸入 + 輸出"""
while True:
global before_source
before_source = copy.deepcopy(after_source)
key = input("鍵入:")
if key == "a": pass
if key == "d": pass
if key == "w": pass
if key == "s": pass
if key == "n": pass
if key == "q": break
print_list()
main() # 調用main函數,即正常游戲的入口
當準備輸入時,手動中止程式,會發現很煩人的紅色報錯
因此加上try和except簡單的處理一下:
def main():
"""程式入口:初始化 + 輸入 + 輸出"""
while True:
try:
key = input("鍵入:")
global before_source
before_source = copy.deepcopy(after_source)
if key == "a": pass
if key == "d": pass
if key == "w": pass
if key == "s": pass
if key == "n": pass
if key == "q": break
print(f"當前分數:{score}")
print_list()
except KeyboardInterrupt:
break
main() # 調用main函數,即正常游戲的入口
4. 實現認輸功能
構建forfeit函數,列印最終分數後,人為拋出KeyboardInterrupt異常,直接調到except執行break打斷迴圈(為了不在列印最終得分後,執行後續兩條語句):
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-JqFS4KKo-1674483797912)(/home/ronan/.config/Typora/typora-user-images/image-20230123214632123.png)]
def forfeit():
"""認輸"""
print(f"玩家已認輸,最終得分:{score}")
raise KeyboardInterrupt
5. 加分機制
每發生方塊碰撞合併後,相加數字即為當局加分,比如初始為0,兩個相鄰的方塊2碰撞合併後變成方塊4,當前分數+4,即分數為4。
在merge_single函數中(參考2.2的函數),list_data.append(0)語句後添加下述代碼:
global score
score += list_data[i]
即:
def merge_single(list_data):
"""
合併元素函數(核心演算法)
重排序後,左邊兩個相鄰相同的非0元素相加,後方補0,並加分(可diy)
如果兩個相鄰的元素不同或者為0,則不做其他操作
:param list_data: list 一維列表
:return: None
"""
zero_to_end(list_data)
for i in range(3):
if list_data[i] == 0: break
if list_data[i] == list_data[i + 1]:
list_data[i] *= 2
del list_data[i + 1]
list_data.append(0)
global score
score += list_data[i]
6. 游戲初始化
為了讓游戲設置得靈活一點,增加全局變數init_count,表示初始方塊需賦值2或者4的個數,通常為2。
init_count = 2 # 初始值的個數
def init():
"""
游戲初始化
:return: None
"""
print(f"""當前分數:{score}\n操作方式:q退出 n認輸
w(上)
a(左) s(下) d(右)""")
for i in range(init_count): # 隨機生成init_count個初始值
random_site()
print_list()
7. 各方向移動操作
首先要明確,核心函數中第2節的相等相撞合併函數,僅僅針對一維列表,而整個游戲以二維列表為主,因此需要復用該代碼:
def merge():
"""合併操作,詳見merge_single()函數"""
for i in range(4):
merge_single(after_source[i])
接下來逐一分析,左右上下操作如何實現...
(1)左(基礎操作)
調用merge函數,完成滑動合併,然後比較前後矩陣相等來決定是否調用隨機填充(即調用compare_matrix函數)
def left():
"""向左操作"""
merge()
(2)右
最暴力無腦的辦法就是複製上述函數代碼,然後更改,但是我一直寫這些簡單清晰的函數,就是為了復用,所以這裡應該想辦法復用merge等代碼,我以向左操作為基礎,僅看merge函數,假設每一行都逆轉,再進行向左的核心操作,再逆轉回去,不就可以了嗎,比如:
[0, 2, 2, 4]向右移動操作後變成[0, 0, 4, 4]
[0, 2, 2, 4] >逆轉> [4, 2, 2, 0] >merge> [4, 4, 0, 0] >逆轉> [0, 0, 4, 4]
因此代碼如下:
def reverse():
"""逆轉2048二維列表中的每一行一維列表"""
for i in range(4):
after_source[i].reverse()
def right():
"""向右操作"""
reverse()
merge()
reverse()
(3)上
同樣為了復用,以向左操作為基礎,僅看merge函數,假設進行矩陣轉置,然後調用merge操作,再轉置,比如:
[0, 2, 0, 0]
[4, 2, 0, 0]
[0, 0, 4, 0]
[4, 0, 0, 0]
轉置後
[0, 4, 0, 4]
[2, 2, 0, 0]
[0, 0, 4, 0]
[0, 0, 0, 0]
調用merge後
[8, 0, 0, 0]
[4, 0, 0, 0]
[4, 0, 0, 0]
[0, 0, 0, 0]
再轉置回來,得到結果
[8, 4, 4, 0]
[0, 0, 0, 0]
[0, 0, 0, 0]
[0, 0, 0, 0]
因此代碼如下(3種轉置均可):
def transposition():
"""二維列表轉置(矩陣轉置)"""
for x in range(4):
for y in range(x, 4):
after_source[x][y], after_source[y][x] = after_source[y][x], after_source[x][y]
def transposition():
"""二維列表轉置(矩陣轉置)"""
new_map = [list(item) for item in zip(*after_source)]
after_source.clear()
after_source.extend(new_map)
def transposition():
"""二維列表轉置(矩陣轉置)"""
new_map = [list(item) for item in zip(*after_source)]
after_source[:] = new_map
def up():
"""向上操作"""
transposition()
merge()
transposition()
(3)下
根據上移和右移操作所得的靈感,同樣是以左移為基礎操作。假設進行矩陣轉置,逆轉後,調用merge操作,再逆轉,再轉置,比如:
[0, 2, 0, 0]
[4, 2, 0, 0]
[0, 0, 4, 0]
[4, 0, 0, 0]
轉置後
[0, 4, 0, 4]
[2, 2, 0, 0]
[0, 0, 4, 0]
[0, 0, 0, 0]
逆轉每一行後
[4, 0, 4, 0]
[0, 0, 2, 2]
[0, 4, 0, 0]
[0, 0, 0, 0]
調用merge後
[8, 0, 0, 0]
[4, 0, 0, 0]
[4, 0, 0, 0]
[0, 0, 0, 0]
再逆轉回來
[0, 0, 0, 8]
[0, 0, 0, 4]
[0, 0, 0, 4]
[0, 0, 0, 0]
再轉置回來,得到結果
[0, 0, 0, 0]
[0, 0, 0, 0]
[0, 0, 0, 0]
[8, 4, 4, 0]
因此代碼如下:
def down():
"""向下操作"""
transposition()
reverse()
merge()
reverse()
transposition()
五、最終代碼
"""
2048 GAME
"""
import random
import copy
score = 0 # 分數
init_count = 2 # 初始值的個數
before_source = [
[0, 0, 0, 0],
[0, 0, 0, 0],
[0, 0, 0, 0],
[0, 0, 0, 0]
] # 操作前的矩陣
after_source = [
[0, 0, 0, 0],
[0, 0, 0, 0],
[0, 0, 0, 0],
[0, 0, 0, 0]
] # 操作後的矩陣(當前列印的矩陣)
random_tuple = (2, 4) # 初始添加的值、移動時添加的值
# Controller層
def zero_to_end(list_data):
"""
重排序函數(核心演算法)
非0元素移至最前(保持順序),0元素移至最後,充當中間人處理列表的角色
:param list_data: list 一維列表
:return: None
"""
for i in range(3, -1, -1):
if not list_data[i]:
del list_data[i]
list_data.append(0)
def merge_single(list_data):
"""
合併元素函數(核心演算法)
重排序後,左邊兩個相鄰相同的非0元素相加,後方補0,並加分(可diy)
如果兩個相鄰的元素不同或者為0,則不做其他操作
:param list_data: list 一維列表
:return: None
"""
zero_to_end(list_data)
for i in range(3):
if list_data[i] == 0: break
if list_data[i] == list_data[i + 1]:
list_data[i] *= 2
del list_data[i + 1]
list_data.append(0)
global score
score += list_data[i]
def random_site():
"""
隨機填充0元素函數(非核心)
隨機挑選0元素的位置,進行隨機填充random_list中的任意一個元素
可通過增刪改變random_list中的元素,從而影響到隨機填充的數字
:return: None
"""
random_list_len = len(random_tuple)
while True:
x = random.randint(0, 3)
y = random.randint(0, 3)
if after_source[x][y] == 0:
after_source[x][y] = random_tuple[random.randint(0, random_list_len - 1)]
break
def merge():
"""合併操作,詳見merge_single()函數"""
for i in range(4):
merge_single(after_source[i])
def reverse():
"""逆轉2048二維列表中的每一行一維列表"""
for i in range(4):
after_source[i].reverse()
def transposition():
"""二維列表轉置(矩陣轉置)"""
for x in range(4):
for y in range(x, 4):
after_source[x][y], after_source[y][x] = after_source[y][x], after_source[x][y]
def compare_matrix():
"""
二維數組比較
操作前後的二維數組(矩陣)進行比較
如果不相等,說明有元素可移動,當移動時調用random_site()函數
"""
if not (before_source == after_source):
random_site()
def left():
"""向左操作"""
merge()
def right():
"""向右操作"""
reverse()
merge()
reverse()
def up():
"""向上操作"""
transposition()
merge()
transposition()
def down():
"""向下操作"""
transposition()
reverse()
merge()
reverse()
transposition()
# View層
def init():
"""
游戲初始化
:return: None
"""
print(f"""當前分數:{score}\n操作方式:q退出 n認輸
w(上)
a(左) s(下) d(右)""")
for i in range(init_count): # 隨機生成init_count個初始值
random_site()
print_list()
def print_list():
"""列印游戲過程中必看的矩陣信息"""
for single_list in after_source:
print(single_list)
def forfeit():
"""認輸"""
print(f"玩家已認輸,最終得分:{score}")
raise KeyboardInterrupt
def main():
"""程式入口:初始化 + 輸入 + 輸出"""
init()
while True:
try:
global before_source
before_source = copy.deepcopy(after_source)
key = input("鍵入:")
if key == "a": left()
if key == "d": right()
if key == "w": up()
if key == "s": down()
if key == "n": forfeit()
if key == "q": break
compare_matrix()
print(f"當前分數:{score}")
print_list()
except KeyboardInterrupt:
break
main()