Android中国象棋源码:规则引擎与Alpha-Beta搜索落地

发布时间:2026/11/5 8:23:19
Android中国象棋源码:规则引擎与Alpha-Beta搜索落地 简介这份 Android 中国象棋源码基于 Java 与 Android Studio 构建面向希望借助完整可运行项目入门移动游戏开发的初学者与进阶者覆盖棋盘绘制、棋子走子规则、触摸事件响应、胜负判定与 AI 对弈等核心环节可用于课程设计、自学练手或二次开发参考。压缩包为 rar 格式共 110 个文件、约 4.47MB其中 66 个 png 提供棋子与棋盘图像素材10 个 java 与 20 个 class 构成游戏主体逻辑另有 3 个 mp3 音效、3 个 xml 布局配置及 2 个 db 数据文件代码与资源分层清晰。目前已有 523 人学习。读者可从中理解自定义 View 配合 Canvas 的界面绘制思路、MotionEvent 的落子交互处理、按棋种拆分规则类的实现方式以及异步加载、图片缓存与硬件加速等优化手段也能对照测试用例验证游戏逻辑。1. 一份 Android 中国象棋源码真正值钱的是规则引擎那一层很多人拿到 Android 中国象棋源码第一反应是跑起来看界面像不像样棋盘好不好看、棋子圆不圆、点下去有没有音效。我的习惯反过来先翻规则类在哪个包里。画棋盘、画棋子是最没门槛的部分Canvas 画十几条线、三十二个圆一天能写完真正决定这份源码能不能用、能不能改成别的棋类、能不能接 AI 的是走法生成、胜负判定和搜索这三块。Android 中国象棋这个场景有个特殊性棋盘只有 90 个交叉点状态空间比国际象棋小一个量级但规则里的“蹩马腿”“塞象眼”“炮翻山”“将帅照面”全是非标准几何约束写成 if-else 很容易漏。下面按一份典型工程的真实结构从渲染走到规则再走到 AI 和排错能照着改。2. 工程分层与棋盘渲染从 onDraw 的三段式绘制说起一份能长期维护的棋类工程分层不会太花哨但边界必须清楚。我见过的大部分 Android 中国象棋源码是四五层uiActivity、对话框、view自定义 BoardView、model棋盘数组、棋子常量、走法对象、rule走法生成、合法性校验、ai评估与搜索。差一点的版本会把规则塞进 BoardView 的onTouchEvent里这类代码能跑但你一动 AI 就得大改。判断一份源码值不值得留看rule包是不是纯 Java、有没有import android.*没有就是好底子。2.1 数据表示一维数组还是二维数组棋盘状态最常见的两种存法一种int[10][9]一种int[90]。二维数组可读性好board[row][col]一眼知道在哪儿一维数组在搜索里更快因为拷贝一个int[90]比拷贝十个小数组少一层指针跳转还能直接System.arraycopy。我在改这类源码时习惯把内部表示统一成一维外层包一个Board类提供get(row, col)用起来跟二维没区别但搜索时省事。// Board.java —— 内部一维存储外部按行列访问搜索时可直接 System.arraycopy public final class Board { public static final int ROWS 10, COLS 9, SIZE ROWS * COLS; private final int[] cells new int[SIZE]; public int get(int row, int col) { return cells[row * COLS col]; } public void set(int row, int col, int piece) { cells[row * COLS col] piece; } public Board copy() { Board b new Board(); System.arraycopy(cells, 0, b.cells, 0, SIZE); return b; } }这里的关键是row * COLS col这个映射必须全工程一致一旦某处写成col * ROWS row症状是棋子整体转置而且只有部分棋子表现异常极难查。参数上ROWS10、COLS9是固定的别做成可配置象棋没有变体棋盘。2.2 坐标系换算9 列 10 行对应的是 8 格 9 格这是新手翻车率最高的一处。棋盘有 9 条竖线、10 条横线交叉点 90 个但相邻线之间的“格子”只有 8 列、9 行。你按 9 和 10 去算格宽画出来的棋盘会比屏幕宽一格右边直接被裁掉按 8 和 9 算才刚好塞满。另外象棋棋盘是长方形8:9手机屏幕也是长方形但比例不匹配所以必须取min(cellW, cellH)强制正方形格子再把棋盘整体居中否则棋子会被拉成椭圆。// BoardView.java —— 几何参数只在尺寸变化时算一次绘制和触摸都复用 private int mCell; // 格子边长像素横竖相同 private int mOriginX; // 左上角交叉点的屏幕 x private int mOriginY; // 左上角交叉点的屏幕 y private float mPieceR; // 棋子半径 Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); int pad dp2px(8); int cellW (w - pad * 2) / 8; // 8 格宽不是 9 int cellH (h - pad * 2) / 9; // 9 格高不是 10 mCell Math.min(cellW, cellH); // 锁死正方形防止棋子变扁 mOriginX (w - mCell * 8) / 2; // 棋盘水平居中 mOriginY (h - mCell * 9) / 2; // 棋盘垂直居中 mPieceR mCell * 0.45f; // 留 10% 间隙棋子不粘连 }mCell取min之后棋盘在长边方向会多出空白这是正常的不要为了填满去拉伸。mPieceR用 0.45 而不是 0.5是因为 0.5 时相邻棋子边界正好相切配上 2px 描边会糊成一团。坐标反算就是x mOriginX col * mCell、y mOriginY row * mCell。反过来col round((x - mOriginX) / mCell)。这里必须用round不能用(int)强转强转是向下取整会让交叉点右下方半个格子内的点击全部算到上一个点表现为“点左边一格才生效”。2.3 onDraw 的三段式顺序与重绘触发点棋盘绘制有个顺序依赖先画底纹和木纹背景再画线最后画棋子。顺序反了棋子会被线压住看着像破图。河界“楚河汉界”四个字和九宫斜线要在线之后、棋子之前画不然文字会在棋子上面飘。Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); drawBackground(canvas); // 1. 底色 木纹 drawGrid(canvas); // 2. 10 横 9 竖 九宫斜线 兵炮位十字标记 drawRiverText(canvas); // 3. 楚河汉界 drawPieces(canvas); // 4. 棋子必须最后画 drawLastMoveHint(canvas); // 5. 上一步的起止点高亮框 }drawPieces里别每次都new Paint()、new RectF()这些对象在onDraw里创建会触发 GC落子动画一多就卡顿丢帧。标准做法是在构造函数里初始化好mPiecePaint、mTextPaint、mRect绘制时只改颜色和文字。棋子上的字用Paint.setTextAlign(Align.CENTER)配合Paint.FontMetrics垂直居中比手写y textSize/3稳换字体也不跑偏。重绘时机只有两个走子之后和悔棋之后都调invalidate()。别在onTouchEvent的ACTION_MOVE里调手指一滑就重绘几十次。如果工程里有落子动画棋子平移 150ms用ValueAnimator更新一个mAnimOffset在onDraw里给正在移动的棋子加偏移Animator结束再落数组这样视觉和状态分离悔棋时不会和动画打架。提示onSizeChanged里算出来的mCell一定要存成员变量不要在onDraw里重算否则每次重绘多两次除法虽然不多但棋盘加棋子三十多个元素累积起来在低端机上能看出掉帧。3. 落子与规则校验从触摸事件到走法表的完整链路渲染是皮肤规则是骨架。这一步写错后面 AI 再强都是错的——AI 搜索完全依赖走法生成走法生成漏一个“蹩马腿”AI 就会走出马穿墙的棋用户第一眼就发现。所以规则这块我建议先写纯逻辑、跑通单测再接 View。3.1 ACTION_UP 反算交叉点与误触阈值触摸处理有两个决定用ACTION_DOWN还是ACTION_UP要不要做误触判定。用DOWN的问题是手指按下瞬间就落子用户拖动查看棋盘时容易误走用UP更符合“点选—确认”两段式交互也支持“先点自己的子、再点目标点”的经典操作。误触判定是算点击位置到最近交叉点的距离超过半个格子就当没点。Override public boolean onTouchEvent(MotionEvent e) { if (e.getAction() ! MotionEvent.ACTION_UP) return true; // 只认抬手 int col Math.round((e.getX() - mOriginX) / mCell); // round不是强转 int row Math.round((e.getY() - mOriginY) / mCell); if (col 0 || col 8 || row 0 || row 9) return true; // 棋盘外忽略 // 误触阈值偏离交叉点超过半格视为无效 float dx e.getX() - (mOriginX col * mCell); float dy e.getY() - (mOriginY row * mCell); if (dx * dx dy * dy (mCell * 0.5f) * (mCell * 0.5f)) return true; handleTap(row, col); return true; }getX()拿的是相对于本 View 的坐标如果 BoardView 外面还套了ScrollView或带 padding 的容器必须用getX()而不是getRawX()后者是屏幕绝对坐标一减mOriginX就整体偏移。这个坑很典型在原生 Activity 里测试正常换到ConstraintLayout加个padding就永远点不中。handleTap里是两态状态机mSelected null时尝试选中自己的子否则判断目标点是否可走。可走就执行、入栈、切换行棋方、触发 AI不可走就重选或取消选中。状态机一定要有“点空白处取消选中”的分支不然用户点错了没有退路。3.2 七种棋子的走法生成与合法性过滤走法判定拆成两层第一层是“几何上能不能走到”第二层是“走完之后自己会不会被将”。很多轻量实现只做第一层结果允许送将、允许将帅照面AI 一搜就出鬼棋。几何层按棋子类型分派用switch比一堆if清晰。下面是最容易写错的三个车、炮、马。// Rule.java —— 纯几何判定不修改棋盘 static boolean checkChe(int[] b, int fr, int fc, int tr, int tc) { if (fr ! tr fc ! tc) return false; // 车只能走直线 if (fr tr) { int step tc fc ? 1 : -1; for (int c fc step; c ! tc; c step) if (b[fr * 9 c] ! 0) return false; // 路径有子即挡 return true; } int step tr fr ? 1 : -1; for (int r fr step; r ! tr; r step) if (b[r * 9 fc] ! 0) return false; return true; } static boolean checkPao(int[] b, int fr, int fc, int tr, int tc) { if (fr ! tr fc ! tc) return false; int count 0; // 炮架数量 if (fr tr) { int step tc fc ? 1 : -1; for (int c fc step; c ! tc; c step) if (b[fr * 9 c] ! 0) count; } else { int step tr fr ? 1 : -1; for (int r fr step; r ! tr; r step) if (b[r * 9 fc] ! 0) count; } // 目标为空只能平移中间不能有子目标有子必须正好一个炮架 return b[tr * 9 tc] 0 ? count 0 : count 1; } static boolean checkMa(int[] b, int fr, int fc, int tr, int tc) { int dr tr - fr, dc tc - fc; if (Math.abs(dr) 2 Math.abs(dc) 1) return b[(fr dr / 2) * 9 fc] 0; // 马腿在纵向中点 if (Math.abs(dr) 1 Math.abs(dc) 2) return b[fr * 9 (fc dc / 2)] 0; // 马腿在横向中点 return false; }炮的判定是“数中间有几个子”而不是“路径是否为空”这是炮和车的本质区别写成车那套就变成炮不能吃子。马的蹩腿是看起点到终点方向上的第一个邻格不是看终点附近(fr dr/2, fc)这个位置算错一格马就能斜穿。象的判定除了|dr|2 |dc|2还要加两条象眼(frdr/2, fcdc/2)必须为空不能过河。红方 row 从 9 往下减tr 5就是过河了。士和将都有九宫限制红方九宫是 row 7~9、col 3~5黑方是 row 0~2、col 3~5把这两个矩形写成常量数组比写四个不等式好维护。第二层的合法性过滤本质是“试走 撤销”在数组上落子、判断己方将是否被攻击、再恢复。// 试走一步检查己方是否被将军用于过滤送将走法 static boolean isLegal(int[] b, Move m, boolean redTurn) { int captured b[m.toRow * 9 m.toCol]; b[m.toRow * 9 m.toCol] b[m.fromRow * 9 m.fromCol]; b[m.fromRow * 9 m.fromCol] 0; boolean ok !isKingAttacked(b, redTurn) !isKingFacing(b); b[m.fromRow * 9 m.fromCol] b[m.toRow * 9 m.toCol]; b[m.toRow * 9 m.toCol] captured; // 必须还原被吃子 return ok; }isKingFacing就是“将帅照面”规则两个将帅在同一列且中间无子属于非法局面。这条特别容易被漏掉因为它不是“谁能吃谁”的问题而是一个全局约束。漏了之后 AI 会利用它走出“白脸将”直接取胜的棋人类玩家看了会觉得这程序不讲道理。3.3 走法栈、悔棋与将军提示悔棋的实现取决于栈里存了什么。只存from/to是不够的你还得知道这步吃了什么子、是不是吃子后触发了某种状态。所以Move对象里至少要有fromRow, fromCol, toRow, toCol, capturedPiece, movedPiece五个字段。public final class Move { public final int fromRow, fromCol, toRow, toCol; public final int movedPiece; // 走的什么子 public final int capturedPiece; // 吃掉什么子0 表示空 public Move(int fr, int fc, int tr, int tc, int moved, int captured) { ... } }AI 走一步、人类走一步栈里就是两条记录。悔棋按钮只弹一条的话人类退回去之后立刻又被 AI 走回来看起来像“悔棋没生效”。正确做法是一次弹两条如果栈顶是人类走的那步并且校验栈里剩余的步数。这是交互细节不是算法问题但用户感知极强。将军提示用同一套isKingAttacked每次走完检查对方将是否被攻击是就播提示音、高亮将的位置。判断“将死”还要多一步——把对方所有合法走法生成一遍如果一条都没有且正在被将军才是将死一条都没有但没被将军是困毙象棋规则里同样判负。注意走法生成函数在搜索里会被调用几十万次不要在函数内部new ArrayList之外再new任何对象走法列表用外部传入的MoveList复用或者直接把走法编码成一个intfrom * 90 to放进预先分配的数组里。4. AI 走法搜索极大极小 Alpha-Beta 在手机上怎么落地规则跑通之后AI 是最容易写出“看起来能用、实际很蠢”或者“很强但每步卡两秒”的部分。Android 中国象棋的 AI 难点不在算法本身在于手机 CPU 和搜索深度之间的矛盾深度 4 的全宽搜索在中低端机上轻松上百毫秒到几百毫秒深度 6 就得上秒。所以真正能用的方案是“固定时间预算的迭代加深”深度不是定死的时间才是。4.1 评估函数子力价值加位置分评估函数决定 AI 的“棋感”。最基础的版本只看子力差红方所有子的价值减去黑方所有子的价值。这套子力表在各类象棋引擎里取值大同小异差异主要在兵和炮。棋子常量名基础分值过河/位置加成说明将/帅JIANG10000无被吃即终局分值只为让搜索收敛车CHE900无最强子力两条线控制力大炮PAO450中局略高依赖炮架残局价值下降马MA400中心区域 30边马价值明显偏低士SHI200无只守九宫象XIANG200无只守本方半场兵/卒BING100过河 100过河兵价值翻倍是共识只按子力评AI 会走出“车换马”的亏本棋因为它不知道位置好坏。加一张位置表PST能明显改善同样是马在中心(4,4)附近比在边角强。位置表是int[90]常量数组红黑各一张黑方那张是红方上下翻转评估时score pst[index]。这套东西不需要训练靠经验值填就行网上各类开源象棋引擎的 PST 数值可以互相参考。static int evaluate(int[] b) { int score 0; for (int i 0; i 90; i) { int p b[i]; if (p 0) continue; int type Math.abs(p); int v VALUE[type] PST[type][p 0 ? i : FLIP[i]]; // 黑方位置表上下翻转 score p 0 ? v : -v; // 红正黑负 } return score; }FLIP[i]是预计算好的镜像索引数组FLIP[row * 9 col] (9 - row) * 9 col。每次评估都临时算9 - row会多出乘法和减法预处理成常量数组能省一点搜索几十万节点时这点开销是看得见的。4.2 Alpha-Beta 骨架与剪枝前提极大极小加 Alpha-Beta 是标配但很多人写完发现剪枝没效果原因通常是两条没有按“估值排序”生成走法或者忘了在窗口外提前返回。吃子走法应该优先搜索因为吃子往往直接改变估值能更快收紧窗口。private int alphaBeta(int[] b, int depth, int alpha, int beta, boolean redTurn) { if (depth 0) return evaluate(b); MoveList list generateLegalMoves(b, redTurn); if (list.size() 0) return redTurn ? -MATE (MAX_DEPTH - depth) : MATE - (MAX_DEPTH - depth); sortByCaptureFirst(list, b); // 吃子优先剪枝效率翻倍 for (int i 0; i list.size(); i) { Move m list.get(i); int captured doMove(b, m); int score -alphaBeta(b, depth - 1, -beta, -alpha, !redTurn); // 负极大值写法 undoMove(b, m, captured); if (score beta) return beta; // 剪枝 if (score alpha) alpha score; } return alpha; }负极大值写法-alphaBeta(..., -beta, -alpha, ...)的好处是红黑两方共用一份代码不用写两套max和min。容易写错的是-beta, -alpha的顺序写反了剪枝窗口变成负区间搜索直接返回垃圾值表现为 AI 出招莫名其妙。将死分数的处理要注意MATE - (MAX_DEPTH - depth)让“更早将死”得分更高否则 AI 会拖着不杀在必胜局面下反复绕圈直到超时。这个细节不写用户会觉得 AI 在戏耍他。4.3 迭代加深与时间预算控制手机上的正确做法是迭代加深从深度 2 开始搜搜完一层看时间够不够够就加深一层超时就用上一层的完整结果。这样无论设备快慢都能保证在预算内出招而且浅层结果可以作为深层搜索的走法排序依据越搜越快。private static final long TIME_BUDGET_MS 700; // 单步思考上限留余量给 UI private static final int MAX_DEPTH 6; public Move think(int[] board, boolean redTurn) { Move best null; long deadline System.currentTimeMillis() TIME_BUDGET_MS; for (int d 2; d MAX_DEPTH; d) { Move cur searchRoot(board, d, redTurn, deadline); if (cur null) break; // 本层超时丢弃不完整结果 best cur; // 只在整层搜完后才更新 if (System.currentTimeMillis() deadline) break; } return best; }两个关键点best只在整层搜索完成后更新半层的结果可能因为剪枝不完整而是错的超时检查不要放在每个节点上currentTimeMillis()本身有开销每 2048 个节点查一次就够。线程模型上必须把搜索放到子线程主线程只负责收结果后invalidate()。Java 里用HandlerThread加HandlerKotlin 里用Dispatchers.Default协程本质一样。搜索期间要禁掉用户的落子输入否则用户趁 AI 思考时点一下棋盘状态就乱了。加个mIsAiThinking标志位onTouchEvent开头直接 return。提示置换表Zobrist 哈希能把重复局面的搜索成本砍掉一大截但它对内存敏感。手机上开 116 条约 2MB就够条目里存hash、depth、score、flag、bestMove替换策略用“深度优先”而不是简单覆盖否则浅层结果会把深层结果挤掉。5. 排查清单跑不起来、点不准、AI 卡死的五个血泪现场5.1 棋盘画出来是扁的棋子压在格子里现象竖线间距明显小于横线间距棋子圆心落在格子中央而不是交叉点上整体看着像被横向压缩。原因基本是两个。一是onSizeChanged里用(w - pad*2) / 9算竖线间距、用(h - pad*2) / 10算横线间距除数取错了一格二是虽然cellW、cellH都算对了但没有取min强制相等导致横竖不等距。棋子落在格子中央则是坐标反算时写成mOriginX col * mCell mCell / 2多加了半格。解决先验证“9 条线 8 个间隔”这条不变式把cellW和cellH的除数分别打印出来看是不是 8 和 9再确认mCell Math.min(cellW, cellH)存在最后全局搜mCell / 2凡是出现在坐标计算里的都删掉棋盘坐标永远是交叉点不是格子中心。5.2 点击永远偏一格或永远点不中现象明明点在马上选中的是旁边的炮或者只有点击交叉点左上方一小块区域才生效。原因有三种。第一是坐标用了getRawX()在有 padding 或外层容器的布局里绝对坐标减去mOriginX会整体偏移一个 padding 的距离。第二是反算用了(int)强转代替Math.round所有点被截断到左上方向的格子。第三是把mOriginX当成棋盘左边界而不是左上角交叉点两者差半个格子。解决把三者逐个排除。先确认用getX()/getY()再把强转改成round最后打日志把(col, row)和实际棋子类型一起输出点三个不同位置看是不是线性偏移线性偏移就是常量错了非线性就是mCell错了。5.3 AI 每走一步卡两秒还有概率 ANR现象AI 思考期间界面完全冻结按返回键没反应连续走十几步后弹“应用无响应”。原因是搜索跑在主线程且没有时间上限——深度定死为 6在低端机上单步要两三秒。另外常见的放大器是每层递归都new ArrayList存走法列表一次搜索分配几十万个小对象GC 频繁触发实际耗时比纯计算翻倍。解决把搜索整个搬到HandlerThread主线程只更新 UI把固定深度改成“迭代加深 700ms 时间预算”走法列表改成预分配的MoveList内部用int[]装编码后的走法按需扩容递归时复用同一个实例池。三步做完单步耗时通常能降到 300ms 以内。5.4 悔棋按一次没反应要按两次才退回去现象人类走一步、AI 走一步点悔棋棋盘没变化再点一次退回两步之前。原因走法栈里人类和 AI 的记录一视同仁悔棋逻辑只弹一条弹掉的是 AI 那步界面看不出变化。有些实现还会在悔棋后立刻触发 AI 重新思考等于白退。解决悔棋逻辑先判断栈顶是不是 AI 的走法如果是连弹两条如果栈里只剩人类一步弹一条。悔棋后必须把状态重置为“等待人类落子”并取消 AI 的待执行任务同时清掉上一步高亮和选中状态。如果工程里这个类叫mHistory顺手给它加个size()的断言退到空栈还继续弹就是空指针。5.5 马能穿墙、炮能隔空吃、将帅照面不判负现象AI 或者人类可以走出明显违规的棋比如马被自己的兵挡住还能跳过去炮和目标之间没有任何子却能吃掉对方两个将帅在同一列对视程序不判负。原因马腿判定取错了格子写成了终点方向的邻格而不是起点方向的邻格炮的判定复用了车的“路径为空”逻辑没有单独数炮架将帅照面压根没写因为在很多简化版里这条规则被认为“不影响下棋”。解决马腿位置严格是(fr dr/2, fc)和(fr, fc dc/2)用单测把八个方向都覆盖一遍炮抽出独立的checkPao返回值取决于“目标是空还是有子”两种情况判定条件不同照面规则写进isLegal的全局校验里和“己方是否被将军”并列任何一步走完都要过这两道检查。6. 把规则引擎抽成纯 JVM 模块用 Perft 验走法前面所有排错里最难查的永远是走法生成——它不出异常、不崩溃只是悄悄多生成或少生成几步棋AI 强弱会因此偏移但你很难定位。我的做法是把model和rule两个包抽成一个不依赖 Android 的纯 Java 模块用 JUnit 在电脑上跑然后用 Perft 做冒烟验证。Perft 的思路很朴素从某个局面出发递归枚举所有合法走法到指定深度统计叶子节点数。走法生成只要错一处节点数就对不上参考值。中国象棋初始局面的 perft(1) 是 44这是一个公认值可以直接当断言用。// PerftTest.java —— 不依赖 Android直接在 JVM 上跑 static long perft(int[] b, int depth, boolean redTurn) { if (depth 0) return 1; long nodes 0; MoveList list MoveGen.generate(b, redTurn); for (int i 0; i list.size(); i) { int mv list.get(i); int from mv / 90, to mv % 90; int captured b[to]; b[to] b[from]; b[from] 0; nodes perft(b, depth - 1, !redTurn); b[from] b[to]; b[to] captured; // 完整还原包括被吃子 } return nodes; } Test public void initialPositionHas44Moves() { int[] board FenParser.parse(rnbakabnr/9/1c5c1/p1p1p1p1p/9/9/P1P1P1P1P/1C5C1/9/RNBAKABNR r); assertEquals(44L, perft(board, 1, true)); }走法用from * 90 to编码成一个int比Move对象省内存也方便在数组里排序。断言通过之后再往 depth 2、3 递进把每一层的数字记下来以后每次改动规则代码都重跑一遍——只要数字变了就说明你改动影响了走法生成哪怕是“看起来无关”的重构。移植时这套东西的价值更大。比如想把棋盘换成 Compose 或者做成跨端只要rule模块的单测还是绿的渲染层随便换。同理想加“残局挑战”模式也就是换一个初始 FEN 字符串规则和 AI 一行不用改。从我第一次因为一个蹩马腿写错、被 AI 走出马穿墙的棋开始我每次拿到一份新的棋类源码都先把规则模块单独抠出来跑一遍 perft再动任何 View 代码。这个习惯救过我很多次因为规则错了后面调 AI 的所有努力都是在给错误打补丁。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询