ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Java五子棋完整源代码:从二维数组到胜负判断的全解析

Java五子棋完整源代码:从二维数组到胜负判断的全解析 简介这是一份面向Java初学者的五子棋游戏开发实践资源旨在帮助零基础或入门级学习者通过完整可运行项目掌握Swing图形界面编程、事件驱动机制、二维数组逻辑判断及基础算法设计等核心技能。资源包含全部Java源码文件以.java为主涵盖主程序入口、棋盘绘制、落子逻辑、胜负判定与重置功能等模块代码配有逐行级中文注释关键算法处附有思路说明便于理解黑白棋子坐标映射、连珠检测等实现细节。压缩包为RAR格式共156KB结构简洁无冗余文件适合快速导入IDE运行调试。目前已有9278人下载学习是初学者巩固语法、培养工程思维与提升代码阅读能力的优质参考范例。 写一个可以运行出画面的Java小程序是很多人学编程的第一个里程碑。今天聊的这套Java五子棋游戏完整源代码就属于这种“能看见效果、能交互、还能反复折腾”的入门项目。它不像图书管理、学生管理系统那样全是增删改查也不像仿写电商页面那样偏前端而是把Java基础语法、面向对象思想、GUI界面、事件监听、数组操作、基础算法全部揉进一个棋盘里说它是初学者第一次把课堂知识“组装”成完整作品的绝佳载体一点都不夸张。这套源代码最大的特点是每一处关键逻辑都配了详细注释。不只告诉你代码在做什么还告诉你为什么这么做。比如为什么棋盘要用二维数组存储、鼠标点击坐标为什么要做一次换算、胜负判断为什么只查刚落下的那颗子周围的四个方向。这些恰恰是初学者看普通源码时最容易卡住的地方。如果你刚学完Java基础正愁不知道怎么写一个像样的项目或者做课程设计想找一个难度适中、能讲清楚原理、又能现场演示的题目这个项目都很值得花一个周末完整搞一遍。1. 为什么拿五子棋当第一个练手项目1.1 这个项目覆盖了Java入门的全部关键知识点很多初学者学完循环、数组、类与对象之后最大的困惑是“这些东西到底有什么用”。五子棋项目就能完美回答这个问题。整个项目从技术角度看涉及的几乎全是Java基础语法里最核心的几个板块。首先是类与对象的设计。一个最简单的五子棋程序可以拆成三个类棋盘类负责存储棋局数据界面类负责绘制窗口和棋盘逻辑类负责判断落子是否合法、是否分出胜负。这种职责划分本质上就是面向对象思想的落地比死记硬背“封装、继承、多态”的概念有意义得多。其次是二维数组。棋盘天然就是15乘15的网格用int[][]存储每个交叉点的状态再合适不过。0代表空1代表黑棋2代表白棋。落子操作本质上是修改数组元素胜负判断本质上是沿着数组索引进行搜索。数组这一章学得好不好在这个项目里一眼就能看出来。再次是GUI编程和事件监听。Swing提供了一套完整的事件模型把鼠标点击映射为棋盘上的落子需要理解MouseListener、MouseEvent、paintComponent这些组件的配合逻辑。这部分虽然属于Java入门里的进阶内容但实际写起来代码量并不大非常适合第一次接触GUI的读者。1.2 为什么选Swing而不是JavaFX或网页版技术选型永远是项目的第一个问题。我给初学者的建议是直接用Swing别折腾JavaFX也别一上来就拆成前后端。原因很现实。Swing是JDK自带的图形界面库装好JDK就能用不需要额外安装运行时不需要配置依赖也不用理解Maven和Gradle这些构建工具。对刚入门的同学来说把环境复杂度降到最低才能把注意力全部放在代码逻辑本身上。JavaFX确实更现代、界面更漂亮但它需要单独引入模块还涉及FXML配置文件、场景图这些概念对初学者来说完全是额外的认知负担。另外Swing的事件模型非常直白。你注册一个鼠标监听器点击事件触发后调用你写好的处理方法这个“回调”的思想理解透了后面学Android的点击事件、前端的addEventListener基本都是同一个套路。1.3 项目整体结构与代码组织方式这套源代码遵循了一个极简的分层思路虽然不像企业级项目那样有严格的MVC架构但思路是对的数据、界面、逻辑分开相互之间通过方法调用通信。从源码角度看项目主要包含以下几个核心类src/ Main.java // 程序入口负责启动窗口 board/ChessBoard.java // 棋盘核心逻辑包括数据存储、落子、胜负判断 ui/GameFrame.java // 主窗口负责绘制界面、处理鼠标事件有的版本会把胜负判断单独抽成一个CheckWin类这也挺好。如果代码量再大一点比如加入人机对战把AI算法独立成一个AIPlayer类就非常必要了。这个组织方式其实是在给初学者建立一种观念写代码时先想清楚“谁来负责什么”而不是把几百行代码全塞进main方法里。2. 核心功能拆解与关键代码解析2.1 棋盘数据结构一个二维数组是如何承载整局游戏的五子棋的核心棋盘状态本质上就是一个15乘15的二维数组。代码里通常这样定义public class ChessBoard { public static final int SIZE 15; // 棋盘行列数 public static final int EMPTY 0; // 空位 public static final int BLACK 1; // 黑棋 public static final int WHITE 2; // 白棋 private int[][] board new int[SIZE][SIZE]; // 棋盘数据 }为什么用int而不是boolean因为棋盘有“空、黑、白”三种状态一个boolean只能表达两种状态不够用。用int最直观配合三个常量定义代码可读性远好过直接写魔法数字1和2。落子逻辑的核心方法是move(int row, int col, int player)。它要做三件事判断坐标是否在棋盘范围内、判断该位置是否已经有棋子、如果都通过就把数组对应位置改成当前玩家的编号。这就是一个完整的“落子”操作所有界面上点击棋盘的反馈最终都要落到这个简单的数组赋值上。这里给初学者一个非常重要的建议必须让数组成为棋局的唯一事实来源。界面上棋子画在哪里由数组内容决定而不是界面记一份、数组记一份。很多初学五子棋的人懒得维护数组直接靠绘制坐标来判断哪里有棋结果悔棋功能一加就乱套就是因为两份数据不同步了。2.2 棋盘的绘制与像素坐标换算有了数据模型之后最难理解的一块就是绘制。Swing里绘制棋盘要重写JPanel的paintComponent(Graphics g)方法。在这个方法里画15条横线、15条竖线再画上所有已经落下的棋子。Override protected void paintComponent(Graphics g) { super.paintComponent(g); // 画网格线 for (int i 0; i ChessBoard.SIZE; i) { g.drawLine(margin i * cellSize, margin, margin i * cellSize, margin (SIZE - 1) * cellSize); g.drawLine(margin, margin i * cellSize, margin (SIZE - 1) * cellSize, margin i * cellSize); } // 根据数组内容画棋子 for (int row 0; row SIZE; row) { for (int col 0; col SIZE; col) { if (board[row][col] ChessBoard.BLACK) { g.fillOval(margin col * cellSize - radius, margin row * cellSize - radius, 2 * radius, 2 * radius); } // 白棋类似 } } }初学者最容易在这里犯迷糊画线用的是像素坐标而下棋用的是数组下标。这两个东西之间需要一层换算。棋盘左上角第一个交叉点在像素坐标(margin, margin)处每个格子边长为cellSize像素那么数组下标(row, col)对应的像素坐标就是(margin col * cellSize, margin row * cellSize)。反过来鼠标点击到了像素(x, y)想算出对应的数组下标就需要做一次反换算int col Math.round((x - margin) / (float) cellSize); int row Math.round((y - margin) / (float) cellSize);这里取整要用Math.round而不是直接int强转。因为一个点击落在格子里靠中间的位置必然有小数偏移四舍五入能选到最近的交叉点直接截断会让棋子落点的位置偏左上体验差别很明显。2.3 落子与玩家切换的逻辑顺序落子逻辑从界面层触发最终在棋盘类里完成。整个流程有一个固定的顺序顺序错了就会出现各种神奇的bug。第一步是坐标转换。鼠标点击事件里拿到的是像素坐标先通过上面的公式换算成行列下标。第二步是合法性检查。检查行和列是否在0到14之间再检查该位置是否已经为EMPTY。第三步才是真正落子修改数组并触发重绘。最后一步是切换玩家并检查当前这步棋有没有让某个玩家赢。初学者常犯的错误是把“切换玩家”写在“落子”之前或者忘了在落子前检查棋盘上该位置是否已有棋子。还有一种常见的顺序错误是“先判断胜负再落子”结果刚下的棋子还没写进数组判断永远找不到五子连珠。下面这段伪代码展示了正确的调用顺序public void mouseClicked(MouseEvent e) { // 1. 像素坐标转换成行列下标 int row rowAt(e.getY()); int col colAt(e.getX()); // 2. 合法性检查 if (!isValidMove(row, col)) return; // 3. 落子 board.move(row, col, currentPlayer); // 4. 重绘棋盘 gamePanel.repaint(); // 5. 检查胜负 if (board.checkWin(row, col, currentPlayer)) { showWinMessage(currentPlayer); } // 6. 切换玩家 currentPlayer (currentPlayer ChessBoard.BLACK) ? ChessBoard.WHITE : ChessBoard.BLACK; }这里有一个没那么明显但很值得注意的细节checkWin的入参是row和col也就是刚下的这一步棋的位置。胜负判断不需要检查整个棋盘只需要检查以这个点为中心的四个方向所以传入坐标就能显著缩小搜索范围。2.4 胜负判断算法从落子点出发做方向扫描胜负判断是整个项目里最核心的算法也是面试和答辩时最容易被人追问的部分。它的思路并不复杂一颗棋子落下后如果它是制胜点那么在这颗棋子的水平、垂直、左上到右下、右上到左下四个方向中必然有一个方向上有至少连续5颗同色棋子。以水平方向为例实现逻辑是从落子点出发向左数连续同色棋子的数量再向右数连续同色棋子的数量两者相加再加1自己这一颗如果大于等于5就赢了。public boolean checkWin(int row, int col, int player) { // 四个方向:水平、垂直、主对角线、副对角线 int[][] directions {{0, 1}, {1, 0}, {1, 1}, {1, -1}}; for (int[] dir : directions) { int count 1; // 正方向数 count countDirection(row, col, dir[0], dir[1], player); // 反方向数 count countDirection(row, col, -dir[0], -dir[1], player); if (count 5) { return true; } } return false; } private int countDirection(int row, int col, int dRow, int dCol, int player) { int count 0; int r row dRow; int c col dCol; while (r 0 r ChessBoard.SIZE c 0 c ChessBoard.SIZE board[r][c] player) { count; r dRow; c dCol; } return count; }这个写法把四个方向的检查统一成了一个for循环用方向向量{0,1}代表水平向右{1,0}代表垂直向下{1,1}代表右下{1,-1}代表左下。代码量少、逻辑清晰还能避免复制四份几乎一样的代码。代码里最关键的是while循环的边界检查。数组下标从0到14如果落子点在角落比如(0, 0)往左上方向数就会访问到(-1, -1)不先判断边界直接取数组元素就会抛出ArrayIndexOutOfBoundsException。这个异常初学阶段会经常遇到调试多了反而能加深对数组边界的理解。顺带提一个有对比价值的知识点赢棋判断本质上是一个搜索问题而不是排序问题。网上经常有人把“冒泡排序java”这类算法题和项目里的算法混在一起聊其实五子棋胜负判断用的是穷举搜索思想跟排序没什么关系。认清这个区别能帮助初学者建立“算法是解决具体问题的具体手段”这个观念而不是拿几个经典排序算法去硬套所有场景。2.5 悔棋、重开与棋盘重置的实现思路大部分五子棋源码里会附带“重开一局”的功能好的版本还会带悔棋。这些功能本身不难但它们是练习数据结构的好机会。重开一局最简单把所有数组元素重置为EMPTY再把当前玩家设回黑棋最后重绘棋盘就行。悔棋稍微需要动点脑筋。最简单的方案是用一个栈保存每一步的落子坐标和玩家信息。每次落子时压栈悔棋时从栈顶弹出然后把对应位置的数组元素重置为EMPTY再把当前玩家切换回上一步的玩家。Dequeint[] history new ArrayDeque(); // 落子时 history.push(new int[]{row, col}); // 悔棋时 if (!history.isEmpty()) { int[] last history.pop(); board[last[0]][last[1]] ChessBoard.EMPTY; currentPlayer (currentPlayer ChessBoard.BLACK) ? ChessBoard.WHITE : ChessBoard.BLACK; gamePanel.repaint(); }如果你正在学Java集合框架那么Deque、LinkedList、ArrayList这几个容器在这一步就能实打实地用起来。栈后进先出的特性跟悔棋逻辑天然匹配用起来顺理成章。注意这里不用Stack类因为它是早期的遗留类Deque接口的实现类是如今更推荐的版本用栈数据结构的核心思想去理解它们会很容易。3. 注释的艺术为什么这套代码的注释值得逐行读3.1 注释不是废话而是设计思路的还原标题里“详细注释”这四个字其实是这个项目最重要的价值之一。很多开源项目的源码质量很高但初学者根本读不进去因为注释太少了或者全是代码的复述。比如这一段// 为什么这里要减1因为cellSize是格子边长 // 最后一个交叉点的坐标是 margin (SIZE-1) * cellSize // 如果不减1会多画出来一条出界的线。这种注释看起来像一个人在你旁边讲解而不是简单地把代码翻译成文字。注释的重点应该放在“为什么”上而不是“是什么”。变量名本身已经能说明“是什么”了如果注释还在写“int col 是列号”这种内容那叫噪音。3.2 Javadoc注释与类设计文档类头注释应该说明这个类负责什么、怎么使用以及依赖了哪些其他类。方法的注释则要写清楚入参、返回值以及边界条件。例如/** * 判断指定玩家在指定位置落子是否形成五连 * * param row 落子的行下标范围 0~14 * param col 落子的列下标范围 0~14 * param player 当前玩家ChessBoard.BLACK 或 ChessBoard.WHITE * return 如果该步棋形成了至少五个连续同色棋子返回 true */ public boolean checkWin(int row, int col, int player) { ... }这种写法不仅是给读者看的也是给你自己看的。使用支持Javadoc的IDE时鼠标悬停在方法名上就能看到这个说明调试的时候省很多事。以后你进入团队开发这种标准的文档注释风格会被直接沿用养成习惯越早越划算。3.3 初学者写注释的三个常见误区第一个误区是注释太多。每行代码后面都加一句注释反而淹没了真正的重点。好的注释密度应该集中在复杂逻辑、边界条件、算法思路上简单赋值语句不需要解释。第二个误区是注释和代码不一致。有人改代码的时候忘了改注释结果注释描述的逻辑跟实际行为完全对不上。这种注释比没有注释更坑人因为读者会被带偏。写完代码打开文件扫一遍先检查注释过时没过时。第三个误区是命名不规范靠注释补救。如果变量的名字写的是int a、int b就算注释写了“a是行号b是列号”读起来依然很难受。正确的做法是先让名字自己说话int row、int col、int player。注释应该补充名字说不清楚的信息而不是给糟糕的命名做额外解释。4. 运行环境准备与调试实测记录4.1 第一步当然是配置Java环境这个项目依赖的只有JDK不需要引入第三方库所以环境配置非常轻量。你需要先确认本机的JDK版本。大多数情况下JDK 8以上就能直接运行用较新的JDK 11或JDK 17也没问题。如果还没配置过Java环境变量这里分享一下最常规的做法。以Windows为例安装JDK之后需要设置三个环境变量JAVA_HOME、Path和CLASSPATH。需要注意的关键点是JAVA_HOME应指向JDK的安装目录而不是JDK里的bin目录Path里要增加%JAVA_HOME%\binCLASSPATH在多数情况下可以不用手动设置除非你正好遇到编译找不到类的问题。验证配置是否成功打开命令行工具输入java -version javac -version两条命令都能输出版本信息就说明环境配置正常。如果你在IDEA里直接运行其实不配置环境变量Path也能跑因为IDE内部会定位JDK的位置但配置一遍有很多好处不仅对命令行编译项目管用以后学Spring Boot、Maven这些工具也会顺畅很多。4.2 编译运行步骤从源码到可玩的棋盘拿到源代码之后确保源码目录结构保持原样然后在源码根目录打开命令行执行javac Main.java java Mainjavac是Java编译器会把.java文件编译成.class字节码文件java命令负责启动虚拟机运行程序。如果你是第一次用命令行编译看见目录下生成了几个.class文件属于正常现象那是编译的中间产物不用去管它们。如果在IDEA或Eclipse里打开直接把Main.java设为启动类点运行即可。程序启动后会弹出一个窗口窗口标题通常是“Java五子棋”里面是一个纯色背景的棋盘鼠标点击即可落子。黑棋先行白棋跟随胜负产生后会弹一个对话框显示赢家。4.3 运行中三个高频问题的排查记录我见过很多人第一次跑这个项目时在终端里看到红色的异常信息就直接懵了其实大多数问题都可以归为三类。第一个问题是编译时提示“找不到符号”。这种情况最常见的诱因是类名和文件名不一致或者源码文件在错误的目录下。Java要求公共类名和文件名完全一致ChessBoard类必须保存在ChessBoard.java文件中大小写也不能错。改用IDE能规避大部分这种问题因为IDE会帮你自动处理文件与类名的对应关系。第二个问题是程序能启动窗口也弹出来了但点击棋盘没有任何反应。先说结论90%的情况是鼠标事件没有注册成功或者注册在了错误的组件上。比如你给JFrame添加了鼠标监听器但实际画棋盘的是一个JPanel子类你点击在面板上事件根本没到JFrame那里。解决办法是给你的棋盘面板添加监听器而不是整个窗口。第三个是棋子画出来是错位的。这个问题的根源在于坐标换算公式里的像素偏移算错了。你在纸上画一张示意图标清楚棋盘边距margin、格子间距cellSize、棋子半径radius然后把换算公式推导一遍通常能很快定位到底是哪个参数的问题。调试的时候可以在鼠标点击事件里打印行列下标在绘制方法里打印坐标对比一下是否对应。4.4 加一些“调试验证”的小技巧我建议初学者在一开始就学会用System.out.println打印关键中间量。落子时打印“第几行第几列黑棋/白棋落子”胜负判断时打印“当前方向、累计连子数”这样你能直观看到程序内部状态的变化。更进一步如果你在IDE里用断点调试在checkWin方法第一行打一个断点然后走到自己已经下了四颗子、即将下第五颗子的位置观察countDirection每次循环的返回值五子棋判断的正确性就能被你彻底搞清楚。这种调试过程比看一百遍讲解都管用因为它把你脑海中的逻辑假设与程序真实行为拉到了同一条线上。5. 从双人对战到人机对战给AI扩展留好余地5.1 新手最快能写出什么样的AI双人版做出来以后很多人的下一个念头是“能不能让电脑陪我下”。最简单的AI版本就一个词随机。用Random生成随机坐标如果该位置为空就落子否则重新生成。虽然“电脑”每步都在乱下但起码让游戏在没有朋友陪玩的时候也能继续对初学者的成就感提升是实打实的。随机AI的代码大概长这样public int[] getRandomMove(int[][] board) { Random random new Random(); while (true) { int row random.nextInt(15); int col random.nextInt(15); if (board[row][col] ChessBoard.EMPTY) { return new int[]{row, col}; } } }这段代码有一个理论上的风险如果棋盘满了还没找到空位while循环会永远执行下去。所以调用之前要检查isBoardFull()这个边界处理对养成严谨的编码习惯很有帮助。5.2 从“会堵棋”到“能进攻”的简单规则随机AI很快会让人觉得无聊于是可以进化到“规则AI”。最简单的规则是先检查自己能否通过落下一步棋形成五连能就赢再检查对手是否只差一步就五连是就堵如果都没有随便找个空位下。public int[] getBestMove(int[][] board, int aiPlayer) { // 第一优先级自己成五 for (int r 0; r SIZE; r) { for (int c 0; c SIZE; c) { if (board[r][c] EMPTY) { board[r][c] aiPlayer; if (checkWin(r, c, aiPlayer)) { board[r][c] EMPTY; return new int[]{r, c}; } board[r][c] EMPTY; } } } // 第二优先级堵对手成五 int humanPlayer (aiPlayer BLACK) ? WHITE : BLACK; for (int r 0; r SIZE; r) { for (int c 0; c SIZE; c) { if (board[r][c] EMPTY) { board[r][c] humanPlayer; if (checkWin(r, c, humanPlayer)) { board[r][c] EMPTY; return new int[]{r, c}; } board[r][c] EMPTY; } } } // 第三优先级随机 return getRandomMove(board); }这个AI逻辑中有一个比较巧妙的地方在“模拟落子——判断胜负——恢复棋盘”的过程中你直接在副本数组上操作判断结束后再重置。其实我们这里是直接在真实棋盘上模拟判断完必须恢复。这类“试走一步再回退”的思路再往后做更高级的博弈树搜索时也会用到相当于提前做了储备。我实测下来的感受是这个版本已经具备了最基础的智能感大多数情况下它知道主动赢棋也知道堵你的四连。但它没有长期规划能力不会刻意做“双三”这种必杀局面。到这里一个初学者已经完全可以从项目实践中理解“算法没有银弹”这个道理。5.3 再进阶一步权值评价法做个“有点脑子的”AI如果觉得规则AI不够过瘾可以尝试权值评分法。思路是给每个空位打一个分分别检查该位置在四个方向上能形成多少连续的棋子以及如果放上己方棋子后对局势的贡献度累计所有方向的评分取最高分的位置落子。int score 0; // 对四个方向分别统计 for (int[] dir : directions) { score evaluateDirection(board, row, col, dir, aiPlayer); }evaluateDirection要做的事是沿着这个方向数连子数再结合两端是否被堵死来给出权值。比如“活四”两头都开放的四个连续棋子的权值远高于“冲四”一头被堵的四个连续棋子因为活四基本等于必胜。“活三”和“眠三”的分数也要拉开差距否则AI会影响判断。这个方案大约是初级项目里性价比最高的AI升级路径代码量不大不需要深入的算法基础但下棋水平比随机AI高出一大截至少能跟新手打得有来有回。等你真的走通这条路再去看基于极大极小搜索和Alpha-Beta剪枝的五子棋AI会轻松很多。6. 给准备吃透这个项目的读者的学习建议6.1 拿到源代码之后的第一件事不是运行而是读很多人拿到“源代码详细注释”的下载包第一反应是解压、导入、运行、看到棋盘然后关掉。这样收获是零。我建议按下面的顺序来做先打开Main.java看程序的入口在哪里理清启动时创建了哪些对象。再打开ChessBoard.java一个方法一个方法地读读注释思考数据是怎么流转的。最后打开界面类把鼠标点击事件的处理流程和棋盘类的调用关系连起来。花一两个小时读完整份代码一定比你直接运行十次、点几百下棋盘收获多得多。读完以后亲手把代码重新敲一遍。不要复制粘贴哪怕对着源码敲也行。这个过程能暴露你对每一行的真实理解程度。敲完以后尝试改一个功能比如把棋盘改成19路围棋棋盘、把胜利条件改成需要五子而不仅仅是连续五子、或者给黑白棋加上分数统计。6.2 对后续学习路线的建议五子棋项目做完等于打通了“写一个完整Java程序”这条路。接下来可以有三个方向的选择。偏向界面开发的话把Swing换成JavaFX重写一遍实践FXML布局和样式表界面可以做得远比Swing精致。偏向后端开发的话把棋盘逻辑封装成Java类库然后用Socket做联机对战研究网络通信与线程同步。偏向算法研究的话在AI这条路上继续探索博弈树、蒙特卡洛树搜索每一种都能深入很多。这三个方向的起点都是这一套五子棋源代码甚至共享大部分棋盘逻辑。这说明一个项目只要架构清晰就具备很强的扩展弹性而这套代码里的分层写法从一开始就在为这种扩展留余地。6.3 写在最后的一点个人体会后来我带人的时候几乎每次都推荐五子棋作为第一个训练项目。它不像坦克大战那样需要太多游戏开发知识也不像管理系统那样无聊最关键的是它让你在一个周末内完整走完“需求分析—代码设计—实现—调试—演示”的全流程。我第一次写完自己的五子棋时棋盘画得歪歪扭扭AI只会随机落子但点击鼠标看到黑色棋子出现在屏幕上的一瞬间那种“我自己写出了一个能玩的游戏”的成就感到现在都记得。如果你现在正处在一个学完语法但写不出项目的阶段别焦虑也别急着刷所谓的面试题。找一台电脑装好JDK打开这份带详细注释的五子棋源码把每一行都读明白再亲手敲一遍尝试加一个属于你自己的功能。这个项目做完你对Java的感觉会完全不一样。本文还有配套的精品资源点击获取
返回列表