实现要求

这个程序是一个基于Qt的五子棋游戏,实现了人机对战和双人对战两种游戏模式,提供了基本的棋谱记录和游戏音效功能。程序采用了MVC模式,将数据与界面分离,实现了代码的模块化和可扩展性。主要构建包括三个文件,分别为GameModel.h、mainwindow.h和mainwindow.cpp,它们分别实现了游戏逻辑、游戏界面和界面与逻辑的交互。其中,游戏逻辑主要实现在GameModel类中,包括游戏规则、棋盘的数据结构和游戏状态的维护等;游戏界面主要实现在mainwindow.h和mainwindow.cpp中,包括棋盘的绘制、交互事件的处理、音效的播放和游戏结果的显示等。用户可以通过鼠标点击棋盘落下棋子,在游戏中实时查看落子信息,并通过音效提示,游戏结束后弹出对话框。

实现思路

  1. 定义GameModel类,包括游戏规则、棋盘的数据结构和游戏状态的维护等。
  2. 定义MainWindow类,主要用于具体实现游戏界面,包含了一些私有成员变量和函数,如棋盘、鼠标事件的处理、游戏模式切换等。
  3. 在mainwindow.cpp中实现棋盘的绘制、交互事件的处理和音效的播放等功能。
  4. 在GameModel.cpp中实现游戏开始、更新游戏地图、人类和AI的行动、计算得分、判断胜利和平局等游戏逻辑。
  5. 在main.cpp中创建应用程序对象,创建和显示主窗口,启动事件循环。
  6. 结合信号和槽机制,实现游戏界面与游戏逻辑的交互。
  7. 提供基本的棋谱记录和游戏音效功能,使用户在游戏中获得更好的游戏体验。

难点

双人对战只需要判断是否有五个棋子连成线即可判断对局胜负。人机对战相对复杂,需要实现AI下棋的功能。
电脑下棋的思路是通过计算每个空位 AI 下该棋子后的得分,来决定下哪个位置的棋子。得分高的位置就是AI应该下的最佳位置。

当AI需要决定下一步走哪个位置时,它会考虑每个空白位置对游戏胜利的贡献程度,评分函数就是用来计算每个空白位置的得分。

评分函数的计算方式是,在棋盘上遍历每个空白位置,然后从这个位置出发向上下左右、左上到右下、右上到左下三个方向扩展,统计这些方向上连续出现的X或O的个数。对于每个空白位置,分别计算其在三个方向上的得分,然后将三个方向的得分相加,即可得到该位置的总得分。

整体功能架构

文件名称功能描述
GameModel.h定义了游戏模型类GameModel,包括游戏的基本信息和方法,如棋盘、得分地图、玩家标志、游戏状态、游戏类型和各种操作方法等
mainwindow.h定义了MainWindow类,使用继承自QMainWindow类的方式,主要用于具体实现游戏界面,包含了一些私有成员变量和函数,如棋盘、鼠标事件的处理、游戏模式切换等
GameModel.cpp实现了GameModel类的方法函数,如游戏开始、更新游戏地图、人类和AI的行动、计算得分、判断胜利和平局等
mainwindow.cpp实现了游戏界面的方法函数,包括开始游戏、游戏模式切换、棋盘的绘制、鼠标事件的响应、音效的播放、游戏结果的显示等
main.cpp程序入口文件,创建应用程序对象,创建和显示主窗口,启动事件循环

代码实现

GameModel.h

#ifndef GAMEMODEL_H#define GAMEMODEL_H#include #include enum GameType{person,bot};enum GameStatus{playing,win,dead};const int kBoardSize=15;class GameModel{public:GameModel();public:std::vector<std::vector<int>> gameMap;std::vector<std::vector<int>> scoreMap;bool playerFlag;GameType gameType;GameStatus gameStatus;void startGame(GameType type);void calculateScore();void actionByPerson(int row,int col);void actionByAi(int &row,int &col);void updateMap(int row,int col);bool isWin(int row,int col);bool isDeadGame();signals:};#endif // GAMEMODEL_H

该文件是一个头文件,定义了一个游戏模型类 GameModel,包含一些游戏的基本信息和方法,如游戏地图、得分地图、玩家标志、游戏状态、游戏类型、开始游戏、计算分数、人类玩家行动、计算AI玩家行动、更新地图、判断游戏是否胜利、判断游戏是否结束等。同时包含了两个枚举类型 GameType 和 GameStatus,分别表示游戏类型和游戏状态。

