五子棋估价函数

博主分享了自己在实现五子棋人机对战AI过程中,针对估价函数的优化历程。从最初的遍历棋盘评估,到通过限制搜索范围提高速度,再到考虑棋子分布细化评估,逐步提升了AI的运行效率。优化后的算法在保持准确性的同时,显著减少了计算量,尤其在处理边角位置时效果更佳。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

最近,我写了一个五子棋人机对战的AI,但速度一直很慢。其实,我感觉性能的瓶颈并不是极小化极大算法和α-β剪枝有问题,而是估价函数太差。其实,估价函数比极小化极大算法和α-β剪枝这两个算法的难度大多了,具体表现是,我开始跟着这篇博客(讲解的非常详细,虽然α-β剪枝有点错误,评论区有人指出来了,但是其他地方包括估价函数值得借鉴。在此也向作者表示感谢)学习博弈树的时候,极小化极大算法和α-β剪枝,我就只看了一个介绍和节点的数据结构模型,然后就摸索着写出了代码(虽然不知道正确不正确)。而估价函数,我是一点头绪都没有,后来只好把教程上的代码复制上了。后来我仔细研究那些代码,终于明白了。并且昨天考试写完试卷闲着无聊的时候,又想出来了一种能略微提升速度的方法。
我也把这个AI和之前做的双人五子棋结合了,形成一个拥有MFC图形界面的五子棋人机对战程序,点击此处下载

旧版本估价函数

原来的版本思路基本如下:
遍历棋盘,寻找所有的五个点组成的棋链,对每一个棋链执行以下逻辑:

  1. 如果该棋链中既有白子又有黑子,该棋链估价为0;
  2. 如果该棋链中只有n个一种颜色的白棋(默认计算机为白),若 n ≤ 4 n≤4 n4,得 k n k^n kn分;若 n = 5 n=5 n=5,得INT_MAX分并且立即结束整个算法(并非对一个棋链的估价,而是对整个棋盘的估价)。k可以自由调整,我这里是10。
  3. 如果该棋链中只有n个一种颜色的黑棋(默认计算机为白),若 n ≤ 4 n≤4 n4,得 − p k n -pk^n pkn分;若 n = 5 n=5 n=5,得INT_MIN分并且立即结束整个算法(并非对一个棋链的估价,而是对整个棋盘的估价)。p可以自由调整。如果想让计算机优先防守,将p设为一个较大的值,如1.5。

最后,把所有棋链的分数加起来即可。
C++代码如下:

/*
一个节点类的成员函数,State是枚举,有BLACK,WHITE,SPACE;Board是棋盘,State[15][15]类型的。
*/
int Evaluate()const//估价函数
{
	int result = 0;
	static auto EvaluateSome = [](const std::array<State, 5>& v)//假定自己是白方
	{
		//判断颜色并记录棋子个数
		State lastColor = SPACE;
		uint8_t count = 0;
		for (State i : v)
		{
			if (i != SPACE)
			{
				++count;
				if (i != lastColor)
				{
					if (lastColor == SPACE)//遇到的第一个棋子
					{
						lastColor = i;
					}
					else//有不同颜色的棋子
					{
						return 0;
					}
				}
			}
		}
		if (!count)//没有棋子
			return 0;
		if (count == 5)
		{
			return lastColor == WHITE ? INT_MAX : INT_MIN;//一定不要认为-INT_MAX就是INT_MIN!
		}
		const int result = static_cast<int>(std::pow(10, count - 1));
		return lastColor == WHITE ? result : static_cast<int>(-1.1 * result);//对手返回负值,我方返回正值,乘以1.1后优先防守
	};
	for (uint8_t i = 0; i < 15; i++)//分别从四个方向判断
	{
		for (uint8_t j = 0; j < 15; j++)
		{
			if (j + 4 < 15)
			{
				std::array<State, 5>v;
				for (uint8_t k = 0; k < 5; k++)
					v[k] = board[i][j + k];
				const int t = EvaluateSome(v);
				if (t == INT_MAX || t == INT_MIN)//决出胜负直接返回
					return t;
				result += t;
			}
			if (i + 4 < 15)
			{
				std::array<State, 5>v;
				for (uint8_t k = 0; k < 5; k++)
					v[k] = board[i + k][j];
				const int t = EvaluateSome(v);
				if (t == INT_MAX || t == INT_MIN)//决出胜负直接返回
					return t;
				result += t;
			}
			if (i + 4 < 15 && j + 4 < 15)
			{
				std::array<State, 5>v;
				for (uint8_t k = 0; k < 5; k++)
					v[k] = board[i + k][j + k];
				const int t = EvaluateSome(v);
				if (t == INT_MAX || t == INT_MIN)//决出胜负直接返回
					return t;
				result += t;
			}
			if (i + 4 < 15 && j - 4 >= 0)
			{
				std::array<State, 5>v;
				for (uint8_t k = 0; k < 5; k++)
					v[k] = board[i + k][j - k];
				const int t = EvaluateSome(v);
				if (t == INT_MAX || t == INT_MIN)//决出胜负直接返回
					return t;
				result += t;
			}
		}
	}
	return result;
}

