博学谷人工智能AI进阶022最新

## download:博学谷人工智能AI进阶022最新

五子棋AI入门

前言

本文只是引见五子棋AI的完成,最终的废品只是一个 AI 接口,并不包括 GUI,且不依赖 GUI

五子棋 AI 的完成并不难,只需求处理一个问题就行:

怎样肯定AI的最佳落子位置?

普通状况下,五子棋棋盘是由15条横线和15条纵线组合而成的,15x15 的棋盘共有 225 个穿插点,也就是说共有 225 个落子点。

假设说,AI 是黑棋,先行落子,所以 AI 总共有 225 个落子点能够选择,我们能够对每个落子点停止评价打分,哪个分高低哪里,这样我们就能肯定最佳落子点了。

但这样又引出了一个新的问题:

怎样对落子点停止评价打分呢?

这就是本文的重点了,请看后文!

完成过程

笼统

注:局部根底代码依赖于 lombok,请自行引入,或手写根底代码。

落子位置实体类,这里我们定义棋子类型字段:type1表示黑子,2表示白子。

/**
 * 棋子点位
 *
 * @author anlingyi
 * @date 2021/11/10
 */
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class Point {
    /**
     * 横坐标
     */
    int x;
    /**
     * 纵坐标
     */
    int y;
    /**
     * 棋子类型 1.黑 2.白
     */
    int type;
}
复制代码

AI 对外提供的接口,不会依赖任何 GUI 代码,便当其他程序调用。

/**
 * 五子棋AI接口
 *
 * @author anlingyi
 * @date 2021/11/10
 */
public interface AIService {
    /**
     * 获取AI棋位
     *
     * @param chessData 已下棋子数据
     * @param point     对手棋位
     * @param started   能否刚开局
     * @return
     */
    Point getPoint(int[][] chessData, Point point, boolean started);
}
复制代码

这个接口需求晓得我们如今的棋盘落子数据 chessData,还有对手上一步的落子位置 pointstarted 参数表示能否是刚开局,后续可能对刚开局状况做单独的处置。

完成AI接口

我们创立一个类 ZhiZhangAIService,这个类完成 AIService 接口,来写我们的完成逻辑。

/**
 *
 * 五子棋AI完成
 *
 * @author anlingyi
 * @date 2021/11/10
 */
public class ZhiZhangAIService implements AIService {
    /**
     * 已下棋子数据
     */
    private int[][] chessData;
    /**
     * 棋盘行数
     */
    private int rows;
    /**
     * 棋盘列数
     */
    private int cols;
    /**
     * AI棋子类型
     */
    private int ai;
    /**
     * 声明一个最大值
     */
    private static final int INFINITY = 999999999;
    @Override
    public Point getPoint(int[][] chessData, Point point, boolean started) {
    	// 初始化棋盘数据
    	initChessData(chessData);
    	// 计算AI的棋子类型
        this.ai = 3 - point.type;
        if (started) {
            // AI先下,首子天元
            int centerX = this.cols / 2;
            int centerY = this.rows / 2;
            return new Point(centerX, centerY, this.ai);
        }
        // 获取最佳下棋点位
        return getBestPoint();
    }
    /**
     * 初始化棋盘数据
     * 
     * @param chessData 当前棋盘数据
     */
    private void initChessData(int[][] chessData) {
    	// 获取棋盘行数
        this.rows = chessData.length;
        // 获取棋盘列数
        this.cols = chessData[0].length;
        // 初始化棋盘数据
        this.chessData = new int[this.cols][this.rows];
        // 深拷贝
        for (int i = 0; i < cols; i++) {
            for (int j = 0; j < rows; j++) {
                this.chessData[i][j] = chessData[i][j];
            }
        }
    }
    /**
     * 获取最佳下棋点位
     *
     * @return
     */
    private Point getBestPoint() {
	Point best = null;
        // 初始分值为最小
        int score = -INFINITY;
        /* 遍历一切能下棋的点位,评价各个点位的分值,选择分值最大的点位 */
        for (int i = 0; i < this.cols; i++) {
            for (int j = 0; j < this.rows; j++) {
                if (this.chessData[i][j] != 0) {
                    // 该点已有棋子,跳过
                    continue;
                }
                Point p = new Point(i, j, this.ai);
                // 评价该点AI得分
                int val = evaluate(p);
                // 选择得分最高的点位
                if (val > score) {
                    // 最高分被刷新
                    score = val;
                    // 更新最佳点位
                    best = p;
                }
            }
        }
        return best;
    }
    /**
     * 对当前棋位停止评价
     *
     * @param point 当前棋位
     * @return
     */
    private int evaluate(Point point) {
    	// 中心
    }
}
复制代码

