纳尼,五子棋AI居然这么简单?

前言

最近一直在写博文。虽然没有人看,但是我写博文,并不是为了提高自己的人气,而是为了锻炼自己的表达能力和写代码的能力。

正文

演示

还是不传gif图了

项目简介

好啦,我先来介绍一下本次的项目吧。
EasyX是c++的一个图像渲染框架,通过easyX可以轻松的实现各种简易的gui页面。
五子棋的玩法非常简单,就是两人交替下棋,谁先连出五子谁赢。
我这次的目标是实现图形页面的人机对战。

思路部分

我们先实现相对简单的,UI绘制吧!
首先我们新建一个头文件Chess.h , 并在里面定义一些基本值

#define SIZE 15//定义棋盘内行列棋子的个数
#define debug false//是否为 debug 模式
#define UNIT 20//定义 UNIT 单位,1 UNIT 约等于 20 px

#ifndef NANI_BEAN_H
#define NANI_BEAN_H

class Chess {
public:
    int x;//得益于 c++ 的数组问题,数组内的元素无法感知到自己的 x 和 y 
		  //所以需要自己记录
    int y;
    int color = 0;//黑1 白2   0没有  人是1  AI是2
};

#endif //NANI_BEAN_H

再在main.cpp中定义一些基本值

#define ToolHeight UNIT* 3// 标题栏所占的高度

static int HW = 16; // 窗口大小,单位 UNIT( UNIT 声明在 Bean.h 中)
struct Chess data[SIZE][SIZE]; // 所有棋子的集合
static int mouseX = UNIT, mouseY = UNIT; // 鼠标的 x 和 y,把他们拿到外面,是为了在 debug 模式下输出鼠标信息
static int centerX, length, centerY; // 棋盘中心的 x , y 以及棋盘长度
static int mouseI, mouseJ; // 鼠标所在区域的 x, y

然后 我们开始UI绘制
我们通过while进行帧驱动
然后把绘制区域分为两个部分,即:标题栏棋盘

while (true) {
        BeginBatchDraw();// 收集绘制
        drawToolBar();// 绘制标题栏
        drawBoard();// 绘制棋盘
        EndBatchDraw();// 开始绘制
    }

绘制标题栏drawToolBar

    setfillcolor(WHITE);
    solidrectangle(0, 0, HW * UNIT, ToolHeight);//绘制标题背景
    setbkmode(TRANSPARENT);
    settextcolor(BLUE);
    settextstyle(UNIT, 0, nullptr);
    char *title = "Simon 的 AI 五子棋";
    char version[30] = "EasyX 版本";
    strcat(version, GetEasyXVer());
    outtextxy(UNIT, (ToolHeight - UNIT) / 2, title);
    settextcolor(BLACK);
    outtextxy(UNIT, (ToolHeight - UNIT) / 2 - UNIT, version);// 绘制标题和 EasyX 版本

绘制棋盘drawBoard

    setfillcolor(RGB(0xe9, 0xe7, 0xef));
    solidrectangle(0, ToolHeight, HW * UNIT, HW * UNIT + ToolHeight);//绘制标题以下的区域颜色
    centerX = HW * UNIT / 2;// 算出棋盘中心的 X,Y
    centerY = centerX + ToolHeight;
    length = SIZE * UNIT;// 计算出棋盘的边长
    POINT pts[] = {// 棋盘的四个点
            {centerX - length / 2, centerY - length / 2},
            {centerX + length / 2, centerY - length / 2},
            {centerX + length / 2, centerY + length / 2},
            {centerX - length / 2, centerY + length / 2}
    };
    setfillcolor(RGB(0x41, 0x55, 0x5d));
    solidpolygon(pts, 4);// 绘制棋盘底色

    setlinecolor(BLACK);
    // 绘制棋盘线条
    for (int i = 0; i < (SIZE + 1); ++i) {// 竖着
        line(centerX - length / 2 + UNIT * i, centerY - length / 2, centerX - length / 2 + UNIT * i,
             centerY + length / 2);
    }
    for (int i = 0; i < (SIZE + 1); ++i) {// 横着
        line(centerX - length / 2, centerY - length / 2 + UNIT * i, centerX + length / 2,
             centerY - length / 2 + UNIT * i);
    }
    for (int i = 0; i < SIZE; ++i) {// 棋子
        for (int j = 0; j < SIZE; ++j) {
            if (data[i][j].color == 0)continue;
            setfillcolor(data[i][j].color == 1 ? BLACK : WHITE);
            solidcircle(centerX - length / 2 + UNIT * i + UNIT / 2, centerY - length / 2 + UNIT * j + UNIT / 2,
                        UNIT / 2 - 3);
        }
    }

这里的坐标计算其实很简单,可以参考我下面的图片
字不好看,见谅
UI绘制完成后,我们还需要做与用户的鼠标交互
新建一个函数ObserveMouse(),并在drawBoard后调用

