用python写一个有AI的斗地主游戏(二)——简述后端代码和思路

源码请看我的Github页面
这是我一个课程的学术项目,请不要抄袭,引用时请注明出处。
本专栏系列旨在帮助小白从零开始开发一个项目,同时分享自己写代码时的感想。
请大佬们为我的拙见留情,有不规范之处烦请多多包涵!

开场白

在上一篇博客里,已经介绍了开始前的一些准备。这篇博客讲简要介绍游戏开发中后端代码结构的思路。当然,也是博主自己琢磨的,有遗漏或不足之处请指教!

逻辑

游戏的实现大概可以分为两层(就像网页开发一样):前端和后端。后端负责储存和管理游戏的逻辑(比如现在是谁的回合,谁手里都有什么牌,我能不能出这个对子等等),而前端负责与用户的交互(比如在窗口里显示自己的手牌,获取输入并更新游戏数据等等)。用类来表示前端和后端有种种好处,其中对新手最有帮助的就是能够以更加“人类”的方式来组织和看待程序内容。比如,用类的你可以这样想:“我想要游戏里玩家1打出这些牌然后玩家2出牌,那么我可以调用Game这个游戏逻辑类里的makePlay这个出牌的函数。”以这种方式思考和解决问题需要一些时间磨合,但是对长远的开发习惯和效率都有极大好处。以下是游戏后端大致逻辑:

步骤 人类逻辑/游戏流程 程序逻辑
1 三名玩家进入房间/上桌 初始化游戏类,包括玩家名称,场上顺序,等等
2 洗牌并给每个玩家发初始的17张牌(留出3张地主牌) 用游戏类给每个玩家类添加手牌,并在游戏类里维持地主牌的记录
3 叫地主 按照顺序给每个玩家叫地主的机会,安排出牌顺序、给地主牌、改变玩家身份
4 按照顺序和规则出牌或过牌 按照出牌顺序,玩家出牌时调用相关函数进行可行性检测和模拟出牌
5 如果有人出完了牌,结束游戏 在每一次出牌后检查游戏是否结束,没结束的话继续上一步

这些游戏逻辑将在前端被构造和模拟,这里的后端代码只是提供实现这些逻辑的基本工具。还有AI出牌的部分也会放到前端部分详细讲解。

后端代码和思路

斗地主的后端逻辑和井字棋比起来还是有一定复杂度的。博主设计的游戏后端逻辑主要放到了gameEngine.py里,其中各包含更加细分的类。思来想去,我们需要实现以下功能:

'''
GameEngine.py描述
Game Class: 一个用来表达游戏状态的类(保存所有游戏信息)
    __init__: 用来构造类,需要游玩的三个玩家的id,初始化类
    sortHelper: 目前不重要,用来帮助排序卡的大小(因为卡的表达方式机器不易读)
    shuffleDeck: 创建并打乱初始排队
    dealCard: 将每个玩家17张卡随机分发,并选择3张随机地主牌
    chooseLandlord: 把一个玩家的身份变为地主
    assignPlayOrder: 根据玩家身份调整出牌顺序
    whichPattern: 返回选择牌的种类(比如单张,对子,顺子,三代二等等)以及它们的大小
    isValidPlay: 返回选择的牌是否为可以按照规则打的牌
    makePlay: 模拟现实中的玩家出牌,即出牌后轮到下一家
    checkWin: 检查游戏状态(返回0代表游戏继续,1代表地主获胜,2代表农民获胜)
    createAI: 用AI玩家代替人类玩家
    AIMakePlay: AI出牌
player Class: 一个用来表达玩家状态的类(包括玩家名字/id,手牌,和是否为地主)
    __init__: 用玩家id构造类
    playCard: 从玩家手牌中移除选择的卡
AI Class:
	__init__: 用AI玩家id构造类,继承player类的方法
	getAllMoves: AI根据手牌生成所有可以出的牌
'''

接下来我们就一个一个实现了。

gameEngine.py

首先是Game类。构建它的时候用到了以下内容:

