IE盒子

搜索
查看: 98|回复: 1

C++五子棋人机对战

[复制链接]

1

主题

4

帖子

7

积分

新手上路

Rank: 1

积分
7
发表于 2022-12-2 18:08:15 | 显示全部楼层 |阅读模式
目录

五子棋人机对战,已经有很版本。但是使用纯C++,严格按照C++面向对象思想开发的,却还是很少的,所以准备使用C++面向对象的思想,开发一个完整的五子棋人机对战,对于C++初学者,是很有帮助的哦!
本教程配套视频
1. 项目目标


  • 掌握C++的核心技术
  • 掌握C++开发项目的方法和流程
  • 掌握AI算法的基础应用
2. 效果演示



https://www.zhihu.com/video/1532817917024894976
开局头像,没有看错,就是我哈,棋魂附体 :-)


准备好 了吗?直接上代码!
3. 创建项目

使用VS2019+easyx图形库开发,也可以使用VS的其他版本。
参考:VS2019安装教程easyx图形库入门教程
使用VS2019(或VS2022)创建一个新项目,选择空项目模板。


然后再导入图片素材res目录。因网盘链接不稳定,在评论中回复邮件地址,即发送完整素材。也可以使用自己的素材。


4. 项目框架设计

4.1 设计项目框架

使用C语言开发的初学者,往往直接就在main函数中写详细的过程。使用C++面向对象,就需要“脱胎换骨”,改变开发思路了!不写过程,直接写需要几个类!


这里,设计了4个类,分别表示棋手,AI, 棋盘,游戏控制。这应该是最符合现实情况的简单设计了,如果是做网络对战版,就还需要添加其它模块。
4.2 根据设计框架创建类

创建项目框架中描述的4个类。可以使用如下方式创建类:


填写类名,再单击确定即可。


按照这个方式,一共创建4个类:Man, AI, Chess, ChessGame. 创建完后,项目的目录结构如:


5. 给类添加主要接口

5.1 设计棋盘类Chess的主要接口

注意:在给类设计接口时,建议先只考虑对外暴露的“接口”,可以先不用考虑数据成员,对外(public)提供的接口(函数)才是最重要的。
Chess.h
typedef enum {
        CHESS_WHITE = -1,  // 白方
        CHESS_BLACK = 1    // 黑方
} chess_kind_t;

struct ChessPos {
        int row;
        int col;
};

class Chess
{
public:
        // 棋盘的初始化:加载棋盘的图片资源,初始化棋盘的相关数据
        void init();

        // 判断在指定坐标(x,y)位置,是否是有效点击
        // 如果是有效点击,把有效点击的位置(行,列)保存在参数pos中
        bool clickBoard(int x, int y, ChessPos* pos);

        // 在棋盘的指定位置(pos), 落子(kind)
        void chessDown(ChessPos* pos, chess_kind_t kind);

        // 获取棋盘的大小(13线、15线、19线)
        int getGradeSize();

        // 获取指定位置是黑棋,还是白棋,还是空白
        int getChessData(ChessPos* pos);
        int getChessData(int row, int col);

        // 判断棋局是否结束
        bool checkOver();
};5.2 设计AI类的主要接口

AI.h
#include "Chess.h"
class AI
{
public:
        void init(Chess* chess);
        void go();
}; 5.3 设计Man类的主要接口

Man.h
#include "Chess.h"

class Man
{
public:
        void init(Chess* chess);
        void go();
}; 5.4 设计ChessGame的主要接口

ChessGame.h
class ChessGame
{
public:
        void play();
};5.5 添加各个接口的具体实现

可以使用如下方式自动生成各接口的具体实现。先不用考虑各个接口的真正实现,直接使用空函数体代替。


6. 实现游戏控制

直接调用各个类定义的接口,实现游戏的主体控制。
6.1 添加数据成员

为了便于调用各个类的功能,在ChessGame中,添加3各数据成员,并再构造函数中初始化这三个数据成员。
#include "Man.h"
#include "AI.h"
#include "Chess.h"

class ChessGame
{
public:
        ChessGame(Man*, AI*, Chess*);
        void play();

private:
        Man* man;
        AI* ai;
        Chess* chess;
};

ChessGame::ChessGame(Man* man, AI* ai, Chess* chess)
{
        this->man = man;
        this->ai = ai;
        this->chess = chess;

        ai->init(chess);
        man->init(chess);
}6.2 实现游戏控制
void ChessGame::play()
{
        chess->init();
        while (1) {
                man->go();
                if (chess->checkOver()) {
                        chess->init();;
                        continue;
                }

                ai->go();
                if (chess->checkOver()) {
                        chess->init();
                        continue;
                }
        }
}7. 创建游戏