void ObserveMouse() {
    if (MouseHit()) {//如果鼠标没有点击,则不交互
        MOUSEMSG msg = GetMouseMsg();
        mouseX = msg.x;
        mouseY = msg.y;
        // 下面寻找 X 和 Y 是一个优雅做法,如果两层 for 嵌套,处理不好很容易浪费性能
        for (int i = 0; i < SIZE; ++i)// 获取当前鼠标在棋盘上对应位置的 X
            if (centerX - length / 2 + UNIT * i < mouseX)
                if (mouseX < centerX - length / 2 + UNIT * i + UNIT) {
                    mouseI = i;
                    break;
                }
        for (int j = 0; j < SIZE; ++j)// 获取当前鼠标在棋盘上对应位置的 Y
            if (centerY - length / 2 + UNIT * j < mouseY)
                if (mouseY < centerY - length / 2 + UNIT * j + UNIT) {
                    mouseJ = j;
                    break;
                }
        if (data[mouseI][mouseJ].color != 0)return;
        if (msg.mkLButton && msg.uMsg != WM_MOUSEMOVE) {// 如果只监听点击事件的话,会出现按住左键移动鼠标,下了一排棋
            if (turn == HUMAN) {
                turn = AI;
                data[mouseI][mouseJ].color = 1;// 人下完了,换 AI
                if (isFive())return;
            }
            if (turn == AI) {
            预留代码部分
                turn = HUMAN;
                if (isFive())return;
            }
        }
        FlushMouseMsgBuffer();
    }
    if (data[mouseI][mouseJ].color != 0)return;
    setfillcolor(RGB(0XEE, 0XDE, 0XB0));// #eedeb0
    solidcircle(centerX - length / 2 + UNIT * mouseI + UNIT / 2, centerY - length / 2 + UNIT * mouseJ + UNIT / 2,
                UNIT / 2 - 3);// 绘制鼠标选中但还未落子的区域
}

在交互后,我们需要判断棋盘中是否存在五子连线,我们要写一个简单的算法。

int sameLine(int x, int y, int mLastx, int mLasty) // 判断一条线有多少个同色子
{
    int num = 0; // 同一直线同色棋子数
    int DefaultColor = data[mLastx][mLasty].color;
    if (DefaultColor == 0)return 0; // 遇到 0,直接跳
    int xx;
    int yy;
    // 1,0 横着 0,1 竖着
    if (x == 1 && y == 0) {
        for (xx = mLastx; xx >= 0; xx--) {// 向左判断
            if (data[xx][mLasty].color == DefaultColor) {
                num++;
            } else break;
        }
        for (xx = mLastx; xx < (SIZE - 1); xx++) {// 向右判断
            if (data[xx][mLasty].color == DefaultColor) {
                num++;
            } else break;
        }
    } else if (x == 0 && y == 1) {
        for (yy = mLasty; yy >= 0; yy--) {// 向上判断
            if (data[mLastx][yy].color == DefaultColor) {
                num++;
            } else break;
        }
        for (yy = mLasty; yy < (SIZE - 1); yy++) {// 向下判断
            if (data[mLastx][yy].color == DefaultColor) {
                num++;
            } else break;
        }
    } else if (x == 1 && y == 1) {
        for (xx = mLastx, yy = mLasty; xx < (SIZE - 1) && yy >= 0; xx++, yy--) {// 右上判断
            if (data[xx][yy].color == DefaultColor) {
                num++;
            } else break;
        }
        for (xx = mLastx, yy = mLasty; xx >= 0 && yy < (SIZE - 1); yy++, xx--) {// 左下判断
            if (data[xx][yy].color == DefaultColor) {
                num++;
            } else break;
        }
    } else if (x == -1 && y == 1) {
        for (xx = mLastx, yy = mLasty; xx >= 0 && yy >= 0; yy--, xx--) {// 左上判断
            if (data[xx][yy].color == DefaultColor) {
                num++;
            } else break;
        }
        for (xx = mLastx, yy = mLasty; xx < (SIZE - 1) && yy < (SIZE - 1); yy++, xx++) {// 右下判断
            if (data[xx][yy].color == DefaultColor) {
                num++;
            } else break;
        }
    }
    return --num;
}

bool isFive() {// 在每一个点判断是否存在 5 子连线
    for (int i = 0; i < SIZE; ++i) {
        for (int j = 0; j < SIZE; ++j) {
            if (sameLine(1, 0, i, j) >= 5 || sameLine(0, 1, i, j) >= 5 || sameLine(1, 1, i, j) >= 5 ||
                sameLine(-1, 1, i, j) >= 5)
                return true;
        }
    }
    return false;
}

接下来就是我们程序的重头戏,AI算法了
首先我们要了解五子棋中的一些术语

活X:一方的X个连在一起的子 【两边都没有被堵住】
冲X:一方的X个连在一起的字【一边被堵住】
其余可参考五子棋常用术语

首先我们创建一个类,叫做FiveChessAI_simon
类中变量及作用如下

private:
    const int STEP_KILL = 99999;//活 5
    const int STEP_DANGER = 99998;//冲 5 或跳 5
    const int STEP_FOUR = 88888;//出现有两个点可以成五的四
    const int STEP_SLAY = 77777;//双线成杀
    const int STEP_AT_FOUR = 44444;// 冲四或跳四
    Chess chess[SIZE][SIZE]; // 接受棋盘的所有棋子
    int ownWeight[SIZE][SIZE] = {0,}; // 己方每个点权重
    int oppositeWeight[SIZE][SIZE] = {0,}; // 对方每个点权重
    int computerColor; // 电脑要走的棋子颜色

首先我们实现一个算法,判断某个棋子两边的生存空间

private:
//x,y为要判断的位置坐标,color是当前颜色,px,py是判断方向的
//mode为0时,返回一侧空位个数
//mode为1时,返回连线同色个数
//mode为2时,返回不相邻( 1 个空位)连续同色个数
    int oneSide(int x, int y, int color, int px, int py, int mode) {
        int num = 0, space = 0;
        while (!(x + px < 0 || x + px > (SIZE - 1) ||
                 y + py < 0 || y + py > (SIZE - 1))) {
            x += px;
            y += py;
            if (chess[x][y].color == 3 - color) break;
            if (mode == 1 && chess[x][y].color != color) break;
            if (mode == 2 && chess[x][y].color == 0) {
                space++;
                if (space > 1) break;
                continue;
            }
            num++;
        }
        return num;
    }

我再来实现一个算法,让他根据左右生产空间,连续同色,以及不相邻同色个数来判断跳几

