资讯动态

Android中国象棋源码全拆解:从导入编译到AI搜索实现

发布时间:2026/10/14 23:32:31 来源:尧图企业网站定制
简介android中国象棋源码是一份可直接导入Android工程学习的完整游戏项目面向有Java基础、想通过实际案例掌握自定义View绘图、触摸交互与游戏循环的移动开发者也可作为毕业设计或课程项目的改造蓝本。压缩包共110个文件、4.47MB涵盖66张png图片棋子、棋盘、菜单与帮助界面背景、10个java源码ChessActivity、GameView、GuiZe、WelcomeView等、20个class编译文件、3个mp3音效、2个db数据文件及XML布局配置文件并附带一个可安装调试的apk。源码完整展示了从棋盘格线绘制、棋子按规则走法、触摸选子与落子、胜负状态判断到欢迎、菜单、帮助等界面切换的全部流程若含AI模块还可在GuiZe之外看到评估函数与搜索剪枝的实现思路能直接对照运行效果排查问题。目前已有522人学习下载工程规模不大但覆盖Android游戏开发的各个基本环节适合在Android Studio中边读代码边运行快速建立起从事件分发到界面渲染的完整认知。1. 一套能跑的 Android 中国象棋源码到底能帮你省多少事很多人的第一款安卓练手项目都栽在中国象棋上。不是规则难而是网上下载的 android 中国象棋源码十个里有八个跑不起来要么 Android Studio 打开就红一片要么编译过了闪退要么棋盘画出来是歪的。其实这类源码的套路非常固定——一个 int 二维数组当棋盘、一组走法生成器管规则、一张 Canvas 负责绘制最后再挂一个可选的 AI 搜索。这篇不打算复述某一套代码而是把这类源码的骨架拆开让你拿到手就能跑、能改、能塞进自己的课程设计和面试项目里。适合正在入门 Android 开发、想找一个完整但不过度复杂工程来读的人哪怕你完全没写过游戏逻辑顺着棋盘数组和走法生成这两条线走一遍也能很快建立起整体认识。2. 把源码工程跑起来导入、编译、首屏黑屏排查先别急着读代码目标只有一个让工程在你自己的环境里编译通过、能打开棋局。这一步卡住的人最多但我可以负责任地说九成问题出在环境版本而不是源码本身。2.1 导入工程的三步和一个原则你拿到的包大概率是一个 zip解压后第一件事不是写代码而是找回“能编译”的自信。常见做法是直接用 Android Studio 的 Open 按钮选择解压后带有 settings.gradle 的那一层目录。如果选错进到了内层 app 文件夹Android Studio 会把它当成一个独立工程结果就是整个项目结构乱七八糟连主模块都识别不出来。导入之后遇到 Gradle 版本不一致是最常见的Android Studio 会弹提示说当前工程的 Gradle 版本和你的不匹配。我的习惯是既然源码逻辑才是重点版本号不必迁就它直接把 Gradle 升级到你本地已装的版本或者反过来让 Android Studio 自动下载它要求的版本二选一都行。关键是全程保持网络连接稳定别中途切网络不然 Gradle 缓存会处于一种“半装不装”的状态后面所有报错都像玄学。另一个原则是先改 SDK 再看依赖。最近几代的 Android Studio 升级之后很多老工程还在用很旧的 compileSdk/targetSdk 配置你打开 app/build.gradle 之后把 compileSdk、targetSdk 和 minSdk 统一改成你本机 SDK Manager 里已经下载好的版本再点 Sync。这一步比查依赖冲突更优先因为旧 API 版本往往已经被最新构建工具链标记成废弃sync 失败时满屏红色根本看不出真正问题在哪。# 1. 用模拟器或真机跑之前先清一遍日志缓存 adb logcat -c # 2. 启动应用这里换成你自己的包名和入口 Activity adb shell am start -n com.example.china_chess/.MainActivity # 3. 只过滤崩溃级别日志AS 里那些无关彩色日志全丢掉 adb logcat AndroidRuntime:E *:S参数说明adb logcat -c是清空历史日志避免把上次崩溃的堆栈混进来-n后面跟的是“包名/入口 Activity”这个体现在源码 build.gradle 里的 applicationId最后的过滤器只留 AndroidRuntime 的 Error 级日志闪退原因几秒内就能定位到。如果连adb都提示找不到设备先检查 USB 调试授权弹窗有没有点允许模拟器则确认已经启动完成再连。2.2 看懂目录哪些类管规则哪些类管界面拿到源码先别急着全局搜索“象棋”两个字先看包结构。不同源码命名可能有差异但职责基本一致我整理了一张分工表类名常见命名职责新手改哪里MainActivity入口负责装载棋盘视图和标题栏尽量别动ChessBoard / GameManager持有棋盘数组、当前回合、走法生成改规则在这里ChessView / BoardViewCanvas 绘制和 onTouchEvent负责画和点击改 UI 在这里Move / Step记录一次走子的起止坐标和吃掉棋子基本不用动AIEngine / MinMax搜索和评估函数算下一步调 AI 强度在这里这张表能救你很多时间很多人想改“让兵更强”结果在绘制类里翻了大半天因为没找到规则类和绘制类的边界。我一般拿到源码第一件事是给这些类做个简单编译测试条件允许直接在 IDE 的 Structure 窗口里扫一遍方法名有 generateMoves、isLegalMove、isGameOver 这三个方法名的类才是规则核心其他都是壳。另外如果你发现源码里连 AI 都没有只有人机对战改成了双人对战反而更简单——把点击事件里的回合控制逻辑改为不限制红黑就是一个现成的“两人用一台手机下棋”的应用很多课程设计就是这么交的。2.3 首屏黑屏是玄学其实都是日志里写好的事故跑不起来的第一现场永远是黑屏或闪退。黑屏可以分成两种一种是 Activity 起了但没内容一种是一闪就退。前者多半是布局 XML 里引用了不存在的资源 ID或者自定义 View 的构造方法没有被正确调用后者一般能在 AndroidRuntime 日志里看到某个类的 NoClassDefFoundError 或空指针。这里我给出最常用的排查套路先确认清单文件 AndroidManifest.xml 里 MainActivity 有没有注册很多老源码从 Eclipse 迁过来时会漏掉这一条再确认入口 Activity 的完整类名和包名是否与 adb 启动命令一致最后看一眼模拟器的 ABI 架构。很多源码的资源里只放了 armeabi-v7a 的 so 库如果你用 x86_64 的模拟器运行到调用 so 库那一步就会闪退。我个人的建议是别在 x86 模拟器上死磕老工程直接改用真机调试。把手机开着 USB 调试连上一键 Run日志流式输出比在模拟器里多一层硬件模拟层更快暴露问题。真机上如果安装时报“应用未安装”或签名冲突直接把旧包卸载干净再装这也是老源码折腾人时最常见的坑。3. 棋盘与走法用一条数组撑起整盘规则跑起来之后真正值得你花时间读的就是规则层。象棋一共就 32 个棋子、90 个交叉点规则层代码不管写成什么样最后都会收敛到同一个模型用数组存局面用函数生成所有合法着法。3.1 棋盘建模为什么是 int[10][9]以及红黑方向怎么定中国象棋的棋盘是十条横线、九条竖线交叉点是落子位置。程序里最直观的建模就是用一个二维数组int[10][9]下标正好对应交叉点。数组的值表示棋子0 表示空正数表示红方棋子负数表示黑方棋子。这样写的好处是取绝对值就能知道棋子类型取符号就知道阵营后面评估函数里直接按符号加减分非常顺手。方向问题必须先定死数组第一行 row0 到底是黑方还是红方直接影响绘制和 AI 的每一行代码。我习惯把 row0 定义为黑方底线row9 定义为红方底线这样在屏幕上黑在上、红在下符合传统认棋习惯。源码里如果发现走子方向和你预期相反不要硬改 UI先把棋盘的初始化顺序换一下通常比改十几个 if 省事。public class ChessBoard { // 棋盘状态0 空正数红方负数黑方 private int[][] board; // 初始局面黑方在上红方在下 public static final int[][] INIT_BOARD { {-4, -2, -3, -5, -1, -5, -3, -2, -4}, // 黑方底线 { 0, 0, 0, 0, 0, 0, 0, 0, 0}, { 0, -7, 0, 0, 0, 0, 0, -7, 0}, // 黑炮 {-6, 0, -6, 0, -6, 0, -6, 0, -6}, // 黑兵 { 0, 0, 0, 0, 0, 0, 0, 0, 0}, // 河界 { 0, 0, 0, 0, 0, 0, 0, 0, 0}, { 6, 0, 6, 0, 6, 0, 6, 0, 6}, // 红兵 { 0, 7, 0, 0, 0, 0, 0, 7, 0}, // 红炮 { 0, 0, 0, 0, 0, 0, 0, 0, 0}, { 4, 2, 3, 5, 1, 5, 3, 2, 4} // 红方底线 }; public void reset() { board new int[10][9]; for (int r 0; r 10; r) { System.arraycopy(INIT_BOARD[r], 0, board[r], 0, 9); } } }这段常量表几乎都能抄。1 将/帅2 士3 象4 车5 马6 兵7 炮正负区分红黑数组直接从上往下写黑在上红在下后面所有规则函数的 row 方向都基于这个约定。注意reset()里是深拷贝不是直接把INIT_BOARD赋给board否则后续走子会把常量表也改掉下次 reset 就回不到初始局面了。3.2 走法生成车的直线、炮的跳子、马的蹩腿走法生成是整个象棋源码里最核心的一段。常见实现把“这个棋子能去哪”和“这一步合不合法”分成两层generateMoves先生成候选点再用一个合法性过滤器把走后被将军的着法滤掉。这样代码更清晰AI 搜索里也能直接复用同一个函数。先看总入口public ListMove generateMoves(int[][] board, boolean isRed) { ListMove moves new ArrayList(); for (int r 0; r 10; r) { for (int c 0; c 9; c) { int piece board[r][c]; if (piece 0) continue; if (isRed piece 0) continue; // 红方不看黑子 if (!isRed piece 0) continue; // 黑方不看红子 pieceMoves(board, r, c, piece, moves); } } return moves; }这里注意判别式红方棋子为正所以红方回合遍历到的目标都是整数黑方棋子为负走黑方时跳过正数。pieceMoves里再按棋子类型分发到各个走法函数。常见走法规则可以浓缩成一张表棋子走法吃子最容易写错的地方车直线任意距离路径必须为空同走法吃到敌子即停吃完没停变成隔山打牛炮直线任意距离路径必须为空必须且只能隔一个棋子移动和吃子共用一套条件马日字蹩马腿处不能有子同走法蹩腿点坐标算错兵过河前只能向前过河后可横走同走法不能后退过河判断方向搞反象/士象飞田且不过河士九宫斜走同走法忘记限制在己方阵营将/帅九宫直走一格不能对脸同走法漏掉将帅照面规则车的走法最容易做成“路径穿透”从当前位置向四个方向延伸每走一步都检查路径上的点是不是空点遇到己方棋子立刻停遇到对方棋子则把该点加入着法然后停。很多新手把“吃子”单独列一套结果忘了吃完之后必须停车就变成了隔山打牛。正确写法是一次循环里空点加入着法继续走敌子加入着法停止遍历己子直接停止。炮的跳子是最容易翻车的一处。炮的移动和吃子是两段逻辑目标为空点时路径上不能有任何棋子目标是敌方棋子时路径上必须恰好隔一个棋子。所以炮的扫描要维护一个中间子计数从当前位置往外走没碰到子之前把空点当走法碰到第一个子之后后面第一个遇到的敌子才能作为吃子着法再往后继续扫描。这个“计数标志位”写不明白炮就会原地乱吃。马的蹩腿判断也有统一写法马跳日是 (dr, dc) 的绝对值为 (1,2) 或 (2,1)蹩腿点在横轴或纵轴的步进中间点。比如横向移动两步时要看马当前这一侧的正中间格子有没有子纵向移动两步时看上方中间格子。把这个中间点坐标写成一个通用方法比在每个方向分支里硬编码更不容易漏。3.3 将军、送将过滤与胜负判断能走不代表能下。象棋规则里不允许走完一步后让自己的将帅暴露在对方火力之下所以真正返回给 UI 和 AI 的着法都要经过“送将”过滤。实现上最稳的做法是模拟走这一步走完后检查己方将帅是否被将军被将军就把这一步丢掉然后撤销。将军判断本身不复杂找到己方将/帅的位置遍历对方所有棋子用生成着法的逻辑判断对方某个棋子一步之内能不能吃到这个点。因为棋盘只有 90 个交叉点这个开销完全可以接受AI 搜索时反复调用也不会有压力。public boolean isInCheck(int[][] board, boolean redSide) { int kingValue redSide ? 1 : -1; int kingR -1, kingC -1; for (int r 0; r 10; r) { for (int c 0; c 9; c) { if (board[r][c] kingValue) { kingR r; kingC c; break; } } } if (kingR 0) return true; // 将帅被吃直接视为被将军 // 遍历对方所有棋子看一步之内能不能吃到将帅 for (int r 0; r 10; r) { for (int c 0; c 9; c) { int piece board[r][c]; if (piece 0) continue; if (redSide piece 0) continue; // 只看对方棋子 if (!redSide piece 0) continue; if (canPieceReach(board, r, c, kingR, kingC, piece)) { return true; } } } // 将帅对脸同列且中间无子视为被将军 int otherKingValue redSide ? -1 : 1; for (int r 0; r 10; r) { for (int c 0; c 9; c) { if (board[r][c] otherKingValue c kingC) { boolean blocked false; for (int k Math.min(r, kingR) 1; k Math.max(r, kingR); k) { if (board[k][c] ! 0) { blocked true; break; } } if (!blocked) return true; } } } return false; }这里有一个要注意的边界canPieceReach和generateMoves必须复用同一套走法判定否则会出现“AI 认为将军了UI 却不认”的分裂。我的做法是把 pieceMoves 抽成两个方法一个返回 List一个只做“是否能到达指定点”的布尔判断布尔判断内部用同样的路径扫描逻辑只是命中目标就提前返回。合法性过滤就变成这样public ListMove generateLegalMoves(int[][] board, boolean isRed) { ListMove moves generateMoves(board, isRed); ListMove legal new ArrayList(); for (Move m : moves) { makeMove(board, m); if (!isInCheck(board, isRed)) { legal.add(m); } unmakeMove(board, m); } return legal; }注意makeMove和unmakeMove必须严格配对使用任何一条提前 return 的分支都会让棋盘状态错乱这也是递归搜索里最常见的隐蔽 bug。makeMove和unmakeMove不需要把整张棋盘深拷贝只需要把 Move 对象里记录的 from、to、captured 用回原数组即可这也是之后做悔棋功能要复用的东西。胜负判断则在generateLegalMoves返回空列表的基础上加一层如果当前方无着可走且已经被将军就是被将死如果无着可走但没被将军在正规规则里是困毙很多手机象棋源码也会直接判负看你是否要严格区分。4. 画棋盘与接触摸Canvas 绘制和交叉点坐标换算规则层通了下一步就是让用户看得见、点得动。Android 象棋源码里的 UI 部分几乎都是自定义 View 加 Canvas 绘制没有几个会用复杂动画重点全在坐标换算是否正确。4.1 先量尺寸再画线自适应的棋盘绘制很多源码直接写死 540 像素之类的棋盘宽度换一个屏幕就废了一半。象棋棋盘是 9 列 10 行交叉点之间的格子宽度 cell 应该由 View 的实际尺寸算出来cell min(可用宽度 / 8, 可用高度 / 9)再留出边距。注意竖线 9 条之间是 8 个格宽横线 10 条之间是 9 个格高很多第一次画的人把这两个分母搞反结果棋盘被拉伸。我用onSizeChanged把 boardLeft、boardTop、cellSize 缓存成成员变量onDraw 里所有坐标都从这三个值推导。不要在 onDraw 里反复量宽高不是性能问题而是旋转屏幕时空值会突然变成 0导致棋盘画到窗口左上角。Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); // 左右各留 padding 边距再用棋盘自身比例算出格子宽 float horizontalBoardSize w - 2 * padding; float verticalBoardSize h - 2 * padding; cellSize Math.min(horizontalBoardSize / 8f, verticalBoardSize / 9f); boardLeft (w - cellSize * 8f) / 2f; boardTop (h - cellSize * 9f) / 2f; }这个代码里分母分别是 8 和 9因为 9 根竖线之间有 8 个格子10 根横线之间有 9 个格子。棋盘居中的偏移量就是“剩余空间的一半”让左右上下边距相等。绘制棋盘本体分三部分所有横竖线、九宫两条斜线、河界文字与炮/兵标记。横竖线用 Paint 画成深棕色实线九宫斜线只画四个角的斜向连线规则是两方将帅的九宫都有两条交叉斜线河界可以直接用 drawText 写“楚河 汉界”但注意默认文字宽度算不准最好按 View 宽度量一次再画或者干脆省略换成棋盘中间留白否则中文字体在低分辨率设备上会被截断。4.2 触摸选中与落子Math.round 是唯一正确的换算棋子在屏幕上落在交叉点而交叉点之间的像素距离是 cellSize。所以手指按下的像素坐标要先减去 boardLeft / boardTop再除以 cellSize最后四舍五入到最近的整数交叉点。这个四舍五入就是新手最容易错的地方写 int(col) 看起来能编译实际点击越靠右下角偏差越明显因为浮点数被直接截断了。Override public boolean onTouchEvent(MotionEvent event) { if (event.getAction() MotionEvent.ACTION_DOWN) { int col Math.round((event.getX() - boardLeft) / cellSize); int row Math.round((event.getY() - boardTop) / cellSize); if (row 0 || row 10 || col 0 || col 9) { return false; } handleTap(row, col); invalidate(); return true; } return super.onTouchEvent(event); } private void handleTap(int row, int col) { if (selectedRow 0) { // 第一次点选中己方棋子 if (board[row][col] ! 0 isRedTurn (board[row][col] 0)) { selectedRow row; selectedCol col; } } else { // 第二次点尝试走子走不了就当重新选子 Move move new Move(selectedRow, selectedCol, row, col); if (chessGame.playMove(move)) { selectedRow -1; selectedCol -1; } else if (board[row][col] ! 0 isRedTurn (board[row][col] 0)) { selectedRow row; selectedCol col; } } }handleTap 里维护的是一个两段式选择状态第一次按下选中己方棋子第二次按下要么合法落子、要么换选另一个己方棋子。这里要特别说明为什么用 round 而不是强转手指落在交叉点附近时除法结果可能是 4.49 或 4.51强转会全部变成 4等于把点击区域往左上角偏了半个格子四舍五入才能让邻近四个交叉点的分界线都准确落在格子中央。chessGame是 View 持有的游戏管理类引用实际工程里通常在 Activity 里初始化这个 View 后再把 GameManager 塞进去这样 View 只负责输入和绘制不直接持有规则数据。4.3 悔棋怎么实现一次走子记录里该存什么悔棋几乎是象棋源码的标配很多课程设计验收时也会现场点悔棋。实现方式是在 GameManager 里维护一个 Stack 每次 playMove 合法落子后 new 一个 Move 压栈。Move 至少要记录四个坐标和被抓掉的棋子因为悔棋就是把 to 位置的棋子还原成 from 位置的棋子再把 captured 放回原处。public class Move { public int fromRow, fromCol, toRow, toCol; public int piece; // 谁在走 public int captured; // 目的位置被吃掉的子0 表示没吃 Override public String toString() { return (r fromRow c fromCol ) - (r toRow c toCol ); } }执行悔棋时弹栈然后用 Move 反操作把 board[fromRow][fromCol] 写回 piece把 board[toRow][toCol] 写回 captured回合标志取反即可。这套逻辑和 3.3 里 makeMove/unmakeMove 是一致的我会刻意让 AI 用的 makeMove 也返回 Move 对象这样搜索过程中的撤销和玩家悔棋共用同一段代码避免两套逻辑越改越不一致。还有一个细节悔棋只能悔“当前回合方”。否则红方悔一步、黑方接着又悔一步棋盘状态和回合标志就会对不上玩两轮之后整个局面就是坏掉的。边界处理上我的做法是每次 push Move 时同时记录当时的回合方悔棋时校验栈顶的回合方是不是当前用户。5. 避坑指南改源码最容易翻车的六个现场这一章全部是踩坑记录每一条我都按“现象 → 原因 → 解决”写你在自己的源码里遇到类似问题可以直接照着查不用再从头猜。5.1 红黑方向反了数组第 0 行到底是哪一方现象红黑双方走着走着感觉是黑方在下方操作红方在屏幕上方跟日常下棋习惯相反或者 AI 的第一步总是把棋子走到奇怪的对角线方向。原因源码初始化棋盘时定义的上方一方和你后来在 UI 里的绘制映射不一致。有的工程把 row0 当作红方底线有的把 row0 当作黑方底线而 UI 绘制的 y 坐标是从上往下递增的如果两者一开始就没对齐所有棋子方向都会颠倒。解决以数据结构为准不要改绘制层。先在 reset() 里确认 row0 是什么颜色比如我上面的常量表是黑在上红在下那么 onDraw 里棋子圆心 y 坐标就是 boardTop row * cellSizerow 越大越靠下黑色天然在上。如果看到源码里是红在上那就统一把初始化常量整体镜像而不是在走法函数里到处加负号。5.2 将帅对脸不判少了一条隐藏规则现象红帅黑将处在同一列且中间无子时玩家仍然可以正常走这一步游戏不提示非法AI 甚至可能主动走出这种送死棋。原因很多实现只检查“将帅被车马炮兵攻击”没有把“将帅不能照面”这条规则单独编码。它本质上是两条隐含规则任何一方不能走出让两将同列无遮的状态如果当前已经是同列无遮则等同于被将军。解决在 isInCheck 里增加将帅对脸检查就是 3.3 里那段代码。路径上只要有一枚棋子就算被遮挡空的交叉点不算遮挡。走法过滤是统一走 generateLegalMoves 的所以这一步加完后AI 和人都会自动遵守规则。5.3 炮的走与吃混在一起隔一个到底怎么数现象炮直线移动时被当成有炮架去跳吃或者炮在路径上有子时无法移动但在没子时又能隔子吃人。原因炮的移动规则不是“向一个方向延伸然后跳过第一个子”而是两种模式切换。很多第一次实现的人把“隔一个棋子”当成了移动时的统一条件忘了区分目标是空点还是敌子。解决把目标点分成两段处理。目标为空时路径上遇到任意棋子就终止不加入着法目标为敌子时路径上先遇到第一个子把它作为炮架遇到第二个子就是可吃的目标着法再加入后终止。调试时可以直接在炮分支打日志打印出每一步扫描到的路径点肉眼比对着几秒钟就能发现计数逻辑错在哪。5.4 触摸落点越点越偏int 强转不是四舍五入现象点击 A 交叉点附近时落子却经常出现在 A 的左上或右下的交叉点越往棋盘边角越明显中心区倒还好。原因坐标转换写了int col (event.getX() - boardLeft) / cellSize把 4.7 截断成 4整个点击范围被压缩了大约半个格子导致肉眼点准、代码接收到的却是错位交叉点。解决换成Math.round并且保证 boardLeft 和 boardTop 是 onSizeChanged 里重新算出来的值不是某个像素写死的常量。还有一个连带坑屏幕边缘点击如果换算出的 row/col 超出 10×9一定要做越界判断否则会出现数组越界或落子到棋盘外。5.5 手机一转棋局回到开局Activity 重建把内存棋盘干掉了现象玩到中局手机旋转一下棋局无缝恢复成刚开局或者退到桌面再进来进度全丢。原因棋盘数组存放在 Activity 的普通成员变量里旋转屏幕时 Activity 会被销毁重建内存中的数组自然归零。UI 重启后拿到的是 reset() 后的初始局面。解决最简单的处理是直接在 AndroidManifest 的 Activity 节点上加android:screenOrientationportrait锁定竖屏课程设计交作业没问题。更规范的方案是 onSaveInstanceState 里把 board 编码成字符串比如按行拼接恢复时解析回二维数组。注意 Move 栈也要一并保存否则悔棋栈和棋盘状态会错位恢复后第一次点悔棋就崩。5.6 AI 卡到 ANR搜索在主线程上裸奔现象轮到 AI 走棋界面卡住两三秒系统弹出“无响应”提示点等待之后又活过来看起来像死机。原因AI 搜索在 UI 线程里直接递归深度 6 层加上没有着法排序的剪枝展开的节点数轻松破百万。Android 主线程不允许做这种耗时操作超过 5 秒就被系统判死刑。解决把 AI 搜索丢到后台线程常见做法是一个 Thread 加 Handler 回传结果或者用 ExecutorService。搜索期间把棋盘 View 设置为不可点击并且显示“思考中”状态搜索得到最佳着法后再切回主线程执行 makeMove。深度先从 4 层起步确保秒回再往上加到 6 层。6. 给 AI 装上 MiniMax 搜索从“乱走”到“会吃子”UI 和规则都通了之后最后一个能拿出去展示的就是 AI。象棋源码里的 AI 通常不是深度学习的黑匣子而是可解释的搜索树让程序往前算几步看哪步局面最有利。MiniMax 的思路是“我下完你下”每一层轮流取最大或最小最后回推到根节点选中一步。配合 alpha-beta 剪枝可以砍掉大量分支代码量不大但提升明显。public int alphaBeta(int depth, int alpha, int beta, boolean isRed) { ListMove moves generateLegalMoves(board, isRed); if (depth 0 || moves.isEmpty()) { int score evaluate(board); return isRed ? score : -score; } // 着法排序先走吃子和大价值棋子能显著提高剪枝率 sortMoves(moves); for (Move move : moves) { makeMove(board, move); int score -alphaBeta(depth - 1, -beta, -alpha, !isRed); unmakeMove(board, move); if (score alpha) { alpha score; } if (alpha beta) { break; // 剪枝 } } return alpha; }这里用了负极大值写法每次递归都翻转符号就不需要区分 max 和 min 两层代码。alpha 初始是负无穷beta 初始是正无穷搜索过程中不断收缩区间。注意剪枝时 break 不要写成 return因为返回 alpha 会破坏还没搜索完的兄弟分支。评估函数不复杂但很影响棋力。我一般给子力赋基础分将 10000、车 1000、马 450、炮 480、兵 200、士象 120再叠加位置分比如兵过河加分、马在边线减分。代码private int evaluate(int[][] board) { int score 0; for (int r 0; r 10; r) { for (int c 0; c 9; c) { int piece board[r][c]; if (piece 0) continue; int abs Math.abs(piece); int value pieceScore(abs); score piece 0 ? value : -value; } } return score; // 正数代表红方优势 }实战参数我用得比较保守深度 4 层保证一秒内出结果6 层需要配合吃子排序和评估函数压缩节点量如果只是交作业4 层已经能赢不少新手别再盲目加大深度。把炮的位置分和兵过河分写准比多搜两层更划算这是我自己反复调参得出的血泪经验。最后提醒一句搜索线程和 UI 线程之间交换数据时不要直接操作同一个 board 数组否则搜索到一半玩家点了悔棋两边的状态就串了。我习惯是后台线程持有一份 board 的深拷贝算完只把 Move 结果传回来主线程再应用。这样规则层干净调试时也不会被并发问题折腾到怀疑人生。希望这篇拆解能帮你在 android 中国象棋源码的导入、改规则一直到 AI 调试这条路上少走几步弯路。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