目 录
前 言
黑白棋,也称反棋(Reversi)、奥赛罗棋(Othello),苹果棋,翻转棋是一个经典的策略性游戏。黑白棋是19世纪末英国人发明的。直到上个世纪70年代一个日本人将其发展,借用莎士比亚名剧奥赛罗(Othello)为这个游戏重新命名,也就是现在大家玩的黑白棋。为何借用莎士比亚名剧呢?是因为奥赛罗是莎士比亚一个名剧的男主角。他是一个黑人,妻子是白人,因受小人挑拨,怀疑妻子不忠一直情海翻波,最终亲手把妻子杀死。后来真相大白,奥赛罗懊悔不已,自杀而死。黑白棋就是借用这个黑人白人斗争的故事而命名。
游戏通过相互翻转对方的棋子,最后以棋盘上谁的棋子多来判断胜负。它的游戏规则简单,因此上手很容易,但是它的变化又非常复杂。
起初,黑白棋在西方和日本很流行,近年来,黑白棋正逐步为中国玩家所接受,并深受喜爱。
本次我选择这个题目作课程设计,一方面是巩固和提高以前所学的VC++的知识;另一方面是学习软件设计的一些基本思想,选择一个中等难度且相对成熟的软件来设计既易于实现又留有一定的上升空间,符合实际情况。
我所设计的这种黑白棋游戏有几个特点:程序短小精悍,简洁明了;游戏界面美观,容易操作。
运行环境:Visual C++ 6.0
使用语言:C++语言
1. 课程设计计划
黑白棋游戏设计所完成的就是一个游戏软件系统。我以前下过黑白棋,对其有一定的了解,但只是一个初级玩家。要完成游戏的设计,首先要对黑白棋进行全面的了解,然后进行需求分析,对软件有一个总体的设计,之后进行详细设计,并对各种细节及可能的问题进行分析,最后进行编码调试。
具体步骤如下:
第一步:查询黑白棋的下棋规则及其历史。
第二步:对于如何赢棋及下棋策略进行一定的了解。(前两步花 1 天)
第三步:对于黑白棋游戏软件的功能需求进行全面的分析,其包括:基本功能的需求及拓展空能的需求。(1-2天)
第四步:进行总体的框架设计(1-2天)
第五步:对于总体设计中的各个子模块,进行详细设计,并对可能出现的问题及难以解决的地方进行标记,同时可查阅相关的资料进行初步解决。
第六步:上机编码调试,对出现的问题有针对性的加以解决。不能自行解决的问题,查询相关资料或询问其他人(3—5天)
以上是总体的课程设计计划,具体的时间或问题的出现与解决以具体情而定。
对于可能出现的问题,如果有提前的预案就更好了。后期的调试阶段可能会出现很多问题,尽量压缩前一、两步的时间,为后期调试预留更多的时间。
对后期调试完成后不满意或还希望添加更多功能,可以进行相应的修改。
2. 功能需求
(1).开发背景:
我国是棋文化的发祥地之一,上古的尧时代,围棋就在我国诞生了!在随后的几千年长河里,我国人民不断以自己的聪明才智创造出深受人们喜爱的棋类游戏,像中国象棋,五子棋,军棋等等一直是在民间很流行的棋类项目。同时国外的棋类游戏也流传到中国,比如国际象棋,跳棋等,逐渐在国内盛行。可以说棋类游戏一直是中国人民喜闻乐见的一种休闲方式,由于棋类游戏都是比较注重智力策略的,所以从中也可以折射出我国人民的智慧。
棋类游戏规则简单,对外部要求不高,人们可以随时随地进行对弈。但是,真正能够精通棋类游戏的人却不是很多,主要是棋类游戏具有变化莫测的特点,人们经常得在棋局上深思熟虑才能找到克敌制胜的办法。因此,各种棋类游戏都具有开发智力的效能。在休闲中使自己得到真正的长进,这或许就是其倍受人们青睐的原因所在!
黑白棋游戏,起源于古希腊,在日本和西方国家比较盛行,其独特的游戏规则,对人类智慧进行着不断地挑战和激励,因此人们一直乐此不疲!加上上世纪后期电脑的普及,使黑白棋在全球范围内风靡,人们通过电脑可以轻松地实现人机对弈或者双人对弈!
但是,黑白棋在我国开展的比较晚,流行范围还不够广,很多人对它的规则还不是很了解。为了配合国内黑白棋的普及活动,为广大的人民群众增添一种既能休闲又能益智的娱乐形式,使人们得到身心的全面发展,我们开发了这款PC版黑白棋游戏。现阶段沉迷于网络游戏或者其他游戏的学生不少,作为国家即将的建设者和接班人决不能只是游戏中的高手,我们只有去磨练自己的思维,发挥创造性才能担当得起如此重任,所以我们更希望能将其在学生中加以推广,让我们学生找到真正适合自己的游戏!
(2) 基本功能:
1)、实现人人对战与人机对战及难度的控制
2)、记录双方棋子数
3)、实现悔棋功能的
4)、实现重新开始
(3).拓展功能及对项目期望能实现的东西
a 对能落子的点提示玩家
b 棋力可调,分初级,中级。
c 提示音功能,下错、下对有不同的提示音。
d 玩家遇到疑问,需要帮助时,给予一定的帮助
e 界面色彩友好,给人以良好的视觉冲击。
f 操作方便,容易上手。
3.总体设计
3.1系统模块:
3.2游戏业务处理流程图:
4.详细设计
4.1绘制棋盘与棋子并显示模块
此模块写在DrawImage(int image[10][10],CDC *pDC)函数中,在OnDraw(CDC*pDC)中调用此函数即可。同时在OnDraw(CDC*pDC)函数中采用双缓冲技术,避免屏幕闪烁。
(1) 绘制背景
此处加载位图实现。
CRect rc; //添加背景
CBitmap bitmap;
bitmap.LoadBitmap(IDB_BITMAP7);
CBrush brush;
brush.CreatePatternBrush(&bitmap);
CBrush *pOldBrush = pDC->SelectObject(&brush);
GetClientRect(&rc);
pDC->FillRect(rc,&brush);
pDC->SelectObject(pOldBrush);
(2) 显示双方棋子数
char buf[100];
sprintf(buf,"黑棋数目:%3d ",BlackNum); //显示黑棋数目
pDC->TextOut(30,8,buf);
char buf1[100];
sprintf(buf1,"白棋数目:%3d ",WhiteNum); //显示黑棋数目
pDC->TextOut(236,8,buf1);
(3) 绘制棋子及能落子的点
CRect rc1,rc2;
int nSize=30;
for(int i=0;i<n;i++) //绘制棋子
for(int j=0;j<n;j++)
{
rc1=CRect(30+nSize*i,30+nSize*j,30+nSize*(i+1),30+nSize*(j+1));
if(Image[i][j]==1) //Image[i][j]为1绘制黑棋
{
CBitmap bitmap1;
bitmap1.LoadBitmap(IDB_BITMAP1);//加载位图
CBrush brush1;
brush1.CreatePatternBrush(&bitmap1);
CBrush *pOldBrush1 = pDC->SelectObject(&brush1);
pDC->FillRect(rc1,&brush1);
pDC->SelectObject(pOldBrush1);
}
else if(Image[i][j]==-1)//Image[i][j]为-1绘制白棋
{
CBitmap bitmap2;
bitmap2.LoadBitmap(IDB_BITMAP2); //加载位图
CBrush brush2;
brush2.CreatePatternBrush(&bitmap2);
CBrush *pOldBrush2 = pDC->SelectObject(&brush2);
//pDC->Ellipse(rc1);
pDC->FillRect(rc1,&brush2);
pDC->SelectObject(pOldBrush2);
}
else if(Image[i][j]==11)//Image[i][j]为11绘制刚落子的黑棋
{
CBitmap bitmap3;
bitmap3.LoadBitmap(IDB_BITMAP3); //加载位图
CBrush brush3;
brush3.CreatePatternBrush(&bitmap3);
CBrush *pOldBrush3 = pDC->SelectObject(&brush3);
pDC->FillRect(rc1,&brush3);
pDC->SelectObject(pOldBrush3);
// Image[i][j]=1;
}
else if(Image[i][j]==-11)//Image[i][j]为-11绘制刚落子的白棋
{
CBitmap bitmap4;
bitmap4.LoadBitmap(IDB_BITMAP4); //加载位图
CBrush brush4;
brush4.CreatePatternBrush(&bitmap4);
CBrush *pOldBrush4 = pDC->SelectObject(&brush4);
pDC->FillRect(rc1,&brush4);
pDC->SelectObject(pOldBrush4);
}
else if(Image[i][j]==2) //Image[i][j]为2绘制能够落子的点
{
CBitmap bitmap6;
bitmap6.LoadBitmap(IDB_BITMAP6); //加载位图
CBrush brush6;
brush6.CreatePatternBrush(&bitmap6);
CBrush *pOldBrush5 = pDC->SelectObject(&brush6);
pDC->FillRect(rc1,&brush6);
pDC->SelectObject(pOldBrush5);
}
else if(Image[i][j]==0) ////Image[i][j]为0绘制刚空白
{
CBitmap bitmap5;
bitmap5.LoadBitmap(IDB_BITMAP5); //加载位图
CBrush brush5;
brush5.CreatePatternBrush(&bitmap5);
CBrush *pOldBrush6 = pDC->SelectObject(&brush5);
pDC->FillRect(rc1,&brush5);
pDC->SelectObject(pOldBrush6);
}
}
(4) 画棋盘线
此处采用MoveTo()与LineTo()函数,循环画线。
int m=X+1;
for(i=0;i<m;i++)//画棋盘线
{
pDC->MoveTo(30,30+i*30);
pDC->LineTo(m*30,30+i*30);
pDC->MoveTo(30+i*30,30);
pDC->LineTo(30+i*30,m*30);
}
4.2查找能落子的点模块
此模块写在Search_Enable(int image[10][10],BOOL IsFirst_white)函数中。采用二重循环对二维数组image[10][10]中的每个点扫描,若为空,则向八个方向查找,看此点是否能落子,若能够落子则记录此点的下标,并记录落子后吃掉对方棋子的位置下标,以便在玩家点击此点时,显示吃掉对方后的棋盘情况。
Search_Enable(int image[10][10],BOOL IsWhite)//人机大战中的搜索能落子的点
{
int m,n,flag,b,d,t=0;
int Border=X-1;
int mw1=0,mw2=0;
int mb1=0,mb2=0;
for(int i=0;i<X;i++)
for(int j=0;j<X;j++)
{
if(IsWhite)
{mw1=1;mw2=11;mb1=-1;mb2=-11;}
else
{mw1=-1;mw2=-11;mb1=1;mb2=11;}
if(image[i][j]==0||image[i][j]==2)
{
//////////向下搜索
m=i;n=j;flag=0;
do
{
if(m<Border)
m++;
flag++;
}while((image[m][n]==mw1||image[m][n]==mw2)&&m<Border);
if((image[m][n]==mb1||image[m][n]==mb2)&&flag>1)
{
record1[i][j][i][j]=5; //记录此点能够落子的点
b=i;d=j;
do
{ b=b+1;
//Image[b][d]=6;
Record1[i][j][b][d]=6;//记录能够吃掉对方子的位置Image[i][j]
}while(b<m-1);
}
/////////////向右搜索
m=i;n=j;flag=0;
do
{
if(n<Border)
n++;
flag++;
}while((image[m][n]==mw1||image[m][n]==mw2)&&n<Border);
if((image[m][n]==mb1||image[m][n]==mb2)&&flag>1)
{
record1[i][j][i][j]=5; //记录此点能够落子的点
b=i;d=j;
do
{ d=d+1;
//Image[b][d]=6;
Record1[i][j][b][d]=6;//记录能够吃掉对方子的位置Image[i][j]
}while(d<n-1);
}
//////////向上搜索
m=i;n=j;flag=0;
do
{
if(m>0)
m--;
flag++;
}while((image[m][n]==mw1||image[m][n]==mw2)&&m>0);
if((image[m][n]==mb1||image[m][n]==mb2)&&flag>1)
{
record1[i][j][i][j]=5; //记录此点能够落子的点
b=i;d=j;
do
{ b=b-1;
//Image[b][d]=6;
Record1[i][j][b][d]=6;//记录能够吃掉对方子的位置Image[i][j]
}while(b>m+1);
}
/////////////向左搜索
m=i;n=j;flag=0;
do
{
if(n>0)
n--;
flag++;
}while((image[m][n]==mw1||image[m][n]==mw2)&&n>0);
if((image[m][n]==mb1||image[m][n]==mb2)&&flag>1)
{
record1[i][j][i][j]=5; //记录此点能够落子的点
b=i;d=j;
do
{ d=d-1;
//Image[b][d]=6;
Record1[i][j][b][d]=6;//记录能够吃掉对方子的位置Image[i][j]
}while(d>n+1);
}
//////////////向左上搜索
m=i;n=j;flag=0;
do{
if(n>0&&m>0)
{n--;m--;}
flag++;
}while((image[m][n]==mw1||image[m][n]==mw2)&&m>0&&n>0);
if((image[m][n]==mb1||image[m][n]==mb2)&&flag>1)
{
record1[i][j][i][j]=5; //记录此点能够落子的点
b=i;d=j;
do
{ b=b-1;d=d-1;
//Image[b][d]=6;
Record1[i][j][b][d]=6;//记录能够吃掉对方子的位置Image[i][j]
}while(d>n+1&&b>m+1);
}
//////////////向右下搜索
m=i;n=j;flag=0;
do{
if(n<Border&&m<Border)
{m++;n++;}
flag++;
}while((image[m][n]==mw1||image[m][n]==mw2)&&m<Border&&n<Border);
if((image[m][n]==mb1||image[m][n]==mb2)&&flag>1)
{
record1[i][j][i][j]=5; //记录此点能够落子的点
b=i;d=j;
do
{ b=b+1;d=d+1;
//Image[b][d]=6;
Record1[i][j][b][d]=6;//记录能够吃掉对方子的位置Image[i][j]
}while(d<n-1&&b<m-1);
}
//////////////向左下搜索
m=i;n=j;flag=0;
do{
if(n>0&&m<Border)
{m++;n--;}
flag++;
}while((image[m][n]==mw1||image[m][n]==mw2)&&n>0&&m<Border);
if((image[m][n]==mb1||image[m][n]==mb2)&&flag>1)
{
record1[i][j][i][j]=5; //记录此点能够落子的点
b=i;d=j;
do
{ b=b+1;d=d-1;
//Image[b][d]=6;
Record[i][j][b][d]=6;//记录能够吃掉对方子的位置Image[i][j]
}while(b<m-1&&d>n+1);
}
//////////////向右上搜索
m=i;n=j;flag=0;
do{
if(n<Border&&m>0)
{m--;n++;}
flag++;
}while((image[m][n]==mw1||image[m][n]==mw2)&&m>0&&n<Border);
if((image[m][n]==mb1||image[m][n]==mb2)&&flag>1)
{
record1[i][j][i][j]=5; //记录此点能够落子的点
b=i;d=j;
do
{ b=b-1;d=d+1;
//Image[b][d]=6;
Record1[i][j][b][d]=6;//记录能够吃掉对方子的位置Image[i][j]
}while(b>m+1&&d<n-1);
}
}
}
}
4.3判断游戏是否结束模块
此模块写在IsGameOver()函数中,黑白棋判断游戏结束为:当棋盘摆满棋子时,哪一方的棋子多,则那一方获胜,棋子一样多为平局,或是中途以防被全部吃掉,则另一方胜。
void IsGameOver()//判断游戏是否结束
{
int sum=0,sum1=0,Fullflag=0;
for(int i=0;i<X;i++)
for(int j=0;j<X;j++)
{
if(Image[i][j]==1||Image[i][j]==11)
{sum++;Fullflag++;} //记录黑子个数及总的棋子数
else if(Image[i][j]==-1||Image[i][j]==-11)
{sum1++;Fullflag++;} //记录白子个数及总的棋子数
}
BlackNum=sum;
WhiteNum=sum1;
if(sum>sum1&&Fullflag==X*Y||sum1==0) //黑方胜利
{
if(MessageBox("黑方胜!还想来一局吗?","提示",MB_YESNO)==IDYES)
OnStart();
else
exit(0);
}
else if(sum<sum1&&Fullflag==X*Y||sum==0) //白方胜利
{
if(MessageBox("白方方胜!还想来一局吗?","提示",MB_YESNO)==IDYES)
OnStart();
else
exit(0);
}
else if(sum==sum1&&Fullflag==X*Y) //平局
{
if(MessageBox("平局!还想来一局吗?","提示",MB_YESNO)==IDYES)
OnStart();
else
exit(0);
}
}
4.4点击鼠标左键落子模块
此处为人人对战或人机大战中玩家用鼠标点击棋盘,OnLButtonDown()函数相应鼠标左键按下事件,完成落子。
void OnLButtonDown(UINT nFlags, CPoint point)
if(IsStart) //若开始后点击才能落子
{
if(point.x>30&&point.x<(X+1)*30&&point.y>30&&point.y<(X+1)*30) //鼠标点击是否在指定区域(即棋盘上的点)
{
px=(point.x-30)/30; //将点击的点转化为Image的下标
py=(point.y-30)/30;
if(Image[px][py]==2) //若此点能落子,则落子
{
for(int i=0;i<X;i++)
for(int j=0;j<X;j++)
{
if(Image[i][j]==-11)
Image[i][j]=-1;
if(Image[i][j]==11)
Image[i][j]=1;
CopyImage[i][j]=0;
}
if(IsFirst_White) //该白方落子,下白子
{
// CopyImage[i][j]=0;
Image[px][py]=-11;
CopyImage[px][py]=1;
IsFirst_White=FALSE;
for(int i=0;i<X;i++)
for(int j=0;j<X;j++)
if(Record[px][py][i][j]==6)
{CopyImage[i][j]=6;Image[i][j]=-1;}
}
Else //该黑方落子,下黑子
{
Image[px][py]=11;
IsFirst_White=TRUE;
for(int i=0;i<X;i++)
for(int j=0;j<X;j++)
if(Record[px][py][i][j]==6)
{CopyImage[i][j]=6;Image[i][j]=1;}
TurnCom=TRUE;
}
}
else
{
////////////若没有在能落子点点击,发出error音
}
for(int i=0;i<X;i++)
for(int j=0;j<X;j++)
for(int k=0;k<X;k++)
for(int g=0;g<X;g++)
{if(Record[i][j][k][g]==6)
Record[i][j][k][g]=0;
}
for(i=0;i<X;i++)
for(int j=0;j<X;j++)
if(Image[i][j]==2)
{ Image[i][j]=0;}
Invalidate(FALSE);
}
IsGameOver(); //落子后,判断游戏是否结束
Pass=TRUE;
Search_EnablePoint(); //显示另一方能落子点
for(int i=0;i<X;i++)
for(int j=0;j<X;j++)
for(int k=0;k<X;k++)
for(int g=0;g<X;g++)
if(record[i][j][k][g]==5)
{
Image[i][j]=2;
Pass=FALSE;
record[i][j][k][g]=0;
}
}
CView::OnLButtonDown(nFlags, point);
}
4.5悔棋模块
此模块写在PreStep()函数中,在下棋过程中记录上一步下棋点的下标及吃掉对方棋子的位置下标。在此函数中恢复到上一步即可。(用栈实现)
4.6加按键音模块
此模块使用sndPlaySound()函数加音乐。
引入库文件
#include <mmsystem.h>
#pragma comment(lib,"winmm.lib")
HRSRC res;
void *hSound1;
LPSTR lpSound1;
////////////加按键音 res=FindResource(::AfxGetApp()->m_hInstance,MAKEINTRESOURCE(IDR_PUTSTONE),"WAVE"); hSound1=LoadResource(::AfxGetApp()->m_hInstance,res);
lpSound1=(LPSTR)LockResource(hSound1);
sndPlaySound(lpSound1,SND_ASYNC|SND_MEMORY);
4.7极大极小搜索算法模块(人机大战中用)
此模块是人机大战中核心的算法之一,此算法好坏影响搜索深度。这是人工智能方面的知识。首先介绍一下此算法:
算法思路:在博弈中只有“敌我”两方,他们利益相对,其所赢的函数和为0:C1 + C2 = 0。其中C1代表我方赢(利益);C2为敌方赢(利益)。所以会出现三种情况:
(1)我方胜利 C1 〉0 ,则C2 = -C1 < 0;
(2)敌方胜利 C2 〉0 ,则C1 = -C2 < 0;
(3)平局 C1 = 0 ,则C2 = 0;
算法分析:在程序设计过程中,采取“极大一极小(Min—Max)分析法”,即在博弈树的搜索过程中,估值函数E(x)反映双方赢得的大小,从我方出发,可取估值函数[2]为:E(x)=C1(x),C1>0得分,C1<0失分。双方都根据“极大一极小”的原则,在按照博弈规则生成的博弈树中,选取最佳步[3],即:我方最佳棋步:取max[E(x)],在我方得分C1最大,扩展“或”节点;敌方最佳棋步:取min[E(x)],在我方得分Cl最小,扩展“与”节点。
极大-极小原则
例:初始棋局假设有如图1布局所示
图9 算法例子对应的图
轮到电脑下棋。现在它发现有这样三个地方可以下:e3,c3,c5。这三种下法分别会形成三种局面:A、B、C。如果是人在下棋,就会思考:那一种下法更好呢?比如A被别人占角,B没什么变化,C占了别人的角。当然棋手会选择下C。电脑也是如此,它会对每一种棋局评一个分,比如它判断,如果被别人占角,就减80分,相反占别人的角就加80分。那么A=-80分,B=0分,C=80分。电脑会选择下C。电脑程序对棋局评分的部分,称为“估值函数”(Evaluation Function)。真正的估值函数当然不会这么简单。它会用到如行动力、潜在行动力、余裕手、边角判断、稳定子等综合因素来判断。具体的估值函数网络上学习所以会有
初始棋局(-1)
------------------+------------------
| | |
e3 c3 c5
(A) (B) (C)
图 10 初始棋局
接下来,如果人就这么判断。那么它顶多也就是个初学者。为什么呢?因为它不会推理,碰到对手弃角之类的战术,如“边角判断”中示例的一些情况,就输得一塌糊涂了。当然,可以告诉电脑,碰到“边角判断”中的几种情况,就如何如何下。但是,真实的棋局是非常复杂的,电脑(也包括人脑)几乎不可能对动态的棋局给出静态的评估。因为实际对局总会出现这样那样的情况,是无法预先估计的。碰到这些情况,人就会向后推几步,看一看会是怎样的一个局面。一些棋类大师往往可以推十几步甚至更深。电脑也是如此。
现在设电脑我白棋,推理深度为2 ,那么形成的树就为:
初始棋局 - - 白棋下棋之后 - - 黑棋下棋之后 估值 | 初始棋局(-1) ------------------+------------------ | | | e3(-1) c3(-1) c5(-5) -----+----- ----+---- -----+----- | | | | | | | | | | | | | | f2 f3 f4 f5 f6 c2 d3 e6 f5 b6 c6 d6 e6 f6 +84+36+12 +5 -1+11 -1 +6 +6 +6 +0 -5 +3 +5 |
结果:应该下e3或c3 |
图11 树
具体实现的伪算法类似于经典的八皇后问题。
最大最小搜索伪代码:
int MinMax(局面 p, int depth)//depth是搜索深度
{int bestvalue, value;
//一般来说,这里有一个判断棋局是否结束的函数,一旦棋局结束就不必继续搜索了,直接返回极值。但由于黑白棋不存在中途结束的情况,故省略。
If(depth<=0)//叶子节点
{返回估值(p);//直接返回对局面的估值}
if(当前是计算机走棋)
{bestvalue=-INF;//初始最佳值设为负无穷}
else
{bestvalue=INF;// 初始最佳值设为正无穷
}
for(每一个合法的走法)//走法的生成与具体问题紧密相关,具体方法省略
{走一步棋;//局面p随之改变
value=MinMax(p, depth-1);//搜索子节点
撤销刚才的一步;//恢复局面p
if(当前是计算机走棋)
{if(value>bestvalue)//取最大值
{bestvalue=value;
if(是初始局面)
{保存最佳走法;}}}
else
{if(value<bestvalue)//取最小值
{bestvalue=value;}
}}
return bestvalue;
}
代码如下
int CMyView::MinMax(int image[10][10],int depth,BOOL IsWhite,int m,int n)//极大极小搜索
{
int value,bestvalue;
if(depth<=0) //若为叶节点 返回估值
{return Evaluate(m,n,image,!IsWhite);}
if(IsWhite)
bestvalue=-10000;
else
bestvalue=10000;
Search_Enable(image,IsWhite);//搜索能落子的点
for(int a=0;a<X;a++)
for(int b=0;b<X;b++)
for(int k=0;k<X;k++)
for(int g=0;g<X;g++)
if(record1[a][b][k][g]==5)//对每个能落子的点落子(此落子在虚拟的棋盘image[10][10]上)
{ record1[a][b][k][g]=0;
if(IsWhite) //电脑下
{ image[a][b]=-1; //此点落子
for(int i=0;i<X;i++)
for(int j=0;j<X;j++)
for(int k=0;k<X;k++)
for(int g=0;g<X;g++)
if(Record1[i][j][k][g]==6)
{image[i][j]=-1;Record1[i][j][k][g]=0;}
value=MinMax(image,depth-1,!IsWhite,m,n);//递归调用MinMax()函数搜索
if(value>bestvalue) //极大点取极大值
bestvalue=value;
}
else//对方下
{ image[a][b]=1;
for(int i=0;i<X;i++)
for(int j=0;j<X;j++)
for(int k=0;k<X;k++)
for(int g=0;g<X;g++)
if(Record1[i][j][k][g]==6)
{image[i][j]=1;Record1[i][j][k][g]=0;}
value=MinMax(image,depth-1,!IsWhite,m,n);//递归调用MinMax()函数搜索
if(value<bestvalue) //极小点取极小值
bestvalue=value;
}
}
return bestvalue;//返回最佳值
}
4.8估值函数设计模块(人机大战中用)
此模块写在Evaluate()函数中,此函数的好坏直接决定了电脑的棋力高低。由于这一部分的算法没写好,所以人机大战中电脑的棋力不高。
目前的黑白棋程序使用的估值方法大致有三种,基于棋子位置价值的估值、基于行动力的估值和基于模板的估值。基于模板的估值由于我不了解,说不出个所以然来。这里就讲一下另两种估值方法。
1、基于棋子位置价值的估值
很多人刚学会黑白棋的时候往往贪多,总想吃掉对方更多的棋子,结果往往输得很惨。但他们逐渐就会发现要领,四个角落上的棋子永远不会被翻,因此角落上的棋子有很高的价值,而边上的棋子也不容易被翻,价值也较高,特别的,如果某个角落上有自己的棋子,那么紧靠这个角落的三个棋子也有较高的价值。这就是基于棋子位置价值的估值的基本原理,棋盘上的每个位置有不同的价值,角落的价值最大,要防止对手占据角落,自己就要避免走在角落周围,因此在角落上没有棋子时角落周围的价值应该很低,但如果角落上已经有自己的棋子,情况又不同了。所以,一般来说要为自己和对手各准备一张棋子位置价值表,并且还要设计成动态的,即每次估值前先根据当前棋局来调整这两张价值表。
这种估值方法实现起来很简单,速度也快,因此在很多初学者编写的程序中使用。使用这种估值方法的程序棋力不强,但由于多由初学者编写,它还是有可能击败它的作者的。
2、基于行动力的估值
更好的估值方法是根据行动力和潜在行动力来估值。首先介绍这两个概念。行动力是指一个局面上可以下棋的地方的多少,比如某个局面有10个地方可以走棋,那么行动力就是10。潜在行动力一般指对手棋子周围的空格数,因为只有对手棋子旁边的空格处才是可能可以走棋的地方,因此在一个局面上,对手旁边的空格越多对自己越有利,这就是潜在行动力估值。
在实际的程序中,行动力估值一般要和棋子位置价值联合使用。有时虽然行动力不高,但每一个地方都是好棋,那也是值得的。
总之,原则就是给自己留下好棋,而要让对手没有好棋可走。这种估值方法比较强,编写的好的话,初学者一般都下不赢自己的程序。
int CMyView::Evaluate(int m,int n,int image[10][10],int IsWhite)//估值函数对叶节点估值
{
int bestvalue;
int num=0,num1=0;
BOOL IS=!IsWhite;
Search_Enable(image,IsWhite);
for(int i=0;i<X;i++)
for(int j=0;j<X;j++)
for(int k=0;k<X;k++)
for(int g=0;g<X;g++)
if(record1[i][j][k][g]==5)
{num++;
record1[i][j][k][g]=0;}
PointValue(image);
if(IsWhite)
bestvalue=num+Value[m][n]*4;
else
bestvalue=-num+Value1[m][n]*4;
return bestvalue;
}
5. 测试与实现
6. 总结
软件的优点:界面友好、有良好的视觉感,能实现人机对战、操作简单、有提示音及用鼠标形状提示该哪一方下棋,有游戏说明。
软件的缺点:人机对战中,电脑的智力不高,悔棋只能悔一步,没有游戏保存等功能。
1. 开发过程:
刚开始,觉得人机大战对自己很有挑战。但自己可能不一定能做出来,最后经过自己的努力最终实现了人机大战,还是令人挺开心的,虽然电脑的棋力不高。因为有前面贪吃蛇与俄罗斯方块游戏的开发经历,对于这种能用二维数组表示游戏还有一点点经验。所以人人对战不成问题。
我按照前面写的课程设计计划,先对黑白棋游戏的历史与游戏规则进行了了解,然后对如何取得最终的胜利及其战术的应用进行了深入的了解。这是编写游戏的基础,这项工作用时一天,但后期在做人机大战时,还对一些算法中可能应用的战术进行了再了解。之后对于要实现的功能进行了全面的总结,此处,我重点先对基本功能进行全面的总结与设计,对于额外的拓展功能,先进行初步的策划,并不进行详细的设计,如果基本功能完成后,再对其进行设计与实现。对于游戏功能的评估用时半天左右。
然后,就是总体的设计,有了功能表,总体设计就方便多了。这用时几小时就OK了。最重要的是每一个功能的详细设计,这个必须认真考虑,因为它关系到软件能否成功。对于每一个细节都应该认真设计,采用何种结构,如何协调相互访问及代码重用性都应该仔细考虑。这项工作历时4天左右。只要详细设计能够做好,后期编码实现会相对顺利。
实际上人人大战相对简单,我详细设计完之后,编完代码,基本上都实现了。最重要的是人机大战这块我在详细设计时,刚开始只按自己的想法做,虽然实现了人机大战,但棋力很弱。最后我上网查阅相关的资料,才发现要实现棋力的增强原来还要用到人工智能方面的知识,于是我就查阅相关资料,刚开始看不懂,后来多开了几次,慢慢懂了。但无法编码实现,我又在查资料,重新编码。最后用人工智能方面的知识实现了人机大战,但棋力还是不高,主要是估值函数这块我想不出好的算法。
总之,最终的结果还令人相对满意,对自己的编程思想及能力有了很大的提高。
2. 心得体会:
通过这次的课程设计,对自己的编程能力有了很大的提高,我体会最深的是,自己现在能够不看书,自己编写。
这次小的游戏软件开发经历,对自己来说是个很好的锻炼的机会。从刚开始的相对顺利到后期发现人机大战原来还包含很多东西,自己还有很多东西要学。在这样的过程中,我逐步的学习与提高。我想:提高自己的编程思想是最重要的,看到很多同学,他们学到了某个技术,如:怎样改变窗体形状等就开心的要死,或许这些代码只是拷贝别人的,自己根本就没看懂。思想是一个软件或程序的灵魂,只有提高了自己的编程思想,才能提高自己的编程能力。
通过一次又一次的锻炼,在学习中总结,在总结中提高。最终能够使自己变得强大。同时,我也深深的感受到自己的知识还很欠缺,还有很多有待提高的地方,还有很多需要学习的地方。
参考文献:
[1] 朱福喜.人工智能基础教程[第二版].北京:清华大学出版社,2011.6.
网上的文献:
《奥赛罗棋智能模块的算法设计》
《博弈算法在黑白其中的应用》
《黑白棋战术攻略》