private:
//参数 左生存空间 左连续同色个数 左不相邻同色个数 同理r开头的参数是右
    int isJump(int ll, int ls, int lns, int rl, int rs, int rns) {
        int sum = ls + rs + 1;
        int lj = sum + lns - ls;
        int rj = sum + rns - rs;
        lj = lj == sum ? 0 : lj; //分开活,冲和跳
        rj = rj == sum ? 0 : rj;
        int num = (lj > rj ? lj : rj);
        if (num >= 4) return num; //跳四以上不考虑冲
        if (num < 2) return 0; //跳 2 以下无意义
        int l_side = 0, r_side = 0;
        if ((lns + 1 >= ll) ^ (rl == rs))
            l_side = 1;//左冲
        if ((rns + 1 >= rl) ^ (ll == ls))
            r_side = 1; //右冲
        if (lj == num && l_side == 0)
            return num;//活跳
        if (rj == num && r_side == 0)
            return num;//活跳
        return 10 + num;//冲跳,十位标识有障碍形成冲
    }

我们再实现一个算法,判断冲几和活几

private:
//左生存空间 左连续同色个数 同理右
    int isSide(int ll, int ls, int rl, int rs) {
        if (ll == ls ^ rl == rs)
            return ls + rs + 1;
        return 0;
    }
private:
//参数同上
    int isLife(int ll, int ls, int rl, int rs) {
        int num = ls + rs + 1;
        if (num > 4 || (ll > ls && rl > rs))
            return num;
        return 0;
    }

最后我们根据冲,跳,活可以生成某个点所在的线的权重

private:
//后两个参数是代表过点的哪一条线
    int singleLine(int x, int y, int color, int px, int py) {
        int leftLive = oneSide(x, y, color, px, py, 0); //左边生存空间
        int rightLive = oneSide(x, y, color, -px, -py, 0); //右边生存空间
        if (leftLive + rightLive < 4) return 1;//左右生存空间少于 4,此线无意义;
        int leftSame = oneSide(x, y, color, px, py, 1); //左边相邻连续同色
        int rightSame = oneSide(x, y, color, -px, -py, 1); //右边相邻连续同色
        int leftNSame = oneSide(x, y, color, px, py, 2); //左边不相邻( 1 个空位)连续同色
        int rightNSame = oneSide(x, y, color, -px, -py, 2); //右边不相邻( 1 个空位)连续同色
        int life = isLife(leftLive, leftSame, rightLive, rightSame);
        int side = isSide(leftLive, leftSame, rightLive, rightSame);
        int jump = isJump(leftLive, leftSame, leftNSame, rightLive, rightSame, rightNSame);
        return life * 1000 + side * 100 + jump; //千位为活子数,百位冲子数,十为标识个位的跳是否一边被拦
    }

有了求点的4线权重的函数后,我们通过算法,把四线的权重进一步计算,生成每个点的权重

private:
    int weightSum(int x, int y, int color, bool ignoreFour) {
        int weight = 0; // 总权重
        // 如果坐标处有子,则没有权重,如果是对面权重计算需要判断已有局面
        if (chess[x][y].color > 0)
            if (color == computerColor ||
                chess[x][y].color == computerColor) {
                return -1;//如果当前的点是自己的棋,则设置权重为 -1
            }
        获取 横竖 阳线 左斜线 右斜线 阴线 四线权重
        int line[4];
        line[0] = singleLine(x, y, color, 1, 0);
        line[1] = singleLine(x, y, color, 0, 1);
        line[2] = singleLine(x, y, color, 1, 1);
        line[3] = singleLine(x, y, color, -1, 1);
        int doubleLine = 0; //成双线杀的条件,大于等于 2 表示成杀
        int four = 0;//活四及以上或跳四及以上个数
        int op = chess[x][y].color > 0 ? -1 : 1;//已有子的坐标系数为 -1
        for (int i = 0; i < 4; i++) {
            int life = line[i] / 1000;
            int side = line[i] / 100 % 10;
            int jump = line[i] % 100;
            //活 5
            if (life > 4) {
                weight = STEP_KILL;
                //冲 5 或跳 5
                if (side > 4 || jump % 10 > 4)
                    weight = STEP_DANGER;
                return weight * op;
            }
            //活 4
            if (life == 4) {
                weight = STEP_FOUR;
                return weight * op;
            }
            //活三,冲四 ,跳四及以上,活跳三
            if (life == 3 || side == 4 || jump % 10 >= 4 ||
                (jump / 10 == 0 && jump % 10 == 3)) {
                doubleLine++; //满足双杀条件
                weight += ((jump % 10 >= 3) ? 8000 : 10000);  //跳权重少点
                //冲四跳四
                if (side >= 4 || jump % 10 >= 4)
                    four++;
                //活二,冲三,冲跳三
            } else if (life == 2 || side == 3 || jump % 10 == 3)
                weight += 1000;
                //余下权重计算
            else {
                weight = weight + life * 100 + side * 100 +
                         (jump % 10) * 10 - (jump / 10);
                //中心点权重加 100 ,AI 开局就不会乱走
                if (x == 7 && y == 7)
                    weight += 100;
            }
        }
        if (doubleLine > 1)
            weight = STEP_SLAY;//双线成杀
        else if (doubleLine == 1 && four > 0 && ignoreFour)
            weight = STEP_AT_FOUR;// 冲四或跳四
        return weight * op;
    }

我们已经可以计算某个点的权重,那我们把所有权重记录下了,并和坐标对应

private:
    void calculateWeight() {
        for (int i = 0; i < SIZE; i++)
            for (int j = 0; j < SIZE; j++) {
                ownWeight[i][j] = weightSum(i, j, computerColor, false);
                oppositeWeight[i][j] = weightSum(i, j, 3 - computerColor, false);
            }
    }

