ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Java人机五子棋课设:评估函数与Alpha-Beta剪枝算法实战

Java人机五子棋课设:评估函数与Alpha-Beta剪枝算法实战 简介这一Java课程设计完整项目是一份面向高校Java学习者的人机对战五子棋游戏源码重点演示如何基于Swing构建棋盘界面、管理棋子状态并通过Minimax与Alpha-Beta剪枝算法实现电脑落子决策。项目还包含合法落子判断、五子连线检测、悔棋与重开功能以及异常处理结构完整、功能齐全非常适合课程设计、综合实验及答辩参考。压缩包共19个文件以5个核心Java源文件为主另含可直接运行的jar包、8张界面运行截图、docx版设计报告和README说明整个包体仅295KB轻量易读。目前已有284人学习从其源码与报告可快速了解Java游戏开发的数据结构设计、GUI事件响应和简单AI博弈实现便于在此基础上二次扩展或改造。1. 人机五子棋课程设计真正值钱的是那套落子算法Java 课程设计选人机五子棋的同学很多但九成提交上去的作业界面画得挺漂亮AI 却只会“堵着玩家下”要么就是乱下。答辩时老师一句“你讲讲这个 AI 是怎么决策的”直接就卡住了。这门课设真正的门槛不在 Swing 窗口和鼠标事件而在那个让电脑“看起来有点聪明”的评估与搜索逻辑——它决定你是拿及格分还是拿优秀分。这篇笔记不聊虚的直接按课程设计的交付路径走先把棋盘与胜负判定做扎实再写 AI 核心评分表 极小极大搜索 Alpha-Beta 剪枝最后用 Swing 把两者串起来。每一段都有可直接使用的源码思路和参数说明新手能一步步跟下来手上有项目的人也能直接对照排查。我会把评估函数怎么调、搜索深度设多少、为什么界面会卡死这些晋级时会踩的坑一并说清楚方便你直接落到代码里。2. 棋盘、落子与胜负判定先把数据模型做对界面才有东西可画五子棋的工程结构比算法简单但很多人一上来就写界面结果逻辑全黏在按钮监听器里后面加 AI 时改得想哭。正确的顺序是先把棋盘模型和规则判定做成独立的一组类再让界面去调用它。这样 AI、界面、悔棋各管各的答辩时老师让你“加个功能”你也不慌。2.1 棋盘模型15×15 二维数组与落子记录最省事棋盘我一般用int[15][15]0 表示空、1 表示黑子、2 表示白子。这个选择很朴素但胜在直接——判断胜负时遍历数组即可画界面时按数组值映射棋子颜色即可AI 搜索时拷贝棋盘也容易。public class Board { 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 final int[][] grid new int[SIZE][SIZE]; private final Listint[] moveHistory new ArrayList(); // 记录每一步落子用于悔棋 public boolean isLegalMove(int row, int col) { return row 0 row SIZE col 0 col SIZE grid[row][col] EMPTY; } public boolean placePiece(int row, int col, int player) { if (!isLegalMove(row, col)) return false; grid[row][col] player; moveHistory.add(new int[]{row, col}); return true; } public void undoMove() { if (moveHistory.isEmpty()) return; int[] last moveHistory.remove(moveHistory.size() - 1); grid[last[0]][last[1]] EMPTY; } }逻辑说明placePiece先做合法性校验再落子避免客户端绕过界面直接调方法时产生脏数据undoMove依赖moveHistory出栈回退这是后面做悔棋功能的地基。类里面不引用任何 Swing 组件保证纯逻辑部分可以单独跑单元测试。参数说明棋盘默认 15 路是国际五子棋标准规格想做成 13 路或 19 路直接改SIZE即可。但注意搜索算法的时间复杂度随棋盘边长增长很快课程设计用 15 路足够了。2.2 五子连珠判定四方向扫描不必递归也不必状态机胜负判定是五子棋里最容易写错的部分。常见错误是只检查横竖两个方向或者用递归去做连通块计数既难懂又有 BUG。最稳的方式是固定从当前落点出发沿横、竖、左斜、右斜四个方向各自统计同色连续棋子数。public boolean checkWin(int row, int col) { int player grid[row][col]; if (player EMPTY) return false; int[][] directions {{0, 1}, {1, 0}, {1, 1}, {1, -1}}; for (int[] dir : directions) { int count 1; // 正方向延伸 count countInDirection(row, col, dir[0], dir[1], player); // 反方向延伸 count countInDirection(row, col, -dir[0], -dir[1], player); if (count 5) return true; } return false; } private int countInDirection(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 Board.SIZE c 0 c Board.SIZE grid[r][c] player) { count; r dRow; c dCol; } return count; }逻辑说明directions数组只定义了四个基础方向每个方向的正反两边靠countInDirection分别统计。比如横向判定时先往右数同色棋子再往左数加起来是当前方向上的连续长度。这个方法每次只检查当前落点周围不需要扫描全盘时间开销极小。参数说明如果后续要支持“五子以上也算赢”比如长连禁手规则把count 5改成count 5 count 5不合适——更规范的做法是单独判断count 5超过 5 个时视为无法构成胜利。课程设计不需要做禁手规则保持count 5即可答辩时能说出禁手概念反而是加分项。3. AI 落子算法先学会“打分”再学会“想两步以后的事”人机五子棋最核心的部分就是 AI。课程设计里最常见的 AI 方案有两种一种是基于棋型打分表做贪心选点另一种是极小极大搜索加 Alpha-Beta 剪枝。前者实现简单但显得“短视”后者棋力更高但代码量和理解成本都明显上了一个台阶。这里我建议直接做第二种——被打分表吸引的人答辩时往往会被老师追问“如果对方下一步要成五你这步还去堵吗”这类问题而搜索算法天然能覆盖这种远期威胁。分数不需要做到 Gommoku 专业级别课程设计能赢过不熟悉五子棋的普通玩家就足够了。3.1 评估函数别只数棋子个数要数“活二、活三、冲四”评估函数是整个 AI 的“价值观”。它的任务很简单给定一个棋盘状态和某个角色算出这个局面有多好。如果只用“连续棋子数量”来打分AI 会变得很笨——比如它看不出“两头都开放的活三”和“一头被堵死的眠三”是完全不同的两种威胁。我的做法是沿着每个方向扫描棋子序列把连续同色棋子的长度和两端是否开放编码成一种棋型再按棋型表打分。public int evaluate(int[][] board, int aiRole) { int score 0; // 对四个方向分别扫描累加 AI 得分扣除玩家得分 score evaluateDirection(board, aiRole, 0, 1); score evaluateDirection(board, aiRole, 1, 0); score evaluateDirection(board, aiRole, 1, 1); score evaluateDirection(board, aiRole, 1, -1); return score; } private int evaluateDirection(int[][] board, int aiRole, int dRow, int dCol) { int total 0; for (int r 0; r Board.SIZE; r) { for (int c 0; c Board.SIZE; c) { if (board[r][c] 0) continue; int player board[r][c]; // 从当前点开始沿方向数连续同色棋子 int count 1; int nr r dRow, nc c dCol; while (nr 0 nr Board.SIZE nc 0 nc Board.SIZE board[nr][nc] player) { count; nr dRow; nc dCol; } // 检查两端是否开放 boolean openStart (r - dRow 0 r - dRow Board.SIZE c - dCol 0 c - dCol Board.SIZE board[r - dRow][c - dCol] 0); boolean openEnd (nr 0 nr Board.SIZE nc 0 nc Board.SIZE board[nr][nc] 0); total shapeScore(count, openStart, openEnd, player aiRole); } } return total; } private int shapeScore(int count, boolean openStart, boolean openEnd, boolean isAI) { int base 0; if (count 5) base 100000; else if (count 4) { base (openStart openEnd) ? 50000 : (openStart || openEnd) ? 10000 : 0; } else if (count 3) { base (openStart openEnd) ? 5000 : (openStart || openEnd) ? 1000 : 0; } else if (count 2) { base (openStart openEnd) ? 500 : (openStart || openEnd) ? 50 : 0; } else { base (openStart || openEnd) ? 10 : 0; } return isAI ? base : -base; }逻辑说明连续同色棋子被看作一个整体序列两端若都是空格则算“开放”棋型。活四两头开放的四连给 50000 分死四只给 10000 分活三 5000 分这样 AI 才能看出“活三比死四更要紧”这种不那么直观的棋理。AI 的棋型得正分玩家的棋型得负分天然就包含防守意图——这也是为什么这个函数之后配合极小极大搜索不用再单独写“堵截”逻辑因为搜索树里玩家的好棋会让 AI 的分数变小AI 会主动避开。参数说明100000、50000这些分数不是拍脑袋定的它们之间必须满足“一个活三的威胁大于两个死四之和”这类关系否则搜索会走偏。初学者可以先按这个量级用调优时只需要保证成五 活四 冲四 活三 眠三 活二的序关系不变。另外evaluateDirection里对每个棋子都重新向后扫描一段存在重复计算但对于 15×15 的棋盘点数和课程设防的搜索深度来说性能完全顶得住不用过早优化。3.2 极小极大搜索与 Alpha-Beta 剪枝跑到第 6 层的秘诀有了评估函数下一步就是让 AI 往前多想几步。它的核心逻辑是接下来是 AI 走AI 会选分数最高的点但 AI 走完之后是玩家走玩家会选分数最低的点。这样 AI 相当于在“与自己博弈”——这就是极小极大搜索。Alpha-Beta 剪枝是在这个搜索过程中剪掉那些“必然不会被选中的分支”让搜索深度和速度同时得到保障。没有剪枝时15×15 棋盘每个节点的分支数大约几十个搜 4 层就是百万级别加上剪枝和候选点筛选搜到第 4 层也只需要遍历几千个节点。private int minimax(int[][] board, int depth, int alpha, int beta, int currentRole, int aiRole) { // 判断终止条件 if (depth 0) { return evaluate(board, aiRole); } Listint[] candidates generateCandidates(board, 8); // 只考虑已有棋子周围两步内的点 if (candidates.isEmpty()) return evaluate(board, aiRole); if (currentRole aiRole) { int maxScore Integer.MIN_VALUE; for (int[] pos : candidates) { board[pos[0]][pos[1]] currentRole; // 如果这一步直接赢了给极大的分数提前返回 int score (checkWinAfterMove(board, pos[0], pos[1])) ? 1000000 depth : minimax(board, depth - 1, alpha, beta, opponent(currentRole), aiRole); board[pos[0]][pos[1]] 0; maxScore Math.max(maxScore, score); alpha Math.max(alpha, maxScore); if (beta alpha) break; // Alpha 剪枝当前分支已经不可能更好了 } return maxScore; } else { int minScore Integer.MAX_VALUE; for (int[] pos : candidates) { board[pos[0]][pos[1]] currentRole; int score (checkWinAfterMove(board, pos[0], pos[1])) ? -1000000 - depth : minimax(board, depth - 1, alpha, beta, opponent(currentRole), aiRole); board[pos[0]][pos[1]] 0; minScore Math.min(minScore, score); beta Math.min(beta, minScore); if (beta alpha) break; // Beta 剪枝 } return minScore; } }逻辑说明搜索过程中直接在二维数组上落子和回退没有复制棋盘——这是性能关键。checkWinAfterMove只检查当前落点是否成五避免了全盘扫描。胜利时的分数加上depth正值表示越快赢越好能让 AI 在面对“多种赢法”时选择最短路径这个细节对棋力影响非常大。参数说明generateCandidates(board, 8)表示只收集已有棋子周围 8 步以内的空位作为候选点。这个范围圈住了几乎所有有意义的落点同时把搜索分支数从近百个降到二十个以内。如果你发现 AI 偶尔会下出脱离战场的“废棋”多半是候选点范围过大或评估函数对空点的分数干扰太多如果你发现 AI 完全不下到边角去防守多半是候选点范围过小把边角攻防给漏了。最终 AI 落子的入口方法如下public int[] getBestMove(Board board) { int[][] snapshot board.getGrid(); Listint[] candidates generateCandidates(snapshot, 8); int bestScore Integer.MIN_VALUE; int[] bestMove candidates.get(0); for (int[] pos : candidates) { snapshot[pos[0]][pos[1]] aiRole; int score minimax(snapshot, depth, Integer.MIN_VALUE, Integer.MAX_VALUE, opponent(aiRole), aiRole); snapshot[pos[0]][pos[1]] 0; if (score bestScore) { bestScore score; bestMove pos; } } return bestMove; }3.3 搜索深度怎么定4 层是基准5 层要控制候选点规模深度太浅 AI 看不见对方的活三深度太深会有明显的等待时间课程设计演示时卡顿非常减分。搜索深度 参考耗时代普通笔记本候选点约20个 棋力表现 2层 50ms 只会看眼前一步棋 3层 100ms 左右 能挡住直接成五但看不见两步之后的威胁 4层 500ms ~ 1s 能主动做棋能挡住多数冲四和活三 5层 2s ~ 5s 棋力接近认真下的业余玩家但偶发超时 6层 多数情况超过10s 课程设计不建议如果你是演示前两天才开始调 AI就直接用 4 层。这个深度配合上面的棋型评估表足以让 AI 做到“自己会做活三、玩家成四时必堵”的效果这在答辩展示中已经很有说服力了。想上 5 层也不是不行但必须把候选点从“周围 8 步”收敛到“周围 3 步”再叠加“若对手上一步有威胁点则优先搜索威胁点附近”的策略。这种做法会让 AI 的棋风变得保守——它倾向于先去处理玩家的进攻然后再经营自己的棋形——但作为课设的“挑战项”写在文档里老师是很吃这一套的。4. 界面与交互Swing 里最容易翻车的不是布局是主线程卡死很多做课程设计的同学在写完 AI 后卡在界面上窗口能弹出来棋盘画得也不错但一点棋盘整个窗口就白屏转圈几秒后恢复。这不是电脑太慢是你在 Swing 的事件分发线程EDT里执行了耗时的 AI 搜索。4.1 棋盘绘制用 JPanel 重写 paintComponent别用按钮摆阵有一种常见做法是放 225 个 JButton 组成棋盘每次落子改按钮背景。这样做不是不行但按钮自带焦点框、边框和点击特效做出来的棋盘中线全是格子缝视觉效果很差。标准做法是用一个自绘 JPanelpublic class BoardPanel extends JPanel { private final Board board; private final int cellSize 36; private final int margin 30; public BoardPanel(Board board) { this.board board; setPreferredSize(new Dimension(margin * 2 cellSize * (Board.SIZE - 1), margin * 2 cellSize * (Board.SIZE - 1))); setBackground(new Color(210, 180, 140)); // 木纹底色 } Override protected void paintComponent(Graphics g) { super.paintComponent(g); // 画网格线 g.setColor(Color.BLACK); for (int i 0; i Board.SIZE; i) { g.drawLine(margin, margin i * cellSize, margin (Board.SIZE - 1) * cellSize, margin i * cellSize); g.drawLine(margin i * cellSize, margin, margin i * cellSize, margin (Board.SIZE - 1) * cellSize); } // 画棋子 for (int r 0; r Board.SIZE; r) { for (int c 0; c Board.SIZE; c) { int stone board.getGrid()[r][c]; if (stone Board.EMPTY) continue; int x margin c * cellSize - cellSize / 2; int y margin r * cellSize - cellSize / 2; g.setColor(stone Board.BLACK ? Color.BLACK : Color.WHITE); g.fillOval(x 2, y 2, cellSize - 4, cellSize - 4); if (stone Board.WHITE) { g.setColor(Color.GRAY); g.drawOval(x 2, y 2, cellSize - 4, cellSize - 4); } } } } }逻辑说明整个棋盘画在一个JPanel上棋盘坐标到像素坐标的换算集中在两行代码里margin c * cellSize。paintComponent里只做绘图不做任何业务计算这是 Swing 的硬性要求——这个方法会在窗口拖动、最小化恢复时被反复调用如果里面塞了耗时的搜索代码界面会肉眼可见的卡顿。参数说明cellSize 36和margin 30是画出来的棋盘大小约 560 像素适合 1080p 屏幕全屏展示如果你用笔记本接投影仪答辩建议把 cellSize 改成 44否则后排看不清棋子。棋子半径比格子一半略小留出空隙会让棋盘显得宽松自然。4.2 点击落子与 AI 回手用 SwingWorker 把搜索丢进后台线程这是整个界面上最重要的一段代码没有之一。直接在鼠标监听器里调用getBestMove事件分发线程会被搜索阻塞表现为窗口失去响应。解决方式是用SwingWorker把搜索放到后台线程搜索完成后回到 EDT 更新界面。boardPanel.addMouseListener(new MouseAdapter() { Override public void mouseClicked(MouseEvent e) { if (gameOver || isAiThinking) return; int col Math.round((e.getX() - margin) / (float) cellSize); int row Math.round((e.getY() - margin) / (float) cellSize); if (!board.isLegalMove(row, col)) return; board.placePiece(row, col, Board.BLACK); boardPanel.repaint(); if (board.checkWin(row, col)) { gameOver true; statusLabel.setText(黑棋获胜); return; } isAiThinking true; statusLabel.setText(AI 思考中...); SwingWorkerint[], Void worker new SwingWorkerint[], Void() { Override protected int[] doInBackground() { return ai.getBestMove(board); } Override protected void done() { try { int[] move get(); if (move null) return; board.placePiece(move[0], move[1], Board.WHITE); boardPanel.repaint(); if (board.checkWin(move[0], move[1])) { gameOver true; statusLabel.setText(白棋获胜); } else { statusLabel.setText(轮到你落子); } } catch (Exception ex) { statusLabel.setText(AI 计算出错); } finally { isAiThinking false; } } }; worker.execute(); } });逻辑说明doInBackground里调用 AI 搜索done里通过get()取出结果并更新界面。这样无论搜索耗时多长窗口都能正常响应重绘事件鼠标还能移动窗口、调整大小。isAiThinking标志位防止用户在 AI 思考时连续点击棋盘产生并发冲突。参数说明坐标换算用Math.round而不是直接取整是为了让点击位置偏向格子中心时能正确落到相邻格子上。如果你发现点击偶发落错位可以改成Math.floor配合手动加 0.5 偏移两种方式都可以但务必统一别混用。4.3 悔棋与“再来一局”逻辑层一步到位悔棋在课程设计里属于“看起来不起眼但很能加分”的功能。实现上只需要在点击“悔棋”按钮时撤销两步——玩家一步、AI 一步——再重绘棋盘。JButton undoButton new JButton(悔棋); undoButton.addActionListener(e - { if (gameOver) return; if (board.getMoveCount() 2) { statusLabel.setText(还没有可悔的棋); return; } board.undoMove(); // 撤销 AI 的落子 board.undoMove(); // 撤销玩家的落子 boardPanel.repaint(); statusLabel.setText(已悔棋轮到你落子); });逻辑说明先撤销 AI 再撤销玩家。如果你的 AI 是先手方则这个顺序要反过来取决于aiRole是BLACK还是WHITE。这个逻辑成立的前提是moveHistory始终是“玩家、AI、玩家、AI”这样严格交替的如果做了“玩家连续下两手”的功能这个链路就会错位。参数说明getMoveCount可以直接取moveHistory.size()。悔棋之后AI 不需要重新计算任何数据——因为下一次getBestMove会基于当前棋盘重新搜索天然支持任意多步悔棋。这种“无状态 AI”的设计在答辩时同样值得说。5. 答辩展示与调试让 AI 表现受控的一套打法课程设计写到这一步代码基本齐了但很多人在答辩演示时栽在“AI 发挥不稳定”上——头两盘下得不错第三盘突然开始乱下。问题通常出在两个地方评估函数分数比例不对或者搜索深度在复杂局面下超时被界面卡断。我调试这种项目时有一套固定的打法既能让 AI 在演示时表现稳定也方便你在老师面前展示“工程能力”。首先把 AI 的搜索深度做成一个可在界面上调整的选项。放一个JComboBox让用户选「简单 / 普通 / 困难」三档分别对应 2 层、4 层、5 层深度。答辩时先用简单档演示一盘再用困难档演示一盘老师能直观感受到算法的差异这是最加分的展示方式。其次准备一个“AI 先手”的对局入口。默认人机对局里让玩家执黑先行AI 执白后手。但在实际调试中AI 先手更容易走出自己擅长的进攻套路。你可以在菜单里加一个“切换先手方”的功能调试时让 AI 执黑能很快检验它的做棋能力。另外给评估函数加一个简易的调试输出。在evaluate()方法返回值之外额外打印每个棋型对总分贡献的拆分比如“活三 5000冲四 10000”。这个输出不需要留在正式版里但演示前你可以开着它跑几盘确认 AI 在关键局面确实优先处理了冲四而不是去补一个无关紧要的活二。最后一个经验是不要尝试让 AI 在演示机上跑到 6 层。5 层搜索在候选点少的中盘可能只要 2 秒但在收官阶段棋盘仍然很空时5 层直接飙到 10 秒以上。我在一次课程设计答辩前夜吃过这个亏——AI 在自己的关键局里思考了 20 秒没落子指导老师直接问“是不是死循环了”。后来我加了一个保护逻辑当搜索时间超过 3 秒时自动降一层深度重搜优先保证落子速度。用一句很朴素的话说课设的 AI 棋力可以不是最强的但它必须永远在合理时间内落子这比任何算法优化都更能避免答辩现场翻车。从数据结构到评估函数再到搜索剪枝和 Swing 线程模型人机五子棋这个题目其实把 Java 课程的核心知识点串得非常完整。按这个顺序把工程做出来你已经不只是“写了个游戏”而是独立完成了一个含规则建模、启发式评估和对抗搜索的小型系统。答辩时把这套思路讲清楚比堆叠界面特效有用得多。希望帮到你。本文还有配套的精品资源点击获取
返回列表