ARTICLE DETAIL

资讯详情

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

安卓中国象棋源码拆解:从规则引擎到Alpha-Beta剪枝

安卓中国象棋源码拆解:从规则引擎到Alpha-Beta剪枝 简介基于安卓平台的中国象棋对战程序源码包适合Android学习者、毕业设计或课程设计使用涵盖棋盘绘制、棋子走法规则、人机/双人对战、音效与菜单等常见模块。源码经过本地编译验证配置好Android开发环境即可运行。资源共109个文件核心代码为10个Java文件与20个class文件配合66个png界面与棋子素材、3个xml布局配置、3个mp3音效另含可直接安装的apk、dex及工程配置文件整体仅4.43MB轻量易用。已有207人学习下载可作为毕业设计参考或Android游戏开发入门练习。借助清晰的项目结构与完整代码可快速理解Canvas绘图、事件处理、游戏逻辑与资源管理等要点按需修改界面或扩展AI功能提升开发实践能力。1. 拿到「基于安卓Android的中国象棋源码.zip」第一件事看什么收到「基于安卓Android的中国象棋源码.zip」这类压缩包别急着解压看界面。以我做棋类 App 的经验这份源码里最容易出问题的不是 Activity 布局而是棋盘坐标、走法合法性校验和对弈循环这三块理顺它们才谈得上在 Android Studio 里跑通并看到棋子在屏幕上走动。中国象棋规则不复杂但将帅照面、蹩马腿、炮架和过河兵这类特例很容易写出“界面好看但走两步就错”的代码。这篇文章按坐标建模、走法生成、自定义 View、AI 搜索到工程排错的顺序把一份棋类源码应该怎么拆、怎么改、怎么验证讲清楚。适合做课程设计、面试作品或棋类 App 的安卓开发者对有几年经验但没写过棋类规则引擎的人也值得过一遍边界条件。2. 棋盘坐标与走法生成Android 中国象棋引擎的最小骨架棋类项目最忌讳一上来写 UI。中国象棋的对弈核心只有两件事棋盘上有哪些子能走、走一步之后局面是否合法。这两件事都用纯 Java 写既不依赖 Android Framework也能在 JUnit 里直接测试后续再做 AI 搜索时全部复用这套接口。2.1 用 [10][9] 二维数组建模红方棋子用正数表示中国象棋棋盘 10 行 9 列最常见的落地方式是int[10][9]。行下标 0 表示红方底线行下标 9 表示黑方底线列下标 0 到 8 对应从左到右。可以硬编码一个二维数组也可以在Board类里用一维数组保存坐标按row * 9 col取下标。对安卓项目来说二维数组更直观调试和触摸坐标换算都少一层换算。棋子用带符号的 int 表示正数是红方负数是黑方0 表示空位。这样判断敌我时只需要比较符号public class Board { public static final int EMPTY 0; public static final int RED_KING 1; // 帅 public static final int RED_GUARD 2; // 仕 public static final int RED_MINISTER 3; // 相 public static final int RED_KNIGHT 4; // 马 public static final int RED_ROOK 5; // 车 public static final int RED_CANNON 6; // 炮 public static final int RED_PAWN 7; // 兵 // 黑方用负值例如 BLACK_KING -1, BLACK_ROOK -5 private final int[][] grid new int[10][9]; public boolean isRed(int piece) { return piece 0; } }/正数红、负数黑是很多象棋源码采用的约定优点是grid[r][c] ! 0即有人piece * piece无所谓Math.abs(piece)取兵种。缺点是直接打印棋盘时负数可读性差所以调试时加一个把数字转成“车马炮兵仕相帅”字符的toChar方法很划算。走法用一个简单结构体表示。建议不要用八个参数而是建一个Move内部类字段为fromRow、fromCol、toRow、toCol和captured其中captured在悔棋和吃子判断里要用。字段类型含义fromRow / fromColint起点坐标toRow / toColint目标坐标capturedint被吃掉的棋子空位为 EMPTY很多源码随手用四个 int 传参改两版之后就会因为“先 to 后 from”的顺序问题出错。把这一切封装进 MoveAI 搜索的 make / unmake 也会好写很多。2.2 走法生成马脚、相心、炮架和九宫一次说清走法生成是引擎核心AI、将军检测、吃子提示全部依赖它。常见做法是写一个generateMoves(int player)遍历棋盘上属于当前玩家的每个棋子再按兵种生成候选走法。以车和炮为例public ListMove generateMoves(int player) { ListMove moves new ArrayList(); for (int r 0; r 10; r) { for (int c 0; c 9; c) { int piece grid[r][c]; if (piece EMPTY || isRed(piece) ! (player 0)) { continue; } switch (Math.abs(piece)) { case RED_ROOK: generateRookMoves(r, c, moves); break; case RED_CANNON: generateCannonMoves(r, c, moves); break; // 其余兵种各自生成 } } } return moves; }generateRookMoves是滑子标准写法沿上下左右四个方向走遇到空位加入走法遇到第一个敌方子加入吃子走法后停止遇到己方子直接停止。炮的吃子需要“隔一个子”所以要分两步写第一步和车一样找空位不吃子第二步数炮架第一颗子不算跨过它之后遇到的第一个子如果是敌方则加入吃子走法。马和相要处理“蹩脚”和“塞相心”。马可用方向数组写出八种日字跳法对应同样长度的“蹩脚位置”数组private static final int[][] KNIGHT_DELTA { {2, 1}, {2, -1}, {-2, 1}, {-2, -1}, {1, 2}, {1, -2}, {-1, 2}, {-1, -2} }; private static final int[][] KNIGHT_LEG { {1, 0}, {1, 0}, {-1, 0}, {-1, 0}, {0, 1}, {0, 1}, {0, -1}, {0, -1} };起点在(r, c)日字落在(r KNIGHT_DELTA[i][0], c KNIGHT_DELTA[i][1])蹩脚位置就是grid[r KNIGHT_LEG[i][0]][c KNIGHT_LEG[i][1]]非空则这条走法不成立。相同理但它还限制在己方半场且落点不能越过河。将和仕限制在九宫兵的规则最简单但要区分过河前后。这个阶段不要急着一口气写完所有规则后调试而是先做一件事把每兵种的走法生成函数单独打日志摆几个固定局面人工检查。等走法生成对了后面的将军检测和 AI 才有意义。2.3 把“能否走到目标格”封装成对外接口UI 触摸落子时不需要每次都生成全部走法。更高效的做法是canMove(fromRow, fromCol, toRow, toCol)找到起点棋子遍历该兵种在起点生成的候选走法看终点是否在其中。多数源码直接复制一份走法生成的判断逻辑到点击事件里导致规则改一处两个地方不同步。正确做法是让canMove调generateMoves(player)哪怕多几个对象分配。安卓上每次移动生成几十个 Move 对象不会造成可感知的性能问题换来的是规则引擎只有一个入口后续加“禁着列表”“悔棋校验”都容易。如果后面做残局题库这个接口还能直接复用。3. 用自定义 View 画棋盘、响应触摸落子棋类 UI 第一版最常见误区是每个棋子放一个 ImageView这样棋盘缩放、吃子动画、悔棋都很难处理。正确做法是整块棋盘用一个自定义 View所有格子线、文字、选中高亮都写在onDraw里触摸事件统一换算成行列坐标。3.1 棋盘绘制一次 onDraw 把网格和棋子都画出来ChessBoardView继承View核心是计算cellSize。棋盘边缘留出 padding10 行 9 列因此Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int size Math.min( MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.getSize(heightMeasureSpec)); setMeasuredDimension(size, size); }宽高等比避免在分辨率不同的模拟器上变形。绘制分四步先画横线再画竖线然后画九宫斜线和炮兵标记最后画棋子。竖线要注意河界红方视角下第 4 行和第 5 行之间不画竖线Override protected void onDraw(Canvas canvas) { float startX padding; float startY padding; float cell cellSize; for (int row 0; row 9; row) { canvas.drawLine(startX, startY row * cell, startX 8 * cell, startY row * cell, linePaint); } for (int col 0; col 8; col) { if (col 0 || col 8) { // 左右边线完整画 canvas.drawLine(startX col * cell, startY, startX col * cell, startY 9 * cell, linePaint); } else { // 中间竖线在河界处断开 canvas.drawLine(startX col * cell, startY, startX col * cell, startY 4 * cell, linePaint); canvas.drawLine(startX col * cell, startY 5 * cell, startX col * cell, startY 9 * cell, linePaint); } } drawPieces(canvas); }drawPieces遍历Board的网格用(col * cell padding, row * cell padding)求每个棋子圆心再根据棋子正负号决定文字。棋子不一定要贴图Paint画一个黄色圆形再canvas.drawText(车, x, y, textPaint)一样能出好效果而且安装包更小。用位图的话记得在onSizeChanged里按cellSize预生成棋子 Bitmap不要在onDraw里每个循环都BitmapFactory.decode否则帧率会很难看。3.2 触摸事件把屏幕坐标换算到棋盘坐标onTouchEvent里拿到event.getX()和event.getY()后先减去 padding再除以cellSize取整得到行列。注意这里要做边界判断和整数保护如果cellSize是 0除零直接崩。正确顺序是“先判断点击在棋盘内再换算坐标”。点击处理逻辑一般分两段。第一段如果没有选中棋子就判断点击格是否有当前玩家棋子有则选中并设置高亮坐标第二段如果已有选中棋子就调用board.canMove尝试走子。这一步直接体现规则引擎的重要性View 层完全不判断兵种规则只调用接口。Override public boolean onTouchEvent(MotionEvent event) { if (event.getAction() ! MotionEvent.ACTION_DOWN) { return super.onTouchEvent(event); } int col (int) ((event.getX() - padding) / cellSize); int row (int) ((event.getY() - padding) / cellSize); if (row 0 || row 9 || col 0 || col 8) { return false; } if (selected null) { int piece board.getPiece(row, col); if (piece ! Board.EMPTY board.isRed(piece) redTurn) { selected new int[]{row, col}; invalidate(); } } else { if (board.canMove(selected[0], selected[1], row, col)) { board.move(selected[0], selected[1], row, col); selected null; listener.onMoveFinished(); } else { // 选中另一颗己方棋子而不是直接忽略 selected new int[]{row, col}; } invalidate(); } return true; }一个常见误点是点到空位时把selected清空这样连续走子的手感很差。更自然的做法是点在己方棋子上就重新选中点在空位或敌方子且不能吃则保持原选中状态。有些手里有源码的人改 UI 时只在这里做文章其实规则正确性不是靠触摸层兜底而是靠 Board 层。3.3 走子后的状态流转吃子、回合切换与悔棋board.move负责更新网格也要把被吃掉的子记录到Move.captured。如果源码里没有这一步后续悔棋时被吃子根本回不来。典型实现是在 move 里保存上一次 Move 对象并提供unmakeMove(Move)public void move(Move m) { m.captured grid[m.toRow][m.toCol]; grid[m.toRow][m.toCol] grid[m.fromRow][m.fromCol]; grid[m.fromRow][m.fromCol] EMPTY; }回合切换放在 Activity 或 ViewModel 层比较清晰。棋盘负责“当前局面的数据”线程负责“谁轮到走子”。如果哪份源码把回合状态存在 View 的成员变量里建议挪出来后面做联机或人机对战都会方便很多。4. 将军检测与 Alpha-Beta 搜索让电脑会下棋源码里如果只做了双人对战AI 通常是被问得最多的扩展点。中国象棋 AI 不需要多高深的算法规则引擎 评估函数 Alpha-Beta 剪枝就能达到一个能玩的水准。前提是将军检测必须做对否则搜索会走出送将的棋。4.1 每次走子后检查将军合法性才算完整棋类走法生成通常先生成所有“伪走法”再逐条在临时棋盘上走一步看己方将帅是否被攻击。这样做最稳代码也不用为“将军”“蹩马腿”各自写特判。public boolean isSquareAttacked(int row, int col, int attackerPlayer) { // 用攻击方的走法生成函数判断能否攻击到 (row, col) ListMove attacks generateAttacksFrom(attackerPlayer, row, col); return !attacks.isEmpty(); }更简单的实现是反过来遍历攻击方所有棋子调用canMoveTo(piecePos, row, col)并忽略这个过程中“被将军一方已经不能动”的逻辑。因为只走一步不存在路径被后续拦截的问题只是要小心被攻击的格子本身就是将帅所在时不能用该将帅“自己走一步”的方式去生成攻击否则会死循环。判断将帅照面也走这条路红帅在(r1, c)黑将在(r2, c)中间没有其他棋子时黑将可以“吃”红帅所以isSquareAttacked返回 true。把照面纳入攻击检测后规则引擎里多一条限制就足够了走子后己方将帅所在格不能处于被攻击状态。4.2 一个看得懂的评估函数AI 强度一半在搜索一半在评估。入门评分按兵种价值加和即可单位用“分”而不是“子力数”方便后续调权值兵种基础分值说明车1100车最活就该最贵炮520开局强残局要看位置马460蹩脚多时实际价值低于面板值卒/兵160过河后可以额外加 40仕/相220防御子不参与进攻加分简单评估函数就是遍历棋盘把红方棋子分值和为正、黑方为负求和。再对每颗棋子在关键位置做一个位置加分表比如巡河炮、过河兵、中路车各加几十分。位置表不用写太复杂30 到 50 行的二维数组足够让电脑知道“把车沉底比守在自家底线强”。public int evaluate() { int score 0; for (int r 0; r 10; r) { for (int c 0; c 9; c) { int piece grid[r][c]; if (piece EMPTY) continue; int value baseValue[Math.abs(piece)]; // 红方走底行黑方走顶行位置表要按己方视角旋转 score isRed(piece) ? value : -value; } } return score; }这部分是你能从一份常见源码里改出最多“棋力感觉”的地方。不要把评估函数写进onDraw或者触摸回调里搜索线程每秒钟要调几千次它必须只读grid不能碰任何 View。4.3 Negamax 写成极大极小白话版深度 3 到 4 层就够搜索部分用递归搜索即可。下面是一个容易复现的极大极小实现红色为 max 方黑色为 min 方public int search(int depth, int alpha, int beta, int player) { if (depth 0) return evaluate(); ListMove moves board.generateMoves(player); if (moves.isEmpty()) { return player RED ? -Integer.MAX_VALUE / 2 : Integer.MAX_VALUE / 2; } orderMoves(moves); if (player RED) { int best -Integer.MAX_VALUE; for (Move m : moves) { board.makeMove(m); best Math.max(best, search(depth - 1, alpha, beta, BLACK)); board.unmakeMove(m); alpha Math.max(alpha, best); if (beta alpha) break; } return best; } else { int best Integer.MAX_VALUE; for (Move m : moves) { board.makeMove(m); best Math.min(best, search(depth - 1, alpha, beta, RED)); board.unmakeMove(m); beta Math.min(beta, best); if (beta alpha) break; } return best; } }orderMoves的作用是把吃子走法排到前面被吃掉的价值高、吃子棋子价值低则越靠前。Alpha-Beta 剪枝最依赖搜索顺序开局走法排好时深度 4 只需生成几千到几万个节点顺序乱的话树会膨胀几倍安卓低端机上每步棋要等好几秒。AI 的计算必须放到子线程。安卓上常见写法是new Thread(() - { int move ai.bestMove(depth); handler.post(() - applyMove(move)); }).start()千万不要在主线程里跑 4 层搜索。红黑双方复用同一套 minimax 逻辑谁先谁后由player参数决定选择走法时从根节点的子走法里按红取大、黑取小即可。5. 在 Android Studio 里跑通源码并验证走法没有漏判压缩包源码要变成能跑的 App步骤其实比大多数人想象的固定。按下面流程走能省下大量解压后四处点错的时间。把 zip 解压到不含中文和空格的路径例如D:\projects\ChineseChess。用 Android Studio 的Open选择解压目录等 Gradle Sync 完成。Sync 报错时先看根目录的build.gradle确认仓库地址和 JDK 版本是否匹配。老项目经常卡在compileSdk和依赖仓库缺失上。连接模拟器或真机点击 Run。若是旧工程Manifest 里包名可能和目录不一致改包名不如直接保留原包名运行。跑起来之后先做一件事自己动几手专门尝试送将、将帅照面、蹩马腿和炮隔多子这四类局面。很多源码能“走起来”但校验规则不严只是界面看起来正常。验证规则最可靠的是写 JUnit 单元测试不走 UI直接测 BoardTest public void rookShouldNotJumpOverPiece() { Board board new Board(); board.setPiece(5, 1, Board.RED_ROOK); board.setPiece(5, 4, Board.BLACK_PAWN); // 车从 (5,1) 不能直接吃到 (5,6)因为 (5,4) 挡住 assertFalse(board.canMove(5, 1, 5, 6)); // 但能吃 (5,4) assertTrue(board.canMove(5, 1, 5, 4)); }能测试的规则越多后面加 AI 越安心。AI self-play 也是一个验证方法让红黑 AI 各 1 秒时限互下 20 步如果期间出现吃掉己方将帅、双方将帅同时照面这类异常就说明走法生成或将军检测还有漏洞。把最后一点留给性能安卓棋类源码最容易在动画上过度设计。棋子移动不需要位移动画闪烁式“从 A 格消失、B 格出现”就已足够真正的性能瓶颈在onDraw里创建新对象。把 Paint、Rect、Path 全部提为成员变量循环里只改坐标不重建是这份源码能被撑到高帧率的关键。本文还有配套的精品资源点击获取
返回列表