
简介这款Android中国象棋完整源码采用Java编写包含全部代码和图片资源遵循Activity、Service等Android基本组件规范面向游戏开发初学者与进阶者覆盖棋盘绘制、走子规则、触摸交互、胜负判断与AI对弈的完整流程适合课程设计、毕业设计或实战练手。压缩包共110个文件大小仅4.47MB其中66个png提供棋子、背景等图像素材10个java源文件承载主要逻辑另有可直接安装的Chess.apk、编译后的class字节码、xml配置和3个mp3音效目录结构清晰便于按模块研读。已有523人学习下载说明其具备不错的参考热度。结合ChessActivity、GameView、GuiZe等关键类可深入理解自定义View绘图、MotionEvent事件处理以及象棋走子规则与胜负判断逻辑。项目还包含欢迎界面、帮助界面与教程线程读者能借鉴小型游戏的场景切换、资源组织及基础AI搜索剪枝思路并结合apk体验实际运行效果。1. 为什么建议直接拿这份 Android 中国象棋源码而不是从零画棋盘做第一个象棋 App 的时候我把大部分时间花在“把棋盘画出来”而不是“把棋下对”上后来翻到这份 Android 中国象棋源码发现棋盘绘制、棋子实体、走法校验甚至简单的电脑对手都已经是完整闭环。它解决的不是“有没有棋盘贴图”而是“为什么我的兵过河不能横走、马被蹩腿不报错、AI 走一步卡三秒”这一串真问题。适合两类人一类是做 Android 游戏练手想看懂自定义 View 与状态机怎么配合另一类是课程设计或毕设要做象棋对弈需要一个能跑、能改、能答辩的底子。以下是拆包过程中比较关键的部分按从易到难的顺序讲。2. 源码模块拆解棋盘绘制、棋子实体与着法生成2.1 棋盘绘制用自定义 View 画九宫格而不是切图拿到源码先看 View 层。这份资源里棋盘不是一张 JPG而是直接在自定义 View 的 onDraw 里用 Paint 画出来的。好处是棋盘尺寸随屏幕宽高缩放不需要为不同分辨率准备多套切图坏处是坐标换算必须认真看否则红黑双方会上下颠倒。棋盘本质上是一个 10×9 的网格横线 10 条、竖线 9 条交叉点是落子位置。源码里通常用两个坐标数组或直接从 View 宽高算出Override protected void onDraw(Canvas canvas) { // 棋盘四周边距格子大小由宽度决定 float margin 20f; float cellSize (getWidth() - margin * 2) / 8f; Paint paint new Paint(); paint.setColor(Color.BLACK); paint.setStrokeWidth(2f); // 画横线 for (int row 0; row 9; row) { canvas.drawLine(margin, margin row * cellSize, margin 8 * cellSize, margin row * cellSize, paint); } // 画竖线左右边界要留出河界开口 for (int col 0; col 9; col) { canvas.drawLine(margin col * cellSize, margin, margin col * cellSize, margin 8 * cellSize, paint); } // 九宫斜线按 row/col 判断后补 drawPalace(canvas, margin, cellSize); }这段代码做的是把所有交叉点绘制成棋盘网格。cellSize 由 View 宽度除以 8 得到所以棋盘会跟随屏幕宽度等比例缩放你如果遇到横屏崩溃通常就是这里没有写死宽高比。九宫斜线只需要在左边 3 列和右边 3 列画两条斜线坐标直接用 (0,0)-(2,2) 这类相对位置换算不需要单独算表。如果源码里用的是 ImageButton 或 Button 当棋子我建议你替换成自定义 View 加 onTouchEvent 命中检测。Button 棋子数量多事件分发容易错位而且每次移动都要重新布局棋盘越复杂越难维护。自定义 View 里只需在 onTouchEvent 中根据落点换算行列x / cellSize、y / cellSize命中哪个交叉点就选中哪颗棋子逻辑干净很多。棋子绘制一般复用同一个 Paint先画一个带底色圆形再画汉字。源码里常见的是用 Typeface 或者直接 drawText。这块逻辑不复杂但注意象棋汉字不是 Unicode 排列的最好用数组维护“车马炮将士相帅”的顺序。2.2 棋子建模常量表与棋子对象的职责看完整份源码后你会发现棋子建模是整个程序的地基。类型用常量阵营用标记坐标用棋盘数组下标。我建议你先把这份常量表抄到自己的项目里后续所有逻辑都依赖它。常量名值说明TYPE_ROOK0车TYPE_HORSE1马TYPE_CANNON2炮TYPE_ELEPHANT3相/象TYPE_GUARD4仕/士TYPE_KING5帅/将TYPE_PAWN6兵/卒棋子对象一般长这样public class ChessPiece { public int id; // 棋子唯一编号 public int type; // 上面常量表里的类型 public int camp; // 0 红方1 黑方 public int x, y; // 当前在棋盘数组中的坐标 public boolean dead; // 是否已被吃 }这个类没有放绘制相关字段意味着它只负责“棋子是什么、在哪”负责“怎么画”的是棋盘 View。职责分离之后走法校验、AI 搜索都只需要操作 ChessPiece不需要碰任何 UI 代码。这个设计也是这份源码值得复用的原因之一。如果你拿到的是把棋子也做成了 ImageView 的版本改造成 AI 会非常痛苦。2.3 着法生成与合法性校验源码里最值钱的一段着法生成简单说就是把当前某个棋子所有能去的位置全部算出来。车和炮的走法都是沿直线扫描区别在于炮吃子必须隔一个棋子。车的扫描代码直接看private ListMove scanLine(int[][] board, int x, int y, int dx, int dy, int camp) { ListMove moves new ArrayList(); int nx x dx, ny y dy; while (nx 0 nx 10 ny 0 ny 9) { int target board[nx][ny]; if (target 0) { moves.add(new Move(x, y, nx, ny)); } else if (isOpponent(target, camp)) { moves.add(new Move(x, y, nx, ny)); // 吃子 break; } else { break; // 自己人挡住 } nx dx; ny dy; } return moves; }dx、dy 是方向向量四个方向分别传 (1,0)、(-1,0)、(0,1)、(0,-1)。board 里 0 表示空位正负号表示阵营。炮的代码要麻烦一点需要标记是否已经越过炮架private ListMove scanCannon(int[][] board, int x, int y, int dx, int dy, int camp) { ListMove moves new ArrayList(); int nx x dx, ny y dy; boolean jumped false; while (nx 0 nx 10 ny 0 ny 9) { int target board[nx][ny]; if (target 0) { if (!jumped) moves.add(new Move(x, y, nx, ny)); } else { if (!jumped) { jumped true; // 第一个棋子作为炮架 } else { if (isOpponent(target, camp)) moves.add(new Move(x, y, nx, ny)); break; } } nx dx; ny dy; } return moves; }这段代码把炮的过子逻辑拆成了两段没遇炮架前只能走空位遇到第一个炮架后标记 jumped往后遇到的第一个对方棋子才能吃遇到自己人或再遇到障碍就停止。炮最容易写错的是把“隔一个子吃”理解成“隔任意多个子吃”对照这段代码逐行检查就能定位。马、象、士、将这些棋子的走法更简单但校验的坑都在“阻挡”上。马走日要检查蹩马腿象走田要检查塞象眼将帅只能走直线一步且不能出九宫。这些规则如果你在 copy 代码时漏掉其中一条在界面上表现为“这步棋能走但实际不能走”用户会直接判你程序写错。合法性校验还有最后一层走完这一步之后己方将/帅不能被将军。所以源码里的 checkLegal 通常会这样做先临时走子再调一次 isChecked(camp)如果自己被将军就撤销并判定非法。这一步往往被初学者省略结果就是 AI 能把自己老将送到对面嘴里。3. 规则逻辑怎么落地坐标体系、吃子判定与三个隐蔽角落3.1 棋盘存储二维数组比对象列表更适合做规则校验源码里 board[][] 用什么类型决定你后面所有算法怎么写。这份源码用的是二维 int 数组10 行 9 列0 代表空位正数代表红方棋子编号负数代表黑方棋子编号。好处是查找目标位置 O(1)坏处是每次移动棋子board 和 ChessPiece 的坐标要同步更新而这两个地方是两套数据。初始化棋局时可以直接用一个 String 数组描述初始局面解析时按字符映射到棋子类型这样以后摆残局用例也方便private String[] initialBoard { rheakaehr, // 黑方底线 ......... , c c , // 炮 p p p p p, // 卒 ......... , ......... , P P P P P, C C , ......... , RHEAKAEHR // 红方底线 };每个字符对应一个棋子R 红车、H 红马、E 红相、A 红仕、K 红帅、C 红炮、P 红兵小写为黑方。解析时逐字符生成 ChessPiece同时写入 board 数组。这么做比硬编码几十个棋子位置更直观也比较容易做棋谱复盘和单元测试。3.2 吃子、照将与胜负判定吃子逻辑很简单目标位置如果是对方棋子从活跃列表里移除或标记 deadtrue然后 board 更新为当前棋子编号。需要留心的是真人对弈时同一个棋子对象可能被吃一次之后又重新摆上复盘或悔棋所以 dead 标记比直接置 null 更安全。胜负判定核心是 isKingCaptured()判断对方阵营里是否存在 KING 类型的存活棋子。大多数源码都会在 makeMove 之后立刻检查这个条件如果老将被吃直接弹胜利提示。另一个容易漏掉的是“将帅照面”也就是双方老将之间没有棋子阻挡时轮到谁走都不能让老将自动见面。private boolean isKingFace() { ChessPiece redKing findKing(RED); ChessPiece blackKing findKing(BLACK); if (redKing.y ! blackKing.y) return false; // 不在同一列 int col redKing.y; int count 0; for (int row redKing.x - 1; row blackKing.x; row--) { if (board[row][col] ! 0) count; } return count 0; }这里的判断基于红王在上、黑王在下且同列。正规写法是从红王出发向上扫遇到的第一个非空棋子如果正好是黑王就说明照面。count 判断在残局里更直观但要注意循环方向与两王上下位置一致别把 row 的起始和终止写反。3.3 三个最容易写错的细节过河、塞象眼、蹩马腿第一兵的走法不是永远向前。红方兵在跨过河界之前只能前进过了河之后才能横移而且不能后退。很多人会把“过了河可以左右走”写成“可以上下左右走”结果兵出现在老将旁边。源码里判断过河红方看 row 是否小于 5黑方看 row 是否大于 4写一个 isCrossedRiver(camp, row) 方法单独管理。第二象走田的“田”字中心是唯一障碍。它和炮架不同炮架不管距离多远只要隔一个子就能吃象的中心点是固定的四个方向斜两格。这里建议用偏移表处理把四个方向的 dx、dy 和中心点的 dx/2、dy/2 放在同一个数组里写代码时不容易漏private int[][] elephantMove { {2, 2, 1, 1}, // 右下 {2, -2, 1, -1}, // 左下 {-2, 2, -1, 1}, // 右上 {-2, -2, -1, -1} // 左上 };数组中前两个是目标位置偏移后两个是象眼检查位置。遍历时先检查象眼和棋盘的边界再检查目标位置是否可走。红方象不能越河黑方同理这个限制通常直接加在目标位置的 row 范围里。第三马的蹩马腿方向是“马步起点到终点之间那条短边”。同样是偏移表马走八个方向每个方向要判断的是起点旁边的那个格。比如向右上走要检查起点右边一格向左上走要检查起点左边一格。如果不按偏移表写改成大量 if-else几乎必然出现某个方向判断错误。这三个细节恰好在 AI 搜索时会被反复触发。因为电脑每走一步都会生成所有合法着法任何一个规则错误都会让 AI 走出违反规则的棋。你可以把它想象成数据结构里的旋转细节直接决定你的引擎好不好使。4. 人机AI的实现思路搜索框架、评估函数与难度参数4.1 极小极大搜索的工程化写法电脑下棋的基本框架是搜索树假设当前轮到红方红方走一步黑方再走一步双方都选择对自己最有利的走法最终倒推回当前局面。源码里常见的是 negamax 变体因为象棋是零和游戏红方得分就是黑方失分。实现如下private int alphaBeta(int depth, int alpha, int beta, int camp) { if (depth 0) { return evaluateBoard(); } ListMove moves generateLegalMoves(camp); if (moves.isEmpty()) { return camp RED ? MATE : -MATE; } for (Move move : moves) { makeMove(move); int score -alphaBeta(depth - 1, -beta, -alpha, 1 - camp); unmakeMove(move); alpha Math.max(alpha, score); if (alpha beta) { break; // 剪枝不再继续搜这条分支 } } return alpha; }这段代码的关键在负号递归当前层返回的分数对上一层来说要取反因为黑白双方立场相反。alpha、beta 是窗口随着搜索不断收紧一旦 alpha beta 就说明当前分支已经无望直接剪掉可以节省大量时间。建议把 MATE 设成 100000 或 99999避免评估值把将死判断冲掉。我一般还会维护一张置换表transposition table把已搜索局面的分数缓存起来这份源码里未必有但如果你发现 AI 在残局阶段反复搜索相同局面加一层 HashMap 缓存能明显提速。注意缓存键要用“棋子编号与棋盘数组整体 hashcode”不能只存棋局字符串否则 hash 碰撞会丢正确性。影响剪枝效率的另一个细节是着法排序。把吃子、将军这类强着排在前面剪枝会提前命中。可以简单按目标位置是否有棋子排序moves.sort((a, b) - { boolean aAttack board[a.toX][a.toY] ! 0; boolean bAttack board[b.toX][b.toY] ! 0; return aAttack bAttack ? 0 : (aAttack ? -1 : 1); });这段排序代码在进入 alphaBeta 循环前执行一次。吃子着法优先搜索会让更好的走法更早抬高 alpha 下限后续分支剪得更快。不需要做复杂的历史启发这一步就足以让深度 3 的搜索在手机端达到可接受速度。4.2 评估函数基础价值加位置修正搜索到叶子节点之后需要给局面打一个分数。最朴素的评估是“我方棋子总值 - 对方棋子总值”。以下是源码里常见的基础价值表棋子基础价值说明帅/将10000不能被吃车900直线长程子炮450隔子打击马400八面威风但会被别腿兵/卒200过河后可加 100~150相/象110防守子仕/士110九宫防守纯价值表不够因为同一个棋子在不同位置的价值天差地别。源码里通常会给车、马、炮附加“位置价值表”比如马在边角比在中路差很多兵过河之后位置价值上涨。评估函数大致是private int evaluateBoard() { int score 0; for (ChessPiece piece : pieceList) { if (piece.dead) continue; int base BASE_VALUE[piece.type]; int posDelta POS_VALUE[piece.type][piece.x][piece.y]; int value piece.camp RED ? base posDelta : -(base posDelta); score value; } return score; }如果源码里没有位置表你可以用一个 10×9 的 int 二维数组去描述每个棋子的加分区域比如把“兵过河后的行”和“马的中路区域”单独加分。这个表不用做得很精确AI 级别从“乱走”到“看出来会吃子”的提升是立竿见影的。4.3 调难度看哪里搜索深度、随机扰动和耗时控制源码的 AI 难度通常只看一个参数搜索深度。深度 1 等于电脑只看自己走一步基本是瞎走深度 3 已经能看懂吃子与基本防守深度 4 以上才有可能走出战术组合。但深度不是无脑上调的深度每加 1节点数量大约乘以 30~40手机端如果不开剪枝优化深度 5 就可能卡到 10 秒。我一般把难度做成三档难度深度附加行为简单2评估函数加入随机抖动 ± 30中等3无随机抖动困难4启用置换表缓存在 Android 上AI 计算不能放在主线程否则点击棋子后界面直接卡死。常见做法是单独的 Thread 跑 alphaBeta算完之后通过 handler.post 回主线程更新棋盘。如果你拿到的源码没处理这一步真机上一走 AI 就 ANR那是线程问题不是搜索问题别把锅甩给算法。5. 编译运行与排查避坑把源码跑起来的一线记录5.1 环境准备SDK 与 Gradle 版本对齐源码适配的环境是 Android Studio JDK导入时第一个坑是 Gradle 版本。老项目用老 Gradle新版 Android Studio 打开会提示升级我一般建议先按源码里的 gradle-wrapper.properties 版本走不要一上来就升到最新。JDK 版本不匹配会报“Unsupported class file major version”解决办法是切换项目的 Java SDK 为 11 或 17视 Gradle 要求而定。先把 local.properties 里 sdk.dir 配置正确再试编译# 以本机实际 SDK 路径为准 sdk.dir/Users/你的用户名/Library/Android/sdk如果源码里工程文件缺少 gradle-wrapper.jar先不要手动下载优先用 Android Studio 打开后让它补全依赖。手工下载容易把 Gradle 版本弄混导致编译链不一致。5.2 真机调试从安装到走完一步棋编译通过后先别急着看 AI 下棋。建议按下面的顺序验证规则层先双人模式红车吃黑马重点看“吃子后 dead 标记和 board 数组是否都更新”再找一个兵走到对方底线确认不能后退最后红黑对调方位看看黑方棋子是从下往上走还是从上往下走。这套流程走完再开人机模式。如果双人模式都走不对后面的 AI 搜索必然全盘错乱。真机运行时注意日志过滤adb logcat | grep -E AndroidRuntime|chess|Move关键是抓两个崩溃点一个在触摸事件触发 makeMove 后产生的空指针另一个在 AI 计算线程里访问 View 更新导致的 CalledFromWrongThreadException。前者通常来自棋子对象未初始化后者需要把 UI 更新统一切回主线程。5.3 避坑记录五条值得抄进笔记的问题现象一编译报“SDK location not found”。原因项目根目录没有 local.properties或者里面路径失效。解决手工创建 local.properties 并写入本机 sdk.dir。现象二模拟器正常真机棋盘左边溢出。原因cellSize 用 px 计算后未考虑屏幕宽度变化。解决棋盘整体宽度改为 min(可用宽度, 可用高度)高度方向留黑边。现象三点击棋子后 App 崩溃Log 显示数组越界。原因棋子坐标已经越界board 数组的 nx、ny 没有做越界检查。解决在 scanLine 进入 while 之前先检查 x、y 合法并给每个方向扫描加上边界判断。这个错最容易出现在马和象的偏移表里方向数是 8 和 4少写一个方向反而会让棋子凭空消失。现象四AI 走完一步后棋盘不刷新。原因makeMove 后调用 invalidate() 的位置放在了子线程。解决所有 UI 刷新集中在主线程搜索线程只负责返回最佳 Move。现象五AI 把老将往对方嘴里送。原因合法性校验里漏了“走子后是否被将军”生成着法时只查了静态规则。解决makeMove 之后临时回退检查 isChecked如果自己被将军这着不算合法。这五条几乎覆盖了从环境到规则再到线程的全部常见翻车点。尤其是最后一个排查起来最费时间因为你单看某个棋子的走法时它看起来永远是对的只有组合走到老将旁边才会暴露。6. 进阶改造给源码加悔棋栈与双档难度拿到源码能跑起来只是第一步。真正让这份 Android 中国象棋源码变成“自己的作品”建议做两件最小改造悔棋和历史步法记录。这两个功能正好能检验你对 makeMove 和棋盘数据同步的理解是否到位。6.1 用栈记录历史走法在 makeMove 里把每一步压入栈悔棋时弹出并回退棋盘数组。private StackStepRecord history new Stack(); private void makeMove(Move move, int camp) { int captured board[move.toX][move.toY]; board[move.toX][move.toY] board[move.fromX][move.fromY]; board[move.fromX][move.fromY] 0; history.push(new StepRecord(move, captured, camp)); } private boolean undoLastMove() { if (history.isEmpty()) return false; StepRecord record history.pop(); board[record.move.fromX][record.move.fromY] board[record.move.toX][record.move.toY]; board[record.move.toX][record.move.toY] record.captured; // 别忘了同步 ChessPiece 的 x、y 坐标 return true; }注意悔棋只回退棋盘数组是不够的ChessPiece 的 x、y 如果不同步AI 下次搜索时读到的棋子位置与实际画面不一致。我见过不少改造项目在这里把“棋盘数组”和“棋子对象”彻底拆成了两套数据悔棋后棋盘画面正常AI 一思考立刻东一步西一步。6.2 把 AI 拆成异步入口发现源码还能改造成双档难度核心是把搜索深度抽象成参数由界面开关控制并且把搜索过程包进线程。private void startAiThink(int depth) { new Thread(() - { Move best searchBestMove(depth, currentCamp); mainHandler.post(() - applyMove(best)); }).start(); }最小改动是新建一个 Thread不要在 main 线程里直接调用 alphaBeta。参数 depth 可以在界面做两个按钮分别传 2 和 4 就行。这样改完在真机上跑“简单档”和“困难档”体会会很明显。6.3 验证改造是否破坏了原规则改造完之后我会强制自己按一个固定顺序验证先双人模式连续走 20 步常规开局看是否有棋子消失再走一个“兵过河横移”确认过河判定没被悔棋逻辑破坏最后人机对弈下三步然后悔棋确认棋盘数组、棋子对象、UI 三者一致。这一套走完才算没把源码改出新 bug。如果你需要这份能跑的底子直接按项目标题去资源站搜就行整理包里的棋盘与棋子实体都有注释能省掉不少时间。从那以后我每次接手这类带规则引擎的 Android 源码都会强制把“棋盘数组、棋子对象、UI 刷新”三者的同步关系写在纸上再开始改代码。希望帮到你。本文还有配套的精品资源点击获取