斗地主老是输?一起用Python做个AI出牌器,欢乐豆蹭蹭涨!

Posted Dragon少年

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了斗地主老是输?一起用Python做个AI出牌器,欢乐豆蹭蹭涨!相关的知识,希望对你有一定的参考价值。

前言

最近在网上看到一个有意思的开源项目,快手团队开发的开源AI斗地主——DouZero。今天我们就一起来学习制作一个基于DouZero的欢乐斗地主出牌器,看看AI是如何来帮助我们斗地主,赢欢乐豆,实现财富自由的吧!

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

下面,我们开始介绍这个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<

以上是关于斗地主老是输?一起用Python做个AI出牌器,欢乐豆蹭蹭涨!的主要内容,如果未能解决你的问题,请参考以下文章

斗地主老是输?一起用Python做个自动出牌器,欢乐豆蹭蹭涨!

斗地主AI出牌

python调用百度语音(语音识别-斗地主语音记牌器)

用python实现的简易记牌器的demo

开发h5斗地主大厅算法——第十二章の主动出牌

简陋的斗地主,js实现