# gobang **Repository Path**: xbaistack/java-18-gobang ## Basic Information - **Project Name**: gobang - **Description**: Java+二维数组+WebSocket=五子棋(双人在线对战版) - **Primary Language**: Unknown - **License**: Apache-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2025-03-29 - **Last Updated**: 2025-03-30 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README ### JAVA | 第18期 - 数组(And五子棋) ![QRCode](qrcode-platform.png) > 直接运行 小提醒,如果你只想体验一下的话,可以将 [gobang.jar](gobang.jar) 下载下来,然后将其放置在你的电脑中,并用下面的命令启动起来。 ```shell java -jar gobang.jar ``` 提醒一下哈,你本地必安装 `Java` 环境变量才可以哦~ --- 数组,相信不是一个陌生的话题。作为开发人员应该无人不知无人不晓这种数据结构,但是知道归知道,用归用。相信在各种框架横行的当下,很少有人真正的去使用过数组这种结构。大部分场景之下,都是在其之上的封装产物进行使用,比如 SDK 提供的 `List`、`Map` 等等,亦或是 `栈`、`队列` 这些数据结构。用起来确实方便,不用关心数组是否满出,什么时候需要扩容等。 但是这里我们也不是要说那么基础的内容,这里我主要是根据数组的特性来分享一个小游戏:`五子棋`。这个游戏大家都玩过吧,那么你在玩的时候有没有想过它是怎么做的呢? 这里面会涉及到一种数据结构 `二维数组`。利用它我们可以很容易的实现这么一款小游戏。 > 一维数组 一维数组,这个很简单吧?就是只需要一次循环就可以遍历完的数组,每一个元素都是一个单一的值,根据其定义不同其值可以是任何类型。 ```java Object[] array = new Object[5]; // OR Object[] array = { ... }; ``` > 二维数组 数组你可能知道,但是二维数组很少用过吧?为了防止你搞忘了什么是二维数组,这里简单给你写一下。 ```java int[][] arrays = { {0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0}, }; ``` 这就是二维数组,数组的子项又是一个一维数组,多维数组的话,再往下继续套就可以了,但是多维数组用到的场景就更少了,扯远了,我们继续说二维数组。 这个数组这样看起来有没有点像我们玩的五子棋盘? `arrays[x]` 就是每一行,而 `arrays[x][y]` 就是每一列,其中的 `x` 是一维数组的元素个数,也就是这里的多少行,而 `y` 就是子项的元素个数,代表多少列。 按照此规则,我们可以利用循环随意创建一个 `M*N` 规格的二维数组,具体如下: ```java public int[][] create(int rows, int cols) { int[][] arrays = new int[ rows ][ cols ]; for (int x = 0; x < rows; x++) { int[] row = new int[ cols ]; for (int y = 0; y < cols; y++) { row[ y ] = 0; } arrays[ x ] = row; } } ``` ```java int[][] arrays = create(5, 5); ``` 这样就可以很方便的得到一个 `5*5` 规格的二维数组了。 > 视觉扩展 不知道你的观察能力怎么样,你多看几眼这个二维数组,有没有觉得方方正正,有点像什么? 你不觉得很像我们玩的大部分 `具有坐标类的游戏` 吗?比如 `贪吃蛇`、`连连看`、`消消乐`、`五子棋` 之类的简单游戏?当然了,本篇幅不是为了教大家怎么做游戏,只是利用这么一个话题,让我们感受一下 `二维数组` 的延生应用,以便更好的认识这一种数据结构。 > 游戏渲染 当然了,数组终究只是一种数据结构,还无法让我们更直观的观察到,因此这里我们可以利用 `HTML` 前端网页技术将其渲染出来,原理同理,双重循环将每一个元素通过页面元素呈现到页面之上。 这部分内容可能有点考验你的前端知识,需要你会一点点前端相关的技术,否则无法更好的将其渲染出来。 呃,前端涉及的内容太多了,这里就不贴代码了,文章拉到最后我提供了仓库访问地址,大家感兴趣的话可以去看一看。 > 胜出条件 既然将一个 `二维数组` 看作是一个棋盘,那怎么算赢了呢?又怎么用程序来判断结果呢? 我们都知道,在棋盘中 `水平`、 `垂直`、 `交叉` 几个方向上只要同色系的棋子等于5个就胜出了,那按照此逻辑,我们可以分别从 `当前棋子` 所在的坐标分别向 `左侧`、`右侧`、`上方`、`下方`、`左上`、`右下`、`左下`、`右上` 八个方向去计数,看当前同色系的棋子累计是否有达到5个的情况,书面语言是这样的,那我们将其翻译成代码看一下。 定义一个方向类:[AxisDirection](src/main/java/top/xbaistack/common/AxisDirection.java) 代表某一个方向,其中包含了左侧和右侧的坐标提取。 ```java @Getter @Setter public class AxisDirection { private AxisSupplier left; private AxisSupplier right; } ``` 然后我们可以写出以下代码(摘抄于 [Room](src/main/java/top/xbaistack/game/Room.java) 类): ```java private static final List directions = new ArrayList<>(4); static { // 水平 // [ 0, 0, 0, 0, 0 ] // [ 0, 0, 0, 0, 0 ] // [ #, #, #, #, # ] // [ 0, 0, 0, 0, 0 ] // [ 0, 0, 0, 0, 0 ] AxisDirection direction = new AxisDirection(); direction.setLeft((x, y) -> new int[] { x, y - 1 }); // 左侧,行不变,列递减 direction.setRight((x, y) -> new int[] { x, y + 1 }); // 右侧,行不变,列递曾 directions.add(direction); // 垂直 // [ 0, 0, #, 0, 0 ] // [ 0, 0, #, 0, 0 ] // [ 0, 0, #, 0, 0 ] // [ 0, 0, #, 0, 0 ] // [ 0, 0, #, 0, 0 ] direction = new AxisDirection(); direction.setLeft((x, y) -> new int[] { x - 1, y }); // 上方,列不变,行递减 direction.setRight((x, y) -> new int[] { x + 1, y }); // 下方,列不变,行递增 directions.add(direction); // 左上到右下 // [ #, 0, 0, 0, 0 ] // [ 0, #, 0, 0, 0 ] // [ 0, 0, #, 0, 0 ] // [ 0, 0, 0, #, 0 ] // [ 0, 0, 0, 0, # ] direction = new AxisDirection(); direction.setLeft((x, y) -> new int[] { x - 1, y - 1 }); // 左上,列递减,行递减 direction.setRight((x, y) -> new int[] { x + 1, y + 1 }); // 右下,行递增,列递增 directions.add(direction); // 左下到右上 // [ 0, 0, 0, 0, # ] // [ 0, 0, 0, #, 0 ] // [ 0, 0, #, 0, 0 ] // [ 0, #, 0, 0, 0 ] // [ #, 0, 0, 0, 0 ] direction = new AxisDirection(); direction.setLeft((x, y) -> new int[] { x + 1, y - 1 }); // 左下,行递增,列递减 direction.setRight((x, y) -> new int[] { x - 1, y + 1 }); // 右上,行递减,列递增 directions.add(direction); } ``` 这样,我们就可以很方便的通过以上不同的方向来得到某一个坐标 `临近` 的八个位置的棋子数据了,当然了,这个只会提供临近的那一个坐标,我们需要通过循环去获取八个方向的所有坐标,并累加同色系棋子,判断数量是否达标。 ```java public boolean isFinished(Role role, int x, int y) { // 依次判断四个大方向上是否有满足凑齐五个棋子的条件, // 但凡是某一个方向满足五个棋子就立即返回 true, // 四个方向依次是:水平(─)、垂直(|)、左上到右下(╲)、左下到右上(╱); Predicate tester = piece -> piece.getRole() == role; for (AxisDirection direction : directions) { int left = countPieceNumber(direction.getLeft(), x, y, tester); int right = countPieceNumber(direction.getRight(), x, y, tester); if ((left > 0 || right > 0) && left + right + 1 >= 5) { // 5 = left-count + 1 + right-count return true; } } return false; } ``` 统计单一方向 `左侧`(上/左上/左下)以及 `右侧`(下/右下/右上)上的同色系棋子数量,然后将其两侧的累加起来,判断数量是否等于 5(记得要加上当前棋子本身的数量),以及要注意是否下标越界判定等。 ```java private int countPieceNumber(AxisSupplier supplier, int x, int y, Predicate tester) { int count = 0; int[] axis = supplier.accept(x, y); while (axis[ 0 ] >= 0 && axis[ 0 ] < rows && axis[ 1 ] >= 0 && axis[ 1 ] < cols) { ChessPiece item = chessboard[ axis[ 0 ] ][ axis[ 1 ] ]; if (tester.test(item)) { count++; axis = supplier.accept(axis[ 0 ], axis[ 1 ]); } else break; } return count; } ``` 上面涉及到 `Role`、`AxisSupplier`、`ChessPiece` 等类没有列出来,这里我们来看看里面有些什么。 [Role](src/main/java/top/xbaistack/game/Role.java),用于表示当前玩家的角色是 `白子`、`黑子` 还是未知(棋盘默认未下的时候就是 `NONE`)。 ```java import java.util.Locale; public enum Role { WHITE, BLACK, NONE; public static Role of(String name) { return valueOf(name.toUpperCase(Locale.ENGLISH)); } public Role anotherRole() { return this == WHITE ? BLACK : WHITE; } public static boolean isValid(String name) { try { valueOf(name.toUpperCase(Locale.ENGLISH)); } catch (Exception e) { return false; } return true; } } ``` [AxisSupplier](src/main/java/top/xbaistack/common/AxisSupplier.java),用于接收坐标并返回一个新的坐标。 ```java @FunctionalInterface public interface AxisSupplier { int[] accept(int x, int y); } ``` [ChessPiece](src/main/java/top/xbaistack/game/ChessPiece.java),表示棋盘的每一个棋子,这里二维数组中我们不用简单的数字来作为内容,那样不太好判断当前棋子状态,因此我们单独定义一个类来表示。 ```java @Getter @Setter public class ChessPiece { private Role role; public ChessPiece() { this(Role.NONE); } public ChessPiece(Role role) { this.role = role; } } ``` 里面很简单,只需要记录当前棋子的占领者是谁就可以了,这是为了在判断棋盘的棋子的时候能知道是谁下的。 > 消息推送 上面只是核心的部分代码,并不是全部的代码,代码量太多,其他逻辑相关的内容就不放上来了。 这里我们可以接入 `WebSocket` ,让五子棋变成可以双人对战的五子棋,这部分内容也很简单,只需要在你项目接入 `Websocket` 组件即可,以下我也列出来重要的部分内容。 引入依赖: ```xml org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-websocket ``` 创建端点:[RoomEndpoint](src/main/java/top/xbaistack/ws/RoomEndpoint.java) ```java @Slf4j @Component @ServerEndpoint("/room/{room}/{player}") public class RoomEndpoint { @OnOpen public void onOpen(Session session, @PathParam("room") String room, @PathParam("player") String player) { System.out.println("Socket.onOpen: room -> " + room + ", player -> " + player); } @OnMessage public void onMessage( Session session, @PathParam("room") String room, @PathParam("player") String player, String message ) { System.out.println("Socket.onMessage: room -> " + room + ", player -> " + player); System.out.println(message); } @OnClose public void onClose(Session session, @PathParam("room") String room, @PathParam("player") String player) { System.out.println("Socket.onClose: room -> " + room + ", player -> " + player); } @OnError public void onError( Session session, @PathParam("room") String room, @PathParam("player") String player, Throwable error ) { System.out.println("Socket.onError " + error); } } ``` 好了,本次分享的关于 `数组` 相关的内容就到此结束了,本来数组也不是很难的东西,所以可以讲的内容会少一些,更多的是在代码上面,这点大家可以去看项目代码。 感谢您的观看,咱们下期见。