六子棋人机程序Java版(附源码+设计思路)


                                   效果界面展示

在这里插入图片描述
                     智能程度挺低的,改了好多bug。拿去比赛,然后被贪心打爆了,还是存在一些bug。




GUI界面解读(MainFrame.java)

                          整个GUI界面由JSplitPane分割成两个Japnel。
  • 左边的Jpanel作为画板,使用的空布局,绘制任何形状都是用的相对坐标在上面绘制。其中主要有棋盘、棋子、坐标、棋子落子后显示的红色矩形框、棋子序号即五个黑点(例如天元)需要绘制,每次落子结束后重绘一次。重绘需要先重写Jpanel的paint方法,落子后使用Jpanel.repaint()方法重绘,因为repaint()方法调用后可能画板不会立刻重绘,此时需要调用Jpanel.paintImmediately()立即重绘。棋盘绘制即画直线,然后拼接成棋盘,棋子是画圆,填充不同的颜色,红色矩形框是绘制矩形,坐标1-19,A-S是绘制字符串,同理棋子序号也是绘制字符串。

  • 右边的Jpanel使用的空布局,加入了几个按钮,按钮上的字是绘制出来的,按钮位置根据给的坐标绘制。右边的两个计时器的设计网上有相关教程,本质上是创建一个线程,每隔几十毫秒更新时间字符串并重新绘制,一方落子结束之后该线程结束,创建一个新线程控制另一个计时器。六个按钮均需要加入按钮监听事件,当事件触发时执行相应的代码。实时显示落子情况的是一个文本框JTextArea,每次落子之后再文本框最后一行追加(append)字符串。

AI算法(ChessMap.java)

                             该程序主要用到了博弈树中的阿尔法贝塔搜索+评估函数。

人机算法:
       1.贪心
       2.博弈树:极大极小值算法、阿尔法贝塔剪枝、主要变例搜索(PVS)、MTD(f)算法等
       3.深度学习

推荐网站:
http://www.soongsky.com/othello/computer/index.php
https://www.xqbase.com/computer.htm


阿尔法贝塔搜索


阿尔法贝塔剪枝中的一些个人理解

       阿尔法贝塔搜索是在极大极小值算法基础上进行一定程度的优化,搜索途中参考之前计算出的alpha和beta值,将一些不会出现的落子点剪掉,同时再搜索途中更新alpha和beta值,以达到减少搜索时间的目的。

       对于两层极大极小值搜索,双方各落两子,若预备落子点为整个棋盘,那将有(19*19)^4即170亿个叶子节点,即使剪枝剪去95%,还剩下8.5亿叶子节点,再算上评估函数,以当前的CPU计算速度上来说,总花费时间也是极大的。若预备落子点有100个,那么两层博弈树将有一亿个叶子节点,三层有一万亿个,所以最多只能搜两层,其实比贪心好不了多少,更可能因为预选点较少而劣势落子。在极端情况下,阿尔法贝塔剪枝其实并没有剪掉很多,甚至可能花费时间降到与极大极小值算法一样的地步。

       根据阿尔法贝塔搜索的特点和搜索先后顺序对剪枝的影响,如果在人机搜索途中模拟落子时先搜到最优或者尽量优的落子点,那么其后将进行很多次剪枝,大大减少了搜索耗时。那么哪些合法的落子点应该作为预备落子点,哪些预备落子点应该排在前面,最先搜呢?观察落子规律可知,除第一个落子以外,双方基本上都是在有棋子的周围落子,且落子位置周围八格尽量有棋子。当一方准备落子时,通常有三种选择,一是堵对方的字,二是连我方的子,三是堵+连同时存在。可以将棋盘中所有的合法落子位置设置权值,当某方实际落子为(x,y)时,该坐标一定范围内,越靠近(x,y),所赋权值越大,权值可以叠加,不应重置。将所有的合法落子点存在数组里面,按照权值和从大到小排序,取其前100个坐标,对其进行搜索。当然,这可能出现对方连四时,应该堵连四的落子点权值太小,不在前一百名,然后被删了…解决方法:对于(x,y)周围权值的更新后的基础上暂时再加上一定大小的权值,使之排名尽量靠前,减少被删掉的可能性,注意是暂时,暂时指的是搜索期间

