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
5
6
7
8
9
10
11
12
13
14
15
16
//绘制棋子
for( int i=0;i< kbBlockSize+ 1;i++){
for(int j=0;j<kbBlockSize + 1;j++){
if(boardMap[i][j]==1){
painter.setBrush(Qt::white);
painter.drawEllipse(kbMargin + kbBlockLength * j - kbRadius ,
kbMargin + kbBlockLength * i - kbRadius,
kbRadius * 2, kbRadius *2);
}else if(boardMap[i][j]==-1){
painter.setBrush(Qt::black);
painter.drawEllipse(kbMargin + kbBlockLength * j - kbRadius ,
kbMargin + kbBlockLength * i - kbRadius,
kbRadius * 2, kbRadius *2);
}
}
}

落子

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

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

胜负

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

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
bool FIRWidget::isWin(int row, int col)
{
//需要判断横/竖/斜上/斜下
//水平方向
for(int i=0;i<5;i++){
if(col -i > 0 &&
col -i + 4 < kbBlockSize+1 &&
boardMap[row][col -i]== boardMap[row][col - i +1] &&
boardMap[row][col -i]== boardMap[row][col - i +2] &&
boardMap[row][col -i]== boardMap[row][col - i +3] &&
boardMap[row][col -i]== boardMap[row][col - i +4] )
return true;
}

//竖直方向
for(int i=0;i<5;i++){
if(row -i > 0 &&
row -i + 4 < kbBlockSize+1 &&
boardMap[row-i][col]== boardMap[row - i + 1][col] &&
boardMap[row-i][col]== boardMap[row - i + 2][col] &&
boardMap[row-i][col]== boardMap[row - i + 3][col] &&
boardMap[row-i][col]== boardMap[row - i + 4][col] )
return true;
}

// 左斜方向
for (int i = 0; i < 5; i++){
if (row + i < kbBlockSize+1 &&
(row + i - 4 > 0) &&
(col - i > 0) &&
col - i + 4 < kbBlockSize+1 &&
boardMap[row + i][col - i] == boardMap[row + i - 1][col - i + 1] &&
boardMap[row + i][col - i] == boardMap[row + i - 2][col - i + 2] &&
boardMap[row + i][col - i] == boardMap[row + i - 3][col - i + 3] &&
boardMap[row + i][col - i] == boardMap[row + i - 4][col - i + 4])
return true;
}

// 右斜方向
for (int i = 0; i < 5; i++){
if (row - i > 0 &&
row - i + 4 < kbBlockSize+1 &&
col - i > 0 &&
col - i + 4 < kbBlockSize+1 &&
boardMap[row - i][col - i] == boardMap[row - i + 1][col - i + 1] &&
boardMap[row - i][col - i] == boardMap[row - i + 2][col - i + 2] &&
boardMap[row - i][col - i] == boardMap[row - i + 3][col - i + 3] &&
boardMap[row - i][col - i] == boardMap[row - i + 4][col - i + 4])
return true;
}

return false;
}

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

win

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

人机对战

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

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

ui


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