首先看 getPoint 办法,这个是 AI 的出入口办法,我们要对传入的棋盘数据做一个初始化,调用 initChessData 办法,计算出当前游戏的棋盘行数、列数,并且拷贝了一份棋子数据到本地(深拷贝还是浅拷贝视状况而定)。

this.ai = 3 - point.type;
复制代码

这行代码能够计算出AI是执黑子还是执白子,应该很好了解。

if (started) {
    // AI先下,首子天元
    int centerX = this.cols / 2;
    int centerY = this.rows / 2;
    return new Point(centerX, centerY, this.ai);
}
复制代码

这段代码是处置刚开局时 AI 先行落子的状况,我们这边是简单的将落子点肯定为棋盘中心位置(天元)。开局状况的落子我们能够本人定义,并不是固定的,只是说天元的位置比拟好而已。

    private Point getBestPoint() {
	Point best = null;
        // 初始分值为最小
        int score = -INFINITY;
        /* 遍历一切能下棋的点位,评价各个点位的分值,选择分值最大的点位 */
        for (int i = 0; i < this.cols; i++) {
            for (int j = 0; j < this.rows; j++) {
                if (this.chessData[i][j] != 0) {
                    // 该点已有棋子,跳过
                    continue;
                }
                Point p = new Point(i, j, this.ai);
                // 评价该点AI得分
                int val = evaluate(p);
                // 选择得分最高的点位
                if (val > score) {
                    // 最高分被刷新
                    score = val;
                    // 更新最佳点位
                    best = p;
                }
            }
        }
        return best;
    }
复制代码

然后就到了我们最主要的办法了 getBestPoint,这个办法用于选择出 AI 的最佳落子位置。这个办法的思绪就是遍历棋盘上一切能下棋的点,然后对这个点停止评分,假如这个点的评分比之前点的评分高,就更新当前最佳落子点位,并更新最高分,一切的落子点都评价完成之后,我们就能肯定最好的点位在哪了。

   /**
     * 对当前棋位停止评价
     *
     * @param point 当前棋位
     * @return
     */
    private int evaluate(Point point) {
    	// 中心
    }
复制代码

最后就是评价函数的完成了。

评价函数

在写评价函数之前,我们要先理解一下五子棋的几种棋型。(还不熟的朋友,五子棋入门理解一下:和那威学五子棋)

在这里,我把五子棋棋型大致分为:连五活四冲四活三眠三活二眠二眠一 等共8种棋型。

0:空位 1:黑子 2:白子
连五:11111
活四:011110
冲四:21111
活三:001110
眠三:211100
活二:001100
眠二:001120
眠一:001200
复制代码

冲四活三 假如构成,赢的可能性很大,活四 假如构成,棋局胜负根本肯定,连五 构成就曾经赢了。所以说,假如 AI 落的点可以构成这几种胜率很高的棋型的话,我们要给这个点评一个高分,这样对 AI 最有利。

我这边定义好了各个棋型的分数状况

棋型分数
连五10000000
活四1000000
活三10000
冲四8000
眠三1000
活二800
眠二50
眠一10

评价模型的笼统

我们创立一个枚举内部类,然后定义这几种棋型和它的分数。

    @AllArgsConstructor
    private enum ChessModel {
        /**
         * 连五
         */
        LIANWU(10000000, new String[]{"11111"}),
        /**
         * 活四
         */
        HUOSI(1000000, new String[]{"011110"}),
        /**
         * 活三
         */
        HUOSAN(10000, new String[]{"001110", "011100", "010110", "011010"}),
        /**
         * 冲四
         */
        CHONGSI(8000, new String[]{"11110", "01111", "10111", "11011", "11101"}),
        /**
         * 眠三
         */
        MIANSAN(1000, new String[]{"001112", "010112", "011012", "211100", "211010"}),
        /**
         * 活二
         */
        HUOER(800, new String[]{"001100", "011000", "000110"}),
        /**
         * 眠二
         */
        MIANER(50, new String[]{"011200", "001120", "002110", "021100", "001010", "010100"}),
        /**
         * 眠一
         */
        MIANYI(10, new String[]{"001200", "002100", "020100", "000210", "000120"});
        /**
         * 分数
         */
        int score;
        /**
         * 局势数组
         */
        String[] values;
    }
复制代码

为了评价便当,我们能够把一切定义好的棋型以及棋型对应的分数存入 Hash 表。

