用C++开发五子棋及其AI(一)

在这里插入图片描述
前言:有了用C++开发象棋及其AI的经验后,我就萌生了再用C++开发五子棋及其AI的想法。有了想法还等什么?付诸实施呗!

在这里插入图片描述
首先明确一盘五子棋需要什么属性:一盘五子棋有黑棋有白棋,与象棋不同的是,五子棋的棋子数目不是确定的,而是随着下棋随着增加的,所以,需要一个存放整盘棋棋子数目的整型变量num_of_Stone。五子棋下棋的有两方,黑方和白方,所以需要一个存放当前该谁走的布尔型变量_bBlackTurn,黑棋先行,顾名思义,初始化时应该将它的值置为true。还需要一个存放棋盘上存在的所有棋子的数组_s,由于不确定棋子的数目,所以将这个数组的类型设置为QVector。此外,为了更方便地处理棋盘信息,再引入存放棋盘信息的二维数组map[15][15],和将横竖撇捺四个方向上线段化之后形成的一维数组的集合L。

给大家看一下棋盘类:

#ifndef BOARD_H

#define BOARD_H

#include

#include “Stone.h”

#include “Step.h”

#include

#include

class Board : public QWidget

{

Q_OBJECT

public:

//棋盘类的构造函数

explicit Board(QWidget *parent = 0);

//棋盘类的析构函数

~Board();

//棋子半径

int _r;

//棋子数目

int num_of_stone;

//存放所有棋子的数组

QVector _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& 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,id),QPoint(15d,i*d));

//画9条竖线

for(int i=1;i<=15;i++)

painter.drawLine(QPoint(id,d),QPoint(id,15*d));

//绘制棋子

for(int i=0;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 = dxdx + dydy;

if(dist<_r*_r)

return true;

}

return false;

}

getRowCol函数有两个引用的参数,因此该函数的功能不仅仅是返回了一个布尔类型的值,其主要功能还有确定用户点击位置所属的行列坐标。以所有行列坐标为圆心,假想一个个圆形区域,用户点击在圆形区域范围内便确定了一个行列坐标。

//输入行列坐标 返回像素坐标

QPoint Board::center(int row,int col)

{

QPoint ret;

ret.rx()=(col+1)_r2;

ret.ry()=(row+1)_r2;

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§;

_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

{

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;

}

该函数的函数体较为庞杂,因为它采用了一个棋子一个棋子判断的方法,遍历到一个棋子,那么由该棋子向其左上、左、左下、右上、右、右下延伸判断,出现五子连珠则跳出循环。

最后给大家看一下程序效果图:

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/weixin_43884642/article/details/88551101
今日推荐