《棋牌游戲服務器》斗地主AI設計


設計目標

要取得良好效果,首先要搞清楚一個問題:我們想得到一個什么樣的斗地主AI?
我們的AI是用在手游產品當中,在真實玩家不足時為用戶提供陪玩服務,這個目標決定了這個AI要具備以下兩個核心特點:
1、執行效率高,要為在線運行為玩家提供服務,不能給服務器太大壓力;
2、模擬人的思維方式,讓AI看起來像一個中等水平的玩家,而不是追求很高的勝率。

斗地主的AI一般有三個核心部分:拆牌、出牌、接牌。要提高算法的執行效率,在這三個環節就不能完全依賴深度搜索的策略,在本設計方案中,所有的策略基本都是“靜態規則+有限搜索”的套路。打個比方,在拆牌過程中,我拆出了三張、對子、單張之后,在給三張配帶牌的時候怎么選擇呢?我只會對單張、對子做一下簡單的篩選,而不會去考慮是否把某個順子的尾部拆出來當帶牌會更優。

要讓AI看起來像人,其實就是把常見的一些出牌套路用算法表達出來。這里運用策略模式來組織代碼,不同的策略有不同的優先級,理論上,我們可以通過不斷地添加策略來完善這個AI。打個比方,對於出牌,假設我們有以下三條策略(實際算法當然不止三條):1)只剩一手牌策略,直接出完就贏了,2)只有一手小牌,其他全部大牌,小牌放到最后,其他從小往大出;3)小牌優先策略,找出最小的牌來出。這幾個策略的優先級就是1>2>3。

“搜索”在接牌過程中用得比較頻繁,比如別人出一個三帶一,我手上的牌拆出來是三帶一對,那么是把這個對子拆掉呢,還是另外找一個單張。打個比方如果手上是"QQQkk345678",那么顯然是帶一張3比較好,如果手上是“QQQKK56789”,那么應該拆對k。這里會對所有符合規則的帶牌做一個搜索,然后評判哪個方案是最優的,於是關鍵點就是采用什么評判規則。

我采用一種基於分值的手牌評價規則,一手牌,無論多少張,先進行拆牌,然后每個牌組(牌組指單張、順子、對子等可以一次出掉的牌)都有一個分值,最后把所有牌組的分值加起來,形成一個手牌分值。上面所說的接牌搜索過程,會進行反復的拆牌和分值計算,因此拆牌的算法必須非常高效。

上面介紹了這個斗地主AI的核心設計思路,其中評分規則參考了這篇文章:https://blog.csdn.net/sm9sun/article/details/70787814;拆牌算法參考了這篇文章:http://www.360doc.com/content/11/0108/09/2617151_84917660.shtml,在此對兩位作者表示感謝。網上介紹的所有斗地主AI設計,都過於簡單,用來調試程序還行,直接用於線上產品則遠遠不夠。

拆牌算法

拆牌算法總體上是一個基於牌型優先級的查找過程: 

  1. 如果有火箭,找出來;
  2. 如果有炸彈,找出來(炸彈不拆);
  3. 如果有2,全部找出來(2不會參與順子);
  4. 如果有飛機,找出來(飛機也不拆);
  5. 找出所有的順子,每個順子盡量長;
  6. 處理順子
    1. 順子分裂,現有一個順子345678910,發現手上還剩下一張6和7,那么順子分裂成34567和678910;
    2. 順子拆出連對,現有一個順子345678910,發現手上還有345,變成334455和678910更好,就把順子里面的345放回去;
    3. 順子拆出三張,現有一個順子345678,發現手上還有88,那么變成34567和888更好,就把順子里面的8放回去;
    4. 順子如果蓋住對子、三張、連對,如果發現打散牌組數更少,則打散,比如7789910JJJQKK,拆散了更好;
    5. 順子拆出兩頭的對子,現有一個順子67890JQ,發現手上還有Q和6,那么把孫子里面的Q和6放回去;
    6. 反復進行1)到5),直到沒有進一步變化;
  7. 剩余的牌里面找出所有的連對;
  8. 查看所有的連對,如果長度超過3,且一端有三條,把三條拆出來;
  9. 剩余的牌里面找出所有的三張;
  10. 延長順子:如果一個順子的兩端外面有一個對子,如果這個對子小於10,則並入順子,比如34567+88,那么變成345678+8;
  11. 合並順子:相同的順子變成連對,首尾相接的順子連成一個;
  12. 剩余的牌里面找出所有對子和單張。

