当前位置 博文首页 > Yunlord的博客:斗地主就用AI出牌器,让我们一起用Python制作,

    Yunlord的博客:斗地主就用AI出牌器,让我们一起用Python制作,

    作者:[db:作者] 时间:2021-09-11 10:50

    前言

    最近在CSDN上看到一个很火的开源项目《斗地主老是输?一起用Python做个AI出牌器!》,但是在扑克牌牌型识别的时候由于使用的是特定模板匹配,所以只适用于固定分辨率的某游戏大厅的斗地主,而无法适用于大部分的斗地主,尤其是某手游助手的斗地主。于是打算通过YOLOv5算法进行扑克牌目标检测与识别,从而实现AI出牌

    那么现在我们就一起来学习自己制作一个基于Dragon少年版本的斗地主出牌器,看看AI是如何来帮助我们斗地主,赢取海量欢乐豆,走上豆豆巅峰的吧!

    首先一起来看看AI斗地主出牌器的效果:

    出牌器实现效果:?

    可以看到,该游戏平台自带的推荐牌型是一对八,而我们这款出牌器推荐出一对A,可以看到效果还是比较明显的。

    下面,我们开始介绍这个AI出牌器的制作过程。

    一、核心功能设计

    这款出牌器是基于Dragon少年开发的,核心是DouZero,利用训练好的AI模型给出最优出牌方案。

    所以我们首先要让这个出牌器确认一个AI出牌角色,代表我们玩家自己。

    其次我们只要给这个AI出牌器输入玩家手牌和三张底牌。确认好地主和农民的各个角色,告诉它三个人对应的关系,这样就可以确定队友和对手。我们还要识别每一轮其他两人的出牌并将其输入到出牌器,这样出牌器就可以根据出牌数据,及时提供给我们最优出牌决策,带领我们取得胜利。

    那么如何获取三者之间的关系呢?谁是地主?谁是农民?是自己一人作战还是农民合作?自己玩家的手牌是什么?三张底牌是什么?其他人出牌情况是什么?这些也都需要在开局后逐步确认。

    拆解需求后,整理出核心功能如下:

    • UI设计排版布局
      • 显示三张底牌
      • 显示AI角色出牌数据区域,上家出牌数据区域,下家出牌数据区域,本局胜率区域
      • AI玩家手牌区域
      • AI出牌器开始停止
    • 手牌和出牌数据识别
      • 游戏刚开始根据屏幕位置,截图识别AI玩家手牌及三张底牌
      • 确认三者之间的关系,识别地主和农民角色,确认队友及对手关系
      • 识别每轮三位玩家依次出了什么牌,刷新显示对应区域
    • AI出牌方案输出
      • 加载训练好的AI模型,初始化游戏环境
      • 每轮出牌判断,根据上家出牌数据给出最优出牌决策
      • 自动刷新玩家剩余手牌和本局胜率预测

    二、实现步骤

    1. UI设计排版布局

    由于我们是基于Dragon少年开发的AI出牌器进行开发,所以我们不需要重新制作界面,只需要理解该界面设计,并将通用的扑克牌牌型识别加入到手牌和出牌数据识别就可以了。

    其使用的是pyqt5进行简单的UI布局设计,核心代码如下:

    def retranslateUi(self, Form):
        _translate = QtCore.QCoreApplication.translate
        Form.setWindowTitle(_translate("Form", "AI欢乐斗地主"))
        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玩家手牌及三张底牌:

    我们可以截取游戏屏幕的固定位置,送入训练好的YOLOv5网络,来识别当前AI玩家的手牌和三张底牌。核心代码如下:

    def find_three_landlord_cards(self, pos):
            three_landlord_cards_real = ""
            img = pyautogui.screenshot(region=pos)
            three_landlord_cards_real=detect_cards(img)
            return three_landlord_cards_real
    
    def find_my_cards(self, pos):
            user_hand_cards_real = ""
            img = pyautogui.screenshot(region=pos) 
            # img2 = color.rgb2gray(img)
            user_hand_cards_real=detect_cards(img)
            return user_hand_cards_real
    def detect_cards(img):
        path="datas\cards.png"
        img.save(path)
        raw_cards=detect(source=path)
        replace_cards=[replace_num[i] if i in replace_num else i for i in raw_cards]
        list_cards = sorted(replace_cards, key=lambda x: ranks_value[x])
        cards=("".join(list_cards))
        return cards
    
    def detect()
        # Initialize
        set_logging()
        # device = select_device(device)
        device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")#若有gpu可用则用gpu
        # half &= device.type != 'cpu'  # half precision only supported on CUDA
        w = weights[0] if isinstance(weights, list) else weights
        classify, pt, onnx = False, w.endswith('.pt'), w.endswith('.onnx')  # inference type
        stride, names = 64, [f'class{i}' for i in range(1000)]  # assign defaults
        if pt:
            model = attempt_load(weights, map_location=device)  # load FP32 model
            stride = int(model.stride.max())  # model stride
            names = model.module.names if hasattr(model, 'module') else model.names  # get class names
            if half:
                model.half()  # to FP16
            if classify:  # second-stage classifier
                modelc = load_classifier(name='resnet50', n=2)  # initialize
                modelc.load_state_dict(torch.load('resnet50.pt', map_location=device)['model']).to(device).eval()
        elif onnx:
            check_requirements(('onnx', 'onnxruntime'))
            import onnxruntime
            session = onnxruntime.InferenceSession(w, None)
        dataset = LoadImages(source, img_size=imgsz, stride=stride)
        bs = 1  # batch_size
        vid_path, vid_writer = [None] * bs, [None] * bs
        t0 = time.time()
    
        imgsz = check_img_size(imgsz, s=stride)  # check image size
        for path, img, im0s, vid_cap in dataset:
            if pt:
                img = torch.from_numpy(img).to(device)
                img = img.half() if half else img.float()  # uint8 to fp16/32
            elif onnx:
                img = img.astype('float32')
            img /= 255.0  # 0 - 255 to 0.0 - 1.0
            if len(img.shape) == 3:
                img = img[None]  # expand for batch dim
            # Inference
            t1 = time_sync()
            if pt:
                pred = model(img, augment=augment, visualize=visualize)[0]
            elif onnx:
                pred = torch.tensor(session.run([session.get_outputs()[0].name], {session.get_inputs()[0].name: img}))
            # NMS
            pred = non_max_suppression(pred, conf_thres, iou_thres, classes, agnostic_nms, max_det=max_det)
            t2 = time_sync()
            # Second-stage classifier (optional)
            if classify:
                pred = apply_classifier(pred, modelc, img, im0s)
            # Process predictions
            for i, det in enumerate(pred):  # detections per image
                p, s, im0, frame = path, '', im0s.copy(), getattr(dataset, 'frame', 0)
                p = Path(p)  # to Path
                gn = torch.tensor(im0.shape)[[1, 0, 1, 0]]  # normalization gain whwh
                imc = im0.copy() if save_crop else im0  # for save_crop
                if len(det):
                    # Rescale boxes from img_size to im0 size
                    det[:, :4] = scale_coords(img.shape[2:], det[:, :4], im0.shape).round()
                    
                    lists=[]
                    # Print results
                    for c in det[:, -1].unique():
                        n = (det[:, -1] == c).sum()  # detections per class
                        for i in range(n):
                            lists.append(names[int(c)])
            return lists

    效果如下所示:

    ?

    地主、地主上家、地主下家:

    同理我们可以根据游戏屏幕截图,识别地主的图标,确认地主角色。核心代码如下:、

    # 查找地主角色
    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手牌,其他玩家手牌(预测),地主三张底牌,三者角色关系,出牌顺序。

    3. AI出牌方案输出

    这一部分,我们依然是基于Dragon少年所用到的DouZero开源的AI斗地主了。DouZero项目地https://github.com/kwai/DouZero。我们需要将该开源项目下载并导入项目中。
    创建一个AI玩家角色,初始化游戏环境,加载模型,进行每轮的出牌判断,控制一局游戏流程的进行和结束。

    但在这过程中需要判断斗地主中其他二人的出牌情况,包括等待不出或者出牌。所以在这一阶段,我们训练了ResNet50网络,将对应区域的图像送入网络中进行三种状态的判断。如果是出牌的话,那么再将对应区域的图像送入YOLOv5网络进行牌型识别。

    出牌、不出、等待状态:

    同理我们可以根据游戏屏幕截图,识别其他人出牌区域,判断其出牌状态 。核心代码如下:

    labels=['等待','出牌','不出']
    device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")#若有gpu可用则用gpu
    model = models.resnet50(pretrained=False)
    fc_inputs = model.fc.in_features
    model.fc = nn.Sequential(
        nn.Linear(fc_inputs, 256),
        nn.ReLU(),
        nn.Dropout(0.4),
        nn.Linear(256, config.NUM_CLASSES),
        nn.LogSoftmax(dim=1)
    )
    pthfile=config.TRAINED_BEST_MODEL
    checkpoint = torch.load(pthfile)
    model.load_state_dict(checkpoint['model'])
    # optimizer.load_state_dict(checkpoint['optimizer'])
    start_epoch = checkpoint['epoch']
    # test(model, test_load)
    model.to(device).eval()
    def detect_pass(pos):
        
        img = pyautogui.screenshot(region=pos) 
        # path="datas\state.png"
        time =datetime.datetime.now().strftime(TIMESTAMP)
        path="datas\states\state"+'_'+time+'.png'
        img.save(path)
        # path="datas/states/state_20210807160852.png"
        src = cv2.imread(path) # aeroplane.jpg
        image = cv2.resize(src, (224, 224))
        image = np.float32(image) / 255.0
        image[:,:,] -= (np.float32(0.485), np.float32(0.456), np.float32(0.406))
        image[:,:,] /= (np.float32(0.229), np.float32(0.224), np.float32(0.225))
        image = image.transpose((2, 0, 1))
        input_x = torch.from_numpy(image).unsqueeze(0).to(device)
        pred = model(input_x)
        pred_index = torch.argmax(pred, 1).cpu().detach().numpy()
        pred_index=int(pred_index)
        print(pred_index)
        return pred_index

    效果如下所示:?

    到这里,整个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 = (110, 1310, 3620, 600) # 我的截图区域
    self.LPlayedCardsPos = (880, 340, 1020, 545)  # 左边截图区域
    self.RPlayedCardsPos = (1940, 340, 1020, 545)  # 右边截图区域
    self.LandlordFlagPos = [ (105, 545, 240, 260), (640, 1190, 240, 260),  (3475, 545, 240, 260)]  # 地主标志截图区域(右-我-左)
    self.ThreeLandlordCardsPos = (1770, 50, 300, 170)     # 地主底牌截图区域,resize成349x168

    调整无误后,效果如下:


    3. 运行测试

    当所有环境配置完成,各区域坐标位置确认无误之后,下面我们就可以直接运行程序,测试效果啦~

    首先我们运行AI出牌器程序,打开欢乐斗地主游戏界面,进入游戏。当玩家就位,手牌分发完毕,地主身份确认之后,我们就可以点击画面中开始按钮,让AI来帮助我们斗地主了。

    下面可以一起来看看这款AI出牌器的实验效果喔,看看AI是如何带领农民打倒地主,取得胜利的!
    ?

    斗地主就用AI出牌器!

    cs