您的位置:首页 > 产品设计 > UI/UE

黑白棋算法简单实现与基于Qt的GUI编程的综合应用

2017-06-28 11:42 1171 查看

一、序言:

最近学习了Qt的界面编程,包括了QObject、QWidget、QIODevice、QMessageBox、QTcpSockt、QTcpServer、QFile、QFileInfo、QDataStream、QTextStream、QBuff、QPixmap、QImage、QBitmap、QPicture、QTimer定时器…等基本类的使用;基本事件的处理与过滤的学习、信号与槽的学习、QCreator的基本空控件与自定义控件的提升、Lamda表达式在Qt中代替槽函数的定义。还学习了Qt中多线程的使用,以及SQLite、MySql数据库在Qt中的应用,还有XML的简单使用…感觉学的不多,说起来似乎又说不完,但完全消化记在脑子里的是少之又少(接口过多,重要的还是要熟悉基本的事件处理、信号与槽机制、UI界面的基本控件)。

完成后做了一个带数据库的简单的汽车销售系统,但是由于水平有限写的不够完美,而且数据库的操作不够熟练,不打算在博客上写出来了。接着找了一个黑白棋的小游戏的教程,花了两天时间进行了算法到代码的设计、编写以及优化,今天作以总结,上半年的编程学习算是告一段落,接下来要开始为期四天的苦逼的光电子学复习(学习)工作。

二、游戏规则介绍

最早玩黑白棋这个游戏的时候是刚上初中,那会儿拿着学习机打游戏、一个人在宿舍看着《鬼吹灯之黄皮子坟》…现在还记忆犹新(又扯远了)。黑白棋的规则很简单,有N*N的棋盘(比如说8*8=64格,10*10=100格…)也就是和围棋棋盘相似,棋子也和围棋相似:有黑白两种颜色的棋子(对弈双方各执一种颜色),最开始未下棋时,棋盘中间有四个棋子,双方各两个,如下图所示(我用“滑稽”的表情代表黑子,用另一个表情代表白子):



规则:

1、落子(点击有效)规则:

在方格内点击,但是点击后要满足两个条件方可下子,否则点击无效:

①鼠标点击的方格其紧邻的八个方向必须至少有一个方格已经存在棋子,即“上、下、左、右、左上、右上、左下、右下”(也可称为周围)的八个方向。比如最开始不能点击左上角,因为其周围无棋子。

②满足条件①以后,每次点击必须产生吃子,否则即使点击的方格周围有棋子,点击也无效。那么怎么就算能吃子,怎么就吃不了?

2、吃子规则:

如果在任意一个贯同(连通)的直线方向,即有水平、垂直、左上到右下的对角线、左下到右上对角线四种直线方向,如下图:



则吃子只能存在一种情况,即夹击与被夹击的状态:

点击点所在垂直线/水平线/对角线上满足同类型棋子夹击对方棋子。则被夹击的棋子完全被转化成夹击方的棋子。

拿初始化的中间四子来说,白子先行。则白子只有四种落子方式:

①落在左上角黑子的左边

②落在左上角黑子的上方

③落在右下角黑子的下方

④落在右下角黑子的右方

对应的白子落子后的局势变化则为下图所示:



轮到黑方落子时,依旧遵循四条线上夹击则吃子的规则(这四种情况黑方均有三种落子方式选择),以此类推。如果轮到某一方,但是该拥有落子权的一方不能落子时(即没有落子的方式能够产生夹击的结果,无法吃子)则跳过本回合,落子权交给另一方。若落子权从无吃子的一方转让回来时,但是此时自己也不能落子,即双方此时都不存在能够夹击的落子方法,而棋盘未下满,则对弈提前结束,否则棋盘落满对弈结束。统计结束时双方在棋盘上的棋子数目,多者获胜,棋子数目相同则为平局。

这样的落子规则与吃子规则,就决定了棋盘初始时刻应该有四个棋子:两黑两白。

三、基本的设计思路与重要的算法剖析:

1、从鼠标点击到规则实现:

知道了落子与吃子规则,则其基本思路基本也一目了然了,我们先来看鼠标点击与点击有效与否的处理流程分析(非标准流程图,只是叙述基本思路):



这只是一个大致思路,比较粗糙,编程流程会更为细致,但是我们先来看二维数组是如何进行每一个鼠标点击点与棋子状态的表示,状态是如何变化的。鼠标点击的事件处理代码如下所示:

