QT学习日记21——五子棋AI

学习视频链接

五子棋-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 代码


一、基本框架 

1.1 绘制棋盘和初始化 

QT学习日记21——五子棋AI_第1张图片

#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

QT学习日记21——五子棋AI_第2张图片

#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

QT学习日记21——五子棋AI_第3张图片

#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 ;
}

QT学习日记21——五子棋AI_第4张图片

#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();
}

1.2 显示落点

添加头文件  #include 、#include

重写鼠标移动事件方法

打开自动检测鼠标移动的按钮

QT学习日记21——五子棋AI_第5张图片

QT学习日记21——五子棋AI_第6张图片 QT学习日记21——五子棋AI_第7张图片

给鼠标移动事件添加逻辑

1、准备需要的变量

QT学习日记21——五子棋AI_第8张图片

2、检测是离哪个点最近

QT学习日记21——五子棋AI_第9张图片

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、绘制落点

QT学习日记21——五子棋AI_第10张图片

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.3 落子功能

1、重写鼠标释放事件

QT学习日记21——五子棋AI_第11张图片

void MainWindow::mouseReleaseEvent(QMouseEvent *event)
{
    if(selectPos == false) {
        return;
    } else {
        selectPos = false;
    }

    chessOneByPerson();
    
    if(game_type == AI) {  // 人机模式
        // AI下棋
    }
}

2、写下棋的方法

QT学习日记21——五子棋AI_第12张图片

void MainWindow::chessOneByPerson()
{
    // 根据当前存储的坐标下子
    // 只有有效点击才下子,并且该处没有子
    if(clickPosRow != -1 && clickPosCol != -1 && game->gameMapVec[clickPosRow][clickPosRow] == 0)
    {
        game->actionByPerson(clickPosRow, clickPosCol);

        // 播放落子音效,待实现

        //重绘
        update() ;
    }
}

QT学习日记21——五子棋AI_第13张图片

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、绘制棋子

QT学习日记21——五子棋AI_第14张图片

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);
            }
        }
}

1.4 判断输赢

QT学习日记21——五子棋AI_第15张图片

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;
}

在重绘函数里面判断输赢

QT学习日记21——五子棋AI_第16张图片

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;
            }
        }
    }
}

二、五子棋 Ai

2.1 Ai策略

所有的空白点往8个方向寻找对方子弟的个数,如果子弟个数比较多,优先堵住

QT学习日记21——五子棋AI_第17张图片

这个 Ai 是以防守为主的,如果对方没有对白棋构成威胁的旗形,白棋才会主动出击

2.2 代码

QT学习日记21——五子棋AI_第18张图片

// 最关键的计算评分
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

QT学习日记21——五子棋AI_第19张图片

写槽函数

QT学习日记21——五子棋AI_第20张图片

 

QT学习日记21——五子棋AI_第21张图片

加入头文件 #include #include ,写 AI 下棋方法

QT学习日记21——五子棋AI_第22张图片

 

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);
}

你可能感兴趣的:(QT,学习)