斗地主类游戏牌型判断算法实现思路

Posted by lx8421bcd on February 16, 2017

最近深度参与了一款地方牌类游戏的开发,规则类似于四人斗地主,游戏做得少,毕竟还是图样图森破,任务拆分的时候,估时估计的过于乐观,结果付出了连续加班的惨痛代价。不过经过这次经历也算是对牌类游戏开发有了一定程度的了解。

这次开发中我觉得最有意义的就是在客户端实现了牌型判断和出牌提示算法,由于是工作内容不能贴代码,所以这里只是叙述一下实现思路,写点伪代码做个意思,作为备份,另一方面供后来者参考。

棋牌类游戏开发流程其实很像普通的移动app,因为场景比较固定,交互也不复杂。一局游戏始于玩家进入房间准备就绪,终于游戏结束玩家退出房间。牌型、判断规则这些东西在房间服务内均有定义,而客户端也需要一份同样的定义,相比于麻将,扑克牌类游戏的数据定义可要简单太多了。扑克牌这个数据结构一般包含以下要素:

card = {}
card.id = 14   -- 扑克牌ID,一副扑克54张牌 + 1牌背,每一张都有一个独一的ID
card.value = 1 -- 牌点,从A到K,对应1-13
card.suits = 1 -- 花色,也有对应的枚举值

由于斗地主游戏一般是无关花色的,所以我们做牌型判断时主要判断的就是牌型和牌点大小。
首先来说下牌型判断,对于斗地主类游戏,一般常见的牌型有以下几种:

  • nX (n >= 1) 单张、对子、三张(部分地区玩法)、炸弹
  • nX + mY (n >= 3, m >= 1) 三带一、三带二
  • nX + nY + ... + nZ (n <= 3) 连对(len >= 3)、顺子(n = 1, len > 5)
  • nJoker + nJOKER (n >= 1,看用几副牌) 天王炸

把上面列举的牌型按照牌点分组,可以将任意牌型整理成一个有若干相同牌点牌组组成的牌组序列

-- 以一个三带二为例 这里仅以cardId作为区分
-- 初始牌组: 三个K带一对3
cardArr = [13, 26, 39, 52, 3, 16]
-- 整理后的同牌点牌组序列 3个K,2个3
cardSeq = [{13, 26, 39, 52},{3, 16}]

根据序列长度可以形成初步的判断规则:

  • 牌组长度 = 1, 单张、对子、三张、炸弹
  • 牌组长度 = 2, 三带一、三带二、天王炸
  • 牌组长度 >= 3, 顺子、连对、连三张

按照这样的逻辑,我们就有归一化的比较简单的方法区分牌型。

-- 传入牌组生成同牌点牌组序列
local function genCardSeq(cardArr)
    local ret = {}
    for i = 1, #cardArr do
        local j
        for j = 1, #ret do
            if ret[j][1].value == cardArr[i].value then
                table.insert(ret[j], cardArr[i])
                break
            end
        end
        if #ret == 0 or j > #ret  then 
            sameValueSeq = {cardArr[i]}
            table.insert(ret, sameValueSeq)
        end
    end
    return ret
end

-- 得到牌型
local function getCardType(cardSeq)
    if #cardSeq = 1 then
        if #cardSeq[1] = 1 then
            return SINGLE
        elseif #cardSeq[1] = 2 then
            return DOUBLE
        elseif #cardSeq[1] = 3 then
            return THREE
        elseif #cardSeq[1] = 4 then
            return BOMB
        -- 根据用几副牌判断是否有更高级别的炸弹等
        end
    elseif #cardSeq = 2 then
        if isJokerBomb(cardSeq) then
            return JOKER_BOMB
        elseif isThreeAndOne(cardSeq) then
            return THREE_AND_ONE
        elseif isThreeAndTwo(cardSeq) then
            return THREE_AND_TWO
        end
    elseif #cardSeq >= 3 then
        if isSingeSequence(cardSeq) then
            return SINGLE_SEQUENCE
        elseif isDoubleSequence(cardSeq) then
            return DOUBLE_SEQUENCE
        end
    end
    return NONE
end

能够取得牌型和比较方法之后,写出牌判断就比较简单了

-- 判断是否能出牌
function judgeCanOut(outCardArr, handCardArr)
    local outCardSeq = genCardSeq(outCardArr)
    local handCardSeq = genCardSeq(handCardArr)

    local outCardType = getCardType(ourCardSeq)
    local handCardType = getCardType(handCardSeq)
    -- 如果手牌不是任何牌型,返回false
    if handCardType == NONE then 
        return false
    -- 天王炸直接GG
    elseif outCardType == JOKER_BOMB then
        return false
    -- 如果牌型不匹配,直接判断是不是出炸弹,能否炸
    elseif outCardType ~= handCardType then
        return judgeBomb(outCardSeq, handCardSeq)
    -- 按牌型进行判断,这样枚举的好处是需要添加/屏蔽牌型时修改迅速
    elseif outCardType == SINGLE then
        return judgeSingle(outCardArr, handCardArr) -- 判断单张、对子、三张的方法
    elseif outCardType == DOUBLE then
        return judgeSingle(outCardArr, handCardArr) -- 判断单张、对子、三张的方法
    elseif outCardType == THREE_AND_ONE then
        return judgeThree(outCardSeq, handCardSeq) -- 判断三带一、三带二

    ......

    elseif outCardType == SINGLE_SEQUENCE then
        return judgeSequence(outCardArr, handCardArr) --判断顺子
    elseif outCardType == BOMB then 
        return judgeBomb(outCardArr, handCardArr)
    end
    return false
end

......

这里还可以根据游戏规则做出一些优化,并不一定是无脑枚举,这样出牌判断算法就基本写完了,调用时传入上家出牌和当前选中手牌即可完成判断。

在出牌判断算法完成之后,出牌提示算法其实也比较容易了,注意此处说的出牌提示算法只是一个简单的提示功能,并不会去计算玩家当前出什么牌组是最优解。所以我们在写出牌提示算法时,最主要的目标,时根据上家手牌,列出当前手牌中可出的组合,从小到大提示给用户。

function genHintTable(outCardArr, handCardArr)
    local outCardSeq = genCardSeq(outCardArr)
    local outCardType = getCardType(ourCardSeq)
    local hintTable = {}
    -- 天王炸直接GG
    if outCardType == JOKER_BOMB then
        return hintTable
    end

    if outCardType == SINGLE then
        table.concat(genSingleHint(handCardArr))
    elseif outCardType == DOUBLE then
        table.concat(genDoubleHint(handCardArr))

    ......

    end
    -- 最后添加炸弹提示
    table.concat(hintTable, genBombHint(handCardArr))
    return hintTable
end

将提示牌组返回给UI层之后供UI层使用即可