class Game:
    def __init__(self, p1id, p2id, p3id):
        # 用来生成和表达卡的一些常量
        self.colors = ['heart', 'spade', 'diamond', 'club'] # 扑克牌的四个色
        self.nums = ['A', '2', '3', '4', '5', '6',
                     '7', '8', '9', '10', 'J', 'Q', 'K'] # 扑克牌的数字大小
        self.specials = ['X', 'D'] # 小王和大王
        self.cardOrder = {
    
    '3': 1, '4': 2, '5': 3, '6': 4, '7': 5, '8': 6, '9': 7, '10': 8,
                          'J': 9, 'Q': 10, 'K': 11, 'A': 12, '2': 13, 'X': 14, 'D': 15} # 用来比较牌大小的字典
        # 创建游戏内的玩家(player类)和一个用玩家名称指向player对象的字典
        self.p1 = player(p1id)
        self.p2 = player(p2id)
        self.p3 = player(p3id)
        self.playerDict = {
    
    p1id: self.p1, p2id: self.p2, p3id: self.p3}
        # 一些重要的游戏状态
        self.currentPlayer = '' # 当前玩家id
        self.prevPlayer = '' # 上一个玩家id
        self.prevPlay = ['', []] # 上一次出牌,包括出牌者id和出的牌
        self.playOrder = [] # 出牌顺序,包含三个玩家的id
        self.landLordCards = [] # 地主牌

