Skip to content
This repository has been archived by the owner on Feb 26, 2024. It is now read-only.

Commit

Permalink
修改了搜索的结构,提高了搜索的效率
Browse files Browse the repository at this point in the history
  • Loading branch information
PikaCat committed May 26, 2022
1 parent df52860 commit 1cce6ca
Show file tree
Hide file tree
Showing 9 changed files with 102 additions and 107 deletions.
2 changes: 1 addition & 1 deletion ChineseChess.pro.user
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE QtCreatorProject>
<!-- Written by QtCreator 7.0.1, 2022-05-15T19:36:59. -->
<!-- Written by QtCreator 7.0.2, 2022-05-26T12:53:40. -->
<qtcreator>
<data>
<variable>EnvironmentId</variable>
Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
+ 引擎算法基于超出边界(Fail-Soft)的AlphaBeta剪枝,使用迭代加深(含内部迭代加深)的搜索方式
+ 在局面评价上使用渐进式的评估方法对进行局面评估,考虑了棋子的子力价值、位置分、王安全分(包括空头炮、炮镇窝心马、沉底炮等危险棋形的评估)
+ 支持历史表启发,杀手启发,吃子启发,有良好的走法排序器
+ 支持基于SSE的无锁置换表裁剪、带验证的空着裁剪、落后着法衰减/裁剪、杀棋步数裁剪、剃刀裁剪、静态评分裁剪、差值裁剪
+ 支持基于SSE的无锁置换表裁剪、带验证的空着裁剪、落后着法衰减、杀棋步数裁剪、剃刀裁剪、无用裁剪、差值裁剪
+ 支持将军延伸和重复局面检测(支持长将检测和部分长捉检测)
+ 支持主要变例搜索、使用OpenMP与QtConcurrent并发库进行Lazy-SMP多线程搜索
+ 联网的情况下支持ChessDB提供的开局库、对局库和残局库,大约可提升引擎200ELO左右
Expand Down Expand Up @@ -71,4 +71,5 @@
#### 参考代码
1. 象棋小巫师: https://github.com/xqbase/xqwlight
2. 象眼: https://github.com/xqbase/eleeye
3. 国际象棋位棋盘: https://github.com/maksimKorzh/bbc
3. 国际象棋位棋盘: https://github.com/maksimKorzh/bbc
4. 佳佳象棋:https://github.com/leedavid/NewGG
2 changes: 1 addition & 1 deletion src/GUI/dialog.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -650,7 +650,7 @@ void Dialog::computerMove() {
this->m_chessEngine->makeMove(move);
// 如果走云端走法会产生重复局面,只有对方长打才采纳云端走法
auto repeatScore { this->m_chessEngine->getRepeatScore() };
if (not repeatScore.has_value() or repeatScore.value() == BAN_SCORE_MATE) {
if (not repeatScore.has_value() or repeatScore.value() == BAN_SCORE_LOSS) {
emit threadOK(step);
return;
}
Expand Down
6 changes: 4 additions & 2 deletions src/GUI/dialog.ui
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
</property>
<property name="font">
<font>
<family>Microsoft YaHei Light</family>
<pointsize>-1</pointsize>
</font>
</property>
Expand All @@ -38,6 +39,7 @@
</property>
<property name="font">
<font>
<family>Microsoft YaHei Light</family>
<pointsize>-1</pointsize>
</font>
</property>
Expand Down Expand Up @@ -773,9 +775,9 @@ color:transparent;</string>
<widget class="QLabel" name="ComputerScore">
<property name="geometry">
<rect>
<x>10</x>
<x>0</x>
<y>20</y>
<width>181</width>
<width>201</width>
<height>61</height>
</rect>
</property>
Expand Down
5 changes: 0 additions & 5 deletions src/board/chessboard.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -223,11 +223,6 @@ bool Chessboard::isLegalMove(Move &move) const {
return false;
}

bool Chessboard::isNotEndgame() const {
if (RED == this->m_side) return this->m_redScore > 400;
else return this->m_blackScore > 400;
}

std::optional<qint16> Chessboard::getRepeatScore(quint8 distance) const {
/* mySide代表的是是否是调用本函数的那一方(下称"我方")
* 因为一调用搜索就马上调用了本函数,我方没有走棋
Expand Down
3 changes: 0 additions & 3 deletions src/board/chessboard.h
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,6 @@ class Chessboard final {
*/
bool isLegalMove(Move &move) const;

/** 判断当前是否属于残局 */
bool isNotEndgame() const;

/**
* @brief 获得一个局面的重复情况
* @param distance 距离根节点的深度
Expand Down
5 changes: 2 additions & 3 deletions src/evaluate/evaluate.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -139,9 +139,8 @@ qint16 Chessboard::kingSafety_helper(quint8 side, quint8 center,
safety -= 20;
}
// 如果车在底线保护将,给予更大的罚分
index = (PRE_GEN.getRookAttack(middle, this->m_occupancy) &
this->m_bitboards[ROOK + side]).getLastBitIndex();
if (index < 90) safety -= 80;
if (PRE_GEN.getRookAttack(middle, this->m_occupancy) &
this->m_bitboards[ROOK + side]) safety -= 80;
}
break;

Expand Down
168 changes: 78 additions & 90 deletions src/search/searchinstance.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
#include "searchinstance.h"

namespace PikaChess {
/** 搜索的衰减层数 [是否是CUT Node][第几层][第几个走法] */
quint8 REDUCTIONS[2][64][128];

SearchInstance::SearchInstance(const Chessboard &chessboard, HashTable &hashTable)
: m_chessboard { chessboard }, m_hashTable { hashTable } { }

Expand All @@ -14,6 +17,7 @@ void SearchInstance::searchRoot(const qint8 depth) {

qint16 bestScore { LOSS_SCORE };
// 搜索计数器
quint8 moveCount { 0 };
this->m_legalMove = 0;
// 是否被对方将军
bool notInCheck { not this->m_chessboard.getLastMove().isChecked() };
Expand All @@ -22,33 +26,25 @@ void SearchInstance::searchRoot(const qint8 depth) {
while ((move = search.getNextMove()).isVaild()) {
// 如果被将军了就不搜索这一步
if (makeMove(move)) {
++moveCount;
// 不然就获得评分并更新最好的分数
qint16 tryScore;
const HistoryMove &lastMove { this->m_chessboard.getLastMove() };
// 将军延伸,如果将军了对方就多搜几步
qint8 newDepth = lastMove.isChecked() ? depth : depth - 1;
// PVS
if (this->m_legalMove == 0) {
if (moveCount == 1) {
tryScore = -searchFull(LOSS_SCORE, MATE_SCORE, newDepth, NO_NULL);
} else {
// 对于延迟走法的处理,要求没有被将军,没有将军别人,该步不是吃子步
tryScore = bestScore + 1;
if (notInCheck and newDepth not_eq depth and not lastMove.isCapture()) {
// LMR,在层数大于等于3时使用
if (depth >= 3) {
qint8 reduce = 1;
// 靠后的走法采用更加激进的裁剪策略
if (this->m_legalMove >= 6) reduce = depth / 3;
tryScore = -searchFull(-bestScore - 1, -bestScore, newDepth - reduce);
}
// LMP,在层数小于3时使用,要求已经搜索了一定数量的走法
else if (this->m_legalMove >= 3 * depth) tryScore = LOSS_SCORE;
if (depth >= 3 and notInCheck and newDepth not_eq depth and not lastMove.isCapture()) {
tryScore = -searchFull(-bestScore - 1, -bestScore,
newDepth - REDUCTIONS[false][depth][moveCount]);
}
// 如果不满足条件则不衰减层数
else tryScore = -searchFull(-bestScore - 1, -bestScore, newDepth);
if (tryScore > bestScore) {
tryScore = -searchFull(-bestScore - 1, -bestScore, newDepth);
if (tryScore > bestScore) {
tryScore = -searchFull(LOSS_SCORE, -bestScore, newDepth, NO_NULL);
}
tryScore = -searchFull(LOSS_SCORE, -bestScore, newDepth, NO_NULL);
}
}
// 撤销走棋
Expand Down Expand Up @@ -86,76 +82,77 @@ qint16 SearchInstance::searchFull(qint16 alpha, const qint16 beta,
// 如果有重复的情况,直接返回分数
if (repeatScore.has_value()) return repeatScore.value();

// 获得静态评分
qint16 staticEval { this->m_chessboard.staticScore() };

// 不是PV节点
bool notPVNode { beta - alpha <= 1 };

bool notInCheck { not this->m_chessboard.getLastMove().isChecked() };

// 静态评分裁剪
if (depth < 3 and notPVNode and notInCheck and beta - 1 > BAN_SCORE_LOSS) {
// 裁剪的边界
int evalMargin = 40 * depth;

// 如果放弃一定的分值还是超出边界就返回
if (staticEval - evalMargin >= beta) return staticEval - evalMargin;
}

// 当前走法
Move move;
// 尝试置换表裁剪,并得到置换表走法
tryScore = probeHash(alpha, beta, depth, move);
// 置换表裁剪成功
if (tryScore > LOSS_SCORE) return tryScore;

/* 进行空步裁剪,不能连着走两步空步,被将军时不能走空步
残局走空步,需要进行检验,不然会有特别大的风险
根节点的Beta值是"MATE_SCORE",所以不可能发生空步裁剪 */
if (notPVNode and nullOk and notInCheck) {
// 走一步空步
makeNullMove();
// 获得评分,深度减掉空着裁剪推荐的两层,然后本身走了一步空步,还要再减掉一层
tryScore = -searchFull(-beta, 1 - beta, depth - 3, NO_NULL);
// 撤销空步
unMakeNullMove();
// 如果足够好就可以发生截断,残局阶段要注意进行校验
if (tryScore >= beta and (this->m_chessboard.isNotEndgame() or
searchFull(beta - 1, beta, depth - 2, NO_NULL) >= beta)) {
return tryScore;
// 不被将军时可以进行一些裁剪
bool notInCheck { not this->m_chessboard.getLastMove().isChecked() };
bool notPVNode { beta - alpha <= 1 };
if (notInCheck) {
qint16 staticEval { this->m_chessboard.staticScore() };

// 无用裁剪
if (depth < 7 and abs(beta) < WIN_SCORE) {
// 裁剪的边界
quint8 futilityMargin = 40 * depth;

// 如果放弃一定的分值还是超出边界就返回
if (staticEval - futilityMargin >= beta) return staticEval - futilityMargin;
}
}

// 剃刀裁剪
if (notPVNode and notInCheck and depth <= 3) {
// 给静态评价加上第一个边界
tryScore = staticEval + 40;
// 适用于非PV节点的前期裁剪
if (notPVNode) {
// 剃刀裁剪
if (depth <= 3) {
// 给静态评价加上第一个边界
tryScore = staticEval + 40;

// 如果超出边界
if (tryScore < beta) {
// 第一层直接返回评分和静态搜索的最大值
if (depth == 1) return std::max(tryScore, searchQuiescence(alpha, beta));
// 如果超出边界
if (tryScore < beta) {
// 第一层直接返回评分和静态搜索的最大值
if (depth == 1) return std::max(tryScore, searchQuiescence(alpha, beta));

// 其余情况加上第二个边界
tryScore += 60;
// 其余情况加上第二个边界
tryScore += 60;

// 如果还是超出边界
if (tryScore < beta and depth <= 2) {
// 获得静态评分
qint16 newScore { searchQuiescence(alpha, beta) };
// 如果还是超出边界
if (tryScore < beta and depth <= 2) {
// 获得静态评分
qint16 newScore { searchQuiescence(alpha, beta) };

// 如果静态评分也超出边界,返回评分和静态搜索的最大值
if (newScore < beta) return std::max(tryScore, newScore);
}
}
}

// 如果静态评分也超出边界,返回评分和静态搜索的最大值
if (newScore < beta) return std::max(tryScore, newScore);
/* 进行空步裁剪,不能连着走两步空步,被将军时不能走空步,层数较大时,需要进行检验
根节点的Beta值是"MATE_SCORE",所以不可能发生空步裁剪 */
if (nullOk) {
// 走一步空步
makeNullMove();
// 获得评分,深度减掉空着裁剪推荐的两层,然后本身走了一步空步,还要再减掉一层
tryScore = -searchFull(-beta, 1 - beta, depth - 3, NO_NULL);
// 撤销空步
unMakeNullMove();
// 如果足够好就可以发生截断,层数较大时要注意进行校验
if (tryScore >= beta and ((depth < 12 and abs(beta) < WIN_SCORE) or
searchFull(beta - 1, beta, depth - 2, NO_NULL) >= beta)) {
return tryScore;
}
}
}
}

// 内部迭代加深启发,只在PV节点上使用
if (beta - alpha > 1 and depth > 2 and move == INVALID_MOVE) {
tryScore = searchFull(alpha, beta, depth >> 1, NO_NULL);
if (tryScore <= alpha) tryScore = searchFull(LOSS_SCORE, beta, depth >> 1, NO_NULL);
move = this->m_iidMove;
// 适用于PV节点的内部迭代加深启发
else if (depth > 2 and move == INVALID_MOVE) {
tryScore = searchFull(alpha, beta, depth >> 1, NO_NULL);
if (tryScore <= alpha) tryScore = searchFull(LOSS_SCORE, beta, depth >> 1, NO_NULL);
move = this->m_iidMove;
}
}

// 搜索有限状态机
Expand All @@ -169,37 +166,28 @@ qint16 SearchInstance::searchFull(qint16 alpha, const qint16 beta,
Move bestMove { };

// 搜索计数器
quint8 moveSearched { 0 };
quint8 moveCount { 0 };
// 遍历所有走法
while ((move = search.getNextMove()).isVaild()) {
// 如果被将军了就不搜索这一步
if (makeMove(move)) {
++moveCount;
// 不然就获得评分并更新最好的分数
const HistoryMove &lastMove { this->m_chessboard.getLastMove() };
// 将军延伸,如果将军了对方就多搜几步
qint8 newDepth = lastMove.isChecked() ? depth : depth - 1;
// PVS
tryScore = alpha + 1;
// 对于延迟走法的处理,要求没有被将军,没有将军别人,该步不是吃子步
if (notInCheck and newDepth not_eq depth and not lastMove.isCapture() and moveSearched) {
// LMR,在层数大于等于3时使用
if (depth >= 3) {
qint8 reduce = 1;
if (moveSearched >= 6) reduce = depth / 3;
tryScore = -searchFull(-alpha - 1, -alpha, newDepth - reduce);
}
// LMP,在层数小于3时使用,要求已经搜索了一定数量的走法
else if (moveSearched >= 3 * depth) tryScore = LOSS_SCORE;
}
if (tryScore > alpha) {
tryScore = -searchFull(-alpha - 1, -alpha, newDepth);
if (tryScore > alpha and tryScore < beta) {
tryScore = -searchFull(-beta, -alpha, newDepth);
}

// PVS,对于延迟走法的处理,要求没有被将军,没有将军别人,该步不是吃子步
if (depth >= 3 and notInCheck and newDepth not_eq depth and not lastMove.isCapture()) {
tryScore = -searchFull(-alpha - 1, -alpha,
newDepth - REDUCTIONS[notPVNode][depth][moveCount]);
}
// 如果不满足条件就不衰减层数
else tryScore = -searchFull(-alpha - 1, -alpha, newDepth);
if (tryScore > alpha and tryScore < beta) tryScore = -searchFull(-beta, -alpha, newDepth);

// 撤销走棋
unMakeMove();
if (tryScore > LOST_SCORE) ++moveSearched;
if (tryScore > bestScore) {
// 找到最佳走法(但不能确定是Alpha、PV还是Beta走法)
bestScore = tryScore;
Expand Down
13 changes: 13 additions & 0 deletions src/table/pregen.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ extern __m128i BITBOARD_MASK[90];
/** 位棋盘反掩码 */
extern __m128i BITBOARD_NOT_MASK[90];

/** 延迟走法衰减的衰减层数 */
extern quint8 REDUCTIONS[2][64][128];

PreGen::PreGen() {
// 位棋盘掩码初始化
for (quint8 index { 0 }; index < 90; ++index) {
Expand Down Expand Up @@ -81,6 +84,16 @@ PreGen::PreGen() {

// 生成Zobrist值;
genZobristValues();

// 生成LMR的衰减层数数据
for (quint8 depth = 1; depth < 64; ++depth) {
for (quint8 moveCount = 1; moveCount < 128; ++moveCount) {
double reduce = log(depth) * log(moveCount) / 1.95;
REDUCTIONS[true][depth][moveCount] = int(std::round(reduce));
REDUCTIONS[false][depth][moveCount] =
std::max(REDUCTIONS[true][depth][moveCount] - 1, 0);
}
}
}

void PreGen::genRookOccupancy() {
Expand Down

0 comments on commit 1cce6ca

Please sign in to comment.