我用Python做個AI出牌器鬥地主把把贏
前言
最近在網上看到一個有意思的開源項目,基於快手團隊開發的開源AI鬥地主——DouZero做的一個“成熟”的AI,項目開源地址【https://github.com/tianqiraf/DouZero_For_HappyDouDiZhu – tianqiraf】。
今天我們就一起來學習下是如何制作一個基於DouZero的出牌器,看看AI是如何來幫助鬥地主的!
一、核心功能設計
首先這款出牌器是基於DouZero開發的,核心是需要利用訓練好的AI模型來幫住我們,給出最優出牌方案。
其次關於出牌器,先要需要確認一個AI出牌角色,代表我們玩傢自己。我們隻要給這個AI輸入玩傢手牌和三張底牌。確認好地主和農民的各個角色,告訴它三個人對應的關系,這樣就可以確定隊友和對手。
我們還要將每一輪其他兩人的出牌輸入,這樣出牌器就可以根據出牌數據,及時提供給我們最優出牌決策,帶領我們取得勝利!
那麼如何獲取三者之間的關系呢?誰是地主?誰是農民?是自己一人作戰還是農民合作?自己玩傢的手牌是什麼?三張底牌是什麼?這些也都需要在開局後確認好。
大致可以整理出要實現的核心功能如下:
UI設計排版佈局
- 顯示三張底牌
- 顯示AI角色出牌數據區域,上傢出牌數據區域,下傢出牌數據區域,本局勝率區域
- AI玩傢手牌區域
- AI出牌器開始停止
手牌和出牌數據識別
- 遊戲剛開始根據屏幕位置,截圖識別AI玩傢手牌及三張底牌
- 確認三者之間的關系,識別地主和農民角色,確認隊友及對手關系
- 識別每輪三位玩傢依次出瞭什麼牌,刷新顯示對應區域
AI出牌方案輸出
- 加載訓練好的AI模型,初始化遊戲環境
- 每輪出牌判斷,根據上傢出牌數據給出最優出牌決策
- 自動刷新玩傢剩餘手牌和本局勝率預測
二、實現步驟
1. UI設計排版佈局
根據上述功能,首先考慮進行簡單的UI佈局設計,使用的是pyqt5。核心設計代碼如下:
def setupUi(self, Form): Form.setObjectName("Form") Form.resize(440, 395) font = QtGui.QFont() font.setFamily("Arial") font.setPointSize(9) font.setBold(True) font.setItalic(False) font.setWeight(75) Form.setFont(font) self.WinRate = QtWidgets.QLabel(Form) self.WinRate.setGeometry(QtCore.QRect(240, 180, 171, 61)) font = QtGui.QFont() font.setPointSize(14) self.WinRate.setFont(font) self.WinRate.setAlignment(QtCore.Qt.AlignCenter) self.WinRate.setObjectName("WinRate") self.InitCard = QtWidgets.QPushButton(Form) self.InitCard.setGeometry(QtCore.QRect(60, 330, 121, 41)) font = QtGui.QFont() font.setFamily("Arial") font.setPointSize(14) font.setBold(True) font.setWeight(75) self.InitCard.setFont(font) self.InitCard.setStyleSheet("") self.InitCard.setObjectName("InitCard") self.UserHandCards = QtWidgets.QLabel(Form) self.UserHandCards.setGeometry(QtCore.QRect(10, 260, 421, 41)) font = QtGui.QFont() font.setPointSize(14) self.UserHandCards.setFont(font) self.UserHandCards.setAlignment(QtCore.Qt.AlignCenter) self.UserHandCards.setObjectName("UserHandCards") self.LPlayer = QtWidgets.QFrame(Form) self.LPlayer.setGeometry(QtCore.QRect(10, 80, 201, 61)) self.LPlayer.setFrameShape(QtWidgets.QFrame.StyledPanel) self.LPlayer.setFrameShadow(QtWidgets.QFrame.Raised) self.LPlayer.setObjectName("LPlayer") self.LPlayedCard = QtWidgets.QLabel(self.LPlayer) self.LPlayedCard.setGeometry(QtCore.QRect(0, 0, 201, 61)) font = QtGui.QFont() font.setPointSize(14) self.LPlayedCard.setFont(font) self.LPlayedCard.setAlignment(QtCore.Qt.AlignCenter) self.LPlayedCard.setObjectName("LPlayedCard") self.RPlayer = QtWidgets.QFrame(Form) self.RPlayer.setGeometry(QtCore.QRect(230, 80, 201, 61)) font = QtGui.QFont() font.setPointSize(16) self.RPlayer.setFont(font) self.RPlayer.setFrameShape(QtWidgets.QFrame.StyledPanel) self.RPlayer.setFrameShadow(QtWidgets.QFrame.Raised) self.RPlayer.setObjectName("RPlayer") self.RPlayedCard = QtWidgets.QLabel(self.RPlayer) self.RPlayedCard.setGeometry(QtCore.QRect(0, 0, 201, 61)) font = QtGui.QFont() font.setPointSize(14) self.RPlayedCard.setFont(font) self.RPlayedCard.setAlignment(QtCore.Qt.AlignCenter) self.RPlayedCard.setObjectName("RPlayedCard") self.Player = QtWidgets.QFrame(Form) self.Player.setGeometry(QtCore.QRect(40, 180, 171, 61)) self.Player.setFrameShape(QtWidgets.QFrame.StyledPanel) self.Player.setFrameShadow(QtWidgets.QFrame.Raised) self.Player.setObjectName("Player") self.PredictedCard = QtWidgets.QLabel(self.Player) self.PredictedCard.setGeometry(QtCore.QRect(0, 0, 171, 61)) font = QtGui.QFont() font.setPointSize(14) self.PredictedCard.setFont(font) self.PredictedCard.setAlignment(QtCore.Qt.AlignCenter) self.PredictedCard.setObjectName("PredictedCard") self.ThreeLandlordCards = QtWidgets.QLabel(Form) self.ThreeLandlordCards.setGeometry(QtCore.QRect(140, 10, 161, 41)) font = QtGui.QFont() font.setPointSize(16) self.ThreeLandlordCards.setFont(font) self.ThreeLandlordCards.setAlignment(QtCore.Qt.AlignCenter) self.ThreeLandlordCards.setObjectName("ThreeLandlordCards") self.Stop = QtWidgets.QPushButton(Form) self.Stop.setGeometry(QtCore.QRect(260, 330, 111, 41)) font = QtGui.QFont() font.setFamily("Arial") font.setPointSize(14) font.setBold(True) font.setWeight(75) self.Stop.setFont(font) self.Stop.setStyleSheet("") self.Stop.setObjectName("Stop") self.retranslateUi(Form) self.InitCard.clicked.connect(Form.init_cards) self.Stop.clicked.connect(Form.stop) QtCore.QMetaObject.connectSlotsByName(Form) def retranslateUi(self, Form): _translate = QtCore.QCoreApplication.translate Form.setWindowTitle(_translate("Form", "AI歡樂鬥地主--Dragon少年")) self.WinRate.setText(_translate("Form", "勝率:--%")) self.InitCard.setText(_translate("Form", "開始")) self.UserHandCards.setText(_translate("Form", "手牌")) self.LPlayedCard.setText(_translate("Form", "上傢出牌區域")) self.RPlayedCard.setText(_translate("Form", "下傢出牌區域")) self.PredictedCard.setText(_translate("Form", "AI出牌區域")) self.ThreeLandlordCards.setText(_translate("Form", "三張底牌")) self.Stop.setText(_translate("Form", "停止"))
2. 手牌和出牌數據識別
接下來需要所有撲克牌的模板圖片與遊戲屏幕特定區域的截圖進行對比,這樣才能獲取AI玩傢手牌、底牌、每一輪出牌、三者關系(地主、地主上傢、地主下傢)。
識別AI玩傢手牌及三張底牌:
我們可以截取遊戲屏幕,根據固定位置來識別當前AI玩傢的手牌和三張底牌。核心代碼如下:
# 牌檢測結果濾波 def cards_filter(self, location, distance): if len(location) == 0: return 0 locList = [location[0][0]] count = 1 for e in location: flag = 1 # “是新的”標志 for have in locList: if abs(e[0] - have) <= distance: flag = 0 break if flag: count += 1 locList.append(e[0]) return count # 獲取玩傢AI手牌 def find_my_cards(self, pos): user_hand_cards_real = "" img = pyautogui.screenshot(region=pos) for card in AllCards: result = pyautogui.locateAll(needleImage='pics/m' + card + '.png', haystackImage=img, confidence=self.MyConfidence) user_hand_cards_real += card[1] * self.cards_filter(list(result), self.MyFilter) return user_hand_cards_real # 獲取地主三張底牌 def find_three_landlord_cards(self, pos): three_landlord_cards_real = "" img = pyautogui.screenshot(region=pos) img = img.resize((349, 168)) for card in AllCards: result = pyautogui.locateAll(needleImage='pics/o' + card + '.png', haystackImage=img, confidence=self.ThreeLandlordCardsConfidence) three_landlord_cards_real += card[1] * self.cards_filter(list(result), self.OtherFilter) return three_landlord_cards_real
效果如下所示:
地主、地主上傢、地主下傢:
同理我們可以根據遊戲屏幕截圖,識別地主的圖標,確認地主角色。核心代碼如下:
# 查找地主角色 def find_landlord(self, landlord_flag_pos): for pos in landlord_flag_pos: result = pyautogui.locateOnScreen('pics/landlord_words.png', region=pos, confidence=self.LandlordFlagConfidence) if result is not None: return landlord_flag_pos.index(pos) return None
這樣我們就可以得到玩傢AI手牌,其他玩傢手牌(預測),地主三張底牌,三者角色關系,出牌順序。核心代碼如下:
# 坐標 self.MyHandCardsPos = (414, 804, 1041, 59) # AI玩傢截圖區域 self.LPlayedCardsPos = (530, 470, 380, 160) # 左側玩傢截圖區域 self.RPlayedCardsPos = (1010, 470, 380, 160) # 右側玩傢截圖區域 self.LandlordFlagPos = [(1320, 300, 110, 140), (320, 720, 110, 140), (500, 300, 110, 140)] # 地主標志截圖區域(右-我-左) self.ThreeLandlordCardsPos = (817, 36, 287, 136) # 地主底牌截圖區域,resize成349x168 def init_cards(self): # 玩傢手牌 self.user_hand_cards_real = "" self.user_hand_cards_env = [] # 其他玩傢出牌 self.other_played_cards_real = "" self.other_played_cards_env = [] # 其他玩傢手牌(整副牌減去玩傢手牌,後續再減掉歷史出牌) self.other_hand_cards = [] # 三張底牌 self.three_landlord_cards_real = "" self.three_landlord_cards_env = [] # 玩傢角色代碼:0-地主上傢, 1-地主, 2-地主下傢 self.user_position_code = None self.user_position = "" # 開局時三個玩傢的手牌 self.card_play_data_list = {} # 出牌順序:0-玩傢出牌, 1-玩傢下傢出牌, 2-玩傢上傢出牌 self.play_order = 0 self.env = None # 識別玩傢手牌 self.user_hand_cards_real = self.find_my_cards(self.MyHandCardsPos) self.UserHandCards.setText(self.user_hand_cards_real) self.user_hand_cards_env = [RealCard2EnvCard[c] for c in list(self.user_hand_cards_real)] # 識別三張底牌 self.three_landlord_cards_real = self.find_three_landlord_cards(self.ThreeLandlordCardsPos) self.ThreeLandlordCards.setText("底牌:" + self.three_landlord_cards_real) self.three_landlord_cards_env = [RealCard2EnvCard[c] for c in list(self.three_landlord_cards_real)] # 識別玩傢的角色 self.user_position_code = self.find_landlord(self.LandlordFlagPos) if self.user_position_code is None: items = ("地主上傢", "地主", "地主下傢") item, okPressed = QInputDialog.getItem(self, "選擇角色", "未識別到地主,請手動選擇角色:", items, 0, False) if okPressed and item: self.user_position_code = items.index(item) else: return self.user_position = ['landlord_up', 'landlord', 'landlord_down'][self.user_position_code] for player in self.Players: player.setStyleSheet('background-color: rgba(255, 0, 0, 0);') self.Players[self.user_position_code].setStyleSheet('background-color: rgba(255, 0, 0, 0.1);') # 整副牌減去玩傢手上的牌,就是其他人的手牌,再分配給另外兩個角色(如何分配對AI判斷沒有影響) for i in set(AllEnvCard): self.other_hand_cards.extend([i] * (AllEnvCard.count(i) - self.user_hand_cards_env.count(i))) self.card_play_data_list.update({ 'three_landlord_cards': self.three_landlord_cards_env, ['landlord_up', 'landlord', 'landlord_down'][(self.user_position_code + 0) % 3]: self.user_hand_cards_env, ['landlord_up', 'landlord', 'landlord_down'][(self.user_position_code + 1) % 3]: self.other_hand_cards[0:17] if (self.user_position_code + 1) % 3 != 1 else self.other_hand_cards[17:], ['landlord_up', 'landlord', 'landlord_down'][(self.user_position_code + 2) % 3]: self.other_hand_cards[0:17] if (self.user_position_code + 1) % 3 == 1 else self.other_hand_cards[17:] }) print(self.card_play_data_list) # 生成手牌結束,校驗手牌數量 if len(self.card_play_data_list["three_landlord_cards"]) != 3: QMessageBox.critical(self, "底牌識別出錯", "底牌必須是3張!", QMessageBox.Yes, QMessageBox.Yes) self.init_display() return if len(self.card_play_data_list["landlord_up"]) != 17 or \ len(self.card_play_data_list["landlord_down"]) != 17 or \ len(self.card_play_data_list["landlord"]) != 20: QMessageBox.critical(self, "手牌識別出錯", "初始手牌數目有誤", QMessageBox.Yes, QMessageBox.Yes) self.init_display() return # 得到出牌順序 self.play_order = 0 if self.user_position == "landlord" else 1 if self.user_position == "landlord_up" else 2
效果如下:
3. AI出牌方案輸出
下面我們就需要用到DouZero開源的AI鬥地主瞭。DouZero項目地址:https://github.com/kwai/DouZero。我們需要將該開源項目下載並導入項目中。
創建一個AI玩傢角色,初始化遊戲環境,加載模型,進行每輪的出牌判斷,控制一局遊戲流程的進行和結束。核心代碼如下:
# 創建一個代表玩傢的AI ai_players = [0, 0] ai_players[0] = self.user_position ai_players[1] = DeepAgent(self.user_position, self.card_play_model_path_dict[self.user_position]) # 初始化遊戲環境 self.env = GameEnv(ai_players) # 遊戲開始 self.start() def start(self): self.env.card_play_init(self.card_play_data_list) print("開始出牌\n") while not self.env.game_over: # 玩傢出牌時就通過智能體獲取action,否則通過識別獲取其他玩傢出牌 if self.play_order == 0: self.PredictedCard.setText("...") action_message = self.env.step(self.user_position) # 更新界面 self.UserHandCards.setText("手牌:" + str(''.join( [EnvCard2RealCard[c] for c in self.env.info_sets[self.user_position].player_hand_cards]))[::-1]) self.PredictedCard.setText(action_message["action"] if action_message["action"] else "不出") self.WinRate.setText("勝率:" + action_message["win_rate"]) print("\n手牌:", str(''.join( [EnvCard2RealCard[c] for c in self.env.info_sets[self.user_position].player_hand_cards]))) print("出牌:", action_message["action"] if action_message["action"] else "不出", ", 勝率:", action_message["win_rate"]) while self.have_white(self.RPlayedCardsPos) == 1 or \ pyautogui.locateOnScreen('pics/pass.png', region=self.RPlayedCardsPos, confidence=self.LandlordFlagConfidence): print("等待玩傢出牌") self.counter.restart() while self.counter.elapsed() < 100: QtWidgets.QApplication.processEvents(QEventLoop.AllEvents, 50) self.play_order = 1 elif self.play_order == 1: self.RPlayedCard.setText("...") pass_flag = None while self.have_white(self.RPlayedCardsPos) == 0 and \ not pyautogui.locateOnScreen('pics/pass.png', region=self.RPlayedCardsPos, confidence=self.LandlordFlagConfidence): print("等待下傢出牌") self.counter.restart() while self.counter.elapsed() < 500: QtWidgets.QApplication.processEvents(QEventLoop.AllEvents, 50) self.counter.restart() while self.counter.elapsed() < 500: QtWidgets.QApplication.processEvents(QEventLoop.AllEvents, 50) # 不出 pass_flag = pyautogui.locateOnScreen('pics/pass.png', region=self.RPlayedCardsPos, confidence=self.LandlordFlagConfidence) # 未找到"不出" if pass_flag is None: # 識別下傢出牌 self.other_played_cards_real = self.find_other_cards(self.RPlayedCardsPos) # 找到"不出" else: self.other_played_cards_real = "" print("\n下傢出牌:", self.other_played_cards_real) self.other_played_cards_env = [RealCard2EnvCard[c] for c in list(self.other_played_cards_real)] self.env.step(self.user_position, self.other_played_cards_env) # 更新界面 self.RPlayedCard.setText(self.other_played_cards_real if self.other_played_cards_real else "不出") self.play_order = 2 elif self.play_order == 2: self.LPlayedCard.setText("...") while self.have_white(self.LPlayedCardsPos) == 0 and \ not pyautogui.locateOnScreen('pics/pass.png', region=self.LPlayedCardsPos, confidence=self.LandlordFlagConfidence): print("等待上傢出牌") self.counter.restart() while self.counter.elapsed() < 500: QtWidgets.QApplication.processEvents(QEventLoop.AllEvents, 50) self.counter.restart() while self.counter.elapsed() < 500: QtWidgets.QApplication.processEvents(QEventLoop.AllEvents, 50) # 不出 pass_flag = pyautogui.locateOnScreen('pics/pass.png', region=self.LPlayedCardsPos, confidence=self.LandlordFlagConfidence) # 未找到"不出" if pass_flag is None: # 識別上傢出牌 self.other_played_cards_real = self.find_other_cards(self.LPlayedCardsPos) # 找到"不出" else: self.other_played_cards_real = "" print("\n上傢出牌:", self.other_played_cards_real) self.other_played_cards_env = [RealCard2EnvCard[c] for c in list(self.other_played_cards_real)] self.env.step(self.user_position, self.other_played_cards_env) self.play_order = 0 # 更新界面 self.LPlayedCard.setText(self.other_played_cards_real if self.other_played_cards_real else "不出") else: pass self.counter.restart() while self.counter.elapsed() < 100: QtWidgets.QApplication.processEvents(QEventLoop.AllEvents, 50) print("{}勝,本局結束!\n".format("農民" if self.env.winner == "farmer" else "地主")) QMessageBox.information(self, "本局結束", "{}勝!".format("農民" if self.env.winner == "farmer" else "地主"), QMessageBox.Yes, QMessageBox.Yes) self.env.reset() self.init_display()
到這裡,整個AI鬥地主出牌流程基本已經完成瞭。
三、出牌器用法
按照上述過程,這款AI出牌器已經制作完成瞭。後面應該如何使用呢?如果不想研究源碼,隻想使用這款AI鬥地主出牌器,驗證下效果,該怎麼配置環境運行這個AI出牌器呢?下面就開始介紹。
1. 環境配置
首先我們需要安裝這些第三方庫,配置相關環境,如下所示:
torch==1.9.0 GitPython==3.0.5 gitdb2==2.0.6 PyAutoGUI==0.9.50 PyQt5==5.13.0 PyQt5-sip==12.8.1 Pillow>=5.2.0 opencv-python rlcard
2. 坐標調整確認
我們可以打開遊戲界面,將遊戲窗口模式下最大化運行,把AI出牌器程序窗口需要移至右下角,不能遮擋手牌、地主標志、底牌、歷史出牌這些關鍵位置。
其次我們要確認屏幕截圖獲取的各個區域是否正確。如果有問題需要進行區域位置坐標調整。
# 坐標 self.MyHandCardsPos = (414, 804, 1041, 59) # 我的截圖區域 self.LPlayedCardsPos = (530, 470, 380, 160) # 左邊截圖區域 self.RPlayedCardsPos = (1010, 470, 380, 160) # 右邊截圖區域 self.LandlordFlagPos = [(1320, 300, 110, 140), (320, 720, 110, 140), (500, 300, 110, 140)] # 地主標志截圖區域(右-我-左) self.ThreeLandlordCardsPos = (817, 36, 287, 136) # 地主底牌截圖區域,resize成349x168
3. 運行測試
當所有環境配置完成,各區域坐標位置確認無誤之後,下面我們就可以直接運行程序,測試效果啦~
首先我們運行AI出牌器程序,打開遊戲界面,進入遊戲。當玩傢就位,手牌分發完畢,地主身份確認之後,我們就可以點擊畫面中開始按鈕,讓AI來幫助我們鬥地主瞭。
基於這個DouZero項目做一個“成熟”的AI,項目開源地址【https://github.com/tianqiraf/DouZero_For_HappyDouDiZhu – tianqiraf】。
今天我們就到這裡,明天繼續努力!
如果本篇博客有任何錯誤,請批評指教,不勝感激 !
到此這篇關於我用Python做個AI出牌器鬥地主把把贏的文章就介紹到這瞭,更多相關Python自動出牌器內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!
推薦閱讀:
- Python3中PyQt5簡單實現文件打開及保存
- 基於Python+Pyqt5開發一個應用程序
- pycharm配置QtDesigner的超詳細方法
- Python實現上課點名器系統
- 使用pyQT5顯示網頁的實現步驟