mainwindow.h

#ifndef MAINWINDOW_H#define MAINWINDOW_H#include #include "GameModel.h"QT_BEGIN_NAMESPACEnamespace Ui { class MainWindow; }QT_END_NAMESPACEclass MainWindow : public QMainWindow{Q_OBJECTpublic:MainWindow(QWidget *parent = nullptr);~MainWindow();protected:void paintEvent(QPaintEvent *event);void mouseMoveEvent(QMouseEvent *event);void mouseReleaseEvent(QMouseEvent *event);private:GameModel *game;GameType game_type;int clickPosRow,clickPosCol;void initGame();void checkGame(int y,int x);private slots:void chessOneByPerson();void chessOneByAi();void initPVPGame();void initPVCGame();};#endif // MAINWINDOW_H

这是一个名为mainwindow.h的头文件,其中定义了一个MainWindow类,该类继承自QMainWindow类。该类主要用于实现一个游戏,包括绘制棋盘,响应鼠标事件等功能。在该文件中还定义了一些私有函数和私有变量,包括一个GameModel类型的指针game,一个GameType类型的game_type,以及一些与游戏操作相关的函数,例如initGame、checkGame、chessOneByPerson、chessOneByAi等。此外,还定义了一些槽函数,例如initPVPGame和initPVCGame等,用于处理不同的游戏模式。

main.cpp

#include "mainwindow.h"#include int main(int argc, char *argv[]){QApplication a(argc, argv);MainWindow w;w.show();return a.exec();}

此程序文件名为main.cpp,是一个Qt程序的主函数文件。该程序使用了Qt中的MainWindow类作为主窗口,通过调用show函数显示该窗口,然后进入Qt程序的事件循环。此程序中主要功能由MainWindow类实现,通过该类的构造函数和成员函数完成窗口的初始化、显示和其他操作。主函数的作用是创建应用程序对象,然后创建和显示主窗口,最后启动事件循环。整个程序的作用是显示一个Qt窗口,供用户进行交互操作。

GameModel.cpp

