在上篇文章中,我们简单介绍了一些胡牌算法的实现。现在,我们根据这个实现,编写对应的简单的胡牌机器人。
关于七对子和碎胡的机器人的编写非常容易,因此本文将着重阐述顺胡的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;
}
得到了胡牌概率估计算法,我们只需要在游戏中枚举打掉的牌,计算打掉之后胡牌概率,最后选择概率最大的牌打掉即可。
关于吃碰杠,同样也是,计算操作后概率是否会提升,来判断是否要进行操作。