在我们完成构造函数后,就要开始写功能性函数了。由于部分比较多,请小伙伴们挑选自己感兴趣的部分阅读,比如“欸这个whichPattern好像有点技术含量,了解了解”。具体如下:

	# 帮助给卡排序的函数,输入卡的名称如‘club 2’,根据之前定义的self.cardOrder返回它的虚拟大小/排名
    def sortHelper(self, x):
        if x[-1] == '0': # 如果卡以0结尾,那么它是个10
            return self.cardOrder['10']
        return self.cardOrder[x[-1]]

    # 创建牌堆并洗牌,放到self.deck即游戏牌堆里
    def shuffleDeck(self):
        self.deck = []
        for i in self.colors: # 生成所有排列组合
            for j in self.nums:
                self.deck.append(i+' '+j) # “花色 数字”
        self.deck.append(self.specials[0]) # 放入小王和大王
        self.deck.append(self.specials[1])
        random.shuffle(self.deck) # 打乱列表元素顺序(洗牌),别忘了先import random

    # 发每个玩家初始的17张手牌和3张地主牌
    def dealCard(self):
    	# 这里介绍下,如果牌堆是完全随机的,发牌顺序对游戏公平性和体验不会有影响
    	# 我们自己洗牌一般都不会洗的太散,所以要一个人一个人发避免炸弹太多
        self.landLordCards = [] # 发地主牌
        for i in range(3):
            choice = random.choice(self.deck) # 随机选一张
            self.landLordCards.append(choice) # 放到地主牌里
            self.deck.remove(choice) # 从牌堆里移除它
        self.p1Card = [] # 发玩家1的牌
        for i in range(17):
            choice = random.choice(self.deck) # 随机选一张
            self.p1Card.append(choice) # 放到该玩家手牌中
            self.deck.remove(choice) # 从牌堆里移除它
        self.p2Card = [] # 发玩家2的牌
        for i in range(17):
            choice = random.choice(self.deck)
            self.p2Card.append(choice)
            self.deck.remove(choice)
        self.p3Card = [] # 发玩家3的牌
        for i in range(17):
            choice = random.choice(self.deck)
            self.p3Card.append(choice)
            self.deck.remove(choice)
        self.p1.cards = self.p1Card # 把每个玩家的牌放到他们的类里
        self.p2.cards = self.p2Card
        self.p3.cards = self.p3Card
        self.p1.cards.sort(key=lambda x: self.sortHelper(x)) # 给他们排序,模拟了玩的时候自己理牌
        self.p2.cards.sort(key=lambda x: self.sortHelper(x))
        self.p3.cards.sort(key=lambda x: self.sortHelper(x))

    # 根据输入的玩家名称/id,选择地主身份
    def chooseLandlord(self, name):
        self.playerDict[name].identity = 'p' # 把这个玩家身份改为p,农民为s
        for card in self.landLordCards: # 把地主牌放到这个玩家手牌里
            self.playerDict[name].cards.append(card)
        self.playerDict[name].cards.sort(key=lambda x: self.sortHelper(x)) # 给手牌排序

    # 出牌顺序
    def assignPlayOrder(self):
        if self.p1.identity == 'p': # 地主第一个出,剩下两个玩家随机打乱
            self.playOrder = [self.p2.name, self.p3.name]
            random.shuffle(self.playOrder)
            self.playOrder.insert(0, self.p1.name)
        elif self.p2.identity == 'p':
            self.playOrder = [self.p1.name, self.p3.name]
            random.shuffle(self.playOrder)
            self.playOrder.insert(0, self.p2.name)
        elif self.p3.identity == 'p':
            self.playOrder = [self.p1.name, self.p2.name]
            random.shuffle(self.playOrder)
            self.playOrder.insert(0, self.p3.name)
        else: # 还没选地主的情况,三个玩家顺序随机(等待叫地主)
            self.playOrder = [self.p1.name, self.p2.name, self.p3.name]
            random.shuffle(self.playOrder)
        self.currentPlayer = self.playOrder[0] # 当前玩家为第一个玩家

    # 获取选择卡的种类和大小
    def whichPattern(self, selectedCards):
        cardValues = []
        for i in selectedCards: # 把所有选择的卡的值放到列表里
            if i[-1] == '0':  # 如果0结尾那么它是个10
                cardValues.append(self.cardOrder['10'])
            else:
                cardValues.append(self.cardOrder[i[-1]])
        # 这里获取卡的种类和大小。utils是我写的另一个文件,借鉴了DouZero的utils库
        # DouZero的项目很有趣,AI打斗地主很智能,感兴趣的话请看https://github.com/kwai/DouZero
        # 我代码里的utils.py里有着各种各样的工具,为了方便管理就都放到了一个文件里,后面会展开介绍
        # get_move_type(cardValues)输入选择的卡(的值),返回整数代表的卡的种类和卡的大小
        pattern = utils.get_move_type(cardValues)
        return pattern # 返回一个字典{type: 1, value: 5},有卡的种类和大小

    # 检查是否为可以打的牌
    def isValidPlay(self, selected):
        selectedCards = sorted(selected, key=lambda x: self.sortHelper(x)) # 先排序,方便比较
        if self.prevPlay[0] == self.currentPlayer:
            return True # 即上两家都过牌,又轮到自己了,出啥都行
        pattern1 = self.whichPattern(self.prevPlay[1]) # 上一个出牌的种类和大小
        pattern2 = self.whichPattern(selectedCards) # 当前选择牌的种类和大小
        if pattern2['type'] == 15 or pattern1['type'] == 5:
            return False  # 即上家出牌是王炸或者当前选择牌非法
        elif pattern2['type'] == 5 or pattern1['type'] == 0:
            return True  # 即当前出牌是王炸或者前一家过牌
        else:
            if pattern1['type'] == pattern2['type'] and\
                    pattern1['rank'] < pattern2['rank']:
                try: # 看看是不是三带一
                	if pattern1['len'] == pattern2['len']:
                		return True
                	return False
                except:
                	return True # 如果种类一样并且选择的要更大,可以出
            else:
                return False

    # 模拟游戏出牌
    def makePlay(self, selectedCards):
        if selectedCards == [] and self.prevPlay != []:
            pass # 过牌,什么都不做
        else: # 有牌打的话那就打牌
            self.playerDict[self.currentPlayer].playCard(selectedCards) # 当前玩家出牌
            self.prevPlay = [self.currentPlayer, selectedCards] # 记录本次出牌
        self.prevPlayer = self.currentPlayer # 顺序轮换
        playerIndex = self.playOrder.index(self.currentPlayer) # 当前玩家位置
        if playerIndex == 2: # 如果是列表里最后一个,循环到第一个
            self.currentPlayer = self.playOrder[0]
        else: # 否则转到下一个玩家
            self.currentPlayer = self.playOrder[playerIndex+1]

    # 检查游戏是否结束
    def checkWin(self):
        if self.playerDict[self.prevPlayer].cards == []: # 游戏在当前玩家出完牌后没有手牌时结束
            if self.playerDict[self.prevPlayer].identity == 'p':
                return 1  # 地主赢
            else:
                return 2  # 农民赢
        else:
            return 0  # 游戏还在进行

