五子棋人工智能人机博弈毕业设计

合集下载
  1. 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
  2. 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
  3. 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。

五子棋人工智能人机博弈毕业设计
目录
第1章引言 (3)
§1.1人工智能 (3)
§1.2人机博弈和五子棋 (3)
§1.3 Visual C++ (4)
第2章需求分析 (5)
§2.1使用围要求 (5)
§2.2功能要求 (5)
§2.3系统平台要求 (5)
第3章人机界面设计 (6)
第4章面向对象分析 (9)
§4.1对象设计 (9)
§4.2动态模型 (10)
§4.3功能模型 (10)
第5章面向对象设计 (12)
§5.1类设计 (12)
§5.2控制流程 (13)
第6章详细设计及编码 (15)
§6.1全局数据 (15)
§6.2游戏循环 (15)
§6.3界面设计及事件处理 (15)
§6.4人类玩家的Think操作 (17)
第7章计算机智能设计 (18)
§7.1棋局估值 (18)
§7.2极大极小值算法 (19)
§7.3 Alpha-beta算法 (22)
§7.4 Alpha-beta算法的窗口效应 (26)
§7.5极小窗口搜索/PVS算法 (27)
§7.6预估排序和历史启发 (28)
§7.7有限围限定 (31)
§7.8多核优化 (31)
第8章总结结论 (33)
§8.1各算法效率对比 (33)
§8.2成果与不足 (34)
参考文献 (35)
致谢 (36)
第1章引言
§1.1人工智能
提到人工智能,可能最著名的便是1997年超级计算机“深蓝”战胜国际象棋冠军卡斯帕罗夫的事,可以说“深蓝”的获胜是人工智能影响力的一个里程碑。

对于什么是人工智能,有很多定义,我认为就是能自动完成人类所能完成的一些思维活动。

如果从这个意义上说的话,计算机学科所要解决的所有问题都与人工智能有关。

它的发展历史和计算机科学的发展历史是联系在一起的,但也不仅仅局限于计算机科学,也涉及到心理学、哲学、语言学、医学等很多门学科。

它所包含的容有:知识表示、自动推理和搜索方法、机器学习和知识获取、知识处理系统、自然语言理解、计算机视觉、智能机器人、自动程序设计等方面。

§1.2人机博弈和五子棋
人机博弈就是人和计算机下棋,其重点在于计算机如何实现对弈,它是人工智能研究的一个重要分枝,五子棋是一种对弈游戏,本设计的目的就是通过五子棋这种对弈游戏初步探索实现博弈算法,为将来的人工智能研究打下一个基础,其实现方法可以从以下几方面概述:
一、棋盘状态用一种数学模型来表示当前棋局的状态,不同的对弈游戏有不同的表示方法,本设计用五子棋这种游戏来实现,它的棋盘状态比较简单,可以用一个15*15的二维数组表示15*15的棋盘,某棋盘上位置的状态就用该数组中单元的数字表示,比如1表示黑子,0表示白子,-1表示空位。

二、对弈规则每种对弈游戏都有不同的规则,规定了棋子的走法(如象棋)或者能下子的位置以及胜负的判定。

五子棋的规则很简单,如果不考虑禁手,任意空位都可以放棋子,只要一方有五个棋子连成一线,则判胜。

三、搜索技术要让计算机在下棋中有智能,搜索技术是至关重要的,简单的说就是让计算机试着某步走法,这一步走了之后,再往后走若干步,按照某种规则给这一步评分,最后,在所有走法中选择一个相对最佳的走法。

四、棋局估值就是对某一种棋盘局面的优劣作出评价,简单地就是说走某一步棋后,这一步棋走得好不好。

它在博弈技术中也是很重要的,从某种意义上来说,搜索也是为了估值,或者说对估值作修正。

§1.3Visual C++
本次的开发工具为Microsoft visual c++ 2005,它是微软公司推出的win32的开发工具,本次实现之所以选用Visual C++作为开发工具,除因个人使用习惯外,主要是它生成的代码高效,具有灵活方便的类管理,以及界面设计的可视化特点。

另外,在Visual C++ 2005中引入了OpenMp的支持,OpenMp为快速开发具有多核优化的程序提供简单方便的支持,使得博弈程序更加高效。