在main函数中,创建游戏。
#include <iostream>
#include "ChessGame.h"

int main(void) {
        Chess chess;
        Man man;
        AI ai;
        ChessGame game(&man, &ai, &chess);

        game.play();

        return 0;
}8. 棋盘的“数据成员”设计

为棋盘类,添加private权限的“数据成员”。
private:
        // 棋盘尺寸
        int gradeSize;
        float margin_x;//49;
        int margin_y;// 49;
        float chessSize; //棋子大小(棋盘方格大小)

        IMAGE chessBlackImg;
        IMAGE chessWhiteImg;

        // 存储当前游戏棋盘和棋子的情况,空白为0,黑子1,白子-1
        vector<vector<int>> chessMap;

        // 标示下棋方, true:黑棋方  false: AI 白棋方(AI方)
        bool playerFlag;再补充一下头文件。
#include <graphics.h>
#include <vector>
using namespace std;9. 使用棋盘类的“构造函数” 对棋盘进行构造

添加棋盘类的构造函数的定义以及实现。
Chess.h
Chess(int gradeSize, int marginX, int marginY, float chessSize);Chess.cpp
Chess::Chess(int gradeSize, int marginX, int marginY, float chessSize)
{
        this->gradeSize = gradeSize;
        this->margin_x = marginX;
        this->margin_y = marginY;
        this->chessSize = chessSize;
        playerFlag = CHESS_BLACK;

        for (int i = 0; i < gradeSize; i++) {
                vector<int>row;
                for (int j = 0; j < gradeSize; j++) {
                        row.push_back(0);
                }
                chessMap.push_back(row);
        }
}同时修改main函数的Chess对象的创建。、
        //Chess chess;
        Chess chess(13, 44, 43, 67.4);10. 棋盘的“初始化”

对棋盘进行数据初始化,使得能够看到实际的棋盘。
void Chess::init()
{
        initgraph(897, 895);
        loadimage(0, "res/棋盘2.jpg");

        mciSendString("play res/start.wav", 0, 0, 0); //需要修改字符集为多字节字符集

        loadimage(&chessBlackImg, "res/black.png", chessSize, chessSize, true);
        loadimage(&chessWhiteImg, "res/white.png", chessSize, chessSize, true);

        for (int i = 0; i < chessMap.size(); i++) {
                for (int j = 0; j < chessMap.size(); j++) {
                        chessMap[j] = 0;
                }
        }

        playerFlag = true;
}添加头文件和相关库,使得能够播放落子音效。
Chess.cpp
#include <mmsystem.h>
#pragma comment(lib, "winmm.lib")修改项目的字符集为“多字节字符集”。



测试效果



11. 实现棋手走棋

现在执行程序,除了弹出的棋盘,什么都不能干。因为,棋手的走棋函数,还没有实现哦!现在来实现棋手走棋功能。
11.1 棋手的初始化

为棋手类,添加数据成员,表示棋盘
Man.h
private:
        Chess* chess;实现棋手对象的初始化。
Man.cpp
void Man::init(Chess* chess)
{
        this->chess = chess;
}在ChessGame的构造函数中,实现棋手的初始化。
ChessGame.cpp
ChessGame::ChessGame(Man* man, AI* ai, Chess* chess)
{
        this->man = man;
        this->ai = ai;
        this->chess = chess;

        man->init(chess);  //初始化棋手
}11.2 棋手走棋

Man.cpp
void Man::go(){
        // 等待棋士有效落子
        MOUSEMSG msg;
        ChessPos pos;
        while (1) {
                msg = GetMouseMsg();
                if (msg.uMsg == WM_LBUTTONDOWN && chess->clickBoard(msg.x, msg.y, &pos)) {
                        break;
                }
        }

        // 落子
        chess->chessDown(&pos, CHESS_BLACK);
}11.3 判断落子点击位置是否有效

执行程序后,还是没有任何效果,因为落子的有效性还没有判断。
原理分析




先计算点击位置附近的4个点的位置,然后再计算点击位置到这四个点之间的距离,如果离某个点的距离小于“阈值”,就认为这个点是落子位置。这个“阈值”, 小于棋子大小的一半即可。我们这里取棋子大小的0.4倍。
代码实现