最后我们取出权重最大的点,并加入一些额外的判断,让其选出最适合的点

public:
    Chess *AIGo(Chess chess[SIZE][SIZE], int color) { // 返回计算机落子
        for (int i = 0; i < SIZE; ++i)
            for (int j = 0; j < SIZE; ++j)
                this->chess[i][j] = chess[i][j];//复制棋盘
        this->computerColor = color;
        calculateWeight(); // 计算权重
        Chess *point = new Chess(); // 定义返回的对象
        point->color = computerColor; //返回的颜色必然是自己要走的颜色
        point->x = -1;
        point->y = -1;
        int x1 = -1, y1 = -1; //己方最大权重记录坐标
        int x2 = -1, y2 = -1; //对方最大权重记录坐标
        int x3 = -1, y3 = -1; //双方权重合值最大记录坐标
        int ownMax = 0; // 己方最大权重
        int oppositeMax = 0; // 对方最大权重
        int oppositeMin = 0; // 对方最小权重
        int sumMax = 0;//双方合起来的权重
        开始分析所有权重
        for (int i = 0; i < SIZE; i++)
            for (int j = 0; j < SIZE; j++) {
                处理己方权重
                if (ownMax < ownWeight[i][j]) {
                    ownMax = ownWeight[i][j]; // 获取己方最大权重
                    x1 = i; // 获取坐标
                    y1 = j;
                    if (debug)
                        printf("owner AIGo:weight=%4d,X=%2d,Y=%2d\n", ownMax, i, j);
                } else if (ownMax == ownWeight[i][j]) {//权重相同,则随机选中一个点
                    if (rand() % 100 > 50) {
                        ownMax = ownWeight[i][j]; // 获取己方最大权重
                        x1 = i; // 获取坐标
                        y1 = j;
                        if (debug)
                            printf("owner AIGo:weight=%4d,X=%2d,Y=%2d\n", ownMax, i, j);
                    }
                }
                //己方已经四连,直接落子
                if (ownMax == STEP_KILL || ownMax == STEP_DANGER) {
                    point->x = x1;
                    point->y = y1;
                    return point;
                }
                ///处理对方权重
                if (oppositeMax < oppositeWeight[i][j]) {
                    oppositeMax = oppositeWeight[i][j]; // 获取对方最大权重
                    x2 = i; // 获取坐标
                    y2 = j;
                    if (debug)
                        printf("opposite AIGo:weight=%4d,X=%2d,Y=%2d\n", ownMax, i, j);
                } else if (oppositeMax == oppositeWeight[i][j])
                    if (rand() % 100 > 50) { //权重相同,则随机选中一个点
                        oppositeMax = oppositeWeight[i][j]; // 获取对方最大权重
                        x2 = i; // 获取坐标
                        y2 = j;
                        if (debug)
                            printf("opposite AIGo:weight=%4d,X=%2d,Y=%2d\n", ownMax, i, j);
                    }
                //最小权重,负的所以是大于
                if (oppositeMin > oppositeWeight[i][j])
                    oppositeMin = oppositeWeight[i][j];
                ///处理双方权重相加
                if (sumMax < ownWeight[i][j] + oppositeWeight[i][j]) {  //两边总权重
                    sumMax = ownWeight[i][j] + oppositeWeight[i][j];
                    x3 = i;
                    y3 = j;
                } else if (sumMax == ownWeight[i][j] + oppositeWeight[i][j]) {
                    if (rand() % 100 > 50) { //权重相同,则随机选中一个点
                        sumMax = ownWeight[i][j] + oppositeWeight[i][j];
                        x3 = i;
                        y3 = j;
                    }
                }
            }

        if (ownMax < 10 && oppositeMax < 10) return point;//和棋
        ///开始根据权重分析形势
        //对方将要 5 连但是可以拦截
        if (oppositeMax == STEP_DANGER) {
            if (debug)
                printf("对面即将五连,X=%2d,Y=%2d\n", x2, y2);
            point->x = x2;
            point->y = y2;
            return point;
        }
        //己方将活四
        if (ownMax == STEP_FOUR) {
            point->x = x1;
            point->y = y1;
            return point;
        }
        //对方已经双线成杀,不拦截,全力冲四跳四
        if (oppositeMin == -STEP_SLAY) {
            if (debug)
                printf("对面双线成杀,X=%2d,Y=%2d\n", x2, y2);
            for (int i = 0; i < SIZE; i++)
                for (int j = 0; j < SIZE; j++) {
                    ownWeight[i][j] = weightSum(i, j, computerColor, true);
                    if (ownWeight[i][j] == STEP_AT_FOUR) {
                        point->x = i;
                        point->y = j;
                        return point;
                    }
                }
        }
        //己方将要双线成杀
        if (ownMax == STEP_SLAY) {
            point->x = x1;
            point->y = y1;
            return point;
        }
        //对面将活四 或 将双线成杀
        if (oppositeMax == STEP_FOUR ||
            oppositeMax == STEP_SLAY) {
            point->x = x2;
            point->y = y2;
            return point;
        }
        //剩下走双方权重相合最大的点
        point->x = x3;
        point->y = y3;
        return point;
    }

至此,整个程序的功能均已完成

完整代码

Bean.h

//
// Created by Simon on 2022/3/5.
//
#define SIZE 15//定义棋盘内行列棋子的个数
#define debug false//是否为 debug 模式
#define UNIT 20//定义 UNIT 单位,1 UNIT 约等于 20 px

#ifndef NANI_BEAN_H
#define NANI_BEAN_H