这个代码需要把整个棋盘遍历一遍,粗略估计一下,遍历的范围是15×15=225。

新版本估价函数

我突然想到,你下到一个地方,只能影响到周围9×9的范围,我们可以把每一个节点的估价保存下来,然后,创建一个新的节点时,先按照原来的方法估价这9×9的范围,然后估价父节点9×9的范围,用父节点原来的估价分数减去父节点9×9的范围得分,再加上子节点9×9范围的得分,就是子节点的得分。这种算法在最坏情况下(落子地方靠近棋盘中间),需要遍历的范围是9×9×2=162,计算量大大减小。另外,如果落子的地方靠近棋盘边角,使得9×9的范围缩小,那就可以更节省时间,最好情况下(下到棋盘角上)遍历范围只有6×6×2=72。现实情况中,有许多次都是遍历了边角处,所以速度加快了许多。这样还有一个好处,就是最坏情况始终是162,不会因为棋盘的变大而变大,如果棋盘是100×100,这个算法还是遍历162个,而第一种算法遍历100×100=10000个。经过测试,这种方法比原来的速度提高了许多。下面是C++代码:

int Evaluate()const//估价函数
{
	static auto EvaluateSome = [](State board[BOARDSIZE][BOARDSIZE], uint8_t beginX, uint8_t endX, uint8_t beginY, uint8_t endY) {
		static auto EvaluateList = [](const std::array<State, 5>& v)//假定自己是白方
		{
			//判断颜色并记录棋子个数
			State lastColor = SPACE;
			uint8_t count = 0;
			for (State i : v)
			{
				if (i != SPACE)
				{
					++count;
					if (i != lastColor)
					{
						if (lastColor == SPACE)//遇到的第一个棋子
						{
							lastColor = i;
						}
						else//有不同颜色的棋子
						{
							return 0;
						}
					}
				}
			}
			if (!count)//没有棋子
				return 0;
			if (count == 5)
			{
				return lastColor == WHITE ? INT_MAX : INT_MIN;//一定不要认为-INT_MAX就是INT_MIN!
			}
			const int result = static_cast<int>(std::pow(10, count - 1));
			return lastColor == WHITE ? result : -result;//对手返回负值,我方返回正值
		};
		int result = 0;
		for (uint8_t i = beginX; i < endX; i++)//分别从四个方向判断
		{
			for (uint8_t j = beginY; j < endY; j++)
			{
				if (j + 4 < endY)
				{
					std::array<State, 5>v;
					for (uint8_t k = 0; k < 5; k++)
						v[k] = board[i][j + k];
					const int t = EvaluateList(v);
					if (t == INT_MAX || t == INT_MIN)//决出胜负直接返回
						return t;
					result += t;
				}
				if (i + 4 < endX)
				{
					std::array<State, 5>v;
					for (uint8_t k = 0; k < 5; k++)
						v[k] = board[i + k][j];
					const int t = EvaluateList(v);
					if (t == INT_MAX || t == INT_MIN)//决出胜负直接返回
						return t;
					result += t;
				}
				if (i + 4 < endX && j + 4 < endY)
				{
					std::array<State, 5>v;
					for (uint8_t k = 0; k < 5; k++)
						v[k] = board[i + k][j + k];
					const int t = EvaluateList(v);
					if (t == INT_MAX || t == INT_MIN)//决出胜负直接返回
						return t;
					result += t;
				}
				if (i + 4 < endX && j >= 4)
				{
					std::array<State, 5>v;
					for (uint8_t k = 0; k < 5; k++)
						v[k] = board[i + k][j - k];
					const int t = EvaluateList(v);
					if (t == INT_MAX || t == INT_MIN)//决出胜负直接返回
						return t;
					result += t;
				}
			}
		}
		return result;
	};
	uint8_t beginX, endX, beginY, endY;
	if (lastX <= 5)
		beginX = 0;
	else
		beginX = lastX - 5;
	endX = lastX + 5;
	if (endX > BOARDSIZE)
		endX = BOARDSIZE;
	if (lastY <= 5)
		beginY = 0;
	else
		beginY = lastY - 5;
	endY = lastY + 5;
	if (endY > BOARDSIZE)
		endY = BOARDSIZE;
	const int t = EvaluateSome((State(*)[15])board, beginX, endX, beginY, endY);
	if (t == INT_MAX || t == INT_MIN)//决出胜负直接返回
		return t;
	return  t - EvaluateSome((State(*)[15])father->board, beginX, endX, beginY, endY) + father->evaluateValue;
}

第三版估价函数