Game类里还有个很重要的部分,那就是用AI出牌。具体如下:

    # 创建AI玩家
    def createAI(self, name2, name3):
        self.p2 = AI(name2) # AI是player的子类
        self.p3 = AI(name3)
        self.playerDict[name2] = self.p2 # 把对应的玩家改成AI
        self.playerDict[name3] = self.p3

    # AI出牌。这里的逻辑很简单,即从能出的牌里随便选一个出。之后可以进行提升
    def AIMakePlay(self, name, chosenLandLord):
        AIplayer = self.playerDict[name]
        if chosenLandLord: # 如果叫了地主正在出牌
            moves = AIplayer.getAllMoves() # 生成可以出的牌
            possibleMoves = [] # 根据场上情况实际可以打的牌牌
            for move in moves:
                realcards = []
                for card in move: # 先把生成的牌(无花色,只有大小)变成实际的牌(有花色和大小)
                    for hand in AIplayer.cards: 
                        if hand[-1] == card[-1] and hand not in realcards:
                            realcards.append(hand)
                if self.isValidPlay(realcards): # 如果可以打,加到选项里
                    possibleMoves.append(realcards)
            possibleMoves.append([]) # 即允许过牌
            move = random.choice(possibleMoves) # 随机选一个出牌方式出牌
            self.makePlay(move)
        else: # 还没人叫地主的话就自己叫地主
            self.chooseLandlord(name)
            self.assignPlayOrder()

Game类里的时候我们用到了player对象来表达玩家的相关信息,这里我们定义下player类:

class player:
    def __init__(self, name):
        self.name = name # 玩家名称/id
        self.identity = 's' # s代表农民,p代表地主
        self.cards = [] # 初始手牌

    # 出牌/从手牌中移除某些牌
    def playCard(self, selectedCards):
        for i in selectedCards:
            self.cards.remove(i)

还有刚用到的AI玩家类。具体如下:

class AI(player): # 这里用到了继承
    def __init__(self,name):
        super().__init__(name) # 继承了player类的构造函数
    # 因为用到了继承,所以AI也继承了player类的playCard函数,所以无需再定义
    
    # 生成所有可以出的牌
    def getAllMoves(self):
        envmoves = utils.MovesGener(self.cards).gen_moves() # 这里又用到了utils.py,后面会介绍
        # MovesGener是一个出牌生成器,gen_moves()生成可以出的牌
        EnvCard2RealCard = {
    
    3: '3', 4: '4', 5: '5', 6: '6', 7: '7',
                            8: '8', 9: '9', 10: '10', 11: 'J', 12: 'Q',
                            13: 'K', 14: 'A', 17: '2', 20: 'X', 30: 'D'}
        realmoves = []
        for move in envmoves:
            realmove = [] # 把生成的虚拟数字变回扑克无花色数字
            for card in move:
                realmove.append(EnvCard2RealCard[card])
            realmoves.append(realmove)
        return realmoves

到这里,游戏引擎/后端逻辑最主要的部分就写好了。

utils.py

刚才提到了很多工具都来自utils.py。以下是它的代码以及介绍:

'''
重要声明:以下打星号(*)的函数均借鉴于DouZero,请感兴趣的同学前往https://github.com/kwai/DouZero查看他们的代码
utils.py描述
    is_contiuous_seq: 输入出的牌,返回它是否连续(类似顺子,但没限制长度) *
    get_move_type: 输入出的牌,返回它的种类(比如顺子,单张,三带一)和它的大小(比如三带一的大小就是那三张的大小) *
    select: 输入一些牌和一个代表长度的数字,生成该长度这些牌的不同组合 *
MovesGener Class: 用来生成可行出牌的类 *
'''