舉個例子,一手牌“333455678910JJK22小王”: 

  1. 先把炸彈、2、王拆出來,剩下“333455678910JJK”
  2. 找順子,得到345678910J,剩下"335JK"
  3. 由於順子的左側有3張,於是順子變成45678910J,剩下3335JK
  4. 由於順子右側的J有一對,於是順子變成45678910,剩下3335JJK
  5. 找三張,得到333;
  6. 剩下的得到5、JJ、K。

至此,一手牌就拆好了,為了簡化算法,我們不拆炸彈,不拆飛機。在主動出牌時,會按着這個拆牌來出;在接牌時則不會,會依據對方的牌型來搜索一個更優方案,此時順子、飛機、炸彈都可能被拆散,具體細節下文會講。

帶牌選擇

上面拆好的牌組,三張和飛機是沒有帶牌的,在此基礎上選擇帶牌是比較容易的,就是在對子和單張里面選擇最小的牌即可。

減少單張的拆牌

一個常見的場景是,當我的敵人只剩下一張牌時,此時應該采用一種“讓單張盡量少的策略”,這個策略和常規策略大體相同,有兩個不同點: 1. "延長順子“這一步不執行; 2. 帶牌選擇的時候,優先選擇單張

評分規則

一手牌到底好不好,我們需要一個量化標准,也就是給手牌評一個分值。否則沒法決策是否叫地主,在接牌過程中也沒法決策A方案好,還是B方案好,抑或不要才是最好選擇。

評分的大體理念是,對一個牌組來說,越能管牌,分值越大;越不容易被管,分值越大。而一手牌的分值,則不僅取決於拆出來的牌組的分數,還取決於拆出來牌組的組數,和前者正相關和后者負相關。 

為了平衡,牌組的分值可能是正的,也可能是負數,以10為參考點,單張10為0分,9為-1分,J為1分。每個牌組有一個maxCard屬性,單張和對子就是自身,順子和連對是最大那張牌,三帶是有三張的那個牌,這個很容易理解,相同的牌型比較大小,就是比較maxCard。我們評分也基於這個屬性。

牌型 分值 備注
單牌 maxCard-10  
對子 maxCard-10 如果為正,+50%
三張 maxCard-10 如果為正,+100%
三帶 maxCard-10 如果為正,+50%,因為帶牌了比三張加得少
順子 Max(0,(maxCard-10)/2)  
連對 同上  
飛機 同上  
飛機帶 同上,如果帶牌的分數為正,把帶牌的分數也加上  
炸彈 固定9  
火箭 12  

這個設定規則,是在調試過程中依據經驗感覺不斷調節的出來的。比如對子和三張的額外加分問題,是要體現出大對子比如(22)的價值。而順子分值公式,是基於“順子很難管牌,分數不宜過高,但是本身也很難被管”這個事實。所以這組規則沒啥嚴密的邏輯,也不一定合理。 

接下來就是手牌的評分規則,也就是若干個牌組在一起怎么評分,最初的公式是:sum(牌組分)-PxN,每個輪次設定一個固定的分值P,用牌組分值的和減去P乘以牌組數量N。這個機制有一個很難解決的問題,就是P的取值,P過大,過於強調輪次的價值,在牌局初期接牌的時候過於激進(無論出什么牌都導致總分值增加,因為輪次減少了);P過小,過於忽略輪次的價值,在牌局末期出牌過於保守(大牌攥着出不去,因為減少一個輪次加不了幾分)。

幾經周折,最后定個兩個規則:
1、大牌的輪次忽略,因為大牌總是出得去的(我們手上多個炸彈,不會有任何負面影響),包括王、2、炸彈;
2、P的取值是一個列表:{ 6, 6, 6, 5, 5, 5, 4, 4, 4, 3},意思是索引1~3->P=6,索引4~6->P=5,索引7~9->P=4;之后P=3,這樣在早期一手大牌出去減分會比較多(這時減少一組牌所得到的輪次分增益比較少),到后期減分比較少。 