Chess.cpp
bool Chess::clickBoard(int x, int y, ChessPos* pos)
{
        int col = (x - margin_x) / chessSize;
        int row = (y - margin_y) / chessSize;

        int leftTopPosX = margin_x + chessSize * col;
        int leftTopPosY = margin_y + chessSize * row;
        int offset = chessSize * 0.4; // 20 鼠标点击的模糊距离上限

        int len;
        int selectPos = false;

        do {
                len = sqrt((x - leftTopPosX) * (x - leftTopPosX) + (y - leftTopPosY) * (y - leftTopPosY));
                if (len < offset) {
                        pos->row = row;
                        pos->col = col;
                        if (chessMap[pos->row][pos->col] == 0) {
                                selectPos = true;
                        }
                        break;
                }

                // 距离右上角的距离
                len = sqrt((x - leftTopPosX - chessSize) * (x - leftTopPosX - chessSize) + (y - leftTopPosY) * (y - leftTopPosY));
                if (len < offset) {
                        pos->row = row;
                        pos->col = col + 1;
                        if (chessMap[pos->row][pos->col] == 0) {
                                selectPos = true;
                        }
                        break;
                }

                // 距离左下角的距离
                len = sqrt((x - leftTopPosX) * (x - leftTopPosX) + (y - leftTopPosY - chessSize) * (y - leftTopPosY - chessSize));
                if (len < offset) {
                        pos->row = row + 1;
                        pos->col = col;
                        if (chessMap[pos->row][pos->col] == 0) {
                                selectPos = true;
                        }
                        break;
                }

                // 距离右下角的距离
                len = sqrt((x - leftTopPosX - chessSize) * (x - leftTopPosX - chessSize) + (y - leftTopPosY - chessSize) * (y - leftTopPosY - chessSize));
                if (len < offset) {
                        pos->row = row + 1;
                        pos->col = col + 1;

                        if (chessMap[pos->row][pos->col] == 0) {
                                selectPos = true;
                        }
                        break;
                }
        } while (0);

        return selectPos;
}可以通过打印语句,测试判断是否准确。
12. 实现棋盘落子

12.1 实现Chess类的chessDown成员函数

void Chess::chessDown(ChessPos *pos, chess_kind_t kind)
{
        mciSendString("play res/down7.WAV", 0, 0, 0);

        int x = margin_x + pos->col * chessSize - 0.5 * chessSize;
        int y = margin_y + pos->row * chessSize - 0.5 * chessSize;

        if (kind == CHESS_WHITE) {
                putimagePNG(x, y, &chessWhiteImg);
        }
        else {
                putimagePNG(x, y, &chessBlackImg);
        }

}检查落子效果:


棋子背后有黑色背景。这是因为easyx图形库默认不支持背景透明的png格式图片,把透明部分直接渲染为黑色了。解决方案,使用自定义的图形渲染接口,如下:
void putimagePNG(int x, int y, IMAGE* picture) //x为载入图片的X坐标,y为Y坐标
{
        // 变量初始化
        DWORD* dst = GetImageBuffer();    // GetImageBuffer()函数,用于获取绘图设备的显存指针,EASYX自带
        DWORD* draw = GetImageBuffer();
        DWORD* src = GetImageBuffer(picture); //获取picture的显存指针
        int picture_width = picture->getwidth(); //获取picture的宽度,EASYX自带
        int picture_height = picture->getheight(); //获取picture的高度,EASYX自带
        int graphWidth = getwidth();       //获取绘图区的宽度,EASYX自带
        int graphHeight = getheight();     //获取绘图区的高度,EASYX自带
        int dstX = 0;    //在显存里像素的角标

        // 实现透明贴图 公式: Cp=αp*FP+(1-αp)*BP , 贝叶斯定理来进行点颜色的概率计算
        for (int iy = 0; iy < picture_height; iy++)
        {
                for (int ix = 0; ix < picture_width; ix++)
                {
                        int srcX = ix + iy * picture_width; //在显存里像素的角标
                        int sa = ((src[srcX] & 0xff000000) >> 24); //0xAArrggbb;AA是透明度
                        int sr = ((src[srcX] & 0xff0000) >> 16); //获取RGB里的R
                        int sg = ((src[srcX] & 0xff00) >> 8);   //G
                        int sb = src[srcX] & 0xff;              //B
                        if (ix >= 0 && ix <= graphWidth && iy >= 0 && iy <= graphHeight && dstX <= graphWidth * graphHeight)
                        {
                                dstX = (ix + x) + (iy + y) * graphWidth; //在显存里像素的角标
                                int dr = ((dst[dstX] & 0xff0000) >> 16);
                                int dg = ((dst[dstX] & 0xff00) >> 8);
                                int db = dst[dstX] & 0xff;
                                draw[dstX] = ((sr * sa / 255 + dr * (255 - sa) / 255) << 16)  //公式: Cp=αp*FP+(1-αp)*BP  ; αp=sa/255 , FP=sr , BP=dr
                                        | ((sg * sa / 255 + dg * (255 - sa) / 255) << 8)         //αp=sa/255 , FP=sg , BP=dg
                                        | (sb * sa / 255 + db * (255 - sa) / 255);              //αp=sa/255 , FP=sb , BP=db
                        }
                }
        }
}再把chessDown中的putimage更换为putimagePNG, 测试效果如下:


