简介
程序基于Python3.7开发的斗地主AI出牌助手,目前支持欢乐斗地主桌面版,微信版,也可以自己制作相应其他版本。
此出牌助手核心是识别出三位玩家出牌内容,调用基于DouZero封装的API接口,输入出牌内容,根据AI出牌方案,打出相应的牌。
运行效果
![](https://img2023.cnblogs.com/blog/129408/202311/129408-20231114235431212-40412513.gif)
核心功能
- 手牌和位置识别
- 游戏刚开始根据屏幕位置,截图识别AI玩家手牌及三张底牌
- 根据玩家手牌判断是否抢地主和加倍
- 确认三者之间的关系,识别地主和农民角色,确认队友及对手关系
- 识别每轮三位玩家出牌
- 根据提示按钮,判断当前的操作
- 识别三位玩家此轮出的牌,并记录下来
- AI出牌方案输出
- 将出牌记录按格式要求发送给斗地主AI
- 获取斗地主AI出牌方案,选取最高胜率方案
- 根据AI出牌方案,选择对相应的牌,并出牌
素材准备
-
区域定位,获取坐标值
-
按钮及牌面
核心代码
-
根据指定窗口句柄截图
pythondef 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)
-
在图片中查找第一个相似的图片
pythondef 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)
-
在图片中查找所有相似的图片
pythondef 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)
-
识别手牌
pythondef 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,进行预测
pythondef GetCardsForPredict(self): #将出牌记录发送给AI,进行预测 resultStr = self.PostPredict() #获取最高胜率的出牌 last_move_cards = self.GetPredictWinRates(resultStr) return last_move_cards
-
出牌
pythondef 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 的请求结构体
pythonPredict = { "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 后端地址
- 快手的 DouZero
https://douzero.org:5000/predict - 网易的 PerfectDou
https://outer-perfectdou-demo-gzailab.nie.netease.com:10074/predict - 也可根据 RLCard-Showdown 自行搭建本地运行的后端服务
免责声明
本程序仅供娱乐和学习使用,不得用于任何非法用途
参考项目
- DouZero:https://github.com/kwai/DouZero
- PerfectDou:https://github.com/Netease-Games-AI-Lab-Guangzhou/PerfectDou
- rlcard-showdown:https://github.com/datamllab/rlcard-showdown
- DouZero_For_HLDDZ_FullAuto: https://github.com/Vincentzyx/DouZero_For_HLDDZ_FullAuto