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

Commit

Permalink
实现了NNUE局面评分
Browse files Browse the repository at this point in the history
  • Loading branch information
PikaCat committed Jun 6, 2022
1 parent 1cce6ca commit 1fc3bfa
Show file tree
Hide file tree
Showing 19 changed files with 959 additions and 646 deletions.
10 changes: 10 additions & 0 deletions ChineseChess.pro
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ QT += core gui network # testlib

greaterThan(QT_MAJOR_VERSION, 4): QT += widgets

QMAKE_LFLAGS_WINDOWS += -Wl,--stack,32000000
QMAKE_CXXFLAGS += -std=gnu++2b -march=native -masm=intel -fopenmp
QMAKE_CXXFLAGS_RELEASE += -Ofast -flto
# The following define makes your compiler emit warnings if you use
Expand All @@ -19,6 +20,13 @@ HEADERS += \
src/GUI/dialog.h \
src/board/bitboard.h \
src/board/chessboard.h \
src/evaluate/accumulator.h \
src/evaluate/evaluate.h \
src/evaluate/layer/clippedrelu.h \
src/evaluate/layer/dense.h \
src/evaluate/layer/featuretransformer.h \
src/evaluate/layer/input.h \
src/evaluate/model.h \
src/global.h \
src/machine/searchmachine.h \
src/machine/searchquiescencemachine.h \
Expand Down Expand Up @@ -59,6 +67,8 @@ INCLUDEPATH += src \
src/search \
src/table \
src/move \
src/evaluate \
src/evaluate/layer \
# test

LIBS += -fopenmp
Expand Down
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.2, 2022-05-26T12:53:40. -->
<!-- Written by QtCreator 7.0.2, 2022-06-06T19:53:51. -->
<qtcreator>
<data>
<variable>EnvironmentId</variable>
Expand Down
37 changes: 23 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@
#### 介绍
+ 个人项目,中国象棋Qt界面与AI象棋引擎
+ 棋盘结构为 **PEXT位棋盘** ,使用CPU中128位寄存器的低90位来存储棋盘,对应C++的数据结构为__m128i
+ 使用了 **POPCNT指令,BMI位操作指令集中的PEXT与TZCNT指令,SSE指令集中的与、或、非、异或、零测试** 等指令来进行走法预生成与快速运算,需要相应的CPU支持
+ 使用了 **POPCNT指令,BMI位操作指令集中的PEXT与TZCNT指令,SSE指令集中的与、或、非、异或、零测试,AVX2指令集** 等指令来进行走法预生成与快速运算,需要相应的CPU支持
+ 引擎算法基于超出边界(Fail-Soft)的AlphaBeta剪枝,使用迭代加深(含内部迭代加深)的搜索方式
+ 在局面评价上使用渐进式的评估方法对进行局面评估,考虑了棋子的子力价值、位置分、王安全分(包括空头炮、炮镇窝心马、沉底炮等危险棋形的评估)
+ 在局面评价上使用NNUE快速更新的神经网络对进行局面评估
+ 支持历史表启发,杀手启发,吃子启发,有良好的走法排序器
+ 支持基于SSE的无锁置换表裁剪、带验证的空着裁剪、落后着法衰减、杀棋步数裁剪、剃刀裁剪、无用裁剪、差值裁剪
+ 支持基于SSE的无锁置换表裁剪、带验证的空着裁剪、落后着法衰减、杀棋步数裁剪、无用裁剪、差值裁剪
+ 支持将军延伸和重复局面检测(支持长将检测和部分长捉检测)
+ 支持主要变例搜索、使用OpenMP与QtConcurrent并发库进行Lazy-SMP多线程搜索
+ 联网的情况下支持ChessDB提供的开局库、对局库和残局库,大约可提升引擎200ELO左右
Expand All @@ -22,21 +22,21 @@
#### 语言标准
+ C++最新标准,开启GNU最新的语言级别扩展特性

#### 引擎棋力(使用云库、CPU:i5-8265U
+ 足以应对一般的纯人,但由于搜索速度和评分函数知识上的缺陷,暂不足以应对其他优秀的象棋软件(如佳佳象棋与象棋旋风)。
#### 引擎棋力(非NNUE版本,使用云库、CPU:i5-8265U
+ 足以应对一般的纯人,但由于搜索速度上的缺陷,暂不足以应对其他优秀的象棋软件(如象棋旋风)。
+ 与一般的象棋引擎的对战评测在b站:
+ 对战悟空象棋引擎:https://www.bilibili.com/video/BV1TF41147Do/
+ 对战象棋小巫师引擎:https://www.bilibili.com/video/BV1va411h7yo/
+ 对战象眼引擎:https://www.bilibili.com/video/BV1Q34y1b7PA/