这里是对于DouZero这部分代码的引用:

@InProceedings{
    
    pmlr-v139-zha21a,
  title = 	 {
    
    DouZero: Mastering DouDizhu with Self-Play Deep Reinforcement Learning},
  author =       {
    
    Zha, Daochen and Xie, Jingru and Ma, Wenye and Zhang, Sheng and Lian, Xiangru and Hu, Xia and Liu, Ji},
  booktitle = 	 {
    
    Proceedings of the 38th International Conference on Machine Learning},
  pages = 	 {
    
    12333--12344},
  year = 	 {
    
    2021},
  editor = 	 {
    
    Meila, Marina and Zhang, Tong},
  volume = 	 {
    
    139},
  series = 	 {
    
    Proceedings of Machine Learning Research},
  month = 	 {
    
    18--24 Jul},
  publisher =    {
    
    PMLR},
  pdf = 	 {
    
    http://proceedings.mlr.press/v139/zha21a/zha21a.pdf},
  url = 	 {
    
    http://proceedings.mlr.press/v139/zha21a.html},
  abstract = 	 {
    
    Games are abstractions of the real world, where artificial agents learn to compete and cooperate with other agents. While significant achievements have been made in various perfect- and imperfect-information games, DouDizhu (a.k.a. Fighting the Landlord), a three-player card game, is still unsolved. DouDizhu is a very challenging domain with competition, collaboration, imperfect information, large state space, and particularly a massive set of possible actions where the legal actions vary significantly from turn to turn. Unfortunately, modern reinforcement learning algorithms mainly focus on simple and small action spaces, and not surprisingly, are shown not to make satisfactory progress in DouDizhu. In this work, we propose a conceptually simple yet effective DouDizhu AI system, namely DouZero, which enhances traditional Monte-Carlo methods with deep neural networks, action encoding, and parallel actors. Starting from scratch in a single server with four GPUs, DouZero outperformed all the existing DouDizhu AI programs in days of training and was ranked the first in the Botzone leaderboard among 344 AI agents. Through building DouZero, we show that classic Monte-Carlo methods can be made to deliver strong results in a hard domain with a complex action space. The code and an online demo are released at https://github.com/kwai/DouZero with the hope that this insight could motivate future work.}
}

以下是主要代码:

import collections
import itertools


################## 检查这些牌是不是连续的序列 ##################
def is_continuous_seq(move):
    i = 0
    while i < len(move) - 1: # 遍历排序好的牌,看相邻两张是否大小差值为1
        if move[i+1] - move[i] != 1:
            return False
        i += 1
    return True


################## 获取出牌的种类和大小 ##################
def get_move_type(move):
    move_size = len(move)
    move_dict = collections.Counter(move) # 一个用来计数可哈希对象的字典子类
    # 它的len就是move里值/牌种类的数量(比如有3和4那len就是2,只有3那len就是1)

    if move_size == 0:  # 过牌
        return {
    
    'type': 0}

    if move_size == 1:  # 单张
        return {
    
    'type': 1, 'rank': move[0]}

    if move_size == 2:
        if move[0] == move[1]:  # 对子
            return {
    
    'type': 2, 'rank': move[0]}
        elif move == [20, 30]:  # 王炸
            return {
    
    'type': 5}
        else:  # 违规出法
            return {
    
    'type': 15}

    if move_size == 3: # 三张(不带)
        if len(move_dict) == 1:
            return {
    
    'type': 3, 'rank': move[0]}
        else:  # 违规出法
            return {
    
    'type': 15}

    if move_size == 4:
        if len(move_dict) == 1:  # 炸弹
            return {
    
    'type': 4,  'rank': move[0]}
        elif len(move_dict) == 2:  # 三带一
            if move[0] == move[1] == move[2] or move[1] == move[2] == move[3]:
                return {
    
    'type': 6, 'rank': move[1]}
            else:  # 违规出法
                return {
    
    'type': 15}
        else:  # 违规出法
            return {
    
    'type': 15}

    if is_continuous_seq(move):  # 顺子
        return {
    
    'type': 8, 'rank': move[0], 'len': len(move)}

    if move_size == 5:
        if len(move_dict) == 2:  # 三带二
            return {
    
    'type': 7, 'rank': move[2]}
        else:  # 违规出法
            return {
    
    'type': 15}

    count_dict = collections.defaultdict(int)
    for c, n in move_dict.items(): # 遍历每个卡面值-计数对
        count_dict[n] += 1 # 看每个计数有多少张卡

    if move_size == 6:  # 四带两单
        if (len(move_dict) == 2 or len(move_dict) == 3) and count_dict.get(4) == 1 and \
                (count_dict.get(2) == 1 or count_dict.get(1) == 2):
            return {
    
    'type': 13, 'rank': move[2]}
    # 四带两对
    if move_size == 8 and (((len(move_dict) == 3 or len(move_dict) == 2) and
                            (count_dict.get(4) == 1 and count_dict.get(2) == 2)) or count_dict.get(4) == 2):
        return {
    
    'type': 14, 'rank': max([c for c, n in move_dict.items() if n == 4])}

    mdkeys = sorted(move_dict.keys())
    if len(move_dict) == count_dict.get(2) and is_continuous_seq(mdkeys):
        # 飞机(连对)
        return {
    
    'type': 9, 'rank': mdkeys[0], 'len': len(mdkeys)}

    if len(move_dict) == count_dict.get(3) and is_continuous_seq(mdkeys):
        # 火箭(连续三不带)
        return {
    
    'type': 10, 'rank': mdkeys[0], 'len': len(mdkeys)}

    # 检查三带一火箭和三带二火箭
    if count_dict.get(3, 0) >= 2:
        serial_3 = list()
        single = list()
        pair = list()

        for k, v in move_dict.items():
            if v == 3:
                serial_3.append(k)
            elif v == 1:
                single.append(k)
            elif v == 2:
                pair.append(k)
            else:  # 违规出法
                return {
    
    'type': 15}

        serial_3.sort()
        if is_continuous_seq(serial_3):
            if len(serial_3) == len(single)+len(pair)*2:
                # 三带一火箭
                return {
    
    'type': 11, 'rank': serial_3[0], 'len': len(serial_3)}
            if len(serial_3) == len(pair) and len(move_dict) == len(serial_3) * 2:
                # 三带二火箭
                return {
    
    'type': 12, 'rank': serial_3[0], 'len': len(serial_3)}

        if len(serial_3) == 4: # 三带一火箭
            if is_continuous_seq(serial_3[1:]):
                return {
    
    'type': 11, 'rank': serial_3[1], 'len': len(serial_3) - 1}
            if is_continuous_seq(serial_3[:-1]):
                return {
    
    'type': 11, 'rank': serial_3[0], 'len': len(serial_3) - 1}

    return {
    
    'type': 15}  # 违规出法


################## 生成指定长度的指定卡牌的所有组合 ##################
def select(cards, num):
    return [list(i) for i in itertools.combinations(cards, num)]


################## 生成可行出牌的类 ##################
class MovesGener(object):
    """
    This is for generating the possible combinations
    """

    def __init__(self, cards_list):
        RealCard2EnvCard = {
    
    '3': 3, '4': 4, '5': 5, '6': 6, '7': 7,
                            '8': 8, '9': 9, '10': 10, 'J': 11, 'Q': 12,
                            'K': 13, 'A': 14, '2': 17, 'X': 20, 'D': 30}
        self.cards_list = []
        for i in cards_list:
            if i[-1] == '0':
                self.cards_list.append(RealCard2EnvCard['10'])
            else:
                self.cards_list.append(RealCard2EnvCard[i[-1]])
        self.cards_dict = collections.defaultdict(int)

        for i in self.cards_list:
            self.cards_dict[i] += 1

        self.single_card_moves = []
        self.gen_type_1_single()
        self.pair_moves = []
        self.gen_type_2_pair()
        self.triple_cards_moves = []
        self.gen_type_3_triple()
        self.bomb_moves = []
        self.gen_type_4_bomb()
        self.final_bomb_moves = []
        self.gen_type_5_king_bomb()
	# 生成顺子
    def _gen_serial_moves(self, cards, min_serial, repeat=1, repeat_num=0):
        if repeat_num < min_serial:  # at least repeat_num is min_serial
            repeat_num = 0

        single_cards = sorted(list(set(cards)))
        seq_records = list()
        moves = list()

        start = i = 0
        longest = 1
        while i < len(single_cards):
            if i + 1 < len(single_cards) and single_cards[i + 1] - single_cards[i] == 1:
                longest += 1
                i += 1
            else:
                seq_records.append((start, longest))
                i += 1
                start = i
                longest = 1

        for seq in seq_records:
            if seq[1] < min_serial:
                continue
            start, longest = seq[0], seq[1]
            longest_list = single_cards[start: start + longest]

            if repeat_num == 0:  # No limitation on how many sequences
                steps = min_serial
                while steps <= longest:
                    index = 0
                    while steps + index <= longest:
                        target_moves = sorted(
                            longest_list[index: index + steps] * repeat)
                        moves.append(target_moves)
                        index += 1
                    steps += 1

            else:  # repeat_num > 0
                if longest < repeat_num:
                    continue
                index = 0
                while index + repeat_num <= longest:
                    target_moves = sorted(
                        longest_list[index: index + repeat_num] * repeat)
                    moves.append(target_moves)
                    index += 1

        return moves
	# 生成单张
    def gen_type_1_single(self):
        self.single_card_moves = []
        for i in set(self.cards_list):
            self.single_card_moves.append([i])
        return self.single_card_moves
	# 生成对子
    def gen_type_2_pair(self):
        self.pair_moves = []
        for k, v in self.cards_dict.items():
            if v >= 2:
                self.pair_moves.append([k, k])
        return self.pair_moves
	# 生成三不带
    def gen_type_3_triple(self):
        self.triple_cards_moves = []
        for k, v in self.cards_dict.items():
            if v >= 3:
                self.triple_cards_moves.append([k, k, k])
        return self.triple_cards_moves
	# 生成炸弹
    def gen_type_4_bomb(self):
        self.bomb_moves = []
        for k, v in self.cards_dict.items():
            if v == 4:
                self.bomb_moves.append([k, k, k, k])
        return self.bomb_moves
	# 生成王炸
    def gen_type_5_king_bomb(self):
        self.final_bomb_moves = []
        if 20 in self.cards_list and 30 in self.cards_list:
            self.final_bomb_moves.append([20, 30])
        return self.final_bomb_moves
	# 生成三带一
    def gen_type_6_3_1(self):
        result = []
        for t in self.single_card_moves:
            for i in self.triple_cards_moves:
                if t[0] != i[0]:
                    result.append(t+i)
        return result
	# 生成三带二
    def gen_type_7_3_2(self):
        result = list()
        for t in self.pair_moves:
            for i in self.triple_cards_moves:
                if t[0] != i[0]:
                    result.append(t+i)
        return result
	# 生成顺子
    def gen_type_8_serial_single(self, repeat_num=0):
        return self._gen_serial_moves(self.cards_list, 5, repeat=1, repeat_num=repeat_num)
	# 生成飞机(连对)
    def gen_type_9_serial_pair(self, repeat_num=0):
        single_pairs = list()
        for k, v in self.cards_dict.items():
            if v >= 2:
                single_pairs.append(k)

        return self._gen_serial_moves(single_pairs, 3, repeat=2, repeat_num=repeat_num)
	# 生成三不带火箭
    def gen_type_10_serial_triple(self, repeat_num=0):
        single_triples = list()
        for k, v in self.cards_dict.items():
            if v >= 3:
                single_triples.append(k)

        return self._gen_serial_moves(single_triples, 2, repeat=3, repeat_num=repeat_num)
	# 生成三带一火箭
    def gen_type_11_serial_3_1(self, repeat_num=0):
        serial_3_moves = self.gen_type_10_serial_triple(repeat_num=repeat_num)
        serial_3_1_moves = list()

        for s3 in serial_3_moves:  # s3 is like [3,3,3,4,4,4]
            s3_set = set(s3)
            new_cards = [i for i in self.cards_list if i not in s3_set]

            # Get any s3_len items from cards
            subcards = select(new_cards, len(s3_set))

            for i in subcards:
                serial_3_1_moves.append(s3 + i)

        return list(k for k, _ in itertools.groupby(serial_3_1_moves))
	# 生成三带二火箭
    def gen_type_12_serial_3_2(self, repeat_num=0):
        serial_3_moves = self.gen_type_10_serial_triple(repeat_num=repeat_num)
        serial_3_2_moves = list()
        pair_set = sorted([k for k, v in self.cards_dict.items() if v >= 2])

        for s3 in serial_3_moves:
            s3_set = set(s3)
            pair_candidates = [i for i in pair_set if i not in s3_set]

            # Get any s3_len items from cards
            subcards = select(pair_candidates, len(s3_set))
            for i in subcards:
                serial_3_2_moves.append(sorted(s3 + i * 2))

        return serial_3_2_moves
	# 生成四带两单
    def gen_type_13_4_2(self):
        four_cards = list()
        for k, v in self.cards_dict.items():
            if v == 4:
                four_cards.append(k)

        result = list()
        for fc in four_cards:
            cards_list = [k for k in self.cards_list if k != fc]
            subcards = select(cards_list, 2)
            for i in subcards:
                result.append([fc]*4 + i)
        return list(k for k, _ in itertools.groupby(result))
	# 生成四带两对
    def gen_type_14_4_22(self):
        four_cards = list()
        for k, v in self.cards_dict.items():
            if v == 4:
                four_cards.append(k)

        result = list()
        for fc in four_cards:
            cards_list = [k for k, v in self.cards_dict.items()
                          if k != fc and v >= 2]
            subcards = select(cards_list, 2)
            for i in subcards:
                result.append([fc] * 4 + [i[0], i[0], i[1], i[1]])
        return result

    # 生成所有可能的出牌方式
    def gen_moves(self):
        moves = []
        moves.extend(self.gen_type_1_single())
        moves.extend(self.gen_type_2_pair())
        moves.extend(self.gen_type_3_triple())
        moves.extend(self.gen_type_4_bomb())
        moves.extend(self.gen_type_5_king_bomb())
        moves.extend(self.gen_type_6_3_1())
        moves.extend(self.gen_type_7_3_2())
        moves.extend(self.gen_type_8_serial_single())
        moves.extend(self.gen_type_9_serial_pair())
        moves.extend(self.gen_type_10_serial_triple())
        moves.extend(self.gen_type_11_serial_3_1())
        moves.extend(self.gen_type_12_serial_3_2())
        moves.extend(self.gen_type_13_4_2())
        moves.extend(self.gen_type_14_4_22())
        return moves

以上就是游戏逻辑用到的所有代码了!

结束语

写AI的时候比较懒事情比较多,没去研究怎么给DouZero写个API调用这个很强的AI,也没自己写比较有技术含量的AI。国际象棋和井字棋用到的minmax和alpha-beta这种AI算法好像不太能胜任(要自己调整/找很多参数),对于斗地主这个七分靠运气的游戏来说随机选择是性价比最高的了。感兴趣的小伙伴可以尝试自己研究下这种有团队协作、游戏信息半透明、规则较多的AI该怎么写(提前对研究出来的大佬说太强了)。
平时我们自己打斗地主的一些额外规则(比如三个人都不叫地主就重新洗牌,赢的人先叫地主,其他人可以抢地主,下筹码等等)和甚至某些基础规则(前两个人都过牌那么你就不能过牌),我都没添加到游戏中,因为这些是bug游戏特性。
本系列的下一篇博客将会展示如何在前端用tkinter和pygame写游戏界面并调用后端逻辑,敬请期待!有各种问题和见解也欢迎评论或者私信!

猜你喜欢

转载自blog.csdn.net/EricFrenzy/article/details/128084950