如上,黑色背景已经被去除。
12.2 修改棋盘的棋子数据
在界面上落子之后,还需要修改棋盘的棋子数据。为Chess类添加updateGameMap函数来修改棋子数据。这个方法,是给棋盘对象内部使用的,不需要开放给他人使用,所有把权限设置为private,设置为public也可以,但是从技术角度就不安全了。如果他人直接调用这个函数,就会导致棋盘的数据和界面上看到的数据不一样。
Chess.h
private:
        void updateGameMap(ChessPos *pos);Chess.cpp
void Chess::updateGameMap(ChessPos* pos)
{
    lastPos = *pos;
        chessMap[pos->row][pos->col] = playerFlag ? 1 : -1;
        playerFlag = !playerFlag; // 换手
}在落子后,调用updateGameMap更新棋子数据。
void Chess::chessDown(ChessPos *pos, chess_kind_t kind)
{
        // ......

        updateGameMap(pos);
}13. 实现AI走棋

终于可以设计我们的AI模块了!
13.1 设计AI的数据成员


  • 添加棋盘数据成员,以表示对哪个棋盘下棋。
  • 添加评分数组, 用来存储AI对棋盘所有落点的价值评估。这也是人机对战最重要的部分。
AI.h
private:
        Chess* chess;
        // 存储各个点位的评分情况,作为AI下棋依据
        vector<vector<int>> scoreMap;13.2 对AI进行初始化

AI.cpp
void AI::init(Chess* chess)
{
    this->chess = chess;

    int size = chess->getGradeSize();
    for (int i = 0; i < size; i++) {
        vector<int> row;
        for (int j = 0; j < size; j++) {
            row.push_back(0);
        }
        scoreMap.push_back(row);
    }
}13.3 AI“思考”怎样走棋

AI的思考方法,就是对棋盘的所有可能落子点,做评分计算,然后选择一个评分最高的点落子。
13.3.1 AI对落子点进行评分

对每一个可能的落子点,从该点周围的八个方向,分别计算,确定出每个方向已经有几颗连续的棋子。
棋理格言:敌之好点,即我之好点。
就是说,每个点,都要考虑,如果敌方占领了这个点,会产生多大的价值,如果我方占领了这个点,又会产生多大的价值。如果我方占领这个点,价值只有1000,但是敌方要是占领了这个点,价值有2000,而在自己在其它位置没有价值更高的点,那么建议直接抢占这个敌方的好点。
兵家必争之地:荆州(隆中对的第一步,就是取荆州)



AI先计算棋手如果在这个位置落子,会有多大的价值。然后再计算自己如果在这个位置落子,有大大价值。具体计算方法,就是计算如果黑棋或者白棋在这个位置落子,那么在这个位置的某个方向上, 一共有连续几个黑子或者连续几个白子。连续的数量越多,价值越大。


常见棋形
连2:



活3




死3




活4



死4



连5(赢棋)



如果走这个点,产生的棋形以及对应评分:


用代码实现评分计算
AI.h
private:
        void calculateScore();AI.cpp