nbsp;      评估函数的设计对搜索的花费时间和落子的优劣影响很大。常见的评估函数是搜到叶子节点时对整个棋盘评估,估值越大,对人机越有利,估值越小,对人机越不利。评估函数设计的不准确可能会对落子产生重大的影响,有些程序中的评估函数是基于点的评估,此时会造成评估重叠问题,导致评估过大或者过小,同时对于棋型(例如连五或者连四)的匹配也很消耗时间。棋盘由点组成,点的评估组成棋盘的总评估,除此之外,应该还有一种评估。棋盘由点组成,但点又组成线,线组成棋盘。对于19*19的棋盘,存在着19行、19列,37条45°对角线、37条135°对角线,两个37条对角线中又各自有十条无效对角线(该对角线上最多能下的点不足六个),实际上有效线段应该只有19+19+27+27=92条线段,将这每一条线段,其上empty、black、white分别用0、1、2表示,则可以当成字符串表示,只需要对这92条线段上的字符串进行评估,总和既是整个棋盘的评估。对于字符串的评估无非就是棋型字符串的匹配。


评估函数


       评估函数在搜索中的优化。动态规划实际上可以应用于评估函数,能避免大量重复线段的估值,当然你也可以当作迷宫问题用dfs解决时用的回溯思想。当对方落子结束之后,我们对整个棋盘进行评估,记为E,如果此时我们对(x1,y1)、(x2,y2)两点落子,那么(x1,y1)影响了它所在的行列对角线四条线段的估值,而剩下的88条线段估值是不变的,当(x1,y1)落子之后,此时的棋盘估值=E-E中坐标(x1,y1)未落子时所在的所行列对角线四条线段的旧估值+(x1,y1)落子后它所在的行列对角线四条线段的新估值。我们将旧的估值记录下来,方便回溯到E,同理(x2,y2)落子后依旧可以如此修改回溯。当两层博弈树搜到叶子节点时,不再必须评估整个棋盘,对局部估值的修改也能评估整个棋盘。

       评估函数中字符串匹配的优化。对于一个待匹配的线段字符串,两端可能存在大量的empty即0,可以删掉一部分,只保留有black或者white的子串及其两端一定范围内的empty字符。字符串匹配时,应优先匹配连四或者连五这种棋型,防止因为匹配连三这种棋形结果把连四分割了。首先对整个字符串匹配一次,匹配到之后将匹配到的子串作为边界,二分继续匹配边界左边和右边,左边和右边没有匹配到的区域应该继续匹配,物尽其用,用二分就行。左边匹配的棋型优先度应该比边界匹配到的棋型低,不然就不会匹配到它右边的边界了,边界右边的字符串匹配的棋形优先度应该从最高开始,两端字符串都是二分匹配,尽量使该线段评估值最大。为什么时尽量?实际上这种匹配不一定最优,要达到最优是组合问题,更复杂,效率也不一定高。



后续可以进行哪些优化?


1.棋型库的完善

2.设置开局库

3.将阿尔法贝塔搜索继续优化,例如前面提到的主要变例搜索即MTD(f)算法

4.评估函数继续优化

5.加入贪心算法,将贪心与博弈树相结合。在博弈树搜索之前,可以先用贪心算法,若对方连五,两边需要两子直接堵,若对方连四,有122221、1022221、1222201三种堵法,具体用哪种需要考虑实际情况,好的落子可能同时堵了隔壁的连三或者眠三之类的棋型。若对方眠四或者眠五,则我方必有一子堵对方,可以优先搜索该坐标的搭配落子走法,可能剪枝效率加快。若我方连四或者连五,直接落子六连即可,不需要博弈树搜索。当这三种情况都不存在时,可以将贪心所求的的几种贪心认为最好的走法优先进行博弈树搜索,此时剪枝的效率也可能加快。

 6.博弈树搜索时阿尔法贝塔剪枝一直没剪到怎么办?即花费时间降到极大极小值算法的地步,导致某一方落子之后AI迟迟不落子,看似陷入卡死的境地。解决方案:在搜索开始前先用贪心搜一遍,获取一些贪心自认为优秀的落子,然后获取系统当前时间T1,开始博弈树搜索,当博弈树搜索期间某一次获取的系统当前时间T2-T1>给定定值M的时候,终止搜索。此时可以参考贪心的结果落子,也可以参考博弈树中当前已经搜过的走法中最好的走法,虽然可能走法不够优秀,但是实际上两层博弈树也比贪心好不到哪去,甚至优秀的贪心算法可能比两层博弈树表现的更好。




