用C++开发五子棋及其AI(一)
本人从事在线教育c++十年工作经验现在精心整理了一套从小白到项目实践开发各种学习资料如果你想学想加入我们请关注我在私信回复“编程”就可以领取学习资料!!!
前言:有了用C++开发象棋及其AI的经验后,我就萌生了再用C++开发五子棋及其AI的想法。有了想法还等什么?付诸实施呗!
首先明确一盘五子棋需要什么属性:一盘五子棋有黑棋有白棋,与象棋不同的是,五子棋的棋子数目不是确定的,而是随着下棋随着增加的,所以,需要一个存放整盘棋棋子数目的整型变量num_of_Stone。五子棋下棋的有两方,黑方和白方,所以需要一个存放当前该谁走的布尔型变量_bBlackTurn,黑棋先行,顾名思义,初始化时应该将它的值置为true。还需要一个存放棋盘上存在的所有棋子的数组_s,由于不确定棋子的数目,所以将这个数组的类型设置为QVector<Stone>。此外,为了更方便地处理棋盘信息,再引入存放棋盘信息的二维数组map[15][15],和将横竖撇捺四个方向上线段化之后形成的一维数组的集合L。
给大家看一下棋盘类:
#ifndef BOARD_H #define BOARD_H #include <QFrame> #include "Stone.h" #include "Step.h" #include <QVector> #include <QMouseEvent> class Board : public QWidget { Q_OBJECT public: //棋盘类的构造函数 explicit Board(QWidget *parent = 0); //棋盘类的析构函数 ~Board(); //棋子半径 int _r; //棋子数目 int num_of_stone; //存放所有棋子的数组 QVector<Stone> _s; //是不是应该黑棋走 bool _bBlackTurn; //棋盘地图 int map[15][15]; //线段化的棋盘信息 struct { int line[15]; }L[21+15+15+21]; //画界面的函数 void paintEvent(QPaintEvent *); //鼠标释放的响应函数 void mouseReleaseEvent(QMouseEvent *); //点击屏幕上某个点的响应函数 void click(QPoint pt); virtual void click(int id,int row,int col); //绘制棋子的函数 void drawStone(QPainter& painter,int id); //判断行走是否合法的函数 bool canMove(int row,int col); //走棋函数 void moveStone(int moveid, int row, int col); //悔棋函数 void reliveStone(int row,int col); //输入行列坐标获取棋子id的函数 int getStoneId(int row,int col); //判断谁胜谁负的函数 int whowin(); //将棋盘信息线段化 void turnLine(); //保存棋子的行棋信息的函数 void saveStep(int moveid, int row, int col, QVector<Step*>& steps); //获取用户点击位置的行列坐标的函数 bool getRowCol(QPoint pt,int &row,int &col); //输入行列坐标返回像素坐标的函数 QPoint center(int row,int col); //输入棋子的id 返回像素坐标 QPoint center(int id); }; #endif // BOARD_H
首先使用棋盘类的构造函数初始化棋盘信息:
Board::Board(QWidget *parent) : QWidget(parent) { int i,j; //将棋子个数初始化为0 num_of_stone=0; //黑棋先行 _bBlackTurn=true; //初始化地图 二维数组的元素的值为-1代表该位置上没有棋子 值为0代表该位置上有黑棋 值为1代表该位置上有白棋 for(i=0;i<15;i++) for(j=0;j<15;j++) { map[i][j]=-1; } }
然后在paintEvent函数里绘制整个棋盘:
void Board::paintEvent(QPaintEvent *) { //初始化画笔painter QPainter painter(this); //初始化棋子直径 int d=30; //初始化棋子半径 _r=d/2; //整盘棋局的胜负信息 flag=1时黑方胜利 flag=2时白方胜利 int flag=whowin(); if(flag==1) { //将画笔painter的颜色设置为黑色 painter.setPen(Qt::black); painter.drawText(rect(), Qt::AlignCenter, QStringLiteral("黑方胜利!")); } else if(flag==2) { //将画笔painter的颜色设置为红色 painter.setPen(Qt::red); painter.drawText(rect(), Qt::AlignCenter, QStringLiteral("白方胜利!")); } else { //画10条横线 for(int i=1;i<=15;i++) painter.drawLine(QPoint(d,i*d),QPoint(15*d,i*d)); //画9条竖线 for(int i=1;i<=15;i++) painter.drawLine(QPoint(i*d,d),QPoint(i*d,15*d)); //绘制棋子 for(int i=0;i<num_of_stone;i++) { drawStone(painter,i); } } }
五子棋的棋盘共15行15列。paintEvent函数是用来画界面的,每调用一次update函数就会被自动调用,paintEvent函数里也需要绘制棋子,因此在paintEvent函数里点用了drawStone函数,drawStone函数在下面介绍。此外,调用whowin函数是用来判断棋局是否已经分出胜负的,如果已经分出胜负当然要展示对局结果给用户看啦,whowin函数在下文中介绍。
另外,棋子也需要单独划分成一类,给大家看一下棋子类:
#ifndef STONE_H #define STONE_H class Stone { public: Stone(); bool _black; int _row; int _col; int _id; void init(int id); }; #endif // STONE_H
五子棋里的棋子类就比象棋的简单多了,棋子的属性基本上只有只有行列坐标和棋子属于哪一方。
棋子有个初始化函数init:
void Stone::init(int id) { _id=id; if(_id%2==0) { _black=true; } else { _black=false; } }
这里就有必要说一下对棋子类型的判断了。前面讲到了一个存放全盘棋子数目的变量num_of_Stone,这里就巧妙运用这个变量对棋子进行初始化,棋子的id就是该棋子放到棋盘上之前的num_of_Stone的值。举个例子,第一个下上去的棋子是黑棋,该棋子的id为该棋子放到棋盘上之前的num_of_Stone的值,也就是0,id除以2的余数为0,所以以id除以2的余数为标志,初始化棋子类型,余数为0的是黑棋。余数为1的是白棋;第二个下上去的棋子是白棋。该棋子的id便是1,id除以2的余数是1,也就初始化该枚棋子白棋。
上drawStone函数的源代码:
void Board::drawStone(QPainter& painter,int id) { painter.setPen(Qt::black); if(_s[id]._black) painter.setBrush(QBrush(Qt::black)); else painter.setBrush(QBrush(Qt::white)); //画圆 painter.drawEllipse(center(id),_r,_r); }
给大家看一下一系列鼠标事件响应函数的源代码:
void Board::mouseReleaseEvent(QMouseEvent *ev) { if(ev->button() != Qt::LeftButton) { return; } click(ev->pos()); } void Board::click(QPoint pt) { int row, col; bool bClicked = getRowCol(pt, row, col); if(!bClicked) return; if(canMove(row,col)) { int id=num_of_stone; click(id, row, col); } } //获取用户点击位置的行列坐标 如果点击在合法范围内返回true 点击在合法范围外返回false bool Board::getRowCol(QPoint pt,int &row,int &col) { for(row=0;row<15;row++) for(col=0;col<15;col++) { QPoint c = center(row,col); int dx = c.x() - pt.x(); int dy = c.y() - pt.y(); int dist = dx*dx + dy*dy; if(dist<_r*_r) return true; } return false; }
getRowCol函数有两个引用的参数,因此该函数的功能不仅仅是返回了一个布尔类型的值,其主要功能还有确定用户点击位置所属的行列坐标。以所有行列坐标为圆心,假想一个个圆形区域,用户点击在圆形区域范围内便确定了一个行列坐标。
//输入行列坐标 返回像素坐标 QPoint Board::center(int row,int col) { QPoint ret; ret.rx()=(col+1)*_r*2; ret.ry()=(row+1)*_r*2; return ret; } //输入棋子的id 返回像素坐标 QPoint Board::center(int id) { return center(_s[id]._row,_s[id]._col); }
形参为id的center函数在drawStone函数中有调用。
void Board::click(int id, int row, int col) { moveStone(id,row,col); update(); } //行棋函数 void Board::moveStone(int moveid, int row, int col) { Stone p; p.init(moveid); //将新生成的棋子压入vector _s.append(p); _s[moveid]._row = row; _s[moveid]._col = col; //转换行棋的一方 _bBlackTurn=!_bBlackTurn; //全盘棋子数目增加1 num_of_stone++; //修改相应的地图信息 map[row][col]=moveid%2; }
在moveStone函数里,要生成一个棋子并将其压入已存在的棋子的集合中,转换行棋方,将棋盘上已有的棋子数加1,最后修改相应的地图信息。
接下来,重点介绍将棋盘信息线段化的turnLine函数和判断棋局胜负的whowin函数,turnLine函数是whowin函数的基础。
turnLine函数的作用是在棋盘所有的可放棋子的位置个数大于等于5的横竖撇捺四个方向上分别形成单独的一维数组,并将所有的一维数组存放在board类的结构体数组L中。
//将棋盘坐标线段化 void Board::turnLine() { int i,row,col,pos,start_row=0,start_col=10; int t; for(i=0;i<21+15+15+21;i++) { //走捺 if(i>=0&&i<10) { pos=0; for(row=start_row,col=start_col;row<15&&row>=0&&col<15&&col>=0;row++,col++,pos++) { L[i].line[pos]=map[row][col]; } for(t=pos;t<15;t++) { L[i].line[t]=-1; } start_col--; } //走捺 else if(i<21) { pos=0; for(row=start_row,col=start_col;row<15&&row>=0&&col<15&&col>=0;row++,col++,pos++) L[i].line[pos]=map[row][col]; for(t=pos;t<15;t++) { L[i].line[t]=-1; } start_row++; if(i==20) { start_row=14; start_col=0; } } //走竖 else if(i<21+15) { pos=0; for(row=start_row,col=start_col;row<15&&row>=0&&col<15&&col>=0;row--,pos++) L[i].line[pos]=map[row][col]; for(t=pos;t<15;t++) { L[i].line[t]=-1; } start_col++; if(i==21+14) { start_row=14; start_col=14; } } //走横 else if(i<21+15+15) { pos=0; for(row=start_row,col=start_col;row<15&&row>=0&&col<15&&col>=0;col--,pos++) L[i].line[pos]=map[row][col]; for(t=pos;t<15;t++) { L[i].line[t]=-1; } start_row--; if(i==21+15+14) { start_row=0; start_col=4; } } //走撇 else if(i<21+15+15+10) { pos=0; for(row=start_row,col=start_col;row<15&&row>=0&&col<15&&col>=0;row++,col--,pos++) L[i].line[pos]=map[row][col]; for(t=pos;t<15;t++) { L[i].line[t]=-1; } start_col++; } //走撇 else if(i<21+15+15+21) { pos=0; for(row=start_row,col=start_col;row<15&&row>=0&&col<15&&col>=0;row++,col--,pos++) L[i].line[pos]=map[row][col]; for(t=pos;t<15;t++) { L[i].line[t]=-1; } start_row++; } } }
一维数组中的元素值为-1代表没有棋子,值为0代表有黑子,值为1代表有白子。turnLine的过程就是初始化结构体数组L的过程。这就为判断胜负提供了遍历,判断胜负的时候只需要一行一行遍历结构体数组L,如果L中有形成五子连珠的情况则返回胜负信息并break。
上whowin函数的源代码:
//获取棋局胜负信息的函数 int Board::whowin() { //初始化结构体数组L turnLine(); int sum_black,sum_white; int i,j; //看看是否有黑棋五子连珠的情况 for(i=0;i<21+15+15+21;i++) { sum_black=0; for(j=0;j<15;j++) { if(L[i].line[j]==0) sum_black++; else //计数器重新置零 sum_black=0; if(sum_black>=5) { return 1; } } } //看看是否有白棋五子连珠的情况 for(i=0;i<21+15+15+21;i++) { sum_white=0; for(j=0;j<15;j++) { if(L[i].line[j]==1) sum_white++; else //计数器重新置零 sum_white=0; if(sum_white>=5) { return 2; } } } return 0; }
先调用turnLine函数初始化结构体数组L,再分别对黑白两方判断胜利与否。在判断的时候以行为单位,出现胜利的一方立刻跳出循环。若黑方胜利返回1;若白方胜利返回2;若两方都没有胜利,则最后返回0。
还有另外一种形式的whowin函数:
int Board::whowin() { int sum; int i,j,k; for(i=0;i<num_of_stone;i++) { if(i%2==0) { sum=0; for(j=_s[i]._row;j>=0&&j<15&&j<_s[i]._row+5;j++) { if(getStoneId(j,_s[i]._col)%2==0) sum++; } if(sum==5) return 1; sum=0; for(j=_s[i]._row;j>=0&&j<15&&j>_s[i]._row-5;j--) { if(getStoneId(j,_s[i]._col)%2==0) sum++; } if(sum==5) return 1; sum=0; for(j=_s[i]._col;j>=0&&j<15&&j<_s[i]._col+5;j++) { if(getStoneId(_s[i]._row,j)%2==0) sum++; } if(sum==5) return 1; sum=0; for(j=_s[i]._col;j>=0&&j<15&&j>_s[i]._col-5;j--) { if(getStoneId(_s[i]._row,j)%2==0) sum++; } if(sum==5) return 1; sum=0; for(j=_s[i]._row,k=_s[i]._col;j>=0&&j<15&&k>=0&&k<15&&j<_s[i]._row+5&&k<_s[i]._col+5;j++,k++) { if(getStoneId(j,k)%2==0) sum++; } if(sum==5) return 1; sum=0; for(j=_s[i]._row,k=_s[i]._col;j>=0&&j<15&&k>=0&&k<15&&j<_s[i]._row+5&&k>_s[i]._col-5;j++,k--) { if(getStoneId(j,k)%2==0) sum++; } if(sum==5) return 1; sum=0; for(j=_s[i]._row,k=_s[i]._col;j>=0&&j<15&&k>=0&&k<15&&j>_s[i]._row-5&&k<_s[i]._col+5;j--,k++) { if(getStoneId(j,k)%2==0) sum++; } if(sum==5) return 1; sum=0; for(j=_s[i]._row,k=_s[i]._col;j>=0&&j<15&&k>=0&&k<15&&j>_s[i]._row-5&&k>_s[i]._col-5;j--,k--) { if(getStoneId(j,k)%2==0) sum++; } if(sum==5) return 1; } else { sum=0; for(j=_s[i]._row;j>=0&&j<15&&j<_s[i]._row+5&&j<15;j++) { if(getStoneId(j,_s[i]._col)%2==1) sum++; } if(sum==5) return 2; sum=0; for(j=_s[i]._row;j>=0&&j<15&&j>_s[i]._row-5&&j<15;j--) { if(getStoneId(j,_s[i]._col)%2==1) sum++; } if(sum==5) return 2; sum=0; for(j=_s[i]._col;j>=0&&j<15&&j<_s[i]._col+5&&j<15;j++) { if(getStoneId(_s[i]._row,j)%2==1) sum++; } if(sum==5) return 2; sum=0; for(j=_s[i]._col;j>=0&&j<15&&j>_s[i]._col-5&&j<15;j--) { if(getStoneId(_s[i]._row,j)%2==1) sum++; } if(sum==5) return 2; sum=0; for(j=_s[i]._row,k=_s[i]._col;j>=0&&j<15&&k>=0&&k<15&&j<_s[i]._row+5&&k<_s[i]._col+5;j++,k++) { if(getStoneId(j,k)%2==1) sum++; } if(sum==5) return 2; sum=0; for(j=_s[i]._row,k=_s[i]._col;j>=0&&j<15&&k>=0&&k<15&&j<_s[i]._row+5&&k>_s[i]._col-5;j++,k--) { if(getStoneId(j,k)%2==1) sum++; } if(sum==5) return 2; sum=0; for(j=_s[i]._row,k=_s[i]._col;j>=0&&j<15&&k>=0&&k<15&&j>_s[i]._row-5&&k<_s[i]._col+5;j--,k++) { if(getStoneId(j,k)%2==1) sum++; } if(sum==5) return 2; sum=0; for(j=_s[i]._row,k=_s[i]._col;j>=0&&j<15&&k>=0&&k<15&&j>_s[i]._row-5&&k>_s[i]._col-5;j--,k--) { if(getStoneId(j,k)%2==1) sum++; } if(sum==5) return 2; } } return 0; }
该函数的函数体较为庞杂,因为它采用了一个棋子一个棋子判断的方法,遍历到一个棋子,那么由该棋子向其左上、左、左下、右上、右、右下延伸判断,出现五子连珠则跳出循环。
最后给大家看一下程序效果图:
本人从事在线教育c++十年工作经验现在精心整理了一套从小白到项目实践开发各种学习资料如果你想学想加入我们请关注我在私信回复“编程”就可以领取学习资料!!!