给出中国象棋伪代码逻辑链

66 2025-09-30 11:56

主评估入口:Evaluate(pos, vlAlpha, vlBeta)
│
├─ 1. 基础层:PreEval 已初始化
│     vlWhite = 红方棋子位置价值之和
│     vlBlack = 黑方棋子位置价值之和
│     存入 pos.vlWhite / pos.vlBlack
│
├─ 2. 搜索节点类型判断
│     if 当前深度 == LIMIT_DEPTH          → 叶子节点
│         return SearchQuiesc(pos, vlAlpha, vlBeta)   // 进入吃子延伸
│     else
│         goto 静态评估链
│
├─ 3. 静态评估链(逐步累加 + 可剪枝)
│     vl = pos.Material()                 // 子力基准
│     if vl + MARGIN1 ≤ vlAlpha or vl - MARGIN1 ≥ vlBeta
│         return 截断值                   // 浅层剪枝
│
│     vl += pos.AdvisorShape()            // 士象形状
│     if vl + MARGIN2 ≤ vlAlpha or vl - MARGIN2 ≥ vlBeta
│         return 截断值
│
│     vl += pos.StringHold()              // 牵制威胁
│     if vl + MARGIN3 ≤ vlAlpha or vl - MARGIN3 ≥ vlBeta
│         return 截断值
│
│     vl += pos.RookMobility()            // 车机动性
│     if vl + MARGIN4 ≤ vlAlpha or vl - MARGIN4 ≥ vlBeta
│         return 截断值
│
│     vl += pos.KnightTrap()              // 马陷阱
│
├─ 4. 兵过河奖励(源码在 Evaluate 末尾)
│     for 每方
│         for 每只兵
│             if 已过河
│                 vl += SIDE_VALUE(sdPlayer, 1)   // 红+1 黑-1
│
├─ 5. 返回最终静态值
│     return vl
│
└─ 5'. 叶子节点替代路径(Quiescence)
      SearchQuiesc(pos, vlAlpha, vlBeta)
      ├─ 只生成吃子 / 将军等“高波动”走法
      ├─ 递归调用静态搜索直至平静
      └─ 返回平静局面评估值

 

preeval的PreEvaluate()函数和evaluate的Evaluate()函数是什么关系

一句话先说结论:
PreEvaluate() 是“提前做好的功课”,Evaluate() 是“考试现场直接抄功课 + 再写几行附加题”。

🔍 1. 调用时机

表格
 
复制
函数 谁在何时调用 频率
PreEvaluate() 每次「进入新局(FromFen / MakeMove 链)」后 一次性 调用 极低(每局 1~n 次)
Evaluate() 搜索树 每个内部节点 都可能调用 极高(百万级)

🔍 2. 数据依赖

cpp
 
复制
// PreEvaluate() 产出
PreEval.ucvlWhitePieces[pt][sq]   // 红方 pt 类棋子在 sq 的“位置价值”
PreEval.ucvlBlackPieces[pt][sq]   // 黑方同上
PreEval.vlAdvanced                // 中残局倾向偏移分

// Evaluate() 直接使用
vl += PreEval.ucvlWhitePieces[pt][sq];   // 直接查表,不再计算
 

🔍 3. 职能边界

表格
 
复制
PreEvaluate() Evaluate()
只做「位置价值」的预计算与插值(中局↔残局) 只做「动态特征」的实时计算(形状、牵制、机动、陷阱、过河兵)
结果存入全局表,供后续秒查 把 PreEval 的表值当常数,再加上自己的动态分数
不依赖 α-β 边界,无剪枝 利用 α-β 边界,随时提前 return

🔍 4. 伪代码对照

pseudo
 
复制
////////// preeval.cpp //////////
PreEvaluate(pos):
    μ ← 计算中残局因子
    for pt in 0..6:
        for sq in 棋盘:
            PreEval.ucvlWhitePieces[pt][sq] ←
                (μ*MidTable[pt][sq] + (66-μ)*EndTable[pt][sq]) / 66
    PreEval.vlAdvanced ← 根据剩余子力给偏移分


////////// evaluate.cpp //////////
Evaluate(pos, α, β):
    vl ← pos.Material()                 // 1. 子力
    if 剪枝: return vl
    vl ← vl + PreEval 已算好的位置值    // 2. 直接查表(PreEval 成果)
    if 剪枝: return vl
    vl ← vl + AdvisorShape()            // 3. 动态附加题
    vl ← vl + StringHold()
    vl ← vl + RookMobility()
    vl ← vl + KnightTrap()
    vl ← vl + 过河兵奖励
    return vl
 

✅ 一句话总结

PreEvaluate() 把“昂贵但不变”的位置价值提前算好并制成表;
Evaluate() 在搜索时“秒查这张表”作为基础,再实时加上“便宜但变化”的动态因素。

 

那么为什么他们最后的值要累加,而可以在各自的运算中有不同的子力值体系?

根本原因:两类量纲不同、使用场景不同、更新频率不同,因此无需统一子力值体系,也不会冲突

✅ 一、量纲本质不同

表格
 
复制
体系 量纲 物理意义 是否同单位
PreEval 位置价值 “位置优越度" 描述“某个棋子放在这个格子对最终胜负的影响” ❌ 无量纲
Evaluate 动态附加 “局面优劣度" 描述“当前整个局面因形状、牵制、机动性而产生的优劣” ❌ 无量纲
Material 子力价值 “交换性价比" 仅用于吃子排序快速剪枝参考 ❌ 无量纲
三者单位都是“评估分”,但来源不同、用途不同,就像温度、湿度、风速都可读数,却不需要数值上对齐