总结


       由于六子棋落子数除第一颗一子一外,之后双方一轮各落两子,无论博弈树如何优化,都不可能去搜三层甚至更多层,即使加上贪心,偶尔还是会下很智障的棋,导致劣势增大,甚至可能直接因此输掉。如果是五子棋,博弈树可能表现的更好,但是局限于六子棋时,无论是博弈树还是贪心,其AI落子可能并不智能,甚至智障。或者说局限于当前CPU的运算速度,这两种算法的棋力上限并不算高。如果想提高AI棋力,可能就需要将深度学习应用于六子棋了,当然也可能存在其他优秀的算法。



                            源代码地址:GitHub

  • 10
    点赞
  • 113
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
以下是一个简单的Java五子棋人机对战的源码,希望能对您有所帮助: ```java import java.util.Scanner; public class GobangGame { public static void main(String[] args) { Scanner scanner = new Scanner(System.in); GobangBoard board = new GobangBoard(); boolean isUserTurn = true; // 标志用户是否该下棋 int x, y; System.out.println("欢迎来到五子棋游戏!"); System.out.println(board); while (!board.isGameOver()) { if (isUserTurn) { // 用户下棋 System.out.print("请输入您要下的棋子的坐标(x y):"); x = scanner.nextInt(); y = scanner.nextInt(); while (!board.isUserMoveValid(x, y)) { System.out.println("您输入的坐标不合法,请重新输入!"); System.out.print("请输入您要下的棋子的坐标(x y):"); x = scanner.nextInt(); y = scanner.nextInt(); } board.makeUserMove(x, y); } else { // 电脑下棋 System.out.println("电脑正在思考中..."); Move move = board.getComputerMove(); board.makeComputerMove(move); System.out.println("电脑在(" + move.getX() + ", " + move.getY() + ")处下了一个棋子。"); } System.out.println(board); isUserTurn = !isUserTurn; } // 游戏结束 if (board.isUserWin()) { System.out.println("恭喜您,您赢了!"); } else if (board.isComputerWin()) { System.out.println("很遗憾,您输了!"); } else { System.out.println("游戏结束,双方打成平局!"); } } } class GobangBoard { private int[][] board; private final int SIZE = 15; private final int EMPTY = 0; private final int USER = 1; private final int COMPUTER = 2; public GobangBoard() { board = new int[SIZE][SIZE]; } public boolean isUserMoveValid(int x, int y) { if (x < 0 || x >= SIZE || y < 0 || y >= SIZE) { return false; } return board[x][y] == EMPTY; } public void makeUserMove(int x, int y) { board[x][y] = USER; } public void makeComputerMove(Move move) { board[move.getX()][move.getY()] = COMPUTER; } public boolean isGameOver() { return isUserWin() || isComputerWin() || isBoardFull(); } public boolean isUserWin() { return isWin(USER); } public boolean isComputerWin() { return isWin(COMPUTER); } private boolean isWin(int player) { int count; // 检查每一行 for (int i = 0; i < SIZE; i++) { count = 0; for (int j = 0; j < SIZE; j++) { if (board[i][j] == player) { count++; if (count == 5) { return true; } } else { count = 0; } } } // 检查每一列 for (int i = 0; i < SIZE; i++) { count = 0; for (int j = 0; j < SIZE; j++) { if (board[j][i] == player) { count++; if (count == 5) { return true; } } else { count = 0; } } } // 检查每一条对角线 for (int i = 0; i <= SIZE - 5; i++) { count = 0; for (int j = 0; j < SIZE - i; j++) { if (board[i + j][j] == player) { count++; if (count == 5) { return true; } } else { count = 0; } } } for (int i = 1; i <= SIZE - 5; i++) { count = 0; for (int j = 0; j < SIZE - i; j++) { if (board[j][i + j] == player) { count++; if (count == 5) { return true; } } else { count = 0; } } } for (int i = 0; i <= SIZE - 5; i++) { count = 0; for (int j = 0; j < SIZE - i; j++) { if (board[i + j][SIZE - j - 1] == player) { count++; if (count == 5) { return true; } } else { count = 0; } } } for (int i = 1; i <= SIZE - 5; i++) { count = 0; for (int j = 0; j < SIZE - i; j++) { if (board[j][SIZE - i - j - 1] == player) { count++; if (count == 5) { return true; } } else { count = 0; } } } return false; } private boolean isBoardFull() { for (int i = 0; i < SIZE; i++) { for (int j = 0; j < SIZE; j++) { if (board[i][j] == EMPTY) { return false; } } } return true; } public Move getComputerMove() { // TODO: 实现电脑下棋的算法 return new Move(0, 0); } @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append(" "); for (int i = 0; i < SIZE; i++) { sb.append(i).append(" "); } sb.append("\n"); for (int i = 0; i < SIZE; i++) { sb.append(i).append(" "); for (int j = 0; j < SIZE; j++) { if (board[i][j] == EMPTY) { sb.append("+ "); } else if (board[i][j] == USER) { sb.append("X "); } else { sb.append("O "); } } sb.append("\n"); } return sb.toString(); } } class Move { private int x; private int y; public Move(int x, int y) { this.x = x; this.y = y; } public int getX() { return x; } public int getY() { return y; } } ``` 这个源码中实现了一个简单的五子棋游戏,用户和电脑轮流下棋,直到游戏结束。其中`GobangBoard`类表示棋盘,`Move`类表示一次下棋的位置。具体的电脑下棋算法需要自己实现。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值