class Chess {
public:
    int x;//得益于 c++ 的数组问题,数组内的元素无法感知到自己的 x 和 y ,所以需要自己记录
    int y;
    int color = 0;//黑 1 白 2   0 没有  人是 1 AI 是 2
};

#endif //NANI_BEAN_H

SimonAi.h

//
// Created by Simon on 2022/3/5.
//
#include <cstdlib>
#include <valarray>
#include "Bean.h"

#ifndef NANI_SIMONAI_H
#define NANI_SIMONAI_H


class FiveChessAI_simon {
// AI 算法,本算法防守为主,可攻则攻
private:
    const int STEP_KILL = 99999;//活 5
    const int STEP_DANGER = 99998;//冲 5 或跳 5
    const int STEP_FOUR = 88888;//出现有两个点可以成五的四
    const int STEP_SLAY = 77777;//双线成杀
    const int STEP_AT_FOUR = 44444;
    Chess chess[SIZE][SIZE]; // 接受棋盘的所有棋子
    int ownWeight[SIZE][SIZE] = {0,}; // 己方每个点权重
    int oppositeWeight[SIZE][SIZE] = {0,}; // 对方每个点权重
    int computerColor; // 电脑要走的棋子颜色 黑 1  白 2 ,0 为空 默认人类 1 AI 2
    /**
     * 五子棋 AI
     *
     * @author Simon
     */
public:
    Chess *AIGo(Chess chess[SIZE][SIZE], int color) { // 返回计算机落子
        for (int i = 0; i < SIZE; ++i)
            for (int j = 0; j < SIZE; ++j)
                this->chess[i][j] = chess[i][j];//复制棋盘
        this->computerColor = color;
        calculateWeight(); // 计算权重
        Chess *point = new Chess(); // 定义返回的对象
        point->color = computerColor; //返回的颜色必然是自己要走的颜色
        point->x = -1;
        point->y = -1;
        int x1 = -1, y1 = -1; //己方最大权重记录坐标
        int x2 = -1, y2 = -1; //对方最大权重记录坐标
        int x3 = -1, y3 = -1; //双方权重合值最大记录坐标
        int ownMax = 0; // 己方最大权重
        int oppositeMax = 0; // 对方最大权重
        int oppositeMin = 0; // 对方最小权重
        int sumMax = 0;//双方合起来的权重
        开始分析所有权重
        for (int i = 0; i < SIZE; i++)
            for (int j = 0; j < SIZE; j++) {
                处理己方权重
                if (ownMax < ownWeight[i][j]) {
                    ownMax = ownWeight[i][j]; // 获取己方最大权重
                    x1 = i; // 获取坐标
                    y1 = j;
                    if (debug)
                        printf("owner AIGo:weight=%4d,X=%2d,Y=%2d\n", ownMax, i, j);
                } else if (ownMax == ownWeight[i][j]) {//权重相同,则随机选中一个点
                    if (rand() % 100 > 50) {
                        ownMax = ownWeight[i][j]; // 获取己方最大权重
                        x1 = i; // 获取坐标
                        y1 = j;
                        if (debug)
                            printf("owner AIGo:weight=%4d,X=%2d,Y=%2d\n", ownMax, i, j);
                    }
                }
                //己方已经四连,直接落子
                if (ownMax == STEP_KILL || ownMax == STEP_DANGER) {
                    point->x = x1;
                    point->y = y1;
                    return point;
                }
                ///处理对方权重
                if (oppositeMax < oppositeWeight[i][j]) {
                    oppositeMax = oppositeWeight[i][j]; // 获取对方最大权重
                    x2 = i; // 获取坐标
                    y2 = j;
                    if (debug)
                        printf("opposite AIGo:weight=%4d,X=%2d,Y=%2d\n", ownMax, i, j);
                } else if (oppositeMax == oppositeWeight[i][j])
                    if (rand() % 100 > 50) { //权重相同,则随机选中一个点
                        oppositeMax = oppositeWeight[i][j]; // 获取对方最大权重
                        x2 = i; // 获取坐标
                        y2 = j;
                        if (debug)
                            printf("opposite AIGo:weight=%4d,X=%2d,Y=%2d\n", ownMax, i, j);
                    }
                //最小权重,负的所以是大于
                if (oppositeMin > oppositeWeight[i][j])
                    oppositeMin = oppositeWeight[i][j];
                ///处理双方权重相加
                if (sumMax < ownWeight[i][j] + oppositeWeight[i][j]) {  //两边总权重
                    sumMax = ownWeight[i][j] + oppositeWeight[i][j];
                    x3 = i;
                    y3 = j;
                } else if (sumMax == ownWeight[i][j] + oppositeWeight[i][j]) {
                    if (rand() % 100 > 50) { //权重相同,则随机选中一个点
                        sumMax = ownWeight[i][j] + oppositeWeight[i][j];
                        x3 = i;
                        y3 = j;
                    }
                }
            }

        if (ownMax < 10 && oppositeMax < 10) return point;//和棋
        ///开始根据权重分析形势
        //对方将要 5 连但是可以拦截
        if (oppositeMax == STEP_DANGER) {
            if (debug)
                printf("对面即将五连,X=%2d,Y=%2d\n", x2, y2);
            point->x = x2;
            point->y = y2;
            return point;
        }
        //己方将活四
        if (ownMax == STEP_FOUR) {
            point->x = x1;
            point->y = y1;
            return point;
        }
        //对方已经双线成杀,不拦截,全力冲四跳四
        if (oppositeMin == -STEP_SLAY) {
            if (debug)
                printf("对面双线成杀,X=%2d,Y=%2d\n", x2, y2);
            for (int i = 0; i < SIZE; i++)
                for (int j = 0; j < SIZE; j++) {
                    ownWeight[i][j] = weightSum(i, j, computerColor, true);
                    if (ownWeight[i][j] == STEP_AT_FOUR) {
                        point->x = i;
                        point->y = j;
                        return point;
                    }
                }
        }
        //己方将要双线成杀
        if (ownMax == STEP_SLAY) {
            point->x = x1;
            point->y = y1;
            return point;
        }
        //对面将活四 或 将双线成杀
        if (oppositeMax == STEP_FOUR ||
            oppositeMax == STEP_SLAY) {
            point->x = x2;
            point->y = y2;
            return point;
        }
        //剩下走双方权重相合最大的点
        point->x = x3;
        point->y = y3;
        return point;
    }
    获取双方所有坐标的权重
private:
    void calculateWeight() {
        for (int i = 0; i < SIZE; i++)
            for (int j = 0; j < SIZE; j++) {
                ownWeight[i][j] = weightSum(i, j, computerColor, false);
                oppositeWeight[i][j] = weightSum(i, j, 3 - computerColor, false);
            }
    }

