yemaster的小窝

简单麻将bot示例

2024-05-11
43

在上篇文章中,我们简单介绍了一些胡牌算法的实现。现在,我们根据这个实现,编写对应的简单的胡牌机器人。

关于七对子和碎胡的机器人的编写非常容易,因此本文将着重阐述顺胡的AI算法编写。

类似胡牌算法,我们依旧将牌分成4个部分:万筒条和字牌。为了简单,我们不考虑龙的情况。接着,根据当前牌型和牌堆中剩余的牌,计算出胡牌的概率。然后每次打牌就打出打出之后胡牌概率最大的牌即可。

胡牌概率的计算

分别对每一部分进行考虑。首先预处理出所有可以胡牌的牌型。由于整副牌只有1个对子,所以要分别处理有对子和没对子的情况。这一步直接用dfs先生成所有可能的牌型,然后暴力判断就可以了:

allPaiSets = []
nowPaiSet = [0 for i in range(9)]

def factorial(n: int) -> int:
    d = 1
    for i in range(1, n + 1):
        d *= i
    return d

def binomial(n: int, m: int) -> int:
    return factorial(n) // factorial(m) // factorial(n - m)

def permutation(n: int, m: int) -> int:
    d = 1
    for i in range(n, n - m, -1):
        d *= i
    return d

def genAllSets(u: int, num: int) -> None:
    global allPaiSets, nowPaiSet
    if u >= 9:
        allPaiSets.append(nowPaiSet.copy())
        return
    for i in range(5):
        if num + i >= 14:
            break
        nowPaiSet[u] = i
        genAllSets(u + 1, num + i)

allHuSets1 = [] # No Pairs
allHuSets2 = [] # 1 Pair

def judgeSet(pair: bool) -> bool:
    global nowPaiSet
    if sum(nowPaiSet) == 0:
        return not pair
    pos = 0
    for i in range(len(nowPaiSet)):
        if nowPaiSet[i] > 0:
            pos = i
            break
    flag = False
    if nowPaiSet[pos] >= 2 and pair:
        nowPaiSet[pos] -= 2
        flag = flag or judgeSet(False)
        nowPaiSet[pos] += 2
    if nowPaiSet[pos] >= 3:
        nowPaiSet[pos] -= 3
        flag = flag or judgeSet(pair)
        nowPaiSet[pos] += 3
    if pos <= 6 and nowPaiSet[pos] >= 1 and nowPaiSet[pos + 1] >= 1 and nowPaiSet[pos + 2] >= 1:
        nowPaiSet[pos] -= 1
        nowPaiSet[pos + 1] -= 1
        nowPaiSet[pos + 2] -= 1
        flag = flag or judgeSet(pair)
        nowPaiSet[pos] += 1
        nowPaiSet[pos + 1] += 1
        nowPaiSet[pos + 2] += 1
    return flag

def getAllHuSets() -> None:
    global nowPaiSet
    for s in allPaiSets:
        nowPaiSet = s.copy()
        if judgeSet(False):
            allHuSets1.append(s.copy())
        nowPaiSet = s.copy()
        if judgeSet(True):
            allHuSets2.append(s.copy())

if __name__ == "__main__":
    genAllSets(0, 0)
    getAllHuSets()
    with open("sets1.txt", "w") as f:
        for i in allHuSets1:
            f.write("".join([str(l) for l in i])+"\n")
    with open("sets2.txt", "w") as f:
        for i in allHuSets2:
            f.write("".join([str(l) for l in i])+"\n")

字牌的处理是类似的,因此这里不再展示。

处理出这个表之后,我们只要把当前牌型跟所有牌型一一进行比较,计算出到达这一牌型的的概率,最后取最大值就可以了:

#include <cstdio>
#include <algorithm>
#include <cstring>

using namespace std;

#define multier 100.0

int huSet1[8023][20], huSet2[8023][20];
int wordSet1[300][20], wordSet2[300][20];

long double factorial(int n)
{
    long double res = (long double)(1.0);
    for (int i = 2; i <= n; ++i)
        res *= i;
    return res;
}

long double C(int n, int m)
{
    long double res = (long double)(1.0);
    for (int i = 1; i <= m; ++i)
        res = res * (i + n - m) / i;
    return res;
}

long double P(int n, int m)
{
    long double res = (long double)(1.0);
    for (int i = 1; i <= m; ++i)
        res = res * (i + n - m);
    return res;
}

