Python破解 鬥地主殘局 ,對王,而後3帶2!

相信你們都玩過鬥地主,規則就再也不介紹了。緩存

直接上一張朋友圈看到的殘局圖:函數

這道題我剛看到時,曾嘗試用手工來破解,每次都覺得找到了農民的必勝策略時,最後都發現其實農民跑不掉。因爲手工破解沒法窮盡全部可能性,因此這道題究竟農民有沒有妙手跑掉呢,只能經過代碼來幫助咱們運算了。學習

本文將簡要講述怎麼經過代碼來求解此類問題,在最後會公佈殘局的最後結果優化

minimaxspa

代碼的核心思想是minimax。minimax能夠拆解爲兩部分,mini和max,分別是最小和最大的意思。code

直觀的理解是什麼呢?就有點像A、B兩我的下棋。A如今能夠在N個點走棋,假設A在某個點走棋了,使得A的這一步的盤面評估分數最高;可是輪到B下的時候,就必定會朝着讓A最不利的方向走,使得A的下一步必然按照B設定的軌跡來,而無法達到A在第一步時估算到這一步的最高盤面評分。blog

在牌局中是同樣的,若是農民的一手牌,讓地主不管如何應對都不能贏的話,那麼能夠說農民有必勝策略;不然,農民必輸。遞歸

核心邏輯ip

咱們能夠用一個函數hand_out來模擬一我的的出牌過程。在現實生活中,一我的想要出牌的話,必然須要知道本身手上的全部牌:me_pokers,也須要知道上一手的出的牌:last_hand。若是咱們要用這個函數來模擬兩我的的出牌,則還須要知道對手當前的全部牌:enemy_pokers。rem

這個函數的返回值,是輪到我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)。

結果

代碼運算出來的結果是,農民沒有必勝策略。換言之,只要地主會玩,農民不可能贏。階級固化已經如斯了麼……

若是你們想找一個Python學習環境,能夠加入咱們的Python學習圈: 784758214 ,咱們相互幫助,相互關心,相互分享內容,這樣出問題幫助你的人就比較多,羣號是784758214,這樣就能夠找到大神聚合,若是你只願意別人幫助你,不肯意分享或者幫助別人,那就請不要加了,你把你會的告訴別人這是一種分享。
相關文章
相關標籤/搜索