五子棋人机对战完整代码

目录

〇,前言

一,五子棋棋盘

二,五子棋比赛规则

1,行棋顺序

2,判断胜负

三,重要棋型解释

1,五连:

2,活四:

3,冲四:

4,活三:

四,禁手规则

1,三三禁手

2,四四禁手

3,长连禁手

五,代码解释

1,棋子表示

2,棋盘表示

3,flat技术

4,棋型判断和禁手判断

4.1 活四

4.2 冲四

4.3 活3

5,AI算法

6,AI的打分机制

7,搜索剪枝

8,棋谱和禁手调试

六,代码


〇,前言

本文代码修改了数次,但是只保留了有代表性的V201912和V202001,版本名是“年+月”。

本来是帮一个朋友写的作业,结果自从博客发表之后,我发现每隔一段时间,这篇博客就有挺多人看。

其中还有几个人加了我QQ或微信,我一问,全都是要交作业的大学生。

唉,当代大学生啊。

一,五子棋棋盘

棋盘正中一点为“天元”。棋盘两端的横线称端线。棋盘左右最外边的两条纵线称边线。从两条端线和两条边线向正中发展而纵横交叉在第四条线形成的四个点称为“星”。

以持黑方为准,棋盘上的纵轴线从左到右用英文字母A~O标记。横行线从近到远用阿拉伯数字1~15标记。纵横轴上的横纵线交叉点分别用横纵线标记的名称合写成。如“天元”H8,四个“星”分别为D4、D12、L12、L4等。

二,五子棋比赛规则

1,行棋顺序

黑先、白后,从天元开始相互顺序落子。

2,判断胜负

最先在棋盘横向、竖向、斜向形成连续的相同色五个棋子的一方为胜。
黑棋禁手判负,白棋无禁手。黑棋禁手包括三三禁手,四四禁手,长连禁手。
如分不出胜负,则定为平局。

三,重要棋型解释

1,五连:

五颗同色棋子连在一起,

即4个方向的11111这种形式的棋型。

2,活四:

有2个成五点的四颗棋子,

即4个方向的011110这种形式的棋型,注意两边一定要有空格。

3,冲四:

有1个成五点的四颗棋子,棋型有点多。

4,活三:

可以形成活四的三颗棋子,

要么是三连的形式,即4个方向的01110这种形式的棋型

要么是非三连的形式,即8个方向的010110这种形式的棋型

PS:这个三连描述的不准确,在01110的两端,必须至少有一个空格

四,禁手规则

1,三三禁手

由于黑方落一子,同时形成二个或二个以上黑方活三的局面

2,四四禁手

由于黑方落一子,同时形成二个或二个以上黑方四(活四或者冲四)的局面

3,长连禁手

由于黑方落一子,形成六个或者六个以上的同色连续棋子

五,代码解释

1,棋子表示

为了使自已与对手看得更清楚,刚落下的子区别表示,
白方正常子:○    白方刚落下之子:△
黑方正常字:●    黑方刚落下字:▲

2,棋盘表示

利用专门画棋盘的9个拓展字符,可以在控制台上画出非常漂亮的棋盘

out函数用来画棋盘的一个格子,要么是表示棋盘的9个拓展字符,要么是表示棋子的4个拓展字符

DrawBoard函数用来打印整个游戏界面,需要调用out函数

3,flat技术

通过dx和dy这2个常数数组,存下8个方向的向量,就可以把棋型判断、禁手判断等二维问题化作一维问题。

通过for循环即可遍历每个方向,使得代码变得非常简洁。

4,棋型判断和禁手判断

对于任何一个可以落子的位置,要独立的判断如果落子就会形成几个活四,几个冲四,几个活三。

三三禁手和四四禁手直接根据三种棋型的数量判断即可,长连禁手单独判断,很简单。

4.1 活四

判断活4的逻辑比较简单,遍历4个方向,判断是不是011110的形式即可

4.2 冲四

冲4的棋型比较多:

(1)8个方向的Y01111X,X是超出边界或者对方棋子,Y是超出边界或者对方棋子或空格,下同

(2)8个方向的Y11101Y,当前位置是右边的那个1

(3)8个方向的Y10111Y,当前位置是右边的3个1中的一个

(4)8个方向的Y11011Y,当前位置是右边的2个1中的一个

可以说按照这种思路来求就非常复杂。

分析这几种情况,我们可以发现,活四和冲四加起来,可以用一个统一的描述:

YabcdeY,其中abcde由4个1和1个0组成。

进一步,我们可以得到,成五点的数量是活四的数量*2+冲四的数量,而成五点的数量可以根据YabcdeY的形式来求,即:

在8个方向上求成五点,对于每个方向,从当前位置往前延伸,第一个不同色的点是空格,跳过空格继续往前延伸直到第二个不同色的点,同时从当前位置往反方向延伸直到第一个不同色的点, 算上当前位置本身,如果同色点的数量一共是4,那么该方向就有成五点。

由此,我们可以根据成五点的数量和活四的数量求出冲四的数量。

4.3 活3

在V201912代码中,活3是分开计算三连活3的数量和非3连的活3的数量,然后加起来

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++;  //据网友提示这里应该是i--,写代码过了很久了,懒得确认真相了
    off;
    if (sumk == 3)sum++;
}

网友提示把其中一个i++改成i--,这样确实好一些,不过仍然是错的。

改成i--之后,代码实际求的是4个方向的0011100这种三连,但是实际上011100或者001110的形式都可以。

新的代码在上述i++改成i--的基础之上,再加一个flag变量,用来判断01110的两端是否至少还有一个空格。

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++;  //据网友提示这里应该是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;
}

2020年1月再次更新,修改了宏,使得代码可读性更高,修改了live3函数。

V202001:

#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;
}

PS:很多网友反馈棋盘显示异常的问题,为此我特意写了一篇博客来解释:

https://blog.csdn.net/nameofcsdn/article/details/87923401

发布了1185 篇原创文章 · 获赞 679 · 访问量 171万+

猜你喜欢

转载自blog.csdn.net/nameofcsdn/article/details/78640511