✅ 二、使用场景与更新频率

表格
 
复制
场景 使用值 更新频率 是否依赖子力价值
搜索剪枝 Material 每节点 ✅ 需要快速对比
走法排序 MVV-LVA 每吃子走法 ✅ 仅用于排序
静态评估 PreEval + 动态附加 每叶子节点 不依赖子力价值
Quiescence 静态搜索 每叶子节点 不依赖子力价值

✅ 三、累加合法性:只是“线性加权”

cpp
 
复制
vl = Material()                      // 子力差
   + PreEval 表值                     // 位置差
   + AdvisorShape()                   // 形状差
   + ...
 
  • 所有项都是“差值”(红 - 黑),因此量纲一致
  • 线性可加性是 Alpha-Beta 搜索的基本假设
  • 系数无耦合:PreEval 的“车=90”与 MVV-LVA 的“车=4”永不相遇,不会出现在同一算式。

✅ 四、形象比喻

表格
 
复制
类比 角色 数值作用
房地产 PreEval 同户型在不同地段的价格差异
搬家费 Evaluate 动态附加 楼层、电梯、距离等额外费用
二手房底价 Material 按面积给的“保底价”,只用于快速比价
不会把“地段溢价”与“保底面积价”拿来比大小,但它们可以加在一起算总房价。

✅ 五、一句话总结

“位置价值”“动态特征”“交换性价比”三类分数来源不同、用途不同、更新频率不同,只因它们都表示‘红方减黑方的优劣’,所以可以在最终评估中线性累加;而它们各自的子力值体系互不干扰,因为永远不会出现在同一算式里。”

 

你贴出的这行代码:
cpp
 
复制
inline int Evaluate(const PositionStruct &pos, int vlAlpha, int vlBeta) {
  int vl;
  vl = Search.bKnowledge ? pos.Evaluate(vlAlpha, vlBeta) : pos.Material();
  return vl == pos.DrawValue() ? vl - 1 : vl;
}
 
并不是“把不同量纲的值相加”,而是在两条互斥的分支里选一条结果返回,只是赋值给同一个变量名 vl,所以不存在“不同体系相加”的问题

✅ 逐行拆解

表格
 
复制
代码 实际含义
Search.bKnowledge ? pos.Evaluate(...) : pos.Material(); 二选一
• 开启知识 → 走 完整静态评估链(PreEval表值 + 形状/牵制/机动/陷阱)
• 关闭知识 → 只走 子力差(Material)
vl = ... 选中的那个分支的返回值赋给 vl另一条分支根本不会执行
return vl == pos.DrawValue() ? vl - 1 : vl; 防止“真实和棋值”与“搜索和棋值”冲突,只做 ±1 微调,仍属于同一量纲

✅ 形象比喻

你手里只有一杯液体
  • 要么装的是 矿泉水(Material)
  • 要么装的是 糖水(Evaluate 全链)
绝不会把矿泉水和糖水倒在一起喝,只是用同一个杯子装其中一种而已。

✅ 一句话总结

这行代码是“二选一”而非“相加”
开知识就返回完整评估分,关知识就返回子力差;两者永不同时出现,因此不存在量纲冲突。

 

局面评价函数接口干什么用的?

局面评价函数接口(就是你贴出的这行 Evaluate(...))是搜索树与“静态世界”之间的唯一法定网关
它把“复杂、多源、随时会剪枝”的评估细节封装成一行调用,让搜索框架无需关心内部到底用几张表、加了几项、是否做静态搜索,只需拿到一个能直接参与 Alpha-Beta 比较的整数即可。

✅ 一句话职责

“给搜索一个数,告诉它当前局面红方比黑方好多少分,且这个数必须能安全地与 α、β 做比较。”

✅ 为什么必须存在这一层“接口”

表格
 
复制
搜索框架(search.cpp) 评价细节(evaluate.cpp / preeval.cpp)
只认整数 int vl 内部有表查询、形状、牵制、机动、陷阱、边界剪枝
需要统一量纲 来源多样,可能预计算、可能实时算
需要开关可控(bKnowledge) 用户可关掉知识只做子力
需要防和棋值冲突(DrawValue ±1) 原始值可能与搜索约定值相同
接口层 Evaluate(pos, α, β) 把这些全部消化,对外只返回一个干净、可用、不冲突的分数。

✅ 伪代码视角:搜索框架怎么看它

pseudo
 
复制
function AlphaBeta(pos, depth, α, β):
    if depth ≤ 0:
        return Evaluate(pos, α, β)   // ← 仅此一句,万事大吉

    for mv in 生成走法():
        pos.MakeMove(mv)
        val = -AlphaBeta(pos, depth-1, -β, -α)
        pos.UndoMakeMove()
        ...
 
搜索完全不需要知道
  • 有没有用 PreEval 表;
  • 有没有加形状分;
  • 有没有做静态搜索;
  • 返回值为什么比 DrawValue 小 1。
它只要一个整数就能继续剪枝。

✅ 再通俗一点

搜索是裁判,只问“谁领先多少分”;
Evaluate 是计分板,把复杂规则翻译成裁判能懂的数字。

✅ 总结

局面评价函数接口的核心作用:
  1. 封装:把多种评估源包成“一行调用”。
  2. 统一量纲:返回的分数可直接与 α、β 比较。
  3. 可控开关:通过 bKnowledge 一键切换“快速子力”与“全知识”。
  4. 防冲突:微调和棋值,避免与搜索约定值撞车。
没有这一层,搜索代码就会被评估细节淹没,也无法安全剪枝。

 

 

 

 

 

 

全部评论

·