给出中国象棋伪代码逻辑链
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 全链)绝不会把矿泉水和糖水倒在一起喝,只是用同一个杯子装其中一种而已。
✅ 一句话总结
这行代码是“二选一”而非“相加”:
开知识就返回完整评估分,关知识就返回子力差;两者永不同时出现,因此不存在量纲冲突。
局面评价函数接口干什么用的?
局面评价函数接口(就是你贴出的这行
它把“复杂、多源、随时会剪枝”的评估细节封装成一行调用,让搜索框架无需关心内部到底用几张表、加了几项、是否做静态搜索,只需拿到一个能直接参与 Alpha-Beta 比较的整数即可。
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 是计分板,把复杂规则翻译成裁判能懂的数字。
✅ 总结
局面评价函数接口的核心作用:
-
封装:把多种评估源包成“一行调用”。
-
统一量纲:返回的分数可直接与 α、β 比较。
-
可控开关:通过
bKnowledge
一键切换“快速子力”与“全知识”。 -
防冲突:微调和棋值,避免与搜索约定值撞车。
没有这一层,搜索代码就会被评估细节淹没,也无法安全剪枝。
全部评论