void AI::calculateScore()
{
    // 统计玩家或者电脑连成的子
    int personNum = 0;  // 玩家连成子的个数
    int botNum = 0;     // AI连成子的个数
    int emptyNum = 0;   // 各方向空白位的个数

    // 清空评分数组
    for (int i = 0; i < scoreMap.size(); i++) {
        for (int j = 0; j < scoreMap.size(); j++) {
            scoreMap[j] = 0;
        }
    }

    int size = chess->getGradeSize();
    for (int row = 0; row < size; row++)
        for (int col = 0; col < size; col++)
        {
            // 空白点就算
            if (chess->getChessData(row, col) == 0) {
                // 遍历周围八个方向
                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++)
                            {
                                int curRow = row + i * y;
                                int curCol = col + i * x;
                                if (curRow >= 0 && curRow < size &&
                                    curCol >= 0 && curCol < size &&
                                    chess->getChessData(curRow, curCol) == 1) // 真人玩家的子
                                {
                                    personNum++;
                                }
                                else if (curRow >= 0 && curRow < size &&
                                    curCol >= 0 && curCol < size &&
                                    chess->getChessData(curRow, curCol) == 0) // 空白位
                                {
                                    emptyNum++;
                                    break;
                                }
                                else            // 出边界
                                    break;
                            }

                            for (int i = 1; i <= 4; i++)
                            {
                                int curRow = row - i * y;
                                int curCol = col - i * x;
                                if (curRow >= 0 && curRow < size &&
                                    curCol >= 0 && curCol < size &&
                                    chess->getChessData(curRow, curCol) == 1) // 真人玩家的子
                                {
                                    personNum++;
                                }
                                else if (curRow >= 0 && curRow < size &&
                                    curCol >= 0 && curCol < size &&
                                    chess->getChessData(curRow, curCol) == 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] += 200;
                            }
                            else if (personNum == 4)                 // 杀五
                                scoreMap[row][col] += 20000;

                            // 进行一次清空
                            emptyNum = 0;

                            // 对白棋评分
                            for (int i = 1; i <= 4; i++)
                            {
                                int curRow = row + i * y;
                                int curCol = col + i * x;
                                if (curRow > 0 && curRow < size &&
                                    curCol > 0 && curCol < size &&
                                    chess->getChessData(curRow, curCol) == -1) // 玩家的子
                                {
                                    botNum++;
                                }
                                else if (curRow > 0 && curRow < size &&
                                    curCol > 0 && curCol < size &&
                                    chess->getChessData(curRow, curCol) == 0) // 空白位
                                {
                                    emptyNum++;
                                    break;
                                }
                                else            // 出边界
                                    break;
                            }

                            for (int i = 1; i <= 4; i++)
                            {
                                int curRow = row - i * y;
                                int curCol = col - i * x;
                                if (curRow > 0 && curRow < size &&
                                    curCol > 0 && curCol < size &&
                                    chess->getChessData(curRow, curCol) == -1) // 玩家的子
                                {
                                    botNum++;
                                }
                                else if (curRow > 0 && curRow < size &&
                                    curCol > 0 && curCol < size &&
                                    chess->getChessData(curRow, curCol) == 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] += 55;
                                else if (emptyNum == 2)
                                    scoreMap[row][col] += 10000; // 活四
                            }
                            else if (botNum >= 4)
                                scoreMap[row][col] += 30000;   // 活五,应该具有最高优先级
                        }
                    }
                }
            }
        }
}13.3.2  AI根据评分进行“思考”

各个落子点的评分确定后,“思考”就很简单了,直接使用“遍历”,找出评分最高的点即可。
AI.h
ChessPos think();  //private权限AI.cpp
ChessPos AI::think()
{
    // 计算评分
    calculateScore();

    // 从评分中找出最大分数的位置
    int maxScore = 0;
    //std::vector<std::pair<int, int>> maxPoints;
    vector<ChessPos> maxPoints;
    int k = 0;

    int size = chess->getGradeSize();
    for (int row = 0; row < size; row++) {
        for (int col = 0; col < size; col++)
        {
            // 前提是这个坐标是空的
            if (chess->getChessData(row, col) == 0) {
                if (scoreMap[row][col] > maxScore)          // 找最大的数和坐标
                {
                    maxScore = scoreMap[row][col];
                    maxPoints.clear();
                    maxPoints.push_back(ChessPos(row, col));
                }
                else if (scoreMap[row][col] == maxScore) {   // 如果有多个最大的数,都存起来
                    maxPoints.push_back(ChessPos(row, col));
                }
            }
        }
    }

    // 随机落子,如果有多个点的话
    int index = rand() % maxPoints.size();
    return maxPoints[index];
}对ChesPos类补充构造函数
Chess.h
ChessPos(int r=0, int c=0) :row(r), col(c){} 12.3.3 AI走棋

AI.cpp
void AI::go()
{
        ChessPos pos = think();
        Sleep(1000); //假装思考
    chess->chessDown(&pos, CHESS_WHITE);
}因为思考速度太快,使用Sleep休眠作为停顿,以提高棋手的“对局体验” :-)
12.3.4 测试

