Qt开发五子棋

五子棋(five in row,gobang,gomoku)在一个正方形棋盘上使用黑白两色棋子对局,以最先将5个同色棋子连成一条线者为胜(不论什么方向)

本文主要涉及界面相关,当然界面使用Qt开发

开发环境

戴尔G15 1511 i7-11800H 8核 16GB Manjaro stable

Qt 6.3.0 GCC 11.2.0

双人对战

首先创建一个带ui的Qt工程(新版本Qt中,官方舍弃了qmake,使用cmake)

project

运行显示空白界面

window

棋盘

方格边长为30像素,上下左右留20像素空白

1
2
3
4
5
6
7
//全局常量
const int kbMargin = 20;//棋盘边缘空白
const int kbRadius = 10;//棋子半径
const int kbMarkRadius = 6;//落子标记边长
const int kbBlockLength = 30;//格子大小
const int kbBlockSize = 20;//格子数量
const int kbPosDelta = 15;//鼠标点击模糊距离上限

绘制棋盘

1
2
3
4
5
6
7
8
9
10
11
QPainter painter(this);
painter.setPen(Qt::green);
painter.setRenderHint(QPainter::Antialiasing);

//绘制网格
for(int i=0;i<kbBlockSize+1;i++){
painter.drawLine(kbMargin + kbBlockLength * i, kbMargin ,
kbMargin + kbBlockLength * i, kbMargin + kbBlockLength * kbBlockSize);
painter.drawLine(kbMargin, kbMargin + kbBlockLength * i,
kbMargin + kbBlockLength * kbBlockSize , kbMargin + kbBlockLength * i);
}

效果为

board

指示

当鼠标在棋盘上移动时,我们使用一个小点来指示,表示当前位置有效可以落点,当然,已经有棋子的点需要过滤掉。

那么如何过滤呢?首先棋盘最先联想到的就是二维数组了,数组的每个元素保存一个值,1表示当前位置已经落白子,0表示当前位置还没有落子,-1表示当前位置已经落黑子。

1
2
3
4
5
6
7
8
9
//落子标志
if(clickPosRow >= 0 && clickPosRow < kbBlockSize+1 &&
clickPosCol >= 0 && clickPosCol < kbBlockSize+1 &&
boardMap[clickPosRow][clickPosCol]==0){
painter.setBrush(isWhitePlayer?Qt::white:Qt::black);
painter.drawRect(kbMargin + kbBlockLength * clickPosCol - kbMarkRadius /2,
kbMargin + kbBlockLength * clickPosRow - kbMarkRadius/2,
kbMarkRadius, kbMarkRadius);
}

那么问题来了,如何确定当前鼠标所在的行列呢?我们采用近似值,先使用鼠标坐标获得一个棋盘中的坐标,然后计算棋盘坐标右侧的四个点与鼠标点的距离,与鼠标模糊距离常量比较选出一个合适点作为指示点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58

void FIRWidget::mouseMoveEvent(QMouseEvent *event)
{
//通过鼠标的hover确定落子标记
int x = event->x();
int y = event->y();

clickPosCol = -1;
clickPosRow = -1;

//先把棋盘外的坐标过滤掉
if(x>= kbMargin &&
x <= width() - kbMargin &&
y >= kbMargin &&
y <= height() - kbMargin){
//获取最近的左上角的点
int col = x / kbBlockLength;
int row = y /kbBlockLength;

//计算得到棋盘中靠近点
int leftTopPosX = kbMargin + kbBlockLength * col;
int leftTopPosY = kbMargin + kbBlockLength * row;

int len = 0;//计算结果取整

//计算距离,根据半径选择
//最靠近点
len = sqrt(pow(x-leftTopPosX,2)+pow(y-leftTopPosY,2));
if(len < kbPosDelta){
clickPosRow = row;
clickPosCol = col;
}
//最靠近点水平右侧的点
len = sqrt(pow(x-leftTopPosX - kbBlockLength,2)+pow(y - leftTopPosY,2));
if (len < kbPosDelta)
{
clickPosRow = row;
clickPosCol = col + 1;
}
//最靠近点垂直下方的点
len = sqrt(pow(x - leftTopPosX,2) + pow(y - leftTopPosY - kbBlockLength,2) );
if (len < kbPosDelta)
{
clickPosRow = row + 1;
clickPosCol = col;
}
//最靠近点右斜下方点
len = sqrt(pow(x - leftTopPosX - kbBlockLength,2) + pow(y - leftTopPosY - kbBlockLength,2));
if (len < kbPosDelta)
{
clickPosRow = row + 1;
clickPosCol = col + 1;
}
}

//存了坐标后要重绘
update();
}

plot

如图所示,箭头为鼠标,左侧为空白。

最左侧表示棋盘左侧的空白,

col/row就是A所在点,leftTopPosX/leftTopPosY表示点B,计算1是B与箭头距离,计算2是C与箭头距离,计算3是D与箭头距离,计算4是E与箭头距离

以上算法成立的条件是空白距离比格子边长小,如果比他大算法不一样。

效果为

mark

落子

鼠标点击后进行落子,我们以鼠标松开事件为准

1
2
3
4
void FIRWidget::mouseReleaseEvent(QMouseEvent *event)
{
chessOneByPerson();
}

落子就简单了,根据游戏角色,设置数组值,每下一次子就切换游戏角色的状态

1
2
3
4
5
6
7
8
9
10
11
12
13
void FIRWidget::chessOneByPerson()
{
//根据当前存储的坐标下棋,坐标要有效
if(clickPosRow !=-1 && clickPosCol !=-1 && boardMap[clickPosRow][clickPosCol]==0){
boardMap[clickPosRow][clickPosCol]=isWhitePlayer?1:-1;
//判断是否结束
if(isWin(clickPosRow,clickPosCol)){
emit showMsg(isWhitePlayer?1:-1);
}
isWhitePlayer=!isWhitePlayer;
update();
}
}

chess

胜负

判断胜负就是五子连线,落子之后就判断当前子是否满足连线状态

胜负平局判断完成之后提出提示信息并重置所有变量

win

当然还有一种情况就是平局,只需要在判断输赢之后判断棋盘是否填满就可以了。

人机对战

双人对战是基础,但是不是所有人都有人可以对战,接下来我们看看人机对战

在双人对战的基础上添加一些控件,设置玩家使用白子还是黑子,玩家先走还是后走,AI的难度等级等等。

ui


Qt开发五子棋
https://feater.top/qt/development-of-qt-fir/
作者
JackeyLea
发布于
2022年5月19日
许可协议