    分析一个点所能形成的局势,用权重表示
private:
    int weightSum(int x, int y, int color, bool ignoreFour) {
        int weight = 0; // 总权重
        // 如果坐标处有子,则没有权重,如果是对面权重计算需要判断已有局面
        if (chess[x][y].color > 0)
            if (color == computerColor ||
                chess[x][y].color == computerColor) {
                return -1;//如果当前的点是自己的棋,则设置权重为 -1
            }
        获取 横竖 阳线 左斜线 右斜线 阴线 四线权重
        int line[4];
        line[0] = singleLine(x, y, color, 1, 0);
        line[1] = singleLine(x, y, color, 0, 1);
        line[2] = singleLine(x, y, color, 1, 1);
        line[3] = singleLine(x, y, color, -1, 1);
        int doubleLine = 0; //成双线杀的条件,大于等于 2 表示成杀
        int four = 0;//活四及以上或跳四及以上个数
        int op = chess[x][y].color > 0 ? -1 : 1;//已有子的坐标系数为 -1
        for (int i = 0; i < 4; i++) {
            int life = line[i] / 1000;
            int side = line[i] / 100 % 10;
            int jump = line[i] % 100;
            //活 5
            if (life > 4) {
                weight = STEP_KILL;
                //冲 5 或跳 5
                if (side > 4 || jump % 10 > 4)
                    weight = STEP_DANGER;
                return weight * op;
            }
            //活 4
            if (life == 4) {
                weight = STEP_FOUR;
                return weight * op;
            }
            //活三,冲四 ,跳四及以上,活跳三
            if (life == 3 || side == 4 || jump % 10 >= 4 ||
                (jump / 10 == 0 && jump % 10 == 3)) {
                doubleLine++; //满足双杀条件
                weight += ((jump % 10 >= 3) ? 8000 : 10000);  //跳权重少点
                //冲四跳四
                if (side >= 4 || jump % 10 >= 4)
                    four++;
                //活二,冲三,冲跳三
            } else if (life == 2 || side == 3 || jump % 10 == 3)
                weight += 1000;
                //余下权重计算
            else {
                weight = weight + life * 100 + side * 100 +
                         (jump % 10) * 10 - (jump / 10);
                //中心点权重加 100 ,AI 开局就不会乱走
                if (x == 7 && y == 7)
                    weight += 100;
            }
        }
        if (doubleLine > 1)
            weight = STEP_SLAY;//双线成杀
        else if (doubleLine == 1 && four > 0 && ignoreFour)
            weight = STEP_AT_FOUR;// 冲四或跳四
        return weight * op;
    }

    单线权重计算
private:
    int singleLine(int x, int y, int color, int px, int py) {
        int leftLive = oneSide(x, y, color, px, py, 0); //左边生存空间
        int rightLive = oneSide(x, y, color, -px, -py, 0); //右边生存空间
        if (leftLive + rightLive < 4) return 1;//左右生存空间少于 4,此线无意义;
        int leftSame = oneSide(x, y, color, px, py, 1); //左边相邻连续同色
        int rightSame = oneSide(x, y, color, -px, -py, 1); //右边相邻连续同色
        int leftNSame = oneSide(x, y, color, px, py, 2); //左边不相邻( 1 个空位)连续同色
        int rightNSame = oneSide(x, y, color, -px, -py, 2); //右边不相邻( 1 个空位)连续同色
        int life = isLife(leftLive, leftSame, rightLive, rightSame);
        int side = isSide(leftLive, leftSame, rightLive, rightSame);
        int jump = isJump(leftLive, leftSame, leftNSame, rightLive, rightSame, rightNSame);
        return life * 1000 + side * 100 + jump; //千位为活子数,百位冲子数,十为标识个位的跳是否一边被拦
    }

    判断活几
private:
    int isLife(int ll, int ls, int rl, int rs) {
        int num = ls + rs + 1;
        if (num > 4 || (ll > ls && rl > rs))
            return num;
        return 0;
    }

    判断冲几
private:
    int isSide(int ll, int ls, int rl, int rs) {
        if (ll == ls ^ rl == rs)
            return ls + rs + 1;
        return 0;
    }

    判断跳几
private:
    int isJump(int ll, int ls, int lns, int rl, int rs, int rns) {
        int sum = ls + rs + 1;
        int lj = sum + lns - ls;
        int rj = sum + rns - rs;
        lj = lj == sum ? 0 : lj; //分开活,冲和跳
        rj = rj == sum ? 0 : rj;
        int num = (lj > rj ? lj : rj);
        if (num >= 4) return num; //跳四以上不考虑冲
        if (num < 2) return 0; //跳 2 以下无意义
        int l_side = 0, r_side = 0;
        if ((lns + 1 >= ll) ^ (rl == rs))
            l_side = 1;//左冲
        if ((rns + 1 >= rl) ^ (ll == ls))
            r_side = 1; //右冲
        if (lj == num && l_side == 0)
            return num;//活跳
        if (rj == num && r_side == 0)
            return num;//活跳
        return 10 + num;//冲跳,十位标识有障碍形成冲
    }