检查执行效果:


当AI在“思考”时,程序崩溃!设置断点后检查,发现ai对象的chess成员指向一个无效内存。因为可以判定,还没有对AI对象进行初始化。检查后发现,之前为AI对象定义了初始化init函数,但是没有调用这个函数。补充如下:
ChessGame.cpp
ChessGame::ChessGame(Man* man, AI* ai, Chess* chess)
{
        //...
        ai->init(chess);
}调试后还是发现,程序崩溃:


加断点检查发现Chess类的getGradeSize函数返回0. 修改如下:
int Chess::getGradeSize()
{
        return gradeSize;
}测试运行后,发现AI很傻,落子很“臭”:


加断点调试,发现getChessData函数的返回值始终为0,原来是之前设计这个接口时,使用自动生产的,没有做真正的实现,需改如下:
int Chess::getChessData(ChessPos* pos)
{
        return chessMap[pos->row][pos->col];
}

int Chess::getChessData(int row, int col)
{
        return chessMap[row][col];
}测试后发现,AI的棋力,已经正常:


14. AI的BUG

现在的AI已经能够走棋了,而且还很不错,但是通过调试,发现AI在某些时候会下“昏招”, 成为“臭棋篓子”, 情况如下:
当下到这个局面时:


当棋手在第9行第9列落子时,形成冲4形态时,白棋应该进行阻挡防守,但是白棋却判断错误,在其它位置落子了!


通过加断点判断分析,原因是我们对8个方向做了判断,而在每个方向进行判断时,又对反方向进行了判断。最终导致AI在第行第5列的位置进行价值分析时,在正上方和正下方两次判断中,认为改点有“活三”价值,导致这点的价值被重复计算了一次,被累加到 20000,超过了黑棋冲四的价值!解决方法也很简单,就是8个方向,只要判断4次即可(如下图的绿色箭头


修改后的AI评分方法。
void AI::calculateScore()
{
        int personNum = 0; //棋手方(黑棋)多少个连续的棋子
        int aiNum = 0; //AI方(白棋)连续有多少个连续的棋子
        int emptyNum = 0; // 该方向上空白位的个数

        // 评分向量数组清零
        for (int i = 0; i < scoreMap.size(); i++) {
                for (int j = 0; j < scoreMap.size(); j++) {
                        scoreMap[j] = 0;
                }
        }

        int size = chess->getGradeSize();
        for (int row = 0; row < size; row++) {
                for (int col = 0; col < size; col++) {
                        //对每个点进行计算
                        if (chess->getChessData(row, col)) continue;

                        for (int y = -1; y <= 0; y++) {        //Y的范围还是-1, 0
                                for (int x = -1; x <= 1; x++) {    //X的范围是 -1,0,1
                                        if (y == 0 && x == 0) continue;
                                        if (y == 0 && x != 1) continue; //当y=0时,仅允许x=1

                                        personNum = 0;
                                        aiNum = 0;
                                        emptyNum = 0;

                                        // 假设黑棋在该位置落子,会构成什么棋型
                                        for (int i = 1; i <= 4; i++) {
                                                int curRow = row + i * y;
                                                int curCol = col + i * x;

                                                if (curRow >= 0 && curRow < size &&
                                                        curCol >= 0 && curCol < size &&
                                                        chess->getChessData(curRow, curCol) == 1) {
                                                        personNum++;
                                                }
                                                else if (curRow >= 0 && curRow < size &&
                                                        curCol >= 0 && curCol < size &&
                                                        chess->getChessData(curRow, curCol) == 0) {
                                                        emptyNum++;
                                                        break;
                                                }
                                                else {
                                                        break;
                                                }
                                        }

                                        // 反向继续计算
                                        for (int i = 1; i <= 4; i++) {
                                                int curRow = row - i * y;
                                                int curCol = col - i * x;

                                                if (curRow >= 0 && curRow < size &&
                                                        curCol >= 0 && curCol < size &&
                                                        chess->getChessData(curRow, curCol) == 1) {
                                                        personNum++;
                                                }
                                                else if (curRow >= 0 && curRow < size &&
                                                        curCol >= 0 && curCol < size &&
                                                        chess->getChessData(curRow, curCol) == 0) {
                                                        emptyNum++;
                                                        break;
                                                }
                                                else {
                                                        break;
                                                }
                                        }

                                        if (personNum == 1) { //连2
                                                //CSDN  程序员Rock
                                                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] = 5000; //200
                                                }
                                        }
                                        else if (personNum == 4) {
                                                scoreMap[row][col] = 20000;
                                        }

                                        // 假设白棋在该位置落子,会构成什么棋型
                                        emptyNum = 0;

                                        for (int i = 1; i <= 4; i++) {
                                                int curRow = row + i * y;
                                                int curCol = col + i * x;

                                                if (curRow >= 0 && curRow < size &&
                                                        curCol >= 0 && curCol < size &&
                                                        chess->getChessData(curRow, curCol) == -1) {
                                                        aiNum++;
                                                }
                                                else if (curRow >= 0 && curRow < size &&
                                                        curCol >= 0 && curCol < size &&
                                                        chess->getChessData(curRow, curCol) == 0) {
                                                        emptyNum++;
                                                        break;
                                                }
                                                else {
                                                        break;
                                                }
                                        }

                                        for (int i = 1; i <= 4; i++) {
                                                int curRow = row - i * y;
                                                int curCol = col - i * x;

                                                if (curRow >= 0 && curRow < size &&
                                                        curCol >= 0 && curCol < size &&
                                                        chess->getChessData(curRow, curCol) == -1) {
                                                        aiNum++;
                                                }
                                                else if (curRow >= 0 && curRow < size &&
                                                        curCol >= 0 && curCol < size &&
                                                        chess->getChessData(curRow, curCol) == 0) {
                                                        emptyNum++;
                                                        break;
                                                }
                                                else {
                                                        break;
                                                }
                                        }

                                        if (aiNum == 0) {
                                                scoreMap[row][col] += 5;
                                        }
                                        else if (aiNum == 1) {
                                                scoreMap[row][col] += 10;
                                        }
                                        else if (aiNum == 2) {
                                                if (emptyNum == 1) {
                                                        scoreMap[row][col] += 25;
                                                }
                                                else if (emptyNum == 2) {
                                                        scoreMap[row][col] += 50;
                                                }
                                        }
                                        else if (aiNum == 3) {
                                                if (emptyNum == 1) {
                                                        scoreMap[row][col] += 55;
                                                }
                                                else if (emptyNum == 2) {
                                                        scoreMap[row][col] += 10000;
                                                }
                                        }
                                        else if (aiNum >= 4) {
                                                scoreMap[row][col] += 30000;
                                        }
                                }
                        }
                }
        }
}15. 判断胜负

