序言
本文对笔者使用Java独立开发的PC端五子棋游戏的总结,概述。本游戏基于Swing组件,实现悔棋,认输,人机对弈等功能,属于Java的入门/初级应用,仅用于记录自身成长及供道友参考。大致效果图如下: 游戏实现代码已放到github上,供大家参考:
代码结构
本游戏基于Intellij实现,共有GobangUI,ChessListener,Check,AI,Config五个Java类;
GobangUI为界面实现类,内部放有主调函数,顶级容器为JFrame的实例,使用方位式布局(BorderLayout)包含有menuPane和chessTable两个JPanel的实例,分别用于实现选项界面和棋盘界面;此外,此处选择使GobangUI自身继承JPanel,并将其自身用作chessTable,此做法的优点在于可方便地修改棋盘类的paint()方法,进而实现棋盘绘制,悔棋时前n-1个棋子的重绘等功能。
ChessListener类为实现/继承了ActionListener和MouseAdapter的主界面事件监听类,负责实现棋盘监听-基本下棋功能,选项界面按钮监听-对开始游戏,悔棋,认输等不同操作做出反应,是游戏功能的主要实现类。
Check类为用于封装判断输赢方法的功能类,其内部包含如何判断某一位置处是否在八个方向上实现连珠,在ChessListener中实例化。
AI类为权值法下棋的AI下棋功能实现类,内部存放有对从活一眠一到活四眠四各种情况和对应权值的HashMap,以及遍历棋盘,计算出每一处未下棋处的总权值并依此找到权值最大处并返回此位置的横纵坐标。
其中Config为接口类,用于系统地存放主要的参数,方便进一步修改,由其他所有类实现之以共享主要参数,主要包含棋盘的左上角坐标,行列数,格子和棋子的大小,以及记录棋盘上每一处权值得分或棋子情况的二维数组,具体内容如下:
public static final int x0 = 40;
public static final int y0 = 40;
public static final int chessSize = 35;
public static final int gridSize = 40;
public static final int RowsAndColumns = 15;
//chessTable记录棋盘每一个位置的棋子情况,0,1,2 分别表示无棋子,黑棋,白棋,用于判断胜负,判断某处是否可下棋
public static final int[][] chessTable = new int[15][15];
//pointArray按下棋顺序记录每一个棋子的坐标,用于悔棋,AI
public static final Point[] pointArray = new Point[15 * 15];
//存放AI下棋权值法参数的二维数组
public static final int[][] blackScore = new int[15][15];
public static final int[][] whiteScore = new int[15][15];
//棋盘界面和选项界面的颜色
public static final Color menuPaneColor = new Color(120,117,116);
public static final Color chessTableColor = new Color(156, 156, 155);
主要功能
棋盘绘制与选项界面
通过重写棋盘对象(JPanel-chessBoard)的绘制方法paint(),在其中增添通过双重for循环调用drawline()方法绘制棋盘,以实现棋盘的稳定绘制。
for (int i = 0; i < RowsAndColumns; i++) {
if (i == 0 || i == RowsAndColumns - 1) ig.setStroke(new BasicStroke(3.5f));
else if (i == 1) ig.setStroke(new BasicStroke(2.0f));
ig.drawLine(x0, x0 + i * gridSize, x0 + (RowsAndColumns - 1) * gridSize, y0 + i * gridSize);//绘制横线
ig.drawLine(x0 + i * gridSize, y0, x0 + i * gridSize, y0 + (RowsAndColumns - 1) * gridSize);//绘制竖线
}
选项界面(JPanel-menuPane)采用流式布局,上共有五个选项按钮,其中有三个JButton的实例和两个JRadioButton的实例,对应内容分别为“开始游戏”,“认输”,“悔棋”,“玩家对战”和“人机对战”,最后两个JRadioButton并于一个ButtonGroup中,实现单选功能。每个按钮均加上ChessListener类的实例对象以监听之。
下棋操作
使用一个JPanel的实例作为棋盘对象,通过监听在此界面上点击操作获取点击处的横纵坐标,判断以下两条件:
- 此点是否位于棋盘范围内——保证棋子不错误地绘制在其他位置。
- 此处是否已存在棋子——避免在同一位置重复下棋。 若满足这两条件,则可在近似取整处理后得出距离此点最近的横纵坐标线交叉点对应的行列数,根据已下棋子数(index)判断棋子颜色,在此处绘制该棋子。
注意此处并非直接使用chessBoard.getGraphics()获取其画笔并使用g.fillOval()来画棋子,这样画出来的棋子边界锯齿很明显,不美观;故在此处加上如下中间步骤:
Graphics2D ig = (Graphics2D) chessBoard.getGraphics();
//抗锯齿,改善图片/所绘图形质量
ig.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
输赢判断
据五子棋规则可知,逻辑顺序为下棋——判断是否出现五连珠,可进一步归结为:下一步棋——判断该步棋是否促成五连珠;相比与每次下棋后遍历棋盘寻找五连珠,仅在下棋后判断此棋四周是否存在五连珠可大幅减少开销。
笔者选择将与输赢判断有关的所有方法封装于Check类中,在事件监听类中使用时只需使用Check的实例调用check()方法即可,部分代码如下:
public boolean check(int r, int c) {
if (checkRow(r, c) || checkColumn(r, c) || checkLean_Left(r, c) || checkLean_Right(r, c)) {
return true;
} else
return false;
}
悔棋功能
悔棋的实质是将撤销上一步下的棋,也即重绘除上一步棋外的所有棋子+棋盘,为实现此功能可使用大小为Rows*Columns的Point数组(15*15=225,空间开销不大)+记录棋子数变量index构建简单的栈,在每次下棋时记录下其坐标位置,悔棋时只需取出此步棋子的坐标,将chessTable上此位置的记录对应清除及index–,最后调用chessBoard.repaint()方法即可。
Point point = new Point(pointArray[--index]);
chessTable[point.x][point.y] = 0;
pointArray[index] = null;
AI下棋
由于五子棋规则极其简单,笔者选取权值法作为本游戏AI的实现方法,此外若想实现更智能,更强大的AI,可考虑博弈树,此处不多做讨论。
一句话概括权值法,即预先设定好所有棋盘上可能出现的所有情况对应的权值,对棋盘上每一处空位打分,选取分数最高的位置下棋。
此处也牵涉AI的一个核心理念,也即根据具体情况具体分析,选择最适合当前情景的方法构建AI,并非所有的AI均要涉及ML,DL,以及基本不存在(或是目前不存在)能解决所有问题的AI实现方法,AI工程师除了研究,优化算法以外,更多的工作其实是将已有的工具,算法创新而灵活地应用到不同场景中。
总结
此游戏仅是一Java入门级练手应用,通透的理解可促进对使用Java完成项目起到很好的铺垫作用,欢迎大家指正其中的不足和值得改进之处,也欢迎大家fork。