    单线参数获取
private:
    int oneSide(int x, int y, int color, int px, int py, int mode) {
        int num = 0, space = 0;
        while (!(x + px < 0 || x + px > (SIZE - 1) ||
                 y + py < 0 || y + py > (SIZE - 1))) {
            x += px;
            y += py;
            if (chess[x][y].color == 3 - color) break;
            if (mode == 1 && chess[x][y].color != color) break;
            if (mode == 2 && chess[x][y].color == 0) {
                space++;
                if (space > 1) break;
                continue;
            }
            num++;
        }
        return num;
    }

};

#endif //NANI_SIMONAI_H

main.cpp

#include <string>
#include <conio.h>
#include <graphics.h>
#include "Bean.h"
#include "SimonAi.h"

#define ToolHeight UNIT* 3// 标题栏所占的高度

// 0 没下棋 1 黑 2 白
static int HW = 16; // 窗口大小,单位 UNIT( UNIT 声明在 Bean.h 中)
static time_t start; // 程序开始时的时间
struct Chess data[SIZE][SIZE]; // 所有棋子的集合
static int mouseX = UNIT, mouseY = UNIT; // 鼠标的 x 和 y,把他们拿到外面,是为了在 debug 模式下输出鼠标信息
static int centerX, length, centerY; // 棋盘中心的 x , y 以及棋盘长度
static int mouseI, mouseJ; // 鼠标所在区域的 x, y

enum Turn // 这里其实可以删掉,没有必要做枚举
{
    HUMAN, AI
} turn = HUMAN;

int sameLine(int x, int y, int mLastx, int mLasty) // 判断一条线有多少个同色子
{
    int num = 0; // 同一直线同色棋子数
    int DefaultColor = data[mLastx][mLasty].color;
    if (DefaultColor == 0)return 0; // 遇到 0,直接跳
    int xx;
    int yy;
    // 1,0 横着 0,1 竖着
    if (x == 1 && y == 0) {
        for (xx = mLastx; xx >= 0; xx--) {// 向左判断
            if (data[xx][mLasty].color == DefaultColor) {
                num++;
            } else break;
        }
        for (xx = mLastx; xx < (SIZE - 1); xx++) {// 向右判断
            if (data[xx][mLasty].color == DefaultColor) {
                num++;
            } else break;
        }
    } else if (x == 0 && y == 1) {
        for (yy = mLasty; yy >= 0; yy--) {// 向上判断
            if (data[mLastx][yy].color == DefaultColor) {
                num++;
            } else break;
        }
        for (yy = mLasty; yy < (SIZE - 1); yy++) {// 向下判断
            if (data[mLastx][yy].color == DefaultColor) {
                num++;
            } else break;
        }
    } else if (x == 1 && y == 1) {
        for (xx = mLastx, yy = mLasty; xx < (SIZE - 1) && yy >= 0; xx++, yy--) {// 右上判断
            if (data[xx][yy].color == DefaultColor) {
                num++;
            } else break;
        }
        for (xx = mLastx, yy = mLasty; xx >= 0 && yy < (SIZE - 1); yy++, xx--) {// 左下判断
            if (data[xx][yy].color == DefaultColor) {
                num++;
            } else break;
        }
    } else if (x == -1 && y == 1) {
        for (xx = mLastx, yy = mLasty; xx >= 0 && yy >= 0; yy--, xx--) {// 左上判断
            if (data[xx][yy].color == DefaultColor) {
                num++;
            } else break;
        }
        for (xx = mLastx, yy = mLasty; xx < (SIZE - 1) && yy < (SIZE - 1); yy++, xx++) {// 右下判断
            if (data[xx][yy].color == DefaultColor) {
                num++;
            } else break;
        }
    }
    return --num;
}

bool isFive() {// 在每一个点判断是否存在 5 子连线
    for (int i = 0; i < SIZE; ++i) {
        for (int j = 0; j < SIZE; ++j) {
            if (sameLine(1, 0, i, j) >= 5 || sameLine(0, 1, i, j) >= 5 || sameLine(1, 1, i, j) >= 5 ||
                sameLine(-1, 1, i, j) >= 5)
                return true;
        }
    }
    return false;
}

void ObserveMouse() {
    if (MouseHit()) {
        MOUSEMSG msg = GetMouseMsg();
        mouseX = msg.x;
        mouseY = msg.y;
        // 下面寻找 X 和 Y 是一个优雅做法,如果两层 for 嵌套,处理不好很容易浪费性能
        for (int i = 0; i < SIZE; ++i)// 获取当前鼠标在棋盘上对应位置的 X
            if (centerX - length / 2 + UNIT * i < mouseX)
                if (mouseX < centerX - length / 2 + UNIT * i + UNIT) {
                    mouseI = i;
                    break;
                }
        for (int j = 0; j < SIZE; ++j)// 获取当前鼠标在棋盘上对应位置的 Y
            if (centerY - length / 2 + UNIT * j < mouseY)
                if (mouseY < centerY - length / 2 + UNIT * j + UNIT) {
                    mouseJ = j;
                    break;
                }
        if (data[mouseI][mouseJ].color != 0)return;
        if (msg.mkLButton && msg.uMsg != WM_MOUSEMOVE) {// 如果只监听点击事件的话,会出现按住左键移动鼠标,下了一排棋
            if (turn == HUMAN) {
                turn = AI;
                data[mouseI][mouseJ].color = 1;// 人下完了,换 AI
                if (isFive())return;
            }
            if (turn == AI) {
                FiveChessAI_simon AI;
                Chess *chess = AI.AIGo(data, 2);// 2 代表的是 AI 的颜色
                data[chess->x][chess->y].x = chess->x;
                data[chess->x][chess->y].y = chess->y;
                data[chess->x][chess->y].color = chess->color;
                turn = HUMAN;
                if (isFive())return;
            }
        }
        FlushMouseMsgBuffer();
    }
    if (data[mouseI][mouseJ].color != 0)return;
    setfillcolor(RGB(0XEE, 0XDE, 0XB0));// #eedeb0
    solidcircle(centerX - length / 2 + UNIT * mouseI + UNIT / 2, centerY - length / 2 + UNIT * mouseJ + UNIT / 2,
                UNIT / 2 - 3);// 绘制鼠标选中但还未落子的区域
}