總體來說,牌的分值更多具有比較意義,而不具備絕對意義。我們在整個AI系統中,除了在叫地主時,盡量去比較兩個出牌方案的優劣,而避免把一手牌的分值去和一個常數進行比較。

出牌策略

出牌策略就是在一種特定模式下的出牌套路,所以我們識別一個特定模式,就可以編寫一個策略。反過來,這個策略也會檢查一下,當前的牌局是否符合對應的模式,如果不符合它就不處理,交給下一個策略處理。

有些策略適用於農民,有些適用於地主,這里我們羅列一下農民的出牌策略,按優先級從搞到低:

策略 適用模式 出牌
oneHand 手里只剩一組牌 出完即贏
allBig 手里只有一組小牌,其他大牌 先出大的,再出小的
foreEnemySingle 敵人只有兩張牌,我有絕對的大單,其他都是對子 出單,迫使對方出單
letFirend 我下手是隊友,只有一張牌,且我有<10的牌 出小單張
smallAndLong 有些小的順子、連對、飛機 先出這些
fewPoke 牌比較少的時候 優先出單張以外的牌
enemyOnePoke 地主一張牌時,我在他上手 盡量出其他牌型,否則從大往小
enemyOnePoke 地主一張牌時,我在他下手 如果有對子,而且單張很多,出非最小單張,期望和對家配合,否則同上
smallFirst 兜底的策略 小牌優先出

理論上,如果我們找到新的模式,就可以創建新的策略添加到這個列表,我們的AI也就越完善。

牌的大小預測

在上面的策略中,提到一組牌是大還是小,實際是指,這手牌對方接住的可能性大還是小。比如我有一個34567,對方只有3張牌,而且王已經出過了,那么我認為這順子是大牌。

我們合理地利用“出過的牌“,“我手里的牌”,“別人手上牌的數量”這個幾個信息,做出上面的判斷。所謂“合理”,就是指正常的玩家,也知道這些信息,也能做出類似的判斷;而不能去作弊開“天眼”。

策略之間的呼應

如果仔細琢磨一下,可以發現allBig,foreEnemySingle,enemyOnePoke這幾個策略之間是相互呼應的。這一輪使用了這個策略,如果順利,下一輪就可能落入另外一個模式,這也有點像真人的布局謀划。我在設計策略的時候,有兩個原則:1)策略上下文無關(以前的出牌過程不影響);2)策略之間盡量互相呼應。

接牌策略

接牌是被動的,所以策略相對少一些。 以農民接牌策略為例:

策略 適用模式 出牌
oneHand 手里只剩一組牌,且能接 接牌
jieAndWin 接完之后,進入上面的allBig模式 接牌
enemyOnePoke 地主一張牌,在我下手 除非隊友出牌足夠大,否則盡量用大牌接
letFriend 隊友下手,且只有一張牌,我有<10的牌 盡量大牌接
normal 兜底的策略  

接牌策略里,這個兜底的策略是最難寫的,因為這里要決策接還是不接,大體考慮以下幾個因素: 

  1. 是否隊友的牌;
  2. 我的牌是否非常好;
  3. 地主是我的上手還是下手;
  4. 對方還剩多少牌;
  5. 出牌大小;
  6. 接牌的大小;

我設計的兜底策略大體是這樣的:

  1. 首先通過搜索找到一個能接牌的最佳牌組,如果找不到,就結束了;
  2. 如果是隊友的牌,相對還比較大,我不接
  3. 如果是隊友的牌,我肯定不用大牌接;
  4. 如果是地主的牌,我的牌絕對得好,肯定接;
  5. 如果是地主的牌,接了之后我的牌不變差太多,接;
  6. 如果是地主的牌,我的接牌比他大得不多(比如王對2),接;
  7. 如果是地主的牌,接牌是順子、飛機、連對這類,接;
  8. 如果是地主的牌,我要動用炸彈,如果他牌還很多,出的又不是王和2之類,不接;
  9. 如果是地主的牌,他的牌很少,或者他出大牌了,接。