void Widget::mousePressEvent(QMouseEvent *ev)
{
if(ev->button() == Qt::LeftButton){
p = ev->globalPos() - this->frameGeometry().topLeft();
}

int i = 0, j = 0;
i = ev->x() - startPoint.x();
j = ev->y() - startPoint.y();

if(i >= 0 && i <= 8*gridWidth && j >= 0 && j <= 8*gridHigh){
//点击点在正确的棋盘内
i = i/gridWidth;
j = j/gridHigh;

if( judgeRole(i,j,role) > 0 ){//判断点击点是否满足吃子规则,大于0则满足
whiteFlag = true;//落子成功则将白子标志位置为true
changeRole();//更换角色,落子权转让
update();//更新绘图
}
}
}


注意到,其中有一个whiteFlag标志位,其作用是:当有一方不能落子(吃子)时,落子权转让给对方,但是对方也无法落子。导致了changeRole()函数的无休止的调用,最后会导致函数栈溢出。所以为了避免这种情况的发生,我们要用标志位whiteFlag和blackFlag标记此次落子是否是对方未落子而进行的落子转让,若是,经过一个回合的转让,标志位均被置为false,而每次鼠标点击事件中/机器下子前都要进行标志位的判断,如果两个标志位均为false,则游戏提前结束。

//判断能落子的最大值
int num = 0,max = 0;
for(int i = 0; i<8; i++){
for(int j = 0; j<8; j++){//判断能吃的位置
if( (num = judgeRole(i, j, role, false)) > max){//寻找最多吃子的位置
max = num;
}
}
}

//最大吃0个子,即不能落子,则直接转让落子权或直接判断胜负
if(max == 0){
if(blackFlag | whiteFlag){//有一个为true则还可以进行落子权转让
//宏函数用来对落子权进行判断与修改
WHITE_FLAG();
BLACK_FLAG();
#if 0
#define WHITE_FLAG() (role == White) ? (whiteFlag = false) : true
#define BLACK_FLAG() (role == Black) ? (blackFlag = false) : true
#endif
changeRole();
update();
}
else{//均为false则直接结束
if(overFlag == false){//overFlag为true则说明游戏结束的QMessageBox已经弹出过了就不需再次弹出
overFlag = true;
//修改标志位并弹出结果提示信息,在函数返回时另一个gameOver()就不会执行了
gameOver();
}
}
}


overFlag标志位是为了避免,程序中多处对游戏结束的判断导致的QMessageBox::about()的弹窗多次调用,弹出多次则用户体验极差。

2、吃子的算法分析:

我们上面的代码中有体现的算法实现即judgeRole()函数。该函数原型如下:

int judgeRole(int x, int y, ChessFlag currentRole, bool eatChess = true);


参数(x,y)作为点击点所在方格处于二维数组的下标,currentRole是ChessFlag类型的枚举值,标记(x,y)的方格棋子状态,eatChess则是用来标记是要进行吃子操作(调用完毕后二维数组状态会被修改,绘图事件根据更新的二维数组来更新棋盘),还是要进行是否能吃子的判断操作(二维数组状态不会被修改,只返回能吃子最多的个数,返回0则表示不能吃子)。

//用枚举标记黑白棋状态
enum ChessFlag{Empty,Black,White};


下来我们就来具体分析该吃子/判断是否可以吃子的算法:

我们知道,要想能够吃子,则需要判断夹击方与被夹击方的关系,但是对于点击的一个格子来说,有8个方向需要去一一判断,判断该方向是能够吃子,并根据判断的结果修改二维数组。

以下面的局势为例分析:



此时轮到白棋(White)落子,若白棋落到A格,则:

①先分别(用for(int i=0; i<8; i++)进行八个方向的一次判断)判断其八个方向是否都有棋子,其中CDE三个方向存在棋子,BIHGF五个方向均不存在棋子,则首先满足落子规则。

②由于BIHGF五个格子不满足吃子规则(吃子首先得该方向紧邻的格子有一个对方的棋子),所以在这五个方向判断时直接就结束,进行下一个方向的判断。

③在CDE方向中,由于存在对方棋子,所以要进行该方向是否还存在己方棋子的判断,我们可以一眼看出存在,即这三个方向的CDE棋子以及A点空白均会被修改为白棋,绘图事件将重新画出吃子结果:



④但是计算机不能一眼看出,就需要用算法逻辑来实现:首先对该方向(以向左为例)进行遍历,如果便利过程中遇到与落子方棋子属性状态相同的棋子,则将起点A点的属性从Empty置为role(role为全局变量标记当前落子方棋子属性ChessFlag),此时role为White,即二维数组中A的位置被置为White。接着向回倒退,在倒退的过程中遇到的均是对方的棋子(Black),将其状态均修改为White并统计个数,当回到A点以后则该方向判断并修改状态的工作完毕,开始进行下一个方向的判断,八个方向判断完毕,则一次落子到吃子的数组状态改变也就完毕(注意:若函数参数列表传递的eatChess传的是false,A的状态不修改依旧为Empty,且向回倒退的过程不修改状态只统计个数,并将最终的个数返回)。如下图所示(左边只有C一个,八个方向统计CDE共3个):



