犹豫工作和自己学习了一些新的东西,今天打开博客吓自己一跳,原来自己这么久没有更新博客了。看来以后还是要坚持每周最少写一篇博客啊。
在讲解麻将胡牌算法之前,先说说为什么写这么一篇博客吧。在做项目中,其实前辈们早就封装好了一些胡牌的检测算法,不过我还算是一个比较喜欢刨根问到底的人,每次调用别人写好的算法的时候总是想知道算法的具体实现。然而在看算法具体实现的时候,发现里面一个二维矩阵有点复杂,并且没有没有注释。所以改换另一条路去研究胡牌算法,先去网上找了找一般胡牌算法实现的原理然后自己写了写,就是下面即将展示给大家的胡牌算法(不含特殊牌型的检测)
1.名词解释
名词
解释
将
即胡牌必须具备的一个对子
克子
即三个相同的牌
顺子
即同种牌型三个连续的牌
胡牌
即手中的牌除了一对将之外全部能组成克子或者顺子
2.数据结构
2.1.麻将矩阵(二维矩阵)
mahjongMatrix = {
[0] = {[-1]=0,[0]=0,[1]=0,[2]=0,[3]=0,[4]=0,[5]=0,[6]=0,[7]=0,[8]=0,[9]=0,[10]=0,[11] = 0}, --万
[1] = {[-1]=0,[0]=0,[1]=0,[2]=0,[3]=0,[4]=0,[5]=0,[6]=0,[7]=0,[8]=0,[9]=0,[10]=0,[11] = 0}, --筒
[2] = {[-1]=0,[0]=0,[1]=0,[2]=0,[3]=0,[4]=0,[5]=0,[6]=0,[7]=0,[8]=0,[9]=0,[10]=0,[11] = 0}, --条
}
麻将矩阵的第一维
Key :表示麻将的类型,比如万,筒,条。当然有一些地区麻将含有东南西北等,这样也只需要扩展麻将矩阵的一维数据而已
Value : 即这个类型的麻将集合cardList
麻将矩阵的第二维
Key : 表示麻将的值,1~9。可能有同学有疑问 为什么会有-1,0,10,11。其实我在这里添加这几个值 单纯是为了在检测顺子的时候不需要考虑边界情况简化算法
"Value" : 即这个麻将值的牌有几个
2.2.麻将
card = 0x0101 --四位的16进制的数表示一张牌
第一位表示这张牌的类型 如:0->万 1->筒 2->条
第二位表示表示这张牌的值 如:1表示牌值是1,结合第一位就能推断出这张牌是1万,1筒还是1条
第三四位表示这张牌的索引,这样可以确保每一张牌都有唯一的编号,如:0x0201表示第一张2万
3.算法核心流程
玩过麻将都应该知道,一般判断手上的牌是否能胡,就是检测手牌除了一对将之后剩下的牌是否能组成克字或顺子并且没有剩余的手牌。
3.1.定义手牌
local cardList = {0x0101,0x0201,0x0202,0x0301,0x0302,0x0303,0x0401,0x0402,0x0501,0x1601,0x1701,0x1801,0x0901,0x0902}
3.2.将手牌转换成麻将矩阵
上面我们也提到了检测胡牌时依据麻将矩阵,因此我们要将手牌转换成麻将矩阵
-- 麻将类型定义
local mahjongType = {
[0] = "万",
[1] = "筒",
[2] = "条",
}
-- 初始化一个麻将矩阵
function initMahjongMatrix()
local mahjongMatrix = {}
for i = 0, 3 do
mahjongMatrix[i] = {}
for j = -1, 11 do
mahjongMatrix[i][j] = 0
end
end
return mahjongMatrix;
end
-- 打印麻将矩阵
function dumpMahjongMatrix(mahjongMatrix)
local dumpInfo = "{ "
for cardType, mahjongList in pairs(mahjongMatrix) do
for mahjongValue, count in pairs(mahjongList) do
for i = 1, count do
dumpInfo = dumpInfo .. mahjongValue .. mahjongType[cardType] .. " "
end
end
end
dumpInfo = dumpInfo .. "}"
print(dumpInfo)
end
-- 将手牌转换成麻将矩阵
function cardListConvertToMatrix(cardList)
local mahjongMatrix = initMahjongMatrix()
for _, card in pairs(cardList) do
local cardType = card >> 12
local cardValue = (card >> 8) & 0x0F
mahjongMatrix[cardType][cardValue] = mahjongMatrix[cardType][cardValue] + 1
end
dumpMahjongMatrix(mahjongMatrix)
return mahjongMatrix
end
函数initMahjongMatrix即使用双重for循环初始化一个麻将举证,
函数cardListConvertToMatrix将手牌转换成麻将矩阵
首先根据我们对牌的数据结构的定义,使用card >> 12得到16进制的第一位,即牌的类型
同样card >> 8得到16进制的第一和二位,然后位与0x0F结果即为card的值
根据我们对麻将矩阵的定义,知道card的type和value也就是确定了这张牌在麻将矩阵中的位置,因此我们只需要将这张牌在麻将矩阵中对应的个数加一,即表示这张牌被存储在了麻将矩阵中
函数dumpMahjongMatrix将麻将矩阵转换成很容易看懂的数据输出
{ 6筒 7筒 8筒 1万 2万 2万 3万 3万 3万 4万 4万 5万 9万 9万 }
3.3.检测将
有了麻将矩阵,我们先检测将(为什么第一步要检测将而不是检测克字和顺子,稍后我们再来解释)
-- 深拷贝
function deepCopy(sourceData)
if type(sourceData) == "table" then
local temp = {}
for key, value in pairs(sourceData) do
temp[key] = deepCopy(value)
end
return temp
end
return sourceData
end
-- 通过去除麻将矩阵中一个将之后的麻将矩阵列表
function getMahjongMatrixListByRemoveTwoCards(mahjongMatrix)
local mahjongMatrixList = {}
for cardType, mahjongList in pairs(mahjongMatrix) do
for mahjongValue, count in pairs(mahjongList) do
if count >= 2 then
local temp = deepCopy(mahjongMatrix)
temp[cardType][mahjongValue] = temp[cardType][mahjongValue] - 2
table.insert(mahjongMatrixList, temp);
end
end
end
return mahjongMatrixList
end
函数getMahjongMatrixListByRemoveTwoCards去除麻将矩阵中一个将之后的麻将矩阵列表
因为在麻将矩阵中找到一个对子作为将,有多个可能性。但是在这个阶段不能确定那一种可能性可以胡牌,哪一种可能性不能胡牌,因此要将每一种可能性都保存起来,后续继续检测
函数deepCopy深拷贝,在去除麻将矩阵中一个将之后,不能影响下一种可能性中的数据,因此在去除一个将之前都要对麻将矩阵深拷贝一次
看一下上面的测试手牌,有几种可能性
{ 6筒 7筒 8筒 1万 3万 3万 3万 4万 4万 5万 9万 9万 }
{ 6筒 7筒 8筒 1万 2万 2万 3万 4万 4万 5万 9万 9万 }
{ 6筒 7筒 8筒 1万 2万 2万 3万 3万 3万 5万 9万 9万 }
{ 6筒 7筒 8筒 1万 2万 2万 3万 3万 3万 4万 4万 5万 }
3.4.检测句子和克字
现在便利麻将矩阵,如果只有一张的牌那么这张牌A就只能当作顺子的开头;如果有两张的牌,因为已经有将而这两张也不能组成克子,所以这两张只能当作两个顺子的开头;如果有三张这样的牌,可以组成克子,但是如果让他组成顺子则要求为AAABBBCCC与后面的三张也能组成克子,所以组成顺子或者克子本质是相同的。但是组成克子AAA的通用性要高于组成顺子AAABBBCCC所以当有三个及以上这样牌的时候优先组成克子AAA;如果有四张这样的牌,要能胡牌则需要AAAABBBBCCCC或者AAAABC,对于是先组一个顺子还是一个克子都会回到上述的情况
3.4.1.检测句子
通过上面的分析我们先检测麻将矩阵中的句子
-- 移除麻将矩阵中的句子
function removeThreeLinkCards(mahjongMatrix)
for cardType, mahjongList in pairs(mahjongMatrix) do
for mahjongValue, count in pairs(mahjongList) do
for i=1, count do
local mahjongValuePlusOneCount = mahjongList[mahjongValue+1]
local mahjongValuePlusTwoCount = mahjongList[mahjongValue+2]
if count > 0 and mahjongValuePlusOneCount > 0 and mahjongValuePlusTwoCount > 0 then
mahjongList[mahjongValue] = mahjongList[mahjongValue] - 1
mahjongList[mahjongValue+1] = mahjongList[mahjongValue+1] - 1
mahjongList[mahjongValue+2] = mahjongList[mahjongValue+2] - 1
end
end
end
end
end
函数removeThreeLinkCards移除麻将矩阵中的句子
在上面我们解释了麻将矩阵二维的含义,因此我们判断牌值为mahjongValue的这张牌,在剩余牌中是否存在牌与这张牌组成句子,我们只需要判断,在麻将矩阵中是否存在牌值为(mahjongValue+1)和牌值为(mahjongValue+2)的牌。
如果牌值为(mahjongValue+1)和牌值为(mahjongValue+2)的牌都存在,则能组成句子,我们将这三张牌从麻将矩阵中移除掉
当然牌值为mahjongValue可能存在多个,但是每次检测到能组成句子,只去处一张牌,因此这张牌有几张我们就检测几次,防止漏掉
便利麻将矩阵的时候,是从牌值为1开始找的,因此在检测中间某一牌值mahjongValue的时候,不必回头检测是否存在牌值为(mahjongValue-1)和(mahjongValue-2)的牌
3.4.2.检测克子
检测克子比检测句子就更加简单了
-- 检测克子
function removeTheSameThreeCards(mahjongMatrix)
for cardType, mahjongList in pairs(mahjongMatrix) do
for mahjongValue, count in pairs(mahjongList) do
if count >= 3 then
mahjongList[mahjongValue] = mahjongList[mahjongValue] - 3
end
end
end
end
只需要检测牌值为mahjongValue的牌在麻将矩阵中的数量是否大于等于三
3.5.是否胡牌
有上面分析可知,如果麻将矩阵中所有的元素全部为0表示手中的牌除了一对将之外全部能组成克子或者顺子,即可以胡牌
-- 检测矩阵中元素是否全部为0
function checkMatrixAllElemEqualZero(mahjongMatrix)
for cardType, mahjongList in pairs(mahjongMatrix) do
for mahjongValue, count in pairs(mahjongList) do
if count ~= 0 then
return false
end
end
end
return true
end
-- 检测是否胡牌
function checkHu(cardList)
local mahjongMatrix = cardListConvertToMatrix(cardList)
local mahjongCardList = getMahjongMatrixListByRemoveTwoCards(mahjongMatrix)
for _, matrix in ipairs(mahjongCardList) do
removeThreeLinkCards(matrix)
removeTheSameThreeCards(matrix)
local result = checkMatrixAllElemEqualZero(matrix)
if result == true then
return true
end
end
return false
end
函数checkMatrixAllElemEqualZero便利麻将矩阵判断所有元素是否为0
函数checkHu检测是否胡牌
3.6.回答上面的疑问
上面分析算法的时候留下了一个问题,就是为什么第一步要检测将而不是检测克字和顺子
第一,因为如果要糊牌,必须要有唯一的一个将。而克子和顺子是非必要的
第二,如果出现ABCCCDEF这样的牌型的时候,先检测顺子或者先检测克子都不会将CC作为将
4.欢迎讨论