FEATURED · 精选文章

Java控制台五子棋实战:从二维数组到胜负判断与AI扩展

发布时间 / 2026/9/1 20:38:40
来源 / 创域科博编辑部
栏目 / 资讯中心
Java控制台五子棋实战:从二维数组到胜负判断与AI扩展 简介本资源是一份面向Java初学者与课程设计实践者的五子棋游戏完整实现源码聚焦图形界面开发、游戏逻辑建模与事件驱动编程等核心能力训练。压缩包共27个文件24个Java源文件、1个Maven配置pom.xml、1个说明文档readme.txt、1个.gitignore总大小仅40KB轻量易读适合快速导入IDE运行调试。其中包含Main入口类、Board棋盘管理、GameController胜负判定、Piece棋子封装等关键模块代码结构清晰注释规范辅以Maven构建支持和标准版本控制配置便于学习者理解MVC分层思想与Swing界面交互机制。已有269人下载学习可直接用于Java GUI编程实训、算法课设或小游戏开发入门参考是掌握二维数组状态管理、鼠标事件响应及回合制逻辑实现的典型教学范例。 直接说结论这个项目看起来只是“一个简单的五子棋”但它恰好覆盖了Java入门阶段最值得反复练手的三块硬骨头——二维数组的建模、状态机的流转、以及算法思路的落地。我见过太多人学完Java语法后卡在“不知道写什么项目”五子棋就是一个刚好能让你把语法糖吐出来变成代码的题目。接下来我把整个实现思路、核心源码、以及我实测踩过的坑一条条拆开讲代码可以直接复用逻辑也能迁移到其他棋类游戏上。1. 项目整体设计与思路拆解1.1 为什么选控制台五子棋而不是Swing版很多人一提“Java五子棋”第一反应是做个带界面的Swing版本画个棋盘、鼠标点击落子看起来很炫。但我的建议是第一版一定要做控制台版本。原因很简单Swing会分散你的注意力。你要处理窗口布局、鼠标监听、事件分发、重绘线程这些和五子棋核心规则没有直接关系。等你费半天劲把界面调好可能连胜负判断都还没写利索。控制台版本则强制你把精力集中在两件事上数据怎么存、规则怎么判。这两个问题想透了后面套任何界面框架都只是时间问题。而且从代码量来看控制台版核心逻辑大概150到200行就能写完非常适合在一个晚上内跑通全流程。别小看这种“快速跑通”的价值它能给你一个完整的正反馈从输入坐标到棋盘变化再到胜负提示所有环节都掌握在自己手里。1.2 棋盘的数据结构选型固定二维数组是最优解五子棋棋盘标准是15×15也有19×19的变体。我这里用15×15。数据结构上很多人第一反应是用集合或者动态数组但其实固定大小的二维数组才是最合适的。为什么因为棋盘的物理结构就是固定的15×15的格子不会在游戏过程中增加或减少。用char[][]或者int[][]既能直接映射棋盘坐标又能通过下标访问做到O(1)级别的读写而且打印棋盘时两层for循环就是天然的顺序遍历。我用的是char[][]空位置用表示黑子用●表示白子用○表示这里有个小设计用char而不是int或String。char在输出时直接拼接视觉上干净在判断时也可以用字符常量的方式进行匹配比如board[x][y] ●。如果用int你还得约定0、1、2分别代表什么增加记忆负担。1.3 核心模块划分把逻辑拆成三个独立层整个程序我拆成三个部分这也是我后来觉得最值得坚持的划分方式棋盘模块负责棋盘的初始化、打印、坐标合法性校验、落子写入。胜负判断模块负责在每次落子后检查以当前落子点为中心四个方向是否存在五子连珠。游戏流程模块负责玩家轮流落子、调用棋盘模块和胜负判断模块、控制游戏结束。这三个模块之间通过方法调用串联不互相持有对方的内部数据。比如胜负判断模块只需要接收棋盘数组和当前落子坐标就能返回结果完全不关心是谁下的、下到第几手了。这样设计的好处是以后想加AI对战只需要替换游戏流程模块里的“玩家二”部分棋盘和判断模块一行都不用改。很多人写小项目喜欢把所有代码堆在main方法里500行一个方法写完。这种方式跑起来没问题但后面想加功能、想排查问题会非常痛苦。分层是成本最低、收益最明显的习惯。2. 核心细节解析与实操要点2.1 棋盘初始化和打印细节决定体感棋盘初始化的代码很简单一个双循环把二维数组填充为就行。但打印这里我踩过一个坑直接System.out.print(board[i][j])会把每个字符紧紧贴在一起棋盘看起来像一堵密不透风的墙。我的做法是在每个字符后面加一个空格让每个棋子之间有间隔同时为了便于定位坐标我在棋盘左侧和顶部打印了行列号。private static final int BOARD_SIZE 15; private static final char EMPTY ; private static final char BLACK ●; private static final char WHITE ○; public static void printBoard(char[][] board) { System.out.print( ); for (int i 0; i BOARD_SIZE; i) { System.out.printf(%2d , i); } System.out.println(); for (int i 0; i BOARD_SIZE; i) { System.out.printf(%2d , i); for (int j 0; j BOARD_SIZE; j) { System.out.print(board[i][j] ); } System.out.println(); } }这里%2d的作用是让列号对齐个位数的坐标前补一个空格。别小看这个格式化输出坐标对齐后你在控制台里输入坐标时眼睛不容易看错行。棋盘打印完毕后控制台会显示一个带坐标的网格玩家输入“7 7”就能精确落在对应位置。2.2 落子合法性校验一个被无数人忽略的细节落子合法性的判断逻辑很简单坐标是否越界该位置是否已经有棋子但简单归简单我在帮别人review代码时发现一个高频bug很多人判断越界时只检查了下界也就是x 0 || y 0忘了检查上界x BOARD_SIZE || y BOARD_SIZE。结果玩家输入“20 20”程序直接抛出ArrayIndexOutOfBoundsException。另一个高频问题是把判断逻辑写成这样if (board[x][y] ! EMPTY) { return false; } else { return true; }这种写法没错但完全可以简化成一句return board[x][y] EMPTY;。说实话这不是什么大问题但如果你有面试打算这种小而美的写法会给面试官留下更好的印象。2.3 胜负判断为什么只需要检查四个方向五子棋的胜负规则是“横、竖、左斜、右斜”四个方向上有连续五个同色棋子。有些初学者会写出检查八个方向的代码但这样会重复计算。比如从左到右和从右到左其实是同一条线。我的做法是以当前落子点为起点向某个方向的正向和反向同时延伸统计同色棋子的连续数量然后和四个方向逐一比对。只要某个方向计数大于等于5就判定当前玩家获胜。这里还有一个效率优化的点不需要每次都扫描整个棋盘。因为胜负一定和“最后一步落子”相关所以只需要以当前落子点为中心在四个方向分别向两端搜索。这一点在开局阶段提升不明显但在残局阶段棋盘上可能有上百个棋子每次都做全盘扫描会造成不必要的计算浪费。核心代码如下public static boolean checkWin(char[][] board, int row, int col) { char current board[row][col]; int[][] directions {{1, 0}, {0, 1}, {1, 1}, {1, -1}}; for (int[] dir : directions) { int count 1; count countDirection(board, row, col, current, dir[0], dir[1]); count countDirection(board, row, col, current, -dir[0], -dir[1]); if (count 5) { return true; } } return false; } private static int countDirection(char[][] board, int row, int col, char current, int dx, int dy) { int count 0; int x row dx; int y col dy; while (x 0 x BOARD_SIZE y 0 y BOARD_SIZE board[x][y] current) { count; x dx; y dy; } return count; }directions数组里的四个元素分别代表水平、垂直、正斜线、反斜线。每个方向只需要用一组方向向量再配合反向取负即可。这段代码写完之后我也顺手做了个简单测试构造一个只有四个棋子的局部棋盘然后强制在第五个位置落子验证胜利。测试通过后再接入主流程这样可以避免把棋局玩到一半才发现判断逻辑有问题。3. 实操过程与核心环节实现3.1 完整源码一个可直接编译运行的控制台五子棋下面是完整源码包含双人对战模式代码里加了比较详细的中文注释方便你对照理解。主类命名为GobangGame直接复制到一个Java文件里就能编译运行。import java.util.Scanner; public class GobangGame { private static final int BOARD_SIZE 15; private static final char EMPTY ; private static final char BLACK ●; private static final char WHITE ○; private char[][] board; private Scanner scanner; public GobangGame() { board new char[BOARD_SIZE][BOARD_SIZE]; scanner new Scanner(System.in); initBoard(); } private void initBoard() { for (int i 0; i BOARD_SIZE; i) { for (int j 0; j BOARD_SIZE; j) { board[i][j] EMPTY; } } } public void printBoard() { System.out.print( ); for (int i 0; i BOARD_SIZE; i) { System.out.printf(%2d , i); } System.out.println(); for (int i 0; i BOARD_SIZE; i) { System.out.printf(%2d , i); for (int j 0; j BOARD_SIZE; j) { System.out.print(board[i][j] ); } System.out.println(); } } public boolean placePiece(int x, int y, char piece) { if (x 0 || x BOARD_SIZE || y 0 || y BOARD_SIZE) { System.out.println(坐标越界请输入0~14之间的数字); return false; } if (board[x][y] ! EMPTY) { System.out.println(该位置已有棋子请重新输入); return false; } board[x][y] piece; return true; } public boolean checkWin(int row, int col) { char current board[row][col]; int[][] directions {{1, 0}, {0, 1}, {1, 1}, {1, -1}}; for (int[] dir : directions) { int count 1; count countDirection(row, col, current, dir[0], dir[1]); count countDirection(row, col, current, -dir[0], -dir[1]); if (count 5) { return true; } } return false; } private int countDirection(int row, int col, char current, int dx, int dy) { int count 0; int x row dx; int y col dy; while (x 0 x BOARD_SIZE y 0 y BOARD_SIZE board[x][y] current) { count; x dx; y dy; } return count; } public boolean isBoardFull() { for (int i 0; i BOARD_SIZE; i) { for (int j 0; j BOARD_SIZE; j) { if (board[i][j] EMPTY) { return false; } } } return true; } public void startGame() { boolean isBlackTurn true; while (true) { printBoard(); System.out.println((isBlackTurn ? 黑方 : 白方) 落子请输入行和列用空格隔开); int x, y; try { x scanner.nextInt(); y scanner.nextInt(); } catch (Exception e) { System.out.println(输入格式错误请重新输入两个整数); scanner.nextLine(); continue; } char currentPiece isBlackTurn ? BLACK : WHITE; if (!placePiece(x, y, currentPiece)) { continue; } if (checkWin(x, y)) { printBoard(); System.out.println((isBlackTurn ? 黑方 : 白方) 获胜); break; } if (isBoardFull()) { printBoard(); System.out.println(棋盘已满平局); break; } isBlackTurn !isBlackTurn; } scanner.close(); } public static void main(String[] args) { new GobangGame().startGame(); } }主流程的startGame方法是一个典型的游戏循环打印棋盘、接收输入、落子、判胜负、切换玩家。isBlackTurn这个布尔值负责交替控制黑白方。try-catch用来拦截非数字输入避免程序崩溃。3.2 输入解析如何优雅地处理玩家输入控制台程序里输入处理是最容易翻车的地方。Scanner的nextInt()方法在遇到非数字输入时会抛出InputMismatchException如果不加处理整个程序直接崩溃。我在上面代码里采用了try-catch加scanner.nextLine()清空缓冲区的做法。这里解释一下为什么需要nextLine()当nextInt()读取失败时错误输入仍然留在缓冲区里如果不清空下一次循环会继续读到这个残留值造成死循环。另一种可行方案是全部读取字符串再手动解析String input scanner.nextLine(); String[] parts input.split( ); int x Integer.parseInt(parts[0]); int y Integer.parseInt(parts[1]);这个方案更灵活甚至可以让玩家输入“5,7”这种带逗号的格式只需要把split参数改成英文逗号。但缺点是代码量稍多而且Integer.parseInt也需要处理NumberFormatException。我最终选择nextInt()加try-catch的方案是因为代码更简洁对新手更友好。但你在项目里可以根据自己的偏好调整。3.3 体验优化加一个简单的落子提示和当前回合信息纯控制台五子棋最大的问题是“坐在电脑前的两个人需要凑到屏幕前才能看清棋盘”。一个很实用的优化是在每次落子后打印出上一步的落子坐标和当前是第几手。实现方式很简单在startGame方法里加一个moveCount计数器每次成功落子后自增然后在打印棋盘前输出一条信息比如System.out.println(第 moveCount 手黑方落子于 ( x , y ));这个信息虽然简单但能大幅提升复盘体验。棋局结束后你们可以根据这个输出记录把整个对局复盘一遍对新手理解棋局走势特别有帮助。4. 常见问题与排查技巧实录4.1 输入坐标越界或输入字母导致崩溃这是我在测试中遇到最多的一个问题。解决方案在第一版代码里用的是return退出当前循环但后面我发现单纯return会导致玩家直接失去这次落子机会体验不好。正确姿势是在异常捕获后继续循环而不是终止循环。同时考虑增加一个坐标范围的显式提示System.out.println(请输入0~14之间的两个整数用空格分隔);另一个细节是Scanner在读取完int后如果后面还有nextLine()需要额外调用一次nextLine()吃掉换行符。我这里没有混用nextLine()所以没这个问题但如果你在读完坐标后想加一个“是否重新开始”的确认就要注意换行符残留。4.2 胜负判断漏判斜线方向有次我把directions数组写成了{{1, 0}, {0, 1}, {1, 1}, {-1, 1}}第四项的-1, 1和第三项的1, 1其实在同一条斜线上只是方向相反。按我的实现方式每个方向会在代码里同时向正向和反向延伸所以只需要存一个方向向量就够了重复添加反而会导致某条线被计两次。这里有一个判断逻辑的自我检查方法每次落子后手动在棋盘上模拟摆出“横五”“竖五”“左斜五”“右斜五”四种子形状看程序是否正确识别胜利。常见的漏判场景是斜线因为视觉上不如横竖直观代码里也容易把方向向量弄错。4.3 棋盘满但没人赢的判断棋盘满意味着两百多个格子全部被棋子占据这种情况在真实对局里几乎没有但作为程序健壮性的一部分还是应该处理。我在代码里加了isBoardFull()方法每次落子后检查一次。这个检查在最坏情况下也是O(n²)的复杂度但因为棋盘只有15×15实际开销非常小不需要额外优化。4.4 设计一个简单的AI从完全随机到基础防守代码走到这一步双人对战已经能玩了。但如果你想进一步提升试着给这个程序加一个简易AI对手也就是热搜词里提到的“五子棋地狱难度ai”的入门版。最简单的AI是随机落子每次从所有空格中随机挑一个位置。这个难度大概连新手都打不过。进阶版本是“堵对方”扫描整个棋盘如果发现对方有四个连续的棋子就在两端落子堵住。再进一步可以做一个基于评分的AI给每个空格打分评估该位置在四个方向上既有的同色棋子数量和潜在连子潜力选最高分落子。我当时给这个项目扩展AI时用的是评分表法。核心思路是对每个空格分别模拟黑方和白方在该位置落子计算各自的“威胁值”然后取加权和。public int evaluate(char[][] board, int row, int col, char aiPiece, char humanPiece) { int score 0; score evaluateDirection(board, row, col, aiPiece); score evaluateDirection(board, row, col, humanPiece); return score; }AI的代码量大约比基础版多一倍但能显著提升可玩性。这个阶段你会接触到“打分”“权重”这些算法概念对后续学习更复杂的AI算法很有帮助。4.5 从控制台到可视化如果你非要界面版控制台版本跑通后想加界面的心情我理解。我当时是把控制台版本的核心逻辑原封不动保留单独写了一个GobangUI类用Swing画棋盘。核心逻辑只需要把char[][]暴露出来UI层负责渲染。Swing版的关键点有两个用JPanel重写paintComponent方法绘制网格和棋子用MouseListener监听鼠标点击换算成棋盘坐标后调用核心逻辑鼠标坐标换算是这样一个公式int col (mouseX - offsetX) / cellSize; int row (mouseY - offsetY) / cellSize;注意处理点击位置在两个格子中间的情况可以做四舍五入取整。这一步看似简单但很考验对坐标系的理解建议在控制台版跑通之后再动手。5. 项目复盘与扩展思路5.1 这个项目在Java学习路线里的位置如果有人问我Java学到什么程度可以开始写五子棋我的回答是学完数组、循环、方法、条件判断就可以。它不需要面向对象的高级特性甚至不需要集合框架。但恰恰是这种“基础够用”的特性让它成为检验基础功底的试金石。像我上面演示的代码已经把面向对象的基本思想融进去了用类封装游戏状态用方法抽象行为用私有字段保护数据。如果后续学完继承和多态还能进一步重构比如把棋子类型做成枚举把玩家行为抽象成接口。所以这个项目的价值不在于“能跑”而在于你从“能跑”到“跑得优雅”之间能学到多少东西。5.2 可以扩展的方向悔棋功能用一个栈存储每一步的落子坐标悔棋时出栈并清空对应棋盘位置。保存/加载棋局把棋盘数组序列化到文件下次启动时读取。网络对战基于Socket编程实现双人对战一个作为服务端一个作为客户端。难度分级的AI简单难度随机落子中等难度只会堵四连困难难度结合评分法。房间制和录像回放功能在保存的基础上增加关键节点分支方便复盘讲解。5.3 为什么建议你自己动手写一遍网络上确实能找到大量五子棋源码也包括我上面分享的这份。但我的建议是你可以参考但一定要自己动手敲一遍。因为写五子棋的过程本质上是“把需求翻译成代码”的完整练习你在过程中踩过的每一个坑都会在未来写复杂项目时变成你的直觉。比如你会发现“棋盘越界”不只是简单的if判断还要考虑输入容错你会发现“胜负判断”看似简单但方向向量的正负号写错一次就会导致斜线判负失败你会发现“游戏主循环”如果设计得不清晰加悔棋功能时会无从下手。这些东西光看别人代码是学不来的。最后的经验之谈这个项目带我走过了Java入门最枯燥的阶段。说实话语法书里的每一个知识点单独拎出来都很无趣但当你抱着“我要写一个五子棋”的目标去学时一切都变得具体起来。数组不再是一堆抽象概念而是棋盘上真实存在的一个个交叉点循环不再是为了做题而做题而是因为我们确实需要遍历棋盘上的每一行每一列。如果你拿来练手我强烈建议你完成第一版后尝试自己加上一个“悔棋”功能。这个功能会强迫你思考怎么记录每一步的状态是记录坐标还是记录整个棋盘快照栈的顺序应该怎么控制这些问题想清楚了你对程序状态管理的理解会上一个台阶。先跑起来再跑得优雅最后跑出你自己的设计味道——这就是做编程小项目最实在的路径。本文还有配套的精品资源点击获取
RELATED — 相关阅读

相关资讯

LATEST — 最新资讯

最新发布

TODAY — 本日精选

新闻

WEEKLY — 本周精选

新闻

MONTHLY — 本月精选

新闻