用Python破解鬥地主殘局
作者:吳智煒(Tim)
連結:https://wuzhiwei.net/doudizhu_solver/
相信大家都玩過鬥地主,規則就不再介紹了。直接上一張朋友圈看到的殘局圖: 鬥地主殘局
這道題我剛看到時,曾嘗試用手工來破解,每次都以為找到了農民的必勝策略時,最後都發現其實農民跑不掉。由於手工破解無法窮盡所有可能性,所以這道題究竟農民有沒有妙手跑掉呢,只能通過程式碼來幫助我們運算了。
本文將簡要講述怎麼通過程式碼來求解此類問題,在最後會公佈殘局的最後結果,並開原始碼以供大家吐槽。
minimax
程式碼的核心思想是minimax。minimax可以拆解為兩部分,mini和max,分別是最小和最大的意思。
直觀的理解是什麼呢?就有點像A、B兩個人下棋。A現在可以在N個點走棋,假設A在某個點走棋了,使得A的這一步的盤面評估分數最高;但是輪到B下的時候,就一定會朝著讓A最不利的方向走,使得A的下一步必然按照B設定的軌跡來,而沒法達到A在第一步時估算到這一步的最高盤面評分。
在牌局中是一樣的,如果農民的一手牌,讓地主無論如何應對都不能贏的話,那麼可以說農民有必勝策略;否則,農民必輸。
核心邏輯
我們可以用一個函式hand_out來模擬一個人的出牌過程。在現實生活中,一個人想要出牌的話,必然需要知道自己手上的所有牌:me_pokers,也需要知道上一手的出的牌:last_hand。如果我們要用這個函式來模擬兩個人的出牌,則還需要知道對手當前的所有牌:enemy_pokers。
這個函式的返回值,是輪到我me_pokers出牌時,是否能夠必贏牌。如果能贏則返回真,否則返回假。
def hand_out(me_pokers, enemy_pokers, last_hand)
假設輪到我出牌時,如果我手上的牌都出完了,那麼我將立刻知道我贏了;反之如果對手的牌都出完了,而我沒有,則我失敗了。
if not me_pokers:
return True
if not enemy_pokers:
return False
因為現在輪到我出牌,所以我首先需要知道我現在能出的所有手牌組合。注意:這個組合中,包括過牌(即不出牌)的策略。
all_hands = get_all_hands(me_pokers)
現在我們要對所有可能的手牌組合進行遍歷。首先我需要知道,上一手對方出的牌是什麼。
如果對方上一手選擇過牌,或者沒有上一手牌,那麼我這一輪必須不能過牌,但是我可以出任意的牌
如果對手上一手出了牌,則我必須要出一個比它更大的牌或者選擇這一輪直接過牌(不出牌)
關鍵點來了,在出完我的牌或選擇過牌後,我們需要用一個遞迴呼叫來模擬對手下一步的行為。如果對手的下一次出牌不能獲勝的話,則我這一次的出牌必勝;否則,對於我的每一個出牌選擇,對手都能獲勝的話,則我必敗。
全部程式碼如下:(程式碼可左右滑動)
def hand_out(me_pokers, enemy_pokers, last_hand, cache):
if not me_pokers:
# 我全部過牌,直接獲勝
return True
if not enemy_pokers:
# 對手全部過牌,我失敗
return False
# 獲取我當前可以出的所有手牌組合,包括過牌
all_hands = get_all_hands(me_pokers)
# 遍歷我的所有出牌組合,進行模擬出牌
for hand in all_hands:
# 如果上一輪對手出了牌,則這一輪我必須要出比對手更大的牌 或者 對手上一輪選擇過牌,那麼我只需出任意牌,但是不能過牌
if (last_hand and can_comb2_beat_comb1(last_hand, hand)) or (not last_hand and hand['type'] != COMB_TYPE.PASS):
# 模擬對手出牌,如果對手不能取勝,則我必勝
if not hand_out(enemy_pokers, make_hand(me_pokers, hand), hand, cache):
return True
# 如果上一輪對手出了牌,但我這一輪選擇過牌
elif last_hand and hand['type'] == COMB_TYPE.PASS:
# 模擬對手出牌,如果對手不能取勝,則我必勝
if not hand_out(enemy_pokers, me_pokers, None, cache):
return True
# 如果之前的所有出牌組合均不能必勝,則我必敗
return False
構建
以上核心邏輯理清楚後,構建破解器將變得十分簡單。
首先,我們要用數字來表示牌的大小,這裡我們用3表示3,11來表示J,12表示Q,依次類推……
其次,我們需要求出一個手牌的所有出牌組合,這裡需要get_all_hands函式,具體實現比較繁瑣但是很簡單,就不在此贅述。
然後,我們還需要一個牌力判斷函式can_comb2_beat_comb1(comb1, comb2),這個函式用於比較兩組手牌的牌力,看是否comb2可以擊敗comb1。唯一需要注意的一點,在鬥地主的規則中,除了炸彈外,其他所有牌力均等,只有牌型一樣時才能去比較。
最後,我們需要一個模擬出牌函式make_hand(pokers, hand),用於求出在手牌為pokers的情況下打出一手牌hand後,剩下的手牌,實現也非常簡單,只需簡單的移除掉那些打出的牌即可。
效率
由於一副牌的可能手牌巨大,導致遞迴的分支數巨大。所以時間開銷非常大,為階乘級O(N!),根據斯特林公式,大約為O(N^N)。
由於可能會有很多重複的牌面出現,導致了很多重複的遞迴呼叫。所以加一個快取能極大提升效率。
即對我方手牌和敵方手牌和上一輪手牌的描述(str(me_pokers)+str(enemy_pokers)+str(last_hand))為鍵,將求出的結果存進快取字典中。下一次遇到相同的局面時,即可直接從快取字典中取出,而無需再次重複計算。時間複雜度優化為指數級O(C^N)。
結果
程式碼運算出來的結果是,農民沒有必勝策略。換言之,只要地主會玩,農民不可能贏。階級固化已經如斯了麼……
開源
程式碼放於Github,MIT協議,隨便玩。
https://github.com/iWoz/doudizhu_solver
相關文章
- Python鬥地主Python
- Python 三人鬥地主程式碼Python
- 模擬鬥地主
- 鬥地主老是輸?一起用Python做個AI出牌器!PythonAI
- 自己實現鬥地主引擎
- Java鬥地主專案碎片Java
- 8、用java的ArrayList集合完成模擬鬥地主案例Java
- 用鬥地主的例項學會使用java Collections工具類Java
- Java寫的鬥地主遊戲原始碼Java遊戲原始碼
- Golang多執行緒簡單鬥地主Golang執行緒
- 趁老王不在,和隔壁鄰居鬥鬥地主,比比大小
- 使用Java實現簡單的鬥地主案例Java
- Map實現鬥地主發牌有序版二
- 遊戲《鬥遊鬥地主》被撤銷出版物號,為今年首款遊戲
- “歡喜鬥地主”等44款App存違規行為APP
- P2540 [NOIP2015 提高組] 鬥地主 加強版
- 位元組跳動入股《芒果鬥地主》開發商林子互娛
- 鬥地主AI演算法——第五章の總值計算AI演算法
- 上班划水神器:一個可以在控制檯玩鬥地主的專案!
- 鬥圖?教你用Python製作表情包Python
- springboot+Java+cocos creater鬥地主,麻將非常的完整棋牌遊戲專案Spring BootJava遊戲
- 賽博鬥地主——使用大語言模型扮演Agent智慧體玩牌類遊戲。模型智慧體遊戲
- 歡樂鬥地主x喜茶:天生一對小歡喜,打牌喝茶在一起
- 用Python寫個魂鬥羅,另附30個Python小遊戲原始碼Python遊戲原始碼
- 數學家破解11選5騙局
- 當《歡樂鬥地主》開啟全新“518王炸節”,號準了多少玩家的脈搏?
- BurpSuite 啟用破解UI
- 一款“鬥地主”遊戲靠位元組跳動的流量空降免費榜TOP1遊戲
- DAU超百萬、日均廣告收入150萬,對話春節檔黑馬《小美鬥地主》製作人
- 從《歡樂鬥地主》歡樂全民賽中,發現拓盤全民電競的新藍海
- 用python暴力破解rar加密檔案(經過測試)Python加密
- 行列如稱鬥也八道局那二kab
- python實現密碼破解Python密碼
- 阿拉丁12月榜單:17款產品上榜 《歡樂鬥地主》增加新玩法消除類產品回暖
- 鬥地主產品問鼎小遊戲榜單 位元組跳動超休閒遊戲仍受歡迎遊戲
- Pycharn破解補丁啟用
- 破解有道JS引數,教你用python自制一個翻譯軟體!JSPython
- 同事加密壓縮包密碼忘記了,我用python幫他破解!加密密碼Python