#include "GameModel.h"#include #include GameModel::GameModel(){}void GameModel::startGame(GameType type){gameType = type;gameMap.clear();for(int i=0;i<kBoardSize;i++){std::vector<int> lineBoard;for(int j=0;j<kBoardSize;j++){lineBoard.push_back(0);}gameMap.push_back(lineBoard);}if(gameType==bot){scoreMap.clear();for(int i=0;i<kBoardSize;i++){std::vector<int> lineScores;for(int j=0;j<kBoardSize;j++){lineScores.push_back(0);}scoreMap.push_back(lineScores);}}playerFlag=true;}void GameModel::updateMap(int row, int col){if(playerFlag)gameMap[row][col]=1;elsegameMap[row][col]=-1;playerFlag=!playerFlag;}void GameModel::actionByPerson(int row, int col){updateMap(row,col);}void GameModel::actionByAi(int &crow, int &ccol){calculateScore();int maxScore=0;std::vector<std::pair<int,int>> maxPoint;for(int i=1;i<kBoardSize;i++)for(int j=1;j<kBoardSize;j++){if(gameMap[i][j]==0){if(scoreMap[i][j]>maxScore){maxPoint.clear();maxScore=scoreMap[i][j];maxPoint.push_back(std::make_pair(i,j));}else if(scoreMap[i][j]==maxScore)maxPoint.push_back(std::make_pair(i,j));}}srand((unsigned)time(0));int index=rand()%maxPoint.size();std::pair<int,int> pointPair=maxPoint.at(index);crow=pointPair.first;ccol=pointPair.second;updateMap(crow,ccol);}void GameModel::calculateScore(){int personNum=0;int botNum=0;int emptyNum=0;scoreMap.clear();for(int i=0;i<kBoardSize;i++){std::vector<int> lineScore;for(int j=0;j<kBoardSize;j++)lineScore.push_back(0);scoreMap.push_back(lineScore);}for(int row=0;row<kBoardSize;row++)for(int col=0;col<kBoardSize;col++){if(row>0&&col>0&&gameMap[row][col]==0){for(int y=-1;y<=1;y++)for(int x=-1;x<=1;x++){int personNum=0;int botNum=0;int emptyNum=0;if(!(y==0&&x==0)){for(int i=1;i<=4;i++){if(row+i*y>0&&row+i*y<kBoardSize&&col+i*x>0&&col+i*x<kBoardSize&&gameMap[row+i*y][col+i*x]==1){personNum++;}else if(row+i*y>0&&row+i*y<kBoardSize&&col+i*x>0&&col+i*x<kBoardSize&&gameMap[row+i*y][col+i*x]==0){emptyNum++;break;}elsebreak;}for(int i=1;i<=4;i++){if(row-i*y>0&&row-i*y<kBoardSize&&col-i*x>0&&col-i*x<kBoardSize&&gameMap[row-i*y][col-i*x]==1){personNum++;}else if(row-i*y>0&&row-i*y<kBoardSize&&col-i*x>0&&col-i*x<kBoardSize&&gameMap[row-i*y][col-i*x]==0){emptyNum++;break;}elsebreak;}if(personNum==1)scoreMap[row][col]+=10;else if(personNum==2){if(emptyNum==1)scoreMap[row][col]+=30;else if(emptyNum==2)scoreMap[row][col]+=40;}else if(personNum==3){if(emptyNum==1)scoreMap[row][col]+=60;else if(emptyNum==2)scoreMap[row][col]+=110;}else if(personNum==4)scoreMap[row][col]+=10000;emptyNum=0;for(int i=1;i<=4;i++){if(row+i*y>0&&row+i*y<kBoardSize&&col+i*x>0&&col+i*x<kBoardSize&&gameMap[row+i*y][col+i*x]==1){botNum++;}else if(row+i*y>0&&row+i*y<kBoardSize&&col+i*x>0&&col+i*x<kBoardSize&&gameMap[row+i*y][col+i*x]==0){emptyNum++;break;}elsebreak;}for(int i=1;i<=4;i++){if(row-i*y>0&&row-i*y<kBoardSize&&col-i*x>0&&col-i*x<kBoardSize&&gameMap[row-i*y][col-i*x]==-1){ botNum++;}else if(row-i*y>0&&row-i*y<kBoardSize&&col-i*x>0&&col-i*x<kBoardSize&&gameMap[row-i*y][col-i*x]==0){emptyNum++;break;}elsebreak;}if(botNum==0)scoreMap[row][col]+=5;else if(botNum==1)scoreMap[row][col]+=10;else if(botNum==2){if(emptyNum==1)scoreMap[row][col]+=25;else if(emptyNum==2)scoreMap[row][col]+=50;}else if(botNum==3){if(emptyNum==1)scoreMap[row][col]+=50;else if(emptyNum==2)scoreMap[row][col]+=110;}else if(botNum==4)scoreMap[row][col]+=10000;}}}}}bool GameModel::isWin(int row, int col){for(int i=0;i<5;i++){if(col-i>0&&col-i+4<kBoardSize&&gameMap[row][col-i]==gameMap[row][col-i+1]&&gameMap[row][col-i]==gameMap[row][col-i+2]&&gameMap[row][col-i]==gameMap[row][col-i+3]&&gameMap[row][col-i]==gameMap[row][col-i+4])return true;}for(int i=0;i<5;i++){if(row-i>0&&row-i+4<kBoardSize&&gameMap[row-i][col]==gameMap[row-i+1][col]&&gameMap[row-i][col]==gameMap[row-i+2][col]&&gameMap[row-i][col]==gameMap[row-i+3][col]&&gameMap[row-i][col]==gameMap[row-i+4][col])return true;}for(int i=0;i<5;i++){if(row+i<kBoardSize&&row+i-4>0&&col-i>0&&col-i+4<kBoardSize&&gameMap[row+i][col-i]==gameMap[row+i-1][col-i+1]&&gameMap[row+i][col-i]==gameMap[row+i-2][col-i+2]&&gameMap[row+i][col-i]==gameMap[row+i-3][col-i+3]&&gameMap[row+i][col-i]==gameMap[row+i-4][col-i+4])return true;}for(int i=0;i<5;i++){if(row-i>0&&row-i+4<kBoardSize&&col-i>0&&col-i+4<kBoardSize&&gameMap[row-i][col-i]==gameMap[row-i+1][col-i+1]&&gameMap[row-i][col-i]==gameMap[row-i+2][col-i+2]&&gameMap[row-i][col-i]==gameMap[row-i+3][col-i+3]&&gameMap[row-i][col-i]==gameMap[row-i+4][col-i+4])return true;}return false;}bool GameModel::isDeadGame(){for(int i=1;i<kBoardSize;i++)for(int j=1;j<kBoardSize;j++){if(!(gameMap[i][j]==1||gameMap[i][j]==-1))return false;}return true;}