判断五子棋游戏是否结束。
15.1 对胜负进行处理

Chess.cpp
bool Chess::checkOver()
{
        if (checkWin()) {
                Sleep(1500);
                if (playerFlag == false) {  //黑棋赢(玩家赢),此时标记已经反转,轮到白棋落子
                        mciSendString("play res/不错.mp3", 0, 0, 0);
                        loadimage(0, "res/胜利.jpg");
                }
                else {
                        mciSendString("play res/失败.mp3", 0, 0, 0);
                        loadimage(0, "res/失败.jpg");
                }

                _getch(); // 补充头文件 #include <conio.h>
                return true;
        }

        return false;
}补充头文件 conio.h, 并添加CheckWin的定义和实现。
15.2 胜负判定原理

具体的判定原理,就是对刚才的落子位置进行判断,判断该位置在4个方向上是否有5颗连续的同类棋子。
对于水平位置的判断:


其他方向的判断,原理类似。
15. 3 实现胜负判定

添加最近落子位置。
Chess.h
ChessPos lastPos; //最近落子位置, Chess的private数据成员 更新最近落子位置。
Chess.cpp
void Chess::updateGameMap(ChessPos* pos)
{
        lastPos = *pos;
        //...
}实现胜负判定。
Chess.cpp
bool Chess::checkWin()
{
        // 横竖斜四种大情况,每种情况都根据当前落子往后遍历5个棋子,有一种符合就算赢
        // 水平方向
        int row = lastPos.row;
        int col = lastPos.col;

        for (int i = 0; i < 5; i++)
        {
                // 往左5个,往右匹配4个子,20种情况
                if (col - i >= 0 &&
                        col - i + 4 < gradeSize &&
                        chessMap[row][col - i] == chessMap[row][col - i + 1] &&
                        chessMap[row][col - i] == chessMap[row][col - i + 2] &&
                        chessMap[row][col - i] == chessMap[row][col - i + 3] &&
                        chessMap[row][col - i] == chessMap[row][col - i + 4])
                        return true;
        }

        // 竖直方向(上下延伸4个)
        for (int i = 0; i < 5; i++)
        {
                if (row - i >= 0 &&
                        row - i + 4 < gradeSize &&
                        chessMap[row - i][col] == chessMap[row - i + 1][col] &&
                        chessMap[row - i][col] == chessMap[row - i + 2][col] &&
                        chessMap[row - i][col] == chessMap[row - i + 3][col] &&
                        chessMap[row - i][col] == chessMap[row - i + 4][col])
                        return true;
        }

        // “/"方向
        for (int i = 0; i < 5; i++)
        {
                if (row + i < gradeSize &&
                        row + i - 4 >= 0 &&
                        col - i >= 0 &&
                        col - i + 4 < gradeSize &&
                        // 第[row+i]行,第[col-i]的棋子,与右上方连续4个棋子都相同
                        chessMap[row + i][col - i] == chessMap[row + i - 1][col - i + 1] &&
                        chessMap[row + i][col - i] == chessMap[row + i - 2][col - i + 2] &&
                        chessMap[row + i][col - i] == chessMap[row + i - 3][col - i + 3] &&
                        chessMap[row + i][col - i] == chessMap[row + i - 4][col - i + 4])
                        return true;
        }

        // “\“ 方向
        for (int i = 0; i < 5; i++)
        {
                // 第[row+i]行,第[col-i]的棋子,与右下方连续4个棋子都相同
                if (row - i >= 0 &&
                        row - i + 4 < gradeSize &&
                        col - i >= 0 &&
                        col - i + 4 < gradeSize &&
                        chessMap[row - i][col - i] == chessMap[row - i + 1][col - i + 1] &&
                        chessMap[row - i][col - i] == chessMap[row - i + 2][col - i + 2] &&
                        chessMap[row - i][col - i] == chessMap[row - i + 3][col - i + 3] &&
                        chessMap[row - i][col - i] == chessMap[row - i + 4][col - i + 4])
                        return true;
        }

        return false;
}15. 4 测试效果

