一、前言
Qt提供了UI控件和信号与槽以及多种绘制方法,结合这些方法,进行黑白棋游戏的实现,包含人机对战和人人对战以及网络对战功能,并且实时记录双方棋子数量。
二、思路和功能
黑白棋游戏,是由棋盘和两种棋子组成的吃子游戏。
1.棋盘绘制
通过QPainter类中的画线方法绘制8*8的棋盘,并且设置背景图片,同时重写调整事件,防止调整窗体大小后导致棋盘布局紊乱
2.落子功能
在画好的棋盘上通过坐标区分格子,通过二维数组进行存储格子中的值,通过判断进行棋子的绘制
3.吃子功能
黑白棋的吃子是在横向,竖向,斜向两同色棋子围住时进行吃子,所以吃子功能应该在落点进行周围八个格子的判断,不出界且相邻棋子是对方棋子的时候,才有吃子可能,然后对该方向进行循环前进,遇到空位时跳出循环,找到自己的棋子时则循环后退并将棋子都标记为自己的棋子。
4.PVP模式
设置变量currentRole对应当前下子玩家,并且在下子之前遍历棋盘,判断是否有可以下子的条件,无法下子则切换角色。
5.PVE模式
遍历棋盘寻找可下子的格子进行下子
三、函数功能和具体实现(附代码)
1.棋盘,棋子绘制和初始化函数
(1)棋盘绘制
通过QPainter类的drawPixmap绘制出背景
Qpen类中setColor,setStyle,setWidth,setPen进行画笔样式的设置和画笔选择,通过循环9次进行画线,同时设置gridwidth和gridheight变量将窗体长宽分为十份,进行坐标点的循环绘制。
void chess::paintEvent(QPaintEvent *)
{
QPainter painter(this);
//绘制背景
QRect rec(QPoint(0,0),QPoint(this->width(),this->height()));
QPixmap pix(bgFilename);
// pix.load("../mychess/background.png");
painter.drawPixmap(rec,pix);
//绘制棋盘
//画线
QPen pen1;
pen1.setColor(lineColor);
pen1.setStyle(lineStyle);
pen1.setWidth(lineWidth);
painter.setPen(pen1);
for(int i=0;i<9;i++)
{
painter.drawLine(startX,startY+ i*gridheight,9*gridwidth,startY+i*gridheight);
painter.drawLine(startX+i*gridwidth,startY,startX+gridwidth*i,gridheight*9);
}
(2)棋子绘制
定义chessFilename变量和chessData二维数组记录棋盘中棋子的颜色,通过drawPixmap()方法绘制黑白棋图片
//画棋子
QString chessFilename;
for (int i=0; i<8;i++)
{
for(int j=0;j<8;j++)
{
if(chessData[i][j] == White)
{
chessFilename = "../mychess/whitestone.png";
}
else if(chessData[i][j] == Black)
{
chessFilename = "../mychess/blackstone.png";
}
else
{
chessFilename.clear();
continue;
}
painter.drawPixmap(startX+i*gridwidth,startY+j*gridheight,gridwidth,gridheight,QPixmap(chessFilename));
}
}
(3)初始化
设置图片和画笔样式,将棋盘二维数组全部设置为空。
void chess::Init()
{
bgFilename.clear();
bgFilename = "../mychess/background.jpg";
lineColor = Qt::black;
lineStyle = Qt::SolidLine;
lineWidth = 3;
}
void chess::InitChess()
{
//初始化棋盘数据
// memset(chessData,0,sizeof(int)*64);
for(int i=0;i<8;i++)
{
for(int j=0;j<8;j++)
{
chessData[i][j] = Empty;
}
}
}
2.落子,棋子显示,计数显示,游戏模式
(1)落子
重写mousePressEvent()事件,利用点击位置坐标进行判断属于哪个格子,传输出格子信号
连接信号和槽,编写槽函数,并判断该处是否可以落子,可以落子时进行游戏模式判断,再调用对应函数进行落子
void chess::mousePressEvent(QMouseEvent *event)
{
int x = event->x();
int y = event->y();
//x
if(x>=startX && (x<=startX+8*gridwidth))
{
//y
if(y>=startY && (y<=startY+8*gridheight))
{
//得出当前坐标属于哪个格子
int i = 0,j = 0;
i = (x-startX)/gridwidth;
j = (y-startY)/gridheight;
// chessData[i][j] = Black;
// this -> update();
SignalSendChessData(i,j);
}
}
}
(2)棋子显示
编写setChessStatus(void *p)方法,调用update()方法进行棋盘更新,并通过二维数组确定各位置棋子的颜色
void chess::setChessStatus(void *p)
{
memcpy(chessData,p,sizeof(int)*8*8);
this->update();
}
(3)计数显示
编写ChessShow()方法,遍历二维数组,对不同颜色的棋子进行计数,通过UI控件的lcdNumber进行显示
void ChessForm::ChessShow()
{
int blackcount = 0,whitecount =0;
for(int i=0;i<8;i++)
{
for (int j=0;j<8;j++)
{
if(formChessData[i][j]==chess::Black)
{
blackcount++;
}
else if(formChessData[i][j] == chess::White)
{
whitecount++;
}
}
}
ui->lcdNumber1->display(whitecount);
ui->lcdNumber2->display(blackcount);
}
(4)游戏模式
设定黑白子先行模式,并将人人设置为PVP,人机设置为PVC,利用currentPK变量进行存储,并设置点击时进行棋盘初始化和先手棋子的选择
void ChessForm::on_btn_pvp_clicked()
{
currentPK = PVP;
//把界面初始化
if(ui->cbox_item->currentIndex() == 0)
{
setRole(chess::White);
}
else
{
setRole(chess::Black);
}
//把棋盘初始化
setChessInit();
}
3.吃子判定,机器落子,无法落子的角色切换
(1)吃子判定
定义一个8行2列数组代表当前坐标的八个方向,循环八个方向对相邻棋子进行判定,如果是对方棋子,则继续朝该方向移动,当遇到空位或者出界时,跳出循环进行另外方向的判定,当该方向找到自己的棋子时候,循环后退到当前落子点,并将沿途棋子标记为自己的棋子。
int ChessForm::judegRule(int x, int y, void *chess, chess::ChessType currentRole, bool eatChess)
{
//棋盘的八个方向
int dir[8][2] = {{1,0},{1,-1},{0,-1},{-1,-1},{-1,0},{-1,1},{0,1},{1,1}};
//临时保存棋盘数组坐标位置
int temp_x = x,temp_y = y;
//初始化数据
int i = 0, eatNum = 0;
//自定义类型
typedef int (*p)[8];
//类型转换
p chessFlag = p(chess);
//如果此方格内已有棋子,返回;
if(chessFlag[temp_x][temp_y] != chess::Empty)
{
return 0;
}
//棋盘的八个方向
for(int i=0;i<8;i++)
{
//准备判断相邻棋子
temp_x += dir[i][0];
temp_y += dir[i][1];
//如果没有出界,且相邻棋子是对方棋子,才有吃子可能
if((temp_x <8 && temp_x>=0 && temp_y <8 && temp_y>=0) && (chessFlag[temp_x][temp_y]!=currentRole) && (chessFlag[temp_x][temp_y]!=chess::Empty))
{
temp_x+=dir[i][0];temp_y+=dir[i][1];
while(temp_x <8 && temp_x >=0 && temp_y <8 && temp_y >=0)
{
//遇到空位,代表不能吃子,跳出
if(chessFlag[temp_x][temp_y] == chess::Empty)
{
break;
}
//找到自己的棋子,可以吃子
if(chessFlag[temp_x][temp_y] == currentRole)
{
//确定吃子
if(eatChess == true)
{
//标记为自己的棋子
chessFlag[x][y] = currentRole;
//后退一步
temp_x -= dir[i][0];temp_y -= dir[i][1];
//只要没有回到开始的位置就执行
while((temp_x!=x)||(temp_y!=y))
{
//标记为自己的棋子
chessFlag[temp_x][temp_y] = currentRole;
//继续后退一步
temp_x -= dir[i][0];temp_y -= dir[i][1];
//累计
eatNum++;
}
}
//不吃子,判断这个位置能不能吃子
else
{
//后退一步
temp_x -= dir[i][0];temp_y -= dir[i][1];
//计算可以吃子的个数
while((temp_x!=x)||(temp_y!=y))
{
//继续后退一步
temp_x -= dir[i][0];temp_y -= dir[i][1];
eatNum++;
}
}
//跳出循环
break;
}
//没有找到自己的棋子,向前一步
temp_x += dir[i][0];temp_y += dir[i][1];
}
}
//如果这个方向不能吃子,就换一个方向
temp_x = x;temp_y = y;
}
return eatNum;
}
(2)机器落子
进行棋盘的遍历,记录可落子位置的格子,调用吃子判定函数进行落子,然后切换角色界面显示。
机器落子或许可以结合深度学习中的局部最优思想进行算法编写,本文注重实现功能,不做详解和实现
void ChessForm::RebootRole(chess::ChessType role)
{
//0,不能下子,1,能下子
int flag =0;
int ret,oldret;
int c_i,c_j;
for(int i=0;i<8;i++)
{
for(int j=0;j<8;j++)
{
//遍历,能否下子
if(formChessData[i][j] == chess::Empty)
{
ret = judegRule(i,j,formChessData,role,false);
if(ret>0)
{
flag++;
c_i = i,c_j =j;
oldret = ret;
}
}
}
}
if(flag)
{
//下子
judegRule(c_i,c_j,formChessData,currentRole,true);
mychess->setChessStatus(formChessData);
//切换界面显示
RoleChange();
}
else
{
RoleChange();
}
}
(3)无法落子的角色切换
定义一个落子判断函数,可以落子返回值为1,不可落子返回值为0
人人对战时:当一方无法落子时,调用切换角色函数,更新棋盘
人机对战时:当玩家无法落子时,调用切换角色函数和机器落子函数
if(currentPK == PVP)
{
//角色切换
RoleChange();
//判断能否落子
int judgment = JudgmentRole(currentRole);
if(judgment)
{
//数据统计
ChessShow();
}
else
{
RoleChange();
ChessShow();
}
}
else if(currentPK == PVC)
{
//机器下子
RoleChange();
RebootRole(currentRole);
int judgment = JudgmentRole(currentRole);
if(judgment)
{
//数据统计
ChessShow();
}
else
{
RoleChange();
RebootRole(currentRole);
//数据统计
ChessShow();
}
}