简单五子棋算法——初级篇

简单五子棋算法——初级篇

前言

五子是中国古老的棋类之一,是老少咸宜的娱乐项目。也是人机博弈中最简单的一类,相较于围棋、象棋变化更少,算法实现起来就相对比较简单。

五子棋先手胜率理论上是百分之百的,因此在发展中逐渐出现了一些禁手规则来限制先手优势。但是这些都是对于职业棋手而言,对于普通玩家来说就不需要这么多的限制,简简单单即可。这里的算法也是如此,既然是简单五子棋,因此并不考虑那么多的限制。

设计思路

既然要设计算法,我们就要挖掘五子棋背后的原理。下棋都可以归类到博弈问题。二者博弈,就是一场利益争夺战,那么最终结果就看博弈双方谁能够获得最大的利益。

我们由浅入深的分析,首先下棋分为进攻和防守模式。例如此时我方已有三颗子连线,在下一颗就四颗连线了,这是进攻;又有对方三颗子连线时,我们要去阻止对方连成四颗子,这是防守。

根据进攻和防守的思路,我们需要权衡怎么下才能获得最大利益。一个简单的方法就是将利益量化为分数,根据每个位置落子的分数高低来权衡。

如此一来建立一个公正的评分制度就很重要。根据连子的数目和连子两侧有无对方落子设立评分表如下(X为敌子,O为我子,_为空位):

布局 无子 一子 二子 三子 四子 五子
二防 XX XOX XOOX XOOOX XOOOOX XOOOOOX
一防 X_ XO_ XOO_ XOOO_ XOOOO_ XOOOOO_
无防 _ O OO OOO OOOO OOOOO
分数 无子 一子 二子 三子 四子 五子
二防 0 0 0 0 0 10000
一防 0 0 20 100 500 10000
无防 0 20 100 500 2500 10000

如果有连子数大于五子,将按五子计算。
OK,我们可以开始写程序了。

算法实现

标准的五子棋一般是15*15的格子,因此先建立棋盘,并约定1代表黑子,-1代表白子。

vector<vector<int>> topo(15, vector<int>(15, 0));

根据上面的评分表,我们来写每个位置的评分程序:

//米字型搜索
//[—, | , / , \]四个移动方向
static const int inX[] = { 1,0,1,1 };
static const int inY[] = { 0,1,1,-1 };

//评分表
static const int Score[3][6] = {
	{ 0, 0,  0,  0,   0,10000 },//防守2子
    { 0, 0, 20,100, 500,10000 },//防守1子
    { 0,20,100,500,2500,10000 } //防守0子
};

//topo:棋盘
//x,y:棋盘位置
//color:落子颜色(黑色:1;白色:-1)
int getScore(vector<vector<int>> topo,
             const int x, const int y,
             const int color) {
	//返回评分值
	int re = 0;

	//向 [—,|, /, \]四个方向搜索,对应inX,inY
	for (int i = 0; i < 4; ++i) {
		//k记录连子两侧空位的数目(非墙,非敌方落子)
		int k = 0;
		//记录连子的数目,初始值为1因为假设在当前位置落子
		int count = 1;

		//[—,|, /, \]四个方向的正负方向
		for (int j = -1; j < 2; j += 2) {
			int dx = x + j * inX[i];
			int dy = y + j * inY[i];

			//判断是否超出棋盘边界
			while (dx >= 0 && dx < topo.size() &&
			       dy >= 0 && dy < topo[0].size()) {
				//假如遇到颜色相同的子,count+1,反之则退出循环,并判断此时有无空位
				if (color*topo[dx][dy] > 0) {
					++count;
				}
				else {
					if (color*topo[dx][dy] == 0) ++k;
					break;
				}

				dx += j * inX[i];
				dy += j * inY[i];
			}
		}

		//假如连子大于5,使之等于5
		if (count > 5) count = 5;

		//加上该方向所得评分
		re += Score[k][count];
	}

	return re;
}

评分函数写好,我们开始设计AI棋手,按照之前的思路,我们只需要遍历所有空位,然后找到获取利益最大点即可。如下:

void AiChesser(int color,
	           int &x, int &y, 
	           vector<vector<int>> topo) {
	int max = INT_MIN;
	vector<int> X;
	vector<int> Y;

	for (int i = 0; i < topo.size(); ++i) {
		for (int j = 0; j < topo[i].size(); ++j) {
			//判断是否为空位
			if (topo[i][j] == 0) {
				//进攻还是防守,寻找利益最大值
				int score = max(getScore(topo, i, j, color),
				                getScore(topo, i, j, -color));

				//以防有多个相同的最大值
				if (score >= max) {
					if (score > max) {
						X.clear();
						Y.clear();
						max = score;
					}
					X.push_back(i);
					Y.push_back(j);
				}
			}
		}
	}

	//在最大值中任取一个返回,大部分情况只有一个
	int r = rand() % X.size();
	x = X[r];
	y = Y[r];
}

后言

当然这样程序还并不是一个可以运行的玩意,具体的UI设计就见仁见智了,这里主要只说明算法。提供easyx库设计的完整五子棋项目作为参考,附上可执行的exe文件。

在这里插入图片描述

这个算法其实也只达到初级棋手的水平,但足以和普通人一战了。
算法有很多地方还是值得优化的,为了代码更容易理解,许多地方简单设计了。

进阶设计

咱们的这个设计AI还是相当简单的,但是真正的高手可不会只看到眼前,他们往往下的一步是为了后面的很多步服务的。因此我们希望进一步提高我们的算法能力,就需要从后面的很多步来综合考虑,而不能只看到眼前的一步。此处就要引入极大极小值算法和alpha-beta剪枝算法,我会在以后的文章中讲解这些。

发布了63 篇原创文章 · 获赞 73 · 访问量 7万+

猜你喜欢

转载自blog.csdn.net/jjwwwww/article/details/84593137