简介面向有一定编程基础、对游戏开发与AI算法感兴趣的读者此教程以完整工程为例系统讲解五子棋从结构设计到智能对弈的实现过程涵盖棋盘、棋子、玩家、事件驱动游戏循环等核心模块并深入介绍极大极小算法与Alpha-Beta剪枝等经典AI方案。压缩包共65个文件以14个.h头文件和12个.cpp源文件为主另有obj编译中间文件、可直接运行的exe、若干wav/bmp/ico游戏资源以及一份doc格式论文文档工程文件完整整体仅2.73MB。已有205人学习下载既适合自学也适合作为毕业设计或课程项目参考。包内可运行程序与论文说明搭配便于边运行边对照代码快速理解胜负判断、合法落子及AI决策过程。在此基础上还可尝试扩展悔棋、存档、难度调节等玩法非常适合学习面向对象设计、GUI事件处理与博弈树搜索并能借助文档梳理从需求分析到编码实现的完整思路是提升编程实战能力的实用素材。1. 是时候把 Java 五子棋游戏从“能看懂教程”变成“能自己跑通”了“Java 五子棋游戏”这个关键词几乎是每个学完 Java SE 的人都会碰到的练手题。但我见过不少照着教程敲完代码的人卡在同一个地方棋盘画出来了棋子点不上去或者棋子能下了五子连珠却永远判不出来。这份教程带的源码包和课程文档把棋盘绘制、落子逻辑、胜负判定、悔棋这一条链路完整串了起来属于典型的“拿到就能跑、跑完能改”的资源。最适合刚学完集合、异常、Swing 的读者也适合想快速复习 Java GUI 编程的开发者——它能直接解决“代码写完了但窗口不响应、逻辑有边界 bug”这类实际问题。2. Java 五子棋游戏的核心骨架棋盘数据模型与胜负判定2.1 棋盘数据模型为什么二维数组比 List 更省事一份合格的五子棋教程开篇不会急着画窗口而是先把棋盘的数据结构定下来。最常见的做法是直接用int[][]二维数组而不是用ListPoint加颜色集合。原因很直白五子棋的棋盘是固定网格二维数组天然对应行列坐标落子、判断、悔棋都能通过下标直接定位不需要遍历查找。package com.gobang.demo; public class GobangBoard { public static final int SIZE 15; // 棋盘路数15 路是五子棋标配 public static final int EMPTY 0; // 空位 public static final int BLACK 1; // 黑子 public static final int WHITE 2; // 白子 private final int[][] board new int[SIZE][SIZE]; private int currentPlayer BLACK; // 黑子先手 public boolean isValidMove(int row, int col) { // 行列越界直接返回 false防止后续数组下标越界 if (row 0 || row SIZE || col 0 || col SIZE) { return false; } return board[row][col] EMPTY; } public boolean placePiece(int row, int col) { if (!isValidMove(row, col)) { return false; } board[row][col] currentPlayer; currentPlayer (currentPlayer BLACK) ? WHITE : BLACK; // 换手 return true; } }这段代码里SIZE是全局可调的配置把 15 改成 19 就能得到一副十九路棋盘。isValidMove把越界和已落子两个条件合并placePiece里先校验后落子、落子后立刻换手顺序不能反——如果先换手再落子棋子颜色就会落到对方头上。教程源码里通常会把这个模型单独放一个类窗口绘制和逻辑判断共用这份数据。2.2 落子逻辑校验、记录与颜色切换的先后顺序落子这个动作看起来就三行代码但先后顺序错了后面所有功能都会跟着错。正确的落地顺序是先校验坐标合法性再写棋盘数组最后切换执棋方。上面代码里已经体现出来了但还有一个容易漏掉的细节——把已经赢了的棋局继续落子。这个放到第 4 章专门讲这里先记住核心原则校验永远在写值之前。public boolean tryPlace(int row, int col) { // 二次校验窗口层可能已经过滤但逻辑层必须再过滤一次 if (!isValidMove(row, col)) { return false; } board[row][col] currentPlayer; playerHistory.push(new Move(row, col, currentPlayer)); // 落子记录入栈 currentPlayer (currentPlayer BLACK) ? WHITE : BLACK; return true; }这里多引入了一个playerHistory栈属于悔棋功能的“后悔药”预留位。Move对象保存行、列、棋子颜色三个信息悔棋时弹栈就能还原。它的移动逻辑不是board[row][col] 0就完事还要把currentPlayer换回去否则悔棋后会变成同一方连续下两步。2.3 胜负判定四个方向、一条循环、两个计数器五子棋的胜负判定是这份教程里最值得反复推敲的地方。很多初写者会写四段重复代码分别检查横、竖、左斜、右斜。能跑但代码丑且容易在复制粘贴时改错方向。更聪明的写法是用方向数组把四个方向抽象成向量在一个循环里完成。public int checkWin(int row, int col) { // 四个方向的单位向量右、下、右下、左下 int[][] directions {{1, 0}, {0, 1}, {1, 1}, {1, -1}}; int color board[row][col]; if (color EMPTY) { return EMPTY; } for (int[] d : directions) { int count 1; // 向正方向数 count countDirection(row, col, d[0], d[1], color); // 向反方向数 count countDirection(row, col, -d[0], -d[1], color); if (count 5) { return color; } } return EMPTY; } private int countDirection(int row, int col, int dx, int dy, int color) { int count 0; int r row dx; int c col dy; while (r 0 r SIZE c 0 c SIZE board[r][c] color) { count; r dx; c dy; } return count; }checkWin接收最后落子的坐标以这个点为中心向两端延伸数同色棋子。四个方向的向量组合起来就是完整检测矩阵方向dxdy检测的连线水平10左到右垂直01上到下主斜线11左上到右下副斜线1-1左下到右上countDirection里的while条件顺序是先判边界再取数组值这行代码一旦写成board[r][c] color r 0...当坐标已经是 -1 时 Java 会直接抛数组越界异常。教程源码里这个方法的边界判断顺序是新手最容易抄错的地方。3. 从控制台到鼠标窗口事件监听、重绘与悔棋3.1 鼠标坐标换算像素点怎么映射到棋盘格子控制台版的五子棋靠输入行列号但图形界面里玩家点的是像素坐标。如果把鼠标点的x、y直接当成数组下标棋子永远画不到格子里。换算公式不复杂核心是搞清楚偏移量和格子宽度。private int colFromX(int x) { // margin 是棋盘左边留白cellSize 是每个格子的像素宽度 int col (x - margin) / cellSize; if (col 0 || col SIZE) { return -1; // 点到了棋盘外 } return col; } private int rowFromY(int y) { int row (y - margin) / cellSize; if (row 0 || row SIZE) { return -1; } return row; }margin一般取 20 到 30 像素给棋盘外沿留出画坐标线的空间。cellSize等于棋盘可用宽度除以SIZE - 1因为线画在格子边界上不是格子中心。换算公式里最容易翻车的是“减不减 margin”和“除以几”这两个问题——不减 margin第一列格子会偏移除以SIZE而不是SIZE - 1最后一列怎么也点不准。鼠标点击时把e.getX()和e.getY()分别传入这两个方法拿到行列号后再交给逻辑层落子。3.2 重绘机制每次 repaint 都要先清底窗口界面的棋子不是“贴”上去的而是每次重绘时把所有内容重新画一遍。理解了这一点很多“残影”“花屏”问题就迎刃而解。Override protected void paintComponent(Graphics g) { super.paintComponent(g); // 先清空画布这是防残留的关键 Graphics2D g2d (Graphics2D) g.create(); drawBoard(g2d); // 画横纵线 drawPieces(g2d); // 遍历二维数组画棋子 g2d.dispose(); }super.paintComponent(g)这行是必须的不写的话旧的棋子会叠在新画面底下。drawPieces里用双层循环遍历board数组遇到非空值就在对应交叉点画实心圆。Swing 本身默认开启了双缓冲抖动问题不大但如果你看到明显闪屏检查是不是在paint方法里直接做了耗时操作——把胜负判定放在 paint 里是大忌。3.3 悔棋栈撤销一步棋的“后悔药”悔棋功能在教程源码里通常是一个JButton绑定的事件。实现要点是“撤销落子 换回执棋方 重绘画布”三件事缺一不可。private void undoLastMove() { if (playerHistory.isEmpty()) { return; } Move last playerHistory.pop(); board[last.row][last.col] EMPTY; // 清除棋子 currentPlayer last.color; // 换回上一步的执棋方 repaint(); // 触发整体重绘 }playerHistory用ArrayDeque或者Stack都行我习惯用ArrayDeque因为它不是线程同步的单线程游戏里性能更好。Move里存color而不是存“当前执棋方”是因为悔棋后要恢复的是“下这步棋的人”直接存颜色最直观。如果只清棋子不换手就会出现“黑方悔棋后还是黑方继续下”的 bug。3.4 教程运行导览文件清单与最小可跑步骤拿到源码包后不要急着在 IDE 里点运行先看到代码结构再动手。常见教程的源码结构大致如下文件职责GobangMain.java启动入口创建窗口并设置关闭策略GobangBoard.java棋盘数据、落子逻辑、胜负判定GobangPanel.java绘制棋盘和棋子、监听鼠标事件Move.java悔棋记录的数据封装跑通这套源码的最小步骤就两步先把四个.java文件放进同一个包目录然后在命令行执行javac -encoding UTF-8 com/gobang/demo/*.java java com.gobang.demo.GobangMain-encoding UTF-8参数要加上否则含中文注释的源码在 Windows 下默认用 GBK 编码编译会直接报“未结束的字符串字面量”。在 IDE 里导入则更省事新建工程后把源码目录拖进src根目录等索引完成直接运行GobangMain。首次运行如果窗口弹不出来八成是main方法里少了setVisible(true)这是 Swing 新手最容易漏的一行。4. 五子棋游戏实战避坑我踩过的四个高频问题4.1 数组越界异常胜负判定扫到了 -1现象在棋盘左上角或右上角落子时程序突然抛出ArrayIndexOutOfBoundsException但中间区域怎么下都正常。原因胜负判定从落子点向两端数棋子时方向向量会带着坐标持续相加。落子在边界附近时row dx或col dy会变成 -1 或 15而代码里先取了board[r][c]的值再判断边界越界就炸了。解决把边界判断放在数组访问之前也就是while (r 0 r SIZE c 0 c SIZE board[r][c] color)。判断顺序从左到右执行只要越界就直接短路根本不会访问到数组。这个坑在 2.3 的代码里特意提到了源码答疑区里出现频率最高的就是这个问题。4.2 同一格棋子被覆盖换手逻辑和落子校验打架现象同一位置能反复落子黑子落上去变白子白子再点一下又变黑子棋盘数据怎么看都是乱的。原因落子时没有校验格子是否为空直接把board[row][col] currentPlayer覆盖写入了。另一种情况是校验写了但放在换手之后——先切换了currentPlayer再写入棋子颜色永远是下一次的颜色。解决严格按“先校验 → 再写入 → 最后换手”的顺序执行。校验方法里把“格子不为空”作为硬条件返回false后窗口层直接忽略这次点击。我在调试时习惯在落子方法入口加一行System.out.println(落子: ( row , col ) 颜色: currentPlayer)日志一打出来问题马上定位。4.3 重绘残留与窗口花屏缺少清底现象棋子越下越多时窗口里开始出现棋盘线变粗、棋子边缘有残影移动窗口后甚至能看到之前棋子的“鬼影”。原因paintComponent里直接开始画棋盘没有先清理画布。Swing 并不会自动把上一帧的内容抹掉不清底的话旧画面会叠加在新画面上。另一个次要原因是自定义绘制后没有释放Graphics2D资源。解决paintComponent第一行强制写super.paintComponent(g)让父类先把背景清干净。创建Graphics2D后用完调用dispose()释放。这里有个小细节如果你用了g.create()获取副本释放时释放副本不要释放原对象否则后续绘制会报异常。4.4 判赢后还能继续落子缺少游戏状态锁现象黑方已经连成五子提示窗口弹出来了但关掉弹窗后棋盘还能继续点甚至白方还能接着下。原因checkWin返回胜利颜色后代码只弹了个对话框没有把游戏状态改成“已结束”。窗口的鼠标监听器依然会把点击事件交给落子逻辑处理。解决在窗口类加一个boolean gameOver标志每次落子前先判断gameOver是否为true是则直接返回。胜利后把这个标志置为true并禁用棋盘相关的交互组件。悔棋和重开对局时必须重新置为false否则会出现“新游戏无法落子”的连锁问题。5. 从示例到自己的小工具验证与扩展的两个关键习惯五子棋源码包跑通只是第一步真正拉开差距的是“改参数再回归”的能力。这份教程的棋盘尺寸写成了SIZE 15常量连珠判定的阈值count 5直接硬编码在checkWin里。想从示例升级成自己的棋盘工具第一步是把这两个魔数抽成可配置参数public class GobangConfig { public static final int BOARD_SIZE 15; // 棋盘路数可改 19 public static final int WIN_COUNT 5; // 连珠数可改 6 玩六子棋 }这不止是“改常量”这么简单它逼你把所有依赖 15 和 5 的代码全部扫一遍。你会发现还有些地方没写对绘制面板的宽度计算用了BOARD_SIZE - 1坐标换算用到了BOARD_SIZE胜负判定用到了WIN_COUNT。把这些地方全部替换成配置项后改一行配置就能从五子棋变六子棋才说明你真的读懂了这份源码。第二个习惯是写一个不依赖窗口的纯逻辑测试。GUI 测试麻烦但棋盘逻辑完全可以脱离窗口单独跑public class LogicTest { public static void main(String[] args) { GobangBoard board new GobangBoard(); board.placePiece(7, 7); // 黑 board.placePiece(0, 0); // 白 board.placePiece(7, 8); // 黑 board.placePiece(1, 1); // 白 board.placePiece(7, 9); // 黑 board.placePiece(2, 2); // 白 board.placePiece(7, 10); // 黑 board.placePiece(3, 3); // 白 int winner board.checkWin(7, 10); System.out.println(winner GobangBoard.BLACK ? 通过 : 失败); } }这个测试把黑子连成横线并在斜线方向上用白子干扰验证checkWin不会误判斜线。从那以后我每次拿到类似的游戏源码包第一件事都是先写一个四五行的纯逻辑测试把边界位置、反方向连线、双色交叉这三种场景各跑一遍跑通了再碰 GUI。这个习惯帮我拦下了不知道多少“界面看着正常、逻辑却有黑匣子”的隐藏 bug。希望帮到你。本文还有配套的精品资源点击获取