该程序文件是一个游戏模型的实现,其中包含了游戏开始、更新游戏地图、人类和AI的行动、计算得分、判断胜利和平局等功能。程序使用了C++语言,其中定义了常量kBoardSize表示棋盘的大小。程序中的gameMap代表游戏地图,scoreMap代表每个空位的得分,playerFlag代表玩家的标记(true表示玩家为黑棋,false表示玩家为白棋),gameType代表游戏类型(bot表示人机对战)。在startGame()中初始化gameMap和scoreMap,并根据游戏类型初始化玩家标记。在actionByPerson()和actionByAi()中分别更新gameMap,并在actionByAi()中使用calculateScore()计算每个空位的得分,再根据得分为AI选择最好的空位进行行动。在calculateScore()中,分别计算空位周围读和AI已下的棋子形成的连续棋子数,并将其转换为得分,存储在scoreMap中。在isWin()中,根据五子棋规则判断某个空位是否胜利。在isDeadGame()中,判断游戏是否为平局。

mainwindow.cpp

#include "mainwindow.h"#include #include #include #include #include #include #include #include #include #define CHESS_ONE_SOUND ":/res/chessone.wav"#define WIN_SOUND ":/res/win.wav"#define LOSE_SOUND ":/res/lose.wav"const int kBoardMargin = 30; // 棋盘边缘空隙const int kRadius = 15; // 棋子半径const int kMarkSize = 6; // 落子标记边长const int kBlockSize = 40; // 格子的大小const int kPosDelta = 20; // 鼠标点击的模糊距离上限const int kAIDelay = 700; // AI下棋的思考时间MainWindow::MainWindow(QWidget *parent): QMainWindow(parent){//ui->setupUi(this);setFixedSize(kBoardMargin*2+kBlockSize*kBoardSize,kBoardMargin*2+kBlockSize*kBoardSize);//setStyleSheet("background-color:white;");//setStyleSheet("background-color:transparent;");setMouseTracking(true);QMenu *gameMenu = menuBar()->addMenu(tr("Game Model:"));QAction *actionPVP = new QAction("PVP",this);connect(actionPVP,SIGNAL(triggered()),this,SLOT(initPVPGame()));menuBar()->addAction(actionPVP);//gameMenu->addAction(actionPVP);QAction *actionPVC = new QAction("PVC",this);connect(actionPVC,SIGNAL(triggered()),this,SLOT(initPVCGame()));menuBar()->addAction(actionPVC);//gameMenu->addAction(actionPVC);initGame();}MainWindow::~MainWindow(){if(game){delete game;game=nullptr;}}void MainWindow::initGame(){game = new GameModel;initPVPGame();}void MainWindow::initPVPGame(){game_type=person;game->gameStatus=playing;game->startGame(game_type);update();}void MainWindow::initPVCGame(){game_type=bot;game->gameStatus=playing;game->startGame(game_type);update();}void MainWindow::paintEvent(QPaintEvent *event){QPainter painter(this);painter.setRenderHint(QPainter::Antialiasing,true);for(int i=0;i<kBoardSize+1;i++){painter.drawLine(kBoardMargin+kBlockSize*i,kBoardMargin,kBoardMargin+kBlockSize*i,size().height()-kBoardMargin);painter.drawLine(kBoardMargin,kBoardMargin+kBlockSize*i,size().width()-kBoardMargin,kBoardMargin+kBlockSize*i);}QBrush brush;brush.setStyle(Qt::SolidPattern);if(clickPosRow>0&&clickPosRow<kBoardSize&&clickPosCol>0&&clickPosCol<kBoardSize&&game->gameMap[clickPosRow][clickPosCol]==0){if(game->playerFlag)brush.setColor(Qt::white);elsebrush.setColor(Qt::black);painter.setBrush(brush);painter.drawRect(kBoardMargin+kBlockSize*clickPosCol-kMarkSize/2,kBoardMargin+kBlockSize*clickPosRow-kMarkSize/2,kMarkSize,kMarkSize);}for(int i=0;i<kBoardSize;i++)for(int j=0;j<kBoardSize;j++){if(game->gameMap[i][j]==1){brush.setColor(Qt::white);painter.setBrush(brush);painter.drawEllipse(kBoardMargin+kBlockSize*j-kRadius,kBoardMargin+kBlockSize*i-kRadius,kRadius*2,kRadius*2);}else if(game->gameMap[i][j]==-1){brush.setColor(Qt::black);painter.setBrush(brush);painter.drawEllipse(kBoardMargin+kBlockSize*j-kRadius,kBoardMargin+kBlockSize*i-kRadius,kRadius*2,kRadius*2);}}if(clickPosRow>0&&clickPosRow<kBoardSize&&clickPosCol>0&&clickPosCol<kBoardSize&&(game->gameMap[clickPosRow][clickPosCol]==1||game->gameMap[clickPosRow][clickPosCol]==-1)){if(game->isWin(clickPosRow,clickPosCol)&&game->gameStatus==playing){qDebug()<<"win";game->gameStatus=win;QSound::play(WIN_SOUND);QString str;if(game->gameMap[clickPosRow][clickPosCol]==1)str="white player";else if(game->gameMap[clickPosRow][clickPosCol]==-1)str="black player";QMessageBox::StandardButton btnValue = QMessageBox::information(this,"congratulations",str+"win");if(btnValue==QMessageBox::Ok){game->startGame(game_type);game->gameStatus=playing;}}}if(game->isDeadGame()){QSound::play(LOSE_SOUND);QMessageBox::StandardButton btnValue=QMessageBox::information(this,"oops","dead game");if(btnValue==QMessageBox::Ok){game->startGame(game_type);game->gameStatus=playing;}}}void MainWindow::mouseMoveEvent(QMouseEvent *event){int x=event->x();int y=event->y();if(x>=kBoardMargin+kBlockSize/2&&x<size().width()-kBoardMargin&&y>=kBoardMargin+kBlockSize/2&&y<size().height()-kBoardMargin){int col=x/kBlockSize;int row=y/kBlockSize;int leftTopPosX=kBoardMargin+kBlockSize*col;int leftTopPosY=kBoardMargin+kBlockSize*row;clickPosRow=-1;clickPosCol=-1;int len=0;len=sqrt((x - leftTopPosX) * (x - leftTopPosX) + (y - leftTopPosY) * (y - leftTopPosY));if(len<kPosDelta){clickPosRow = row;clickPosCol = col;}len=sqrt((x - leftTopPosX - kBlockSize) * (x - leftTopPosX - kBlockSize) + (y - leftTopPosY) * (y - leftTopPosY));if (len < kPosDelta){clickPosRow = row ;clickPosCol = col + 1;}len=sqrt((x-leftTopPosX)*(x-leftTopPosX)+(y-leftTopPosY-kBlockSize)*(y-leftTopPosY-kBlockSize));if(len<kPosDelta){clickPosRow=row+1;clickPosCol=col;}len=sqrt((x-leftTopPosX-kBlockSize)*(x-leftTopPosX-kBlockSize)+(y-leftTopPosY-kBlockSize)*(y-leftTopPosY-kBlockSize));if(len<kPosDelta){clickPosRow=row+1;clickPosCol=col+1;}}update();}void MainWindow::mouseReleaseEvent(QMouseEvent *event){// 人下棋,并且不能抢机器的棋if (!(game_type == bot && !game->playerFlag)){chessOneByPerson();// 如果是人机模式,需要调用AI下棋if (game->gameType == bot && !game->playerFlag){// 用定时器做一个延迟QTimer::singleShot(kAIDelay, this, SLOT(chessOneByAi()));}}}void MainWindow::chessOneByPerson(){if(clickPosRow!=-1&&clickPosCol!=-1&&game->gameMap[clickPosRow][clickPosCol]==0){game->actionByPerson(clickPosRow,clickPosCol);QSound::play(CHESS_ONE_SOUND);update();}}void MainWindow::chessOneByAi(){game->actionByAi(clickPosRow,clickPosCol);QSound::play(CHESS_ONE_SOUND);update();}

该程序实现了一个围棋游戏的主窗口界面,可以通过菜单选择双人对战或人机对战,人机对战中可以调节AI下棋的思考时间。用户可以通过鼠标点击棋盘落下棋子,在游戏中实时查看落子信息,并通过音效提示,游戏结束后弹出对话框。程序采用了MVC模式,利用GameModel类管理游戏逻辑,将数据与界面分离,实现了代码的模块化和可扩展性。

程序运行结果

如下图所示:

打包发布

可以使用Enigma Virtual Box软件打包程序。
Enigma Virtual Box是一个用于将应用程序打包为单一可执行文件的工具。它将应用程序和所有相关文件打包到一个虚拟的可执行文件中,这个文件可以像一个普通的可执行文件一样运行,而不需要安装或配置任何依赖项。这样在其他计算机上程序也可以运行,实现可移植性。