学习视频链接
五子棋-3_bilibili_哔哩哔哩_bilibilihttps://www.bilibili.com/video/BV1ZK4y1G7Am?p=3&vd_source=0471cde1c644648fafd07b54e303c905
目录
一、基本框架
1.1 绘制棋盘和初始化
1.2 显示落点
1.3 落子功能
1.4 判断输赢
二、五子棋 Ai
2.1 Ai策略
2.2 代码
#ifndef GAMEMODEL_H
#define GAMEMODEL_H
#include
// 游戏类型,双人还是AI(目前固定让AI下黑子)
enum GameType
{
MAN, // 双人模式
AI // 人机对弈模式
};
// 游戏状态
enum GameStatus
{
PLAYING, // 游戏中
WIN, // 赢了
DEAD // 和棋
};
// 棋盘尺寸
const int BOARD_GRAD_SIZE = 15;
const int MARGIN = 30; // 棋盘边缘空隙
const int CHESS_PADTUS = 15; // 棋子半径
const int MARK_SIZE = 6; // 落子标记边长
const int BLOCK_SIZE = 40; // 格子的大小
const int POS_OFFSET = BLOCK_SIZE * 0.4; // 20 鼠标点击的模糊距离上限
const int AI_THINK_TIME = 700; // AI下棋思考时间
class GameModel
{
public:
GameModel();
~GameModel();
public:
// 存储当前游戏棋盘和棋子的情况,空白为0,黑子1,白子-1
std::vector> gameMapVec;
// 存储各个点位的评分情况,作为AI下棋依据
std::vector> scoreMapVec;
// 标示下棋方,true:黑棋方 false:AI白棋方
bool playerFlag;
GameType gameType; // 游戏模式:人机对弈,还是双人
GameStatus gameStatus; // 游戏状态
void startGame(GameType type); // 开始游戏
void calculateScore(); // 计算评分
void actionByPerson(int row, int col); // 人执行下棋
void actionByAI(int &clickRow, int &clickCol); // 机器执行下棋
void updateGameMap(int row, int col); // 每次落子后更新游戏棋盘
bool isWin(int row, int col); // 判断游戏是否胜利
bool isDeadGame(); // 判断是否和棋
};
#endif // GAMEMODEL_H
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include
#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();
void paintEvent(QPaintEvent *event);
void initGame();
void initAIGame();
private:
Ui::MainWindow *ui;
GameModel *game; // 游戏指针
GameType game_type; // 存储游戏类型
};
#endif // MAINWINDOW_H
#include "gamemodel.h"
GameModel::GameModel()
{
}
GameModel::~GameModel()
{
}
void GameModel::startGame(GameType type)
{
gameType = type;
//初始棋盘
gameMapVec.clear() ;
for(int i = 0; i < BOARD_GRAD_SIZE; i++)
{
std::vector lineBoard;
for(int j = 0; j < BOARD_GRAD_SIZE; j++)
lineBoard.push_back(0);
gameMapVec.push_back(lineBoard);
}
// 如果是AI模式, 需要初始化评分数组
if(gameType == AI)
{
scoreMapVec.clear() ;
for(int i = 0; i < BOARD_GRAD_SIZE; i++)
{
std::vector lineScores;
for(int j = 0; j < BOARD_GRAD_SIZE; j++)
lineScores.push_back(0);
scoreMapVec.push_back(lineScores);
}
}
// 轮到黑方下棋为true,白方为false
playerFlag = true ;
}
#include "mainwindow.h"
#include "ui_mainwindow.h"
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
setFixedSize(MARGIN * 2 + BLOCK_SIZE * BOARD_GRAD_SIZE, MARGIN * 2 + BLOCK_SIZE * BOARD_GRAD_SIZE);
initGame();
}
MainWindow::~MainWindow()
{
delete ui;
}
void MainWindow::paintEvent(QPaintEvent *event)
{
QPainter painter(this);
//绘制棋盘
painter.setRenderHint (QPainter::Antialiasing, true); // 抗锯齿
for (int i = 0; i < BOARD_GRAD_SIZE + 1; i++)
{
// 从左到右,第(i+1)条竖线
painter.drawLine(MARGIN + BLOCK_SIZE * i, MARGIN, MARGIN + BLOCK_SIZE * i, size().height() - MARGIN);
// 从上到下,第(i+1)条横线
painter.drawLine(MARGIN, MARGIN + BLOCK_SIZE * i, size().width() - MARGIN, MARGIN + BLOCK_SIZE * i);
}
}
void MainWindow::initGame()
{
game = new GameModel;
initAIGame();
}
void MainWindow::initAIGame()
{
game_type = AI;
game->gameStatus = PLAYING;
// 在数据模型中进行初始化功能
game->startGame(game_type);
update();
}
添加头文件 #include
重写鼠标移动事件方法
打开自动检测鼠标移动的按钮
给鼠标移动事件添加逻辑
1、准备需要的变量
2、检测是离哪个点最近
void MainWindow::mouseMoveEvent(QMouseEvent *event)
{
// 通过鼠标的位置确定落子的标记
int x = event->x();
int y = event->y();
// 棋盘边缘不能落子
if(x >= MARGIN + BLOCK_SIZE / 2 && x < size().width() - MARGIN - BLOCK_SIZE / 2 &&
y >= MARGIN + BLOCK_SIZE / 2 && y < size().height() - MARGIN - BLOCK_SIZE / 2)
{
// 获取最近的左上角的点
// add by rock
int col = (x - MARGIN) / BLOCK_SIZE;
int row = (y - MARGIN) / BLOCK_SIZE;
int leftTopPosX = MARGIN + BLOCK_SIZE * col;
int leftTopPosY = MARGIN + BLOCK_SIZE * row;
//根据距离算出合适的点击位置,一 共四个点,根据半径距离选最近的
clickPosRow = -1; //初始化最终的值
clickPosCol = -1;
int len = 0; // 计算完后取整就可以了
selectPos = false;
//确定一个误差在范围内的点,且只可能确定一个出来
len = sqrt((x - leftTopPosX) * (x - leftTopPosX) + (y - leftTopPosY) * (y - leftTopPosY));
if(len < POS_OFFSET)
{
clickPosRow = row;
clickPosCol = col;
if(game->gameMapVec[clickPosRow][clickPosCol] == 0) {
selectPos = true;
}
}
len = sqrt((x - leftTopPosX - BLOCK_SIZE) * (x - leftTopPosX - BLOCK_SIZE) + (y - leftTopPosY - BLOCK_SIZE) * (y - leftTopPosY - BLOCK_SIZE));
if(len < POS_OFFSET)
{
clickPosRow = row;
clickPosCol = col + 1;
if(game->gameMapVec[clickPosRow][clickPosCol] == 0) {
selectPos = true;
}
}
len = sqrt((x - leftTopPosX) * (x - leftTopPosX) + (y - leftTopPosY - BLOCK_SIZE) * (y - leftTopPosY - BLOCK_SIZE));
if(len < POS_OFFSET)
{
clickPosRow = row + 1;
clickPosCol = col;
if(game->gameMapVec[clickPosRow][clickPosCol] == 0) {
selectPos = true;
}
}
len = sqrt((x - leftTopPosX - BLOCK_SIZE) * (x - leftTopPosX - BLOCK_SIZE) + (y - leftTopPosY) * (y - leftTopPosY));
if(len < POS_OFFSET)
{
clickPosRow = row + 1;
clickPosCol = col + 1;
if(game->gameMapVec[clickPosRow][clickPosCol] == 0) {
selectPos = true;
}
}
}
// 存了坐标后要重绘
update();
}
3、绘制落点
void MainWindow::paintEvent(QPaintEvent *event)
{
QPainter painter(this);
//绘制棋盘
painter.setRenderHint (QPainter::Antialiasing, true); // 抗锯齿
for(int i = 0; i < BOARD_GRAD_SIZE + 1; i++)
{
// 从左到右,第(i+1)条竖线
painter.drawLine(MARGIN + BLOCK_SIZE * i, MARGIN, MARGIN + BLOCK_SIZE * i, size().height() - MARGIN);
// 从上到下,第(i+1)条横线
painter.drawLine(MARGIN, MARGIN + BLOCK_SIZE * i, size().width() - MARGIN, MARGIN + BLOCK_SIZE * i);
}
// 绘制选中点
QBrush brush;
brush.setStyle(Qt::SolidPattern);
// 绘制落子标记(防止鼠标出框越界)
if(clickPosRow > 0 && clickPosRow < BOARD_GRAD_SIZE &&
clickPosCol > 0 && clickPosCol < BOARD_GRAD_SIZE &&
game->gameMapVec[clickPosRow][clickPosCol] == 0)
{
if(game->playerFlag) {
brush.setColor(Qt::black);
}
else {
brush.setColor(Qt::white);
}
painter.setBrush(brush);
painter.drawRect(MARGIN + BLOCK_SIZE * clickPosCol - MARK_SIZE / 2, MARGIN + BLOCK_SIZE * clickPosRow - MARK_SIZE, 8, 8);
}
}
1、重写鼠标释放事件
void MainWindow::mouseReleaseEvent(QMouseEvent *event)
{
if(selectPos == false) {
return;
} else {
selectPos = false;
}
chessOneByPerson();
if(game_type == AI) { // 人机模式
// AI下棋
}
}
2、写下棋的方法
void MainWindow::chessOneByPerson()
{
// 根据当前存储的坐标下子
// 只有有效点击才下子,并且该处没有子
if(clickPosRow != -1 && clickPosCol != -1 && game->gameMapVec[clickPosRow][clickPosRow] == 0)
{
game->actionByPerson(clickPosRow, clickPosCol);
// 播放落子音效,待实现
//重绘
update() ;
}
}
void GameModel::actionByPerson(int row, int col)
{
updateGameMap(row, col);
}
void GameModel::updateGameMap(int row, int col)
{
if(playerFlag)
gameMapVec[row][col] = 1;
else
gameMapVec[row][col] = -1;
// 换手
playerFlag = !playerFlag;
}
3、绘制棋子
void MainWindow::paintEvent(QPaintEvent *event)
{
QPainter painter(this);
//绘制棋盘
painter.setRenderHint (QPainter::Antialiasing, true); // 抗锯齿
for(int i = 0; i < BOARD_GRAD_SIZE + 1; i++)
{
// 从左到右,第(i+1)条竖线
painter.drawLine(MARGIN + BLOCK_SIZE * i, MARGIN, MARGIN + BLOCK_SIZE * i, size().height() - MARGIN);
// 从上到下,第(i+1)条横线
painter.drawLine(MARGIN, MARGIN + BLOCK_SIZE * i, size().width() - MARGIN, MARGIN + BLOCK_SIZE * i);
}
// 绘制选中点
QBrush brush;
brush.setStyle(Qt::SolidPattern);
// 绘制落子标记(防止鼠标出框越界)
if(clickPosRow > 0 && clickPosRow < BOARD_GRAD_SIZE &&
clickPosCol > 0 && clickPosCol < BOARD_GRAD_SIZE &&
game->gameMapVec[clickPosRow][clickPosCol] == 0)
{
if(game->playerFlag) {
brush.setColor(Qt::black);
}
else {
brush.setColor(Qt::white);
}
painter.setBrush(brush);
painter.drawRect(MARGIN + BLOCK_SIZE * clickPosCol - MARK_SIZE / 2, MARGIN + BLOCK_SIZE * clickPosRow - MARK_SIZE, 8, 8);
}
//绘制棋子
for(int i = 0; i < BOARD_GRAD_SIZE; i++)
for(int j = 0; j < BOARD_GRAD_SIZE; j++)
{
if(game->gameMapVec[i][j] == 1)
{
// brush.setColor(Qt::white);
brush.setColor(Qt::black);
painter.setBrush(brush);
painter.drawEllipse(MARGIN + BLOCK_SIZE * j - CHESS_RADTUS/2, MARGIN + BLOCK_SIZE * i - CHESS_RADTUS/2, CHESS_RADTUS, CHESS_RADTUS);
}
else if(game->gameMapVec[i][j] == -1)
{
//brush.setColor(Qt::black);
brush.setColor(Qt::white);
painter.setBrush(brush);
painter.drawEllipse(MARGIN + BLOCK_SIZE * j - CHESS_RADTUS/2, MARGIN + BLOCK_SIZE * i - CHESS_RADTUS/2, CHESS_RADTUS, CHESS_RADTUS);
}
}
}
bool GameModel::isWin(int row, int col)
{
// 横竖斜四种大情况,每种情况都根据当前落子往后遍历5个棋子,有一种符合就算赢
// 水平方向
for(int i = 0; i < 5; i++)
{
// 往左5个,往右匹配4个子,20种情况
if(col - i > 0 && col - i + 4 < BOARD_GRAD_SIZE &&
gameMapVec[row][col - i] == gameMapVec[row][col - i + 1] &&
gameMapVec[row][col - i] == gameMapVec[row][col - i + 2] &&
gameMapVec[row][col - i] == gameMapVec[row][col - i + 3] &&
gameMapVec[row][col - i] == gameMapVec[row][col - i + 4]) {
return true;
}
}
// 竖直方向(上下延伸4个)
for(int i = 0; i < 5; i++)
{
if(row - i > 0 && row - i + 4 < BOARD_GRAD_SIZE &&
gameMapVec[row - i][col] == gameMapVec[row - i + 1][col] &&
gameMapVec[row - i][col] == gameMapVec[row - i + 2][col] &&
gameMapVec[row - i][col] == gameMapVec[row - i + 3][col] &&
gameMapVec[row - i][col] == gameMapVec[row - i + 4][col]) {
return true;
}
}
// "/"方向(上下延伸4个)
for(int i = 0; i < 5; i++)
{
if(row + i < BOARD_GRAD_SIZE && row + i - 4 > 0 &&
col - i > 0 && col - i + 4 < BOARD_GRAD_SIZE &&
gameMapVec[row + i][col - i] == gameMapVec[row + i - 1][col - i + 1] &&
gameMapVec[row + i][col - i] == gameMapVec[row + i - 2][col - i + 2] &&
gameMapVec[row + i][col - i] == gameMapVec[row + i - 3][col - i + 3] &&
gameMapVec[row + i][col - i] == gameMapVec[row + i - 4][col - i + 4]) {
return true;
}
}
// "\"方向(上下延伸4个)
for(int i = 0; i < 5; i++)
{
if(row - i > 0 && row + i + 4 < BOARD_GRAD_SIZE &&
col - i > 0 && col - i + 4 < BOARD_GRAD_SIZE &&
gameMapVec[row - i][col - i] == gameMapVec[row - i + 1][col - i + 1] &&
gameMapVec[row - i][col - i] == gameMapVec[row - i + 2][col - i + 2] &&
gameMapVec[row - i][col - i] == gameMapVec[row - i + 3][col - i + 3] &&
gameMapVec[row - i][col - i] == gameMapVec[row - i + 4][col - i + 4]) {
return true;
}
}
return false;
}
在重绘函数里面判断输赢
void MainWindow::paintEvent(QPaintEvent *event)
{
QPainter painter(this);
//绘制棋盘
painter.setRenderHint (QPainter::Antialiasing, true); // 抗锯齿
for(int i = 0; i < BOARD_GRAD_SIZE + 1; i++)
{
// 从左到右,第(i+1)条竖线
painter.drawLine(MARGIN + BLOCK_SIZE * i, MARGIN, MARGIN + BLOCK_SIZE * i, size().height() - MARGIN);
// 从上到下,第(i+1)条横线
painter.drawLine(MARGIN, MARGIN + BLOCK_SIZE * i, size().width() - MARGIN, MARGIN + BLOCK_SIZE * i);
}
// 绘制选中点
QBrush brush;
brush.setStyle(Qt::SolidPattern);
// 绘制落子标记(防止鼠标出框越界)
if(clickPosRow > 0 && clickPosRow < BOARD_GRAD_SIZE &&
clickPosCol > 0 && clickPosCol < BOARD_GRAD_SIZE &&
game->gameMapVec[clickPosRow][clickPosCol] == 0)
{
if(game->playerFlag) {
brush.setColor(Qt::black);
}
else {
brush.setColor(Qt::white);
}
painter.setBrush(brush);
painter.drawRect(MARGIN + BLOCK_SIZE * clickPosCol - MARK_SIZE / 2, MARGIN + BLOCK_SIZE * clickPosRow - MARK_SIZE, 8, 8);
}
//绘制棋子
for(int i = 0; i < BOARD_GRAD_SIZE; i++)
for(int j = 0; j < BOARD_GRAD_SIZE; j++)
{
if(game->gameMapVec[i][j] == 1)
{
// brush.setColor(Qt::white);
brush.setColor(Qt::black);
painter.setBrush(brush);
painter.drawEllipse(MARGIN + BLOCK_SIZE * j - CHESS_RADTUS/2, MARGIN + BLOCK_SIZE * i - CHESS_RADTUS/2, CHESS_RADTUS, CHESS_RADTUS);
}
else if(game->gameMapVec[i][j] == -1)
{
//brush.setColor(Qt::black);
brush.setColor(Qt::white);
painter.setBrush(brush);
painter.drawEllipse(MARGIN + BLOCK_SIZE * j - CHESS_RADTUS/2, MARGIN + BLOCK_SIZE * i - CHESS_RADTUS/2, CHESS_RADTUS, CHESS_RADTUS);
}
}
// 判断输赢
if(clickPosRow > 0 && clickPosRow < BOARD_GRAD_SIZE &&
clickPosCol > 0 && clickPosCol < BOARD_GRAD_SIZE &&
(game->gameMapVec[clickPosRow][clickPosCol] == 1 ||
game->gameMapVec[clickPosRow][clickPosCol] == -1))
{
if(game->isWin(clickPosRow, clickPosCol) && game->gameStatus == PLAYING)
{
game->gameStatus = WIN;
QString str;
if(game->gameMapVec[clickPosRow][clickPosCol] == 1) {
str = "黑棋";
}
if(game->gameMapVec[clickPosRow][clickPosCol] == -1) {
str = "白棋";
}
QMessageBox::StandardButton btnValue = QMessageBox::information(this, "五子棋嬴家", str + "胜利");
// 重置游戏状态,否则容易死循环
if(btnValue == QMessageBox::Ok) {
game->startGame(game_type);
game->gameStatus = PLAYING;
}
}
}
}
所有的空白点往8个方向寻找对方子弟的个数,如果子弟个数比较多,优先堵住
这个 Ai 是以防守为主的,如果对方没有对白棋构成威胁的旗形,白棋才会主动出击
// 最关键的计算评分
void GameModel::calculateScore()
{
//統計玩家或者電腦連成的子
int personNum = 0; //玩家連成子的個數
int botNum = 0; //AI連成子的個數
int emptyNum = 0; //各方向空白位的個數
//清空評分數組
scoreMapVec.clear();
for(int i=0;i lineScores;
for(int j=0;j攻的分數
*/
for(int row=0;row0 && col>0 && gameMapVec[row][col]==0){
//遍歷周圍8個方向
for(int y=-1;y<=1;y++){
for(int x=-1;x<=1;x++){
//重置
personNum = 0;
botNum = 0;
emptyNum = 0;
//原坐標不算
if(!(y==0 && x==0)){
//每個方向延伸4個子
//對玩家黑子評分(正反兩個方向)
for(int i=1;i<=4;i++){
if(row+i*y>0 && row+i*y0 && col+i*x0 && row+i*y0 && col+i*x0 && row-i*y0 && col-i*x0 && row-i*y0 && col-i*x0 && row+i*y0 && col+i*x0 && row+i*y0 && col+i*x0 && row-i*y0 && col-i*x0 && row-i*y0 && col-i*x= 4){ //活5
scoreMapVec[row][col]+=20000;
}
}
}
}
}
}
}
}
每次 AI 思考的时间为 AI_THINK_TIME
写槽函数
加入头文件 #include
void GameModel::actionByAI(int &clickRow, int &clickCol)
{
//計算評分
calculateScore();
//從評分中找出最大分數的位置
int maxScore = 0;
std::vector> maxPoints;
for(int row = 1;rowmaxScore){ //找最大數和坐標
maxPoints.clear();
maxScore = scoreMapVec[row][col];
maxPoints.push_back(std::make_pair(row,col));
}else if(scoreMapVec[row][col] == maxScore){ //如果有多個最大值就將他們存儲起來,在後面的代碼隨機抽1個
maxPoints.push_back(std::make_pair(row,col));
}
}
}
}
//隨機落子,如果有多個點的話
srand((unsigned)time(0));
int index = rand()%maxPoints.size();
std::pair pointPair = maxPoints.at(index);
clickRow = pointPair.first;
clickCol = pointPair.second;
updateGameMap(clickRow,clickCol);
}