实现要求
这个程序是一个基于Qt的五子棋游戏,实现了人机对战和双人对战两种游戏模式,提供了基本的棋谱记录和游戏音效功能。程序采用了MVC模式,将数据与界面分离,实现了代码的模块化和可扩展性。主要构建包括三个文件,分别为GameModel.h、mainwindow.h和mainwindow.cpp,它们分别实现了游戏逻辑、游戏界面和界面与逻辑的交互。其中,游戏逻辑主要实现在GameModel类中,包括游戏规则、棋盘的数据结构和游戏状态的维护等;游戏界面主要实现在mainwindow.h和mainwindow.cpp中,包括棋盘的绘制、交互事件的处理、音效的播放和游戏结果的显示等。用户可以通过鼠标点击棋盘落下棋子,在游戏中实时查看落子信息,并通过音效提示,游戏结束后弹出对话框。
实现思路
- 定义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 <QObject>
#include <vector>
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 <QMainWindow>
#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等。此外,还定义了一些槽函数,例如initPVPGame和initPVCGame等,用于处理不同的游戏模式。
main.cpp
#include "mainwindow.h"
#include <QApplication>
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 <utility>
#include <time.h>
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 <QPainter>
#include <QTimer>
#include <QSound>
#include <QMouseEvent>
#include <QMessageBox>
#include <QMenu>
#include <QMenuBar>
#include <QDebug>
#include <math.h>
#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类管理游戏逻辑,将数据与界面分离,实现了代码的模块化和可扩展性。
程序运行结果
如下图所示:
文章来源:https://www.toymoban.com/news/detail-418648.html
打包发布
可以使用Enigma Virtual Box软件打包程序。
Enigma Virtual Box是一个用于将应用程序打包为单一可执行文件的工具。它将应用程序和所有相关文件打包到一个虚拟的可执行文件中,这个文件可以像一个普通的可执行文件一样运行,而不需要安装或配置任何依赖项。这样在其他计算机上程序也可以运行,实现可移植性。文章来源地址https://www.toymoban.com/news/detail-418648.html
到了这里,关于【C++】Qt环境下的五子棋小程序的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!