yemaster的小窝

冲击麻将胡牌算法

2024-05-11
31

冲击麻将是浙江省慈溪、余姚地区特有的麻将玩法。一共有136张牌,分为万牌,筒/饼牌,条/索牌,字牌/风头牌,不包括花牌。

胡法可以分为以下三类:

  • 碎胡/大乱/十三不搭 即任意两张同种牌差距大于等于3,并且没有对子。(例如1条4条可以,但是3筒5筒不行)。
  • 七对子 即牌中有7个对子
  • 顺胡 即牌可以分成1个对子,若干个刻子(3张一样的牌)/顺子(3张相连的牌)。

另外,游戏中还加入了“龙/财神/百搭”牌。即该种牌可以代替任何一种牌。

接下来,分别实现每种胡法的算法。用0—8表示一万—九万,9—17表示一筒—九筒,18—26表示一条—九条,27—33表示东南西北中发白。用一个数组表示手牌,dragon变量表示龙。

碎胡的胡牌算法

首先统计每张牌的个数,如果有大于1说明有对子,不能胡。然后看万筒条中有没有差小于3的即可。

function isAgari(paiArr, dragon) {
    let cardsCount = [];
    for (let i = 0; i < 34; ++i)
        cardsCount.push(0);
    for (let i = 0; i < paiArr.length; ++i)
        if (paiArr[i] != dragon) // 不必统计龙
            cardsCount[paiArr[i]]++;

    let pai = []; // 给牌排好序
    for (let i = 0; i < 34; ++i)
        for (let j = 0; j < cardsCount[i]; ++j)
            pai.push(i);

    for (let i = 1; i < pai.length; ++i) { // 有相邻或间隔为1的万筒条,不能胡
        if (Math.abs(pai[i] - pai[i - 1]) <= 2 &&
            Math.floor(pai[i] / 9) == Math.floor(pai[i - 1] / 9) &&
            pai[i] < 27)
            return false;
    }
    return true;
}

七对子的胡牌算法

这个更简单,只需要统计对子个数就好了,龙特殊处理一下就好了。

function isAgari(paiArr, dragon) {
    let cardsCount = [], longCount = 0;
    for (let i = 0; i < 34; ++i)
        cardsCount.push(0);
    for (let i = 0; i < paiArr.length; ++i)
        if (paiArr[i] != dragon) // 不必统计龙
            cardsCount[paiArr[i]]++;
        else
            longCount++;

    let pairCount = 0, singleCount = 0; // 分别计算对子数和但张牌个数
    for (let i = 0; i < cardsCount.length; ++i) {
        pairCount += Math.floor(cardsCount[i] / 2);
        singleCount += cardsCount[i] % 2;
    }
    if (singleCount <= longCount) // 龙把单个牌配成对后还有的多,两张龙也能成对
        pairCount += singleCount + Math.floor((longCount - singleCount) / 2);
    else // 把所有龙都把单张牌配对。
        pairCount += longCount;
    if (pairCount >= 7)
        return true
    return false;
}

顺胡的胡牌算法

如果没有龙,那么这个将是非常简单的。我们只要枚举对子,然后一次判断剩下牌是否为刻字与顺子的组合,只需一个dfs就可以了。但是,有龙的存在,如果我们枚举龙所对应的牌,效率将会非常低下。

因此,我们考虑换个思路。首先,万、筒、条、字牌是分别独立的。我们只需要分别考虑每一块牌是否可胡就行了。这样就分解成了4个子问题。并且这4个子问题规模不大,而且都有重复性,可以暴力枚举。为了优化时间效率,我们还可以把所有可以胡的牌型处理出来,然后只需要枚举龙的分配和对子的位置即可。

比如对于牌:[0,0,0,1,2,5,5,5,10,11,12,27,27,27]。那么我们先拆成 [0,0,0,1,2,5,5,5][10,11,12][][27,27,27] 四部分。然后分别判断每个部分是否可胡。不难发现,对子在第一部分的时候就是都可胡的。我们只需要枚举每部分分别在有无对子、有 $n$ 条龙的情况下是否可胡即可,而这数据量不大,完全可以先爆搜预处理,然后查表。

表的生成

#include <cstdio>
#include <cstdlib>

using namespace std;

int cnt[10];
int q[10];

// Array(8),有对子/无对子+龙0123

inline bool check(int z)
{
    if (z >= 10)
        return true;
    if (q[z] > 0)
    {
        bool flag;
        if (z + 2 <= 9 && q[z + 1] > 0 && q[z + 2] > 0) // 可以抽顺子,枚举抽顺子
        {
            q[z] -= 1;
            q[z + 1] -= 1;
            q[z + 2] -= 1;
            flag = check(z);
            q[z] += 1;
            q[z + 1] += 1;
            q[z + 2] += 1;
            if (flag)
                return true;
        }
        if (q[z] >= 3) // 可以抽刻字,枚举抽刻字
        {
            q[z] -= 3;
            flag = check(z);
            q[z] += 3;
            if (flag)
                return true;
        }
        return false;
    }
    else
        return check(z + 1);
}

inline bool check_dz(bool dz, int sum)
{
    if (dz)
    {
        for (int i = 1; i <= 9; ++i) // 枚举哪张牌作为对子
        {
            if (q[i] < 2)
                continue;
            q[i] -= 2;
            bool flag = check(1);
            q[i] += 2;
            if (flag)
                return true;
        }
    }
    else
    {
        if (check(1))
            return true;
    }
    return false;
}

