一、跳棋游戏的博弈系统概要
本文的跳棋游戏博弈系统由浅至深分为四个层面:
1、跳棋游戏的各种数据结构。包括棋盘、棋子等。
2、可移动棋格搜索算法。可移动棋格搜索算法可分为人类一侧与机器一侧,二者主要区别在于棋格搜索量的不同,以及人类玩家可移动棋格搜索算法更偏向于判断而非搜索。
3、估分算法。估分算法是对棋局局面的可视化算法,将棋局数字化,让机器判断走法优劣。本研究的跳棋估分算法基于双方玩家当前棋子的权值实现。
4、棋类博弈搜索算法。将所有走法罗列出来,以此构成一颗博弈树。传统博弈搜索算法中,有极大极小值算法以及Alpha-Beta剪枝技术。
二、跳棋游戏规则解析
1.跳跃规则
共有两种基础跳跃方式,一字跳方式和空跳方式。
一字跳方式中,某个方向相邻的位置上有一个棋子,并且该方向上的下一个位置没有棋子,则该棋子可以跳跃到该方向上距离为2的棋格。空跳方式中,设x大于0,某个方向上有连续x个棋格上没有棋子,第x+1个棋格上有一个棋子,第x+2个棋格到第2x+2个棋格上没有棋子,则棋子可跳跃至第2x+2个棋格。
2.约束规则
设中间营地为白色,六名玩家的本方营地各为一种颜色。
约束一:棋子只允许停留在本方营地、目标营地和中间营地。其他颜色的营地可在跳跃时经过,但不可停留。
约束二:不允许一方玩家的一个棋子在两个棋位位置之间来回跳跃。
约束三:在规定轮数内玩家离开本方营地的棋子数目需达到规定的数量,否则判为负。
三、数据结构
1.棋子
建立两个预制体,一个代表人类方棋子,一个代表机器方棋子。其中黄色为人类方棋子,红色为机器方棋子。由于预制体的存在,可方便的将棋子放置于游戏中,对象棋子的大小颜色均能得到统一的控制,唯一需要考虑的是棋子位置的存放。
为了区分人类玩家与机器玩家的棋子,需对双方预制体加上不同的标签Tag,人类玩家棋子的标签为Player,机器棋子的标签为Bot Chess。通过返回棋子的标签名称即可判断棋子所属玩家。
2.棋盘
跳棋根据棋盘的大小不同,棋格数有所不同。本文采用的是单个玩家10个棋子,棋格共计121格的棋盘。棋盘图片如下所示。
与棋子相同的方式建立一个预制体,此预制体为空对象,用于记录棋格位置。而棋盘的可视化采用图片的形式实现,实际上玩家与棋格的互动是与棋盘无关的,而与表示棋格的空对象有关。要注意的是,该空对象需加入一个碰撞器组件,该组件在可移动棋格位置搜索算法中起到重要作用。
3.游戏规则的实现
(1)胜利条件
任意一方玩家赢得游戏的条件为己方所有棋子都在对方营地内。为了判定每轮行棋过后玩家是否胜利,需在每一轮下棋结束后对棋盘进行遍历,判断棋子是否已经全部处于敌方营地当中。若是,则玩家胜利;反之,游戏继续进行。
(2)回合制
为了实现棋类游戏的回合制,在跳棋游戏脚本当中设置一个int型公共变量round。每当一方玩家行棋结束后,round自增1。人类玩家能够行棋的条件为round对2取余为1时,机器玩家能够行棋的条件为round对2取余为0时。通过该方式即可实现回合制。
(3)行棋规则
跳棋游戏的行棋规则是跳棋游戏的核心,在游戏实现的过程中,行棋规则是表现在双方棋子的移动上,是可视的。但行棋规则的实现是在脚本中实现的,是不可视的。实现方案是在最基础的棋子两点移动上加以限制,添加判断棋子是否可移动的代码。
四、可移动棋格位置搜索算法
可移动棋格算法的目的是搜索出任意一方玩家所有棋子的所有可移动的棋格位置。
1.棋子移动
棋子的移动是根据坐标变化而达成的。以人类玩家为例,以移动合法为前提,通过左键点击棋子获得棋子的Transform信息,右键点击棋格获得棋格的Transform信息,将棋子的坐标信息更改为棋格Transform信息中的坐标,即可实现棋子的移动。
2.人类玩家的可移动棋格位置搜索算法
棋子向棋格方向发出一条射线,射线的长度应为棋子到棋格的距离大小。若射线长度无穷,有可能会碰撞到棋格往后的棋子,反馈发生碰撞,进而产生错误。
为了鼠标能够与棋子与棋格都进行互动,棋子与棋格都需设置碰撞器,使得主摄像机发出的射线与棋子、棋格都能发生碰撞。但这会产生一个冲突,棋子用于查询棋子到棋格之间是否存在其他棋子的射线与棋格空对象处于同一水平面上,直接发出的射线有可能会与路径上其他的棋格发生碰撞,进而导致无法判断发生碰撞的是棋子还是棋格。因此需要对棋子与棋格加以区分。
3.机器玩家的可移动棋格位置搜索算法
机器玩家的可移动棋格位置搜索算法是在人类玩家的可移动棋格位置搜索算法的基础上加以扩展。人类玩家具备思考能力,查询鼠标交互的棋子与棋格信息并反馈即可。但机器玩家并不具备人类的思考能力,为实现机器玩家选择合适的棋格并对棋子进行移动。机器玩家的可移动棋格位置需遍历整个棋盘。
机器玩家的可移动棋格位置搜索算法与人类玩家的可移动棋格位置搜索算法的逻辑极其相似。10个棋子逐个进行搜索,每个棋子按6个方向逐个进行搜索,获得可移动棋格后将其坐标用数组保存。由此可获得该棋局下机器玩家所有棋子的可移动棋格。
五、估分算法
估分算法是对棋局进行分数评估,跳棋游戏的估分算法根据考虑的方面而有所不同。譬如五子棋,考虑禁手和不考虑禁手的估分算法不同。在本文中,估分方法考虑的是己方棋子与己方营地最底部棋格的棋格距离。
分值越高,表明该棋局越优,该局面对己方越有利。跳棋游戏中估分方法名为score(Vector3[] all_chess_posion2),Vector3数组all_chess_posion2是所有棋子的坐标信息。对棋局的估分,是计算每个己方棋子到己方最底部棋格的距离,将得到的10个值累加获得值num1。再计算对方每个棋子离对方最底部棋格的距离,将得到的10个值累加获得值num2,将num1减去num2即可得到该局面的分数num。
为提高num值,每一次的走法应使num1尽可能的大,num2尽可能的小,最终得到的num值才大。由此可知当己方棋子距离己方营地最底部棋格距离越远,对方棋子距离对方营地最底部棋格越近,分值越高。从另一角度来说,己方棋子离对方营地越近,对方棋子离己方营地越远,分值越高。这正是跳棋游戏中类似于人类的一般思路,使自己的棋子尽可能的达到终点,同时尽可能拖慢对方棋子达到终点。
取num1与num2可代表两方玩家到达终点的距离,值越大即越近。二者取差可表示该走法的分数,可用于与其他走法的分数进行比较,最终得出走法优劣。
但仅用距离进行分数表示会出现一些问题,由于棋子与己方营地底部棋格固定距离达到一定距离后,进行两个棋格来回移动将会比从起点移动的距离要多得多,这会导致机器选择棋子来回移动而不挪动还在起点的棋子。因此需对每一行的棋格进行合适的加分,以避免该情况的发生。
六、棋类博弈搜索算法
1.使用到的算法概述
传统博弈搜索算法中,有极大极小值算法以及Alpha-Beta剪枝技术。
(1)极大极小值算法是先生成一棵博弈树,再计算其倒推值。缺点是效率较低。
(2)Alpha-Beta剪枝技术通过得出其中一些节点的值后,后继节点就不需估值。
在此基础上,可通过搜索节点有序化、开局优化、搜索宽度随棋局变化三个方面对Alpha-Beta剪枝算法进行改进,从而提高博弈速度。
2.MaxMin极大极小值算法
博弈树是由棋局逐步发展而形成的。通俗的讲,是机器玩家与人类玩家轮流行棋形成的。设机器玩家行棋的层为Max层,预测人类玩家行棋的层为Min层。
博弈树的建立过程中,不会对每个局面都进行打分。机器期望的是博弈树最底层叶子结点局面,应对叶子结点局面进行打分。叶子结点打分后,将分数返回至父节点,与其他叶子结点返回的分数,选择一个合适的分数再向上返回,直至根节点获得最终的分数为止。
当机器玩家行棋时,期望的分数总是最高的;而预测人类玩家行棋时,预测人类玩家会选择局面对机器方最不友好的,因此会期望分数最低的。而博弈树正是Max层与Min层逐层交替。返回值时,如果是Min层,则保存返回值中最低的分数;如果是Max层,则保存返回值中最高的分数。最终返回到根节点Max层。根据根节点得到的分数,机器按照对应分数的走法行棋。
下列代码是应用到跳棋游戏中的极大极小值算法:
float Max_Min(int deepth,Transform[] all_chess_tran)//MaxMin极大极小值算法
{
if (deepth == 0)
{
Vector3[] all_chess_posion2 = new Vector3[20];
for (int i = 0; i < 20; i++)
{
all_chess_posion2[i] = all_chess_tran[i].position;
}
return score(all_chess_posion2);
}
float best;
if (deepth % 2 == 0)
{
//建立局部可移动棋子数组
Vector3[,] bot_chess_move2 = new Vector3[10, 6];
for (int i = 0; i < 10; i++)//搜索可移动棋子
{
for (int j = 0; j < 6; j++)
{
check_hit_new(i, j,all_chess_tran,bot_chess_move2);
}
}
best = -Mathf.Infinity;
//更改all_chess_posion2值
for(int i = 0; i < 10; i++)
{
for(int j = 0; j < 6; j++)
{
if (bot_chess_move2[i, j] !=new Vector3(999, 999, 999))
{
Vector3 old_posion = all_chess_tran[i].position;
all_chess_tran[i].position = bot_chess_move2[i, j];
float newval = Max_Min(deepth - 1, all_chess_tran);
if (newval > best)
{
best = newval;
//保存最佳移动方式
best_num = i;
best_posion = all_chess_tran[i].position;
}
all_chess_tran[i].position = old_posion;
}
}
}
}
else
{
Vector3[,] bot_chess_move3 = new Vector3[10, 6];
for (int i = 0; i < 10; i++)//搜索可移动棋格
{
for (int j = 0; j < 6; j++)
{
check_grid_new(i, j,all_chess_tran, bot_chess_move3);
}
}
best = Mathf.Infinity;
for (int i = 0; i < 10; i++)
{
for (int j = 0; j < 6; j++)
{
if (bot_chess_move3[i, j] != new Vector3(999, 999, 999))
{
Vector3 old_posion = all_chess_tran[i + 10].position;
all_chess_tran[i+10].position = bot_chess_move3[i, j];
float newval = Max_Min(deepth - 1, all_chess_tran);
if (newval < best)
{
best = newval;
}
all_chess_tran[i + 10].position = old_posion;
}
}
}
}
return best;
}
3.Alpha-Beta剪枝算法
Alpha-Beta剪枝算法是优化算法,是为了减少极大极小值算法的叶子结点搜索数量。可加快叶子结点值的返回,让机器更快作出决策。
设x为根节点的值,初始各结点Alpha的值为-∞,Beta的值为+∞,其中-∞≤x≤+∞。在同一分支中,当返回值到Min层中时,若当前值小于Beta值,则修改Beta值为返回值,反之则不作修改。当返回值到Max层中时,若当前值大于Alpha值,则修改Alpha值为返回值,反之则不作修改。当Alpha>Beta时,该分支可不再搜索,即实现剪枝,加快返回值的回溯。
下列代码是应用到跳棋游戏中的Alpha-Beta剪枝算法:
float Alpha_Beta(int deepth, Transform[] all_chess_tran,float alpha,float beta)//Alpha-Beta算法
{
if (deepth == 0)
{
Vector3[] all_chess_posion2 = new Vector3[20];
for (int i = 0; i < 20; i++)
{
all_chess_posion2[i] = all_chess_tran[i].position;
}
return score(all_chess_posion2);
}
if (deepth % 2 == 0)
{
//建立局部可移动棋子数组
Vector3[,] bot_chess_move2 = new Vector3[10, 6];
for (int i = 0; i < 10; i++)//搜索可移动棋子
{
for (int j = 0; j < 6; j++)
{
check_hit_new(i, j, all_chess_tran, bot_chess_move2);
}
}
//更改all_chess_posion2值
for (int i = 0; i < 10; i++)
{
for (int j = 0; j < 6; j++)
{
if (bot_chess_move2[i, j] != new Vector3(999, 999, 999))
{
Vector3 old_posion = all_chess_tran[i].position;
all_chess_tran[i].position = bot_chess_move2[i, j];
float newval = Alpha_Beta(deepth - 1, all_chess_tran,alpha,beta);
if (newval > alpha)
{
alpha = newval;
//保存最佳移动方式
best_num = i;
best_posion = all_chess_tran[i].position;
}
all_chess_tran[i].position = old_posion;
if (alpha > beta)
{
return alpha;
}
}
}
}
return alpha;
}
else
{
Vector3[,] bot_chess_move3 = new Vector3[10, 6];
for (int i = 0; i < 10; i++)//搜索可移动棋格
{
for (int j = 0; j < 6; j++)
{
check_grid_new(i, j, all_chess_tran, bot_chess_move3);
}
}
for (int i = 0; i < 10; i++)
{
for (int j = 0; j < 6; j++)
{
if (bot_chess_move3[i, j] != new Vector3(999, 999, 999))
{
Vector3 old_posion = all_chess_tran[i + 10].position;
all_chess_tran[i + 10].position = bot_chess_move3[i, j];
float newval = Alpha_Beta(deepth - 1, all_chess_tran, alpha, beta);
if (newval < beta)
{
beta = newval;
}
all_chess_tran[i + 10].position = old_posion;
if (alpha > beta)
{
return beta;
}
}
}
}
return beta;
}
}