第一步搜索接牌,不會考慮拆牌的結果,而是去手牌里面強行搜索,比如要接一對QQ,我手里有KKK2235,那么KK和22會先后被搜索到,然后判斷剩下手牌的評分來比較方案的優劣。

后面都是一些瑣碎的判斷,其中“多少分算絕對好牌”,“多少張牌算多”,都是比較主觀的設定。

贏牌路徑

在出牌策略和接牌策略里,有一個優先級最高的策略,即“贏牌策略”,現在的牌面存在一個能贏的出牌方法。打個比方,我現在手里有3組牌,如果只有一組小牌,那么把它放的最后出就可以了。這個場景下,判斷一組牌的大小,是看對手有沒有可能接住它,而不是看它的絕對大小。先把如何“判斷大小”的細節放在一邊,先看看算法過程。

出牌贏牌路徑判定

1、先掃描一下所有的牌組,別人能接住的牌組數,記為s;
2、如果s=0或1,那么判定成功,只要把僅有的小牌放到后面即可;
3、如果s==2,假設這兩組牌為s1,s2;那么看剩下的牌里面是否有一組牌能接住s1,如果有,那么判定成功,此時出s1即可,s2也類似;如果s1和s2都沒有牌能接住,判定失敗; 4、s>2,判定失敗。

接牌贏牌路徑判定

1、先找到一組能接住當前牌的牌組,記為p,找不到則判定失敗;
2、如果p是對手接不住的牌,那么看剩下的牌是否滿足“出牌贏牌路徑判定”;
3、如果p是對手能接住的牌,那么看剩下的牌組里面是否存在相同牌型的的當前最大牌記為l(意思就是肯定能把牌再要回來,比如當前出對子55,p是66,但是我手里還有22),再看剩下的牌(除了p和l)是否滿足“出牌贏牌路徑判定”; 

判斷牌的大小

在贏牌路徑判定這個場景下,判斷一組牌的大小,是看對手有沒有可能接住它,而不是看它的絕對大小。考慮三個因素: 桌面上還剩什么牌、我的手里有多少牌、對手還有幾張牌。依據這三個數據能計算出對手能接住某個牌組的概率。

算法大體是這樣的:
1、假設桌面上剩下牌的集合記作T,我手里的牌集合就做H,那么E=T-H是敵人手里牌的一個超集,敵人手里的牌張數是n,現在要牌組p能被敵人接住的概率;
2、如果敵人總的牌數n比p的張數還要少,那么p被接的概率等於敵人有火箭的概率;
3、看E里面能否找到能接住p的牌,如果找不到,那么p被接的概率等於敵人有火箭的概率;
4、如果找到一個牌組q能接住p,那么假設q包含的每張牌在敵人手里的概率是e/|E|,以二項分布的公式來計算敵人手里有q的概率;
5、如果能找到多組類似的q,那么分別計算再以“或的關系”組合起來。

這個算法計算出的是一個大概的估計值,基本已經夠用,而且我們並不考慮一般“炸彈”的存在,因為真實的玩家也會傾向於忽略有普通炸彈的情況。

總結

 
從測試的情況來看,這個AI系統的效果還是可以的,看起來比較像人在出牌。這個實現方案有幾個顯著的缺點:
1、單純的對手牌評分局限很大,場上的局勢不僅包括我手里有什么牌,還包括主動權的變化,桌面剩余牌的變化,沒有能找到一個合適的數據模型來統一這些因素;
2、由於前一個原因,導致算法不斷地針對特殊情況打補丁,上面的羅列的策略內部,或多或少都有一些補丁,導致的后果就是算法的可維護性急劇下降,而且往往牽一發而動全身;
3、不考慮上下文,比如不會針對某個人前面出過什么牌來猜測他手里有什么牌;
4、所謂按人的思維來建立策略,實際上就是按作者本人的打牌習慣,因此缺少一些變化,萬一遇到沒考慮到的某種牌型,AI有可能犯傻;
5、沒有能設計出自動化測試方案,我讓三個機器人來出牌,人工觀察是否有問題,調試效率非常的低。 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM