前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家:点击跳转
目录
〇,前言
本文代码修改了数次,但是只保留了有代表性的V201912和V202001,版本名是“年+月”。
本来是帮一个朋友写的作业,结果自从博客发表之后,我发现每隔一段时间,这篇博客就有挺多人看。
其中还有几个人加了我QQ或微信,我一问,全都是要交作业的大学生。
唉,当代大学生啊。
一,五子棋棋盘
棋盘正中一点为“天元”。棋盘两端的横线称端线。棋盘左右最外边的两条纵线称边线。从两条端线和两条边线向正中发展而纵横交叉在第四条线形成的四个点称为“星”。
以持黑方为准,棋盘上的纵轴线从左到右用英文字母A~O标记。横行线从近到远用阿拉伯数字1~15标记。纵横轴上的横纵线交叉点分别用横纵线标记的名称合写成。如“天元”H8,四个“星”分别为D4、D12、L12、L4等。
二,五子棋比赛规则
1,行棋顺序
黑先、白后,从天元开始相互顺序落子。
2,判断胜负
最先在棋盘横向、竖向、斜向形成连续的相同色五个棋子的一方为胜。
黑棋禁手判负,白棋无禁手。黑棋禁手包括三三禁手,四四禁手,长连禁手。
如分不出胜负,则定为平局。
三,重要棋型解释
1,五连
五颗同色棋子连在一起,
即4个方向的11111这种形式的棋型。
PS:这里1表示同色棋子,0表示空格
2,活四
有2个成五点的四颗棋子,
即4个方向的011110这种形式的棋型,注意两边一定要有空格。
3,冲四
有1个成五点的四颗棋子,棋型有点多。
4,活三
可以形成活四的三颗棋子,
要么是三连的形式,即8个方向的011100这种形式的棋型
要么是非三连的形式,即8个方向的010110这种形式的棋型
四,禁手规则
1,三三禁手
由于黑方落一子,同时形成二个或二个以上黑方活三的局面
2,四四禁手
由于黑方落一子,同时形成二个或二个以上黑方四(活四或者冲四)的局面
3,长连禁手
由于黑方落一子,形成六个或者六个以上的同色连续棋子
4,抓禁
如果白方的成五点在黑方的禁手点,轮到黑方下,黑方也是不能下这个位置的。此时黑方只要没有成五点就直接输了。
这个取胜的技巧叫抓禁。
五,代码解释
1,棋子表示
为了使自已与对手看得更清楚,刚落下的子区别表示,
白方正常子:○ 白方刚落下之子:△
黑方正常字:● 黑方刚落下字:▲
2,棋盘表示
利用专门画棋盘的9个拓展字符,可以在控制台上画出非常漂亮的棋盘
out函数用来画棋盘的一个格子,要么是表示棋盘的9个拓展字符,要么是表示棋子的4个拓展字符
DrawBoard函数用来打印整个游戏界面,需要调用out函数
3,flat技术
通过dx和dy这2个常数数组,存下8个方向的向量,就可以把棋型判断、禁手判断等二维问题化作一维问题。
通过for循环即可遍历每个方向,使得代码变得非常简洁。
4,棋型判断和禁手判断
对于任何一个可以落子的位置,要独立的判断如果落子就会形成几个活四,几个冲四,几个活三。
三三禁手和四四禁手直接根据三种棋型的数量判断即可,长连禁手单独判断,很简单。
4.1 活四
判断活4的逻辑比较简单,遍历4个方向,判断是不是011110的形式即可。
PS:1011110对于左边的1是冲四,对于右边的4个1是活四。
4.2 冲四
冲4的棋型比较多,总之只要有成五点,要么是活四,要么是冲四。
进一步,我们可以得到,成五点的数量是活四的数量*2+冲四的数量。
而成五点的数量可以这么求:
在8个方向上求成五点,对于每个方向,从当前位置往前延伸,第一个空格是这个方向唯一可能的成五点,跳过第一个空格继续往前延伸直到不是自身棋子的位置A,同时从当前位置往反方向延伸直到不是自身棋子的位置B,如果AB的距离大于5,即AB之间至少能放5个棋子,那么该方向就有一个成五点。
由此,我们可以根据成五点的数量和活四的数量求出冲四的数量。
4.3 活3
分开计算三连活3的数量和非3连的活3的数量,然后加起来
5,AI算法
AI 采取三层的极大极小算法,基于固定的打分机制,对每个落子位置进行打分,从而得到比较优的解。
6,AI的打分机制
为了降低计算量,采取对每个落子位置单独进行打分的方法。
打分核心机制:在不形成禁手的前提下,形成最优棋型。
落子之后计算棋型,活四计1000分,冲四和活三都计100分。
PS:虽然规则允许下禁手点,但是禁手判负,所以AI认为黑方绝对不会下禁手点(无论黑方是AI还是玩家)
7,搜索剪枝
AI采取的策略是,如果要落子,周围的8个邻居至少要有一个棋子,无论是黑是白。
对于不满足这个条件的地方,AI是不下的,直接减掉。
(PS:这个限制会导致AI的水平下降,但是计算速度大大提升。当然,如果被对方知道了这个限制,也很容易基于此完胜AI)
基于这个剪枝策略,调整打分机制:落子位置的8个邻居中,只要是有落子的位置,无论是黑是白,都计1分。
(对于边界或角落上的点,只有5个或者3个邻居,为了编程方便,棋盘本身应该用一圈空格包围)
这样的话,对于0分的点直接忽略,即可大大增加剪枝效率。
8,棋谱和禁手调试
代码有棋谱功能,棋谱存在manu数组中,下棋过程中可以随时输出棋谱,只要把棋谱复制下来,下次运行直接全部粘贴即可,这样就很方便调试,因为我的AI不带随机行为,所以每次相同情况下给出的结果也都是固定的。
比如调试三三禁手:(玩家执黑)
(1)H8 J10 J9 I8 (活三)
(2)D14 H7 C13 C11 D10 (不是活三)
调试四四禁手:
(3)H1 L9 L10 M11 N11 K11 L12 (俩活四)
(4)A1 B2 B4 A5 D2 D4(俩冲四)
(5)A1 B2 C3 C5 B6 E3 (活四加冲四)
PS:调试禁手代码时,可以把main函数中的while (!is_end)改为while (true),便于调试。让AI占先机,AI就不会妨碍我们随便做禁手。
六,代码
V201912
#include <stdio.h>
#include<string>
#include<windows.h>
#define N 15
#define samep same(row + dx[u] * i, col + dy[u] * i, p[row][col])
#define off if(!inboard(row + dx[u] * i, col + dy[u] * i) || p[row + dx[u] * i][col + dy[u] * i] != 0)continue;
int p[N + 2][N + 2]; //0空1黑2白 1●2○ -1▲-2△
int s = 0, ais = 1, s0;//s是轮到谁下,s=1,2,s=1是ai下,s=2是玩家,s=s0是黑方下,否则是白方下
bool is_end = false;
int dx[8] = { 1, 1, 0, -1, -1, -1, 0, 1 }; //flat技术
int dy[8] = { 0, 1, 1, 1, 0, -1, -1, -1 };//(dx,dy)是8个方向向量
int manu[2][300], manukey = 0;
int out(int i, int j)
{
if (p[i][j] == 1)return printf("●");
if (p[i][j] == 2)return printf("○");
if (p[i][j] == -1)return printf("▲");
if (p[i][j] == -2)return printf("△");
if (i == N)
{
if (j == 1)return printf("┏");
if (j == N)return printf("┓");
return printf("┯");
}
if (i == 1)
{
if (j == 1)return printf("┗");
if (j == N)return printf("┛");
return printf("┷");
}
if (j == 1)return printf("┠");
if (j == N)return printf("┨");
return printf("┼");
}
void DrawBoard()//画棋盘
{
system("cls");
int row = 0, col = 0, keyr = 0, keyc = 0;
char alpha = 'A';
printf("\n\n\n ");
for (col = 1; col <= N; col++)printf("%c ", alpha++);
for (row = N; row >= 1; row--)
{
printf("\n %2d", row);
for (col = 1; col <= N; col++)
{
out(row, col);
if (p[row][col] < 0)keyr = row, keyc = col;
}
printf("%d", row);
}
alpha = 'A';
printf("\n ");
for (col = 1; col <= N; col++)printf("%c ", alpha++);
printf("\n\n");
if (s0 == ais)printf(" AI执黑,玩家执白\n");
else printf(" AI执白,玩家执黑\n");
alpha = 'A';
if (keyr)printf(" 最后落子位置:%c%d\n", alpha + keyc - 1, keyr);
}
void init()
{
system("color f0");
printf("输入1或者2进行选择\n1,AI执黑先行\n2,玩家执黑先行\n");
scanf_s("%d", &s);
if (s != 1 && s != 2)return init();
s0 = s;
int i, j;
for (i = 0; i <= N + 1; i++)for (j = 0; j <= N + 1; j++)p[i][j] = 0;//以空格包围棋盘
DrawBoard();
for (j = 0; j < 300; j++)manu[0][j] = manu[1][j] = 0;
}
bool inboard(int row, int col)//是否在棋盘内
{
if (row <1 || row > N)return false;
return col >= 1 && col <= N;
}
int same(int row, int col, int key)//判断2个棋子是否同色
{
if (!inboard(row, col))return false;
return (p[row][col] == key || p[row][col] + key == 0);
}
int num(int row, int col, int u)//坐标(row,col),方向向量u
{
int i = row + dx[u], j = col + dy[u], sum = 0, ref = p[row][col];
if (ref == 0)return 0;
while (same(i, j, ref))sum++, i += dx[u], j += dy[u];
return sum;
}
int live4(int row, int col)//活4的数量
{
int sum = 0, i, u;
for (u = 0; u < 4; u++)//4个方向,每个方向最多1个
{
int sumk = 1;
for (i = 1; samep; i++)sumk++;
off;
for (i = -1; samep; i--)sumk++;
off;
if (sumk == 4)sum++;
}
return sum;
}
int chong4(int row, int col)//冲4的数量
{
int sum = 0, i, u;
for (u = 0; u < 8; u++)//8个方向,每个方向最多1个
{
int sumk = 0;
bool flag = true;
for (i = 1; samep || flag; i++)//成五点的方向
{
if (!samep)
{
if (flag&&p[row + dx[u] * i][col + dy[u] * i])sumk -= 10;
flag = false;
}
sumk++;
}
if (!inboard(row + dx[u] * --i, col + dy[u] * i))continue;
for (i = -1; samep; i--)sumk++;
if (sumk == 4)sum++;
}
return sum - live4(row, col) * 2;
}
int live3(int row, int col)//活3的数量
{
int key = p[row][col], sum = 0, i, u;
for (u = 0; u < 4; u++)//三连的活三
{
int sumk = 1;
for (i = 1; samep; i++)sumk++;
off;
i++;
off;
for (i = -1; samep; i--)sumk++;
off;
i++;
off;
if (sumk == 3)sum++;
}
for (u = 0; u < 8; u++)//8个方向,每个方向最多1个非三连的活三
{
int sumk = 0;
bool flag = true;
for (i = 1; samep || flag; i++)//成活四点的方向
{
if (!samep)
{
if (flag&&p[row + dx[u] * i][col + dy[u] * i])sumk -= 10;
flag = false;
}
sumk++;
}
off;
if (p[row + dx[u] * --i][col + dy[u] * i] == 0)continue;
for (i = 1; samep; i++)sumk++;
off;
if (sumk == 3)sum++;
}
return sum;
}
bool overline(int row, int col)//长连禁手
{
for (int u = 0; u < 4; u++)if (num(row, col, u) + num(row, col, u + 4) > 4)return true;
return false;
}
bool ban(int row, int col)//判断落子后是否成禁手
{
if (same(row, col, 2))return false;//白方无禁手
return live3(row, col) > 1 || overline(row, col) || live4(row, col) + chong4(row, col) > 1;
}
bool end_(int row, int col)//(row,col)处落子之后是否游戏结束
{
for (int u = 0; u < 4; u++)if (num(row, col, u) + num(row, col, u + 4) >= 4)is_end = true;
if (is_end)return true;
is_end = ban(row, col);
return is_end;
}
void go(int row, int col)//落下一子
{
if (s == s0)p[row][col] = -1; //标出最新下的棋
else p[row][col] = -2;
for (int i = 0; i <= N; i++)for (int j = 0; j <= N; j++) //取消上一个最新棋的标识
{
if (i == row && j == col)continue;
if (p[i][j] < 0)p[i][j] *= -1;
}
DrawBoard();
if (ban(row, col))
{
if (s0 == 1)printf("玩家胜");
else printf("AI胜");
Sleep(10000);
}
if (end_(row, col))
{
if (s == ais)printf("AI胜");
else printf("玩家胜");
Sleep(10000);
}
manu[0][manukey] = row, manu[1][manukey++] = col;
}
bool ok(int row, int col)//能否落子
{
return inboard(row, col) && (p[row][col] == 0);
}
int point(int row, int col)//非负分值
{
if (ban(row, col))return 0;//禁手0分
if (end_(row, col))
{
is_end = false;
return 10000;
}
int ret = live4(row, col) * 1000 + (chong4(row, col) + live3(row, col)) * 100, u;
for (u = 0; u < 8; u++)if (p[row + dx[u]][col + dy[u]])ret++;//无效点0分
return ret;
}
int AI3(int p2)
{
int keyp = -100000, tempp;
for (int i = 1; i <= N; i++)for (int j = 1; j <= N; j++)
{
if (!ok(i, j))continue;
p[i][j] = s0;
tempp = point(i, j);
if (tempp == 0)
{
p[i][j] = 0;
continue;
}
if (tempp == 10000)
{
p[i][j] = 0;
return 10000;
}
p[i][j] = 0;
if (tempp - p2 * 2 > keyp)keyp = tempp - p2 * 2;//第三层取极大
}
return keyp;
}
int AI2()
{
int keyp = 100000, tempp;
for (int i = 1; i <= N; i++)for (int j = 1; j <= N; j++)
{
if (!ok(i, j))continue;
p[i][j] = 3 - s0;
tempp = point(i, j);
if (tempp == 0)
{
p[i][j] = 0;
continue;
}
if (tempp == 10000)
{
p[i][j] = 0;
return -10000;
}
tempp = AI3(tempp);
p[i][j] = 0;
if (tempp < keyp)keyp = tempp;//第二层取极小
}
return keyp;
}
void AI()
{
DrawBoard();
printf(" 轮到AI下,请稍候: ");
if (p[8][8] == 0)return go(8, 8);
int i, j;
int keyp = -100000, keyi, keyj, tempp;
for (i = 1; i <= N; i++)
{
for (j = 1; j <= N; j++)
{
if (!ok(i, j))continue;
p[i][j] = s0;
tempp = point(i, j);
if (tempp == 0)
{
p[i][j] = 0;
continue;
}//高效剪枝,避开了禁手点和无效点
if (tempp == 10000)return go(i, j);
tempp = AI2();
p[i][j] = 0;
if (tempp > keyp)keyp = tempp, keyi = i, keyj = j;//第一层取极大
}
}
return go(keyi, keyj);
}
void out_manual()
{
char alpha = 'A';
int i;
printf("\n 黑方落子位置: ");
for (i = 0; i < manukey; i += 2)printf(" %c%d", alpha + manu[1][i] - 1, manu[0][i]);
printf("\n 白方落子位置: ");
for (i = 1; i < manukey; i += 2)printf(" %c%d", alpha + manu[1][i] - 1, manu[0][i]);
Sleep(5000);
}
void player()
{
DrawBoard();
printf(" 轮到玩家下,请输入坐标(输入=0查看棋谱): ");
char c = '\n';
int row = 0, col = 0;
while (c<'0')scanf("%c%d", &c, &row);
if (c == '=')
{
out_manual();
return player();
}
if (c < 'a')col = c - 'A' + 1;
else col = c - 'a' + 1;
if (!ok(row, col))
{
printf("此处不能下");
Sleep(1000);
return player();
}
go(row, col);
}
void main()
{
init();
while (!is_end)
{
if (s == ais)AI();
else player();
s = 3 - s;//换下棋方
}
return;
}
V202001
2020年1月再次更新,修改了宏,使得代码可读性更高,修改了live3函数的逻辑。
#include <stdio.h>
#include<string>
#include<windows.h>
#define N 15
#define same_u_i same(row + dx[u] * i, col + dy[u] * i, p[row][col])//u方向i距离的点是否同色
#define OutOrNotEmpty (!inboard(row + dx[u] * i, col + dy[u] * i) || p[row + dx[u] * i][col + dy[u] * i] != 0) //出了棋盘或者非空格点
int p[N + 2][N + 2]; //0空1黑2白 1●2○ -1▲-2△
int s = 0, ais = 1, s0;//s是轮到谁下,s=1,2,s=1是ai下,s=2是玩家,s=s0是黑方下,否则是白方下
bool is_end = false;
int dx[8] = { 1, 1, 0, -1, -1, -1, 0, 1 }; //flat技术
int dy[8] = { 0, 1, 1, 1, 0, -1, -1, -1 };//(dx,dy)是8个方向向量
int manu[2][300], manukey = 0;//棋谱
int out(int i, int j)//打印棋盘
{
if (p[i][j] == 1)return printf("●");
if (p[i][j] == 2)return printf("○");
if (p[i][j] == -1)return printf("▲");
if (p[i][j] == -2)return printf("△");
if (i == N)
{
if (j == 1)return printf("┏");
if (j == N)return printf("┓");
return printf("┯");
}
if (i == 1)
{
if (j == 1)return printf("┗");
if (j == N)return printf("┛");
return printf("┷");
}
if (j == 1)return printf("┠");
if (j == N)return printf("┨");
return printf("┼");
}
void DrawBoard()//打印整个游戏界面
{
system("cls");
int row = 0, col = 0, keyr = 0, keyc = 0;
char alpha = 'A';
printf("\n\n\n ");
for (col = 1; col <= N; col++)printf("%c ", alpha++);
for (row = N; row >= 1; row--)
{
printf("\n %2d", row);
for (col = 1; col <= N; col++)
{
out(row, col);
if (p[row][col] < 0)keyr = row, keyc = col;
}
printf("%d", row);
}
alpha = 'A';
printf("\n ");
for (col = 1; col <= N; col++)printf("%c ", alpha++);
printf("\n\n");
if (s0 == ais)printf(" AI执黑,玩家执白\n");
else printf(" AI执白,玩家执黑\n");
alpha = 'A';
if (keyr)printf(" 最后落子位置:%c%d\n", alpha + keyc - 1, keyr);
}
void init()//游戏开局初始化
{
system("color f0");
printf("输入1或者2进行选择\n1,AI执黑先行\n2,玩家执黑先行\n");
scanf("%d", &s);
if (s != 1 && s != 2)return init();
s0 = s;
int i, j;
for (i = 0; i <= N + 1; i++)for (j = 0; j <= N + 1; j++)p[i][j] = 0;//以空格包围棋盘
DrawBoard();
for (j = 0; j < 300; j++)manu[0][j] = manu[1][j] = 0;
}
bool inboard(int row, int col)//判断(row,col)是否在棋盘内
{
if (row <1 || row > N)return false;
return col >= 1 && col <= N;
}
int same(int row, int col, int key)//判断2个棋子是否同色
{
if (!inboard(row, col))return false;
return (p[row][col] == key || p[row][col] + key == 0);
}
int num(int row, int col, int u)//坐标(row,col),方向向量u,返回该方向有多少连续同色棋子
{
int i = row + dx[u], j = col + dy[u], sum = 0, ref = p[row][col];
if (ref == 0)return 0;
while (same(i, j, ref))sum++, i += dx[u], j += dy[u];
return sum;
}
int live4(int row, int col)//落子成活4的数量
{
int sum = 0, i, u;
for (u = 0; u < 4; u++)//4个方向,判断每个方向是否落子就成活4
{
int sumk = 1;
for (i = 1; same_u_i; i++)sumk++;
if(OutOrNotEmpty)continue;
for (i = -1; same_u_i; i--)sumk++;
if(OutOrNotEmpty)continue;
if (sumk == 4)sum++;
}
return sum;
}
int cheng5(int row, int col)//成5点的数量
{
int sum = 0, i, u;
for (u = 0; u < 8; u++)//8个成五点的方向
{
int sumk = 0;
bool flag = true;
for (i = 1; same_u_i || flag; i++)
{
if (!same_u_i)//该方向的第一个不同色的点,超出边界或者对方棋子或空格
{
if (p[row + dx[u] * i][col + dy[u] * i])sumk -= 10;//该方向的第一个不同色的点是对方棋子,没有成五点
flag = false;
}
sumk++;
}
if (!inboard(row + dx[u] * --i, col + dy[u] * i))continue;//该方向的第一个不同色的点是超出边界,没有成五点
for (i = -1; same_u_i; i--)sumk++;
if (sumk == 4)sum++;
}
return sum;
}
int chong4(int row, int col)//冲4的数量
{
return cheng5(row, col) - live4(row, col) * 2;
}
int live3(int row, int col)//落子成活3的数量
{
int key = p[row][col], sum = 0, i, u,flag=2;
for (u = 0; u < 4; u++)//三连的活三
{
int sumk = 1;
for (i = 1; same_u_i; i++)sumk++;
if(OutOrNotEmpty)continue;
i++;
if(OutOrNotEmpty)flag--;
for (i = -1; same_u_i; i--)sumk++;
if(OutOrNotEmpty)continue;
i--;
if(OutOrNotEmpty)flag--;
if (sumk == 3 && flag>0)sum++;
}
for (u = 0; u < 8; u++)//8个方向,每个方向最多1个非三连的活三
{
int sumk = 0;
bool flag = true;
for (i = 1; same_u_i || flag; i++)//成活四点的方向
{
if (!same_u_i)
{
if (flag&&p[row + dx[u] * i][col + dy[u] * i])sumk -= 10;
flag = false;
}
sumk++;
}
if(OutOrNotEmpty)continue;;
if (p[row + dx[u] * --i][col + dy[u] * i] == 0)continue;
for (i = 1; same_u_i; i++)sumk++;
if(OutOrNotEmpty)continue;;
if (sumk == 3)sum++;
}
return sum;
}
bool overline(int row, int col)//长连禁手
{
for (int u = 0; u < 4; u++)if (num(row, col, u) + num(row, col, u + 4) > 4)return true;
return false;
}
bool ban(int row, int col)//判断落子后是否成禁手
{
if (same(row, col, 2))return false;//白方无禁手
return live3(row, col) > 1 || overline(row, col) || live4(row, col) + chong4(row, col) > 1;
}
bool end_(int row, int col)//(row,col)处落子之后是否游戏结束
{
for (int u = 0; u < 4; u++)if (num(row, col, u) + num(row, col, u + 4) >= 4)is_end = true;
if (is_end)return true;
is_end = ban(row, col);
return is_end;
}
void go(int row, int col)//落下一子
{
if (s == s0)p[row][col] = -1; //标出最新下的棋
else p[row][col] = -2;
for (int i = 0; i <= N; i++)for (int j = 0; j <= N; j++) //取消上一个最新棋的标识
{
if (i == row && j == col)continue;
if (p[i][j] < 0)p[i][j] *= -1;
}
DrawBoard();
if (ban(row, col))
{
printf("禁手\n");
if (s0 == 1)printf("玩家胜");
else printf("AI胜");
Sleep(10000);
}
if (end_(row, col))
{
if (s == ais)printf("AI胜");
else printf("玩家胜");
Sleep(10000);
}
manu[0][manukey] = row, manu[1][manukey++] = col;
}
bool ok(int row, int col)//能否落子
{
return inboard(row, col) && (p[row][col] == 0);
}
int point(int row, int col)//非负分值
{
if (ban(row, col))return 0;//禁手0分
if (end_(row, col))
{
is_end = false;
return 10000;
}
int ret = live4(row, col) * 1000 + (chong4(row, col) + live3(row, col)) * 100, u;
for (u = 0; u < 8; u++)if (p[row + dx[u]][col + dy[u]])ret++;//无效点0分
return ret;
}
int AI3(int p2)
{
int keyp = -100000, tempp;
for (int i = 1; i <= N; i++)for (int j = 1; j <= N; j++)
{
if (!ok(i, j))continue;
p[i][j] = s0;
tempp = point(i, j);
if (tempp == 0)
{
p[i][j] = 0;
continue;
}
if (tempp == 10000)
{
p[i][j] = 0;
return 10000;
}
p[i][j] = 0;
if (tempp - p2 * 2 > keyp)keyp = tempp - p2 * 2;//第三层取极大
}
return keyp;
}
int AI2()
{
int keyp = 100000, tempp;
for (int i = 1; i <= N; i++)for (int j = 1; j <= N; j++)
{
if (!ok(i, j))continue;
p[i][j] = 3 - s0;
tempp = point(i, j);
if (tempp == 0)
{
p[i][j] = 0;
continue;
}
if (tempp == 10000)
{
p[i][j] = 0;
return -10000;
}
tempp = AI3(tempp);
p[i][j] = 0;
if (tempp < keyp)keyp = tempp;//第二层取极小
}
return keyp;
}
void AI()
{
DrawBoard();
printf(" 轮到AI下,请稍候: ");
if (p[8][8] == 0)return go(8, 8);
int i, j;
int keyp = -100000, keyi, keyj, tempp;
for (i = 1; i <= N; i++)
{
for (j = 1; j <= N; j++)
{
if (!ok(i, j))continue;
p[i][j] = s0;
tempp = point(i, j);
if (tempp == 0)
{
p[i][j] = 0;
continue;
}//高效剪枝,避开了禁手点和无效点
if (tempp == 10000)return go(i, j);
tempp = AI2();
p[i][j] = 0;
if (tempp > keyp)keyp = tempp, keyi = i, keyj = j;//第一层取极大
}
}
return go(keyi, keyj);
}
void out_manual()
{
char alpha = 'A';
int i;
printf("\n 黑方落子位置: ");
for (i = 0; i < manukey; i += 2)printf(" %c%d", alpha + manu[1][i] - 1, manu[0][i]);
printf("\n 白方落子位置: ");
for (i = 1; i < manukey; i += 2)printf(" %c%d", alpha + manu[1][i] - 1, manu[0][i]);
Sleep(5000);
}
void player()
{
DrawBoard();
printf(" 轮到玩家下,请输入坐标(输入=0查看棋谱): ");
char c = '\n';
int row = 0, col = 0;
while (c<'0')scanf("%c%d", &c, &row);
if (c == '=')
{
out_manual();
return player();
}
if (c < 'a')col = c - 'A' + 1;
else col = c - 'a' + 1;
if (!ok(row, col))
{
printf("此处不能下");
Sleep(1000);
return player();
}
go(row, col);
}
void main()
{
init();
while (!is_end)
{
if (s == ais)AI();
else player();
s = 3 - s;//换下棋方
}
return;
}
V202401
再次修改了live3函数的逻辑。
#include <stdio.h>
#include<string>
#include<windows.h>
#define N 15
#define same_u_i same(row + dx[u] * i, col + dy[u] * i, p[row][col])//u方向i距离的点是否同色
#define OutOrNotEmpty (!inboard(row + dx[u] * i, col + dy[u] * i) || p[row + dx[u] * i][col + dy[u] * i] != 0) //出了棋盘或者非空格点
int p[N + 2][N + 2]; //0空1黑2白 1●2○ -1▲-2△
int s = 0, ais = 1, s0;//s是轮到谁下,s=1,2,s=1是ai下,s=2是玩家,s=s0是黑方下,否则是白方下
bool is_end = false;
int dx[8] = { 1, 1, 0, -1, -1, -1, 0, 1 }; //flat技术
int dy[8] = { 0, 1, 1, 1, 0, -1, -1, -1 };//(dx,dy)是8个方向向量
int manu[2][300], manukey = 0;//棋谱
int out(int i, int j)//打印棋盘
{
if (p[i][j] == 1)return printf("●");
if (p[i][j] == 2)return printf("○");
if (p[i][j] == -1)return printf("▲");
if (p[i][j] == -2)return printf("△");
if (i == N)
{
if (j == 1)return printf("┏");
if (j == N)return printf("┓");
return printf("┯");
}
if (i == 1)
{
if (j == 1)return printf("┗");
if (j == N)return printf("┛");
return printf("┷");
}
if (j == 1)return printf("┠");
if (j == N)return printf("┨");
return printf("┼");
}
void DrawBoard()//打印整个游戏界面
{
system("cls");
int row = 0, col = 0, keyr = 0, keyc = 0;
char alpha = 'A';
printf("\n\n\n ");
for (col = 1; col <= N; col++)printf("%c ", alpha++);
for (row = N; row >= 1; row--)
{
printf("\n %2d", row);
for (col = 1; col <= N; col++)
{
out(row, col);
if (p[row][col] < 0)keyr = row, keyc = col;
}
printf("%d", row);
}
alpha = 'A';
printf("\n ");
for (col = 1; col <= N; col++)printf("%c ", alpha++);
printf("\n\n");
if (s0 == ais)printf(" AI执黑,玩家执白\n");
else printf(" AI执白,玩家执黑\n");
alpha = 'A';
if (keyr)printf(" 最后落子位置:%c%d\n", alpha + keyc - 1, keyr);
}
void init()//游戏开局初始化
{
system("color f0");
printf("输入1或者2进行选择\n1,AI执黑先行\n2,玩家执黑先行\n");
scanf("%d", &s);
if (s != 1 && s != 2)return init();
s0 = s;
int i, j;
for (i = 0; i <= N + 1; i++)for (j = 0; j <= N + 1; j++)p[i][j] = 0;//以空格包围棋盘
DrawBoard();
for (j = 0; j < 300; j++)manu[0][j] = manu[1][j] = 0;
}
bool inboard(int row, int col)//判断(row,col)是否在棋盘内
{
if (row <1 || row > N)return false;
return col >= 1 && col <= N;
}
int same(int row, int col, int key)//判断2个棋子是否同色
{
if (!inboard(row, col))return false;
return (p[row][col] == key || p[row][col] + key == 0);
}
int num(int row, int col, int u)//坐标(row,col),方向向量u,返回该方向有多少连续同色棋子
{
int i = row + dx[u], j = col + dy[u], sum = 0, ref = p[row][col];
if (ref == 0)return 0;
while (same(i, j, ref))sum++, i += dx[u], j += dy[u];
return sum;
}
int live4(int row, int col)//活4的数量
{
int sum = 0, i, u;
for (u = 0; u < 4; u++)//4个方向,判断每个方向是否成活4
{
int sumk = 1;
for (i = 1; same_u_i; i++)sumk++;
if (OutOrNotEmpty)continue;
for (i = -1; same_u_i; i--)sumk++;
if (OutOrNotEmpty)continue;
if (sumk == 4)sum++;
}
return sum;
}
int cheng5(int row, int col)//成5点的数量
{
int sum = 0, i, u;
for (u = 0; u < 8; u++)//8个成五点的方向
{
int sumk = 0;
bool flag = true;
for (i = 1; same_u_i || flag; i++)
{
if (!same_u_i)//该方向的第一个不同色的点,超出边界或者对方棋子或空格
{
if (p[row + dx[u] * i][col + dy[u] * i])sumk -= 10;//该方向的第一个不同色的点是对方棋子,没有成五点
flag = false;
}
sumk++;
}
if (!inboard(row + dx[u] * --i, col + dy[u] * i))continue;//该方向的第一个不同色的点是超出边界,没有成五点
for (i = -1; same_u_i; i--)sumk++;
if (sumk == 4)sum++;
}
return sum;
}
int chong4(int row, int col)//冲4的数量
{
return cheng5(row, col) - live4(row, col) * 2;
}
int live3(int row, int col)//活3的数量
{
int key = p[row][col], sum = 0, i, u, flag = 2;
for (u = 0; u < 4; u++)//三连的活三
{
int sumk = 1;
for (i = 1; same_u_i; i++)sumk++;
if (OutOrNotEmpty)continue;
i++;
if (OutOrNotEmpty)flag--;
for (i = -1; same_u_i; i--)sumk++;
if (OutOrNotEmpty)continue;
i--;
if (OutOrNotEmpty)flag--;
if (sumk == 3 && flag > 0)sum++;
}
for (u = 0; u < 8; u++)//8个方向,每个方向最多1个非三连的活三
{
int sumk = 0;
bool flag = true;
for (i = 1; same_u_i || flag; i++)//成活四点的方向
{
if (!same_u_i)
{
if (p[row + dx[u] * i][col + dy[u] * i])sumk -= 10;
flag = false;
}
sumk++;
}
if (OutOrNotEmpty)continue;
if (p[row + dx[u] * --i][col + dy[u] * i] == 0)continue;
for (i = -1; same_u_i; i--)sumk++;
if (OutOrNotEmpty)continue;
if (sumk == 3)sum++;
}
return sum;
}
bool overline(int row, int col)//长连禁手
{
for (int u = 0; u < 4; u++) {
if (num(row, col, u) + num(row, col, u + 4) > 4)return true;
}
return false;
}
bool ban(int row, int col)//判断落子后是否成禁手
{
if (same(row, col, 2))return false;//白方无禁手
return live3(row, col) > 1 || overline(row, col) || live4(row, col) + chong4(row, col) > 1;
}
bool end_(int row, int col)//(row,col)处落子之后是否游戏结束
{
for (int u = 0; u < 4; u++) {
if (num(row, col, u) + num(row, col, u + 4) >= 4)return is_end = true;
}
return is_end = ban(row, col);
}
void go(int row, int col)//落下一子
{
if (s == s0)p[row][col] = -1; //标出最新下的棋
else p[row][col] = -2;
for (int i = 0; i <= N; i++)for (int j = 0; j <= N; j++) //取消上一个最新棋的标识
{
if (i == row && j == col)continue;
if (p[i][j] < 0)p[i][j] *= -1;
}
DrawBoard();
if (ban(row, col))
{
printf("禁手\n");
if (s0 == 1)printf("玩家胜");
else printf("AI胜");
Sleep(10000);
}
if (end_(row, col))
{
if (s == ais)printf("AI胜");
else printf("玩家胜");
Sleep(10000);
}
manu[0][manukey] = row, manu[1][manukey++] = col;
}
bool ok(int row, int col)//能否落子
{
return inboard(row, col) && (p[row][col] == 0);
}
int point(int row, int col)//非负分值
{
if (ban(row, col))return 0;//禁手0分
if (end_(row, col))
{
is_end = false;
return 10000;
}
int ret = live4(row, col) * 1000 + (chong4(row, col) + live3(row, col)) * 100, u;
for (u = 0; u < 8; u++) {
if (p[row + dx[u]][col + dy[u]])ret++;//无效点0分
}
return ret;
}
int AI3(int p2)
{
int keyp = -100000, tempp;
for (int i = 1; i <= N; i++)for (int j = 1; j <= N; j++)
{
if (!ok(i, j))continue;
p[i][j] = s0;
tempp = point(i, j);
if (tempp == 0)
{
p[i][j] = 0;
continue;
}
if (tempp == 10000)
{
p[i][j] = 0;
return 10000;
}
p[i][j] = 0;
if (tempp - p2 * 2 > keyp)keyp = tempp - p2 * 2;//第三层取极大
}
return keyp;
}
int AI2()
{
int keyp = 100000, tempp;
for (int i = 1; i <= N; i++)for (int j = 1; j <= N; j++)
{
if (!ok(i, j))continue;
p[i][j] = 3 - s0;
tempp = point(i, j);
if (tempp == 0)
{
p[i][j] = 0;
continue;
}
if (tempp == 10000)
{
p[i][j] = 0;
return -10000;
}
tempp = AI3(tempp);
p[i][j] = 0;
if (tempp < keyp)keyp = tempp;//第二层取极小
}
return keyp;
}
void AI()
{
DrawBoard();
printf(" 轮到AI下,请稍候: ");
if (p[8][8] == 0)return go(8, 8);
int i, j;
int keyp = -100000, keyi, keyj, tempp;
for (i = 1; i <= N; i++)
{
for (j = 1; j <= N; j++)
{
if (!ok(i, j))continue;
p[i][j] = s0;
tempp = point(i, j);
if (tempp == 0)
{
p[i][j] = 0;
continue;
}//高效剪枝,避开了禁手点和无效点
if (tempp == 10000)return go(i, j);
tempp = AI2();
p[i][j] = 0;
if (tempp > keyp)keyp = tempp, keyi = i, keyj = j;//第一层取极大
}
}
return go(keyi, keyj);
}
void out_manual()
{
char alpha = 'A';
int i;
printf("\n 黑方落子位置: ");
for (i = 0; i < manukey; i += 2)printf(" %c%d", alpha + manu[1][i] - 1, manu[0][i]);
printf("\n 白方落子位置: ");
for (i = 1; i < manukey; i += 2)printf(" %c%d", alpha + manu[1][i] - 1, manu[0][i]);
Sleep(5000);
}
void player()
{
DrawBoard();
printf(" 轮到玩家下,请输入坐标(输入=0查看棋谱): ");
char c = '\n';
int row = 0, col = 0;
while (c < '0')scanf("%c%d", &c, &row);
if (c == '=')
{
out_manual();
return player();
}
if (c < 'a')col = c - 'A' + 1;
else col = c - 'a' + 1;
if (!ok(row, col))
{
printf("此处不能下");
Sleep(1000);
return player();
}
go(row, col);
}
void main()
{
init();
while (!is_end)
{
if (s == ais)AI();
else player();
s = 3 - s;//换下棋方
}
return;
}
七,棋盘显示问题
很多网友反馈棋盘显示异常的问题,为此我特意写了一篇博客来解释:
命令行显示棋盘异常_nameofcsdn的博客-CSDN博客
八,更多规则
1,三手交换
在三手交换规则里,第一手必须下天元,而第二手必须在第一手的周围。黑棋下盘面第3手棋后,白方在下第四手之前,如感觉黑方棋形不利于己方,可提出交换,即执白棋一方变为执黑棋一方,而黑方不可以不换。
2,五手两打
黑棋在下盘面上关键的第5手棋时,必须下两步棋,让白棋在这两步棋中拿掉一粒棋子,然后再继续对弈。
九,相关问题
力扣 LCP 48. 无限棋局
小力正在通过残局练习来备战「力扣挑战赛」中的「五子棋」项目,他想请你能帮他预测当前残局的输赢情况。棋盘中的棋子分布信息记录于二维数组 pieces
中,其中 pieces[i] = [x,y,color]
表示第 i
枚棋子的横坐标为 x
,纵坐标为 y
,棋子颜色为 color
(0
表示黑棋,1
表示白棋)。假如黑棋先行,并且黑棋和白棋都按最优策略落子,请你求出当前棋局在三步(按 黑、白、黑 的落子顺序)之内的输赢情况(三步之内先构成同行、列或对角线连续同颜色的至少 5 颗即为获胜):
- 黑棋胜, 请返回
"Black"
- 白棋胜, 请返回
"White"
- 仍无胜者, 请返回
"None"
注意:
- 和传统的五子棋项目不同,「力扣挑战赛」中的「五子棋」项目 不存在边界限制,即可在 任意位置 落子;
- 黑棋和白棋均按 3 步内的输赢情况进行最优策略的选择
- 测试数据保证所给棋局目前无胜者;
- 测试数据保证不会存在坐标一样的棋子。
示例 1:
输入:
pieces = [[0,0,1],[1,1,1],[2,2,0]]
输出:
"None"
解释:无论黑、白棋以何种方式落子,三步以内都不会产生胜者。
示例 2:
输入:
pieces = [[1,2,1],[1,4,1],[1,5,1],[2,1,0],[2,3,0],[2,4,0],[3,2,1],[3,4,0],[4,2,1],[5,2,1]]
输出:
"Black"
解释:三步之内黑棋必胜,以下是一种可能的落子情况:
提示:
0 <= pieces.length <= 1000
pieces[i].length = 3
-10^9 <= pieces[i][0], pieces[i][1] <=10^9
0 <= pieces[i][2] <=1
直接搬运上面的代码,修修补补最后得到这样的代码:
#define same_u_i same(row + dx[u] * i, col + dy[u] * i, p[row][col])//u方向i距离的点是否同色
#define OutOrNotEmpty (!inboard(row + dx[u] * i, col + dy[u] * i) || p[row + dx[u] * i][col + dy[u] * i] != 0) //出了棋盘或者非空格点
map<int, map<int, int>>p;
int s = 0, ais = 1, s0;//s是轮到谁下,s=1,2,s=1是ai下,s=2是玩家,s=s0是黑方下,否则是白方下
bool is_end = false;
int dx[8] = { 1, 1, 0, -1, -1, -1, 0, 1 }; //flat技术
int dy[8] = { 0, 1, 1, 1, 0, -1, -1, -1 };//(dx,dy)是8个方向向量
int manu[2][300], manukey = 0;//棋谱
bool inboard(int row, int col)//判断(row,col)是否在棋盘内
{
return true;
}
int same(int row, int col, int key)//判断2个棋子是否同色
{
if (!inboard(row, col))return false;
return (p[row][col] == key || p[row][col] + key == 0);
}
int num(int row, int col, int u)//坐标(row,col),方向向量u,返回该方向有多少连续同色棋子
{
int i = row + dx[u], j = col + dy[u], sum = 0, ref = p[row][col];
if (ref == 0)return 0;
while (same(i, j, ref))sum++, i += dx[u], j += dy[u];
return sum;
}
int cheng5(int row, int col)//成5点的数量
{
int sum = 0, i, u;
for (u = 0; u < 8; u++)//8个成五点的方向
{
int sumk = 0;
bool flag = true;
for (i = 1; same_u_i || flag; i++)
{
if (!same_u_i)//该方向的第一个不同色的点,超出边界或者对方棋子或空格
{
if (p[row + dx[u] * i][col + dy[u] * i])sumk -= 10;//该方向的第一个不同色的点是对方棋子,没有成五点
flag = false;
}
sumk++;
}
if (!inboard(row + dx[u] * --i, col + dy[u] * i))continue;//该方向的第一个不同色的点是超出边界,没有成五点
for (i = -1; same_u_i; i--)sumk++;
if (sumk >= 4)sum++;
}
return sum;
}
bool line(int row, int col) //是成5点
{
for (int u = 0; u < 4; u++) {
if (num(row, col, u) + num(row, col, u + 4) >= 4)return true;
}
return false;
}
class Solution {
public:
string gobang(vector<vector<int>>& pieces) {
p.clear();
for (auto v : pieces) {
p[v[0]][v[1]] = 2 - v[2];
}
if (canWinOneStep(pieces))return "Black";
int x = canWin1PosNum(pieces);
if (x>1)return "White";
if(x==0){
if (canWin0(pieces))return "Black";
return "None";
}else{
if (canWin0(pieces,ti,tj))return "Black";
return "None";
}
}
bool canWinOneStep(vector<vector<int>>& pieces) {
//0已经有成5点
for (auto v : pieces) {
if (v[2] == 0 && cheng5(v[0], v[1]))return true;
}
return false;
}
int canWin1PosNum(vector<vector<int>>& pieces) {
//1的成5点数量
set<pair<int, int>>s;
for (auto v : pieces) {
if (v[2]==0)continue;
for (int i = v[0] - 2; i <= v[0] + 2; i++) {
for (int j = v[1] - 2; j <= v[1] + 2; j++) {
if (p[i][j] == 0)s.insert(make_pair(i, j));
}
}
}
int n=0;
for (auto par : s) {
int i = par.first, j = par.second;
p[i][j]=1;
if(line(i,j))n++,ti=i,tj=j;
p[i][j]=0;
}
cout<<n<<endl;
return n;
}
bool canWin0(vector<vector<int>>& pieces) {
set<pair<int, int>>s;
for (auto v : pieces) {
if (v[2])continue;
for (int i = v[0] - 2; i <= v[0] + 2; i++) {
for (int j = v[1] - 2; j <= v[1] + 2; j++) {
if (p[i][j] == 0)s.insert(make_pair(i, j));
}
}
}
for (auto par : s) {
int i = par.first, j = par.second;
p[i][j] = 2;
if (cheng5(i, j)>1){
return true;
}
p[i][j] = 0;
}
return false;
}
bool canWin0(vector<vector<int>>& pieces,int i,int j) {
p[i][j] = 2;
if (cheng5(i, j)>1){
return true;
}
p[i][j] = 0;
return false;
}
int ti,tj;
};
稍微化简一下:
#define same_u_i same(row + dx[u] * i, col + dy[u] * i, p[row][col])//u方向i距离的点是否同色
map<int, map<int, int>>p;
int dx[8] = { 1, 1, 0, -1, -1, -1, 0, 1 }; //flat技术
int dy[8] = { 0, 1, 1, 1, 0, -1, -1, -1 };//(dx,dy)是8个方向向量
int same(int row, int col, int key)//判断2个棋子是否同色
{
return (p[row][col] == key || p[row][col] + key == 0);
}
int num(int row, int col, int u)//坐标(row,col),方向向量u,返回该方向有多少连续同色棋子
{
int i = row + dx[u], j = col + dy[u], sum = 0, ref = p[row][col];
if (ref == 0)return 0;
while (same(i, j, ref))sum++, i += dx[u], j += dy[u];
return sum;
}
int cheng5(int row, int col)//成5点的数量
{
int sum = 0, i, u;
for (u = 0; u < 8; u++)//8个成五点的方向
{
int sumk = 0;
bool flag = true;
for (i = 1; same_u_i || flag; i++)
{
if (!same_u_i)//该方向的第一个不同色的点,超出边界或者对方棋子或空格
{
if (p[row + dx[u] * i][col + dy[u] * i])sumk -= 10;//该方向的第一个不同色的点是对方棋子,没有成五点
flag = false;
}
sumk++;
}
for (i = -1; same_u_i; i--)sumk++;
if (sumk >= 4)sum++;
}
return sum;
}
bool line(int row, int col) //是成5点
{
for (int u = 0; u < 4; u++) {
if (num(row, col, u) + num(row, col, u + 4) >= 4)return true;
}
return false;
}
class Solution {
public:
string gobang(vector<vector<int>>& pieces) {
p.clear();
for (auto v : pieces) {
p[v[0]][v[1]] = 2 - v[2];
}
if (numCheng5(pieces,0))return "Black";
int x = numCheng5(pieces,1);
if (x>1)return "White";
if(x==0){
if (canWin0(pieces))return "Black";
}else{
if (canWin0(pieces,ti,tj))return "Black";
}
return "None";
}
int numCheng5(vector<vector<int>>& pieces,int x) {
set<pair<int, int>>s;
for (auto v : pieces) {
if (v[2]!=x)continue;
for (int i = v[0] - 1; i <= v[0] + 1; i++) {
for (int j = v[1] - 1; j <= v[1] + 1; j++) {
if (p[i][j] == 0)s.insert(make_pair(i, j));
}
}
}
int n=0;
for (auto par : s) {
int i = par.first, j = par.second;
p[i][j]=2-x;
if(line(i,j))n++,ti=i,tj=j;
p[i][j]=0;
}
return n;
}
bool canWin0(vector<vector<int>>& pieces) {
set<pair<int, int>>s;
for (auto v : pieces) {
if (v[2])continue;
for (int i = v[0] - 2; i <= v[0] + 2; i++) {
for (int j = v[1] - 2; j <= v[1] + 2; j++) {
if (p[i][j] == 0)s.insert(make_pair(i, j));
}
}
}
for (auto par : s) {
if(canWin0(pieces, par.first, par.second))return true;
}
return false;
}
bool canWin0(vector<vector<int>>& pieces,int i,int j) {
p[i][j] = 2;
if (cheng5(i, j)>1){
return true;
}
p[i][j] = 0;
return false;
}
int ti,tj;
};