创立一个 LinkedHashMap 类型的类变量 SCORE,然后在静态代码块内停止初始化。

    /**
     * 棋型分数表
     */
    private static final Map<String, Integer> SCORE = new LinkedHashMap<>();
    static {
        // 初始化棋型分数表
        for (ChessModel chessScore : ChessModel.values()) {
            for (String value : chessScore.values) {
                SCORE.put(value, chessScore.score);
            }
        }
    }
复制代码

判别落子点位的棋型

棋型和分数都定义好了,如今我们要晓得一个点位它的棋型的状况,这样才干评价这个点位的分数。

我们以落子点位为中心,分横、纵、左斜、右斜等4个大方向,分别取出各方向的9个点位的棋子,每个方向的9个棋子都组合成一个字符串,然后匹配现有的棋型数据,累积分值,这样就计算出了这个点位的分数了。

对横、纵、左斜、右斜做如上操作,能够得出:

横:000111000 -> 活三 +10000
纵:000210000 -> 眠一 +10
左斜:000210000 -> 眠一 +10
右斜:000010000 -> 未匹配到棋型 +0
复制代码

所以这个点位总得分为:

10000 + 10 + 10 + 0 = 10020
复制代码

代码完成:

    /**
     * 获取局势分数
     *
     * @param situation 局势
     * @return
     */
    private int getScore(String situation) {
        for (String key : SCORE.keySet()) {
            if (situation.contains(key)) {
                return SCORE.get(key);
            }
        }
        return 0;
    }
    /**
     * 获取棋位局势
     *
     * @param point     当前棋位
     * @param direction 大方向 1.横 2.纵 3.左斜 4.右斜
     * @return
     */
    private String getSituation(Point point, int direction) {
        // 下面用到了relativePoint函数,依据传入的四个大方向做转换
        direction = direction * 2 - 1;
        // 以下是将各个方向的棋子拼接成字符串返回
        StringBuilder sb = new StringBuilder();
        appendChess(sb, point, direction, 4);
        appendChess(sb, point, direction, 3);
        appendChess(sb, point, direction, 2);
        appendChess(sb, point, direction, 1);
        sb.append(1); // 当前棋子统一标志为1(黑)
        appendChess(sb, point, direction + 1, 1);
        appendChess(sb, point, direction + 1, 2);
        appendChess(sb, point, direction + 1, 3);
        appendChess(sb, point, direction + 1, 4);
        return sb.toString();
    }
    /**
     * 拼接各个方向的棋子
     * 

* 由于现有评价模型是对黑棋停止评价 * 所以,为了便当对局势停止评价,假如当前是白棋方,需求将扫描到的白棋转换为黑棋,黑棋转换为白棋 * 如:point(x=0,y=0,type=2) 即当前为白棋方 * 扫描到的某个方向局势为:20212 -> 转换后 -> 10121 * * @param sb 字符串容器 * @param point 当前棋子 * @param direction 方向 1.左横 2.右横 3.上纵 4.下纵 5.左斜上 6.左斜下 7.右斜上 8.右斜下 * @param offset 偏移量 */ private void appendChess(StringBuilder sb, Point point, int direction, int offset) { int chess = relativePoint(point, direction, offset); if (chess > -1) { if (point.type == 2) { // 对白棋停止转换 if (chess > 0) { // 对棋子颜色停止转换,2->1,1->2 chess = 3 - chess; } } sb.append(chess); } } /** * 获取相对点位棋子 * * @param point 当前棋位 * @param direction 方向 1.左横 2.右横 3.上纵 4.下纵 5.左斜上 6.左斜下 7.右斜上 8.右斜下 * @param offset 偏移量 * @return -1:越界 0:空位 1:黑棋 2:白棋 */ private int relativePoint(Point point, int direction, int offset) { int x = point.x, y = point.y; switch (direction) { case 1: x -= offset; break; case 2: x += offset; break; case 3: y -= offset; break; case 4: y += offset; break; case 5: x += offset; y -= offset; break; case 6: x -= offset; y += offset; break; case 7: x -= offset; y -= offset; break; case 8: x += offset; y += offset; break; } if (x < 0 || y < 0 || x >= this.cols || y >= this.rows) { // 越界 return -1; } // 返回该位置的棋子 return this.chessData[x][y]; } 复制代码

评价函数的完成

到这一步,我们曾经能晓得某个落子点位的各个方向的局势,又能经过局势获取到对应的分值,这样一来,评价函数就很好写了,评价函数要做的就是累积4个方向的分值,然后返回就行。

    /**
     * 对当前棋位停止评价
     *
     * @param point 当前棋位
     * @return
     */
    private int evaluate(Point point) {
        // 分值
        int score = 0;
        for (int i = 1; i < 5; i++) {
            // 获取该方向的局势
            String situation = getSituation(point, i);
            // 下此步的得分
            score += getScore(situation);
        }
        return score;
    }
复制代码

如今,曾经能够将我们写的 AI 接入GUI 程序做测试了。假如还没有 GUI,也能够本人写个测试办法,只需依照办法的入参信息传入就行,办法输出的就是 AI 下一步的落子位置。

    /**
     * 获取AI棋位
     *
     * @param chessData 已下棋子数据
     * @param point     对手棋位
     * @param started   能否刚开局
     * @return
     */
    Point getPoint(int[][] chessData, Point point, boolean started);

测试了一下,如今的 AI 只晓得进攻,不晓得防卫,所以我们需求对 getBestPoint 办法停止优化。之前只对 AI 落子停止了评价,如今我们也要对敌方落子停止评价,然后累积分值,这样能够进步 AI 的防卫力度。

    private Point getBestPoint() {
	Point best = null;
        // 初始分值为最小
        int score = -INFINITY;
        /* 遍历一切能下棋的点位,评价各个点位的分值,选择分值最大的点位 */
        for (int i = 0; i < this.cols; i++) {
            for (int j = 0; j < this.rows; j++) {
                if (this.chessData[i][j] != 0) {
                    // 该点已有棋子,跳过
                    continue;
                }
                Point p = new Point(i, j, this.ai);
                // 该点得分 = AI落子得分 + 对手落子得分
                int val = evaluate(p) + evaluate(new Point(i, j, 3 - this.ai));
                // 选择得分最高的点位
                if (val > score) {
                    // 最高分被刷新
                    score = val;
                    // 更新最佳点位
                    best = p;
                }
            }
        }
        return best;
    }
复制代码

只要这行代码停止了改动,如今加上了对手落子到该点的得分。

// 该点得分 = AI落子得分 + 对手落子得分
int val = evaluate(p) + evaluate(new Point(i, j, 3 - this.ai));
复制代码

再次测试,如今 AI 棋力还是太普通,防卫才能是进步了,但还是输给了我这个“臭棋篓子”。

有一些局势的评分需求进步,例如:

  • 活三又活二
  • 冲四又活二
  • 两个或两个以上的活三
  • 冲四又活三

上面这些状况都得加一些分数,假如分数太普通,AI 棋力就会很普通以至更弱,能够说目前的 AI 只能算是一个刚入门五子棋的新手。

我这边对这些状况的处置是这样的:

  • 活三又活二:总分x2
  • 冲四又活二:总分x4
  • 两个或两个以上的活三:总分x6
  • 冲四又活三:总分x8

新增一个办法,用于判别当前局势是属于什么棋型

    /**
     * 检查当前局势能否处于某个局势
     *
     * @param situation  当前局势
     * @param chessModel 检查的局势
     * @return
     */
    private boolean checkSituation(String situation, ChessModel chessModel) {
        for (String value : chessModel.values) {
            if (situation.contains(value)) {
                return true;
            }
        }
        return false;
    }
复制代码

修正评价办法 evaluate,对各种棋型做一个统计,最后依照我上面给出的处置规则停止加分处置。

    /**
     * 对当前棋位停止评价
     *
     * @param point 当前棋位
     * @return
     */
    private int evaluate(Point point) {
        // 分值
        int score = 0;
        // 活三数
        int huosanTotal = 0;
        // 冲四数
        int chongsiTotal = 0;
        // 活二数
        int huoerTotal = 0;
        for (int i = 1; i < 5; i++) {
            String situation = getSituation(point, i);
            if (checkSituation(situation, ChessModel.HUOSAN)) {
                // 活三+1
                huosanTotal++;
            } else if (checkSituation(situation, ChessModel.CHONGSI)) {
                // 冲四+1
                chongsiTotal++;
            } else if (checkSituation(situation, ChessModel.HUOER)) {
                // 活二+1
                huoerTotal++;
            }
            // 下此步的得分
            score += getScore(situation);
        }
        if (huosanTotal > 0 && huoerTotal > 0) {
            // 活三又活二
            score *= 2;
        }
        if (chongsiTotal > 0 && huoerTotal > 0) {
            // 冲四又活二
            score *= 4;
        }
        if (huosanTotal > 1) {
            // 活三数大于1
            score *= 6;
        }
        if (chongsiTotal > 0 && huosanTotal > 0) {
            // 冲四又活三
            score *= 8;
        }
        return score;
    }
复制代码

再次停止测试,AI 棋力曾经能够打败我这个菜鸡了,但由于我棋艺不精,打败我不具代表性。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值