int main()
{
    FILE *f1, *f2;
    // 读取所有的文件
    f1 = fopen("sets1.txt", "r");
    char ch;
    int cnt1 = 0;
    int pos = 0;
    while ((ch = fgetc(f1)) != EOF)
    {
        if (ch == '\n')
        {
            cnt1++;
            pos = 0;
        }
        else
            huSet1[cnt1][pos++] = ch - '0';
    }
    fclose(f1);
    // 读取sets2.txt,words1.txt,words2.txt,代码略

    int last[35];
    for (int i = 0; i < 34; ++i)
        scanf("%d", &last[i]); // 读取34种牌每种牌还剩几张。
    int sum = 0;
    for (int i = 0; i < 34; ++i)
        sum += last[i];

    int cardLen = 0;
    int myCard[15];
    scanf("%d", &cardLen); // 读取当前手牌的长度

    int T = 0;
    scanf("%d", &T);
    while (T--)
    {
        for (int i = 0; i < cardLen; ++i)
            scanf("%d", &myCard[i]);
        int parts[5][12];
        memset(parts, 0, sizeof(parts));
        for (int i = 0; i < cardLen; ++i)
            parts[myCard[i] / 9][myCard[i] % 9]++;
        long double prob[8][14][14];
        memset(prob, 0, sizeof(prob));
        for (int i = 0; i < 3; ++i)
        {
            // 计算没对子情况时的概率
            for (int j = 0; j < cnt1; ++j)
            {
                bool flag = false;
                int need[9];
                int need_sum = 0;
                int more[9];
                int more_sum = 0;
                memset(need, 0, sizeof(need)); // 记录每张牌还差多少或者多了多少
                memset(more, 0, sizeof(more));
                for (int k = 0; k < 9; ++k)
                {
                    if (parts[i][k] > huSet1[j][k])
                        more[k] = parts[i][k] - huSet1[j][k];
                    else
                    {
                        need[k] = huSet1[j][k] - parts[i][k];
                        if (need[k] > last[i * 9 + k])
                        {
                            flag = true;
                            break;
                        }
                    }
                    need_sum += need[k];
                    more_sum += more[k];
                }
                if (flag)
                    continue;
                long double p = (long double)(multier);
                int tmp = need_sum;
                for (int k = 0; k < 9; ++k)
                {
                    if (!need[k])
                        continue;
                    p *= C(need_sum, need[k]);
                    p *= P(last[i * 9 + k], need[k]);
                    need_sum -= need[k];
                }
                p /= P(sum, tmp);
                prob[i << 1][tmp][more_sum] = max(prob[i << 1][tmp][more_sum], p);
            }
            // 有对子的情况,代码雷同,略
        }
        // 字牌的处理情况与上面雷同,略
        long double maxP = 0;
        // 用7重循环,分别枚举每一种牌的进几张出几张,并枚举对子在那里
        for (int i1 = 0; i1 <= 14; ++i1)
            for (int i2 = 0; i2 <= 14; ++i2)
                for (int i3 = 0; i3 <= 14; ++i3)
                    for (int i4 = 0; i4 <= 14; ++i4)
                        for (int i5 = 0; i5 <= 14; ++i5)
                            for (int i6 = 0; i6 <= 14; ++i6)
                                for (int i7 = 0; i7 <= 14; ++i7)
                                {
                                    if (i1 + i3 + i5 + i7 >= 14)
                                        break;
                                    int i8 = -i2 - i4 - i6 - 1 + i1 + i3 + i5 + i7;
                                    if (i8 < 0 || i8 > 14)
                                        continue;
                                    if (i2 + i4 + i6 + i8 >= cardLen)
                                        break;
                                    for (int j = 0; j < 4; ++j)
                                    {
                                        long double p = (long double)(1.0);
                                        p *= prob[0 | (int)(j == 0)][i1][i2];
                                        p *= prob[2 | (int)(j == 1)][i3][i4];
                                        p *= prob[4 | (int)(j == 2)][i5][i6];
                                        p *= prob[6 | (int)(j == 3)][i7][i8];
                                        maxP = max(p, maxP);  //所有情况种取最大值
                                    }
                                }
        printf("%.16Lf\n", maxP);
    }
    return 0;
}

得到了胡牌概率估计算法,我们只需要在游戏中枚举打掉的牌,计算打掉之后胡牌概率,最后选择概率最大的牌打掉即可。

关于吃碰杠,同样也是,计算操作后概率是否会提升,来判断是否要进行操作。

分类标签:游戏 麻将

目录