#### 天天象棋测试(使用云库、CPU:i5-8265U
#### 天天象棋测试(非NNUE版本,使用云库、CPU:i5-8265U
+ 可战胜业8-3纯人,得出本软件ELO大约为2000左右
+ 天天象棋人机对战可以战胜精英级别电脑(天天象棋分析12层),由此可得本软件大致与新版天天象棋分析13层相当。
+ 实战测试结果最高等级如下(该账号仅用于测试软件棋力,由于达到业余9-1后,再往后的测试需要实名认证,鉴于已经达到了测试的目的,所以该账号现已注销):
![评测最高等级](https://images.gitee.com/uploads/images/2021/0823/185211_45f94b91_7628839.jpeg "QQ图片20210823185009.jpg")
+ 更多实战测试的内容在:https://www.bilibili.com/video/BV1eR4y1j777

#### JJ象棋测试(使用云库、CPU:i5-8265U
#### JJ象棋测试(非NNUE版本,使用云库、CPU:i5-8265U
+ 实战测试可战胜特大等级纯人,最高达到荣誉顶级,100盘胜率94%,有1盘掉线,1盘与其他软件作和,4盘输给其他软件,其余与纯人对战都赢了
+ 该账号仅用于测试软件棋力,由于特大等级的小部分人和荣誉顶级的绝大部分人都是软件,由于本软件不具备与其他软件对撕的能力,鉴于已经达到了测试的目的,故不再往后测试
![评测最高等级](https://images.gitee.com/uploads/images/2021/0921/212032_434c1039_7628839.jpeg "Screenshot_2021-09-21-21-16-53-960_cn.jj.chess.mi.jpg")
Expand All @@ -49,12 +49,11 @@

#### 未来愿景
+ 这个引擎目前还有很多不完善的地方((>﹏<)一大堆捏~):
1. 没有任何的审局眼光,虽然有云库不会开局落入飞刀局面,但是中局脱库后极其容易跳水,且无法识别官和局面(如双车对车士象全)。(这点将在NNUE版本推出后极大改善,但NNUE版本什么时候才能出来不确定,要看我什么时候能学完NNUE)
2. 没有发挥出位棋盘该有的速度,相比于数组棋盘提升幅度不是很大,所以对应的程序实现还有很多未被发现的Bug没有解决。
3. 搜索速度不快,剪枝力度不够大,NPS比不上免费的佳佳象棋引擎,更不用说商业引擎了。
4. 没有UCI协议支持,目前无法使用命令模式将引擎与界面解耦。
5. 没有引擎ELO测评平台,如CCRL。
6. 没有测试平台,如fishtest。
1. 没有发挥出位棋盘该有的速度,相比于数组棋盘提升幅度不是很大,所以对应的程序实现还有很多未被发现的Bug没有解决。
2. 搜索速度不快,剪枝力度不够大。
3. 没有UCI协议支持,目前无法使用命令模式将引擎与界面解耦。
4. 没有引擎ELO测评平台,如CCRL。
5. 没有测试平台,如fishtest。

+ 建立这个仓库的初心是看到国际象棋Stockfish引擎的开源仓库及其开源社区支持的强大支持,于是想着能不能在国内也建立一个这样的仓库,让更多象棋引擎爱好者参与引擎的改进,更新,提issue,提pull requests,众人拾柴火焰高。就像Stockfish超过商业引擎Komodo一样,有一天我们也能够媲美象棋旋风。
+ 我曾经看到过一句话,我很喜欢:If you love something, set it free. 来自虚幻引擎的官网。这里的free有两种意思,免费与自由。所以如果你喜欢一样东西,想让它变好,就让它免费吧,让它可以被它人自由获取吧!这也是我为什么要开源的原因,这也是我为什么使用WTFPL的原因。
Expand All @@ -63,13 +62,23 @@
#### 云开局库、残局库
+ https://www.chessdb.cn/query/

#### 特别感谢
+ 特别感谢ianfab编写的NNUE工具链以及Belzedar94提供的权重文件,让皮卡喵象棋引擎搭上了NNUE的时代快车
+ 特别感谢ianfab耐心解答我的解惑,使得皮卡喵NNUE成为可能。https://github.com/ianfab/Fairy-Stockfish/discussions/491
+ 以下是ianfab提供的NNUE工具链:
1. 训练数据生成器:https://github.com/ianfab/variant-nnue-tools
2. NNUE网络训练器:https://github.com/ianfab/variant-nnue-pytorch
+ NNUE的最新参数文件(皮卡喵象棋的nnue文件会与其保持同步更新):https://fairy-stockfish.github.io/nnue/#current-best-nnue-networks

#### 参考文献
1. 象棋百科全书:https://www.xqbase.com/computer.htm
2. 象棋编程维基百科:https://www.chessprogramming.org/Main_Page
3. Shark象棋引擎论文:http://rportal.lib.ntnu.edu.tw/bitstream/20.500.12235/106625/1/n060147070s01.pdf
4. NNUE神经网络手册:https://github.com/glinscott/nnue-pytorch/blob/master/docs/nnue.md

#### 参考代码
1. 象棋小巫师: https://github.com/xqbase/xqwlight
2. 象眼: https://github.com/xqbase/eleeye
3. 国际象棋位棋盘: https://github.com/maksimKorzh/bbc
4. 佳佳象棋:https://github.com/leedavid/NewGG
4. 佳佳象棋:https://github.com/leedavid/NewGG
5. Fairy-Stockfish:https://github.com/ianfab/Fairy-Stockfish
2 changes: 1 addition & 1 deletion app.rc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
IDI_ICON1 ICON "ChessImage/ChessIcon.ico"
IDI_ICON1 ICON "ChessImage/ChessIcon.ico"
109 changes: 67 additions & 42 deletions src/board/chessboard.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ void Chessboard::parseFen(const QString &fen) {
this->m_redOccupancy.clearAllBits();
this->m_blackOccupancy.clearAllBits();
this->m_occupancy.clearAllBits();
this->m_piece = 0;
memset(this->m_helperBoard, EMPTY, sizeof(this->m_helperBoard));

// 分割为棋盘和选边两部分
Expand All @@ -42,6 +43,7 @@ void Chessboard::parseFen(const QString &fen) {
case '/': continue;
default:
if (ch.isNumber()) { count += (ch.toLatin1() - '0'); continue; }
++this->m_piece;
this->m_bitboards[FEN_MAP[ch]].setBit(count);
this->m_helperBoard[count] = FEN_MAP[ch];
break;
Expand All @@ -59,8 +61,21 @@ void Chessboard::parseFen(const QString &fen) {
// 重置步数计数器
this->m_historyMovesCount = 1;

// 调用预计算函数
this->preCalculateScores();
// 刷新双方的初始累加器
Accumulator &acc { this->getLastMove().m_acc };
qint32 featureIndexes[33];

// 刷新对方的累加器
this->m_side ^= OPP_SIDE;
acc.kingPos[this->m_side] = this->m_bitboards[KING + this->m_side].getLastBitIndex();
this->getAllFeatures(featureIndexes);
featureTransformer->refreshAccumulator(acc, this->m_side, featureIndexes);

// 刷新自己的累加器
this->m_side ^= OPP_SIDE;
acc.kingPos[this->m_side] = this->m_bitboards[KING + this->m_side].getLastBitIndex();
this->getAllFeatures(featureIndexes);
featureTransformer->refreshAccumulator(acc, this->m_side, featureIndexes);
}

QString Chessboard::getFen() const {
Expand Down Expand Up @@ -160,6 +175,23 @@ quint8 Chessboard::genNonCapMoves(ValuedMove *moveList) const {
return total;
}

void Chessboard::getAllFeatures(qint32 *featureIndexes) const {
// 获取当前走子方将的位置
quint8 kingPos { this->getLastMove().m_acc.kingPos[this->m_side] };

// 遍历所有位置,提取特征
Bitboard occupancy { this->m_occupancy };

quint8 index;
while ((index = occupancy.getLastBitIndex()) < 90) {
occupancy.clearBit(index);
*featureIndexes++ = FeatureIndex(this->m_side, index, this->m_helperBoard[index], kingPos);
}

// 结束标志
*featureIndexes = -1;
}

bool Chessboard::isChecked() const {
// 获取对方的选边
quint8 oppSide = this->m_side ^ OPP_SIDE;
Expand Down Expand Up @@ -247,19 +279,11 @@ std::optional<qint16> Chessboard::getRepeatScore(quint8 distance) const {
if (move->zobrist() == this->m_zobrist) {
myFlag = (myFlag & 0x3fff) == 0 ? myFlag : 0x3fff;
oppFlag = (oppFlag & 0x3fff) == 0 ? oppFlag : 0x3fff;
// 我方长打返回负分,对方长打返回正分
qint16 score { 0 };
if (myFlag > oppFlag) score = BAN_SCORE_LOSS + distance;
else if (myFlag < oppFlag) score = BAN_SCORE_MATE - distance;

/* 如果双方都长打或者双方都没有长打但是有重复局面就返回和棋的分数
* 但无论如何都要使得和棋对于第一层的那一方来说是不利的,是负分
* distance & 1 的作用是确定现在在那一层
* 说明evaluate的那一层和第一层是同一方
* 同一方返回负值,不同方返回正值,这样正值上到第一层就会变成负值 */
if (score == 0) return distance & 1 ? DRAW_SCORE : -DRAW_SCORE;
// 有一方长打
else return score;

// 我方长打返回负分,对方长打返回正分,双方长打返回0分
if (myFlag > oppFlag) return BAN_SCORE_LOSS + distance;
else if (myFlag < oppFlag) return BAN_SCORE_MATE - distance;
else return 0;
}
}
// 如果是对方,更新对方的将军信息
Expand All @@ -282,6 +306,8 @@ bool Chessboard::makeMove(Move &move) {
if (RED == this->m_side) this->m_blackOccupancy.clearBit(move.to());
else this->m_redOccupancy.clearBit(move.to());
this->m_bitboards[move.victim()].clearBit(move.to());
// 存活的子少了一个
--this->m_piece;
// 注意,这里不用移除occupancy中move.to()位,因为攻击的棋子会移动过来
}

Expand All @@ -305,6 +331,9 @@ bool Chessboard::makeMove(Move &move) {
this->m_helperBoard[move.from()] = EMPTY;
this->m_helperBoard[move.to()] = move.chess();

// 获取上一个累加器
const Accumulator &lastAcc { this->getLastMove().m_acc };

// 在历史走法表中记录这一个走法
HistoryMove &historyMove { this->m_historyMoves[this->m_historyMovesCount++] };

Expand All @@ -318,27 +347,25 @@ bool Chessboard::makeMove(Move &move) {
this->m_zobrist ^= PRE_GEN.getSideZobrist();
this->m_zobrist ^= PRE_GEN.getZobrist(move.chess(), move.from());
this->m_zobrist ^= PRE_GEN.getZobrist(move.chess(), move.to());

if (move.isCapture()) {
// 吃子步需要把被吃的子的zobrist去除
this->m_zobrist ^= PRE_GEN.getZobrist(move.victim(), move.to());
// 顺便计算吃子得分
if (RED == this->m_side) this->m_blackScore -= VALUE[move.victim()][move.to()];
else this->m_redScore -= VALUE[move.victim()][move.to()];
}

// 计算得分
if (RED == this->m_side) {
this->m_redScore -= VALUE[move.chess()][move.from()];
this->m_redScore += VALUE[move.chess()][move.to()];
} else {
this->m_blackScore -= VALUE[move.chess()][move.from()];
this->m_blackScore += VALUE[move.chess()][move.to()];
// 吃子步需要把被吃的子的zobrist去除
if (move.isCapture()) this->m_zobrist ^= PRE_GEN.getZobrist(move.victim(), move.to());

// 如果走动的是将,就刷新自己的累加器
if (move.chess() == KING + this->m_side) {
historyMove.m_acc.kingPos[this->m_side] = move.to();
qint32 featureIndexes[33];
this->getAllFeatures(featureIndexes);
featureTransformer->refreshAccumulator(historyMove.m_acc, this->m_side, featureIndexes);
}
// 否则就更新自己的累加器
else featureTransformer->updateAccumulator(lastAcc, historyMove.m_acc, this->m_side, move);

// 换边
this->m_side ^= OPP_SIDE;

// 不要忘记另一边累加器的也要更新
featureTransformer->updateAccumulator(lastAcc, historyMove.m_acc, this->m_side, move);

// 补充对应的将军捉子信息
if (isChecked()) historyMove.setChecked();
else historyMove.setChase(this->getChase());
Expand All @@ -361,26 +388,18 @@ void Chessboard::unMakeMove() {
// 还原原来的Zobrist值
this->m_zobrist = move.zobrist();

// 还原原来的得分
if (RED == this->m_side) {
this->m_redScore -= VALUE[move.chess()][move.to()];
this->m_redScore += VALUE[move.chess()][move.from()];
if (move.isCapture()) this->m_blackScore += VALUE[move.victim()][move.to()];
} else {
this->m_blackScore -= VALUE[move.chess()][move.to()];
this->m_blackScore += VALUE[move.chess()][move.from()];
if (move.isCapture()) this->m_redScore += VALUE[move.victim()][move.to()];
}

// 撤销这个走法
undoMove(move);
}

void Chessboard::makeNullMove() {
// 获取历史走法表项,并将自增走法历史表的大小
const Accumulator &lastAcc { this->getLastMove().m_acc };
HistoryMove &move = this->m_historyMoves[this->m_historyMovesCount++];
// 设置空步信息
move.setNullMove();
// 复制上一个累加器的内容
this->getLastMove().m_acc.copyFrom(lastAcc);
// 换边
this->m_side ^= OPP_SIDE;
// 计算新的Zobrist值
Expand All @@ -400,6 +419,10 @@ void Chessboard::updateHistoryValue(const Move &move, quint8 depth) {
this->m_historyTable.updateValue(move, depth);
}

HistoryMove &Chessboard::getLastMove() {
return this->m_historyMoves[this->m_historyMovesCount - 1];
}

const HistoryMove &Chessboard::getLastMove() const {
return this->m_historyMoves[this->m_historyMovesCount - 1];
}
Expand All @@ -421,6 +444,8 @@ void Chessboard::undoMove(const Move &move) {
if (RED == this->m_side) this->m_blackOccupancy.setBit(move.to());
else this->m_redOccupancy.setBit(move.to());
this->m_bitboards[move.victim()].setBit(move.to());
// 恢复存活子
++this->m_piece;
// 注意,如果是吃子步则不用清空to,因为这里原来有一个棋子
}

Expand Down
28 changes: 11 additions & 17 deletions src/board/chessboard.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#include "historymove.h"
#include "valuedmove.h"
#include "historytable.h"
#include "evaluate.h"

namespace PikaChess {
class Chessboard final {
Expand All @@ -26,6 +27,12 @@ class Chessboard final {
*/
quint8 genNonCapMoves(ValuedMove *moveList) const;

/**
* @brief 获取当前走子方的所有激活的特征
* @param featureIndexes 存放激活的特征的数组
*/
void getAllFeatures(qint32 *featureIndexes) const;

/** 当前是否被将军 */
bool isChecked() const;

Expand Down Expand Up @@ -75,6 +82,7 @@ class Chessboard final {
void updateHistoryValue(const Move &move, quint8 depth);

/** 获得最后一个走法 */
HistoryMove &getLastMove();
const HistoryMove &getLastMove() const;

void setSide(quint8 newSide);
Expand All @@ -83,14 +91,8 @@ class Chessboard final {

quint8 side() const;

/** 评价分预计算,根据局面情况预计算局面分,引擎棋力的主要来源 */
void preCalculateScores();

/** 局面的静态评分,只包括子力的位置分 */
qint16 staticScore() const;

/** 获得当前局面的评分 */
qint16 score() const;
qint16 score();

protected:
/**
Expand All @@ -105,13 +107,6 @@ class Chessboard final {
*/
quint16 getChase();

/** 王安全分,包括空头炮,炮镇窝心马,沉底炮,车封锁将门 */
qint16 kingSafety() const;

/** 计算王安全分的帮助函数 */
qint16 kingSafety_helper(quint8 side, quint8 center,
quint8 left, quint8 middle, quint8 right) const;

private:
/** 用来辅助走法生成的辅助数组棋盘 */
quint8 m_helperBoard[90];
Expand All @@ -129,9 +124,8 @@ class Chessboard final {
/** 当前局面的Zobrist值 */
quint64 m_zobrist;

/** 当前局面的红黑方得分 */
quint16 m_redScore;
quint16 m_blackScore;
/** 当前局面所剩的子力个数 */
quint8 m_piece;

/** 走棋的历史记录 */
HistoryMove m_historyMoves[256];
Expand Down
Loading

0 comments on commit 1fc3bfa

Please sign in to comment.