五子棋(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)
运行显示空白界面
棋盘方格边长为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); }
效果为
指示当鼠标在棋盘上移动时,我们使用一个小点来指示,表示当前位置有效可以落点,当然,已经有棋子的点需要过滤掉。
那么如何过滤呢?首先棋盘最先联想到的就是二维数组了,数组的每个元素保存一个值,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) { 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 (); }
如图所示,箭头为鼠标,左侧为空白。
最左侧表示棋盘左侧的空白,
col/row就是A所在点,leftTopPosX/leftTopPosY表示点B,计算1是B与箭头距离,计算2是C与箭头距离,计算3是D与箭头距离,计算4是E与箭头距离
以上算法成立的条件是空白距离比格子边长小,如果比他大算法不一样。
效果为
自动绘制当前棋盘中所有棋子
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 (); } }
胜负判断胜负就是五子连线,落子之后就判断当前子是否满足连线状态
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 ; }
胜负平局判断完成之后提出提示信息并重置所有变量
当然还有一种情况就是平局,只需要在判断输赢之后判断棋盘是否填满就可以了。
人机对战双人对战是基础,但是不是所有人都有人可以对战,接下来我们看看人机对战
在双人对战的基础上添加一些控件,设置玩家使用白子还是黑子,玩家先走还是后走,AI的难度等级等等。