一个基于Qt的五子棋游戏实现了人机对战和界面分离

一个基于Qt的五子棋游戏实现了人机对战和界面分离

实现要求

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

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

双人对战只需要判断是否有五个棋子连成线即可判断对局胜负。人机对战相对复杂,需要实现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_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE
class MainWindow : public QMainWindow
{
    Q_OBJECT
public:
    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等。此外,还定义了一些槽函数c 五子棋游戏设计,例如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;
    else
        gameMap[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;
                                }
                                else
                                    break;
                            }
                            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;
                                }
                                else
                                    break;
                            }
                            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;
                                }
                                else
                                    break;
                            }
                            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;
                                }
                                else
                                    break;
                            }
                            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);
        else
            brush.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是一个用于将应用程序打包为单一可执行文件的工具。它将应用程序和所有相关文件打包到一个虚拟的可执行文件中,这个文件可以像一个普通的可执行文件一样运行,而不需要安装或配置任何依赖项。这样在其他计算机上程序也可以运行,实现可移植性。

文章来源:https://blog.csdn.net/qq_44878985/article/details/130243063