井字棋程序的
设计
领导形象设计圆作业设计ao工艺污水处理厂设计附属工程施工组织设计清扫机器人结构设计
与算法
作者姓名: 周翔
电子邮箱: seafrog@163.com
摘要:
本文就作者编写的井字棋程序进行了简要的介绍,并重点介绍了该程序采用的算法、程序设计
方案
气瓶 现场处置方案 .pdf气瓶 现场处置方案 .doc见习基地管理方案.doc关于群访事件的化解方案建筑工地扬尘治理专项方案下载
、对算法的改进等内容。
关键字:井字棋,评估函数,极大极小值算法,α-β剪枝算法
1. 程序说明
本程序旨在完成一个具有人机博弈功能的井字棋程序,具有良好的用户界面、支持人机对弈和双人对弈两种模式,并具有悔棋、选择难易级别等功能。该程序是中科院研究生院2005年秋季学期《人工智能原理》课程的项目一。
2. 设计方案
2.1 设计步骤
本程序最主要的任务是完成图形界面的井字棋的人机对弈模块的设计。在人机对弈过程中,计算机方所采用的算法,也就是博弈树的搜索技术是最重要的。所以,设计时,作者按照以下步骤进行:
(1) 选定博弈算法;
(2) 建立一个简单的应用程序(如字符界面程序)来测试算法;
(3) 选定图形界面中要实现的其他功能(如双人对弈、悔棋、难易级别选定、联机对战等); (4) 实现图形界面的井字棋程序。
所采用的核心算法将在第3节介绍。本程序要实现两个程序,一个是字符界面的算法测试程序,另一个是要正式提交的图形界面程序。下面是对这两个程序的设计方案的介绍。 2.2 字符界面的算法测试程序
该测试程序由标准C++编写,作者采用了极大极小值算法。除了主程序外,它还包括具有以下功能的函数:
(1) 棋盘初始化函数:void Init();
(2) 打印棋盘函数:void PrintQP();
(3) 用户输入落子位置函数void UserInput();
(4) 判断当前棋局是否有一方获胜,并判断哪一方获胜的函数:int IsWin(State s); (5) 评估函数值计算函数:int e_fun(State s);
(6) 极大极小值算法主函数:int AutoDone();
其中前三个函数是对程序中当前的棋局进行读写操作的函数,第4、5个函数是对当前的棋局进行判断的函数,最后一个函数中包含了计算机决定在哪个位置落子所采用的核心算法,并且可以判断计算机落子前后棋局的状态,如果在搜索树的深度范围内能判断哪一方必胜,则可提前打印输赢信息,并结束本棋局。字符界面的测试程序的运行过程如图1所示。 2.3 图形界面的井字棋程序
图形界面的井字棋程序使用MFC(微软基础类库)来构建图形界面,同时将字符界面程序中的void Init()、int IsWin(State s)、int e_fun(State s)、int AutoDone()四个函数拿出来放在一个独立的头文件中。棋盘绘制和显示对弈状态信息通过视图类CJzqView类来实现,而且所有的井字棋对弈功能的实现全部包含在CJzqView类中。作者定义的一些菜单消息如下所示,菜单消息包含了该程序实现的所有功能:
ID_NEW_QP:新开一局,
ID_QP_HUI:悔棋,
ID_AUTO_GAME:人机对弈,
ID_TWO_MAN:双人对弈,
ID_MAN_FIRST:设定人先下,
ID_COMPUTER_FIRST:设定计算机先下,
ID_EASY、ID_MID、ID_HARD:设定难易级别(容易、中等、困难), 此外,我还定义了ON_WM_LBUTTONUP消息的响应函数,以便于用户落子。此外,对弈结束后,用鼠标左键点一下棋盘就可以开始新的对弈,而以前的设定保持不变。
图1:字符界面的井字棋程序的运行过程
需要说明的是,在处理双人对战时,虽然程序不会调用AutoDone和e_fun函数,但是程序把参与对弈的第二个人当做“电脑”,即,用来标志电脑的变量被用来标识第二个人,这样可以减少变量的个数。比如ComputerFirst=1时,则
表
关于同志近三年现实表现材料材料类招标技术评分表图表与交易pdf视力表打印pdf用图表说话 pdf
示第二个人先下,MansTurn=0时,则表示当前该第二个人落子。
3. 核心算法及源代码
3.1 定义的数据结构和变量
由于本程序采用的核心算法是极大极小值算法,所以,在实现算法之前,必须定义一些数据结构来保存生成的状态节点。因此,我定义了State结构:
struct State //该结构表示棋盘的某个状态,也可看做搜索树中的一个节点 {
int QP[3][3]; //棋盘格局
int e_fun; //当前状态的评估函数值
int child[9]; //儿女节点的下标
int parent; //双亲节点的下标
int bestChild; //最优节点(评估函数值最大或最小)的儿女节点下标 }States[MAX_NUM]; //用来保存搜索树中状态节点的数组
我使用了States[MAX_NUM]数组来保存生成的状态节点,通过State结构中的parent、child域构成了一个搜索树,并通过bestChild域保存了一条从根节点到叶节点的最优解路径。特别的,States[0]作为根节点保存了当前的棋局状态。为了保存当前对弈过程的状态信息,我定义了以下常量:
//以下常量表示棋局当前的状态
const int DRAW_GAME=1100; //棋局为平局
const int COMPUTER_WIN=1101; //计算机赢了
const int MAN_WIN=1102; //人赢了
const int PLAYING=1103; //对弈正在进行
const int WAIT_4_PLAY=1104; //正在等待开始
const int MAX_NUM=1000; //扩展生成状态节点的最大数目
const int NO_BLANK=-1001; //表示没有空格:棋盘上没有空余的位置来落子 const int NIL=1001; //表示空
并定义下列全局变量保存临时信息:
static bool MansTurn; //是否该人下了
static bool ComputerFirst; //是否计算机先下
static int Level; //当前难易级别
static int Mode; //模式:是人机对战(1)还是双人对战(0)
static int Now; //表示棋局现在的状态
static int pos_x,pos_y; //表示计算机落子的位置的x,y坐标
static int TREE_DEPTH=2; //搜索树的最大深度,如果增加此值可以提高计算机的“智力”,
//但同时也需要增加MAX_NUM的值。
static int s_count; //用来表示当前分析的节点的下标
同时,我还定义了两个3×3的二维数组,AutoDone函数中要用到其中的一个来保存临时的棋盘格局,另一个则是用来保存上一步的棋盘格局,悔棋时可将此棋局覆盖当前的棋局States[0]。
3.2 算法及源代码
极大极小值算法的详细介绍请参看参考文献[2](第53页至55页)。在程序实现时,我使用了和[2]中相同的评估函数。评估函数的源代码如下所示:
int e_fun(State s)//评估函数
{
bool flag=true;
for(int i=0;i<3;i++)
for(int j=0;j<3;j++)
if(s.QP[i][j]==0)flag=false;
if(flag)return NO_BLANK;
if(IsWin(s)==-1)return -MAX_NUM;
if(IsWin(s)==1)return MAX_NUM;
int count=0;//count变量用来保存评估函数的值
//将棋盘中的空格填满自己的棋子,既将棋盘数组中的0变为1
for(i=0;i<3;i++)
for(int j=0;j<3;j++)
if(s.QP[i][j]==0)tmpQP[i][j]=1;
else tmpQP[i][j]=s.QP[i][j];
//电脑一方
//计算每一行中有多少行的棋子连成3个的
for(i=0;i<3;i++)
count+=(tmpQP[i][0]+tmpQP[i][1]+tmpQP[i][2])/3;
//计算每一列中有多少列的棋子连成3个的
for(i=0;i<3;i++)
count+=(tmpQP[0][i]+tmpQP[1][i]+tmpQP[2][i])/3;
//斜行有没有连成3个的,
count+=(tmpQP[0][0]+tmpQP[1][1]+tmpQP[2][2])/3;
count+=(tmpQP[2][0]+tmpQP[1][1]+tmpQP[0][2])/3;
//将棋盘中的空格填满对方的棋子,既将棋盘数组中的0变为-1
for(i=0;i<3;i++)
for(int j=0;j<3;j++)
if(s.QP[i][j]==0)tmpQP[i][j]=-1;
else tmpQP[i][j]=s.QP[i][j];
//对方
//计算每一行中有多少行的棋子连成3个的
for(i=0;i<3;i++)
count+=(tmpQP[i][0]+tmpQP[i][1]+tmpQP[i][2])/3;
//计算每一列中有多少列的棋子连成3个的
for(i=0;i<3;i++)
count+=(tmpQP[0][i]+tmpQP[1][i]+tmpQP[2][i])/3;
//斜行有没有连成3个的,
count+=(tmpQP[0][0]+tmpQP[1][1]+tmpQP[2][2])/3;
count+=(tmpQP[2][0]+tmpQP[1][1]+tmpQP[0][2])/3;
return count;
}
整个算法在AutoDone函数中实现,其实现过程分为以下几步:
(1)为了获得最优的落子位置,在算法中首先要生成搜索树。其中,我把States[0]作为树根节点,根节点所在的层是极大层(MAX层),然后通过向棋盘中没有落子的空格添一个对方的棋子生成下一层(极小层,MIN层)的树节点,如果当前树的高度大于等于TREE_DEPTH(>=1)全局变量,则停止生成节点,否则则继续生成下一层节点(如果当前节点层为MIN层,则下一层为MAX层,否则,则下一层为MIN层)。生成每一层时可为每一层的属性(MAX或MIN)做标记,生成每个节点时,应计算这个节点的评估函数值,并将其保存在状态节点的e_fun域中。这一步的源代码如下所示:
for(int t=0;t
tag) //保留叶节点的评估函数值
{
States[i].e_fun=e_fun(States[i]);
}
else //抹去非叶节点的评估函数值
States[i].e_fun=NIL;
}
(3)然后,通过层次遍历获得每个非叶节点的评估函数值,同时将非叶节点的bestChild域指向最佳子女,从而形成一条从根节点到叶节点的最佳解路径。这一步的源代码如下所示:
while(!IsOK)//寻找最佳落子的循环
{
for(int i=s_count;i>tag;i--)
{
if(max_min)//取子女节点的最大值
{
if(States[States[i].parent].e_funStates[i].e_fun||States[States[i].parent].e_fun==NIL)
{
States[States[i].parent].e_fun=States[i].e_fun; //设置父母节点的最大最小值
States[States[i].parent].bestChild=i; //设置父母节点的最佳子女的下标
}
}
}
s_count=tag; //将遍历的节点上移一层
max_min=!max_min; //如果该层都是MAX节点,则它的上一层都是MIN节点,反之亦然。
if(States[s_count].parent!=NIL) //如果当前遍历的层中不包含根节点, //则tag标志设为上一层的最后一个节点的下标
tag=States[s_count].parent;
else
IsOK=true; //否则结束搜索
}
(4) 最后,将当前的棋局更新为其最优子女节点的棋局,并获得落子的位置: //取落子的位置,将x,y坐标保存在变量pos_x和pos_y中,
//并将根(当前)节点中的棋局设为最佳子女节点的棋局
for(int x=0;x<3;x++)
{
for(int y=0;y<3;y++)
{
if(States[States[0].bestChild].QP[x][y]!=States[0].QP[x][y])
{
pos_x=x;
pos_y=y;
}
States[0].QP[x][y]=States[States[0].bestChild].QP[x][y];
}
}
此外,AutoDone函数中还有一些后续操作(比如判断当前棋局是否有一方获胜、打印状态信息等),请参看附件中的源代码文件zx_jzq.cpp。
3.3 “智商”分析
在我采用的算法中,可以通过增加生成树的层数,即增加TREE_DEPTH的值来提高计算机的智商。这相当于增加了计算机向前预测的步数。对井字棋来说,因为井字棋有9个格,所以TREE_DEPTH的最大值可以设为9,但是实际上,经过试验,当TREE_DEPTH,3时,计算机对井字棋的落子的处理就能达到比较好的效果。在已经实现的程序中,我将游戏难度分为3个等级:容易(TREE_DEPTH,1),中等(TREE_DEPTH,2),困难(TREE_DEPTH,3)。特别的,当TREE_DEPTH,1时,计算机只预测下一步人的走法,而TREE_DEPTH,3时,计算机会综合考虑以后3步的走法。当TREE_DEPTH,3时,程序生成的最大状态节点数为:1,9,9×8,9×8×7,585个,这对现在的计算机来说,是可以在可接受的时间范围内处理完这些状态的。但对于状态个数较多的情况,如中国象棋或围棋,搜索树层数的增加会导致计算机在空间和时间上耗费呈指数级的增长。 当TREE_DEPTH大于等于1时,可以对棋局进行预测,因此就可以提前判断输赢。比如在图1所示的对弈过程中,当棋局矩阵为(1表示一方落下的棋子,-1表示另一方落下的棋子,0表示在该位置上没有棋子):
-1 -1 1
0 1 0
0 0 1
时,就可以判定执“1”的一方赢了,因为执“1”的一方在(1,3)点的棋子落下以后产生了两个先手,输赢关系可以在3步之内判定,如果把搜索树层数设置为2,则计算机在对手下一步棋之后才会知道自己赢。所以,在这个角度上看,搜索树层数的增加可以提高计算机的智商。 4.
总结
初级经济法重点总结下载党员个人总结TXt高中句型全总结.doc高中句型全总结.doc理论力学知识点总结pdf
在本程序中的井字棋程序使用了极大极小值算法,这种算法的思想是“考虑双方对弈若干步之后,从可能的走法中选一步相对较好的走法来走”,并且“在有限的搜索深度范围内进行求解”。
最大最小值算法的核心是将搜索树的层分为MAX层和MIN层,MAX层和MIN层交替相邻(即,一个节点如果在MAX层,则其子女节点在MIN层;如果在MIN层,则其子女节点在MAX层),在MAX层的节点的评估函数值取其子女节点中的最大者,在MIN层的节点的评估函数值取其子女节点中的最小者。
此外,需要定义一个评估函数来计算叶节点的评估函数值,要注意将某方获胜的状态节点的评估函数值设为计算机能表示的最大数(无穷大)或最小数(无穷小)以表明在该状态下有一方获胜。
最后,还要“在有限的搜索深度范围内进行求解”,如果搜索深度太大,则在状态数较多的情况下会使时间耗费或空间耗费达到无法忍受的程度。
5. 改进建议
本程序中的程序的博弈算法采用的是极大极小值算法,如果采用α-β剪枝算法,则可以在一定程度上减少博弈树的节点数。假设一棵树的深度为d,且每个非叶节点的分支系数为b,则在最佳情况下,α-β剪枝算法生成深度为d的叶节点数大约相当于极大极小值算法所生成的深度为d/2的博弈树的节点数。也就是说,为了得到最佳的一步,α-β剪枝算法只需要检测O(b^d/2 )个节点,而不是极大极小值算法的O(b^d )。从另一个角度看,在相同的代价下,α-β剪枝算法向前看走的步数是极大极小值算法向前看走的步数的两倍(见参考文献[2]第57页)。