void drawToolBar() {
    static int R = 10;// 这里的 RGB 是实现标题流光的效果
    static int G = 10;
    static int B = 10;
    R = R + 2;
    G = R * 3;
    B = B + 4;
    if (++R > 240)
        R = 10;
    if (++G > 240)
        G = 10;
    if (++B > 240)
        B = 10;
    setfillcolor(WHITE);
    solidrectangle(0, 0, HW * UNIT, ToolHeight);// 画出标题背景色
    setbkmode(TRANSPARENT);
    settextcolor(RGB(R, G, B));
    settextstyle(UNIT, 0, nullptr);
    char *title = "Simon 的 AI 五子棋";
    char version[30] = "EasyX 版本";
    strcat(version, GetEasyXVer());
    outtextxy(UNIT, (ToolHeight - UNIT) / 2, title);
    settextcolor(BLACK);
    outtextxy(UNIT, (ToolHeight - UNIT) / 2 - UNIT, version);// 绘制标题和 EasyX 版本

    if (debug) {// 判断是否开启 debug 模式,如果开启,则输出鼠标的 X 和 Y 的信息,不开启则输出游戏时长
        char mouse[30];
        sprintf(mouse, "x:%d y:%d", mouseX, mouseY);
        outtextxy(UNIT, (ToolHeight - UNIT) / 2 + UNIT, mouse);
    } else {
        char time[30];
        sprintf(time, "运行时长:%d 秒", (clock() - start) / CLOCKS_PER_SEC);
        outtextxy(UNIT, (ToolHeight - UNIT) / 2 + UNIT, time);
    }
}

void drawBoard() {
    setfillcolor(RGB(0xe9, 0xe7, 0xef));
    solidrectangle(0, ToolHeight, HW * UNIT, HW * UNIT + ToolHeight);//绘制标题以下的区域颜色
    centerX = HW * UNIT / 2;// 算出棋盘中心的 X,Y
    centerY = centerX + ToolHeight;
    length = SIZE * UNIT;// 计算出棋盘的边长
    POINT pts[] = {// 棋盘的四个点
            {centerX - length / 2, centerY - length / 2},
            {centerX + length / 2, centerY - length / 2},
            {centerX + length / 2, centerY + length / 2},
            {centerX - length / 2, centerY + length / 2}
    };
    setfillcolor(RGB(0x41, 0x55, 0x5d));
    solidpolygon(pts, 4);// 绘制棋盘底色

    setlinecolor(BLACK);
    // 绘制棋盘线条
    for (int i = 0; i < (SIZE + 1); ++i) {// 竖着
        line(centerX - length / 2 + UNIT * i, centerY - length / 2, centerX - length / 2 + UNIT * i,
             centerY + length / 2);
    }
    for (int i = 0; i < (SIZE + 1); ++i) {// 横着
        line(centerX - length / 2, centerY - length / 2 + UNIT * i, centerX + length / 2,
             centerY - length / 2 + UNIT * i);
    }
    for (int i = 0; i < SIZE; ++i) {// 棋子
        for (int j = 0; j < SIZE; ++j) {
            if (data[i][j].color == 0)continue;
            setfillcolor(data[i][j].color == 1 ? BLACK : WHITE);
            solidcircle(centerX - length / 2 + UNIT * i + UNIT / 2, centerY - length / 2 + UNIT * j + UNIT / 2,
                        UNIT / 2 - 3);
        }
    }
}

int main() {
    start = clock();
    srand(time(0));// 初始化随机数种子,为了在 AI 下棋时具有随机加权
    initgraph(HW * UNIT, HW * UNIT + ToolHeight, debug ? SHOWCONSOLE : NULL);// 如果是 debug 模式,则开启控制台
    while (true) {// 开启帧驱动
        BeginBatchDraw();// 收集绘制
        drawToolBar();// 绘制标题栏
        drawBoard();// 绘制棋盘
        ObserveMouse();// 监听鼠标
        if (isFive()) // 是否有 5
            goto end;
        EndBatchDraw();// 开始绘制
    }
    end:// 游戏结束后的绘制
    {
        setfillcolor(WHITE);
        solidrectangle(0, 0, HW * UNIT, ToolHeight);
        outtextxy(UNIT, (ToolHeight - UNIT) / 2, "高下立判啦~~");
        drawBoard();// 绘制出绝杀的那步棋
        EndBatchDraw();// 很关键的一行
    }

    getchar();
    return 0;
}

结语

如果存在有和我雷同的代码,那一定是他抄袭的我的(笑)
本来想把这个代码传到codebus(easyX社区)的,但是社区发布代码,必须严格遵守他们设定的代码规范,我这种规范的JAVA式风格,在代码检查中出现1500多个错误。
本人目前的精力实在有限,还是待我改日再试一番吧!

  • 5
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

有头发的琦玉

打点钱,我会再努力的

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值