第2章需求分析
§2.1使用围要求
五子棋规则简单,男女老幼都可以玩,不但能让人们在对弈过程中得到娱乐,而且还能开发智力。

§2.2功能要求
1、支持人机对战。

2、能设置计算机智能等级。

3、能保存棋局状态,同时也能读入上一次保存的棋局状态,并且能够接着上一次继续下棋。

4、能悔棋,即玩家在下错子后能回到上一步。

5、背景音乐和下棋音效。

§2.3系统平台要求
编程语言:C++
操作系统:windows
开发工具:microsoft Visual C++
第3章人机界面设计
图3.1 五子棋程序界面
如图3.1,在左边的为15×15的棋盘,右边的“黑子”和“白子”下接列表框设定持黑子和白子的玩家类型,各有三项,分别是“人”,“计算机”,“当选择“人”后,就可以用鼠标在棋盘下放该类型的棋子,当为“计算机”,则由计算机自动产生走法并在棋盘上下该种类型的棋子,并且会弹出一个选择智力级别的对话框。

图3.2 计算机智力等级设定对话框
在玩的过程中,如果不想玩了,可以把当前棋局保存下来,按保存按钮,此时弹出保存棋局对话框
图3.3 保存棋局对话框
要载入以往的棋局继续玩,点击载入按钮,此时显示载入棋局对话框,选择以往的棋局文件即可。

图3.4 打开棋局对话框
如果不想玩此局了,则可按新局按钮,此时弹出“确认新局”对话框让用户确认:
图3.5 重新开始对话框
选择是将开始新局,否返回到旧局继续下。

如果退出程序,此时显示“确认退出”对话框。

图3.6 是否退出对话框
选择“是”按钮则退出,“否”继续下。

.参考资料.
第4章 面向对象分析
§4.1 对象设计
根据需求分析中的要求,初步的对象设计如下:
图4.1 各对象说明如下:
1、棋盘对象:表示15×15的五子棋棋盘,表示上面放了哪些子以及它们的位置,或者是每一个位置放了什么子。

其属性如下:
(1) Board ,记录棋盘上各棋子的位置信息。

(2) BlackPlayer ,表示持黑棋的玩家。

(3) WhitePlayer ,表示持白棋的玩家。

(4) CurrentPlayer ,指针,表示当前该下子的玩家。

2、玩家对象:表示下棋者,可以是人,计算机,一个棋盘对象对应两个玩家对象。

其属性如下:
(1)Side ,表示该玩家所持什么棋子。

3、人玩家对象,该类继承自玩家类,这个对象代表利用程序界面进行下棋的人。

4、计算机玩家对象,该对象继承自玩家对象,这个对象通过计算产生一个走法。

1 1 对手
下棋 棋盘
玩家
1
2
人玩家
计算机玩家
.参考资料. §4.2 动态模型
棋盘的动态模型
图4.2 动态模型图 §4.3 功能模型
1、系统输入/输出值
图4.3.1 功能模型图
2、下棋数据流图
玩家 下子位置 子
.参考资料.
图4.3.2 下棋数据流图 说明:
接收棋局状态并产生走法:该操作由相应的玩家对象负责,玩家接收当前棋局的状态并产生下子的位置,即棋盘坐标。

不同类的玩家该操作有所不同。

接收下子:该操作由棋盘对象负责,它接收玩家发来的下子位置,同时判断是否合法,不合法则不作任何操作,合法则改变棋盘状态,以及把当前玩家更改为另一方。

是否结束:该操作判断当前游戏是否结束,以及哪一方结束。

下棋数据流图(0层)
该局结束
棋局信息
无效棋盘坐标
第5章面向对象设计
§5.1类设计
由《五子棋程序面向对象分析》可得出该程序一共有四个类。

1、棋盘类
(1)属性
board:表示15×15棋盘,可用一个二维数组表示
int board[15][15];
pBlackPlayer,pWhitePlayer,pCurrentPlayer:分别表示持黑子的玩家和持白子的玩家以及指向当前应下子玩家的指针。

stack<POINT> chessStack,记录下子位置的堆栈,以便于悔棋操作。

(2)操作
bool PutChess(int x,int y,int side),接受下子,如果下子位置以及棋子正确则改变棋盘,并把当前玩家指向对方。

