五子棋人机对战设计任务书目录1.系统需求分析 (1)2.总体设计 (1)3.详细设计 (2)4.系统调试 (4)5.使用说明 (5)6.编程体会 (6)7.源程序清单 (7)1.系统需求分析五子棋是我国古代传统的黑白棋种之一,又称作连珠棋。
五子棋游戏首先需要棋盘,并绘制棋子,若希望人机对战还要为计算机设置一定的算法,以使其能够自动判断落棋的位置,此外,还需要有一定的判断系统来判定胜负,还有悔棋功能。
综上,五子棋人机对战游戏需要提供以下功能:(1)使用图形界面,绘制棋盘,并能够提供虚拟棋盘来作为计算机运算的依据。
(2)判断玩家的落子位置,并相应的画出对应颜色的棋子,判断落子位置时误差要很小。
另外,需要记录玩家的落子情况。
(3)通过运算判定电脑的落子位置,如防范玩家连成五子,或进攻使自己连成五子取得胜利,并相应的显示对应颜色的棋子。
另外,需要记录电脑的落子情况。
(4)根据规则判断出胜负,先连成五子者获得胜利,并显示出胜利的一方,之后可以按任意键再来一局。
(5)当玩家落棋出现重大失误时,可进行悔棋,清除棋子。
2.总体设计五子棋人机对战游戏包括四个方面的功能,分别是绘制棋盘和棋子等图形化显示功能,获取玩家落子功能,计算并判断得到电脑落子位置的功能以及判断胜负的功能。
图形化显示功能方面,运用easyx图形库进行图形的绘制,可以进行背景色的设置,线条的绘制,文字的显示和字体的设置。
通过initgraph初始化界面,设置坐标,用setbkcolor函数设置背景色,用settextcolor函数设置文本颜色,用settextstyle函数设置文本字体,用outtextxy函数显示文字,用line 和fillcircle 函数进行棋盘的绘制,用fillcircle 函数进行棋子的绘制。
通过HWND 句柄可以弹出选项框。
建立二维数组作为虚拟棋盘。
玩家落子方面,首先需要判断鼠标所点击的位置,然后需要在链表上建立新的节点,在虚拟棋盘上设置玩家落子,并在棋盘上绘制出玩家的棋子。
电脑落子方面,需要遍历所有位置,分别对玩家在该处落子和电脑在该处落子进行评分,找出玩家或电脑落子评分最高的位置作为落子的位置,在虚拟棋盘上落子,并在棋盘上绘制出电脑的棋子。
判定胜负功能方面,在玩家及电脑每次落子之后,都通过调用判定函数来判断某一方是否连成五子,若一方胜利,显示游戏结果,跳出当前棋局的循环,并按任意键重新布置棋盘,开始新的一局。
悔棋方面,需要沿着链表的结构进行前一步棋子的清除。
五子棋游戏中的功能模块图:3. 详细设计五子棋人机对战程序中的类的层次图为:chessboard (棋盘类)piece (棋子类)piece_playerpiece_computerplayer_listcomputer_list设置棋盘模块显示界面,绘制棋盘,设置虚拟棋盘五子棋人机对战游戏判断模块判断某一方是否连成五子,显示结果电脑落子模块判定最佳落子处,建立链表新节点玩家落子模块玩家落子捕捉,建立链表的新节点悔棋模块清除原棋子五子棋人机对战游戏中各功能模块的实现:1.布置棋盘模块2.玩家落子模块3.电脑落子模块4.判定胜负模块5.悔棋模块五子棋人机对战游戏中六个类的UML 图:布置棋盘功能绘制15*15棋盘 设置虚拟棋盘玩家落子功能捕捉鼠标信息建立新的链表节点 设置与绘制棋子电脑落子功能运算判断最佳落子位置建立新的链表节点 设置与绘制棋子判定胜负功能判定是否连为五子显示结果,清空缓存,跳出循环开始界面布置棋盘玩家落子 电脑落子判定胜负悔棋悔棋功能清除玩家棋子清除电脑棋子4.系统调试由于使用了图形界面,很多功能的调试只能在程序基本编译完成后进行。
对于棋盘等方面的调试,我是先将程序写为两个玩家的对战进行调试,开始的错误主要是语法错误,例如两次声明全局数组extern int a[16][16],却没有对其进行定义,导致了LNK2001错误。
另外的一个错误是电脑自动先下时,未将数组坐标转化为图形界面上的坐标,使得棋子所在的位置与其应当出现的位置出现了较大的偏差。
接下来的调试主要是算法方面的调试。
开始时,电脑下棋会出现一些致命的错误,例如:玩家执白旗,电脑执黑棋,玩家已在c8,d9,e10连成三珠,但电脑下棋时去选择了f4的位置,而不是b7或f11来堵住玩家,从而导致电脑输下此局,根据这个情况,我判断是f4左上方三颗棋子影响了电脑的判断,另外d4到f4处还可形成进攻,因而修改程序的过程中,我增大了连成三珠且两边没有另一方棋子堵住情况的权重。
经过修改,电脑下棋便不再犯这样的低级错误,但不得不说,电脑下棋的方式也变得比之前保守。
此外,之前还出现过电脑没有注意边界,而依然在边界处下棋企图连成三子的情况,为此,我在程序中另加了判断语句,若连接的棋子数目小于5且一边已到达边界,则不在边界处着棋,另外如果对方棋子已在距边界小于等于五的距离处将该方向堵住,也扣除一定的权重。
经过上述的处理,最佳落棋处的分数会比那些受限制的落棋处的分数高,而不是因为可能的分数相同而造成电脑判断的失误。
在进行悔棋功能的调试时,我先是处理了循环结构造成的问题,然后在棋盘上清除棋子图案的程序经过了两三次的修改,终于使得棋子的图案完全被清除。
此外在悔棋直到没有棋子时,刚开始的时候程序会发生崩溃,原因是我没有添加头指针是否为空的判断,添加该判断后,棋盘上没有棋子时不再发生变化。
5.使用说明启动游戏后,首先会进入开始界面。
此时按任意键(指键盘)即可进入游戏。
进入游戏后首先需要选择是否让电脑先下,另外,游戏中永远是玩家执白棋,电脑执黑棋,游戏中没有禁手的规则。
游戏的窗口大小不能改变,故高度较小、屏幕较小的电脑需要调节分辨率来显示全部的棋盘,否则可能有一部分棋盘(一半是棋盘的最下方)在屏幕中无法显示。
若想要棋盘显示完全,请保证屏幕的竖直方向像素至少为800,水平方向的像素至少为900.点击确定电脑先下,点击取消则玩家先下。
电脑先下时,会自动选择天元的位置。
玩家获得胜利或电脑胜利时时,棋盘的上方会出现玩家胜利或玩家失利的的信息。
此时按任意键(键盘)可以重新布置棋盘,开始新的一局。
请注意,下棋时鼠标是有一定的点击范围的,起一定要点击到交点的中央区域附近,否则程序不会认为玩家已下棋,也不会在玩家认为自己下过棋的地方显示棋子,故一定要点击准确,下棋前一定要三思,点击一定要准确。
此外,游戏设置了悔棋功能,点击右键即可悔棋。
若希望结束游戏,直接点击右上方的关闭按钮关闭程序即可退出游戏。
此外,游戏编译通过是建立在有easyx C++图形库的基础上。
故而没有安装easyx图形库的情况下,头文件graphics.h中是没有相关函数的,会使编译不通过。
程序在VS2008下编译成功。
6.编程体会这是我第一次应用图形界面编程,通过这个游戏程序的编写,我初步了解了MFC,easyx等图形化编程的工具,并初步学会使用easyx C++图形库进行编程。
除了图形界面编程,此程序中还应用了构造函数的重载,类的继承与抽象基类,动态内存分配与链表结构的建立等知识。
这个程序中包含六个类,其中玩家棋子类与电脑棋子类属性与方法相似,通过棋子基类进行派生,各个类的功能分配等方面比我之前的编程也更加明晰。
使用链表结构使得每一步棋间都通过链表进行了关联,既贴合实际,也是程序进一步完善的需要。
通过链表的结构,每一步棋都可以追溯到上一步棋,通过为玩家和电脑双方各建立一个链表,方便了悔棋功能的设置。
使用链表的同时还进行内存的动态分配,每一次添加棋子都通过调用list类的添加函数,使得主程序更加简洁。
判断胜负与对电脑应该下的位置进行打分通过两个函数实现。
判断胜负的函数较简单,只需对当前所下位置处各个方向的连珠情况进行比较,返回游戏是否结束或某一方获胜的结果即可。
难点在于计算机下棋算法的设计,在给计算机下棋的位置进行评估时,需要考虑四个方向,还需考虑一些其他的情况,想得越周到便意味着计算机算法越完善,同时还需要由不同的方面根据不同的情况对某一位置赋予不同的分数,分数的确定也是一个困难所在。
在我的算法中,更多的考虑的是如何让电脑避免失败,同时也有一定的进攻能力。
对每一位置程序都会对电脑下在该处和玩家下在该处同时进行评分,然后选择分数最高处,这样玩家最有威胁的位置和电脑最有威胁的位置都在程序的考虑之内。
在这个评分函数中,取的是四个方向中的最大值,这样的设置虽然在一定程度上能够实现不同方向上的综合考虑(在某一方向评分已经较高的情况下还有另一方向评分更高,当然是一个好位置),但是还不足,因为有些位置落棋后可能对多个方向都有好处,此时仅仅取其中的最高值当然是不够的,可以在函数中增加其他方向加分的功能,但这对分数权重的分配和细节考虑上无疑有更高的要求,否则可能会导致多方向的小利战胜了某一方向上的大利,直接影响电脑的输赢。
这是我的程序还需完善的地方。
总之,这个程序满足了五子棋人机对战游戏的基本要求,同时也可以被改编为双人对战游戏。
游戏还有添加功能的空间,算法也可以更加完善。
以此作为我应用程序编程、电脑算法及图形界面编程的初步尝试。
7.源程序清单1.chessboard.h文件#include<iostream>#include<graphics.h>#include<conio.h>using namespace std;class chessboard{public:chessboard(); //开始界面void initboard(); //布置棋盘};2.chessboard.cpp文件#include"chessboard.h"chessboard::chessboard() //开始界面{initgraph(900,800); //初始化为横向个点,纵向个点setbkcolor(WHITE);cleardevice();settextcolor(RED);settextstyle(60, 0, _T("楷体"));outtextxy(220, 100, _T("五子棋人机对战"));settextstyle(30, 0, _T("宋体"));outtextxy(300,300,_T("按任意键开始游戏"));getch();}void chessboard::initboard(){int i;initgraph(900,800);setbkcolor(BROWN);cleardevice(); //清除屏幕和图形缓冲区settextcolor(RED);settextstyle(120, 0, _T("隶书"));outtextxy(780, 80, _T("五"));outtextxy(780, 200, _T("子"));outtextxy(780, 320, _T("棋"));settextstyle(30,0, _T("楷体"));outtextxy(780,500,_T("按右键"));outtextxy(800,530,_T("悔棋"));for(i=50;i<=750;i=i+50) //绘制棋盘{setlinecolor(BLACK);line(50,i,750,i);line(i,50,i,750);}setfillcolor(BLACK); //绘制天元和星fillcircle(200,200,4);fillcircle(200,600,4);fillcircle(600,200,4);fillcircle(600,600,4);fillcircle(400,400,4);}3.piece.h文件#include<iostream>#include<graphics.h>using namespace std;class piece //棋子基类{public:piece();virtual ~piece();int getx();int gety();virtual void print() const=0; //绘制棋子函数protected:int x; //棋盘上的坐标int y;};class piece_player:public piece //玩家棋子类{public:piece_player();piece_player(int,int);virtual ~piece_player();virtual void print() const; //绘制玩家棋子函数piece_player *p_next; //链表结构};class piece_computer:public piece //电脑棋子类{public:piece_computer();piece_computer(int,int);virtual ~piece_computer();virtual void print() const; //绘制电脑棋子piece_computer *c_next;};class player_list //玩家棋子链表{public:player_list();~player_list();bool insert(int,int); //插入节点函数bool Delete(int a[16][16]);protected:piece_player *p_head;};class computer_list //电脑棋子链表{public:computer_list();~computer_list();bool insert(int,int); //插入节点函数bool Delete(int a[16][16]);protected:piece_computer *c_head;};extern int judge(int,int,int,int a[][16]); //判定胜负函数extern int score(int,int,int,int a[][16]); //打分函数4.FiveChess.cpp文件#include"piece.h"piece::piece(){x=0;y=0;}piece::~piece(){}int piece::getx(){return x;}int piece::gety(){return y;}piece_player::piece_player(){x=0;y=0;}piece_player::piece_player(int a, int b){x=a;y=b;p_next=NULL;}piece_player::~piece_player(){}void piece_player::print() const{setfillcolor(WHITE); //画一个白色的棋子fillcircle(x,y,20);}piece_computer::piece_computer(){x=0;y=0;}piece_computer::piece_computer(int a, int b){x=a;y=b;c_next=NULL;}piece_computer::~piece_computer(){}void piece_computer::print() const{setfillcolor(BLACK); //画一个黑色的棋子fillcircle(x,y,20);}player_list::player_list(){p_head=NULL;}player_list::~player_list(){piece_player *p=p_head; //链表析构函数,直到所有节点都被清除for(;p!=NULL;){p_head=p->p_next;delete p;p=p_head;}}bool player_list::insert(int a, int b) //插入新的节点{piece_player *ptemp=new piece_player(a,b);if(ptemp==NULL)return false;ptemp->p_next=p_head;p_head=ptemp;ptemp->print();return true;}bool player_list::Delete(int a[16][16]) {int x,y;if(p_head==NULL)return false;piece_player *p;x=p_head->getx();y=p_head->gety();a[x/50][y/50]=0;setfillcolor(BROWN);fillcircle(x,y,20);setcolor(BROWN);circle(x,y,20);setcolor(BLACK);line(x-20,y,x+20,y);line(x,y-20,x,y+20);p=p_head->p_next;delete p_head;p_head=p;return true;}computer_list::computer_list(){c_head=NULL;}computer_list::~computer_list(){piece_computer *p=c_head;for(;p!=NULL;){c_head=p->c_next;delete p;p=c_head;}}bool computer_list::insert(int a, int b){piece_computer *ctemp=new piece_computer(a,b);if(ctemp==NULL)return false;ctemp->c_next=c_head;c_head=ctemp;ctemp->print();return true;}bool computer_list::Delete(int a[16][16]){int x,y;if(c_head==NULL)return false;piece_computer *p;x=c_head->getx();y=c_head->gety();a[x/50][y/50]=0;setfillcolor(BROWN);fillcircle(x,y,20);setcolor(BROWN);circle(x,y,20);setcolor(BLACK);line(x-20,y,x+20,y);line(x,y-20,x,y+20);p=c_head->c_next;delete c_head;c_head=p;return true;}extern int a[16][16];extern int judge(int x,int y,int s,int a[][16]){int i,j; //坐标int p,q; //记录连珠数int vertical,horizon,rincline,lincline; //记录四个方向的连珠数for(i=x,p=0;(i<x+5)&&(i<16);i++) //水平连珠数{if(a[i][y]!=s)break;elsep++;}for(i=x-1,q=0;(i>x-5)&&(i>0);i--){if(a[i][y]!=s)break;elseq++;}horizon=p+q;for(j=y,p=0;(j<y+5)&&(j<16);j++) //竖直连珠数{if(a[x][j]!=s)break;elsep++;}for(j=y-1,q=0;(j>y-5)&&(j>0);j--){if(a[x][j]!=s)break;elseq++;}vertical=p+q;for(i=x,j=y,p=0;(i<x+5)&&(j<y+5)&&(i<16)&&(j<16);i++,j++) //右斜方向连珠数{if(a[i][j]!=s)break;elsep++;}for(i=x-1,j=y-1,q=0;(i>x-5)&&(j>y-5)&&(i>0)&&(j>0);i--,j--){if(a[i][j]!=s)break;elseq++;}rincline=p+q;for(i=x,j=y,p=0;(i>x-5)&&(j<y+5)&&(i>0)&&(j<16);i--,j++) //左斜方向连珠数{if(a[i][j]!=s)break;elsep++;}for(i=x+1,j=y-1,q=0;(i<x+5)&&(j>y-5)&&(i<16)&&(j>0);i++,j--){if(a[i][j]!=s)break;elseq++;}lincline=p+q;if((vertical>=5)||(horizon>=5)||(rincline>=5)||(lincline>=5)) //判断某一方是否胜利{if(s==1)return 1;else if(s==2)return 2;elsereturn 0;}else return 0;}extern int score (int m,int n,int s,int a[][16]) //打分函数{int i1,j1,i2,j2; //坐标记录int i;int p=0,q=0; //计算连子数int score[4]={0,0,0,0}; //记录四个方向的一串连子的两头的情况,是否被另一方堵上int x=0; //分数int vertical,horizon,lincline,rincline; //四个方向的连子数for(i1=m;(i1<m+5)&&(i1<16);i1++){if(a[i1][n]!=s){if(a[i1][n]==0) score[0]++;else if(i1<=5) //棋盘大小限制score[0]--;break;}elsep++;}for(i2=m-1;(i2>m-5)&&(i2>0);i2--){if(a[i2][n]!=s){if(a[i2][n]==0) score[0]++;else if(i2>=10) //棋盘大小限制score[0]=0;break;}elseq++;}horizon=p+q;if((horizon<=4)&&((m==1)||(m==15))) //棋盘边界位置score[0]=0;if((horizon>=4)&&(a[i2][n]==0)&&(a[i1][n]==0)) //防止四连珠或形成四连珠{if(s==2)score[3]+=3;score[3]+=2;}for(j1=n,p=0;(j1<n+5)&&(j1<16);j1++){if(a[m][j1]!=s){if(a[m][j1]==0)score[1]++;else if(j1<=5)score[1]--;break;}elsep++;}for(j2=n-1,q=0;(j2>n-5)&&(j2>0);j2--){if(a[m][j2]!=s){if(a[m][j2]==0)score[1]++;else if(j2>=10)score[1]--;break;}elseq++;}vertical=p+q;if((vertical<=4)&&((n==1)||(n==15)))score[1]=0;if((vertical>=4)&&(a[m][j2]==0)&&(a[m][j1]==0)){if(s==2)score[1]+=3;score[1]+=2;}for(i1=m,j1=n,p=0;(i1<16)&&(i1<m+5)&&(j1<16);i1++,j1++) {if(a[i1][j1]!=s){if(a[i1][j1]==0) //{score[2]++;}else if(i1<=5)score[2]--;break;}elsep++;}for(i2=m-1,j2=n-1,q=0;(i2>0)&&(i2>m-5)&&(j2>0);i2--,j2--) {if(a[i2][j2]!=s)if(a[i2][j2]==0){score[2]++;}else if(i2>=10)score[2]--;break;}elseq++;}rincline=p+q;if((rincline<=4)&&((n==1)||(n==15)||(m==1)||(m==15))) score[2]=0;if((rincline>=4)&&(a[i2][j2]==0)&&(a[i1][j1]==0)){if(s==2)score[2]+=3;score[2]+=2;}for(i1=m,j1=n,p=0;(i1>0)&&(i1>m-5)&&(j1<16);i1--,j1++) {if(a[i1][j1]!=s){if(a[i1][j1]==0)score[3]++;else if(j1<=5)score[3]--;break;}elsep++;}for(i2=m+1,j2=n-1,q=0;(i2<16)&&(i2<m+5)&&(j2>0);i2++,j2--) {if(a[i2][j2]!=s){if(a[i2][j2]==0)score[3]++;else if(j2>=10)score[3]--;break;elseq++;}lincline=p+q;if((lincline<=4)&&((n==1)||(n==15)||(m==1)||(m==15)))score[3]=0;if((lincline>=4)&&(a[i2][j2]==0)&&(a[i1][j1]==0)){if(s==2)score[3]+=3;score[3]+=2;}if(horizon>4||vertical>4||lincline>4||rincline>4)x=100;else{for(i=0;i<4;i++){if(score[i]<=0)score[i]=-20;}x=horizon+score[0];if(vertical+score[1]>x)x=vertical+score[1];if(lincline+score[2]>x)x=lincline+score[2];if(rincline+score[3]>x)x=rincline+score[3];}return x; //返回最大的分数}5.main.cpp文件#include"chessboard.h"#include"piece.h"int a[16][16]; //15*15棋盘,只用至的数组int main(){chessboard board1;MOUSEMSG m;int i,j; //棋盘数组坐标bool p=0; //记录player是否下过int score_player[16][16],score_computer[16][16]; //判断电脑的落子处int score_max; //记录打分的最大值int max_x,max_y; //记录打分最大值的坐标int x;while(1){board1.initboard();player_list *player=new player_list; //建立玩家链表computer_list *computer=new computer_list; //建立电脑链表for(i=1;i<16;i++) //初始化棋盘,代表没有棋子,代表player,代表computerfor(j=1;j<16;j++)a[i][j]=0;HWND wnd=GetHWnd(); //获取玩家选择if (MessageBox(wnd, _T("大神,您要让电脑先下吗?"), _T("选项"),MB_OKCANCEL | MB_ICONQUESTION) == IDOK){i=8;j=8;a[i][j]=2;computer->insert(i*50,j*50); //点击确定,电脑先下}while(1){while(p==0){m=GetMouseMsg(); //获取玩家落子位置switch(m.uMsg){case WM_LBUTTONDOWN:{for(i=50;i<=750;i=i+50){for(j=50;j<=750;j=j+50){if((m.x<i+10)&&(m.x>i-10)&&(m.y<j+10)&&(m.y>j-10)&&(a[i/50][j/50]==0)) // 判断是否能下及下好位置{player->insert(i,j);p=1;a[i/50][j/50]=1;break;}}if(p)break;}}break;}case WM_RBUTTONDOWN: //右键悔棋{computer->Delete(a);player->Delete(a);b=1;break;}}if(b) //若悔棋直接跳到玩家出棋{b=0;continue;}p=0;if(judge(i/50,j/50,1,a)==1) //判断玩家是否胜利{settextcolor(YELLOW);settextstyle(30, 0, _T("华文行楷"));outtextxy(200, 0, _T("---恭喜您,您胜利了!按任意键继续---"));delete computer;delete player;getch();break;}for(i=1;i<16;i++) //初始化棋盘各个位置分数for(j=1;j<16;j++){score_computer[i][j]=0;score_player[i][j]=0;}for(i=1;i<16;i++){for(j=1;j<16;j++){if(a[i][j]==0){a[i][j]=1;x=score(i,j,1,a);score_player[i][j]+=x; // 记录如果玩家下在此处,得到多少分a[i][j]=2;x=score(i,j,2,a);score_computer[i][j]+=x; // 记录如果电脑下在此处,得到多少分a[i][j]=0;}}}// 找到能下棋的空位置中,假设电脑和人下在此处,得到分数中最大值for(i=1,score_max=score_computer[1][1];i<16;i++){for(j=1;j<16;j++){if(score_computer[i][j]>score_max){score_max=score_computer[i][j];max_x=i;max_y=j;}}}for(i=1;i<16;i++){for(j=1;j<16;j++){if(score_player[i][j]>score_max){score_max=score_player[i][j];max_x=i;max_y=j;}}}//玩家与电脑的评分标准是一样的,故而电脑既能判断出如何下接近胜利或如何下避免输棋a[max_x][max_y]=2; // 在最高分处落子computer->insert(max_x*50,max_y*50);if(judge(max_x,max_y,2,a)==2){settextcolor(YELLOW);settextstyle(30, 0, _T("华文行楷"));outtextxy(200, 0, _T("---您输了,不太走运哦~按任意键继续---"));delete computer;delete player;getch();break;}} //内部的while} //外部的whilereturn 0;}。