基于MFC实现的人机对战五子棋游戏
1、引言
此报告将详细介绍本次课程设计的动机、设计思路及编写技术的详细过程,展现我所学过的C++知识以及我通过本次课程设计所学到例如MFC等知识。在文档最后我也会记录我所编写过程遇到的问题以及解决方案。
1.1 背景
五子棋是起源于中国古代的传统黑白棋种之一,此游戏不仅能增强思维能力,提高智力,而且变化多端,非常富有趣味性和消遣性,伸手人们喜爱。而且人工智能发展迅速,人们不断制造出可以用机器代替人们做一些事的程序,包括五子棋等棋类小游戏。随着经济的快速发展,人们的生活节奏也越来越快,随之而来的便是人们越来越少的空闲时间,而此类小游戏不占空间,占用时间也少,所以成了很多人喜爱的娱乐方式。
传统五子棋的棋具与围棋大致相同,棋子分为黑白两色,棋盘为15×15,棋子放置于棋盘线交叉点上。两人对局,各执一色,轮流下一子,先将横、竖或斜线的5个或5个以上同色棋子连成不间断的一排者为胜(正规比赛中黑棋只能连成5个。6-9个一排算禁手,另外黑棋还有33和44禁手。黑棋禁手判负。白棋没有限制)。 因为传统五子棋在落子后不能移动或拿掉,所以也可以用纸和笔来进行游戏。随着五子棋的发展,逐步发现先行优势非常大,最后得出"先行必胜"即现代五子棋。本游戏为传统无禁手五子棋,适用于初学者。
1.2 动机
五子棋游戏如果开发成功,有以下几个好处:
- 可以增强人们的抽象思维能力、逻辑推理能力、空间想象力、提高人们的记忆力、心算能力等,而且深含哲理,有助于修身养性
- 可以作为人们休闲时的娱乐,容易上手,老少皆宜,而且趣味横生,引人入胜
所以,本系统旨在开发一个传统五子棋小游戏程序。
1.3 要解决的问题
本系统要提供以下几个功能:
- 玩家信息录入功能:在主界面初始化之前,调用对话框的DoModal函数,产生一个对话框即登陆界面,在登录界面录入玩家姓名、年龄和性别,点击登录在对话框结束后,根据结果判断,再进行主界面的初始化,显示玩家信息
- 棋盘绘制:关于棋盘的呈现采用GDI DrawImage 的方法先准备一张15*15的棋盘图片,在OnPaint()函数中从备份DC拷贝到屏的DC里实现图片的显示。再是绘制棋盘中的棋子,本程序根据棋盘坐标定义二维数组,用户在棋盘中放置棋子时,根据鼠标的坐标点获取对应棋子
- 人机对弈:进入主界面后即可进行人机对弈,程序默认为玩家先,可以通过游戏设置,选择人先机后或者机先人后。同时我为计算机设计了一套策略型智能算法,使得在用户每走一步棋后,电脑都要进行一次全盘扫描,然后根据算法选择出得分最高的位置即对对方最有威胁而对自己最有利的位置,选择最佳下棋点下棋,实现人机轮流走棋
- 输赢判断功能:在玩家或计算机每走一步棋后,通过算法判断是否有一方连续的棋子数在"横"、"竖"、"左斜"、"右斜"等于或超过五个棋子,若是则电脑将调用一个函数Messagebox(),弹出对话框提示输赢
- 悔棋功能:在玩家下棋后,如果想返回之前的棋局,则可点"悔棋"使得棋局回到下每步棋之前的棋局
- 错误示功能:如果玩家在下棋时,将棋子落在其它棋子的位置或者在棋盘外的位置时,将会弹出错误提示框,提示请到棋盘内落子
2、系统流程图
五子棋的游戏规则很简单,在玩家登录后开始游戏,在每次玩家或电脑下棋后,系统都要判断是否分出胜负,若否,另一方继续下棋;若是,提示输赢后,可以选择再来一局。下图是此系统的流程图,描述了系统的运行过程,以便大家了解游戏的原理。
3、数据结构设计
棋盘使用二维数组表示,15*15的棋盘就用m_board[15][15]的二维数组表示,第i行,第j列的元素应该是m_board[i][j]。
以下用自定义数据结构类型pos表示棋子位置:
c
struct pos
{
int x,y; //x,y分别行列坐标
int flag; //记录xy处下的是白棋还是黑棋;1--白棋
};
玩家信息表如下:
表中内容为数据库表格的三列名称:玩家姓名、年龄和性别,以及各自的数据类型。
4、关键技术
本系统是C++中的MFC基于对话框创建的MFC程序,主要致力于解决如下几个关键问题:
4.1 玩家信息登录
在主界面初始化之前,调用Dialog的DoModal函数,产生另外一个Dialog即登陆界面,玩家在登录界面输入玩家信息(玩家名,年龄,性别),完成后进入主界面,显示玩家信息。
玩家输入的信息
c
playername=dlg2.m_name; //玩家名
playerage=dlg2.m_age; //年龄
playersex=dlg2.m_sex; //性别
棋盘旁边显示的信息
c
dlg.m_cname=playername;
dlg.m_csex=playersex;
dlg.m_cage=playerage; //获取对象dlg的信息
dlg.DoModal(); //调用DoModal()函数
4.2 棋盘绘制
关于棋盘的呈现采用GDI DrawImage 的方法先准备一张15*15的棋盘图片,在OnPaint()函数中从源位图拷贝成为目的位图。
c
void CFivezq::DrawBoard()//画棋盘
{
m_pdc->BitBlt(0,0,446,446,m_pboard,0,0,SRCCOPY);
}
4.3 棋子绘制
关于棋子的实现,在于读取鼠标点击的坐标来判断点击位置所在的格子,然后求出该格子的中心位置坐标,以该中心为圆心用GDI画黑色或白色圆,然后用画刷CBrush *brush分别填充黑白色即可。实现代码如下:
c
//游戏中画棋子
void CFivezq::DrawChess(int px, int py, int type)
//判断下棋次序,如果轮到白棋下
if(m_turn==1)
//添加红色中心
CPen pen_r(PS_SOLID,2,RGB(255,0,0));
else
//如果轮到黑棋下
CBrush *brush;
//定义黑色画刷填充黑色
CBrush brush1(RGB(0,0,0));
//窗口无效重画白棋
m_pdc->Ellipse(px*29+7,py*29+7,px*29+34,py*29+34);
CBrush *brush;
//窗口无效重画黑棋子
CBrush brush1(RGB(0,0,0));
4.4 计算机落子
4.4.1 评分算法
为方便后面介绍详细算法先介绍下五子棋相关术语
- 连:2枚以上的同色棋子在一条线上邻接成串
- 五连:五枚同色棋子在一条线上邻接连串
- 成五:五连和长连的统称
- 四:五连去掉1子的棋型
- 活四:有两个威胁的四
- 冲四:只有一个威胁的四
- 死四:不能成五的四连
- 三:可以形成四再形成五的三枚同色棋子组成的棋型
- 活三:再走一着可以形成活四的三
- 连活三:两端都是威胁的连三。简称"连三"
- 眠三:再走一着可以形成冲四的三
- 死三:不能成五的三
- 二:可以形成三、四直至五的两枚同色棋子组成的棋型
- 活二:再走一着可以形成活三的二
- 连活二:连的活二。简称"连二"
- 眠二:再走一着可以形成眠三的二
- 死二:不能成五的二
- 先手:对方必须应答的着法,相对于活三先手而言,冲四称为"绝对先手"
- 三三:一子落下同时形成两个活三。也称"双三"
- 四四:一子落下同时形成两个冲四。也称"双四"
- 四三:一子落下同时形成一个冲四和一个活三
电脑落子之前要对棋局进行判断,那就需要告诉电脑什么时候的局势最好,我们可以通过给不同局势时不同分值的办法来让电脑明白。评分的基本规则及实现代码如下:
c
//评分函数
void CFivezq::Evaluater(int (&flag)[2][2], int &n, int &score,int turn);
判断是否能成5,如果是电脑方给予100000分,如果是控制者方给予-100000分;
c
//五连以上,得最高分
else if(n>=5)
if(turn==1)
score=100000;
else
score=50000;
判断是否能成活4或者是双死4或者是死4活3,如果是电脑方给予10000分,如果是控制方给予-10000分;若成双活3,如果是电脑方给予5000分,如果是控制者方给予-50 00分;若成死3活3,如果是电脑方给予1000分,如果是控制者方给予-1000分;
c
//活四+****+
if(flag[1][1]&&flag[0][1])
if(turn==1)
score=10000;
else
score=4000;
判断是否能成死4,如果是电脑方给予500分,如果是控制者方给予-500;
c
//死四
if(flag[1][0]&&flag[0][0])score=500;
if((flag[1][0]&&flag[0][1])||(flag[1][1]&&flag[0][0]))
//冲四o****+ 轮到己方下子冲四>活三
if(turn==1)
score=4000;
else
score=2500;
判断是否能成单活3,如果是电脑方给予200分,如果是控制者方给予-200分;
c
//o**---|---**o 眠三
if((flag[1][0]&&flag[0][1])||(flag[1][1]&&flag[0][0]))
score=200;
判断是否已成双活2,如果是电脑方给予100分,如果是控制者方给予-100分;若成死3,如果是电脑方给予50分,如果是控制者方给予-50分;
c
//死三o***o
if(flag[1][0]&&flag[0][0])
score=120;
if(flag[1][1]&&flag[0][1])
if(turn==1)
//活三+***+
score=3000;
else
score=2000;
判断是否成眠2,如果是电脑房给予20分,如果是控制者方给予-20分;
c
//o**---|---**o 眠二
if((flag[1][0]&&flag[0][1])||(flag[1][1]&&flag[0][0]))
score=20;
判断是否能成双活2,如果是电脑方给予10分,如果是控制者方给予-10分;判断是否能成活2,如果是电脑方给予5分,如果是控制者方给予-5分;
c
//活二
if(flag[1][1]&&flag[0][1])
if(turn==1)
score=50;
else
score=40;
判断是否能成死2,如果是电脑方给予3分,如果是控制者方给予-3分。
c
//死二
if(flag[1][0]&&flag[0][0])score=3;
4.4.2 判断最佳落子点核心算法
要使电脑落子必须令计算机知道自己及对方的棋型才行。先来分析己方的棋型,我们从棋盘左上角出发,向右逐行搜索,当碰到一个空白点时,以它为中心向左挨个查找,假如碰到己方的子则记录然后继续,假如碰到对方的子、空白点或边界就停止查找。左边完成后再向右进行同样的操作;最后把左右两边的记录合并起来,得到的数据就是该点横向上的棋型,然后把棋型的编号填入到Computer[x][y][n]中就行了(x、y代表坐标,n=0、1、2、3分别代表横、竖、左斜、右斜四个方向)。而其他三个方向的棋型也可用同样的方法得到,当 搜索完整张棋盘后,己方棋型表也就填写完毕了。然后再用同样的方法填写对方棋型表。
实现代码如下:
c
//核心算法
void CFivezq::ChessExpert(CPoint &best)
//应全部初始化为零
int computer[15][15][4]={0},player[15][15][4]={0};
//标志数组:标志该空位两端是否到达边界(遇到对方棋子)/遇到空位
int flag[2][2]={0};
int count=0,i,j,k,m;
//(x,y)存储向右搜索时遇到边界/空位/对方棋子时的前一个坐标,向左搜索的起点
int x=0,y=0;
横向向左搜索
c
//横向--->
for(k=1;k<5;k++)
{
//横向右遇到边界
if(i+k>14)
{
flag[0][0]=1;
break;
}
//横向右遇到对方棋子+***o
if(m_board[i+k][j]==-m_turn)
{
flag[0][0]=1;
break;
}
//横向右遇到空位++*++
if(m_board[i+k][j]==0)
{
flag[0][1]=1;
break;
}
}
横向向右搜索
c
//横向<---
for(m=0;m>-5;m--)
{
//横向左遇到边界
if(x+m<0)
{
flag[1][0]=1;
break;
}
if(m_board[x+m][y]==m_turn)
count++;
//横向左遇到对方棋子o***+
if(m_board[x+m][y]==-m_turn)
{
flag[1][0]=1;
break;
}
//横向左遇到空位++*++
if(m_board[x+m][y]==0)
{
flag[1][1]=1;
break;
}
}
纵向向下搜索
c
//纵向下遇到边界
if(j+k>14)
{
flag[0][0]=1;
break;
}
//纵向下 遇到对方棋子+***o
if(m_board[i][j+k]==-m_turn)
{
flag[0][0]=1;
break;
}
//纵向下遇到空位++*++
if(m_board[i][j+k]==0)
{
flag[0][1]=1;
break;
}
纵向向上搜索
c
//纵向上遇到界
if(y+m<0)
{
flag[1][0]=1;
break;
}
if(m_board[x][y+m]==m_turn)
count++;
//纵向上遇到对方棋子o***+
if(m_board[x][y+m]==-m_turn)
{
flag[1][0]=1;
break;
}
//纵向上遇到空位++*++
if(m_board[x][y+m]==0)
{
flag[1][1]=1;
break;
}
45度向下搜索
c
//45°向下遇到边界
if(i+k>14||j+k>14)
{
flag[0][0]=1;
break;
}
//45°向下遇到对方棋子+***o
if(m_board[i+k][j+k]==-m_turn)
{
flag[0][0]=1;
break;
}
//45°向下遇到空位++*++
if(m_board[i+k][j+k]==0)
{
flag[0][1]=1;
break;
}
45度向上搜索
c
//45°向上遇到边界
if(x+m<0||y+m<0)
{
flag[1][0]=1;
break;
}
//45°向上遇到对方棋子o***+
if(m_board[x+m][y+m]==-m_turn)
{
flag[1][0]=1;
break;
}
//45°向上遇到空位++*++
if(m_board[x+m][y+m]==0)
{
flag[1][1]=1;
break;
}
135度向下搜索
c
//135°向下遇 到边界
if(i-k<0||j+k>14)
{
flag[0][0]=1;
break;
}
//135°向下遇到对方棋子+***o
if(m_board[i-k][j+k]==-m_turn)
{
flag[0][0]=1;
break;
}
//135°向下遇到空位++*++
if(m_board[i-k][j+k]==0)
{
flag[0][1]=1;
break;
}
135度向上搜索
c
//135°向上遇到边界
if(x-m>14||y+m<0)
{
flag[1][0]=1;
break;
}
//135°向上遇到对方棋子o***+
if(m_board[x-m][y+m]==-m_turn)
{
flag[1][0]=1;
break;
}
//135°向上遇到空位++*++
if(m_board[x-m][y+m]==0)
{
flag[1][1]=1;
break;
}
4.4.3 计算机落子函数
有了上面填写的两张棋型表,现在要作的就是让电脑知道在哪一点下子了。其中最简单的计算方法就是遍历棋型表computer[15][15][4]和player[15][15][4],在通过评分算法找出其中数值最大的一点,在该点下子即可。实现代码如下:
c
//电脑得分,玩家得分;初始化为0;
int cpscore[15][15],plscore[15][15];
//求每一可能下子的交叉点得分
for(i=0;i<15;i++)
从电脑角度求出最佳下棋点:
c
//从电脑角度 最好的点(cpx,cpy)
int cpmax=0,cpx=0,cpy=0;
cpmax=cpscore[0][0];
if(cpmax<cpscore[i][j])
{
cpmax=cpscore[i][j];
cpx=i;
cpy=j;
}
从玩家角度选出最佳下棋点:
c
//从玩家角度 最好的点(cpx,cpy)
int plmax=0,plx=0,ply=0;
plmax=plscore[0][0];
if(plmax<plscore[i][j])
{
plmax=plscore[i][j];
plx=i;
ply=j;
}
4.5 判断输赢
当棋盘中出现连续五个或以上同色棋子相连时,即可判定输赢。实现代码如下:
c
// 判赢函数
int CFivezq::WhoWin(int nx,int ny)
判断水平行还是否连成五个子,只要够了5个就返回1,否则返回0.
c
for(i=0;i>-5;i--)
{
if(nx+i<0)
continue;
if(m_board[nx+i][ny]==m_turn)
{
count++;
if(count>=5)
return m_turn;
}
else
break;
}
//向右判断
for(i=1;i<5;i++)
{
if(nx+i>14)
break;
if(m_board[nx+i][ny]==m_turn)
{
count++;
if(count>=5)
return m_turn;
}
else
break;
}
判断竖行是否连成五个子:
c
//向下判断
for(i=0;i>-5;i--)
{
if(ny+i<0)
continue;
if(m_board[nx][ny+i]==m_turn)
{
count++;
if(count>=5)
return m_turn;
}
else
break;
}
//向上判断
for(i=1;i<5;i++)
{
if(ny+i>14)
break;
if(m_board[nx][ny+i]==m_turn)
{
count++;
if(count>=5)
return m_turn;
}
else
break;
}
判断从左上到右下是否连成五个子:
c
//向135度判断
for(i=0;i>-5;i--)
{
if(nx+i<0||ny+i<0)
continue;
if(m_board[nx+i][ny+i]==m_turn)
{
count++;
if(count>=5)
return m_turn;
}
else
break;
}
//向45度判断
for(i=1;i<5;i++)
{
if(nx+i>14||ny+i>14)
break;
if(m_board[nx+i][ny+i]==m_turn)
{
count++;
if(count>=5)
return m_turn;
}
else
break;
}
判断从右上到右下是否连成五个子:
c
//向135度判断
for(i=0;i>-5;i--)
{
if(ny+i<0)
continue;
if(nx-i>14)
break;
if(m_board[nx-i][ny+i]==m_turn)
{
count++;
if(count>=5)
return m_turn;
}
else
break;
}
//向45度判断
for(i=1;i<5;i++)
{
if(nx-i<0)
continue;
if(ny+i>14)
break;
if(m_board[nx-i][ny+i]==m_turn)
{
count++;
if(count>=5)
return m_turn;
}
else
break;
}
4.6 悔棋
在之前的数据中已经保存了各黑白棋子的坐标struct pos ,pos posinfo[225];只要在重画一次即可。实现代码如下:
c
//悔棋函数
void CFivezq::BackGo(HWND hwnd,int gamemode)
若游戏模式为机先人后时的代码:
c
m_board[posinfo[posflag-1].x][posinfo[posflag-1].y]=0;
m_board[posinfo[posflag-2].x][posinfo[posflag-2].y]=0;
posflag=posflag-2;
while(posflag==1)
posflag--;
游戏模式为人先机后时的代码:
c
{
m_board[posinfo[posflag-1].x][posinfo[posflag-1].y]=0;
m_board[posinfo[posflag-2].x][posinfo[posflag-2].y]=0;
posflag=posflag-2;
}
消息提示:
c
else
MessageBox(hwnd,"您已经没有棋可悔!",NULL,MB_OK);
5、系统运行结果
5.1 系统运行环境
- 硬件环境
- 处理器:Intel® Core™ i5 CPU M 460@2.53GHz 2.53GHz
- 内存:2.00GB
- 软件环境
- 操作系统:Windows7
- 编码工具:Microsoft Visual C++ 6.0或 Microsoft Visual Studio 2010
5.2 系统服务模式
本系统是基于C++的MFC中的对话框Dialog进行开发的,所以系统的服务模式是windows窗体程序。
5.3 系统运行结果
5.3.1 系统主界面
本系统是五子棋游戏系统,在系统主界面的棋盘中即可落子下棋,在右侧可以看到玩家信息以及游戏的功能按钮。系统主界面如下图5.1所示:
5.3.2 登录界面
程序运行开始弹出登陆对话框,输入玩家姓名、年龄和性别,然后登录即可进入主界面。玩家登录界面如下图5.2所示:
5.3.3 玩家信息显示界面
玩家在登录界面输入信息登录后,其信息便显示在主界面。玩家信息显示界面如下图5.3所示:
5.3.4 游戏设置界面
玩家在游戏时可以设置游戏的模式:人先机后或者机先人后,也可以选择玩家的棋子颜色:黑棋或者白棋。游戏设置按钮如下图5.4所示:
游戏设置界面如下图5.5所示:
5.3.5 判断输赢界面
玩家下棋后,系统会根据算法判断棋局输赢并弹出对话框提示。胜利界面如下图5.6所示:
失败界面如下图5.7所示:
5.3.6 错误提示界面
当玩家下棋时,如果点击使棋子落在已有棋子的位置上,则会弹出错误提示"此处已经有棋子";若点击使棋子落在棋盘外时,则会弹出错误提示"请到棋盘内下子";当玩家悔棋至没有棋子可以悔时,则会弹出错误提示"您已经没有棋可以悔"。
"此处已经有棋子"错误提示界面如下图5.8所示:
"请到棋盘内下子"错误提示界面如下图5.9所示:
"您已经没有棋可以悔"错误提示界面如下图5.10所示:
5.3.7 帮助界面
如果玩家对五子棋不是很了解,可以通过点击"帮助"按钮进入帮助界面查看五子棋简介及游戏规则。帮助界面如下图5.11所示:
6、调试和改进
在游戏设计之初有许多BUG,其中我觉得影响最大的就是编写评分算法时分值的设置。开始我设置的分值差距较小,计算机在判断棋型得分时因为分数的累加得到相同的分数,而电脑在判断双方的分值的时候,就是在落子时对自己的落子点分值和对方的分值进行对比来进行之后落子的判断,分数相同就会使电脑判断错误,导致程序不够"聪明",即使编写再好的算法也是杯水车薪。所以,后来便将评分算法的分值差距拉大,是电脑能够正确判断,程序能够正确运行。
7、心得和结论
7.1 结论
这是我第一次做课程设计,所以感触颇深。在设计时我遇到了许多问题,但是在解决问题的过程中也收获了很多知识和经验。在诸多困难中,我体会最深的一点就是设计系统架构的重要性。这个五子棋游戏程序不是很大,但是代码的组织是很重要的,因为关系到日后的维护和扩展。在设计之初,我主要针对五子棋的核心算法进行了研究,并得出一段合适的代码,但后来我便认识到这不仅仅是一个算法的问题,因为在完成核心算法之后,还有其他的功能需要编码实现。由于我的系统架构设计在编程之前没有做好,导致很多功能在编码实现的时候非常困难,所以在程序设计之初,必须设计一个好的系统架构才能将如此多个功能、如此多条代码合理的组织在一起,完成一个高性能的完整程序。
本次课程设计也令我获益匪浅。首先,我通过自己的努力完成了一个小型游戏程序设计与实现,也是对自己之前所学专业知识的复习以及能力的肯定,并在此基础上强化自己的实践意识,提高了我的实际动手能力和创新能力。其次,在解决设计过程中遇到的问题时,同学们一起交流经验,不仅获取了知识、开拓了自己的思路,而且还提升了同学之间的凝聚力和解决问题的积极性。再次,因为自己是第一次做课程设计,所以在经验及能力上还是有许多不足之处,需要查阅许多资料才能完成,在找资料的同时我找到了许多好的网站、论坛,比如CSDN IT技术社区,在这个社区中我可以同许多有经验和能力的人交流专业知识,也可以下载许多有用的资料,是自己将来工作或学习中解决问题的良好平台。
虽然这次做的程序有一定的缺陷,但是我相信在进一步的学习以及有了如此宝贵的经验之后,我一定会作出更好的作品。
7.2 进一步改进方向
随着互联网的迅速发展,人们对游戏的要求也越来越高,人机对弈的实现是不够的。本系统的改进方向是能够增加人人对弈功能并连接网络,使得玩家可以通过网络进行对弈。希望有一天可以实现。
主要参考文献
[1] 五子棋百度百科http://baike.baidu.com/view/2697.htm
[2] 刘锐宁、宋坤,visual C++开发典型模块大全,人民邮电出版社,2009年2月。
[3] 郑阿奇、丁有和,C++实用教程,电子工业出版社, 2008年1月。
[4] 明日科技,Visual C++程序开发范例宝典,人民邮电出版社,2007年7月。
[5] C++课程设计五子棋游戏http://wenku.baidu.com/view/979c7ccada38376bae1fae06.html
[6] MFC教程http://wenku.baidu.com/view/9d97c1acdd3383c4bb4cd2f2.html