五子棋估价函数

706 阅读3分钟

前言

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

旧版本估价函数

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

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

最后,把所有棋链的分数加起来即可。
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。

新版本估价函数

我突然想到,你下到一个地方,只能影响到周围11×11的范围,我们可以把每一个节点的估价保存下来,然后,创建一个新的节点时,先按照原来的方法估价这11×11的范围,然后估价父节点11×11的范围,用父节点原来的估价分数减去父节点11×11的范围得分,再加上子节点11×11范围的得分,就是子节点的得分。这种算法在最坏情况下(落子地方靠近棋盘中间),需要遍历的范围是11×11×2=242,看似慢了,但如果落子的地方靠近棋盘边角,使得11×11的范围缩小,那就可以节省时间,最好情况下(下到棋盘角上)遍历范围只有6×6×2=72。现实情况中,有许多次都是遍历了边角处,所以速度加快了许多。这样还有一个好处,就是最坏情况始终是242,不会因为棋盘的变大而变大,如果棋盘是100×100,这个算法还是遍历242个,而第一种算法遍历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;
}