鬥地主AI出牌助手--線上呼叫鬥地主AI,實現自動鬥地主

2023-11-15 06:00:58

簡介

程式基於Python3.7開發的鬥地主AI出牌助手,目前支援歡樂鬥地主桌面版,微信版,也可以自己製作相應其他版本。
此出牌助手核心是識別出三位玩家出牌內容,呼叫基於DouZero封裝的API介面,輸入出牌內容,根據AI出牌方案,打出相應的牌。

執行效果

![](https://img2023.cnblogs.com/blog/129408/202311/129408-20231114235431212-40412513.gif)

核心功能

  • 手牌和位置識別
    1. 遊戲剛開始根據螢幕位置,截圖識別AI玩家手牌及三張底牌
    2. 根據玩家手牌判斷是否搶地主和加倍
    3. 確認三者之間的關係,識別地主和農民角色,確認隊友及對手關係
  • 識別每輪三位玩家出牌
    1. 根據提示按鈕,判斷當前的操作
    2. 識別三位玩家此輪出的牌,並記錄下來
  • AI出牌方案輸出
    1. 將出牌記錄按格式要求傳送給鬥地主AI
    2. 獲取鬥地主AI出牌方案,選取最高勝率方案
    3. 根據AI出牌方案,選擇對相應的牌,並出牌

素材準備

  • 區域定位,獲取座標值

  • 按鈕及牌面

核心程式碼

  • 根據指定視窗控制程式碼截圖

    def WindowShot(self):
        """
        根據視窗控制程式碼截圖
        返回: 圖片物件
        """
        windll.user32.SetProcessDPIAware()
        hwnd = self.Handle
        left, top, right, bottom = win32gui.GetClientRect(hwnd)
        w = right - left
        h = bottom - top
        hwnd_dc = win32gui.GetWindowDC(hwnd)
        mfc_dc = win32ui.CreateDCFromHandle(hwnd_dc)
        save_dc = mfc_dc.CreateCompatibleDC()
        bitmap = win32ui.CreateBitmap()
        bitmap.CreateCompatibleBitmap(mfc_dc, w, h)
        save_dc.SelectObject(bitmap)
        # If Special K is running, this number is 3. If not, 1
        result = windll.user32.PrintWindow(hwnd, save_dc.GetSafeHdc(), 3)
        bmpinfo = bitmap.GetInfo()
        bmpstr = bitmap.GetBitmapBits(True)
        img = np.frombuffer(bmpstr, dtype=np.uint8).reshape((bmpinfo["bmHeight"], bmpinfo["bmWidth"], 4))
        img = np.ascontiguousarray(img)[..., :-1]  # make image C_CONTIGUOUS and drop alpha channel
        #img = Image.frombuffer("RGB",(bmpinfo['bmWidth'], bmpinfo['bmHeight']),bmpstr, 'raw', 'BGRX', 0, 1)
        if not result:  # result should be 1
            win32gui.DeleteObject(bitmap.GetHandle())
            save_dc.DeleteDC()
            mfc_dc.DeleteDC()
            win32gui.ReleaseDC(hwnd, hwnd_dc)
            raise RuntimeError(f"Unable to acquire screenshot! Result: {result}")
        #cv2.imwrite('./imgs/print.png', img)
        #return img
        return cv2.cvtColor(img, cv2.COLOR_BGRA2BGR)
    
  • 在圖片中查詢第一個相似的圖片

    def LocateOnImage(self, template, image=None, region=None, confidence=0.9, grayscale=True):
        """
        在image中尋找template,返回第一個查詢到的範圍
    
        引數:
        template: 需要查詢的圖片,檔名或圖片物件
        image: 被查詢的圖片,檔名或圖片物件
        region: 查詢範圍
        confidence: 置信度
        grayscale: 是否為灰度圖
    
        返回值:
        查詢到的圖片範圍
        """
        if image is None:
            image = self.WindowShot()
        return pyautogui.locate(template, image, region=region, confidence=confidence, grayscale=grayscale)
    
  • 在圖片中查詢所有相似的圖片

    def LocateAllOnImage(self, template, image=None, region=None, confidence=0.9, grayscale=True):
        """
        在image中尋找template,返回第一個查詢到的範圍
    
        引數:
        template: 需要查詢的圖片,檔名或圖片物件
        image: 被查詢的圖片,檔名或圖片物件
        region: 查詢範圍
        confidence: 置信度
        grayscale: 是否為灰度圖
    
        返回值:
        查詢到的圖片範圍
        """
        if image is None:
            image = self.WindowShot()
        return pyautogui.locateAll(template, image, region=region, confidence=confidence, grayscale=grayscale)
    
  • 識別手牌

    def GetCards(self, image, player):
        hand_cards = []
        cards = ""
        start_x = 0
        width = player["width"]
        for card in self.AllCardsNC:
            confidence = player["confidence"] or 0.85
            grayscale = True
            if(card in ('D', 'X')):
                confidence = 0.85
                grayscale = False
    
            card_key = player["prefix"] + card
            matches = self.LocateAllOnImageName(card_key, image, player["region"], confidence, grayscale)
    
            if len(matches) > 0:
                sorted_matches = sorted(matches, key=lambda match: match[0])
                #print(target_position)
                #print(mark)
    
                if(card != 'X'):
                    start_x = 0
    
                for match in sorted_matches:
                    if(start_x == 0 or start_x + player["width"] < match[0]):
                        #大小王容易判斷錯誤,需要再判斷一下顏色值
                        if(card == 'D'):
                            #match = sorted_matches[0]
                            y,x,h,w = match
                            cropped_image = image[x:x+w, y:y+h]
                            #colors = cv2.mean(cropped_image)
                            mean, stddev = cv2.meanStdDev(cropped_image)
                            #print("均值:", mean)
                            #print("差值:", stddev)
                            if(stddev[2][0] > 30):
                                continue
                        start_x = match[0]
                        width = match[2]
                        hand_cards.append({card:match})
                        cards += card
        
        return cards, hand_cards
    
  • 將出牌記錄傳送給AI,進行預測

    def GetCardsForPredict(self):
        #將出牌記錄傳送給AI,進行預測
        resultStr = self.PostPredict()
        #獲取最高勝率的出牌
        last_move_cards = self.GetPredictWinRates(resultStr)
        return last_move_cards
    
  • 出牌

    def PlayedCards(self, cards, image=None):
        if image is not None:
            window_shot_image = image
        else:
            window_shot_image = self.WindowShot()
        player_self_cards, range = self.GetCards(window_shot_image, self.Players["PlayerSelf"])
        select_index = []
        select_cards = []
        #將cards中的字元順序反轉
        play_cards = cards[::-1]
        cards = play_cards
    
        for c in cards:
            index = -1
            for card in player_self_cards:
                index += 1
                
                if c == card and index not in select_index:
                    select_index.append(index)
                    select_cards.append(c)
                    #print(range[index])
                    cards = cards.replace(c, '', 1)
                    card_range = range[index][c]
                    break
    
        cards = play_cards
        index = -1
        #window_shot_image = self.WindowShot()
        for n in select_index:
            window_shot_image = self.WindowShot()
            player_self_cards, range = self.GetCards(window_shot_image, self.Players["PlayerSelf"])
            if(len(player_self_cards) > n):
                #player_self_cards = player_self_cards
                index += 1
                card_range = range[n][cards[index]]
                #如果要出的牌還未選擇完畢,則點選
                if(card_range[1] > self.CardTop):
                    self.LeftClick(card_range)
            
            time.sleep(0.2)
    
        return self.GetSelectCards(play_cards)
    
  • DouZero 的請求結構體

    Predict = {
        "bomb_num":0,#炸彈數量
        "card_play_action_seq":'',#歷史出牌動作序列,用逗號分隔
        "last_move_landlord":'',#地主最後出的牌
        "last_move_landlord_down":'',#地主下家最後出的牌
        "last_move_landlord_up":'',#地主上家最後出的牌
        "num_cards_left_landlord":20,#地主手牌剩餘數量
        "num_cards_left_landlord_down":17,#地主下家手牌剩餘數量
        "num_cards_left_landlord_up":17,#地主上家手牌剩餘數量
        "other_hand_cards":'',#還剩餘的牌
        "played_cards_landlord":'',#地主所有出的牌
        "played_cards_landlord_down":'',#地主下家所有出的牌
        "played_cards_landlord_up":'',#地主上家所有出的牌
        "player_hand_cards":'',#玩家手中的牌
        "player_position":0,	#-當前玩家的位置序號 0 地主,1 地主下家,2 地主上家
        "three_landlord_cards":''#三張底牌
    }
    

可用的 DouZero 後端地址

免責宣告

本程式僅供娛樂和學習使用,不得用於任何非法用途

參考專案