过了一段时间,我又想出了一种更优的方案,,具体来说,在判断一个棋链的得分(即EvaluateList 函数)时,不再简单地根据棋子的数量确定分数,而是考虑棋子的具体分布,对每一种分布给出一个特定的分数。不难看出,这样没有改变时间复杂度,但使结果相对来说准确了一些。下面是一个棋链中不同分布对应的得分,这里的分数是我估摸着打的,可以进行调整优化。
另外,评论区有大佬指出,并不需要重新计算9*9的区域,只需要计算包含上一步落子点的最多20个棋链就可以,这样也可以大大加快速度。
代码:

int Evaluate()const//估价函数
{
	static const auto EvaluateList = [](const std::array<State, 5>& v)//假定自己是白方
		{
			//判断颜色并记录棋子个数
			State lastColor = SPACE;
			uint8_t bitList = 0;//将棋链以二进制形式表示,如01101
			for (State i : v)
			{
				bitList <<= 1;
				if (i != SPACE)
				{
					if (i != lastColor)
					{
						if (lastColor == SPACE)//遇到的第一个棋子
							lastColor = i;
						else//有不同颜色的棋子
							return 0;
					}
					bitList |= 1;
				}
			}
			static constexpr int results[]
				= { 0,5,5,80,5,60,100,500,5,20,80,500,100,500,8000,100000,5,10,//0-17
				20,600,50,600,500,8000,80,600,500,6000,500,8000,100000,maxEvaluteValue };//18-31
			/*
			* 十进制-二进制-得分
			* 0-00000-0
			* 1-00001-5
			* 2-00010-5
			* 3-00011-80
			* 4-00100-5
			* 5-00101-60
			* 6-00110-100
			* 7-00111-500
			* 8-01000-5
			* 9-01001-20
			* 10-01010-80
			* 11-01011-500
			* 12-01100-100
			* 13-01101-500
			* 14-01110-8000
			* 15-01111-100000
			* 16-10000-5
			* 17-10001-10
			* 18-10010-20
			* 19-10011-600
			* 20-10100-50
			* 21-10101-600
			* 22-10110-500
			* 23-10111-5000
			* 24-11000-80
			* 25-11001-600
			* 26-11010-500
			* 27-11011-6000
			* 28-11100-500
			* 29-11101-5000
			* 30-11110-100000
			* 31-11111-MAX
			*/
			//对手返回负值,我方返回正值
			//乘以1.1是为了偏重防守
			if (bitList == 31)
			{
				return lastColor == WHITE ? maxEvaluteValue : -maxEvaluteValue;
			}
			return lastColor == WHITE ? results[bitList] : -results[bitList] * 11 / 10;
		};
	static const auto EvaluteSome = [](State** board, uint8_t lastX, uint8_t lastY)
		{
			int result = 0;
			for (uint8_t i = 0; i < 5; ++i)
			{
				//横向
				if (lastX + i < boardSize && lastX + i >= 4)
				{
					std::array<State, 5>v;
					for (uint8_t k = 0; k < 5; k++)
						v[k] = board[lastX + i - k][lastY];
					int t = EvaluateList(v);
					if (t == maxEvaluteValue || t == -maxEvaluteValue)//决出胜负直接返回
						return t;
					result += t;
				}
				//纵向
				if (lastY + i < boardSize && lastY + i >= 4)
				{
					std::array<State, 5>v;
					for (uint8_t k = 0; k < 5; k++)
						v[k] = board[lastX][lastY + i - k];
					int t = EvaluateList(v);
					if (t == maxEvaluteValue || t == -maxEvaluteValue)//决出胜负直接返回
						return t;
					result += t;
				}
				//左上-右下
				if (lastX + i < boardSize && lastX + i >= 4 && lastY + i < boardSize && lastY + i >= 4)
				{
					std::array<State, 5>v;
					for (uint8_t k = 0; k < 5; k++)
						v[k] = board[lastX + i - k][lastY + i - k];
					int t = EvaluateList(v);
					if (t == maxEvaluteValue || t == -maxEvaluteValue)//决出胜负直接返回
						return t;
					result += t;
				}
				//左下-右上
				if (lastX + i < boardSize && lastX + i >= 4 && lastY - i + 4 < boardSize && lastY - i >= 0)
				{
					std::array<State, 5>v;
					for (uint8_t k = 0; k < 5; k++)
						v[k] = board[lastX + i - k][lastY - i + k];
					int t = EvaluateList(v);
					if (t == maxEvaluteValue || t == -maxEvaluteValue)//决出胜负直接返回
						return t;
					result += t;
				}
			}
			return result;
		};
	int t = EvaluteSome(board, lastX, lastY);
	if (t == maxEvaluteValue || t == -maxEvaluteValue)//决出胜负直接返回
		return t;
	return t + father->GetEvaluateValue() - EvaluteSome(father->board, lastX, lastY);
}
评论 18
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值