inline bool check_dragon(bool dz, int dragon, int sum)
{
    if (dragon + sum > 14)
        return false;
    if (dragon > 0)
    {
        for (int i = 1; i <= 9; ++i) // 枚举龙所对应的牌
            if (q[i] < 4)
            {
                q[i] += 1;
                bool flag = check_dragon(dz, dragon - 1, sum);
                q[i] -= 1;
                if (flag)
                    return true;
            }
        return false;
    }
    else
    {
        return check_dz(dz, sum);
    }
}

inline void dfs(int u, int sum) // 先枚举出所有牌型。用9个数字表示9种牌的个数。
{
    if (u > 9)
    {
        if (sum)
            putchar(',');
        putchar('"');
        for (int i = 1; i <= 9; ++i)
            putchar('0' + cnt[i]);
        putchar('"');
        putchar(':');
        putchar('[');
        for (int j = 0; j < 8; ++j) // j=0~3表示有对子,j条龙;4~7表示无对子,j-5条龙.
        {
            for (int i = 1; i <= 9; ++i)
                q[i] = cnt[i];
            if (check_dragon(j <= 3, j % 4, sum))
                printf("true");
            else
                printf("false");
            if (j < 7)
                putchar(',');
        }
        putchar(']');
        // exit(0);
        return;
    }
    for (int i = 0; i <= 4; ++i)
    {
        if (sum + i <= 14)
        {
            cnt[u] = i;
            dfs(u + 1, sum + i);
        }
    }
}

int main()
{
    freopen("data.js", "w", stdout); // 输出为js文件,方便js调用。
    putchar('h');
    putchar('=');
    putchar('{');
    dfs(1, 0);
    putchar('}');
    putchar('\n');
    printf("module.exports={h}");
    return 0;
}

上面是数字牌的情况,字牌也是类似的。不过我这里为了方便,字牌用了另一种记录方法,记录有4张、3张、2张、1张、0张的字牌分别由几种。因为字牌不能组成顺子,所以只需要判断刻字和对子即可。

inline void dfs(int u, int sum, int sum2)
{
    if (u > 4)
    {
        if (tot)
            putchar(',');
        tot++;
        putchar('"');
        for (int i = 0; i <= 4; ++i)
            putchar('0' + cnt[i]);
        putchar('"');
        putchar(':');
        putchar('[');
        for (int i = 0; i <= 1; ++i)
        {
            for (int j = 0; j < 4; ++j)
            {
                if (i + j)
                    putchar(',');
                if (cnt[4] > 0)
                    printf("false");
                else
                {
                    int need = 2 * cnt[1] + 1 * cnt[2];
                    if (i == 0)
                    {
                        need -= 1;
                        if (need < 0)
                            need += 3;
                    }
                    if (need == j || need + 3 == j)
                        printf("true");
                    else
                        printf("false");
                }
            }
        }
        putchar(']');
    }
    else
    {
        for (int i = 0; i <= 7; ++i)
            if (sum + i * u <= 14 && sum2 + i <= 7)
            {
                cnt[u] = i;
                dfs(u + 1, sum + i * u, sum2 + i);
            }
    }
}

胡牌检验

有了上面两个文件生成的数据,然后就可以判断了。

const distributeLong = [[[0, 0, 0, 0]],
[[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]],
[[2, 0, 0, 0], [0, 2, 0, 0], [0, 0, 2, 0], [0, 0, 0, 2], [1, 1, 0, 0], [1, 0, 1, 0], [1, 0, 0, 1], [0, 1, 1, 0], [0, 1, 0, 1], [0, 0, 1, 1]],
[[3, 0, 0, 0], [0, 3, 0, 0], [0, 0, 3, 0], [0, 0, 0, 3], [1, 2, 0, 0], [1, 0, 2, 0], [1, 0, 0, 2], [2, 1, 0, 0], [0, 1, 2, 0], [0, 1, 0, 2], [2, 0, 1, 0], [0, 2, 1, 0], [0, 0, 1, 2], [2, 0, 0, 1], [0, 2, 0, 1], [0, 0, 2, 1], [1, 1, 1, 0], [1, 1, 0, 1], [1, 0, 1, 1], [0, 1, 1, 1]]]
function isAgari(paiArr, dragon) {
    let cardsCount = [], longCount = 0;
    for (let i = 0; i < 34; ++i)
        cardsCount.push(0);
    for (let i = 0; i < paiArr.length; ++i)
        if (paiArr[i] != dragon) // 不必统计龙
            cardsCount[paiArr[i]]++;
        else
            longCount++;

    let paiStr = ["", "", "", ""] // 计算四部分的牌型
    let fengCount = [0, 0, 0, 0, 0]
    for (let i = 0; i < 3; ++i)
        for (let j = 0; j < 9; ++j)
            paiStr[i] += cardsCount[i * 9 + j]

    for (let i = 27; i < 34; ++i) {
        fengCount[cardsCount[i]]++
    }
    for (let i = 0; i <= 4; ++i)
        paiStr[3] += fengCount[i]

    for (let i = 0; i < 4; ++i) {
        for (let j in distributeLong[longCount]) { // 枚举4个部分龙的个数
            flag = true;
            try {
                for (let k = 0; k < 4; ++k) { // k枚举对子的位置
                    if (k <= 2 && !h[paiStr[k]][(i != k) * 4 + distributeLong[longCount][j][k]]) {
                        flag = false;
                        break;
                    }
                    if (k == 3 && !p[paiStr[k]][(i != k) * 4 + distributeLong[longCount][j][k]]) {
                        flag = false;
                        break;
                    }
                }
            }
            catch (e) {
                flag = false
            }
            if (flag)
                return true;
        }
    }
    return false;
}

分类标签:游戏 麻将

目录