代码如下(对照分析来看):

int Widget::judgeRole(int x, int y, ChessFlag currentRole, bool eatChess)
{
//落子的八个方向:x=-1表示左方,y=-1表示上方,x、y均为-1则是左上方,依次类推
int dir[8][2] = {{1,0},{1,-1},{0,-1},{-1,-1},{-1,0},{-1,1},{0,1},{1,1}};
int tmpX = x,tmpY = y;              //临时保存棋盘坐标位置(如上面分析中的A位置)
int i = 0, eatNum = 0;      //初始化数据,eatNum用来统计可吃子的个数,不能吃则最终返回0

if(chessStatus[tmpX][tmpY] != Empty){//如果此方格中已经有棋子无效操作,直接返回
return 0;
}

//棋盘的8个方向
for(i=0; i<8; i++){
//从鼠标点击点开始
tmpX = x;
tmpY = y;
//确定一个方向:dir[i][0]
tmpX += dir[i][0];
tmpY += dir[i][1];

if(tmpX < GRID_NUMBER && tmpX >= 0 && tmpY < GRID_NUMBER && tmpY >= 0
&& (chessStatus[tmpX][tmpY] != currentRole) && (chessStatus[tmpX][tmpY] != Empty)){

//如果没有出界并且相邻棋子是对方棋子才有可能吃子
tmpX += dir[i][0];
tmpY += dir[i][1];//向前走一步,开始判断该方向还有无自己棋子

while(tmpX < GRID_NUMBER && tmpX >= 0 && tmpY < GRID_NUMBER && tmpY >= 0){
if(chessStatus[tmpX][tmpY] == Empty){
break;//遇到空位跳出循环,外部if最后一个条件也会不满足,则直接判断下一个方向
}
if(chessStatus[tmpX][tmpY] == currentRole){//找到自己的棋子代表可以吃子
//能吃子则点击点标记为自己的棋子,update后是自己的棋子,否则点击处不能落子
(true == eatChess) ? (chessStatus[x][y] = currentRole) : true;//为false则不修改状态
tmpX -= dir[i][0];
tmpY -= dir[i][1];//回退一步开始吃子
//没有回到开始的位置就继续执行
while((tmpX != x) || (tmpY != y)){//没有回退到点击处则继续修改原有棋子状态
//若为true则为落子,修改为自己的棋子,如果为false则为测试,不用修改
(true == eatChess) ? (chessStatus[tmpX][tmpY] = currentRole) : true;
tmpX -= dir[i][0];
tmpY -= dir[i][1];//回退一格,继续
eatNum++;   //吃子累计
}
break;//跳出循环,结束该方向的判断
}
//没找到自己的棋子就向前(指定方向)走一步,走到0或GRID_NUMBER边界条件时就结束该层if语句
tmpX += dir[i][0];
tmpY += dir[i][1];
}
}
}
return eatNum;
}


3、人机中机器自动落子的简单分析:

机器落子的方法其实也是先要判断能否落子,并得知在何处落子更好(针对粗略的算法来说:遍历所有可落子的方格用judgeRole(i, j, role, false)获取最多吃子位置,该算法不是最好的,因为实战中要尽量占据四个角,因为四个角只要占据就永远不会被吃掉,还可以轻松吃掉对方)。如果上面的算法分析清楚了,机器下子则没有什么难度了。代码如下:

void Widget::machinePlay()
{
timer.stop();//定时器停止,定时器的作用是防止计算机下子过快,做的一个1s的延时

int max = 0, num = 0;
int px = 0, py = 0;

//判断能落子的最大值
for(int i = 0; i<8; i++){
for(int j = 0; j<8; j++){
//判断能吃的位置,机器为黑子
if( num = judgeRole(i, j, role, false) > max){//寻找最多吃子的位置
max = num;
px = i;//记录数组坐标
py = j;
}
}
}
if(max == 0){//max=0,最多能吃0个子,也就是无法落子
if((blackFlag | whiteFlag)){
WHITE_FLAG();
BLACK_FLAG();
changeRole();
return;
}
else{
if(overFlag == false){
overFlag = true;
gameOver();
}
}
}

//吃子
if(judgeRole(px, py, role) > 0){
blackFlag = true;   //机器落子则将黑子标志位置为true
changeRole();
update();
}
}


四、代码实现与UI布局:

工程文件:黑白棋游戏代码

(开始本学期的复习生活,愿我光电子学、光学设计、光电系统嵌入式高分飘过!!~~)
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  qt 编程 算法 gui 界面