Undo(),接受玩家的悔棋,恢复棋盘到该玩家上次应下子的状态。

SaveBoard(char filename[]),把当前的棋局保存到filename指定的文件中。

LoadBoard(char filename[]),从指定的文件装入棋局。

void NewRound(void),开始新局清空所有棋子,清空历史走法,把当前轮次玩家置为黑方。

int IsGameOver();//游戏是否结束,返回1表示黑方胜利,返回2表示白方胜利,返回3表示和局,0表示游戏仍然进行。

其C++的类定义如下:
class CChessBoard{
int board[15][15];//表示15×15棋盘
stack<POINT> chessStack;
CPlayer *pBlackPlayer,*pWhitePlayer,*pCurrentPlayer;
Int IsGameOver(void);//判断游戏是否已经结束,没有则返回0,否则返回哪方胜利
void NewRound(void);
public:
bool PutChess(int x,int y,char side);//所指定位置可以放相当的棋子返回true,否则false
bool Undo(void);
bool SaveBoard(char filename[]);//保存棋盘到指定的文件中,如果成功返回true
bool LoadBoard(char filename[]);//从filename中装入棋盘,如果成功返回true。

2、玩家类,它表示玩家,它是一个纯虚类,为“人”、“计算机”玩家的公共接口。

(1)属性:
Side,表示该玩家所持什么棋子。

(2)操作:
POINT Think(char board[15][15]),根据传入的棋盘状态产生走法。

其C++定义如下:
class CPlayer{
int Side;//该玩家所持何子
public:
virtual POINT Think(char board[][15])=0;//根据传入的棋盘信息产生一种走法
cPlayer(char side){
Side=side;
}
~cPlayer();
};
§5.2控制流程
1、由于在Windows环境下程序采用事件驱动,因此,整个程序采用事件驱动机制,而这一控制最好放在消息循环中。

2、在界面建立时把设置玩家对象的列表框都选择为人玩家对象,并建立人玩家对象,并调用该函数在改变设置玩家对象的下拉列表框时调用,用
来设置好玩家对象。

3、改变玩家对象时所进行的操作,用户在改变玩家对象的下拉列表框中选择了另一种玩家对象时,系统会调用相应的事件处理函数,在该函数中删除原有的玩家对象,并创建一个新的玩家对象。

4、在游戏循环中,每当有一个玩家由Think()操作得到了一个下子位置,然后就调用棋盘类的PutChess()来下子,该函数检查下子位置是否合法,如果不合法,则什么也不做,直接返回。

如果合法,则更新棋盘数据,画该棋子,如果该方胜利,则显示胜利信息并开始新局,否则把当前玩家设为对方。

第6章详细设计及编码
§6.1全局数据
CChessBoard类的对象chessBoard为一全局对象,表示棋盘。

§6.2游戏循环
游戏进程的调度在游戏循环中完成,在windows程序中,最适合的位置就是消息循环处理中。

而基于对话框的程序有自己的消息循环,在消息循环中要调用ContinueDodal()函数,帮游戏循环合适在这一函数中处理,代码如下:
BOOL CFiveChessDlg::ContinueModal()
{
POINT point=chessBoard.pCurrentPlayer->Think(chessBoard.board);
if(chessBoard.PutChess(point)){
this->Invalidate();//重绘棋盘
int side=chessBoard.IsGameOver();
if(side){//游戏结束
显示游戏结束信息
}
}
return CDialog::ContinueModal();
}
§6.3界面设计及事件处理
应用程序框架采用基于MFC的对话框程序,使用AppWizard建立好后,添加好各控件,对话框的类名为CFiveChessDlg。

如下图所示:
图6.3.1 五子棋程序界面
1、黑方,白方下拉列表框
黑白下拉列表框在CFiveChessDlg类中类型为CComboBox类的成员对象,名称分别为m_comboBlack、m_comboWhite;为两个下拉列表框里面有“人”和“计算机”两项,默认是“人”这一项,用户重新选择玩家类型时,则改变玩家类型,如果是计算机玩家,则弹出设置计算机等级对话框(用CLevelDlg对话框类表示),确认后创建相应等级的计算机玩家。

图6.3.2 设置计算机智力对话框
2、悔棋、新局、保存、载入、退出按钮
在棋盘类(CChessBoard)中用一个栈来保存下棋历史,该栈中数据元素为POINT类型,悔棋时进行出栈操作,每次出栈两个棋子位置,将它们对应棋盘坐标设置为无子(NONE)。

新局操作将棋盘所有位置设置为无子(NONE),清空下子历史栈,将当前
玩家设置为黑方。

保存按钮按下后先弹出一个保存文件的对话框。

文件扩展名为fsv,用户指定文件名按确定后,先写入4字节的文件标识(‘f’,’i’,’v’,’e’),然后写入棋盘数据(board[15][15]),再写入下子位置历史栈,栈因为顺序是相反的,为以后载入方便,这里又建立了一个栈,将数据倒置后再保存。

载入与保存一致,先读取文件标识,如果标识不对,则不载入,然后载入棋盘数据,再载入历史栈,然后根据历史栈中最后一个模棋子设置好当前玩家。

退出实际上是对话框的OK按钮,点击后直接退出。

3、棋盘的绘制
棋盘的绘制在CFiveChessDlg类中的重绘函数(OnPaint)中处理,先用画线函数画好棋盘网格,15*15个格,从坐标(15,15)处绘制,每格30象素。

棋盘绘制好后绘制棋子,扫描全局对象chessBoard中棋盘数据board[15][15],绘制出所有棋子。

另外,从历史栈中取出最后一次下子坐标,将棋子外线用红色绘制。

4、鼠标消息的处理
为记录下人玩家的下子位置,需要处理鼠标消息,这通过重载OnLButtonDown函数实现,在CFiveChessDlg中添加了bool弄成员变量m_bMouse来记录鼠标是否被按下,mx,my记录按下的位置。

§6.4人类玩家的Think操作
玩家通过Think操作得出下子位置,人类下棋通过鼠标,当鼠标按下,CFiveChessDlg中的OnLButtonDown被执行,记录下鼠标是否按下bMouse,以及按下位置(mx,my)。

所以该Think操作就是查看鼠标是否按下,如果按下则取得按下位置返回。

如果没有按下就返回一个无效坐标(-1,-1),这样,因为返回的是无效下子位置,所以不会改变当前玩家,仍然是由该玩家下子。

一直在正确位置按下了鼠标为止,然后清除掉bMouse标志(表示我已经下过棋子了,需要重新接收鼠标按下)。

第7章计算机智能设计
机器智能设计需要解决的关键问题是估值和搜索技术。

§7.1棋局估值
人们在下棋过程中,走哪一步棋好,哪一步棋坏,有一定的评判标准,这个评判标准就是估值。

计算机估值也就是对这种好坏的一个量化,最简单的是静态估值,它仅从当前棋局来考虑,五子棋的静态估值最简单直观的便是根据相连的棋子数来评价棋局好坏。

所以仅需要扫描棋局中二子相连,三子相连,四子相连,五子相连的数量,下面是constant.h中定义的各种情况的分数。

const int MAX_VALUE=99999;
const int HTWO=20;//冲2分数
const int TWO=40;//活2分数
const int HTHREE=100;//冲3分数
const int THREE=300;//活3分数
const int HFOUR=1000;//冲4分数
const int FOUR=2000;//活4分数
MAX_VALUE是最大值表示会赢的局面分数,冲表示一端处于边界或者被对方棋子所堵的情况,活表示两端都有空位,如果两端都没有空位,则不可能形成五子相连,没有意义,不计分数。

另外,每个位置也给予不同的分数,如下面数组所示:
const int CEvaluation::m_nPosValue[15][15]={
{0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
{0,1,1,1,1,1,1,1,1,1,1,1,1,1,0},
{0,1,2,2,2,2,2,2,2,2,2,2,2,1,0},
{0,1,2,3,3,3,3,3,3,3,3,3,2,1,0},
{0,1,2,3,4,4,4,4,4,4,4,3,2,1,0},
{0,1,2,3,4,5,5,5,5,5,4,3,2,1,0},
{0,1,2,3,4,5,6,6,6,5,4,3,2,1,0},
{0,1,2,3,4,5,6,7,6,5,4,3,2,1,0},
{0,1,2,3,4,5,6,6,6,5,4,3,2,1,0},
{0,1,2,3,4,5,5,5,5,5,4,3,2,1,0},
{0,1,2,3,4,4,4,4,4,4,4,3,2,1,0},
{0,1,2,3,3,3,3,3,3,3,3,3,2,1,0},
{0,1,2,2,2,2,2,2,2,2,2,2,2,1,0},
{0,1,1,1,1,1,1,1,1,1,1,1,1,1,0},
{0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}
};
为方便以后扩展,估值单独用一个类(CEvaluation)来实现,其下有一个公共的虚函数value()用来给指定的棋局估值。

其定义如下:int value(int board[][15],int side);
board传入棋局状态,value给出估值,所得值是相对于黑方来说的,黑方局势越好,估值越大,反之,白方就要尽可能让这个值越小。

计算值的过程实际上就是统计连续相同二子,三子,四子,五子的过程,依次按左下方,下方,右下方,右方扫描棋盘上每个棋子连续相连数目,然后计算分数,最后加上每个棋子所在位置的分数。

可以看出,上面的效率并不高,实际上,父棋局的值估计好后,其子棋局与父棋局仅多一个棋子,所以其它棋子不需要再重复计算,只需要计算由于这一棋子所带来的分数变化即可。

故有一个重载版的估值函数:int value(int board[][15],int side,int pvalue,int y,int x);
其中pvalue是父棋局的估值,(x,y)指出了下子的位置
§7.2极大极小值算法
有了估值算法,我们可以简单地试着产生一个最大估值的走法,如下图所示:
图7.2.1 深度为1的对弈树
根结点表示当前棋盘局势,此时该电脑下子,它试着产生每一种可能走法,对于五子棋,最多15*15种,每一种走法都用估值引擎估值,如果此时电脑执黑子,它会选择估值最大的一种走法(估值引擎中棋局的值是黑方的价值减去白方的价值),因为这种走法对自己最有利,如果执白子,则选择估值最小的走法。

其算法表示如下:
POINT Search(int board[][15],int side,CEvaluation *pe){
POINT pos,best;
if(side==BLACK)
best=-MAX_VALUE;
else
best=MAX_VALUE;
for(int y=0;y<15;y++)
for(int x=0;x<15;x++){
board[y][x]=side;
value=pe->value(board,side==BLACK?WHITE:BLACK);
board[y][x]=NONE;
if(value>best){
best=value;
pos.x=;pos.y=y;
}
}
return pos;
}
有了这个算法,计算机勉强有点智能,可以下一下棋了,但它还是比较笨,只管眼前这一步,而不能看到两步,三步,或者以后更多步的结果。

要让计算机足够聪明,必须采用某种搜索算法。

与估值引擎一样,为了以后方便扩展,单独设计了一个CSearch的纯虚类来作为公共接口。

class CSearch{
public:
virtual POINT Search(int board[][15],int side,int depth,CEvaluation *pE)=0;//
};
在上面深度为1的算法中,我们看到,每一个结点产生子结点,要在这子结点中选择一个对自己最有利的子结点,所以,对于黑方来说,总是选择子结点值为最大的结点来走,而不会选择其它结点,所以,该最大值的子结点值就是该结点的值。

白方也一样,如下图所示:
图7.2.2 深度为3的对弈树
根结点n为当前棋局状态,它产生了子结点n1……nk,并且要在子结点中选择一个对自己最有利的一个节点来下子,如果此时n节点该黑方走,就会选择其中最大值的节点。

但是此时n1……nk的节点值此时还不知,比如说n1的值必须要通过计算n1的子结点n11……n1k,一直到达叶子结点为止,n 结点为黑方,则n1为白方,它会选择所有子结点值为最小的作为自己的结点值,n2,n3,……,nk结点的值计算也一样,最后,n2,n3,……,nk当中最大的一个结点就作为n的结点值,也就是需要确定的走法。

程序中极大极小搜索引擎的类为CMaxMinSearch,它继承至CSearch,按指定的深度,搜索产生一个走法:其搜索引擎伪码表示如下:int generate(board,int side,int depth){
int best;
游戏结束返回估值
深度为0估值返回
Best=极大极小值
for(each possible pos){
board[y][x]=side;
int value=generate(board,anotherside,depth-1);
board[y][x]=NONE;
if(side==BLACK)
best=max(value,best); else
best=min(value,best); }
return best; }
CmaxMinSearch 类在其公共接口中调用这个函数,选择最佳的一个走法,其Search 函数如下:
POINT CMaxMinSearch::Search(int board[][15],int side,int depth,CEvaluation *pe);
它调用generate 函数计算得出子节点的值,然后选择一个最佳子节点的坐标返回,作为下子的位置。

§7.3 Alpha-beta 算法
极大极小值算法的效率不高,如下图(8-1)所示:
n 节点此代表黑方该下子的节点,它要在它的子节点中找一个值最大的子节点max(n1,n2,……,nk)作为自己的值(或下子),此时它得到其子节点n1的值为50,然后计算n2节点的值,此时n2为白方下子的节点,它要搜索一个值最小的子节点作为自己的值,这时它搜索计算得出其子节点n21的值为43,此时,我们就可以得出结论,因为n2要取最小子节点值作为自己的
nk 为极小值节
n11=43
图7.3.1 Alpha 截枝
值,所以n2的值不会大于43,所以n2更不会大于前面的兄弟节点n1,所以n2一定不会被n 选作为最佳子节点,故,在搜索n2的子结点n21——n2k 的过程中,只要n2有子节点的值小于50,则n2就不必再搜索了,后面n3,n4……nk 也一样,只要它们的某子结点值小于前面兄弟结点的最大值,则不必须再搜索了。

我们将取极大值节点(上图中n )所产生子节点的最大值用变量alpha 表示,如果子节点(极小值节点)在搜索过程中,产生的子节点值小于alpha ,则子节点不需要搜索了,这叫做alpha 截枝。

再推广到搜索深度很深的情况,如上图(图8-2)所示,取极大的值节点n 的不知道哪一代子节点nx ,它的祖先为n2m ,n2,n ,它为取极小值节点,假设它的某子节点的值小于n1=50,假如是nx1=40,那么nx 是否应该放弃其子节点的搜索而截枝呢?我们可以这样推论:n 的子节点现在搜索得到的最大值是50,alpha=50,对于n 的子节点n2……nk ,它们在搜索过程中,所产生的不被截枝的任一子节点必须大于50(否则必被截枝),故n2m 未被截枝大于50,n2m 是取极大值的节点,所以其子节点中必有大于50的节点,故可得到在n2m 这一层所得到的最大节点值alpha ≥50,由此可以再推而广之,n2m 的子中,凡是取极大值的节点,它们的子节点的最大值都应该≥50,到了nx 这一层,nx 是取极小值的节点,它的父亲是取极大值的节点,所以alpha 也应该≥50,故nx1=40应该被截枝。

为极小值节
点(白极大值节点
图7.3.2
再来看另一方的情况,如上图(图8-3)情况与图8-1相反,n 为取极小值节点,它的某子节点的值为20(n1),搜索其后继子节点,如图中的n2,n2的子节点中某子节点n21的值为33,而n2取极大值,n2的值至少为33,故n2的父节点不可能选择n2,所以n2中一旦搜索发现其子节点值大于兄弟节点中的最小值,就没有搜索必要了。

我们将取极小值节点的子节点中最小值用变量beta 表示,某子节点在搜索子节点的过程中,如果子节点的值大于beta ,则该子节点不需要再进行搜索,这叫beta 截枝。

再推而广之,任何一层取最小值的节点,它的子节点值大于beta ,则应该截枝。

综合两种情况,由此而得到alpha-beta 算法,下面用伪码表示: int generate(board,int side,int depth,int alpha,int beta){ 游戏结束估值返回; 到叶子节点估值返回;
if(side==BLACK){//此节点该黑方,取极大值 for(Each Possible Move){ if(alpha>=beta)
return alpha; board[y][x]=side; int
value=alphbeta(board,anotherside,depth-1,alpha,beta);
board[y][x]=NONE; if(value>alpha)
alpha=value;//子节点的最大值记录到alpha 中
nk 为极大值节
n11=33
图7.3.3 Beta 截枝
}
return alpha;//返回最大值
}else{
for(each possible move){
if(alpha>=beta)
return beta;
board[y][x]=side;
int
value=generate(board,anotherside,depth-1,alpha,beta);
board[y][x]=NONE;
if(value<beta)
beta=value;//最小子节点的最
}
return beta;
}
其外层调用函数search代码如下,不同的是search返回坐标值:
POINT CAlphaBeta::Search(int board[][15],int side,int depth,CEvaluation *pE){
POINT pos={-1,-1};
int alpha=-MAX_VALUE,beta=MAX_VALUE;
if(side==BLACK){
for(int i=0;i<15*15;i++){
int y=i/15;int x=i%15;
if(board[y][x])
continue;
int cboard[15][15];
memcpy(cboard,board,sizeof(cboard));
cboard[y][x]=BLACK;
int
value=generate(cboard,WHITE,depth-1,pE,alpha,beta);
if(value>alpha){
alpha=value;
pos.x=x;
pos.y=y;
}
}
}else{
for(int i=0;i<15*15;i++){
int y=i/15;int x=i%15;
if(board[y][x])
continue;
int cboard[15][15];
memcpy(cboard,board,sizeof(cboard));
cboard[y][x]=WHITE;
int
value=generate(cboard,BLACK,depth-1,pE,alpha,beta);
if(value<beta){
beta=value;
pos.x=x;
pos.y=y;
}
}
}
return pos;
}
§7.4Alpha-beta算法的窗口效应
前面描述了对于取大值的节点,其任意子点的值不能大于beta,而取极小值的节点,其任意子节点的值不能小于alpha,事实上,alpha-beta算法建立了一个围为(alpha,beta)的窗口,对于搜索到的有意义的子节点,其值必在落在(alpha,beta)这围。

故对于最取节点,alpha=-MAX_VALUE,beta=MAX_VALUE,而alpha-beta算法在搜索取极大值节点的子节点时,alpha 值不怕增大,在搜索取最小值节点的子节点时,beta值不断减小,随着这个窗口不断减小,剪枝效率也就越来越高。

因而,改进alpha-beta效率的关键就在于怎么尽可能地缩小(alpha,beta)这个窗口的大小而又不失其正确性。

这可以从两个方面加以考虑,第一种就是人为地减小这个窗口的大小,比较经典的算法有Fail-soft Alpha-Beta算法、渴望搜索、极小窗口搜索(又称PVS算法),PVS算法是一种很优秀的算法,在实际应用中相当普遍,故本文采用PVS搜索来优化。

第二种对Alpha-beta优化的思路是如果在一开始就能够搜索到比较好的节点,那么Alpha很快就能得到比较大的值,Beta很快就能得到比较小的值,从而缩小了窗口的大小,但怎么一开始就得到比较好的搜索节点这可能与具体的游戏相关,也可以采用历史启发算法。

后面我们一分别实现。

§7.5极小窗口搜索/PVS算法
一、算法分析
顾名思义,极小窗口搜索中的窗口是最小的(0容量),该算法先计算第一个子节点的值作为当前最佳节点值best,如果是取极大值的节点,对以后的子节点搜索中,用(alpha,alpha+1)窗口大小搜索,对于取极小值的节点,则用(beta-1,beta)对后继子节点进行搜索,得到值value,由于不能取边界,这实际上是零大小的窗口搜索,所以很快能得到结果,比如取极大值的节点,要么value比alpha大,要么比alpha小,比alpha小的节点直接可以忽略过了,但如果比alpha大,value也并不准确,但可以确定,最大的值肯定比value还要大,故必须重新搜索,采用新的窗口大小,取极大值的节点采用(value,beta),取极小值的节点采用(alph,value)进行搜索。

综上所述,给出PVS算法与一般的alpha-Beta算法仅在外层调用搜索函数时所给出的alpha,beta的参数的不同,其余一样。

所以,仅需要对Search 函数中调用generate处稍稍进行修改:
将黑方处的代码:int value=generate(board,WHITE,depth-1,pe,alpha,beta);
改为:
int value=generate(cboard,WHITE,depth-1,pe,alpha,alpha+1);
if(value>alpha)
value=generate(cboard,WHITE,depth-1,pe,value,beta);
将白方处的代码:int value=generate(board,BLACK,depth-1,pe,alpha,beta);
改为:。

相关文档
最新文档