超强跑得快机器人智能算法深度研究与设计

11 篇文章 2 订阅
9 篇文章 2 订阅

       上一篇斗地主机器人算法的设计是自主研究学习的,没有做细节上的优化,但最近公司刚好要上线跑得快,也需要我编写跑得快ai算法。于是我直接采用斗地主的算法框架来写这个ai,可以说主体设计没有变,但是呢细节上有重大改进,也是这些细节决定了整个ai的品质,经过一个多月与策划撕逼,测试争吵,最终出来的成果是,可以说在不作弊的情况下,90%的人都打不赢机器人,就连我本人都很难取胜,效果倍儿棒,整个设计和过程值得记录与分享。

       斗地主的单牌分值设计是没有负分设计的,但策划弄了一套负分的设计,原理一样,没有什么区别

     -- 1. 牌力基础价值:以10为参考点,单张10即为0分,即<10的单牌价值为负,大于10的单牌价值为正

       local CARD_VALUE = {-9, -8, -7, -6, -5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5, 6, 7}

       local TURN_VALUE = 7 (每一轮的分值是7)

牌型分值设计如下

function AndroidAI:GetCardTypeScore(cbCardData)
    -- 2. maxCard:牌型大小的牌值,单张和对子就是自身,顺子和连对是最大那张牌,三带是有三张的那个牌,这个很容易理解,相同的牌型比较大小,就是比较maxCard
    local type = m_GameLogic:GetCardType(cbCardData)
    if type == DEF.CT_ERROR then
        return 0
    elseif type == DEF.CT_SINGLE then
        return m_GameLogic:GetCardLogicValue(cbCardData[1]) - 10
    elseif  type == DEF.CT_DOUBLE then
         local  score=m_GameLogic:GetCardLogicValue(cbCardData[1])/100
        return m_GameLogic:GetCardLogicValue(cbCardData[1]) - 10+score
    elseif type == DEF.CT_THREE then
        return (m_GameLogic:GetCardLogicValue(cbCardData[1]) - 3 + 1) / 2
    elseif  type == DEF.CT_DOUBLE_LINE then
        return m_GameLogic:GetCardLogicValue(cbCardData[1]) - 10 + 1
    elseif type == DEF.CT_SINGLE_LINE  then
        return m_GameLogic:GetCardLogicValue(cbCardData[1]) - 10 + 1+((#cbCardData-5)*0.6)
    elseif type == DEF.CT_THREE_TAKE_TWO or type == DEF.CT_THREE_TAKE_ONE then
        
        if #cbCardData<=5 then
            local cardCount=0
            if type == DEF.CT_THREE_TAKE_ONE then
                cardCount=#cbCardData/4
            else
                cardCount=(#cbCardData/5)*2
            end
            local score=0
            local  TwoCardLogic={}
            for i = 1, cardCount do
                local logic =m_GameLogic:GetCardLogicValue(cbCardData[#cbCardData-i+1])
                score=score+logic/100
                TwoCardLogic[i]=logic
            end
            if TwoCardLogic[1]==TwoCardLogic[2] then
                score=score+0.12
            end
            return (m_GameLogic:GetCardLogicValue(cbCardData[1]) - 3 + 1) / 2+score 
        else
            local cardCount=0
            if type == DEF.CT_THREE_TAKE_ONE then
                cardCount=#cbCardData/4
            else
                cardCount=(#cbCardData/5)*2
            end
            local score=0
            local  TwoCardLogic={}
            for i = 1, cardCount do
                local logic =m_GameLogic:GetCardLogicValue(cbCardData[#cbCardData-i+1])
                score=score+logic/100
                TwoCardLogic[i]=logic
            end
            if TwoCardLogic[1]==TwoCardLogic[2] then
                score=score+0.12
            end
            return ((m_GameLogic:GetCardLogicValue(cbCardData[1]) - 3 + 7) )+5+score
        end
       
    elseif type == DEF.CT_BOMB_CARD then
        return m_GameLogic:GetCardLogicValue(cbCardData[1]) - 3 + 7+9
    elseif
        type == DEF.CT_THREE_LINE or type == DEF.CT_FOUR_TAKE_ONE or type == DEF.CT_FOUR_TAKE_TWO or
            type == DEF.CT_FOUR_TAKE_THREE or
            type == DEF.CT_MISSILE_CARD
     then
        return ((m_GameLogic:GetCardLogicValue(cbCardData[1]) - 3 + 7) / 2)
    end

    return m_GameLogic:GetCardLogicValue(cbCardData) - 10
end

1,跑得快的主动出牌时,选择有很多,我们有两大主策略,一是有多手牌时,有收得回的牌就先出,二是没有收得回来的牌,就先出多牌,

次要策略,一是有人报双就避开双牌来出,二是有人报单就避开单牌来出就是这么简单,避无可避时随便出。当然还有很多细节策略没有提到,但那些不是那么重要,当你真正要去做时,自然你就想到需要怎么补其他的了。

2跑得快被动压牌时,因为跑得快是能管得起是必须管的,这个就决定了他的设计简单的特性,那么采用了这么一种设计,就是算出能压牌的所有可能,尝试一遍,保留分值最大的那一手牌就打出去,主体策略这样写是没有问题的。特殊情况补充一些策略就好了,这点与斗地主的设计大有不同,斗地主可以停招,等招,斗地主主要压牌的思路是我要压的牌,如果打出去之后,对整体牌型有严重破坏,那么就可以先不出,等到非出不可时,再拆牌打。

上面的策略只是挑重点的说,实际添加的策略大大小小不下80条,多次讨论,多次推翻,来回修改,久经考验,实战,才能得到现在的预期策略效果。策略方面已经说完了,那么代码怎么写代码设计思路我也分享一下,

1手牌牌型所有可能的牌型先找出来,然后再用算法找出能组合之后和手牌数一样的,才是有效组合(因为有的组合是重复的)。

找出有效组合之后再算每一种组合的分值,挑选出组合最大的分值的组合,然后采用上面的策略去分析出牌,这个就是整个的代码思路。这里有一个小插曲,因为我用的是skynet框架是C+lua的,lua对于算法无能为力,太慢了,大概慢20多倍这样,经过认真测试的,所以核心算法采用了C来写,用C是公司要求,但我个人更加建议用C++来写,C++的vector不需要你去关心传入的数组是多大,调用方也不需要知道C模块的数组限制是多大,这样减少了记忆负担,越界的稳定性,所有牌类都适用,你好,我好,大家好。由于跑得快是16张牌,组合是100万范围以内,处于卡与不卡的交界处,刚好不需要开多线程来跑,如果是斗地主不开多线程,性能是绝对跟不上的。

   关键选牌算法处理

--  C++算法调用处理
    local allsort= CardAllSort.GetCardAllSort(cbResultCardAll,vecHandCardCount,cbHandCardCount)
    for i = 1, #allsort do
        local temSort={}
        for j = 1, #allsort[i] do
            for k = 1, #resultVec do
                if self:ArrayQual(resultVec[k].cbResultCard,allsort[i][j]) then
                    table.insert(temSort, resultVec[k])
                    break
                end
            end
        end
        table.insert(resultAllVec, temSort) 
    end

    local MinTypeCount = 1000
    local MinTypeScore =math.mininteger
    local index = 0
    local tableSore={}
    local bBiYing=false
    for i = 1, #resultAllVec do
        local tempScore = self:GetHandScore(resultAllVec[i])
        tableSore[i]=tempScore
        if  tempScore > MinTypeScore and bBiYing~=true then
            MinTypeScore = tempScore
            index = i
            MinTypeCount = #resultAllVec[i]
        end
        if #cbHandCardData<15 and NoIncludeBiYing~=true and self:isBiYing(resultAllVec[i]) then
            if bBiYing~=true or (bBiYing and #resultAllVec[i]<MinTypeCount)  then
                bBiYing=true
                MinTypeScore = tempScore
                index = i
                MinTypeCount = #resultAllVec[i]
            end
        end
    end
    if #resultAllVec>0 then
        for i = 1, #resultAllVec[index] do
            CardResult[i] = resultAllVec[index][i]
        end
        table.sort(
            CardResult,
            function(first, second)
                if (first.cbCardType > second.cbCardType) then
                    return true
                end
                if (first.cbCardType == second.cbCardType) then
                    if
                        (m_GameLogic:GetCardLogicValue(first.cbResultCard[1]) >
                            m_GameLogic:GetCardLogicValue(second.cbResultCard[1]))
                     then
                        return true
                    end
                end
                return false
            end
        )
    end

       上面牌选好了,那么下面就进入牌的策略分析

--主动出牌
function AndroidAI:ZhuDongOutCard(cbHandCardData, wMeChairID, OutCardResult)
    -- //零下标没用
    local cbHandCardCount = #cbHandCardData

    self.m_wMeChairID=wMeChairID
    local CardTypeResult = {}
    for i = 1, DEF.MAX_TYPE_COUNT - 1 do
        CardTypeResult[i] = self:GetOutCardTypeResult()
    end
    local vecMinTypeCardResult = {}
    local minTypeCount = self:FindCardKindMinNum(cbHandCardData, cbHandCardCount, CardTypeResult, vecMinTypeCardResult)
    if #vecMinTypeCardResult==0 then
        return
     end
    if (minTypeCount == 1)then
	
		OutCardResult.cbCardCount = vecMinTypeCardResult[1].cbCardCount
		OutCardResult.cbResultCard= vecMinTypeCardResult[1].cbResultCard
		return;
	end
   
    if self:FindMaxTypeTakeOneType(cbHandCardData, nil,0, vecMinTypeCardResult, OutCardResult) then
        local type=m_GameLogic:GetCardType(OutCardResult.cbResultCard)
        if type==DEF.CT_THREE or type==DEF.CT_THREE_TAKE_ONE then
            if self:ThreeTakeOneChangeTwoTake(cbHandCardData,vecMinTypeCardResult, OutCardResult) then
                return
            end
        else
            return
        end
       
    end
    --优先打能收回来的牌
    local outIndex, bZhiJieChouPai = self:isMaxAllCard(cbHandCardData, vecMinTypeCardResult)
    if outIndex ~= -1 then

        主要就是两个出牌函数搞定,示例如下

  self:ZhuDongOutCard(tempCard, 1, outCardSearch)
   
   

local s = os.clock()



    local resultIndex=self:NewTurnBeiDongOutCard(tempCard,cbTurnCardData,1,2,outCardSearch,SearchCardResult)
    --print(os.date("%Y-%m-%d-%H-%M-%S",os.time()))
    
local e = os.clock()

print("used time"..e-s.." seconds")

        整个ai是设计好了,但是呢对于真正的打牌专业高手还不一定打得过,所以保险起见还得设计一个发好牌策略,那么发好牌策略也有一番学问,一开始是设计给机器人发好牌,结果发现系统要收分时,效果不理想,后来发现这里面有一个漏洞,那就是给机器人发了好牌,人也可能手上是好牌,这样收分命中率不确定,所以这里还得这么来设计,先给玩家发烂牌,机器人发好牌,这样子,那玩家就是无还手之力,自然ai就收放自如了

设计思路如下:    

--  作弊方先出(出牌方必须要黑桃三)

--  好牌烂牌,只发给一个人/或者一个机器人

-- 1%    2副炸弹,两个对子,4个单牌

-- 5%   1个炸弹,两个三带二,2个单牌

-- 5%   1个炸弹,一个顺子,三带二,一个对子

-- 20%  一个顺子,1个三带二,三个对子

 

    

--  烂牌

-- 烂牌

--  单牌K以下的,不连续的

--  对子,顺子J以下的,三张9,以下的

-- 20%  1个三张,3个对子,7张单牌

-- 15%  5个对子,6张单牌

-- 15%  1个顺子,一个三张,一个对子,6张单牌

-- 15%   2个三张,一个对子,8张单牌

-- 20%  4个对子,一个三张,5张单牌

这样一通整顿下来之后,就是真正的完整的ai体系了,ai就能想胜就胜,犹如真人实战,出牌完全接近人的思路,做到真正的收放自如,让玩家玩得痛快,赢得尽兴,输得心服口服

有更加好想法的一起交流学习一下私信或者留言。

评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值