十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

Java五子棋源码实战:面向对象、Swing界面与简单AI设计

Java五子棋源码实战:面向对象、Swing界面与简单AI设计 简介本资源是一份基于Java语言实现的轻量级五子棋游戏源码包面向Java初学者与GUI编程入门者旨在通过完整可运行的小型项目帮助学习者掌握Swing界面开发、事件驱动编程、二维数组建模及胜负逻辑算法等核心实践技能。压缩包共27个文件包含24个Java源文件涵盖Main启动类、Board棋盘管理、GameController游戏逻辑控制、Piece棋子封装等核心模块、1个Maven配置文件pom.xml用于依赖与构建管理、1个readme.txt提供运行说明以及1个.gitignore规范版本控制行为整体仅40KB结构清晰、无冗余资源。目前已有269人学习下载。读者可直接导入IDE运行体验双人本地对弈深入理解MVC思想在小型游戏中的落地复用Stone、Player、PieceColor等设计良好的类结构并参考其中简洁的胜负判定算法如五连检测与图形绘制逻辑是巩固面向对象编程与GUI实战能力的优质教学范例。 做这个项目前我先把需求边界划清楚了最近学习群里又有人在找五子棋源码说是课程设计要交作业。我翻了一下手头的东西正好有一份很多年前用Java写的五子棋当时为了练手前后重构了三个版本控制台版、Swing界面版后来又加了简单的AI对战。这次干脆把这份源码的完整设计思路、核心逻辑和踩坑过程都整理出来给正在学Java的朋友做个参考。先说结论这份源码虽然叫“简单五子棋”但代码里该有的结构一点都不含糊。它不需要任何第三方依赖纯JDK环境就能编译运行非常适合Java初学者拿来理解面向对象设计、二维数组操作、事件监听和基础算法。如果你是刚学完Java语法想找一个能写到简历上的小项目或者正在做Java课程设计这篇内容可以直接帮你省掉大量的摸索时间。整个项目从完成度上分成三个层次第一层是控制台版本能让两个人在命令行里轮流下棋胜负判断完整第二层是Swing版本支持鼠标点击落子有清晰的棋盘绘制第三层是简单的电脑AI谈不上多聪明但足够让新手看懂“电脑是怎么思考该下哪一步的”。我先把这份源码的项目结构和数据模型讲清楚然后拆解落子逻辑、输赢判断、AI决策这几个核心模块最后把我在测试过程中撞过的坑和性能优化心得一并放出来。每段代码我都会解释为什么要这样写而不是只丢一个能跑的结果了事。1. 五子棋项目的类结构设计与职责划分写任何Java项目第一步不是急着敲代码而是把类结构定好。这份源码的包名是gobang一共拆了五个类每个类的职责非常单一这也是我后来能轻松给项目加AI功能的基础。类的划分如下表所示类名职责关键属性和方法GobangGame程序入口负责初始化游戏循环main()方法控制台与Swing两种启动模式ChessBoard棋盘数据模型保存所有落子状态int[][] boardinitBoard()isValidMove()Player管理玩家信息和落子动作namecolormove()Rule静态规则类判断输赢和合法性checkWin()checkFiveInLine()AI简单电脑玩家计算最佳落子点getBestMove()evaluatePoint()这个设计里最核心的决策是把棋盘状态和规则判断分成了两个独立的类ChessBoard和Rule。很多初学者喜欢把规则判断的方法直接写进棋盘类里短时间看确实方便但如果后面要做悔棋、复盘、AI评估棋盘类和规则逻辑缠在一起会非常痛苦。分开之后棋盘类只负责“我这个位置现在是空还是有子”规则类只负责“当前这步下在这个位置后是否达成了胜利条件”各管各的测试也好写。ChessBoard类内部用了一个int[][]二维数组保存棋盘状态15×15的棋盘就是int[15][15]0表示空位1表示黑子2表示白子。这里用int而不是boolean或枚举的原因很简单这个棋盘状态不仅要表示“有没有子”还要表示“是黑是白”int直接做角色区分最小成本。入口类GobangGame里做了一件比较有意思的事情启动时读取命令行参数如果带-console参数就走控制台交互否则启动Swing界面。这个设计让同一份核心逻辑可以在两个界面形态下复用也方便我在命令行下写自动化测试。启动入口的代码如下public class GobangGame { public static void main(String[] args) { boolean consoleMode false; for (String arg : args) { if (-console.equals(arg)) { consoleMode true; break; } } if (consoleMode) { new ConsoleRunner(new ChessBoard()).start(); } else { SwingUtilities.invokeLater(() - new GameFrame(new ChessBoard()).setVisible(true)); } } }这段代码里SwingUtilities.invokeLater()不是可有可无的装饰Swing组件的创建和更新必须在Event Dispatch Thread也就是EDT线程上执行否则在高分屏或快速点击时可能出现界面假死。控制台模式走的是普通线程两者互不干扰。2. 棋盘初始化与落子逻辑二维数组的正确打开方式五子棋的整个游戏状态就是一张棋盘所以数据模型的设计直接决定了后面所有代码的复杂度。我用的是最简单的int[][]但在这个“简单”的背后有几个关键细节值得展开说。2.1 棋盘尺寸与坐标体系的约定标准五子棋棋盘是15×15这份源码里在ChessBoard类开头定义了两个常量public static final int BOARD_SIZE 15; public static final int EMPTY 0; public static final int BLACK 1; public static final int WHITE 2;坐标体系的约定是board[row][col]row表示行号从上往下0到14col表示列号从左往右0到14。这个约定虽然在程序内部自洽但在后面做Swing界面时鼠标坐标换算成棋盘坐标需要在X轴和Y轴的处理上高度小心我在这里踩过很深的坑后面专门开一节来讲。初始化棋盘的方法特别简单就是遍历二维数组全部置0public void initBoard() { for (int i 0; i BOARD_SIZE; i) { for (int j 0; j BOARD_SIZE; j) { board[i][j] EMPTY; } } }2.2 落子前必须做的合法性校验落子的合法性校验是五子棋里最琐碎也最不能出错的逻辑。玩家点击一个位置必须先判断是不是越界了再判断这个位置是不是已经被占用了跳过一个都会出Bug。我的isValidMove()实现如下public boolean isValidMove(int row, int col) { if (row 0 || row BOARD_SIZE || col 0 || col BOARD_SIZE) { return false; } return board[row][col] EMPTY; }为什么要把越界检查放在数组访问之前因为Java数组越界会抛出ArrayIndexOutOfBoundsException如果这个异常没被捕获整个程序就崩了。先做范围判断再访问数组元素是这段代码能稳跑的前提。虽然题目叫“简单五子棋”但这种边界防护一定是刻在骨子里的习惯。落子操作本身也很直接public boolean placePiece(int row, int col, int color) { if (!isValidMove(row, col)) { return false; } board[row][col] color; moveCount; return true; }这里记录moveCount落子计数不是多余的后面AI判断“是不是该我下”“棋盘满没满”都能用上也可以用来实现简单的超时判断。我见过很多初学版本把落子记录省略了后面做悔棋功能时又得重新加不如一开始就留好扩展位。2.3 棋盘满局的判断不可忽略五子棋看起来棋盘很大15×15有225个位置看似永远不会下满。但实际测试中如果两个玩家水平都一般下到一百多手还没分出胜负是常态一旦没有空位程序必须给出“平局”而不是卡死。判断平局的逻辑我写在isBoardFull()里public boolean isBoardFull() { return moveCount BOARD_SIZE * BOARD_SIZE; }因为每次落子都会记录moveCount满局判断就是一个简单的数值比较时间复杂度O(1)不用遍历整个棋盘。这个细节虽然小但在实际游戏循环中每下一步都要调用一次效率上的差别在性能敏感的AI评估场景里会被放大。3. 输赢判断的核心算法从坐标映射到边界防护很多人写五子棋最头疼的就是输赢判断。初学者习惯在每次落子后遍历整个棋盘看有没有任何一个位置的横、竖、两个斜方向能连成五子。这种思路确实简单但问题是每次落子后都要检查225个位置乘以4个方向效率低且代码冗长。这份源码里的思路是只需要检查最后一步落子位置的四个方向即可。因为胜利的五子连线必然包含刚才落下的那一颗棋子否则这一手棋就不产生胜负。这个方法把每次判断的计算量从棋盘面积降到了固定常数不仅代码清晰算法效率也高出一个数量级。3.1 四方向检查的框架我用了一个方向偏移量数组来统一处理水平、垂直和两个对角线的检查private static final int[][] DIRECTIONS { {0, 1}, // 水平向右 {1, 0}, // 垂直向下 {1, 1}, // 主对角线方向 {1, -1} // 副对角线方向 };checkWin(int row, int col, int color)的实现逻辑是对每个方向从当前落子点出发沿正方向数连续同色棋子数再沿反方向数连续同色棋子数两者相加再减1因为当前棋子被数了两次如果大于等于5就说明连成五子。public boolean checkWin(int row, int col, int color) { for (int[] dir : DIRECTIONS) { int count 1; count countDirection(row, col, dir[0], dir[1], color); count countDirection(row, col, -dir[0], -dir[1], color); if (count 5) { return true; } } return false; } private int countDirection(int row, int col, int dRow, int dCol, int color) { int count 0; int r row dRow; int c col dCol; while (r 0 r ChessBoard.BOARD_SIZE c 0 c ChessBoard.BOARD_SIZE board[r][c] color) { count; r dRow; c dCol; } return count; }这里的row dRow和col dCol就是沿某个方向逐步移动。while循环里必须先判断边界再访问数组这一点和落子合法性校验的逻辑一致都是防数组越界的标准姿势。3.2 为什么不能用“扫描全局”来判断输赢把一个需求实现出来容易但实现得是否优雅决定了代码的后续可维护性。全盘扫描法在功能上完全正确但存在三个问题第一时间复杂度O(N²)N是棋盘边长每手棋都要做225次×4方向的检查虽然绝对耗时不大但如果是AI在对弈中批量评估成百上千个候选点时这个开销会被放大得很明显。第二全盘扫描的输出不包含“是哪个位置连成了五子”的信息做界面高亮胜利棋子的功能时又要再写一套代码。第三从工程思维上看“每次落子只影响局部区域的胜负状态”这个直觉才是正确模型全局扫描是违背直觉的多余计算。所以我在这份源码里采用的是增量检查每次落子后只围绕落子点展开四个方向的连续性统计。这不仅让单次判断变得极快还顺带得到了“哪几个位置的棋子构成了五连”的信息为后续界面高亮和复盘功能铺好了路。3.3 坐标体系不一致引发的Bug一次真实排错记录这里我必须专门讲讲坐标问题因为这是我重构时踩过最深的一个坑而且它非常隐蔽。控制台模式下玩家输入的是行 列比如7 7这在二维数组里对应的是board[6][6]因为数组索引从0开始而人习惯从1开始数。我在控制台版本里做了row - 1的换算一切正常。但到了Swing版本鼠标点击返回的是像素坐标需要先换算成棋盘坐标。我的第一个版本写成了int row (int) ((y - margin) / cellSize); int col (int) ((x - margin) / cellSize);看起来没问题问题出在margin偏移量的定义上。我在绘制棋盘时棋盘的左边界和上边界都留了30像素的边距但绘制棋格线时用的cellSize是40像素。当鼠标点击在棋盘最左上角的交叉点上时像素坐标大约是(30, 30)换算结果是(0, 0)正确。但当鼠标点击在棋盘最右下角交叉点上时坐标大约是(3014×40, 3014×40) (590, 590)换算结果是((590-30)/40, (590-30)/40) (14, 14)也正确。坑不在边界而在棋盘大小不是整数倍的关系。15条线之间有14个间隔每个间隔40像素棋盘总宽14×40560像素。如果我把cellSize理解成“格子边长”那么第15条线的像素坐标应该是margin 14 * cellSize如果理解成“交叉点间距”那15个交叉点之间的像素跨度是14 * cellSize。这两种理解差一个cellSize的距离会导致最右边一列和最下边一行棋子永远画不在交叉点上。我一开始混合使用了这两种定义结果棋盘上的棋子越下越歪九段高手也救不回来。正确的统一约定是cellSize表示相邻交叉点的间距交叉点的像素坐标是margin i * cellSizei从0到14鼠标坐标换算成棋盘索引用int((x - margin) / cellSize 0.5)四舍五入到最近的交叉点。我用一个统一的pointToIndex()方法封装了这个换算逻辑后面再加任何坐标系转换都不慌了。4. 简单AI的实现从“就近堵截”到“打分评估”五子棋的AI是这个项目里最有意思的部分也是Java学习者最容易卡住的地方。网上能搜到很多“五子棋地狱难度ai”的视频那些大多是实现了α-β剪枝搜索树的方案。但作为课程设计或练手项目我建议先实现一个“会堵棋”的简单AI能挡住一般的新手等理解了评估思想再上搜索树不迟。这份源码里的AI分为两个版本第一个版本是“就近堵截型”逻辑极其简单但效果意外地好。4.1 版本一敌人下一步要赢我就堵住它核心思路非常直观遍历棋盘每一个空位模拟把白子放在这里如果这个位置能让白棋直接达成五连那么这个位置就是“威胁点”黑棋AI必须优先堵住。public int[] getDefensiveMove() { for (int row 0; row BOARD_SIZE; row) { for (int col 0; col BOARD_SIZE; col) { if (board.isEmpty(row, col)) { board.placePiece(row, col, WHITE); boolean isThreat rule.checkWin(row, col, WHITE); board.placePiece(row, col, EMPTY); // 模拟后恢复 if (isThreat) { return new int[]{row, col}; } } } } return null; }注意这里的恢复技巧board.placePiece(row, col, EMPTY)。很多人喜欢先备份原来的值再恢复但在这个场景下空位本来就是为了模拟才填入的恢复成空位就行。关键是下完必须恢复否则AI每评估一个位置棋盘就被污染一次几步之后AI就“瞎”了。这个版本能挡住所有“就差一步就赢”的威胁。不过它的局限也很明显如果对手还没形成“差一步”的局面AI就不知道该往哪下了只会走一个默认位置。实际测试下来这个水平大概能赢不会布局的新手但遇到稍微懂一点“活三”“冲四”的人就会败下阵来。4.2 版本二基于位置打分的启发式评估第二个版本把AI从“被动堵漏”升级为“主动评估”。核心思想是对棋盘上的每一个空位分别计算“如果我方下在这里的得分”和“如果对方下在这里的得分”最终选择两者之和最高的位置。得分依据是当前位置在四个方向上能形成的连续棋子数量和开放程度。例如“活三”比“眠三”更危险“冲四”比“活三”更紧急。这里我简化成了棋子连子数量与两端开放状态的乘积没有引入更复杂的棋型库但效果已经能应付大多数业余玩家。核心评估代码是public int evaluatePoint(int row, int col, int color) { int totalScore 0; for (int[] dir : DIRECTIONS) { int count 1; int openEnds 0; count countDirection(row, col, dir[0], dir[1], color); count countDirection(row, col, -dir[0], -dir[1], color); if (isOpen(row, col, dir[0], dir[1], color)) openEnds; if (isOpen(row, col, -dir[0], -dir[1], color)) openEnds; int score count * count * openEnds; totalScore score; } return totalScore; }count * count的设计是个经验值。连子数越多威胁指数级增长4个连子和3个连子的差距绝不是1个棋子的差距而是能不能赢的本质区别。用平方的权重能确保AI优先形成四连而不是去盲目围堵对方的三连。AI最终决策的逻辑是public int[] getBestMove() { int bestScore -1; int[] bestMove {7, 7}; // 默认走天元 for (int row 0; row BOARD_SIZE; row) { for (int col 0; col BOARD_SIZE; col) { if (!board.isEmpty(row, col)) continue; int attackScore evaluatePoint(row, col, BLACK); int defendScore evaluatePoint(row, col, WHITE); int totalScore attackScore defendScore; if (totalScore bestScore) { bestScore totalScore; bestMove new int[]{row, col}; } } } return bestMove; }这个策略属于攻守兼备的贪心算法。attackScore是自己在某个位置落子的进攻价值defendScore是如果对手下在这里的威胁价值两者相加意味着AI既想积极进攻又不会对对手的威胁视而不见。实际对局中它会主动形成活三、冲四同时能及时封堵对手的活三之路。4.3 AI评估的边界条件与优化思路上述评估逻辑有一个隐患当棋盘上都是空位时AI遍历225个空位每个都做4个方向的评估总计算量是900次方向评估人眼完全无感。但AI对战AI或者加上了搜索树之后这一步会被调用成千上万次性能就必须优化了。一个常见优化是只看落子点附近8×8范围内的空位因为距离当前所有棋子太远的位置评估价值极低多半是浪费时间。代码上可以做一个简单的isNearby(int row, int col)判断只有既有棋子存在的周边才纳入评估集。另一个优化点是评估方向的对称性。事实上countDirection(row, col, dRow, dCol)和countDirection(row, col, -dRow, -dCol)的结果有大量重复计算可以用缓存或增量更新的方式减少计算但在“简单五子棋”这个定位上我选择了保持代码可读性优先这些优化可以作为进阶练习留给读者。5. 控制台版本与Swing版本两种交互形态的演进路线同一个核心逻辑我做了两套交互界面这段经验对Java学习者应该很有启发。控制台版本适合理解游戏循环逻辑Swing版本适合理解Java GUI编程和事件模型。5.1 控制台版的游戏循环设计控制台版的核心逻辑是经典的“三方循环”人下棋→判断胜负→电脑下棋→判断胜负→再来一轮。这里的关键是不要让游戏循环和界面渲染耦合在一起。public void start() { chessBoard.initBoard(); while (true) { playerMove(); // 玩家落子 if (rule.checkWin(lastRow, lastCol, ChessBoard.BLACK)) { System.out.println(黑棋胜利); break; } if (chessBoard.isBoardFull()) { System.out.println(平局); break; } int[] aiMove ai.getBestMove(); chessBoard.placePiece(aiMove[0], aiMove[1], ChessBoard.WHITE); System.out.println(电脑落子 (aiMove[0] 1) (aiMove[1] 1)); if (rule.checkWin(aiMove[0], aiMove[1], ChessBoard.WHITE)) { System.out.println(电脑胜利); break; } } }这个循环结构非常标准几乎所有回合制游戏都是这个骨架。玩家输入的部分做了异常处理如果输入不是两个数字就提示重新输入不会直接崩掉。有一点要提醒控制台版本的输入用的是Scanner在连续输入时要注意缓冲区问题最好每次都用nextLine()读取整行再拆分避免nextInt()和nextLine()混用导致的换行符残留。5.2 Swing版本的绘制与事件绑定Swing版本的核心是一个继承JPanel的自定义画板类。我在paintComponent()里绘制棋盘和棋子在MouseListener的mouseClicked()里处理落子事件。绘制棋盘线的代码逻辑清晰简单Override protected void paintComponent(Graphics g) { super.paintComponent(g); Graphics2D g2 (Graphics2D) g; g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); g2.setColor(new Color(210, 180, 140)); g2.fillRect(0, 0, getWidth(), getHeight()); g2.setColor(Color.BLACK); for (int i 0; i BOARD_SIZE; i) { g2.drawLine(margin, margin i * cellSize, margin (BOARD_SIZE - 1) * cellSize, margin i * cellSize); g2.drawLine(margin i * cellSize, margin, margin i * cellSize, margin (BOARD_SIZE - 1) * cellSize); } }这里开启抗锯齿ANTIALIASING是画棋子不毛边的关键。我见过很多朋友画的棋子边缘全是锯齿以为是坐标算错了其实只是没开抗锯齿。另外Swing的paintComponent()方法在窗口最小化、拉伸时会被系统反复调用所以绘制逻辑必须是无状态的不能依赖上一次绘制的中间状态。我最初的版本把棋子列表存在ArrayList里绘制时统一遍历这样窗口重绘后棋子不会消失。5.3 线程模型与界面卡顿的坑Swing版在接入AI后出现过一个经典问题点击鼠标落子后AI计算需要几百毫秒正常情况下可以接受但如果后续升级评估逻辑计算时间超过一秒钟界面就会“卡死”。原因在于鼠标点击事件处理器是在EDT线程里执行的如果在这个线程里做了耗时计算界面就无法重绘。解决方案是在点击事件里开一个新线程执行AI计算算完后再通过SwingUtilities.invokeLater()把结果交回EDT线程更新界面。这个模式是Swing进阶必须掌握的我先在这里点破等你的AI真的变复杂了思路就清晰了。6. 代码验证、测试路径与常见Bug排查写完了五子棋最终要验证它是不是真的能用。这部分我分享一下我的测试路径和遇到的问题每一件都是实际踩过的不是纸上谈兵。6.1 用命令行参数快速验证核心规则核心规则类Rule不依赖界面所以可以做纯逻辑测试。我推荐初学者养成一个习惯写好规则后不急着接界面先写一段模拟落子的测试代码直接在控制台跑。比如模拟一个四连和一个空位验证AI能不能准确“看到”这个威胁。一个简单的测试脚本如下public class RuleTest { public static void main(String[] args) { ChessBoard board new ChessBoard(); board.placePiece(7, 7, BLACK); board.placePiece(7, 8, BLACK); board.placePiece(7, 9, BLACK); board.placePiece(7, 10, BLACK); System.out.println(new Rule(board).checkWin(7, 10, BLACK)); // 应输出 true System.out.println(new Rule(board).checkWin(7, 8, BLACK)); // 应输出 true } }这种测试不需要任何GUI跑一次就能确认输赢判断是否在边界情况下也正确。我在开发中发现所有方向边界面比如把棋子下在第0列、第14行等最容易出错因为方向扫描到棋盘边界就不得不停止一旦边界判断顺序写错就会抛出数组越界异常。建议读者在测试时专门覆盖四角和四条边的位置。6.2 两个容易被忽略的经典BugBug 1数组越界但被try-catch吞掉。我最初写完countDirection时边界判断写在数组访问之后结果抛异常。当时图省事加了try-catch包围程序“看起来”没崩但连续计数永远只数到一个棋子胜负判断全错。排查了一下午才发现是边界判断顺序的问题。这个教训让我深刻明白异常处理是最后一道防线不是解决逻辑错误的万能药。Bug 2Swing点击区域偏移。前面讲坐标系时提到过这个问题。如果你的棋子在棋盘上越画越歪检查一下margin和cellSize两个常量的定义是否统一。我最终使用了一个getIndexByPixel(int pixel)方法把“像素坐标→数组索引”的换算集中在一个方法里界面各处都调用它再也没出过坐标错乱。6.3 运行效果与简单实测数据我用黑棋AI对白棋普通玩家我自己实测了十局胜率大概是7:3。AI前五步基本会走在天元附近后续会主动布活三同时能敏锐地堵住我的冲四。期间也有两局AI判断出现“假赢”就是明明形成了四连但被自己堵了一端导致后续进攻力下降。这个原因是评估函数里没有对“活四”和“冲四”做更细的区分属于简化策略的已知局限。如果你想让AI更聪明可以做的升级方向是引入棋型表活四、冲四、活三、眠三、活二给不同棋型赋予不同的权重再用递归搜索配合α-β剪枝。但这已经是“五子棋地狱难度AI”的范畴了跟这份“简单五子棋”源码的定位不一样我建议先把基础版本吃透再进阶。7. 源码使用方式、扩展方向与个人体会如果你拿到这份源码用javac GobangGame.java编译后运行java GobangGame -console就能在命令行直接对弈不带参数则启动图形界面。图形界面上默认玩家执黑先行AI执白后手这个颜色分配也可以改成玩家执白在入口处调整一下棋子枚举值即可。关于扩展方向我建议三个最顺手的路径第一加上“悔棋”按钮实现方式是在落子历史里加入栈结构悔棋时从栈顶弹出一到两步并更新棋盘第二加上“谁先手”的菜单选项这个改动只需在初始化时交换黑白赋值第三把AI的“只评估当前空位”升级成两层的极小极大搜索每层各评估一次棋盘局面代码量增加不大但棋力会有质的飞跃。我在做这个项目的过程中最大的体会是五子棋看似简单但“规则判断”“界面交互”“AI决策”三个模块的经验几乎覆盖了Java游戏开发的所有核心知识点。不要抱着“网上有现成源码我抄一下”的心态去做自己动手拆一遍、改一遍、踩一遍坑收获会大得多。哪怕只是把小版本的代码逐行读一遍把“为什么用二维数组”“为什么边界先判断”“为什么AI评估要分开计算进攻和防守分”这几个问题想清楚你的Java功力都能往上走一层。如果你在实际跑这份源码时遇到任何异常堆栈或逻辑问题欢迎回来对照这篇内容的排查思路再过一遍。五子棋这个项目我已经写过三个版本每次重构都有新理解现在回头看它的性价比比很多花哨的框架项目都高。本文还有配套的精品资源点击获取
返回列表