用python写一个有AI的斗地主游戏(二)——简述后端代码和思路
源碼請看我的Github頁面。
這是我一個課程的學術項目,請不要抄襲,引用時請注明出處。
本專欄系列旨在幫助小白從零開始開發一個項目,同時分享自己寫代碼時的感想。
請大佬們為我的拙見留情,有不規范之處煩請多多包涵!
文章目錄
- 開場白
- 邏輯
- 后端代碼和思路
- gameEngine.py
- utils.py
- 結束語
開場白
在上一篇博客里,已經介紹了開始前的一些準備。這篇博客講簡要介紹游戲開發中后端代碼結構的思路。當然,也是博主自己琢磨的,有遺漏或不足之處請指教!
邏輯
游戲的實現大概可以分為兩層(就像網頁開發一樣):前端和后端。后端負責儲存和管理游戲的邏輯(比如現在是誰的回合,誰手里都有什么牌,我能不能出這個對子等等),而前端負責與用戶的交互(比如在窗口里顯示自己的手牌,獲取輸入并更新游戲數據等等)。用類來表示前端和后端有種種好處,其中對新手最有幫助的就是能夠以更加“人類”的方式來組織和看待程序內容。比如,用類的你可以這樣想:“我想要游戲里玩家1打出這些牌然后玩家2出牌,那么我可以調用Game這個游戲邏輯類里的makePlay這個出牌的函數。”以這種方式思考和解決問題需要一些時間磨合,但是對長遠的開發習慣和效率都有極大好處。以下是游戲后端大致邏輯:
| 1 | 三名玩家進入房間/上桌 | 初始化游戲類,包括玩家名稱,場上順序,等等 |
| 2 | 洗牌并給每個玩家發初始的17張牌(留出3張地主牌) | 用游戲類給每個玩家類添加手牌,并在游戲類里維持地主牌的記錄 |
| 3 | 叫地主 | 按照順序給每個玩家叫地主的機會,安排出牌順序、給地主牌、改變玩家身份 |
| 4 | 按照順序和規則出牌或過牌 | 按照出牌順序,玩家出牌時調用相關函數進行可行性檢測和模擬出牌 |
| 5 | 如果有人出完了牌,結束游戲 | 在每一次出牌后檢查游戲是否結束,沒結束的話繼續上一步 |
這些游戲邏輯將在前端被構造和模擬,這里的后端代碼只是提供實現這些邏輯的基本工具。還有AI出牌的部分也會放到前端部分詳細講解。
后端代碼和思路
斗地主的后端邏輯和井字棋比起來還是有一定復雜度的。博主設計的游戲后端邏輯主要放到了gameEngine.py里,其中各包含更加細分的類。思來想去,我們需要實現以下功能:
''' GameEngine.py描述 Game Class: 一個用來表達游戲狀態的類(保存所有游戲信息)__init__: 用來構造類,需要游玩的三個玩家的id,初始化類sortHelper: 目前不重要,用來幫助排序卡的大小(因為卡的表達方式機器不易讀)shuffleDeck: 創建并打亂初始排隊dealCard: 將每個玩家17張卡隨機分發,并選擇3張隨機地主牌chooseLandlord: 把一個玩家的身份變為地主assignPlayOrder: 根據玩家身份調整出牌順序whichPattern: 返回選擇牌的種類(比如單張,對子,順子,三代二等等)以及它們的大小isValidPlay: 返回選擇的牌是否為可以按照規則打的牌makePlay: 模擬現實中的玩家出牌,即出牌后輪到下一家checkWin: 檢查游戲狀態(返回0代表游戲繼續,1代表地主獲勝,2代表農民獲勝)createAI: 用AI玩家代替人類玩家AIMakePlay: AI出牌 player Class: 一個用來表達玩家狀態的類(包括玩家名字/id,手牌,和是否為地主)__init__: 用玩家id構造類playCard: 從玩家手牌中移除選擇的卡 AI Class:__init__: 用AI玩家id構造類,繼承player類的方法getAllMoves: AI根據手牌生成所有可以出的牌 '''接下來我們就一個一個實現了。
gameEngine.py
首先是Game類。構建它的時候用到了以下內容:
class Game:def __init__(self, p1id, p2id, p3id):# 用來生成和表達卡的一些常量self.colors = ['heart', 'spade', 'diamond', 'club'] # 撲克牌的四個色self.nums = ['A', '2', '3', '4', '5', '6','7', '8', '9', '10', 'J', 'Q', 'K'] # 撲克牌的數字大小self.specials = ['X', 'D'] # 小王和大王self.cardOrder = {'3': 1, '4': 2, '5': 3, '6': 4, '7': 5, '8': 6, '9': 7, '10': 8,'J': 9, 'Q': 10, 'K': 11, 'A': 12, '2': 13, 'X': 14, 'D': 15} # 用來比較牌大小的字典# 創建游戲內的玩家(player類)和一個用玩家名稱指向player對象的字典self.p1 = player(p1id)self.p2 = player(p2id)self.p3 = player(p3id)self.playerDict = {p1id: self.p1, p2id: self.p2, p3id: self.p3}# 一些重要的游戲狀態self.currentPlayer = '' # 當前玩家idself.prevPlayer = '' # 上一個玩家idself.prevPlay = ['', []] # 上一次出牌,包括出牌者id和出的牌self.playOrder = [] # 出牌順序,包含三個玩家的idself.landLordCards = [] # 地主牌在我們完成構造函數后,就要開始寫功能性函數了。由于部分比較多,請小伙伴們挑選自己感興趣的部分閱讀,比如“欸這個whichPattern好像有點技術含量,了解了解”。具體如下:
# 幫助給卡排序的函數,輸入卡的名稱如‘club 2’,根據之前定義的self.cardOrder返回它的虛擬大小/排名def sortHelper(self, x):if x[-1] == '0': # 如果卡以0結尾,那么它是個10return self.cardOrder['10']return self.cardOrder[x[-1]]# 創建牌堆并洗牌,放到self.deck即游戲牌堆里def shuffleDeck(self):self.deck = []for i in self.colors: # 生成所有排列組合for j in self.nums:self.deck.append(i+' '+j) # “花色 數字”self.deck.append(self.specials[0]) # 放入小王和大王self.deck.append(self.specials[1])random.shuffle(self.deck) # 打亂列表元素順序(洗牌),別忘了先import random# 發每個玩家初始的17張手牌和3張地主牌def dealCard(self):# 這里介紹下,如果牌堆是完全隨機的,發牌順序對游戲公平性和體驗不會有影響# 我們自己洗牌一般都不會洗的太散,所以要一個人一個人發避免炸彈太多self.landLordCards = [] # 發地主牌for i in range(3):choice = random.choice(self.deck) # 隨機選一張self.landLordCards.append(choice) # 放到地主牌里self.deck.remove(choice) # 從牌堆里移除它self.p1Card = [] # 發玩家1的牌for i in range(17):choice = random.choice(self.deck) # 隨機選一張self.p1Card.append(choice) # 放到該玩家手牌中self.deck.remove(choice) # 從牌堆里移除它self.p2Card = [] # 發玩家2的牌for i in range(17):choice = random.choice(self.deck)self.p2Card.append(choice)self.deck.remove(choice)self.p3Card = [] # 發玩家3的牌for i in range(17):choice = random.choice(self.deck)self.p3Card.append(choice)self.deck.remove(choice)self.p1.cards = self.p1Card # 把每個玩家的牌放到他們的類里self.p2.cards = self.p2Cardself.p3.cards = self.p3Cardself.p1.cards.sort(key=lambda x: self.sortHelper(x)) # 給他們排序,模擬了玩的時候自己理牌self.p2.cards.sort(key=lambda x: self.sortHelper(x))self.p3.cards.sort(key=lambda x: self.sortHelper(x))# 根據輸入的玩家名稱/id,選擇地主身份def chooseLandlord(self, name):self.playerDict[name].identity = 'p' # 把這個玩家身份改為p,農民為sfor card in self.landLordCards: # 把地主牌放到這個玩家手牌里self.playerDict[name].cards.append(card)self.playerDict[name].cards.sort(key=lambda x: self.sortHelper(x)) # 給手牌排序# 出牌順序def assignPlayOrder(self):if self.p1.identity == 'p': # 地主第一個出,剩下兩個玩家隨機打亂self.playOrder = [self.p2.name, self.p3.name]random.shuffle(self.playOrder)self.playOrder.insert(0, self.p1.name)elif self.p2.identity == 'p':self.playOrder = [self.p1.name, self.p3.name]random.shuffle(self.playOrder)self.playOrder.insert(0, self.p2.name)elif self.p3.identity == 'p':self.playOrder = [self.p1.name, self.p2.name]random.shuffle(self.playOrder)self.playOrder.insert(0, self.p3.name)else: # 還沒選地主的情況,三個玩家順序隨機(等待叫地主)self.playOrder = [self.p1.name, self.p2.name, self.p3.name]random.shuffle(self.playOrder)self.currentPlayer = self.playOrder[0] # 當前玩家為第一個玩家# 獲取選擇卡的種類和大小def whichPattern(self, selectedCards):cardValues = []for i in selectedCards: # 把所有選擇的卡的值放到列表里if i[-1] == '0': # 如果0結尾那么它是個10cardValues.append(self.cardOrder['10'])else:cardValues.append(self.cardOrder[i[-1]])# 這里獲取卡的種類和大小。utils是我寫的另一個文件,借鑒了DouZero的utils庫# DouZero的項目很有趣,AI打斗地主很智能,感興趣的話請看https://github.com/kwai/DouZero# 我代碼里的utils.py里有著各種各樣的工具,為了方便管理就都放到了一個文件里,后面會展開介紹# get_move_type(cardValues)輸入選擇的卡(的值),返回整數代表的卡的種類和卡的大小pattern = utils.get_move_type(cardValues)return pattern # 返回一個字典{type: 1, value: 5},有卡的種類和大小# 檢查是否為可以打的牌def isValidPlay(self, selected):selectedCards = sorted(selected, key=lambda x: self.sortHelper(x)) # 先排序,方便比較if self.prevPlay[0] == self.currentPlayer:return True # 即上兩家都過牌,又輪到自己了,出啥都行pattern1 = self.whichPattern(self.prevPlay[1]) # 上一個出牌的種類和大小pattern2 = self.whichPattern(selectedCards) # 當前選擇牌的種類和大小if pattern2['type'] == 15 or pattern1['type'] == 5:return False # 即上家出牌是王炸或者當前選擇牌非法elif pattern2['type'] == 5 or pattern1['type'] == 0:return True # 即當前出牌是王炸或者前一家過牌else:if pattern1['type'] == pattern2['type'] and\pattern1['rank'] < pattern2['rank']:try: # 看看是不是三帶一if pattern1['len'] == pattern2['len']:return Truereturn Falseexcept:return True # 如果種類一樣并且選擇的要更大,可以出else:return False# 模擬游戲出牌def makePlay(self, selectedCards):if selectedCards == [] and self.prevPlay != []:pass # 過牌,什么都不做else: # 有牌打的話那就打牌self.playerDict[self.currentPlayer].playCard(selectedCards) # 當前玩家出牌self.prevPlay = [self.currentPlayer, selectedCards] # 記錄本次出牌self.prevPlayer = self.currentPlayer # 順序輪換playerIndex = self.playOrder.index(self.currentPlayer) # 當前玩家位置if playerIndex == 2: # 如果是列表里最后一個,循環到第一個self.currentPlayer = self.playOrder[0]else: # 否則轉到下一個玩家self.currentPlayer = self.playOrder[playerIndex+1]# 檢查游戲是否結束def checkWin(self):if self.playerDict[self.prevPlayer].cards == []: # 游戲在當前玩家出完牌后沒有手牌時結束if self.playerDict[self.prevPlayer].identity == 'p':return 1 # 地主贏else:return 2 # 農民贏else:return 0 # 游戲還在進行Game類里還有個很重要的部分,那就是用AI出牌。具體如下:
# 創建AI玩家def createAI(self, name2, name3):self.p2 = AI(name2) # AI是player的子類self.p3 = AI(name3)self.playerDict[name2] = self.p2 # 把對應的玩家改成AIself.playerDict[name3] = self.p3# AI出牌。這里的邏輯很簡單,即從能出的牌里隨便選一個出。之后可以進行提升def AIMakePlay(self, name, chosenLandLord):AIplayer = self.playerDict[name]if chosenLandLord: # 如果叫了地主正在出牌moves = AIplayer.getAllMoves() # 生成可以出的牌possibleMoves = [] # 根據場上情況實際可以打的牌牌for move in moves:realcards = []for card in move: # 先把生成的牌(無花色,只有大小)變成實際的牌(有花色和大小)for hand in AIplayer.cards: if hand[-1] == card[-1] and hand not in realcards:realcards.append(hand)if self.isValidPlay(realcards): # 如果可以打,加到選項里possibleMoves.append(realcards)possibleMoves.append([]) # 即允許過牌move = random.choice(possibleMoves) # 隨機選一個出牌方式出牌self.makePlay(move)else: # 還沒人叫地主的話就自己叫地主self.chooseLandlord(name)self.assignPlayOrder()在Game類里的時候我們用到了player對象來表達玩家的相關信息,這里我們定義下player類:
class player:def __init__(self, name):self.name = name # 玩家名稱/idself.identity = 's' # s代表農民,p代表地主self.cards = [] # 初始手牌# 出牌/從手牌中移除某些牌def playCard(self, selectedCards):for i in selectedCards:self.cards.remove(i)還有剛用到的AI玩家類。具體如下:
class AI(player): # 這里用到了繼承def __init__(self,name):super().__init__(name) # 繼承了player類的構造函數# 因為用到了繼承,所以AI也繼承了player類的playCard函數,所以無需再定義# 生成所有可以出的牌def getAllMoves(self):envmoves = utils.MovesGener(self.cards).gen_moves() # 這里又用到了utils.py,后面會介紹# MovesGener是一個出牌生成器,gen_moves()生成可以出的牌EnvCard2RealCard = {3: '3', 4: '4', 5: '5', 6: '6', 7: '7',8: '8', 9: '9', 10: '10', 11: 'J', 12: 'Q',13: 'K', 14: 'A', 17: '2', 20: 'X', 30: 'D'}realmoves = []for move in envmoves:realmove = [] # 把生成的虛擬數字變回撲克無花色數字for card in move:realmove.append(EnvCard2RealCard[card])realmoves.append(realmove)return realmoves到這里,游戲引擎/后端邏輯最主要的部分就寫好了。
utils.py
剛才提到了很多工具都來自utils.py。以下是它的代碼以及介紹:
''' 重要聲明:以下打星號(*)的函數均借鑒于DouZero,請感興趣的同學前往https://github.com/kwai/DouZero查看他們的代碼 utils.py描述is_contiuous_seq: 輸入出的牌,返回它是否連續(類似順子,但沒限制長度) *get_move_type: 輸入出的牌,返回它的種類(比如順子,單張,三帶一)和它的大小(比如三帶一的大小就是那三張的大小) *select: 輸入一些牌和一個代表長度的數字,生成該長度這些牌的不同組合 * MovesGener Class: 用來生成可行出牌的類 * '''這里是對于DouZero這部分代碼的引用:
@InProceedings{pmlr-v139-zha21a,title = {DouZero: Mastering DouDizhu with Self-Play Deep Reinforcement Learning},author = {Zha, Daochen and Xie, Jingru and Ma, Wenye and Zhang, Sheng and Lian, Xiangru and Hu, Xia and Liu, Ji},booktitle = {Proceedings of the 38th International Conference on Machine Learning},pages = {12333--12344},year = {2021},editor = {Meila, Marina and Zhang, Tong},volume = {139},series = {Proceedings of Machine Learning Research},month = {18--24 Jul},publisher = {PMLR},pdf = {http://proceedings.mlr.press/v139/zha21a/zha21a.pdf},url = {http://proceedings.mlr.press/v139/zha21a.html},abstract = {Games are abstractions of the real world, where artificial agents learn to compete and cooperate with other agents. While significant achievements have been made in various perfect- and imperfect-information games, DouDizhu (a.k.a. Fighting the Landlord), a three-player card game, is still unsolved. DouDizhu is a very challenging domain with competition, collaboration, imperfect information, large state space, and particularly a massive set of possible actions where the legal actions vary significantly from turn to turn. Unfortunately, modern reinforcement learning algorithms mainly focus on simple and small action spaces, and not surprisingly, are shown not to make satisfactory progress in DouDizhu. In this work, we propose a conceptually simple yet effective DouDizhu AI system, namely DouZero, which enhances traditional Monte-Carlo methods with deep neural networks, action encoding, and parallel actors. Starting from scratch in a single server with four GPUs, DouZero outperformed all the existing DouDizhu AI programs in days of training and was ranked the first in the Botzone leaderboard among 344 AI agents. Through building DouZero, we show that classic Monte-Carlo methods can be made to deliver strong results in a hard domain with a complex action space. The code and an online demo are released at https://github.com/kwai/DouZero with the hope that this insight could motivate future work.} }以下是主要代碼:
import collections import itertools################## 檢查這些牌是不是連續的序列 ################## def is_continuous_seq(move):i = 0while i < len(move) - 1: # 遍歷排序好的牌,看相鄰兩張是否大小差值為1if move[i+1] - move[i] != 1:return Falsei += 1return True################## 獲取出牌的種類和大小 ################## def get_move_type(move):move_size = len(move)move_dict = collections.Counter(move) # 一個用來計數可哈希對象的字典子類# 它的len就是move里值/牌種類的數量(比如有3和4那len就是2,只有3那len就是1)if move_size == 0: # 過牌return {'type': 0}if move_size == 1: # 單張return {'type': 1, 'rank': move[0]}if move_size == 2:if move[0] == move[1]: # 對子return {'type': 2, 'rank': move[0]}elif move == [20, 30]: # 王炸return {'type': 5}else: # 違規出法return {'type': 15}if move_size == 3: # 三張(不帶)if len(move_dict) == 1:return {'type': 3, 'rank': move[0]}else: # 違規出法return {'type': 15}if move_size == 4:if len(move_dict) == 1: # 炸彈return {'type': 4, 'rank': move[0]}elif len(move_dict) == 2: # 三帶一if move[0] == move[1] == move[2] or move[1] == move[2] == move[3]:return {'type': 6, 'rank': move[1]}else: # 違規出法return {'type': 15}else: # 違規出法return {'type': 15}if is_continuous_seq(move): # 順子return {'type': 8, 'rank': move[0], 'len': len(move)}if move_size == 5:if len(move_dict) == 2: # 三帶二return {'type': 7, 'rank': move[2]}else: # 違規出法return {'type': 15}count_dict = collections.defaultdict(int)for c, n in move_dict.items(): # 遍歷每個卡面值-計數對count_dict[n] += 1 # 看每個計數有多少張卡if move_size == 6: # 四帶兩單if (len(move_dict) == 2 or len(move_dict) == 3) and count_dict.get(4) == 1 and \(count_dict.get(2) == 1 or count_dict.get(1) == 2):return {'type': 13, 'rank': move[2]}# 四帶兩對if move_size == 8 and (((len(move_dict) == 3 or len(move_dict) == 2) and(count_dict.get(4) == 1 and count_dict.get(2) == 2)) or count_dict.get(4) == 2):return {'type': 14, 'rank': max([c for c, n in move_dict.items() if n == 4])}mdkeys = sorted(move_dict.keys())if len(move_dict) == count_dict.get(2) and is_continuous_seq(mdkeys):# 飛機(連對)return {'type': 9, 'rank': mdkeys[0], 'len': len(mdkeys)}if len(move_dict) == count_dict.get(3) and is_continuous_seq(mdkeys):# 火箭(連續三不帶)return {'type': 10, 'rank': mdkeys[0], 'len': len(mdkeys)}# 檢查三帶一火箭和三帶二火箭if count_dict.get(3, 0) >= 2:serial_3 = list()single = list()pair = list()for k, v in move_dict.items():if v == 3:serial_3.append(k)elif v == 1:single.append(k)elif v == 2:pair.append(k)else: # 違規出法return {'type': 15}serial_3.sort()if is_continuous_seq(serial_3):if len(serial_3) == len(single)+len(pair)*2:# 三帶一火箭return {'type': 11, 'rank': serial_3[0], 'len': len(serial_3)}if len(serial_3) == len(pair) and len(move_dict) == len(serial_3) * 2:# 三帶二火箭return {'type': 12, 'rank': serial_3[0], 'len': len(serial_3)}if len(serial_3) == 4: # 三帶一火箭if is_continuous_seq(serial_3[1:]):return {'type': 11, 'rank': serial_3[1], 'len': len(serial_3) - 1}if is_continuous_seq(serial_3[:-1]):return {'type': 11, 'rank': serial_3[0], 'len': len(serial_3) - 1}return {'type': 15} # 違規出法################## 生成指定長度的指定卡牌的所有組合 ################## def select(cards, num):return [list(i) for i in itertools.combinations(cards, num)]################## 生成可行出牌的類 ################## class MovesGener(object):"""This is for generating the possible combinations"""def __init__(self, cards_list):RealCard2EnvCard = {'3': 3, '4': 4, '5': 5, '6': 6, '7': 7,'8': 8, '9': 9, '10': 10, 'J': 11, 'Q': 12,'K': 13, 'A': 14, '2': 17, 'X': 20, 'D': 30}self.cards_list = []for i in cards_list:if i[-1] == '0':self.cards_list.append(RealCard2EnvCard['10'])else:self.cards_list.append(RealCard2EnvCard[i[-1]])self.cards_dict = collections.defaultdict(int)for i in self.cards_list:self.cards_dict[i] += 1self.single_card_moves = []self.gen_type_1_single()self.pair_moves = []self.gen_type_2_pair()self.triple_cards_moves = []self.gen_type_3_triple()self.bomb_moves = []self.gen_type_4_bomb()self.final_bomb_moves = []self.gen_type_5_king_bomb()# 生成順子def _gen_serial_moves(self, cards, min_serial, repeat=1, repeat_num=0):if repeat_num < min_serial: # at least repeat_num is min_serialrepeat_num = 0single_cards = sorted(list(set(cards)))seq_records = list()moves = list()start = i = 0longest = 1while i < len(single_cards):if i + 1 < len(single_cards) and single_cards[i + 1] - single_cards[i] == 1:longest += 1i += 1else:seq_records.append((start, longest))i += 1start = ilongest = 1for seq in seq_records:if seq[1] < min_serial:continuestart, longest = seq[0], seq[1]longest_list = single_cards[start: start + longest]if repeat_num == 0: # No limitation on how many sequencessteps = min_serialwhile steps <= longest:index = 0while steps + index <= longest:target_moves = sorted(longest_list[index: index + steps] * repeat)moves.append(target_moves)index += 1steps += 1else: # repeat_num > 0if longest < repeat_num:continueindex = 0while index + repeat_num <= longest:target_moves = sorted(longest_list[index: index + repeat_num] * repeat)moves.append(target_moves)index += 1return moves# 生成單張def gen_type_1_single(self):self.single_card_moves = []for i in set(self.cards_list):self.single_card_moves.append([i])return self.single_card_moves# 生成對子def gen_type_2_pair(self):self.pair_moves = []for k, v in self.cards_dict.items():if v >= 2:self.pair_moves.append([k, k])return self.pair_moves# 生成三不帶def gen_type_3_triple(self):self.triple_cards_moves = []for k, v in self.cards_dict.items():if v >= 3:self.triple_cards_moves.append([k, k, k])return self.triple_cards_moves# 生成炸彈def gen_type_4_bomb(self):self.bomb_moves = []for k, v in self.cards_dict.items():if v == 4:self.bomb_moves.append([k, k, k, k])return self.bomb_moves# 生成王炸def gen_type_5_king_bomb(self):self.final_bomb_moves = []if 20 in self.cards_list and 30 in self.cards_list:self.final_bomb_moves.append([20, 30])return self.final_bomb_moves# 生成三帶一def gen_type_6_3_1(self):result = []for t in self.single_card_moves:for i in self.triple_cards_moves:if t[0] != i[0]:result.append(t+i)return result# 生成三帶二def gen_type_7_3_2(self):result = list()for t in self.pair_moves:for i in self.triple_cards_moves:if t[0] != i[0]:result.append(t+i)return result# 生成順子def gen_type_8_serial_single(self, repeat_num=0):return self._gen_serial_moves(self.cards_list, 5, repeat=1, repeat_num=repeat_num)# 生成飛機(連對)def gen_type_9_serial_pair(self, repeat_num=0):single_pairs = list()for k, v in self.cards_dict.items():if v >= 2:single_pairs.append(k)return self._gen_serial_moves(single_pairs, 3, repeat=2, repeat_num=repeat_num)# 生成三不帶火箭def gen_type_10_serial_triple(self, repeat_num=0):single_triples = list()for k, v in self.cards_dict.items():if v >= 3:single_triples.append(k)return self._gen_serial_moves(single_triples, 2, repeat=3, repeat_num=repeat_num)# 生成三帶一火箭def gen_type_11_serial_3_1(self, repeat_num=0):serial_3_moves = self.gen_type_10_serial_triple(repeat_num=repeat_num)serial_3_1_moves = list()for s3 in serial_3_moves: # s3 is like [3,3,3,4,4,4]s3_set = set(s3)new_cards = [i for i in self.cards_list if i not in s3_set]# Get any s3_len items from cardssubcards = select(new_cards, len(s3_set))for i in subcards:serial_3_1_moves.append(s3 + i)return list(k for k, _ in itertools.groupby(serial_3_1_moves))# 生成三帶二火箭def gen_type_12_serial_3_2(self, repeat_num=0):serial_3_moves = self.gen_type_10_serial_triple(repeat_num=repeat_num)serial_3_2_moves = list()pair_set = sorted([k for k, v in self.cards_dict.items() if v >= 2])for s3 in serial_3_moves:s3_set = set(s3)pair_candidates = [i for i in pair_set if i not in s3_set]# Get any s3_len items from cardssubcards = select(pair_candidates, len(s3_set))for i in subcards:serial_3_2_moves.append(sorted(s3 + i * 2))return serial_3_2_moves# 生成四帶兩單def gen_type_13_4_2(self):four_cards = list()for k, v in self.cards_dict.items():if v == 4:four_cards.append(k)result = list()for fc in four_cards:cards_list = [k for k in self.cards_list if k != fc]subcards = select(cards_list, 2)for i in subcards:result.append([fc]*4 + i)return list(k for k, _ in itertools.groupby(result))# 生成四帶兩對def gen_type_14_4_22(self):four_cards = list()for k, v in self.cards_dict.items():if v == 4:four_cards.append(k)result = list()for fc in four_cards:cards_list = [k for k, v in self.cards_dict.items()if k != fc and v >= 2]subcards = select(cards_list, 2)for i in subcards:result.append([fc] * 4 + [i[0], i[0], i[1], i[1]])return result# 生成所有可能的出牌方式def gen_moves(self):moves = []moves.extend(self.gen_type_1_single())moves.extend(self.gen_type_2_pair())moves.extend(self.gen_type_3_triple())moves.extend(self.gen_type_4_bomb())moves.extend(self.gen_type_5_king_bomb())moves.extend(self.gen_type_6_3_1())moves.extend(self.gen_type_7_3_2())moves.extend(self.gen_type_8_serial_single())moves.extend(self.gen_type_9_serial_pair())moves.extend(self.gen_type_10_serial_triple())moves.extend(self.gen_type_11_serial_3_1())moves.extend(self.gen_type_12_serial_3_2())moves.extend(self.gen_type_13_4_2())moves.extend(self.gen_type_14_4_22())return moves以上就是游戲邏輯用到的所有代碼了!
結束語
寫AI的時候比較懶事情比較多,沒去研究怎么給DouZero寫個API調用這個很強的AI,也沒自己寫比較有技術含量的AI。國際象棋和井字棋用到的minmax和alpha-beta這種AI算法好像不太能勝任(要自己調整/找很多參數),對于斗地主這個七分靠運氣的游戲來說隨機選擇是性價比最高的了。感興趣的小伙伴可以嘗試自己研究下這種有團隊協作、游戲信息半透明、規則較多的AI該怎么寫(提前對研究出來的大佬說太強了)。
平時我們自己打斗地主的一些額外規則(比如三個人都不叫地主就重新洗牌,贏的人先叫地主,其他人可以搶地主,下籌碼等等)和甚至某些基礎規則(前兩個人都過牌那么你就不能過牌),我都沒添加到游戲中,因為這些是bug游戲特性。
本系列的下一篇博客將會展示如何在前端用tkinter和pygame寫游戲界面并調用后端邏輯,敬請期待!有各種問題和見解也歡迎評論或者私信!
總結
以上是生活随笔為你收集整理的用python写一个有AI的斗地主游戏(二)——简述后端代码和思路的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 2019,你不知道的大厂薪酬
- 下一篇: 中国高校计算机大赛--网络技术挑战赛(C