已经能够完美判定胜负了,并能自动开启下一局。


再把落子音效加上,用户体验就更好了。
Chess.cpp
void Chess::chessDown(ChessPos* pos, chess_kind_t kind)
{
        mciSendString("play res/down7.WAV", 0, 0, 0);
    //......
}16. AI进一步优化

现在AI的实力,对于一般的五子棋业余爱好者,已经能够秒杀,但是对于业余中的“大佬”,还是力不从心,甚至会屡战屡败,主要原因有两点:
1. 没有对跳三和跳四进行判断。实际上,跳三和跳四的价值与连三连四的价值,是完全相同的。而现在的AI只计算了连三和连四,没有考虑跳三跳四,所以就会错失“好棋”!


对于上图,在位置1和位置2,都会形成“跳三”。


对于上图在位置3和位置4,都会形成连三.


对于上图,在位置1对黑棋形成“跳四”,跳四的价值和“连四”或“冲四”的价值也是相同的!
2. 没有对黑棋设置“禁手”。因为五子棋已经发展到“黑棋先行必胜”的套路,所以职业五子棋比赛,会对黑棋设置以下“禁手”。

三三禁手
四四禁手
长连禁手
三三禁手(如果在该位置主动落子或者被动落子,直接判黑方战败!)


四四禁手(如果在该位置主动落子或者被动落子,直接判黑方战败!)


长连禁手(如果在该位置主动落子或者被动落子,直接判黑方战败!)


AI提升

    在计算落子点价值的时候,增加对跳三和跳四的价值判断
    在判断胜负时,增加对黑方禁手的判断。

通过以上的优化后,业余高手也很难取胜了!但是对专业棋手,还是难以招架!原因在于,目前的AI只根据当前盘面进行判断,静态的最佳座子点。没有对后续步骤进行连续判断。可以使用“搜索树”,进行连续判定,搜索的深度越深,AI的棋力就越深。最终五子棋,就和象棋一样,彻底碾压人类棋手。
17. 开发拓展

计分以及棋力等级|
悔棋功能
棋力训练(充值送形势判断)
记棋谱功能
网络对战功能
邀请微信好友、QQ好友对战功能
移植到移动端(Android和IOS)

---END.
回复

使用道具 举报

2

主题

3

帖子

7

积分

新手上路

Rank: 1

积分
7
发表于 2022-12-2 18:08:44 | 显示全部楼层
1508265870@qq.com
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

快速回复 返回顶部 返回列表