├── app.rc ├── Images ├── bg.jpg ├── min.ico ├── close.ico ├── min_hover.bmp ├── min_press.bmp ├── close_hover.ico └── close_press.ico ├── ChessImage ├── ba.ico ├── bb.ico ├── bc.ico ├── bk.ico ├── bn.ico ├── bp.ico ├── br.ico ├── mask.png ├── ra.ico ├── rb.ico ├── rc.ico ├── rk.ico ├── rn.ico ├── rp.ico ├── rr.ico ├── BOARD.BMP ├── mask2.png └── ChessIcon.ico ├── src ├── main.cpp ├── move │ ├── valuedmove.cpp │ ├── valuedmove.h │ ├── historymove.cpp │ ├── historymove.h │ ├── move.cpp │ └── move.h ├── table │ ├── historytable.cpp │ ├── killertable.cpp │ ├── historytable.h │ ├── killertable.h │ ├── hashtable.h │ ├── hashtable.cpp │ ├── pregen.h │ └── pregen.cpp ├── machine │ ├── searchquiescencemachine.h │ ├── searchmachine.h │ ├── searchquiescencemachine.cpp │ └── searchmachine.cpp ├── evaluate │ ├── layer │ │ ├── input.h │ │ ├── clippedrelu.h │ │ ├── dense.h │ │ └── featuretransformer.h │ ├── accumulator.h │ ├── evaluate.h │ ├── evaluate.cpp │ └── model.h ├── search │ ├── chessengine.h │ ├── searchinstance.h │ ├── chessengine.cpp │ └── searchinstance.cpp ├── board │ ├── bitboard.h │ ├── bitboard.cpp │ ├── chessboard.h │ └── chessboard.cpp ├── GUI │ ├── dialog.h │ └── dialog.ui └── global.h ├── LICENSE ├── test ├── perfttest.h └── perfttest.cpp ├── res.qrc ├── ChineseChess.pro ├── README.md └── ChineseChess.pro.user /app.rc: -------------------------------------------------------------------------------- 1 | IDI_ICON1 ICON "ChessImage/ChessIcon.ico" -------------------------------------------------------------------------------- /Images/bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PikaCat-OuO/ChineseChess/HEAD/Images/bg.jpg -------------------------------------------------------------------------------- /Images/min.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PikaCat-OuO/ChineseChess/HEAD/Images/min.ico -------------------------------------------------------------------------------- /Images/close.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PikaCat-OuO/ChineseChess/HEAD/Images/close.ico -------------------------------------------------------------------------------- /ChessImage/ba.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PikaCat-OuO/ChineseChess/HEAD/ChessImage/ba.ico -------------------------------------------------------------------------------- /ChessImage/bb.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PikaCat-OuO/ChineseChess/HEAD/ChessImage/bb.ico -------------------------------------------------------------------------------- /ChessImage/bc.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PikaCat-OuO/ChineseChess/HEAD/ChessImage/bc.ico -------------------------------------------------------------------------------- /ChessImage/bk.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PikaCat-OuO/ChineseChess/HEAD/ChessImage/bk.ico -------------------------------------------------------------------------------- /ChessImage/bn.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PikaCat-OuO/ChineseChess/HEAD/ChessImage/bn.ico -------------------------------------------------------------------------------- /ChessImage/bp.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PikaCat-OuO/ChineseChess/HEAD/ChessImage/bp.ico -------------------------------------------------------------------------------- /ChessImage/br.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PikaCat-OuO/ChineseChess/HEAD/ChessImage/br.ico -------------------------------------------------------------------------------- /ChessImage/mask.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PikaCat-OuO/ChineseChess/HEAD/ChessImage/mask.png -------------------------------------------------------------------------------- /ChessImage/ra.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PikaCat-OuO/ChineseChess/HEAD/ChessImage/ra.ico -------------------------------------------------------------------------------- /ChessImage/rb.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PikaCat-OuO/ChineseChess/HEAD/ChessImage/rb.ico -------------------------------------------------------------------------------- /ChessImage/rc.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PikaCat-OuO/ChineseChess/HEAD/ChessImage/rc.ico -------------------------------------------------------------------------------- /ChessImage/rk.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PikaCat-OuO/ChineseChess/HEAD/ChessImage/rk.ico -------------------------------------------------------------------------------- /ChessImage/rn.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PikaCat-OuO/ChineseChess/HEAD/ChessImage/rn.ico -------------------------------------------------------------------------------- /ChessImage/rp.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PikaCat-OuO/ChineseChess/HEAD/ChessImage/rp.ico -------------------------------------------------------------------------------- /ChessImage/rr.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PikaCat-OuO/ChineseChess/HEAD/ChessImage/rr.ico -------------------------------------------------------------------------------- /ChessImage/BOARD.BMP: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PikaCat-OuO/ChineseChess/HEAD/ChessImage/BOARD.BMP -------------------------------------------------------------------------------- /ChessImage/mask2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PikaCat-OuO/ChineseChess/HEAD/ChessImage/mask2.png -------------------------------------------------------------------------------- /Images/min_hover.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PikaCat-OuO/ChineseChess/HEAD/Images/min_hover.bmp -------------------------------------------------------------------------------- /Images/min_press.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PikaCat-OuO/ChineseChess/HEAD/Images/min_press.bmp -------------------------------------------------------------------------------- /ChessImage/ChessIcon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PikaCat-OuO/ChineseChess/HEAD/ChessImage/ChessIcon.ico -------------------------------------------------------------------------------- /Images/close_hover.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PikaCat-OuO/ChineseChess/HEAD/Images/close_hover.ico -------------------------------------------------------------------------------- /Images/close_press.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PikaCat-OuO/ChineseChess/HEAD/Images/close_press.ico -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | #include "dialog.h" 2 | 3 | #include 4 | 5 | int main(int argc, char *argv[]) { 6 | QGuiApplication::setHighDpiScaleFactorRoundingPolicy(Qt::HighDpiScaleFactorRoundingPolicy::Floor); 7 | QApplication a(argc, argv); 8 | Dialog w; 9 | w.show(); 10 | return a.exec(); 11 | } 12 | -------------------------------------------------------------------------------- /src/move/valuedmove.cpp: -------------------------------------------------------------------------------- 1 | #include "valuedmove.h" 2 | 3 | namespace PikaChess { 4 | qint64 ValuedMove::score() const { return this->m_score; } 5 | 6 | void ValuedMove::setScore(qint64 score) { this->m_score = score; } 7 | 8 | bool operator<(const ValuedMove &lhs, const ValuedMove &rhs) { return lhs.m_score > rhs.m_score; } 9 | } 10 | -------------------------------------------------------------------------------- /src/table/historytable.cpp: -------------------------------------------------------------------------------- 1 | #include "historytable.h" 2 | 3 | namespace PikaChess { 4 | quint64 HistoryTable::getValue(const Move &move) const { 5 | return this->m_historyValues[move.chess()][move.to()]; 6 | } 7 | 8 | void HistoryTable::updateValue(const Move &move, quint8 depth) { 9 | this->m_historyValues[move.chess()][move.to()] += depth * depth; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/move/valuedmove.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "move.h" 3 | 4 | namespace PikaChess { 5 | class ValuedMove : public Move { 6 | public: 7 | /** 8 | * @brief 设置该步的分值 9 | * @param 该步的分值 10 | */ 11 | void setScore(qint64 score); 12 | 13 | qint64 score() const; 14 | 15 | /** 比较函数,用于比较两个走法分值的大小 */ 16 | friend bool operator<(const ValuedMove &lhs, const ValuedMove &rhs); 17 | 18 | private: 19 | /** 该步的分值,用于走法排序 */ 20 | qint64 m_score; 21 | }; 22 | } 23 | -------------------------------------------------------------------------------- /src/table/killertable.cpp: -------------------------------------------------------------------------------- 1 | #include "killertable.h" 2 | 3 | namespace PikaChess { 4 | Move &KillerTable::getKiller(const quint8 distance, const quint8 i) { 5 | return this->m_killerMoves[distance][i]; 6 | } 7 | 8 | void KillerTable::updateKiller(const quint8 distance, const Move &move) { 9 | if (this->m_killerMoves[distance][0] not_eq move) { 10 | this->m_killerMoves[distance][1] = this->m_killerMoves[distance][0]; 11 | this->m_killerMoves[distance][0] = move; 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 2 | Version 2, December 2004 3 | 4 | Copyright (C) 2004 Sam Hocevar 5 | 6 | Everyone is permitted to copy and distribute verbatim or modified 7 | copies of this license document, and changing it is allowed as long 8 | as the name is changed. 9 | 10 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 11 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 12 | 13 | 0. You just DO WHAT THE FUCK YOU WANT TO. 14 | -------------------------------------------------------------------------------- /src/table/historytable.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "global.h" 3 | #include "move.h" 4 | 5 | namespace PikaChess { 6 | class HistoryTable { 7 | public: 8 | /** 9 | * @brief 返回一个走法的历史分值 10 | * @param move 一个走法 11 | * @return 该走法的历史分值 12 | */ 13 | quint64 getValue(const Move &move) const; 14 | 15 | /** 16 | * @brief 更新一个走法的历史表值 17 | * @param move 需要更新历史分值的走法 18 | * @param depth 当前的深度 19 | */ 20 | void updateValue(const Move &move, quint8 depth); 21 | 22 | private: 23 | quint64 m_historyValues[14][90] { }; 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /src/table/killertable.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "move.h" 3 | 4 | namespace PikaChess { 5 | class KillerTable { 6 | public: 7 | /** 8 | * @brief 获得杀手走法 9 | * @param distance 距离根节点的距离 10 | * @param i 获取哪一个杀手走法 11 | * @return 一个杀手走法 12 | */ 13 | Move &getKiller(const quint8 distance, const quint8 i); 14 | 15 | /** 16 | * @brief 更新杀手走法 17 | * @param distance 距离根节点的距离 18 | * @param move 一个杀手走法 19 | */ 20 | void updateKiller(const quint8 distance, const Move &move); 21 | 22 | private: 23 | /** 杀手走法表 */ 24 | Move m_killerMoves[256][2] { }; 25 | }; 26 | } 27 | -------------------------------------------------------------------------------- /src/machine/searchquiescencemachine.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "chessboard.h" 3 | 4 | namespace PikaChess { 5 | class SearchQuiescenceMachine final { 6 | public: 7 | /** 8 | * @brief 静态搜索状态机的构造函数 9 | * @param chessboard 当前的棋盘 10 | */ 11 | SearchQuiescenceMachine(const Chessboard &chessboard, bool notInCheck); 12 | 13 | /** 返回下一个走法 */ 14 | Move getNextMove(); 15 | 16 | private: 17 | /** 当前的棋盘 */ 18 | const Chessboard &m_chessboard; 19 | 20 | /** 状态机当前的状态 */ 21 | quint8 m_phase { PHASE_CAPTURE_GEN }; 22 | 23 | /** 当前局面是否不被将军 */ 24 | bool m_notInCheck; 25 | 26 | /** 现在正在遍历第几个走法 */ 27 | quint8 m_nowMove { 0 }; 28 | /** 总共有几个走法 */ 29 | quint8 m_totalMoves; 30 | /** 所有的走法 */ 31 | ValuedMove m_moveList[111]; 32 | }; 33 | } 34 | -------------------------------------------------------------------------------- /src/move/historymove.cpp: -------------------------------------------------------------------------------- 1 | #include "historymove.h" 2 | 3 | namespace PikaChess { 4 | quint64 HistoryMove::zobrist() const { return this->m_zobrist; } 5 | 6 | bool HistoryMove::isChecked() const { return this->m_flag & 0x4000; } 7 | 8 | bool HistoryMove::isNullMove() const { return this->m_flag & 0x8000; } 9 | 10 | quint16 HistoryMove::getFlag() const { return this->m_flag; } 11 | 12 | void HistoryMove::setChase(quint16 chessFlag) { this->m_flag = chessFlag; } 13 | 14 | void HistoryMove::setChecked() { this->m_flag = 0x4000; } 15 | 16 | void HistoryMove::setNullMove() { this->m_flag = 0x8000; } 17 | 18 | void HistoryMove::setZobrist(quint64 zobrist) { this->m_zobrist = zobrist; } 19 | 20 | void HistoryMove::setMove(const Move &move) { 21 | this->m_chess = move.chess(); 22 | this->m_victim = move.victim(); 23 | this->m_fromTo = move.fromTo(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/evaluate/layer/input.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "global.h" 3 | 4 | namespace PikaChess { 5 | /** 神经网络的输入层 <这层网络的输出张量维度1 x OutDims> */ 6 | template 7 | class Input { 8 | public: 9 | /** 输出类型 */ 10 | using OutputType = quint8; 11 | 12 | /** 输出维度 */ 13 | static constexpr quint32 OutputDimensions = OutDims; 14 | 15 | /** 输入层不需要缓冲区 */ 16 | static constexpr quint32 BufferSize = 0; 17 | 18 | /** NNUE网络文件中嵌入的哈希值 */ 19 | static constexpr quint32 getHashValue() { 20 | quint32 hashValue = 0xEC42E90Du; 21 | hashValue ^= OutputDimensions; 22 | return hashValue; 23 | } 24 | 25 | /** 读取网络的权重,输入层没有权重,直接返回 */ 26 | bool readParameters(std::istream&) { return true; } 27 | 28 | /** 前向传播,直接将输入特征返回即可 */ 29 | const OutputType *propagate(const quint8 *transformedFeatures, char*) const { 30 | return transformedFeatures; 31 | } 32 | }; 33 | } 34 | -------------------------------------------------------------------------------- /test/perfttest.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include "searchmachine.h" 4 | 5 | namespace PikaChess { 6 | /** 测试用例,如第1层44个,第2层1920个,则测试用例为[(1, 44), (2, 1920)] */ 7 | using TestCases = QVector>; 8 | 9 | class PerftTest : public QObject { 10 | Q_OBJECT 11 | private slots: 12 | void position1(); 13 | void position2(); 14 | void position3(); 15 | void position4(); 16 | void position5(); 17 | void position6(); 18 | void position7(); 19 | void position8(); 20 | void position9(); 21 | void position10(); 22 | void position11(); 23 | 24 | protected: 25 | /** 26 | * @brief 执行perft测试 27 | * @param testCases 所有的测试数据 28 | */ 29 | void runPerftTest(const QString &fen, const TestCases &testCases); 30 | void perft(quint8 depth); 31 | 32 | private: 33 | /** 总共走过的perft节点数 */ 34 | quint64 m_nodes; 35 | /** 棋盘 */ 36 | Chessboard m_chessboard; 37 | }; 38 | } 39 | -------------------------------------------------------------------------------- /src/evaluate/accumulator.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "model.h" 4 | 5 | namespace PikaChess { 6 | /** 累加器,记录着特征转换层的输出值 */ 7 | struct Accumulator { 8 | /** 双方将的位置 */ 9 | quint8 kingPos[8]; 10 | /** 特征转换层的输出,这些值将被输入到全连接层1 */ 11 | alignas(CACHE_LINE_SIZE) qint16 accumulation[8][512]; 12 | /** PSQT部分的输出值,这些值将直接用于评分 */ 13 | alignas(CACHE_LINE_SIZE) qint32 psqtAccumulation[8][PSQT_BUCKETS]; 14 | 15 | void copyFrom(const Accumulator &acc) { 16 | memmove(accumulation[RED], acc.accumulation[RED], sizeof(accumulation[RED])); 17 | memmove(accumulation[BLACK], acc.accumulation[BLACK], sizeof(accumulation[BLACK])); 18 | memmove(psqtAccumulation[RED], acc.psqtAccumulation[RED], sizeof(psqtAccumulation[RED])); 19 | memmove(psqtAccumulation[BLACK], acc.psqtAccumulation[BLACK], 20 | sizeof(psqtAccumulation[BLACK])); 21 | kingPos[RED] = acc.kingPos[RED]; 22 | kingPos[BLACK] = acc.kingPos[BLACK]; 23 | } 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /src/machine/searchmachine.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "chessboard.h" 3 | 4 | namespace PikaChess { 5 | class SearchMachine final { 6 | public: 7 | /** 8 | * @brief 搜索状态机的构造函数 9 | * @param chessboard 当前的棋盘 10 | * @param hashMove 当前的哈希表走法 11 | * @param killerMove1 杀手走法1 12 | * @param killerMove2 杀手走法2 13 | */ 14 | SearchMachine(const Chessboard &chessboard, const Move &hashMove, 15 | Move &killerMove1, Move &killerMove2); 16 | 17 | /** 返回下一个走法 */ 18 | Move getNextMove(); 19 | 20 | protected: 21 | /** 当前的棋盘 */ 22 | const Chessboard &m_chessboard; 23 | 24 | /** 状态机当前的状态 */ 25 | quint8 m_phase { PHASE_HASH }; 26 | 27 | /** 现在正在遍历第几个走法 */ 28 | quint8 m_nowMove { 0 }; 29 | /** 总共有几个走法 */ 30 | quint8 m_totalMoves; 31 | /** 所有的走法 */ 32 | ValuedMove m_moveList[111]; 33 | 34 | /** 置换表走法, */ 35 | const Move &m_hashMove; 36 | /** 两个杀手走法 */ 37 | Move &m_killerMove1, &m_killerMove2; 38 | }; 39 | } 40 | -------------------------------------------------------------------------------- /res.qrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | Images/bg.jpg 4 | Images/min_press.bmp 5 | Images/close.ico 6 | Images/close_press.ico 7 | Images/min.ico 8 | ChessImage/BOARD.BMP 9 | ChessImage/ba.ico 10 | ChessImage/bb.ico 11 | ChessImage/bc.ico 12 | ChessImage/bk.ico 13 | ChessImage/bn.ico 14 | ChessImage/bp.ico 15 | ChessImage/br.ico 16 | ChessImage/ra.ico 17 | ChessImage/rb.ico 18 | ChessImage/rc.ico 19 | ChessImage/rk.ico 20 | ChessImage/rn.ico 21 | ChessImage/rp.ico 22 | ChessImage/rr.ico 23 | ChessImage/mask2.png 24 | ChessImage/mask.png 25 | ChessImage/ChessIcon.ico 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/search/chessengine.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "searchinstance.h" 3 | 4 | namespace PikaChess { 5 | class ChessEngine { 6 | public: 7 | ChessEngine(); 8 | 9 | void reset(); 10 | 11 | void search(); 12 | 13 | /** 获得当前搜索的深度 */ 14 | quint8 currentDepth() const; 15 | 16 | /** 获得当前局面的最好的分数 */ 17 | qint16 bestScore() const; 18 | 19 | /** 获得当前局面的最好的走法 */ 20 | Move bestMove() const; 21 | 22 | /** 设置搜索的时间 */ 23 | void setSearchTime(clock_t searchTime); 24 | 25 | /** 走一步 */ 26 | bool makeMove(Move &move); 27 | 28 | /** 撤销一步 */ 29 | void unMakeMove(); 30 | 31 | /** 获得重复局面得分 */ 32 | std::optional getRepeatScore(); 33 | 34 | void setSide(quint8 side); 35 | 36 | quint8 side() const; 37 | 38 | QString fen() const; 39 | 40 | private: 41 | /** 当前搜索的深度 */ 42 | quint8 m_currentDepth; 43 | 44 | /** 当前局面下最好的分数 */ 45 | qint16 m_bestScore; 46 | 47 | /** 当前局面下最好的走法 */ 48 | Move m_bestMove; 49 | 50 | /** 设计搜索时间 */ 51 | clock_t m_searchTime { 3000 }; 52 | 53 | /** 当前的棋盘 */ 54 | Chessboard m_chessboard; 55 | 56 | /** 置换表 */ 57 | HashTable m_hashTable; 58 | }; 59 | } 60 | -------------------------------------------------------------------------------- /src/move/historymove.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "move.h" 3 | #include "bitboard.h" 4 | #include "accumulator.h" 5 | 6 | namespace PikaChess { 7 | class HistoryMove : public Move { 8 | public: 9 | quint64 zobrist() const; 10 | 11 | /** 12 | * @brief 该步是否为空步 13 | */ 14 | bool isNullMove() const; 15 | 16 | /** 17 | * @brief 该步是否为将军步 18 | */ 19 | bool isChecked() const; 20 | 21 | /** 22 | * @brief 获取该步的flag 23 | * @return 该步的flag 24 | */ 25 | quint16 getFlag() const; 26 | 27 | /** 28 | * @brief 设置该步是否为捉子步 29 | * @param 被捉的棋子 30 | */ 31 | void setChase(quint16 chessFlag); 32 | 33 | /** 34 | * @brief 设置该步为将军步 35 | */ 36 | void setChecked(); 37 | 38 | /** 39 | * @brief 设置该步为空步 40 | */ 41 | void setNullMove(); 42 | 43 | /** 44 | * @brief 设置该步的Zobrist值 45 | * @param zobrist 走该步之前的Zobrist值 46 | */ 47 | void setZobrist(quint64 zobrist); 48 | 49 | /** 设置一个走法 */ 50 | void setMove(const Move &move); 51 | 52 | /** 该步对应的累加器 */ 53 | Accumulator m_acc; 54 | 55 | private: 56 | /** 走该步之前的Zobrist值 */ 57 | quint64 m_zobrist; 58 | 59 | /** 该步的标志信息,第16位表示是否是空步,第15位表示是否为将军,后面14位表示捉的棋子 */ 60 | quint16 m_flag { 0x8000 }; 61 | }; 62 | } 63 | -------------------------------------------------------------------------------- /src/move/move.cpp: -------------------------------------------------------------------------------- 1 | #include "move.h" 2 | 3 | namespace PikaChess { 4 | Move INVALID_MOVE; 5 | 6 | quint8 Move::chess() const { return this->m_chess; } 7 | 8 | quint8 Move::victim() const { return this->m_victim; } 9 | 10 | quint8 Move::from() const { return this->m_from; } 11 | 12 | quint8 Move::to() const { return this->m_to; } 13 | 14 | quint16 Move::fromTo() const { return this->m_fromTo; } 15 | 16 | bool Move::isVaild() const { return this->m_fromTo; } 17 | 18 | bool Move::isCapture() const { return this->m_victim not_eq EMPTY; } 19 | 20 | void Move::setChess(quint8 chess) { this->m_chess = chess; } 21 | 22 | void Move::setVictim(quint8 victim) { this->m_victim = victim; } 23 | 24 | void Move::setMove(const quint8 from, const quint8 to) { 25 | this->m_from = from; 26 | this->m_to = to; 27 | } 28 | 29 | void Move::setMove(const quint8 chess, const quint8 victim, const quint8 from, const quint8 to) { 30 | this->m_chess = chess; 31 | this->m_victim = victim; 32 | this->m_from = from; 33 | this->m_to = to; 34 | } 35 | 36 | bool operator==(const Move &lhs, const Move &rhs) { return lhs.m_fromTo == rhs.m_fromTo; } 37 | 38 | bool operator!=(const Move &lhs, const Move &rhs) { return lhs.m_fromTo not_eq rhs.m_fromTo; } 39 | } 40 | -------------------------------------------------------------------------------- /src/move/move.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "global.h" 3 | 4 | namespace PikaChess { 5 | class Move { 6 | public: 7 | quint8 from() const; 8 | quint8 to() const; 9 | quint16 fromTo() const; 10 | 11 | quint8 chess() const; 12 | quint8 victim() const; 13 | 14 | /** 15 | * @brief 该步是否合法 16 | */ 17 | bool isVaild() const; 18 | 19 | /** 20 | * @brief 该步是否为吃子走法 21 | */ 22 | bool isCapture() const; 23 | 24 | void setChess(quint8 chess); 25 | 26 | void setVictim(quint8 victim); 27 | 28 | /** 29 | * @brief 设置一个走法 30 | * @param from 从哪里来 31 | * @param to 到哪里去 32 | */ 33 | void setMove(const quint8 from, const quint8 to); 34 | 35 | /** 36 | * @brief 设置一个走法 37 | * @param chess 哪一个棋子 38 | * @param victim 吃掉了什么棋子 39 | * @param from 从哪里来 40 | * @param to 到哪里去 41 | */ 42 | void setMove(const quint8 chess, const quint8 victim, const quint8 from, const quint8 to); 43 | 44 | /** 比较函数,用于比较两个走法是否相同 */ 45 | friend bool operator==(const Move &lhs, const Move &rhs); 46 | 47 | /** 比较函数,用于比较两个走法是否相同 */ 48 | friend bool operator!=(const Move &lhs, const Move &rhs); 49 | 50 | protected: 51 | /** 哪一个棋子 */ 52 | quint8 m_chess; 53 | /** 吃掉了什么棋子 */ 54 | quint8 m_victim; 55 | 56 | union { 57 | struct { 58 | /** 从哪里来 */ 59 | quint8 m_from; 60 | /** 到哪里去 */ 61 | quint8 m_to; 62 | }; 63 | quint16 m_fromTo; 64 | }; 65 | }; 66 | /** 默认的无效走法 */ 67 | extern Move INVALID_MOVE; 68 | } 69 | -------------------------------------------------------------------------------- /src/table/hashtable.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "move.h" 3 | 4 | namespace PikaChess { 5 | union HashItem { 6 | struct { 7 | /** 走该走法前对应的Zobrist,用于校验 */ 8 | quint64 m_zobrist; 9 | /** 走法 */ 10 | Move m_move; 11 | /** 该走法对应的搜索分数 */ 12 | qint16 m_score; 13 | /** 记录该项时所处的深度 */ 14 | qint8 m_depth; 15 | /** 该走法对应的类型(ALPHA,PV,BETA) */ 16 | quint8 m_hashFlag; 17 | }; 18 | __m128i m_data; 19 | 20 | /** 构造函数 */ 21 | HashItem() = default; 22 | /** 复制构造 */ 23 | HashItem(volatile const __m128i &data); 24 | }; 25 | 26 | class HashTable { 27 | public: 28 | /** 29 | * @brief 用于从置换表中获取一个走法 30 | * @param distance 离根节点的距离 31 | * @param zobrist 当前局面对应的Zobrist值 32 | * @param alpha 33 | * @param beta 34 | * @param depth 深度 35 | * @param hashMove 置换表走法返回时存放的位置 36 | * @return 对应走法的置换表分数 37 | */ 38 | qint16 probeHash(quint8 distance, quint64 zobrist, 39 | qint16 alpha, qint16 beta, 40 | qint8 depth, Move &hashMove); 41 | 42 | /** 43 | * @brief 将一个走法保存到置换表中 44 | * @param distance 离根节点的距离 45 | * @param zobrist 当前局面对应的Zobrist值 46 | * @param hashFlag 该走法对应的类型(ALPHA,PV,BETA) 47 | * @param score 这个走法对应的搜索分数 48 | * @param depth 深度信息 49 | * @param move 走法 50 | */ 51 | void recordHash(quint8 distance, quint64 zobrist, 52 | quint8 hashFlag, qint16 score, qint8 depth, const Move &move); 53 | 54 | /** 重置置换表 */ 55 | void reset(); 56 | 57 | /** 清理置换表 */ 58 | void clear(); 59 | 60 | private: 61 | __m128i __attribute__((aligned (16))) m_hashTable[HASH_SIZE]; 62 | }; 63 | } 64 | -------------------------------------------------------------------------------- /src/machine/searchquiescencemachine.cpp: -------------------------------------------------------------------------------- 1 | #include "searchquiescencemachine.h" 2 | 3 | namespace PikaChess { 4 | SearchQuiescenceMachine::SearchQuiescenceMachine(const Chessboard &chessboard, bool notInCheck) 5 | : m_chessboard { chessboard }, m_notInCheck { notInCheck } { } 6 | 7 | Move SearchQuiescenceMachine::getNextMove() { 8 | switch (this->m_phase) { 9 | case PHASE_CAPTURE_GEN: 10 | // 指明下一个阶段 11 | this->m_phase = PHASE_CAPTURE; 12 | // 生成吃子走法,使用MVVLVA对其进行排序 13 | this->m_totalMoves = this->m_chessboard.genCapMoves(this->m_moveList); 14 | std::sort(this->m_moveList, this->m_moveList + this->m_totalMoves); 15 | // 直接下一步 16 | [[fallthrough]]; 17 | 18 | case PHASE_CAPTURE: 19 | /* 遍历走法,逐个返回吃子走法 */ 20 | while (this->m_nowMove < this->m_totalMoves) { 21 | const ValuedMove &move { this->m_moveList[this->m_nowMove++] }; 22 | if (move.score() >= 0) return move; 23 | else { --this->m_nowMove; break; } 24 | } 25 | /* 如果被将军就搜索所有走法,否则只搜索吃子走法 */ 26 | if (this->m_notInCheck) return INVALID_MOVE; 27 | // 如果没有了就下一步 28 | [[fallthrough]]; 29 | 30 | case PHASE_NOT_CAPTURE_GEN: 31 | // 指明下一个阶段 32 | this->m_phase = PHASE_REST; 33 | // 生成非吃子的走法并使用历史表对其进行排序 34 | this->m_totalMoves += this->m_chessboard.genNonCapMoves( 35 | this->m_moveList + this->m_totalMoves); 36 | std::sort(this->m_moveList + this->m_nowMove, this->m_moveList + this->m_totalMoves); 37 | // 直接下一步 38 | [[fallthrough]]; 39 | 40 | case PHASE_REST: 41 | // 遍历走法,逐个返回 42 | while (this->m_nowMove < this->m_totalMoves) return this->m_moveList[this->m_nowMove++]; 43 | // 如果没有了就直接返回 44 | [[fallthrough]]; 45 | 46 | default: return INVALID_MOVE; 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/search/searchinstance.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "killertable.h" 3 | #include "hashtable.h" 4 | #include "searchmachine.h" 5 | #include "searchquiescencemachine.h" 6 | #include "chessboard.h" 7 | 8 | namespace PikaChess { 9 | class SearchInstance { 10 | public: 11 | SearchInstance(const Chessboard &chessboard, HashTable &hashTable); 12 | 13 | /** 根节点的搜索 */ 14 | void searchRoot(const qint8 depth); 15 | 16 | /** 完全局面搜索 */ 17 | qint16 searchFull(qint16 alpha, const qint16 beta, const qint8 depth, const bool nullOk = true); 18 | 19 | /** 静态局面搜索 */ 20 | qint16 searchQuiescence(qint16 alpha, const qint16 beta); 21 | 22 | /** 获得最好的分数 */ 23 | qint16 bestScore() const; 24 | 25 | /** 获得最好的走法 */ 26 | Move bestMove() const; 27 | 28 | /** 当前局面有效的走法数 */ 29 | quint8 legalMove() const; 30 | 31 | /** 让线程停止搜索 */ 32 | void stopSearch(); 33 | 34 | /** 查看当前线程是否已经停止搜索 */ 35 | bool isStopped() const; 36 | 37 | protected: 38 | /** 走一步 */ 39 | bool makeMove(Move &move); 40 | 41 | /** 撤销一步 */ 42 | void unMakeMove(); 43 | 44 | /** 走一步空步 */ 45 | void makeNullMove(); 46 | 47 | /** 撤销一步空步 */ 48 | void unMakeNullMove(); 49 | 50 | /** 查找置换表 */ 51 | qint16 probeHash(qint16 alpha, qint16 beta, qint8 depth, Move &hashMove); 52 | 53 | /** 记录到置换表 */ 54 | void recordHash(quint8 hashFlag, qint16 score, qint8 depth, const Move &move); 55 | 56 | /** 设置最好的走法 */ 57 | void setBestMove(const Move &move, qint8 depth); 58 | 59 | private: 60 | /** 停止标志 */ 61 | volatile bool m_stop { false }; 62 | 63 | /** 距离根节点的距离 */ 64 | quint8 m_distance { 0 }; 65 | 66 | /** 当前局面下最好的分数 */ 67 | qint16 m_bestScore { LOSS_SCORE }; 68 | 69 | /** 当前局面下最好的走法 */ 70 | Move m_bestMove { INVALID_MOVE }; 71 | 72 | /** 局面的内部迭代加深走法 */ 73 | Move m_iidMove { INVALID_MOVE }; 74 | 75 | /** 当前局面的有效走法数 */ 76 | quint8 m_legalMove; 77 | 78 | /** 每个搜索实例都有自己的棋盘 */ 79 | Chessboard m_chessboard; 80 | 81 | /** 杀手走法表 */ 82 | KillerTable m_killerTable; 83 | 84 | /** 置换表的引用 */ 85 | HashTable &m_hashTable; 86 | }; 87 | } 88 | -------------------------------------------------------------------------------- /src/board/bitboard.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include "global.h" 4 | 5 | namespace PikaChess { 6 | /** 位棋盘类,包装__m128i */ 7 | class Bitboard final { 8 | public: 9 | friend class PreGen; 10 | 11 | /** 默认构造函数,位棋盘全部置0 */ 12 | Bitboard(); 13 | 14 | /** 转换函数,在bool测试中使用 */ 15 | operator bool() const; 16 | 17 | /** 按位取反 */ 18 | friend Bitboard operator~(const Bitboard &bitboard); 19 | /** 按位左移,使用内置的__uint128_t获得最好的效果 */ 20 | friend Bitboard operator<<(const Bitboard &bitboard, const qint32 &count); 21 | /** 按位右移,使用内置的__uint128_t获得最好的效果 */ 22 | friend Bitboard operator>>(const Bitboard &bitboard, const qint32 &count); 23 | /** 按位与运算 */ 24 | friend Bitboard operator&(const Bitboard &lhs, const Bitboard &rhs); 25 | /** 按位或运算 */ 26 | friend Bitboard operator|(const Bitboard &lhs, const Bitboard &rhs); 27 | /** 按位异或 */ 28 | friend Bitboard operator^(const Bitboard &lhs, const Bitboard &rhs); 29 | 30 | /** 比较两个位棋盘是否相等 */ 31 | friend bool operator==(const Bitboard &lhs, const Bitboard &rhs); 32 | 33 | /** 按位与并保存 */ 34 | void operator&=(const Bitboard &rhs); 35 | /** 按位或并保存 */ 36 | void operator|=(const Bitboard &rhs); 37 | /** 按位异或并保存 */ 38 | void operator^=(const Bitboard &rhs); 39 | /** 按位左移并保存 */ 40 | void operator<<=(const quint8 &count); 41 | /** 按位右移并保存 */ 42 | void operator>>=(const quint8 &count); 43 | 44 | 45 | /** 用于PEXT位棋盘 */ 46 | quint64 getPextIndex(const quint64 occ0, const quint64 occ1, const quint8 shift) const; 47 | 48 | /** 获取某个位置上面的位 */ 49 | bool operator[](quint8 index) const; 50 | /** 设置某个位置上面的位 */ 51 | void setBit(quint8 index); 52 | /** 清除某个位置上面的位 */ 53 | void clearBit(quint8 index); 54 | /** 清除位棋盘上的所有位 */ 55 | void clearAllBits(); 56 | /** 获得最后一个1的下标 */ 57 | quint8 getLastBitIndex() const; 58 | /** 数一下现在位棋盘上有多少个1 */ 59 | quint8 countBits() const; 60 | 61 | /** 打印位棋盘 */ 62 | void print(); 63 | 64 | protected: 65 | /** 使用128位变量构造位棋盘 */ 66 | Bitboard(const __m128i &rhs); 67 | 68 | private: 69 | /** 位棋盘实际的实现,使用__m128i类型 */ 70 | __m128i m_bitboard; 71 | }; 72 | } 73 | -------------------------------------------------------------------------------- /src/evaluate/evaluate.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "featuretransformer.h" 3 | 4 | #include "windows.h" 5 | #include 6 | 7 | namespace PikaChess { 8 | /** NNUE文件的哈希值 */ 9 | constexpr quint32 HASH_VALUE_FILE = FeatureTransformer::getHashValue() ^ Model::getHashValue(); 10 | 11 | /** 对齐大页分配 */ 12 | inline void *AlignedLargePageAlloc(quint64 allocSize) { 13 | HANDLE hProcessToken { }; 14 | LUID luid { }; 15 | void* mem = nullptr; 16 | 17 | const quint64 largePageSize = GetLargePageMinimum(); 18 | if (not largePageSize) return nullptr; 19 | 20 | // 提升权限以获得SeLockMemory权限 21 | if (not OpenProcessToken(GetCurrentProcess(), 22 | TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hProcessToken)) return nullptr; 23 | 24 | if (LookupPrivilegeValue(NULL, SE_LOCK_MEMORY_NAME, &luid)) { 25 | TOKEN_PRIVILEGES tp { }; 26 | TOKEN_PRIVILEGES prevTp { }; 27 | DWORD prevTpLen = 0; 28 | 29 | tp.PrivilegeCount = 1; 30 | tp.Privileges[0].Luid = luid; 31 | tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED; 32 | 33 | // 调整令牌权限 34 | AdjustTokenPrivileges( 35 | hProcessToken, FALSE, &tp, sizeof(TOKEN_PRIVILEGES), &prevTp, &prevTpLen); 36 | 37 | // 检查是否成功获取权限 38 | if (GetLastError() == ERROR_SUCCESS) { 39 | // 向上取整到页的大小,并分配页面 40 | allocSize = (allocSize + largePageSize - 1) & ~size_t(largePageSize - 1); 41 | mem = VirtualAlloc( 42 | NULL, allocSize, MEM_RESERVE | MEM_COMMIT | MEM_LARGE_PAGES, PAGE_READWRITE); 43 | 44 | // 恢复原有的令牌 45 | AdjustTokenPrivileges(hProcessToken, FALSE, &prevTp, 0, NULL, NULL); 46 | } 47 | } 48 | 49 | CloseHandle(hProcessToken); 50 | 51 | // 如果分配成功,返回地址,如果分配失败,使用普通API重新分配 52 | return mem ? mem : VirtualAlloc(NULL, allocSize, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); 53 | } 54 | 55 | void NNUEInit(); 56 | 57 | /** RAII,自动释放内存 */ 58 | template 59 | struct AlignedDeleter { 60 | void operator()(T* ptr) const { 61 | ptr->~T(); 62 | _mm_free(ptr); 63 | } 64 | }; 65 | 66 | /** RAII,自动释放内存 */ 67 | template 68 | struct LargePageDeleter { 69 | void operator()(T* ptr) const { 70 | ptr->~T(); 71 | if (ptr and not VirtualFree(ptr, 0, MEM_RELEASE)) { 72 | DWORD err = GetLastError(); 73 | std::cerr << "无法分配对齐大页, 错误代码: 0x" 74 | << std::hex << err 75 | << std::dec << std::endl; 76 | exit(EXIT_FAILURE); 77 | } 78 | } 79 | }; 80 | 81 | template 82 | using AlignedPtr = std::unique_ptr>; 83 | 84 | template 85 | using LargePagePtr = std::unique_ptr>; 86 | 87 | /** 输入特征转换器,由于参数都在里面,所以使用大页面管理 */ 88 | extern LargePagePtr featureTransformer; 89 | 90 | /** 模型剩余的部分使用对齐页面 */ 91 | extern AlignedPtr model[LAYER_STACKS]; 92 | } 93 | -------------------------------------------------------------------------------- /ChineseChess.pro: -------------------------------------------------------------------------------- 1 | QT += core gui network # testlib 2 | 3 | greaterThan(QT_MAJOR_VERSION, 4): QT += widgets 4 | 5 | QMAKE_LFLAGS_WINDOWS += -Wl,--stack,32000000 6 | QMAKE_CXXFLAGS += -std=gnu++2b -march=native -masm=intel -fopenmp 7 | QMAKE_CXXFLAGS_RELEASE -= -O2 8 | QMAKE_CXXFLAGS_RELEASE += -Ofast -flto 9 | # The following define makes your compiler emit warnings if you use 10 | # any Qt feature that has been marked deprecated (the exact warnings 11 | # depend on your compiler). Please consult the documentation of the 12 | # deprecated API in order to know how to port your code away from it. 13 | DEFINES += QT_DEPRECATED_WARNINGS 14 | 15 | # You can also make your code fail to compile if it uses deprecated APIs. 16 | # In order to do so, uncomment the following line. 17 | # You can also select to disable deprecated APIs only up to a certain version of Qt. 18 | DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0 19 | 20 | HEADERS += \ 21 | src/GUI/dialog.h \ 22 | src/board/bitboard.h \ 23 | src/board/chessboard.h \ 24 | src/evaluate/accumulator.h \ 25 | src/evaluate/evaluate.h \ 26 | src/evaluate/layer/clippedrelu.h \ 27 | src/evaluate/layer/dense.h \ 28 | src/evaluate/layer/featuretransformer.h \ 29 | src/evaluate/layer/input.h \ 30 | src/evaluate/model.h \ 31 | src/global.h \ 32 | src/machine/searchmachine.h \ 33 | src/machine/searchquiescencemachine.h \ 34 | src/move/historymove.h \ 35 | src/move/move.h \ 36 | src/move/valuedmove.h \ 37 | src/search/chessengine.h \ 38 | src/search/searchinstance.h \ 39 | src/table/hashtable.h \ 40 | src/table/historytable.h \ 41 | src/table/killertable.h \ 42 | src/table/pregen.h \ 43 | # test/perfttest.h 44 | 45 | SOURCES += \ 46 | src/GUI/dialog.cpp \ 47 | src/board/bitboard.cpp \ 48 | src/board/chessboard.cpp \ 49 | src/evaluate/evaluate.cpp \ 50 | src/machine/searchmachine.cpp \ 51 | src/machine/searchquiescencemachine.cpp \ 52 | src/main.cpp \ 53 | src/move/historymove.cpp \ 54 | src/move/move.cpp \ 55 | src/move/valuedmove.cpp \ 56 | src/search/chessengine.cpp \ 57 | src/search/searchinstance.cpp \ 58 | src/table/hashtable.cpp \ 59 | src/table/historytable.cpp \ 60 | src/table/killertable.cpp \ 61 | src/table/pregen.cpp \ 62 | # test/perfttest.cpp 63 | 64 | INCLUDEPATH += src \ 65 | src/GUI \ 66 | src/board \ 67 | src/machine \ 68 | src/search \ 69 | src/table \ 70 | src/move \ 71 | src/evaluate \ 72 | src/evaluate/layer \ 73 | # test 74 | 75 | LIBS += -fopenmp 76 | 77 | FORMS += \ 78 | src\GUI\dialog.ui 79 | 80 | # Default rules for deployment. 81 | qnx: target.path = /tmp/$${TARGET}/bin 82 | else: unix:!android: target.path = /opt/$${TARGET}/bin 83 | !isEmpty(target.path): INSTALLS += target 84 | 85 | RESOURCES += \ 86 | res.qrc 87 | 88 | RC_FILE = \ 89 | app.rc 90 | -------------------------------------------------------------------------------- /src/machine/searchmachine.cpp: -------------------------------------------------------------------------------- 1 | #include "searchmachine.h" 2 | 3 | namespace PikaChess { 4 | SearchMachine::SearchMachine(const Chessboard &chessboard, const Move &hashMove, 5 | Move &killerMove1, Move &killerMove2) 6 | : m_chessboard { chessboard }, m_hashMove { hashMove }, 7 | m_killerMove1 { killerMove1 }, m_killerMove2 { killerMove2 } { } 8 | 9 | Move SearchMachine::getNextMove() { 10 | switch (this->m_phase) { 11 | case PHASE_HASH: 12 | // 指明下一个阶段 13 | this->m_phase = PHASE_CAPTURE_GEN; 14 | // 确保这一个置换表走法不是无效走法 15 | if (this->m_hashMove.isVaild()) return this->m_hashMove; 16 | // 否则就下一步 17 | [[fallthrough]]; 18 | 19 | case PHASE_CAPTURE_GEN: 20 | // 指明下一个阶段 21 | this->m_phase = PHASE_CAPTURE; 22 | // 生成吃子走法,使用MVVLVA对其进行排序 23 | this->m_totalMoves = this->m_chessboard.genCapMoves(this->m_moveList); 24 | std::sort(this->m_moveList, this->m_moveList + this->m_totalMoves); 25 | // 直接下一步 26 | [[fallthrough]]; 27 | 28 | case PHASE_CAPTURE: 29 | // 遍历走法,逐个返回好的吃子走法,吃亏的吃子着法留到最后搜索 30 | while (this->m_nowMove < this->m_totalMoves) { 31 | const ValuedMove &move { this->m_moveList[this->m_nowMove++] }; 32 | if (move == this->m_hashMove) continue; 33 | else if (move.score() >= 0) return move; 34 | else { --this->m_nowMove; break;} 35 | } 36 | // 如果没有了就下一步 37 | [[fallthrough]]; 38 | 39 | case PHASE_KILLER1: 40 | // 指明下一个阶段 41 | this->m_phase = PHASE_KILLER2; 42 | // 确保这一个杀手走法不是默认走法,不是置换表走法,并且要确认是否是合法的步 43 | if (this->m_killerMove1.isVaild() and this->m_killerMove1 not_eq this->m_hashMove and 44 | this->m_chessboard.isLegalMove(this->m_killerMove1)) { 45 | return this->m_killerMove1; 46 | } 47 | // 否则就下一步 48 | [[fallthrough]]; 49 | 50 | case PHASE_KILLER2: 51 | // 指明下一个阶段 52 | this->m_phase = PHASE_NOT_CAPTURE_GEN; 53 | // 确保这一个杀手走法不是默认走法,不是置换表走法 54 | if (this->m_killerMove2.isVaild() and this->m_killerMove2 not_eq this->m_hashMove and 55 | this->m_chessboard.isLegalMove(this->m_killerMove2)) { 56 | return this->m_killerMove2; 57 | } 58 | // 否则就下一步 59 | [[fallthrough]]; 60 | 61 | case PHASE_NOT_CAPTURE_GEN: 62 | // 指明下一个阶段 63 | this->m_phase = PHASE_REST; 64 | // 生成非吃子的走法并使用历史表对其进行排序 65 | this->m_totalMoves += this->m_chessboard.genNonCapMoves( 66 | this->m_moveList + this->m_totalMoves); 67 | std::sort(this->m_moveList + this->m_nowMove, this->m_moveList + this->m_totalMoves); 68 | // 直接下一步 69 | [[fallthrough]]; 70 | 71 | case PHASE_REST: 72 | // 遍历走法,逐个检查并返回 73 | while (this->m_nowMove < this->m_totalMoves) { 74 | const Move &move { this->m_moveList[this->m_nowMove++] }; 75 | if (move not_eq this->m_hashMove and move not_eq this->m_killerMove1 and 76 | move not_eq this->m_killerMove2) return move; 77 | } 78 | // 如果没有了就直接返回 79 | [[fallthrough]]; 80 | 81 | default: return INVALID_MOVE; 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/evaluate/evaluate.cpp: -------------------------------------------------------------------------------- 1 | #include "chessboard.h" 2 | #include 3 | 4 | namespace PikaChess { 5 | /** 输入特征转换器,由于参数都在里面,所以使用大页面管理 */ 6 | LargePagePtr featureTransformer; 7 | 8 | /** 模型剩余的部分使用对齐页面 */ 9 | AlignedPtr model[LAYER_STACKS]; 10 | 11 | /** NNUE的文件名和网络描述信息 */ 12 | std::string fileName { "xiangqi.nnue" }; 13 | std::string netDescription; 14 | 15 | /** 将特征转换器和模型的权重和偏差置为空 */ 16 | template 17 | void ZeroParameters(AlignedPtr &pointer) { 18 | pointer.reset((T*)_mm_malloc(sizeof(T), alignof(T))); 19 | std::memset(pointer.get(), 0, sizeof(T)); 20 | } 21 | 22 | template 23 | void ZeroParameters(LargePagePtr &pointer) { 24 | pointer.reset((T*)(AlignedLargePageAlloc(sizeof(T)))); 25 | std::memset(pointer.get(), 0, sizeof(T)); 26 | } 27 | 28 | void ZeroParameters() { 29 | ZeroParameters(featureTransformer); 30 | for (quint8 i = 0; i < LAYER_STACKS; ++i) ZeroParameters(model[i]); 31 | } 32 | 33 | /** 读取NNUE文件的头部信息 */ 34 | bool ReadHeader(std::istream &stream) { 35 | // 首先读取版本信息并校验 36 | if (ReadInt(stream) not_eq VERSION) return false; 37 | 38 | // 接着读取整个NNUE文件的哈希值并校验 39 | if (ReadInt(stream) not_eq HASH_VALUE_FILE) return false; 40 | 41 | // 读取文件描述信息的长度 42 | quint32 length = ReadInt(stream); 43 | netDescription.resize(length); 44 | // 读取文件描述信息 45 | stream.read(const_cast(netDescription.c_str()), length); 46 | 47 | return not stream.fail(); 48 | } 49 | 50 | /** 读取某层网络的权重和偏差 */ 51 | template 52 | bool ReadParameters(std::istream &stream, T &layer) { 53 | // 首先读取该层的HASH值并校验 54 | if (not stream or ReadInt(stream) not_eq T::getHashValue()) return false; 55 | // 随之调用该层的读参数方法 56 | return layer.readParameters(stream); 57 | } 58 | 59 | /** 读取所有网络层的权重和偏差 */ 60 | bool ReadParameters(std::istream& stream) { 61 | // 首先读取头部信息 62 | if (not ReadHeader(stream)) return false; 63 | 64 | // 接着读取特征转换器的权重和偏差 65 | if (not ReadParameters(stream, *featureTransformer)) return false; 66 | 67 | // 接着读取每一层的权重和偏差 68 | for (quint8 i = 0; i < LAYER_STACKS; ++i) { 69 | if (not ReadParameters(stream, *model[i])) return false; 70 | } 71 | 72 | // 最后检查是否已经读到了文件末尾符号EOF 73 | return stream and stream.peek() == std::ios::traits_type::eof(); 74 | } 75 | 76 | /** 从NNUE文件中初始化所有的内容 */ 77 | void NNUEInit() { 78 | ZeroParameters(); 79 | std::ifstream nnueFile { fileName, std::ios::binary }; 80 | if (not ReadParameters(nnueFile)) throw "读取NNUE神经网络参数失败"; 81 | } 82 | 83 | /** 获得局面评分 */ 84 | qint16 Chessboard::score() { 85 | // 存储中间结果的空间 86 | alignas(CACHE_LINE_SIZE) quint8 transformedFeatures[FeatureTransformer::BufferSize]; 87 | alignas(CACHE_LINE_SIZE) char buffer[Model::BufferSize]; 88 | 89 | /* bucket有点像以前的渐进式评分函数的局面阶段(开局->中局->残局),不同的阶段采用不同的评分模型 90 | * HalfKAv2有8份小的评分模型,分别对应局面的8个阶段,按照下面的公式计算 */ 91 | const quint8 bucket = (this->m_piece - 1) / 4; 92 | const auto psqt = featureTransformer->transform(this->getLastMove().m_acc, this->m_side, 93 | transformedFeatures, bucket); 94 | const auto output = model[bucket]->propagate(transformedFeatures, buffer); 95 | 96 | return (psqt + output[0]) >> OUTPUT_SCALE_BITS; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/board/bitboard.cpp: -------------------------------------------------------------------------------- 1 | #include "bitboard.h" 2 | 3 | namespace PikaChess { 4 | /** 位棋盘掩码 */ 5 | __m128i BITBOARD_MASK[90]; 6 | /** 位棋盘反掩码 */ 7 | __m128i BITBOARD_NOT_MASK[90]; 8 | 9 | Bitboard::Bitboard() :m_bitboard { _mm_setzero_si128() } { } 10 | 11 | Bitboard::Bitboard(const __m128i &rhs) :m_bitboard { rhs } { } 12 | 13 | Bitboard::operator bool() const { 14 | return not _mm_testz_si128(this->m_bitboard, this->m_bitboard); 15 | } 16 | 17 | Bitboard operator~(const Bitboard &bitboard) { return ~bitboard.m_bitboard; } 18 | 19 | Bitboard operator<<(const Bitboard &bitboard, const qint32 &count) { 20 | return __m128i(__uint128_t(bitboard.m_bitboard) << count); 21 | } 22 | 23 | Bitboard operator>>(const Bitboard &bitboard, const qint32 &count) { 24 | return __m128i(__uint128_t(bitboard.m_bitboard) >> count); 25 | } 26 | 27 | Bitboard operator&(const Bitboard &lhs, const Bitboard &rhs) { 28 | return lhs.m_bitboard & rhs.m_bitboard; 29 | } 30 | 31 | Bitboard operator|(const Bitboard &lhs, const Bitboard &rhs) { 32 | return lhs.m_bitboard | rhs.m_bitboard; 33 | } 34 | 35 | Bitboard operator^(const Bitboard &lhs, const Bitboard &rhs) { 36 | return lhs.m_bitboard ^ rhs.m_bitboard; 37 | } 38 | 39 | bool operator==(const Bitboard &lhs, const Bitboard &rhs) { 40 | auto xorResult { lhs.m_bitboard ^ rhs.m_bitboard }; 41 | return _mm_testz_si128(xorResult, xorResult); 42 | } 43 | 44 | void Bitboard::operator&=(const Bitboard &rhs) { this->m_bitboard &= rhs.m_bitboard; } 45 | 46 | void Bitboard::operator|=(const Bitboard &rhs) { this->m_bitboard |= rhs.m_bitboard; } 47 | 48 | void Bitboard::operator^=(const Bitboard &rhs) { this->m_bitboard ^= rhs.m_bitboard; } 49 | 50 | void Bitboard::operator<<=(const quint8 &count) { *this = *this << count; } 51 | 52 | void Bitboard::operator>>=(const quint8 &count) { *this = *this >> count; } 53 | 54 | quint64 Bitboard::getPextIndex(const quint64 occ0, const quint64 occ1, const quint8 shift) const 55 | { 56 | return _pext_u64(this->m_bitboard[0], occ0) << shift | 57 | _pext_u64(this->m_bitboard[1], occ1); 58 | } 59 | 60 | bool Bitboard::operator[](quint8 index) const { 61 | return not _mm_testz_si128(this->m_bitboard, BITBOARD_MASK[index]); 62 | } 63 | 64 | void Bitboard::setBit(quint8 index) { *this |= BITBOARD_MASK[index]; } 65 | 66 | void Bitboard::clearBit(quint8 index) { *this &= BITBOARD_NOT_MASK[index]; } 67 | 68 | void Bitboard::clearAllBits() { this->m_bitboard = _mm_setzero_si128(); } 69 | 70 | quint8 Bitboard::getLastBitIndex() const { 71 | if (this->m_bitboard[0]) return __tzcnt_u64(this->m_bitboard[0]); 72 | else return 64 + __tzcnt_u64(this->m_bitboard[1]); 73 | } 74 | 75 | quint8 Bitboard::countBits() const { 76 | return _mm_popcnt_u64(this->m_bitboard[0]) + _mm_popcnt_u64(this->m_bitboard[1]); 77 | } 78 | 79 | void Bitboard::print() { 80 | qDebug("位棋盘的表示:"); 81 | for (qsizetype rank { 0 }; rank < 10; ++rank) { 82 | // 打印每一个位 83 | for (qsizetype file { 0 }; file < 9; ++file) std::cout << (*this)[rank * 9 + file] << " "; 84 | // 打印换行符 85 | std::cout << std::endl; 86 | } 87 | // 打印位棋盘的十六进制表示 88 | if (this->m_bitboard[1] & 0xFFFFFFFFC0000000) throw "位越界"; 89 | qDebug("十六进制表示: 0x%016llx%016llx", this->m_bitboard[1], this->m_bitboard[0]); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/GUI/dialog.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "chessengine.h" 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | using Step = std::tuple; 12 | 13 | QT_BEGIN_NAMESPACE 14 | namespace Ui { 15 | class Dialog; 16 | } 17 | QT_END_NAMESPACE 18 | 19 | class Dialog : public QDialog { 20 | Q_OBJECT 21 | Q_PROPERTY(QColor mColorClose READ getColorClose WRITE setColorClose) 22 | Q_PROPERTY(QColor mColorMin READ getColorMin WRITE setColorMin) 23 | 24 | public: 25 | Dialog(QWidget *parent = nullptr); 26 | ~Dialog(); 27 | // 执行所有的窗口初始化动作 28 | void initDialog(); 29 | // 执行所有的棋盘有关的初始化动作 30 | void initChess(); 31 | // 玩家走棋 32 | void playerMakeMove(const Step &step); 33 | // 电脑走棋 34 | void computerMove(); 35 | // 判断一个对象是否是棋子类型 36 | inline bool isChess(const QObject *object); 37 | // 判断是否可以走 38 | inline bool canMove(PikaChess::Move move); 39 | // 设置按钮状态,可点击/不可点击 40 | inline void setButtonDisabled(const bool disable); 41 | // 设置走棋状态 42 | inline void setMoving(const bool isMoving); 43 | // 获取云开局库走法 44 | inline std::tuple searchBook(); 45 | 46 | protected: 47 | void closeEvent(QCloseEvent *); 48 | void mousePressEvent(QMouseEvent *event); 49 | void mouseMoveEvent(QMouseEvent *event); 50 | void mouseReleaseEvent(QMouseEvent *event); 51 | void keyPressEvent(QKeyEvent *event); 52 | bool eventFilter(QObject *watched, QEvent *event); 53 | QColor getColorClose() const; 54 | void setColorClose(const QColor color); 55 | QColor getColorMin() const; 56 | void setColorMin(const QColor color); 57 | 58 | signals: 59 | void threadOK(Step); 60 | 61 | private slots: 62 | void on_CloseButton_clicked(); 63 | 64 | void on_MinButton_clicked(); 65 | 66 | void makeMove(Step step); 67 | 68 | void on_PlayerSide_currentIndexChanged(int index); 69 | 70 | void on_Flip_clicked(); 71 | 72 | void on_Reset_clicked(); 73 | 74 | void on_ComputerHard_currentIndexChanged(int index); 75 | 76 | private: 77 | // 窗口相关的成员 78 | Ui::Dialog *ui; 79 | bool mCloseCheck { false }; 80 | bool mOnDialog { false }; 81 | QPoint mMouseStartPoint; 82 | QPoint mDialogStartPoint; 83 | QColor mColorClose { QColor(212, 64, 39, 0) }; 84 | QColor mColorMin { QColor(38, 169, 218, 0) }; 85 | 86 | // 这些是走子吃子所必须获得的对象指针和动画 87 | QLabel *mSelected { nullptr }; 88 | QLabel *mTarget { nullptr }; 89 | QPropertyAnimation *mChessMoveAni { new QPropertyAnimation(mSelected, "geometry") }; 90 | QPropertyAnimation *mChessEatAni { new QPropertyAnimation(mSelected, "geometry") }; 91 | QPropertyAnimation *mComputerMoveAni { new QPropertyAnimation(mSelected, "geometry") }; 92 | QPropertyAnimation *mMaskAni { new QPropertyAnimation(this, "geometry") }; 93 | 94 | // 走棋时不给用户乱动 95 | bool mOnMoving { false }; 96 | 97 | // 电脑是否胜利 98 | bool mComputerWin { false }; 99 | 100 | //这个是提供给电脑走子的对象指针二维数组 101 | QVector> mLabelPointers; 102 | 103 | bool mIsFliped { false }; 104 | 105 | // 象棋引擎 106 | PikaChess::ChessEngine *m_chessEngine; 107 | }; 108 | -------------------------------------------------------------------------------- /src/board/chessboard.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "pregen.h" 3 | #include "historymove.h" 4 | #include "valuedmove.h" 5 | #include "historytable.h" 6 | #include "evaluate.h" 7 | 8 | namespace PikaChess { 9 | class Chessboard final { 10 | public: 11 | /** 将一个FEN串转化为棋盘 */ 12 | void parseFen(const QString &fen); 13 | /** 将一个棋盘上的棋子转化为FEN串 */ 14 | QString getFen() const; 15 | 16 | /** 17 | * 返回当前局面的吃子走法 18 | * @param moveList 生成的走法存放的位置 19 | * @return 生成的走法的个数 20 | */ 21 | quint8 genCapMoves(ValuedMove *moveList) const; 22 | 23 | /** 24 | * 返回当前局面的不吃子走法 25 | * @param moveList 生成的走法存放的位置 26 | * @return 生成的走法的个数 27 | */ 28 | quint8 genNonCapMoves(ValuedMove *moveList) const; 29 | 30 | /** 31 | * @brief 获取当前走子方的所有激活的特征 32 | * @param featureIndexes 存放激活的特征的数组 33 | */ 34 | void getAllFeatures(qint32 *featureIndexes) const; 35 | 36 | /** 当前是否被将军 */ 37 | bool isChecked() const; 38 | 39 | /** 40 | * @brief 判断一个位置是否被保护了 41 | * @param index 位置 42 | * @param side 当前选边 43 | * @return 这个位置是否被保护了 44 | */ 45 | bool isProtected(quint8 index, quint8 side) const; 46 | 47 | /** 48 | * @brief 判断一个走法是否合法 49 | * @param move 50 | * @return 这个走法是否合法 51 | */ 52 | bool isLegalMove(Move &move) const; 53 | 54 | /** 55 | * @brief 获得一个局面的重复情况 56 | * @param distance 距离根节点的深度 57 | * @return 如果局面重复,返回对应的评分,如果局面不重复,返回std::nullopt 58 | */ 59 | std::optional getRepeatScore(quint8 distance) const; 60 | 61 | /** 62 | * @brief 在棋盘上走一步 63 | * @param 一个走法 64 | * @return 这一步能不能走(如果被将军了就自动回退并返回假) 65 | */ 66 | bool makeMove(Move &move); 67 | 68 | /** 还原刚才走的步 */ 69 | void unMakeMove(); 70 | 71 | /** 走一步空步 */ 72 | void makeNullMove(); 73 | 74 | /** 还原一步空步 */ 75 | void unMakeNullMove(); 76 | 77 | /** 78 | * @brief 更新一个走法的历史表值 79 | * @param move 需要更新历史分值的走法 80 | * @param depth 当前的深度 81 | */ 82 | void updateHistoryValue(const Move &move, quint8 depth); 83 | 84 | /** 获得最后一个走法 */ 85 | HistoryMove &getLastMove(); 86 | const HistoryMove &getLastMove() const; 87 | 88 | void setSide(quint8 newSide); 89 | 90 | quint64 zobrist() const; 91 | 92 | quint8 side() const; 93 | 94 | /** 获得当前局面的评分 */ 95 | qint16 score(); 96 | 97 | protected: 98 | /** 99 | * @brief 撤销一个走法 100 | * @param move 一个走法 101 | */ 102 | void undoMove(const Move &move); 103 | 104 | /** 105 | * @brief 获得当前局面下被走子方捉的子 106 | * @return 被捉的子的flag 107 | */ 108 | quint16 getChase(); 109 | 110 | private: 111 | /** 用来辅助走法生成的辅助数组棋盘 */ 112 | quint8 m_helperBoard[90]; 113 | 114 | /** 各种棋子的位棋盘 */ 115 | Bitboard m_bitboards[14]; 116 | /** 双方的占位位棋盘 */ 117 | Bitboard m_redOccupancy; 118 | Bitboard m_blackOccupancy; 119 | Bitboard m_occupancy; 120 | 121 | /** 当前轮到哪一方走 */ 122 | quint8 m_side; 123 | 124 | /** 当前局面的Zobrist值 */ 125 | quint64 m_zobrist; 126 | 127 | /** 当前局面所剩的子力个数 */ 128 | quint8 m_piece; 129 | 130 | /** 走棋的历史记录 */ 131 | HistoryMove m_historyMoves[256]; 132 | /** 当前行棋的步数统计,留一个头部作为头部哨兵 */ 133 | quint16 m_historyMovesCount { 1 } ; 134 | 135 | /** 历史分值表,用于给生成的非吃子走法打分 */ 136 | HistoryTable m_historyTable; 137 | }; 138 | } 139 | -------------------------------------------------------------------------------- /test/perfttest.cpp: -------------------------------------------------------------------------------- 1 | #include "perfttest.h" 2 | 3 | // 以下所有的测试用例来自https://www.chessprogramming.org/Chinese_Chess_Perft_Results 4 | namespace PikaChess { 5 | void PerftTest::position1() { 6 | runPerftTest("rnbakabnr/9/1c5c1/p1p1p1p1p/9/9/P1P1P1P1P/1C5C1/9/RNBAKABNR w", 7 | { { 1, 44 }, { 2, 1920 }, { 3, 79666 }, { 4, 3290240 }, { 5, 133312995 } }); 8 | } 9 | 10 | void PerftTest::position2() { 11 | runPerftTest("r1ba1a3/4kn3/2n1b4/pNp1p1p1p/4c4/6P2/P1P2R2P/1CcC5/9/2BAKAB2 w", 12 | { { 1, 38 }, { 2, 1128 }, { 3, 43929 }, { 4, 1339047 }, { 5, 53112976 } }); 13 | } 14 | 15 | void PerftTest::position3() { 16 | runPerftTest("1cbak4/9/n2a5/2p1p3p/5cp2/2n2N3/6PCP/3AB4/2C6/3A1K1N1 w", 17 | { { 1, 7 }, { 2, 281 }, { 3, 8620 }, { 4, 326201 }, { 5, 10369923 } }); 18 | } 19 | 20 | void PerftTest::position4() { 21 | runPerftTest("5a3/3k5/3aR4/9/5r3/5n3/9/3A1A3/5K3/2BC2B2 w", 22 | { { 1, 25 }, { 2, 424 }, { 3, 9850 }, 23 | { 4, 202884 }, { 5, 4739553 }, { 6, 100055401 } }); 24 | } 25 | 26 | void PerftTest::position5() { 27 | runPerftTest("CRN1k1b2/3ca4/4ba3/9/2nr5/9/9/4B4/4A4/4KA3 w", 28 | { { 1, 28 }, { 2, 516 }, { 3, 14808 }, { 4, 395483 }, { 5, 11842230 } }); 29 | } 30 | 31 | void PerftTest::position6() { 32 | runPerftTest("R1N1k1b2/9/3aba3/9/2nr5/2B6/9/4B4/4A4/4KA3 w", 33 | { { 1, 21 }, { 2, 364 }, { 3, 7626 }, 34 | { 4, 162837 }, { 5, 3500505 }, { 6, 81195154 } }); 35 | } 36 | 37 | void PerftTest::position7() { 38 | runPerftTest("C1nNk4/9/9/9/9/9/n1pp5/B3C4/9/3A1K3 w", 39 | { { 1, 28 }, { 2, 222 }, { 3, 6241 }, 40 | { 4, 64971 }, { 5, 1914306 }, { 6, 23496493 } }); 41 | } 42 | 43 | void PerftTest::position8() { 44 | runPerftTest("4ka3/4a4/9/9/4N4/p8/9/4C3c/7n1/2BK5 w", 45 | { { 1, 23 }, { 2, 345 }, { 3, 8124 }, 46 | { 4, 149272 }, { 5, 3513104 }, { 6, 71287903 } }); 47 | } 48 | 49 | void PerftTest::position9() { 50 | runPerftTest("2b1ka3/9/b3N4/4n4/9/9/9/4C4/2p6/2BK5 w", 51 | { { 1, 21 }, { 2, 195 }, { 3, 3883 }, 52 | { 4, 48060 }, { 5, 933096 }, { 6, 12250386 } }); 53 | } 54 | 55 | void PerftTest::position10() { 56 | runPerftTest("1C2ka3/9/C1Nab1n2/p3p3p/6p2/9/P3P3P/3AB4/3p2c2/c1BAK4 w", 57 | { { 1, 30 }, { 2, 830 }, { 3, 22787 }, { 4, 649866 }, { 5, 17920736 } }); 58 | } 59 | 60 | void PerftTest::position11() { 61 | runPerftTest("CnN1k1b2/c3a4/4ba3/9/2nr5/9/9/4C4/4A4/4KA3 w", 62 | { { 1, 19 }, { 2, 583 }, { 3, 11714 }, { 4, 376467 }, { 5, 8148177 } }); 63 | } 64 | 65 | void PerftTest::runPerftTest(const QString &fen, const TestCases &testCases) { 66 | this->m_chessboard.parseFen(fen); 67 | for (const auto &[depth, nodes] : testCases) { 68 | this->m_nodes = 0; 69 | perft(depth); 70 | QVERIFY2(this->m_nodes == nodes, 71 | QString { "😣第%1层的节点数应该为%2,但实际测试为%3" }.arg(depth) 72 | .arg(nodes).arg(this->m_nodes).toStdString().c_str()); 73 | } 74 | } 75 | 76 | void PerftTest::perft(quint8 depth) { 77 | if (depth == 0) { ++this->m_nodes; return; } 78 | Move move; 79 | SearchMachine search { this->m_chessboard, INVALID_MOVE, INVALID_MOVE, INVALID_MOVE }; 80 | while ((move = search.getNextMove()).isVaild()) { 81 | if (this->m_chessboard.makeMove(move)) { 82 | perft(depth - 1); 83 | this->m_chessboard.unMakeMove(); 84 | } 85 | } 86 | } 87 | } 88 | 89 | QTEST_MAIN(PikaChess::PerftTest) 90 | -------------------------------------------------------------------------------- /src/global.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include "qglobal.h" 4 | #include 5 | 6 | namespace PikaChess { 7 | /** 选边 */ 8 | constexpr quint8 RED { 0 }, BLACK { 7 }, OPP_SIDE { 7 }; 9 | 10 | /** 棋子种类 */ 11 | constexpr quint8 ROOK { 0 }, KNIGHT { 1 }, CANNON { 2 }, BISHOP { 3 }; 12 | constexpr quint8 PAWN { 4 }, ADVISOR { 5 }, KING { 6 }; 13 | 14 | /** 空棋子 */ 15 | constexpr quint8 EMPTY { 14 }; 16 | /** 所有的棋子类型,上层为滑动棋子,下层为跳跃棋子 */ 17 | constexpr quint8 RED_ROOK { 0 }, RED_KNIGHT { 1 }, RED_CANNON { 2 }, RED_BISHOP { 3 }; 18 | constexpr quint8 RED_PAWN { 4 }, RED_ADVISOR { 5 }, RED_KING { 6 }; 19 | constexpr quint8 BLACK_ROOK { 7 }, BLACK_KNIGHT { 8 }, BLACK_CANNON { 9 }, BLACK_BISHOP { 10 }; 20 | constexpr quint8 BLACK_PAWN { 11 }, BLACK_ADVISOR { 12 }, BLACK_KING { 13 }; 21 | 22 | /** 定义赢棋输棋的分数 */ 23 | constexpr qint16 MATE_SCORE { 30000 }, LOSS_SCORE { -30000 }; 24 | /** 长将判负的分值,在该值之内则不写入置换表 */ 25 | constexpr qint16 BAN_SCORE_MATE { 29500 }, BAN_SCORE_LOSS { -29500 }; 26 | /** 搜索出赢棋和输棋的分值界限,超出此值就说明已经搜索出杀棋了 */ 27 | constexpr qint16 WIN_SCORE { 29000 }, LOST_SCORE { -29000 }; 28 | 29 | /** 搜索状态机的阶段 */ 30 | constexpr quint8 PHASE_HASH { 0 }; 31 | constexpr quint8 PHASE_CAPTURE_GEN { 1 }; 32 | constexpr quint8 PHASE_CAPTURE { 2 }; 33 | constexpr quint8 PHASE_KILLER1 { 3 }; 34 | constexpr quint8 PHASE_KILLER2 { 4 }; 35 | constexpr quint8 PHASE_NOT_CAPTURE_GEN { 5 }; 36 | constexpr quint8 PHASE_REST { 6 }; 37 | 38 | /** MVV/LVA Most Valuable Victim Least Valuable Attacker 39 | * 每种子力的MVVLVA价值,车马炮象兵士将 */ 40 | constexpr qint8 MVVLVA[14] { 50, 20, 20, 2, 5, 2, 100, 41 | 50, 20, 20, 2, 5, 2, 100 }; 42 | 43 | /** 置换表的大小 */ 44 | constexpr quint32 HASH_SIZE { 1 << 23 }; 45 | /** 置换表掩码 */ 46 | constexpr quint32 HASH_MASK { HASH_SIZE - 1 }; 47 | 48 | /** 走法类型 */ 49 | constexpr quint8 HASH_ALPHA { 0 }, HASH_BETA { 1 }, HASH_PV { 2 }; 50 | 51 | /** 空着裁剪标志 */ 52 | constexpr bool NO_NULL { false }; 53 | 54 | /** 判断两个位置是否同一行 */ 55 | extern bool SAME_RANK[90][90]; 56 | 57 | /** 判断是否需要检测被捉 */ 58 | extern quint8 CHASE_INFO[14][14]; 59 | 60 | /** 棋子的flag */ 61 | constexpr quint16 CHESS_FLAG[14] { 62 | 1 << 0, 1 << 1, 1 << 2, 1 << 3, 1 << 4, 63 | 1 << 5, 1 << 6, 1 << 7, 1 << 8, 1 << 9, 64 | 1 << 10, 1 << 11, 1 << 12, 1 << 13 65 | }; 66 | 67 | /** 棋子的残局价值,用于差值裁剪,车马炮象兵士将 */ 68 | constexpr quint16 PIECE_VALUE[14] { 69 | 1380, 800, 700, 300, 270, 450, 0, 70 | 1380, 800, 700, 300, 270, 450, 0 71 | }; 72 | 73 | /** NNUE文件的版本 */ 74 | constexpr quint32 VERSION = 0x7AF32F20u; 75 | /** 评分时输出的放缩系数 */ 76 | constexpr quint8 OUTPUT_SCALE_BITS = 4; 77 | /** 权重的放缩系数 */ 78 | constexpr quint8 WEIGHTS_SCALE_BITS = 6; 79 | 80 | /** CPU缓存单元的大小 */ 81 | constexpr quint8 CACHE_LINE_SIZE = 64; 82 | 83 | /** SIMD的宽度 */ 84 | constexpr quint8 SIMD_WIDTH = 32; 85 | constexpr quint8 MAX_SIMD_WIDTH = 32; 86 | 87 | /** 将n向上取整为base的整数倍 */ 88 | template 89 | constexpr IntType CeilToMultiple(IntType n, IntType base) { 90 | return (n + base - 1) / base * base; 91 | } 92 | 93 | /** 从NNUE文件中读取数据 */ 94 | template 95 | inline IntType ReadInt(std::istream &stream) { 96 | IntType result; 97 | stream.read((char*)(&result), sizeof(IntType)); 98 | return result; 99 | } 100 | 101 | /** 从NNUE文件中读取数据 */ 102 | template 103 | inline void ReadInt(std::istream &stream, IntType *out, quint64 count) { 104 | stream.read((char*)(out), sizeof(IntType) * count); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 中国象棋 2 | 3 | #### 介绍 4 | + 个人项目,中国象棋Qt界面与AI象棋引擎 5 | + 棋盘结构为 **PEXT位棋盘** ,使用CPU中128位寄存器的低90位来存储棋盘,对应C++的数据结构为__m128i 6 | + 使用了 **POPCNT指令,BMI位操作指令集中的PEXT与TZCNT指令,SSE指令集中的与、或、非、异或、零测试,AVX2指令集** 等指令来进行走法预生成与快速运算,需要相应的CPU支持 7 | + 引擎算法基于超出边界(Fail-Soft)的AlphaBeta剪枝,使用迭代加深(含内部迭代加深)的搜索方式 8 | + 在局面评价上使用NNUE(快速可更新的神经网络)对进行局面评估 9 | + 支持历史表启发,杀手启发,吃子启发,有良好的走法排序器 10 | + 支持基于SSE的无锁置换表裁剪、带验证的空着裁剪、落后走法衰减、落后走法裁剪、杀棋步数裁剪、无用裁剪、差值裁剪 11 | + 支持将军延伸和重复局面检测(支持长将检测和部分长捉检测) 12 | + 支持主要变例搜索、使用OpenMP与QtConcurrent并发库进行Lazy-SMP多线程搜索 13 | + 联网的情况下支持ChessDB提供的开局库、对局库和残局库,大约可提升引擎200ELO左右 14 | 15 | #### 软件架构 16 | + Qt、C++ 17 | 18 | #### 开发环境 19 | + 集成开发环境: Qt最新版 20 | + 编译器: Qt最新版自带的最新版MinGW 21 | 22 | #### 语言标准 23 | + C++最新标准,开启GNU最新的语言级别扩展特性 24 | 25 | #### 引擎棋力(非NNUE版本,使用云库、CPU:i5-8265U) 26 | + 足以应对一般的纯人,但由于搜索速度上的缺陷,暂不足以应对其他优秀的象棋软件(如象棋旋风)。 27 | + 与一般的象棋引擎的对战评测在b站: 28 | + 对战悟空象棋引擎:https://www.bilibili.com/video/BV1TF41147Do/ 29 | + 对战象棋小巫师引擎:https://www.bilibili.com/video/BV1va411h7yo/ 30 | + 对战象眼引擎:https://www.bilibili.com/video/BV1Q34y1b7PA/ 31 | 32 | #### 天天象棋测试(非NNUE版本,使用云库、CPU:i5-8265U) 33 | + 可战胜业8-3纯人,得出本软件ELO大约为2000左右 34 | + 天天象棋人机对战可以战胜精英级别电脑(天天象棋分析12层),由此可得本软件大致与新版天天象棋分析13层相当。 35 | + 实战测试结果最高等级如下(该账号仅用于测试软件棋力,由于达到业余9-1后,再往后的测试需要实名认证,鉴于已经达到了测试的目的,所以该账号现已注销): 36 | ![评测最高等级](https://images.gitee.com/uploads/images/2021/0823/185211_45f94b91_7628839.jpeg "QQ图片20210823185009.jpg") 37 | + 更多实战测试的内容在:https://www.bilibili.com/video/BV1eR4y1j777 38 | 39 | #### JJ象棋测试(非NNUE版本,使用云库、CPU:i5-8265U) 40 | + 实战测试可战胜特大等级纯人,最高达到荣誉顶级,100盘胜率94%,有1盘掉线,1盘与其他软件作和,4盘输给其他软件,其余与纯人对战都赢了 41 | + 该账号仅用于测试软件棋力,由于特大等级的小部分人和荣誉顶级的绝大部分人都是软件,由于本软件不具备与其他软件对撕的能力,鉴于已经达到了测试的目的,故不再往后测试 42 | ![评测最高等级](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") 43 | 44 | #### 使用说明 45 | + 打开可执行即可运行程序 46 | 47 | #### 参与贡献 48 | + PikaCat 49 | 50 | #### 未来愿景 51 | + 这个引擎目前还有很多不完善的地方((>﹏<)一大堆捏~): 52 | 1. 没有发挥出位棋盘该有的速度,相比于数组棋盘提升幅度不是很大,所以对应的程序实现还有很多未被发现的Bug没有解决。 53 | 2. 搜索速度不快,剪枝力度不够大。 54 | 3. 没有UCI协议支持,目前无法使用命令模式将引擎与界面解耦。 55 | 4. 没有引擎ELO测评平台,如CCRL。 56 | 5. 没有测试平台,如fishtest。 57 | 58 | + 建立这个仓库的初心是看到国际象棋Stockfish引擎的开源仓库及其开源社区支持的强大支持,于是想着能不能在国内也建立一个这样的仓库,让更多象棋引擎爱好者参与引擎的改进,更新,提issue,提pull requests,众人拾柴火焰高。就像Stockfish超过商业引擎Komodo一样,有一天我们也能够媲美象棋旋风。 59 | + 我曾经看到过一句话,我很喜欢:If you love something, set it free. 来自虚幻引擎的官网。这里的free有两种意思,免费与自由。所以如果你喜欢一样东西,想让它变好,就让它免费吧,让它可以被它人自由获取吧!这也是我为什么要开源的原因,这也是我为什么使用WTFPL的原因。 60 | + 作为一条咸鱼ヾ(•ω•`)o,梦想还是要有的,万一实现了呢? 61 | 62 | #### 云开局库、残局库 63 | + https://www.chessdb.cn/query/ 64 | 65 | #### 特别感谢 66 | + 特别感谢ianfab编写的NNUE工具链以及Belzedar94提供的权重文件,让皮卡喵象棋引擎搭上了NNUE的时代快车 67 | + 特别感谢ianfab耐心解答我的解惑,使得皮卡喵NNUE成为可能。https://github.com/ianfab/Fairy-Stockfish/discussions/491 68 | + 以下是ianfab提供的NNUE工具链: 69 | 1. 训练数据生成器:https://github.com/ianfab/variant-nnue-tools 70 | 2. NNUE网络训练器:https://github.com/ianfab/variant-nnue-pytorch 71 | + NNUE的最新参数文件(皮卡喵象棋的nnue文件会与其保持同步更新):https://fairy-stockfish.github.io/nnue/#current-best-nnue-networks 72 | 73 | #### 参考文献 74 | 1. 象棋百科全书:https://www.xqbase.com/computer.htm 75 | 2. 象棋编程维基百科:https://www.chessprogramming.org/Main_Page 76 | 3. Shark象棋引擎论文:http://rportal.lib.ntnu.edu.tw/bitstream/20.500.12235/106625/1/n060147070s01.pdf 77 | 4. NNUE神经网络手册:https://github.com/glinscott/nnue-pytorch/blob/master/docs/nnue.md 78 | 79 | #### 参考代码 80 | 1. 象棋小巫师: https://github.com/xqbase/xqwlight 81 | 2. 象眼: https://github.com/xqbase/eleeye 82 | 3. 国际象棋位棋盘: https://github.com/maksimKorzh/bbc 83 | 4. 佳佳象棋:https://github.com/leedavid/NewGG 84 | 5. Fairy-Stockfish:https://github.com/ianfab/Fairy-Stockfish -------------------------------------------------------------------------------- /src/search/chessengine.cpp: -------------------------------------------------------------------------------- 1 | #include "chessengine.h" 2 | #include "searchinstance.h" 3 | 4 | namespace PikaChess { 5 | ChessEngine::ChessEngine() { 6 | // 加载神经网络 7 | NNUEInit(); 8 | // 初始化棋盘 9 | reset(); 10 | } 11 | 12 | void ChessEngine::reset() { 13 | // 初始局面 14 | this->m_chessboard.parseFen("rnbakabnr/9/1c5c1/p1p1p1p1p/9/9/P1P1P1P1P/1C5C1/9/RNBAKABNR w"); 15 | // 清理置换表 16 | this->m_hashTable.clear(); 17 | } 18 | 19 | void ChessEngine::search() { 20 | // 重置信息 21 | this->m_hashTable.reset(); 22 | // 迭代加深,重置深度 23 | this->m_currentDepth = 1; 24 | 25 | // 多线程搜索,每个线程一个搜索实例,只有置换表是共享的 26 | QVector searchInstances; 27 | // 初始化线程局面 28 | for (quint8 threadID { 0 }; 29 | threadID < qMax(quint8(1), std::thread::hardware_concurrency() / 2); 30 | ++threadID) { 31 | searchInstances.emplaceBack(this->m_chessboard, this->m_hashTable); 32 | } 33 | 34 | // 时间控制 35 | auto startTimeStamp = clock(); 36 | auto timeMan = QtConcurrent::run([&] { 37 | while (not searchInstances.front().isStopped() and 38 | clock() - startTimeStamp < this->m_searchTime) { 39 | std::this_thread::yield(); 40 | } 41 | for (auto &searchInstance : searchInstances) { 42 | searchInstance.stopSearch(); 43 | } 44 | }); 45 | 46 | forever { 47 | // 多线程搜索 48 | #pragma omp parallel for proc_bind(spread) 49 | for (auto &searchInstance : searchInstances) { 50 | searchInstance.searchRoot(this->m_currentDepth); 51 | } 52 | 53 | // 记录所有的线程中最高分数与对应的走法 54 | if (not searchInstances.front().isStopped()) { 55 | this->m_bestScore = searchInstances.front().bestScore(); 56 | this->m_bestMove = searchInstances.front().bestMove(); 57 | for (const auto &searchInstance : searchInstances) { 58 | qint16 threadScore { searchInstance.bestScore() }; 59 | if (this->m_bestScore < threadScore) { 60 | this->m_bestScore = threadScore; 61 | this->m_bestMove = searchInstance.bestMove(); 62 | } 63 | } 64 | } else { 65 | // 如果被迫停止,那么这一层没有搜索完成 66 | --this->m_currentDepth; 67 | } 68 | 69 | // 如果只有一个合法走法或者超过了最大层数或者超过时间就停止搜索就不用再往下搜索了 70 | if (searchInstances.front().isStopped() or 71 | searchInstances.front().legalMove() == 1 or 72 | this->m_currentDepth >= 99 or 73 | clock() - startTimeStamp > this->m_searchTime) { 74 | // 停止时间管理 75 | searchInstances.front().stopSearch(); 76 | timeMan.waitForFinished(); 77 | // 走棋 78 | this->m_chessboard.makeMove(this->m_bestMove); 79 | break; 80 | } 81 | 82 | // 增加层数 83 | ++this->m_currentDepth; 84 | } 85 | } 86 | 87 | quint8 ChessEngine::currentDepth() const { return this->m_currentDepth; } 88 | 89 | qint16 ChessEngine::bestScore() const { return this->m_bestScore; } 90 | 91 | Move ChessEngine::bestMove() const { return this->m_bestMove; } 92 | 93 | void ChessEngine::setSearchTime(clock_t searchTime) { this->m_searchTime = searchTime; } 94 | 95 | bool ChessEngine::makeMove(Move &move) { 96 | if (not this->m_chessboard.isLegalMove(move)) return false; 97 | else return this->m_chessboard.makeMove(move); 98 | } 99 | 100 | void ChessEngine::unMakeMove() { this->m_chessboard.unMakeMove(); } 101 | 102 | std::optional ChessEngine::getRepeatScore() { 103 | return this->m_chessboard.getRepeatScore(0); 104 | } 105 | 106 | void ChessEngine::setSide(quint8 side) { this->m_chessboard.setSide(side); } 107 | 108 | quint8 ChessEngine::side() const { return this->m_chessboard.side(); } 109 | 110 | QString ChessEngine::fen() const { return this->m_chessboard.getFen(); } 111 | } 112 | -------------------------------------------------------------------------------- /src/evaluate/layer/clippedrelu.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "global.h" 3 | 4 | namespace PikaChess { 5 | /** 神经网络的ClippedReLU层,<上一层> */ 6 | template 7 | class ClippedReLU { 8 | public: 9 | /** 输入输出的类型 */ 10 | using InputType = typename PreviousLayer::OutputType; 11 | using OutputType = quint8; 12 | 13 | /** 输入输出的张量维度,因为是relu所以输入输出维度相同 */ 14 | static constexpr quint32 InputDimensions = PreviousLayer::OutputDimensions; 15 | static constexpr quint32 OutputDimensions = InputDimensions; 16 | 17 | /** 本层网络需要使用到的缓冲区大小,对其到CPU的缓冲块,以字节为单位 */ 18 | static constexpr quint32 SelfBufferSize = 19 | CeilToMultiple(OutputDimensions * sizeof(OutputType), CACHE_LINE_SIZE); 20 | 21 | /** 本层网络所在的缓冲区的总大小,包括前面网络的缓冲区和本层网络所需的缓冲区大小 */ 22 | static constexpr quint32 BufferSize = PreviousLayer::BufferSize + SelfBufferSize; 23 | 24 | /** NNUE网络文件中嵌入的哈希值 */ 25 | static constexpr std::uint32_t getHashValue() { 26 | quint32 hashValue = 0x538D24C7u; 27 | hashValue += PreviousLayer::getHashValue(); 28 | return hashValue; 29 | } 30 | 31 | /** 将网络的权重和偏差从文件中读取到内存中,直接调用上一层的读取操作即可,本层没有权重和偏差 */ 32 | bool readParameters(std::istream& stream) { return previousLayer.readParameters(stream); } 33 | 34 | /** 前向传播函数 */ 35 | const OutputType* propagate(const quint8 *transformedFeatures, char* buffer) const { 36 | // 首先调用上一层的传播函数得到本层的输入 37 | const auto input = previousLayer.propagate(transformedFeatures, buffer + SelfBufferSize); 38 | // 输出指针 39 | const auto output = reinterpret_cast(buffer); 40 | 41 | // 如果输入正好是SIMD_WIDTH的倍数,说明输入维度是32,上__m256i 42 | if constexpr (InputDimensions % SIMD_WIDTH == 0) { 43 | // 用于确定下限 44 | const __m256i Zero = _mm256_setzero_si256(); 45 | // 用于重排序 46 | const __m256i Offsets = _mm256_set_epi32(7, 3, 6, 2, 5, 1, 4, 0); 47 | 48 | // 以__m256i为单位的输入输出指针 49 | const auto in = reinterpret_cast(input); 50 | const auto out = reinterpret_cast<__m256i*>(output); 51 | 52 | // 一次操作两个__m256i,经历32->16->16(在这里给予一定的右位移) 53 | const __m256i words0 = _mm256_srai_epi16(_mm256_packs_epi32( 54 | _mm256_load_si256(&in[0]), 55 | _mm256_load_si256(&in[1])), 56 | WEIGHTS_SCALE_BITS); 57 | const __m256i words1 = _mm256_srai_epi16(_mm256_packs_epi32( 58 | _mm256_load_si256(&in[2]), 59 | _mm256_load_si256(&in[3])), 60 | WEIGHTS_SCALE_BITS); 61 | 62 | // 将上面得到的结果进行上下钳位,16->8->clamp(0, 127),因为这一系列操作会打乱顺序,所以最后进行重排序 63 | _mm256_store_si256(out, _mm256_permutevar8x32_epi32( 64 | _mm256_max_epi8(_mm256_packs_epi16(words0, words1), 65 | Zero), Offsets)); 66 | } 67 | // 其他情况说明输入维度是16,上__m128i,具体步骤同上,只是不会打乱,不需要重排序 68 | else { 69 | const __m128i Zero = _mm_setzero_si128(); 70 | const auto in = reinterpret_cast(input); 71 | const auto out = reinterpret_cast<__m128i*>(output); 72 | const __m128i words0 = _mm_srai_epi16(_mm_packs_epi32( 73 | _mm_load_si128(&in[0]), 74 | _mm_load_si128(&in[1])), 75 | WEIGHTS_SCALE_BITS); 76 | const __m128i words1 = _mm_srai_epi16(_mm_packs_epi32( 77 | _mm_load_si128(&in[2]), 78 | _mm_load_si128(&in[3])), 79 | WEIGHTS_SCALE_BITS); 80 | _mm_store_si128(out, _mm_max_epi8(_mm_packs_epi16(words0, words1), Zero)); 81 | } 82 | 83 | return output; 84 | } 85 | 86 | private: 87 | /** 上一层 */ 88 | PreviousLayer previousLayer; 89 | }; 90 | } 91 | -------------------------------------------------------------------------------- /src/table/hashtable.cpp: -------------------------------------------------------------------------------- 1 | #include "hashtable.h" 2 | 3 | namespace PikaChess { 4 | HashItem::HashItem(const volatile __m128i &data) : m_data { data } { } 5 | 6 | qint16 HashTable::probeHash(quint8 distance, quint64 zobrist, 7 | qint16 alpha, qint16 beta, qint8 depth, Move &hashMove) { 8 | // 提取置换表项 9 | volatile __m128i data { this->m_hashTable[zobrist & HASH_MASK] }; 10 | HashItem hashItem { data }; 11 | 12 | // 校验高位zobrist是否对应得上 13 | if (zobrist not_eq hashItem.m_zobrist) { 14 | hashMove = INVALID_MOVE; 15 | return LOSS_SCORE; 16 | } 17 | 18 | // 杀棋的标志,如果杀棋了就不用满足深度条件 19 | bool isMate { false }; 20 | 21 | // 将走法保存下来 22 | hashMove = hashItem.m_move; 23 | if (hashItem.m_score > WIN_SCORE) { 24 | // 有可能会导致搜索的不稳定性,因为这个长打的分数可能来自于另外一条不同的搜索分支 25 | if (hashItem.m_score < BAN_SCORE_MATE) return LOSS_SCORE; 26 | // 真的赢了 27 | isMate = true; 28 | // 给分数添加最短层数信息 29 | hashItem.m_score -= distance; 30 | } else if (hashItem.m_score < LOST_SCORE) { 31 | // 理由同上 32 | if (hashItem.m_score > BAN_SCORE_LOSS) return LOSS_SCORE; 33 | // 真的输了 34 | isMate = true; 35 | // 给分数添加最短层数信息 36 | hashItem.m_score += distance; 37 | } 38 | // 判断该走法是否满足深度条件,即结果比当前的搜索层数同层或者更深,如果赢了就不用满足深度条件 39 | if (hashItem.m_depth >= depth or isMate) { 40 | if (hashItem.m_hashFlag == HASH_BETA) { 41 | /* 42 | * 如果是beta走法,说明在同层或高层中搜索该局面走该走法时发生了beta截断 43 | * 那么查看一下在当前的beta下走该走法是否也可以发生截断,既score >= beta 44 | * 即是否超出alpha-beta的边界(alpha, beta)(here) 45 | * 如果摸到或超过当前beta边界即可截断 46 | * 如果没有超过beta,那么不能直接返回score 47 | * 因为既然同层或高层发生了beta截断,那么就有可能没有搜索完该局面下的所有走法,分数不一定具备参考性 48 | * 但是这个走法能直接返回,因为有助于更快速的缩小当前的alpha-beta范围 49 | */ 50 | if (hashItem.m_score >= beta) return hashItem.m_score; 51 | else return LOSS_SCORE; 52 | } else if (hashItem.m_hashFlag == HASH_ALPHA) { 53 | /* 54 | * 如果是alpha走法,说明在同层或者上层的搜索中遍历了局面中的所有走法 55 | * 并且得到的最好的走法是alpha走法,分数无法超过那层的alpha 56 | * 那么查看一下在当前的alpha下是否也无法超过当前的alpha,既score <= alpha 57 | * 即是否超出alpha-beta的边界(here)(alpha, beta) 58 | * 如果摸到或小于当前alpha即可截断 59 | * 如果大于alpha,那么不能直接返回score 60 | * 因为如果它在alpha-beta范围内,那么它就不是alpha走法,值得搜索一下 61 | * 返回该走法有助于更快速的缩小当前的alpha-beta范围 62 | * 如果它甚至超过了beta,那么这个走法就可以发生beta截断 63 | */ 64 | if (hashItem.m_score <= alpha) return hashItem.m_score; 65 | else return LOSS_SCORE; 66 | } 67 | /* 68 | * 在同层或者上层的搜索中遍历了局面中的所有走法,找到了pv走法,所以就是这个分数了!直接返回即可。 69 | */ 70 | return hashItem.m_score; 71 | } 72 | // 不满足深度条件并且不是杀棋 73 | return LOSS_SCORE; 74 | } 75 | 76 | void HashTable::recordHash(quint8 distance, quint64 zobrist, 77 | quint8 hashFlag, qint16 score, qint8 depth, const Move &move) { 78 | // 提取置换表项 79 | volatile __m128i data { this->m_hashTable[zobrist & HASH_MASK] }; 80 | HashItem hashItem { data }; 81 | 82 | // 查看置换表中的项是否比当前项更加准确 83 | if (hashItem.m_depth > depth) return; 84 | 85 | // 不然就保存置换表标志和深度 86 | hashItem.m_hashFlag = hashFlag; 87 | hashItem.m_depth = depth; 88 | if (score > WIN_SCORE) { 89 | // 可能导致搜索的不稳定性,并且没有最佳着法,立刻退出 90 | if (not move.isVaild() and score <= BAN_SCORE_MATE) return; 91 | // 否则就记录分数,消除分数的最短层数信息 92 | hashItem.m_score = score + distance; 93 | } else if (score < LOST_SCORE) { 94 | // 同上 95 | if (not move.isVaild() and score >= BAN_SCORE_LOSS) return; 96 | hashItem.m_score = score - distance; 97 | } 98 | // 不是杀棋时直接记录分数 99 | else hashItem.m_score = score; 100 | 101 | // 记录走法 102 | hashItem.m_move = move; 103 | 104 | // 记录Zobrist 105 | hashItem.m_zobrist = zobrist; 106 | 107 | // 写回置换表 108 | data = hashItem.m_data; 109 | this->m_hashTable[zobrist & HASH_MASK] = data; 110 | } 111 | 112 | void HashTable::reset() { 113 | for (auto &data : this->m_hashTable) { 114 | HashItem hashItem { data }; 115 | hashItem.m_depth = 0; 116 | data = hashItem.m_data; 117 | } 118 | } 119 | 120 | void HashTable::clear() { memset(this->m_hashTable, 0, sizeof(this->m_hashTable)); } 121 | 122 | } 123 | -------------------------------------------------------------------------------- /src/table/pregen.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "bitboard.h" 3 | #include "move.h" 4 | 5 | namespace PikaChess { 6 | class PreGen final { 7 | public: 8 | PreGen(); 9 | 10 | /** 预计算数据生成 */ 11 | void genRookOccupancy(); 12 | void genKnightOccupancy(); 13 | void genCannonOccupancy(); 14 | void genBishopOccupancy(); 15 | void genAttackByKnightOccupancy(); 16 | void genChaseOccupancy(); 17 | Bitboard getEnumOccupancy(const quint64 &occ0, const quint64 &occ1, quint32 count); 18 | 19 | void genShiftRook(); 20 | void genShiftKnight(); 21 | void genShiftCannon(); 22 | void genShiftBishop(); 23 | void genShiftAttackByKnight(); 24 | void genShiftChase(); 25 | 26 | Bitboard getPreRookAttack(qint8 pos, const Bitboard &occupancy); 27 | Bitboard getPreKnightAttack(qint8 pos, const Bitboard &occupancy); 28 | Bitboard getPreCannonAttack(qint8 pos, const Bitboard &occupancy); 29 | Bitboard getPreBishopAttack(qint8 pos, const Bitboard &occupancy); 30 | Bitboard getPreAttackByKnight(qint8 pos, const Bitboard &occupancy); 31 | void genRook(); 32 | void genKnight(); 33 | void genCannon(); 34 | void genBishop(); 35 | void genRedPawn(); 36 | void genBlackPawn(); 37 | void genAdvisor(); 38 | void genKing(); 39 | void genAttackByKnight(); 40 | void genAttackByRedPawn(); 41 | void genAttackByBlackPawn(); 42 | void genChase(); 43 | void genSide(); 44 | 45 | void genZobristValues(); 46 | 47 | /** 获取棋子的攻击位 */ 48 | Bitboard getAttack(quint8 chessType, quint8 index, const Bitboard &occupancy); 49 | /** 获取攻击位 */ 50 | Bitboard getRookAttack(quint8 index, const Bitboard &occupancy) const; 51 | Bitboard getKnightAttack(quint8 index, const Bitboard &occupancy) const; 52 | Bitboard getCannonAttack(quint8 index, const Bitboard &occupancy) const; 53 | Bitboard getBishopAttack(quint8 index, const Bitboard &occupancy) const; 54 | Bitboard getRedPawnAttack(quint8 index) const; 55 | Bitboard getBlackPawnAttack(quint8 index) const; 56 | Bitboard getAdvisorAttack(quint8 index) const; 57 | Bitboard getKingAttack(quint8 index) const; 58 | 59 | /** 获取被马攻击的位置 */ 60 | Bitboard getAttackByKnight(quint8 index, const Bitboard &occupancy) const; 61 | /** 获取被兵攻击的位置 */ 62 | Bitboard getAttackByPawn(quint8 side, quint8 index) const; 63 | 64 | /** 获取车和炮的捉的位置 */ 65 | Bitboard getRookChase(quint8 index, bool rank, const Bitboard &occupancy) const; 66 | Bitboard getCannonChase(quint8 index, bool rank, const Bitboard &occupancy) const; 67 | 68 | /** 获取选边对应的位置 */ 69 | Bitboard getSide(quint8 side) const; 70 | Bitboard getRedSide() const; 71 | Bitboard getBlackSide() const; 72 | 73 | /** 获取某个棋子在某个位置上的Zobrist值 */ 74 | quint64 getZobrist(quint8 chess, quint8 index) const; 75 | 76 | /** 获取选边的Zobrist值 */ 77 | quint64 getSideZobrist() const; 78 | 79 | private: 80 | /** 所有滑动棋子的占用位,用于走法生成 */ 81 | quint64 m_rookOccupancy[90][2]; 82 | quint64 m_knightOccupancy[90][2]; 83 | quint64 m_cannonOccupancy[90][2]; 84 | quint64 m_bishopOccupancy[90][2]; 85 | quint64 m_attackByKnightOccupancy[90][2]; 86 | /** 车和炮抓子的占用位 */ 87 | quint64 m_chaseOccupancy[90][2][2]; 88 | 89 | /** 所有滑动棋子的PEXT移位,用于走法生成 */ 90 | quint8 m_rookShift[90]; 91 | quint8 m_knightShift[90]; 92 | quint8 m_cannonShift[90]; 93 | quint8 m_bishopShift[90]; 94 | /** 被马攻击的PEXT移位 */ 95 | quint8 m_attackByKnightShift[90]; 96 | /** 车和炮抓子的PEXT移位 */ 97 | quint8 m_chaseShift[90][2]; 98 | 99 | /** 所有棋子的攻击位,用于走法生成 */ 100 | Bitboard m_rookAttack[90][1 << 15]; 101 | Bitboard m_knightAttack[90][1 << 4]; 102 | Bitboard m_cannonAttack[90][1 << 17]; 103 | Bitboard m_bishopAttack[90][1 << 4]; 104 | Bitboard m_redPawnAttack[90]; 105 | Bitboard m_blackPawnAttack[90]; 106 | Bitboard m_advisorAttack[90]; 107 | Bitboard m_kingAttack[90]; 108 | /** 被马攻击的位置 */ 109 | Bitboard m_attackByKnight[90][1 << 4]; 110 | /** 被兵攻击的位置 */ 111 | Bitboard m_attackByRedPawn[90]; 112 | Bitboard m_attackByBlackPawn[90]; 113 | /** 用于判断车和炮抓的子 */ 114 | Bitboard m_cannonChase[90][2][1 << 9]; 115 | Bitboard m_rookChase[90][2][1 << 9]; 116 | 117 | /** 双方的那边 */ 118 | Bitboard m_redSide; 119 | Bitboard m_blackSide; 120 | 121 | /** 棋盘上每个位置,每个棋子的Zobrist值 */ 122 | quint64 m_zobrists[90][14]; 123 | /** 选边的zobrist值 */ 124 | quint64 m_sideZobrist; 125 | }; 126 | /** 全局的预计算数据 */ 127 | extern PreGen PRE_GEN; 128 | } 129 | -------------------------------------------------------------------------------- /src/evaluate/model.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "global.h" 3 | 4 | #include "input.h" 5 | #include "dense.h" 6 | #include "clippedrelu.h" 7 | 8 | namespace PikaChess { 9 | /** HalfKAv2模型 10 | * 9(将的位置) * 13(棋子的个数{2(双方) * 7(每方7种棋子) - 1(将的位置交集为空集,所以合并为一个特征)}) * 11 | * 90(每个棋子的位置) -> 特征转换 -> 12 | * (512(转换后的特征) + 8(PSQT的部分)) x 2 -> 全连接层1 -> ClippedReLU -> 13 | * 16 -> 全连接层2 -> ClippedReLU -> 14 | * 32 -> 输出层 15 | * -> 1(NNUE神经网络的部分估值) 与前面的PSQT部分合并 -> 最终局面评分 16 | */ 17 | static constexpr const char *MODEL_NAME = "HalfKAv2(Friend)"; 18 | 19 | /** 每一个棋子在输入层的位置(A Feature),其中包括将在输入层的位置(K Feature) */ 20 | enum { 21 | PS_R_ROOK = 0 * 90, PS_B_ROOK = 1 * 90, 22 | PS_R_ADVISOR = 2 * 90, PS_B_ADVISOR = 3 * 90, 23 | PS_R_CANNON = 4 * 90, PS_B_CANNON = 5 * 90, 24 | PS_R_PAWN = 6 * 90, PS_B_PAWN = 7 * 90, 25 | PS_R_KNIGHT = 8 * 90, PS_B_KNIGHT = 9 * 90, 26 | PS_R_BISHOP = 10 * 90, PS_B_BISHOP = 11 * 90, 27 | PS_KING = 12 * 90, PS_ALL = 13 * 90, 28 | PS_KING_0 = 0 * PS_ALL, PS_KING_1 = 1 * PS_ALL, PS_KING_2 = 2 * PS_ALL, 29 | PS_KING_3 = 3 * PS_ALL, PS_KING_4 = 4 * PS_ALL, PS_KING_5 = 5 * PS_ALL, 30 | PS_KING_6 = 6 * PS_ALL, PS_KING_7 = 7 * PS_ALL, PS_KING_8 = 8 * PS_ALL 31 | }; 32 | 33 | /** 整个模型的输入维度 */ 34 | static constexpr quint32 INPUT_DIMENSION = 9 * PS_ALL; 35 | /** 一边经过特征转换层转换后的维度,两边就是512 x 2 = 1024 */ 36 | constexpr quint32 TRANSFORMED_FEATURE_DIMENSIONS = 512; 37 | /** 一边特征转换后的PSQT的特征的个数 */ 38 | constexpr quint32 PSQT_BUCKETS = 8; 39 | /** 模型的全连接层子网络的个数 */ 40 | constexpr quint32 LAYER_STACKS = 8; 41 | 42 | /** 全连接层1的输入,经过特征转换后一共有512 x 2也就是1024个特征 */ 43 | using InputLayer = Input; 44 | /** 全连接层1,激活函数采用ClippedReLU */ 45 | using HiddenLayer1 = ClippedReLU>; 46 | /** 全连接层2,激活函数采用ClippedReLU */ 47 | using HiddenLayer2 = ClippedReLU>; 48 | /** 输出层,本层没有激活函数 */ 49 | using OutputLayer = Dense; 50 | 51 | /** 整个模型不包括特征转换层的架构,因为特征转换层需要单独出来以达到快速更新的目的 */ 52 | using Model = OutputLayer; 53 | 54 | /** 根据当前的走子方和子的编号获得子力的特征位置(A Feature),红黑翻转满足NNUE的翻转需求 */ 55 | static constexpr quint32 PIECE_FEATURE_INDEX[8][14] { 56 | { PS_R_ROOK, PS_R_KNIGHT, PS_R_CANNON, PS_R_BISHOP, PS_R_PAWN, PS_R_ADVISOR, PS_KING, 57 | PS_B_ROOK, PS_B_KNIGHT, PS_B_CANNON, PS_B_BISHOP, PS_B_PAWN, PS_B_ADVISOR, PS_KING }, 58 | {}, {}, {}, {}, {}, {}, 59 | { PS_B_ROOK, PS_B_KNIGHT, PS_B_CANNON, PS_B_BISHOP, PS_B_PAWN, PS_B_ADVISOR, PS_KING, 60 | PS_R_ROOK, PS_R_KNIGHT, PS_R_CANNON, PS_R_BISHOP, PS_R_PAWN, PS_R_ADVISOR, PS_KING } 61 | }; 62 | 63 | /** 用将的位置获得将的特征位置(K Feature) */ 64 | static constexpr uint32_t KING_FEATURE_INDEX[90] { 65 | 0, 0, 0, PS_KING_0, PS_KING_1, PS_KING_2, 0, 0, 0, 66 | 0, 0, 0, PS_KING_3, PS_KING_4, PS_KING_5, 0, 0, 0, 67 | 0, 0, 0, PS_KING_6, PS_KING_7, PS_KING_8, 0, 0, 0, 68 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 69 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 70 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 71 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 72 | 0, 0, 0, PS_KING_6, PS_KING_7, PS_KING_8, 0, 0, 0, 73 | 0, 0, 0, PS_KING_3, PS_KING_4, PS_KING_5, 0, 0, 0, 74 | 0, 0, 0, PS_KING_0, PS_KING_1, PS_KING_2, 0, 0, 0, 75 | }; 76 | 77 | /** 根据当前走子方翻转红方和黑方的位置,以满足NNUE的翻转需求 */ 78 | static constexpr quint8 ORIENT[8][90] { 79 | { 81, 82, 83, 84, 85, 86, 87, 88, 89, 80 | 72, 73, 74, 75, 76, 77, 78, 79, 80, 81 | 63, 64, 65, 66, 67, 68, 69, 70, 71, 82 | 54, 55, 56, 57, 58, 59, 60, 61, 62, 83 | 45, 46, 47, 48, 49, 50, 51, 52, 53, 84 | 36, 37, 38, 39, 40, 41, 42, 43, 44, 85 | 27, 28, 29, 30, 31, 32, 33, 34, 35, 86 | 18, 19, 20, 21, 22, 23, 24, 25, 26, 87 | 9, 10, 11, 12, 13, 14, 15, 16, 17, 88 | 0, 1, 2, 3, 4, 5, 6, 7, 8, }, 89 | {}, {}, {}, {}, {}, {}, 90 | { 0, 1, 2, 3, 4, 5, 6, 7, 8, 91 | 9, 10, 11, 12, 13, 14, 15, 16, 17, 92 | 18, 19, 20, 21, 22, 23, 24, 25, 26, 93 | 27, 28, 29, 30, 31, 32, 33, 34, 35, 94 | 36, 37, 38, 39, 40, 41, 42, 43, 44, 95 | 45, 46, 47, 48, 49, 50, 51, 52, 53, 96 | 54, 55, 56, 57, 58, 59, 60, 61, 62, 97 | 63, 64, 65, 66, 67, 68, 69, 70, 71, 98 | 72, 73, 74, 75, 76, 77, 78, 79, 80, 99 | 81, 82, 83, 84, 85, 86, 87, 88, 89, } 100 | }; 101 | 102 | /** 根据当前的走子方,棋子类型,棋子位置,王的位置获取这个特征在特征转换层的输入位置(KA Feature) */ 103 | inline quint32 FeatureIndex(quint8 side, quint8 index, quint8 chess, quint8 kingIndex) { 104 | return KING_FEATURE_INDEX[kingIndex] + PIECE_FEATURE_INDEX[side][chess] + ORIENT[side][index]; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/evaluate/layer/dense.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "global.h" 3 | 4 | namespace PikaChess { 5 | /** 神经网络的Dense层 <上一层网络,这层网络的输出张量维度1 x OutDims> */ 6 | template 7 | class Dense { 8 | public: 9 | /** 输入张量数据类型,输出张量数据类型 */ 10 | using InputType = typename PreviousLayer::OutputType; 11 | using OutputType = qint32; 12 | 13 | /** 输入张量维度,也就是上一层网络的输出张量维度 */ 14 | static constexpr quint32 InputDimensions = PreviousLayer::OutputDimensions; 15 | /** 输入张量维度,对齐到SIMD宽度 */ 16 | static constexpr quint32 PaddedInputDimensions = 17 | CeilToMultiple(InputDimensions, MAX_SIMD_WIDTH); 18 | /** 输出张量维度 */ 19 | static constexpr quint32 OutputDimensions = OutDims; 20 | /** 使用SIMD指令集,每一次可以处理多少个输出元素 21 | * 因为使用的是AVX2(__m256i),输出类型的qint32,所以一次最多处理32 / 4 = 8个输出元素 */ 22 | static constexpr quint32 OutputSIMDWidth = SIMD_WIDTH / 4; 23 | 24 | /** 本层网络需要使用到的缓冲区大小,对其到CPU的缓冲单元,以字节为单位 */ 25 | static constexpr quint64 SelfBufferSize = 26 | CeilToMultiple(OutputDimensions * sizeof(OutputType), CACHE_LINE_SIZE); 27 | 28 | /** 本层网络所在的缓冲区的总大小,包括前面网络的缓冲区和本层网络所需的缓冲区大小 */ 29 | static constexpr quint64 BufferSize = PreviousLayer::BufferSize + SelfBufferSize; 30 | 31 | /** 用于_mm256_madd_epi16的固定乘法因子 */ 32 | static inline const __m256i Ones256 = _mm256_set1_epi16(1); 33 | 34 | /** NNUE网络文件中嵌入的哈希值 */ 35 | static constexpr quint32 getHashValue() { 36 | quint32 hashValue = 0xCC03DAE4u; 37 | hashValue += OutputDimensions; 38 | hashValue ^= PreviousLayer::getHashValue() >> 1; 39 | hashValue ^= PreviousLayer::getHashValue() << 31; 40 | return hashValue; 41 | } 42 | 43 | /** 将网络的权重和偏差从文件中读取到内存中 */ 44 | bool readParameters(std::istream& stream) { 45 | // 首先递归调用上一层网络的读取函数 46 | if (!previousLayer.readParameters(stream)) return false; 47 | // 接着读取偏差,大小为输出维度 48 | for (quint64 i = 0; i < OutputDimensions; ++i) { 49 | biases[i] = ReadInt(stream); 50 | } 51 | // 接着读取权重,因为后面需要作SIMD处理,所以这里需要将权重以四个为单位将列转换为行,读者可以自行打印查看 52 | for (quint64 i = 0; i < OutputDimensions * PaddedInputDimensions; ++i) { 53 | weights[ 54 | (i / 4) % (PaddedInputDimensions / 4) * OutputDimensions * 4 + 55 | i / PaddedInputDimensions * 4 + 56 | i % 4 57 | ] = ReadInt(stream); 58 | } 59 | return !stream.fail(); 60 | } 61 | 62 | /** 前向传播函数 */ 63 | const OutputType *propagate(const quint8* transformedFeatures, char *buffer) const { 64 | // 首先调用上一层的传播函数得到本层的输入 65 | const auto input = previousLayer.propagate(transformedFeatures, buffer + SelfBufferSize); 66 | 67 | // 输出数组指针 68 | const auto output = (OutputType*)(buffer); 69 | 70 | /* 输出维度只能处理元素的整数倍,或者是1(最后一层),因为输出维度其实也是输入维度,是对其到MAX_SIMD_WIDTH的 71 | * 这里使用constexpr if留给编译器根据不同的情况进行优化*/ 72 | if constexpr (OutputDimensions % OutputSIMDWidth == 0) { 73 | // 一次处理4列数据,有多少列需要处理 74 | constexpr qint32 NumChunks = InputDimensions / 4; 75 | 76 | // 将输出转换为32位指针的形式,以供下面_mm256_set1_epi32读取使用,因为我们一次性处理4列 77 | const auto input32 = (const qint32*)(input); 78 | // 输出指针 79 | __m256i *outptr = (__m256i*)(output); 80 | // 首先将偏差复制到output中 81 | std::memmove(output, biases, OutputDimensions * sizeof(OutputType)); 82 | 83 | /* 每次处理4个4列,最后一个4列的坐标是NumChunks(从1开始计算) 84 | * 所以NumChunks - 3就是最后一个4列的第1列 85 | * 因为我们是从0开始的,所以也就是最后一个4列的第2列,刚好满足i的最后一组条件 */ 86 | for (qint32 i = 0; i < NumChunks - 3; i += 4) { 87 | const __m256i in0 = _mm256_set1_epi32(input32[i + 0]); 88 | const __m256i in1 = _mm256_set1_epi32(input32[i + 1]); 89 | const __m256i in2 = _mm256_set1_epi32(input32[i + 2]); 90 | const __m256i in3 = _mm256_set1_epi32(input32[i + 3]); 91 | const auto col0 = (const __m256i*)(&weights[(i + 0) * OutputDimensions * 4]); 92 | const auto col1 = (const __m256i*)(&weights[(i + 1) * OutputDimensions * 4]); 93 | const auto col2 = (const __m256i*)(&weights[(i + 2) * OutputDimensions * 4]); 94 | const auto col3 = (const __m256i*)(&weights[(i + 3) * OutputDimensions * 4]); 95 | // // 以4个8位为单位对应位置相乘相加,一次操作四个__m256i 96 | for (qint32 j = 0; j * OutputSIMDWidth < OutputDimensions; ++j) { 97 | __m256i product0 = _mm256_maddubs_epi16(in0, col0[j]); 98 | __m256i product1 = _mm256_maddubs_epi16(in1, col1[j]); 99 | __m256i product2 = _mm256_maddubs_epi16(in2, col2[j]); 100 | __m256i product3 = _mm256_maddubs_epi16(in3, col3[j]); 101 | product0 = _mm256_adds_epi16(product0, product1); 102 | product0 = _mm256_madd_epi16(product0, Ones256); 103 | product2 = _mm256_adds_epi16(product2, product3); 104 | product2 = _mm256_madd_epi16(product2, Ones256); 105 | outptr[j] = _mm256_add_epi32(outptr[j], _mm256_add_epi32(product0, product2)); 106 | } 107 | } 108 | } 109 | // 如果输出维度只有1,就不需要额外的处理了,直接将输入和权重对应相乘再相加即可 110 | else if constexpr (OutputDimensions == 1) { 111 | const auto inputVector = (const __m256i*)(input); 112 | 113 | // 这里计算处理完所有的输入需要多少次SIMD运算 114 | constexpr qint32 NumChunks = PaddedInputDimensions / SIMD_WIDTH; 115 | __m256i sum0 = _mm256_setzero_si256(); 116 | const auto row0 = (const __m256i*)(&weights[0]); 117 | 118 | for (qint32 j = 0; j < NumChunks; ++j) { 119 | // 以4个8位为单位对应位置相乘相加 120 | sum0 = _mm256_add_epi32(sum0, _mm256_madd_epi16(_mm256_maddubs_epi16( 121 | inputVector[j], row0[j]), Ones256)); 122 | } 123 | 124 | // 最后要加上偏差,将一个__mm256i以32位为单位加在一起,转换成两个__mm128i 对应32位相加 125 | __m128i sum128 = _mm_add_epi32(_mm256_castsi256_si128(sum0), 126 | _mm256_extracti128_si256(sum0, 1)); 127 | // A+B B+A D+C C+D 128 | sum128 = _mm_add_epi32(sum128, _mm_shuffle_epi32(sum128, _MM_PERM_BADC)); 129 | // B+A+D+C A+B+C+D C+D+A+B D+C+B+A 130 | sum128 = _mm_add_epi32(sum128, _mm_shuffle_epi32(sum128, _MM_PERM_CDAB)); 131 | // D+C+B+A+bias 132 | output[0] = _mm_cvtsi128_si32(sum128) + biases[0]; 133 | } 134 | 135 | return output; 136 | } 137 | 138 | private: 139 | /** 偏差的类型和网络的输出类型匹配 */ 140 | using BiasType = OutputType; 141 | /** 权重的类型和网络的输入类型匹配,但是可以为负数 */ 142 | using WeightType = qint8; 143 | 144 | /** 上一层网络 */ 145 | PreviousLayer previousLayer; 146 | 147 | /** 对其到缓冲块的权重和偏差,加速SIMD运算效率 */ 148 | alignas(CACHE_LINE_SIZE) BiasType biases[OutputDimensions]; 149 | alignas(CACHE_LINE_SIZE) WeightType weights[OutputDimensions * PaddedInputDimensions]; 150 | }; 151 | } 152 | -------------------------------------------------------------------------------- /src/evaluate/layer/featuretransformer.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "global.h" 3 | #include "model.h" 4 | #include "move.h" 5 | #include "accumulator.h" 6 | 7 | namespace PikaChess { 8 | /** 偏差的类型 */ 9 | using BiasType = qint16; 10 | /** 权重的类型 */ 11 | using WeightType = qint16; 12 | using PSQTWeightType = qint32; 13 | 14 | /** 对于AVX2而言最佳的寄存器个数,CPU内一共有16个YMM寄存器 */ 15 | static constexpr quint8 NUM_REGS = 16; 16 | 17 | /** 特征转换器 */ 18 | class FeatureTransformer { 19 | private: 20 | /** 一边的特征转换后的结果维度 */ 21 | static constexpr quint32 HalfDimensions = TRANSFORMED_FEATURE_DIMENSIONS; 22 | /** 一次可以处理多少个数据,16个寄存器,每个可以处理16个数据,一次可以处理256个数据 */ 23 | static constexpr quint32 TileHeight = 256; 24 | 25 | /** 在NNUE文件中绑定的哈希值 */ 26 | static constexpr quint32 HASH_VALUE = 0x5f234cb8u; 27 | 28 | public: 29 | /** 特征转换的输出类型 */ 30 | using OutputType = quint8; 31 | 32 | /** 输入维度,输出维度 */ 33 | static constexpr quint32 InputDimensions = INPUT_DIMENSION; 34 | static constexpr quint32 OutputDimensions = HalfDimensions * 2; 35 | 36 | /** 本层需要使用到的缓冲区大小 */ 37 | static constexpr quint64 BufferSize = OutputDimensions * sizeof(OutputType); 38 | 39 | /** NNUE网络文件中嵌入的哈希值 */ 40 | static constexpr quint32 getHashValue() { return HASH_VALUE ^ OutputDimensions; } 41 | 42 | /** 将网络的权重和偏差从文件中读取到内存中 */ 43 | bool readParameters(std::istream& stream) { 44 | ReadInt(stream, biases, HalfDimensions); 45 | ReadInt(stream, weights, HalfDimensions * INPUT_DIMENSION); 46 | ReadInt(stream, psqtWeights, PSQT_BUCKETS * INPUT_DIMENSION); 47 | return !stream.fail(); 48 | } 49 | 50 | /** 将一个棋盘的特征转换,并且将PSQT部分的分数返回 */ 51 | qint32 transform(Accumulator &accumulator, quint8 side, OutputType* output, int bucket) const { 52 | const quint8 perspectives[2] = { side, quint8(side ^ OPP_SIDE) }; 53 | const auto &accumulation = accumulator.accumulation; 54 | const auto &psqtAccumulation = accumulator.psqtAccumulation; 55 | 56 | // 直接获得PSQT部分的分数 57 | const auto psqt = (psqtAccumulation[perspectives[0]][bucket] 58 | - psqtAccumulation[perspectives[1]][bucket]) >> 1; 59 | 60 | // 一共有多少块需要处理 61 | constexpr quint32 NumChunks = HalfDimensions / SIMD_WIDTH; 62 | // 用于ClippedReLU的下限 63 | const __m256i Zero = _mm256_setzero_si256(); 64 | 65 | // 对双方转换后的特征进行ClippedReLU操作 66 | for (quint8 p = 0; p < 2; ++p) { 67 | const quint32 offset = HalfDimensions * p; 68 | auto out = reinterpret_cast<__m256i*>(&output[offset]); 69 | for (quint8 j = 0; j < NumChunks; ++j) 70 | { 71 | __m256i sum0 = _mm256_load_si256(&reinterpret_cast 72 | (accumulation[perspectives[p]])[j * 2 + 0]); 73 | __m256i sum1 = _mm256_load_si256(&reinterpret_cast 74 | (accumulation[perspectives[p]])[j * 2 + 1]); 75 | 76 | _mm256_store_si256(&out[j], _mm256_permute4x64_epi64( 77 | _mm256_max_epi8(_mm256_packs_epi16(sum0, sum1), Zero), 78 | 0b11011000)); 79 | } 80 | } 81 | 82 | return psqt; 83 | } 84 | 85 | /** 更新累加器,针对side方来更新,通常每走一步棋要调用两次本函数,更新双方的累加器 */ 86 | void updateAccumulator(const Accumulator &oldAcc, Accumulator &newAcc, 87 | quint8 side, const Move& move) const { 88 | // 定义需要使用的寄存器 89 | __m256i acc[NUM_REGS]; 90 | __m256i psqt; 91 | 92 | // 先复制将的位置 93 | newAcc.kingPos[side] = oldAcc.kingPos[side]; 94 | 95 | // 一步只可能添加一个特征,也就是走子方走到的那个地方 96 | quint32 added { FeatureIndex(side, move.to(), move.chess(), newAcc.kingPos[side]) }; 97 | 98 | // 一步可能删除一到两个特征,也就是走子方离开的那个地方,加上吃掉走到的那个位置的子 99 | qint32 removed[2]; 100 | // 走到的那个地方 101 | removed[0] = FeatureIndex(side, move.from(), move.chess(), newAcc.kingPos[side]); 102 | // 如果是吃子步则添加上第二个特征,如果不是就置为-1表示没有吃子 103 | if (move.isCapture()) { 104 | removed[1] = FeatureIndex(side, move.to(), move.victim(), newAcc.kingPos[side]); 105 | } else removed[1] = -1; 106 | 107 | // 首先处理特征转换的部分 108 | for (quint32 j = 0; j < HalfDimensions / TileHeight; ++j) { 109 | // 将特征从旧的累加器载入到寄存器中 110 | auto accTile = (__m256i*)(&oldAcc.accumulation[side][j * TileHeight]); 111 | for (quint32 k = 0; k < NUM_REGS; ++k) acc[k] = _mm256_load_si256(&accTile[k]); 112 | 113 | // 删除那些已经移除的特征 114 | for (qint32 index : removed) { 115 | if (-1 not_eq index) { 116 | quint32 offset = HalfDimensions * index + j * TileHeight; 117 | auto column = (const __m256i*)(&weights[offset]); 118 | for (quint32 k = 0; k < NUM_REGS; ++k) acc[k] = _mm256_sub_epi16(acc[k], column[k]); 119 | } 120 | } 121 | 122 | // 添加上新增的特征,也就是本步走到的那个地方 123 | quint32 offset = HalfDimensions * added + j * TileHeight; 124 | auto column = (const __m256i*)(&weights[offset]); 125 | for (quint32 k = 0; k < NUM_REGS; ++k) acc[k] = _mm256_add_epi16(acc[k], column[k]); 126 | 127 | // 将处理后的结果保存到新的累加器中 128 | accTile = (__m256i*)(&newAcc.accumulation[side][j * TileHeight]); 129 | for (quint32 k = 0; k < NUM_REGS; ++k) _mm256_store_si256(&accTile[k], acc[k]); 130 | } 131 | 132 | // 接着处理PSQT的部分,加载PSQT部分的累加器到寄存器中 133 | auto accTilePsqt = (__m256i*)(&oldAcc.psqtAccumulation[side][0]); 134 | psqt = _mm256_load_si256(accTilePsqt); 135 | 136 | // 删除那些已经移除的特征 137 | for (const auto index : removed) { 138 | if (-1 not_eq index) { 139 | const quint32 offset = PSQT_BUCKETS * index; 140 | auto columnPsqt = (const __m256i*)(&psqtWeights[offset]); 141 | psqt = _mm256_sub_epi32(psqt, columnPsqt[0]); 142 | } 143 | } 144 | 145 | // 添加上新增的特征,也就是本步走到的那个地方 146 | const quint32 offset = PSQT_BUCKETS * added; 147 | auto columnPsqt = (const __m256i*)(&psqtWeights[offset]); 148 | psqt = _mm256_add_epi32(psqt, *columnPsqt); 149 | 150 | // 将处理后的结果保存到新的累加器中 151 | accTilePsqt = (__m256i*)(&newAcc.psqtAccumulation[side][0]); 152 | _mm256_store_si256(accTilePsqt, psqt); 153 | } 154 | 155 | /** 刷新累加器,使用提供的特征位置重置整个累加器,只针对side方更新 156 | * 由于将走动了,所以需要调用updateAccumulator,更新另一方的累加器 */ 157 | void refreshAccumulator(Accumulator &accumulator, quint8 side, qint32 *featureIndexes) const { 158 | // 定义需要使用的寄存器 159 | __m256i acc[NUM_REGS]; 160 | __m256i psqt; 161 | 162 | // 首先处理特征转换的部分 163 | for (quint32 j = 0; j < HalfDimensions / TileHeight; ++j) { 164 | // 将偏差复制到寄存器中 165 | auto biasesTile = (const __m256i*)(&biases[j * TileHeight]); 166 | for (quint32 k = 0; k < NUM_REGS; ++k) acc[k] = biasesTile[k]; 167 | 168 | // 将特征逐个添加到寄存器中,直到累加完成 169 | qint32 *now = featureIndexes; 170 | while (-1 not_eq *now) { 171 | quint32 index = *now++; 172 | const quint32 offset = HalfDimensions * index + j * TileHeight; 173 | auto column = (const __m256i*)(&weights[offset]); 174 | 175 | for (quint8 k = 0; k < NUM_REGS; ++k) acc[k] = _mm256_add_epi16(acc[k], column[k]); 176 | } 177 | 178 | // 将处理后的结果保存回累加器中 179 | auto accTile = (__m256i*)(&accumulator.accumulation[side][j * TileHeight]); 180 | for (quint8 k = 0; k < NUM_REGS; ++k) _mm256_store_si256(&accTile[k], acc[k]); 181 | } 182 | 183 | // 接着处理PSQT的部分 184 | psqt = _mm256_setzero_si256(); 185 | 186 | // 将特征逐个累加到寄存器中 187 | while (-1 not_eq *featureIndexes) { 188 | quint32 index = *featureIndexes++; 189 | const quint32 offset = PSQT_BUCKETS * index; 190 | auto columnPsqt = (const __m256i*)(&psqtWeights[offset]); 191 | psqt = _mm256_add_epi32(psqt, *columnPsqt); 192 | } 193 | 194 | // 将处理后的结果保存回累加器中 195 | auto accTilePsqt = (__m256i*)(&accumulator.psqtAccumulation[side][0]); 196 | _mm256_store_si256(accTilePsqt, psqt); 197 | } 198 | 199 | /** 本层所用的偏差,权重和PSQT权重,对其到CPU的缓存块大小 */ 200 | alignas(CACHE_LINE_SIZE) BiasType biases[HalfDimensions]; 201 | alignas(CACHE_LINE_SIZE) WeightType weights[HalfDimensions * InputDimensions]; 202 | alignas(CACHE_LINE_SIZE) PSQTWeightType psqtWeights[InputDimensions * PSQT_BUCKETS]; 203 | }; 204 | } 205 | -------------------------------------------------------------------------------- /src/search/searchinstance.cpp: -------------------------------------------------------------------------------- 1 | #include "searchinstance.h" 2 | 3 | namespace PikaChess { 4 | /** 搜索的衰减层数 [第几层][第几个走法] */ 5 | quint16 REDUCTIONS[100][128]; 6 | 7 | /** 延迟走法裁剪的裁剪层数 [第几层] */ 8 | quint16 LMP_MOVE_COUNT[100]; 9 | 10 | SearchInstance::SearchInstance(const Chessboard &chessboard, HashTable &hashTable) 11 | : m_chessboard { chessboard }, m_hashTable { hashTable } { } 12 | 13 | void SearchInstance::searchRoot(const qint8 depth) { 14 | // 当前走法 15 | Move move; 16 | // 搜索有限状态机 17 | SearchMachine search { this->m_chessboard, this->m_bestMove, 18 | this->m_killerTable.getKiller(this->m_distance, 0), 19 | this->m_killerTable.getKiller(this->m_distance, 1) }; 20 | 21 | qint16 bestScore { LOSS_SCORE }; 22 | // 搜索计数器 23 | quint8 moveCount { 0 }; 24 | this->m_legalMove = 0; 25 | // 是否被对方将军 26 | bool notInCheck { not this->m_chessboard.getLastMove().isChecked() }; 27 | 28 | // 遍历所有走法 29 | while ((move = search.getNextMove()).isVaild()) { 30 | // 如果被将军了就不搜索这一步 31 | if (makeMove(move)) { 32 | ++moveCount; 33 | // 不然就获得评分并更新最好的分数 34 | qint16 tryScore; 35 | const HistoryMove &lastMove { this->m_chessboard.getLastMove() }; 36 | // 将军延伸,如果将军了对方就多搜几步 37 | qint8 newDepth = lastMove.isChecked() ? depth : depth - 1; 38 | // PVS 39 | if (moveCount == 1) { 40 | tryScore = -searchFull(LOSS_SCORE, MATE_SCORE, newDepth, NO_NULL); 41 | } else { 42 | // 对于延迟走法的处理,要求没有被将军,没有将军别人,该步不是吃子步 43 | if (depth >= 3 and notInCheck and newDepth not_eq depth and not lastMove.isCapture()) { 44 | tryScore = -searchFull(-bestScore - 1, -bestScore, 45 | newDepth - REDUCTIONS[depth][moveCount]); 46 | } 47 | // 如果不满足条件则不衰减层数 48 | else tryScore = -searchFull(-bestScore - 1, -bestScore, newDepth); 49 | if (tryScore > bestScore) { 50 | tryScore = -searchFull(LOSS_SCORE, -bestScore, newDepth, NO_NULL); 51 | } 52 | } 53 | // 撤销走棋 54 | unMakeMove(); 55 | // 如果没有被杀棋,就认为这一步是合理的走法 56 | if (tryScore > LOST_SCORE) ++this->m_legalMove; 57 | if (tryScore > bestScore) { 58 | // 找到最佳走法 59 | bestScore = tryScore; 60 | this->m_bestMove = move; 61 | } 62 | } 63 | } 64 | // 记录到置换表 65 | recordHash(HASH_PV, bestScore, depth, this->m_bestMove); 66 | // 如果不是吃子着法,就保存到历史表和杀手着法表 67 | if (not this->m_bestMove.isCapture()) setBestMove(this->m_bestMove, depth); 68 | this->m_bestScore = bestScore; 69 | } 70 | 71 | qint16 SearchInstance::searchFull(qint16 alpha, const qint16 beta, 72 | const qint8 depth, const bool nullOk) { 73 | // 清空内部迭代加深走法 74 | this->m_iidMove = INVALID_MOVE; 75 | 76 | // 达到深度就返回静态评价,由于空着裁剪,深度可能小于-1 77 | if (depth <= 0 or this->m_stop) return searchQuiescence(alpha, beta); 78 | 79 | // 杀棋步数裁剪 80 | qint16 tryScore = LOSS_SCORE + this->m_distance; 81 | if (tryScore >= beta) return tryScore; 82 | 83 | // 先检查重复局面,获得重复局面标志 84 | auto repeatScore { this->m_chessboard.getRepeatScore(this->m_distance) }; 85 | // 如果有重复的情况,直接返回分数 86 | if (repeatScore.has_value()) return repeatScore.value(); 87 | 88 | // 当前走法 89 | Move move; 90 | // 尝试置换表裁剪,并得到置换表走法 91 | tryScore = probeHash(alpha, beta, depth, move); 92 | // 置换表裁剪成功 93 | if (tryScore > LOSS_SCORE) return tryScore; 94 | 95 | // 不被将军时可以进行一些裁剪 96 | bool notInCheck { not this->m_chessboard.getLastMove().isChecked() }; 97 | if (notInCheck) { 98 | // 适用于非PV节点的前期裁剪 99 | if (beta - alpha <= 1) { 100 | // 获得局面的静态评分 101 | qint16 staticEval { this->m_chessboard.score() }; 102 | 103 | // 无用裁剪,如果放弃一定的分值还是超出边界就返回 104 | if (depth < 9 and staticEval - 214 * (depth - 1) >= beta) return staticEval; 105 | 106 | /* 进行空步裁剪,不能连着走两步空步,被将军时不能走空步,层数较大时,需要进行检验 107 | 根节点的Beta值是"MATE_SCORE",所以不可能发生空步裁剪 */ 108 | if (nullOk and staticEval >= beta) { 109 | // 走一步空步 110 | makeNullMove(); 111 | // 获得评分,深度减掉空着裁剪推荐的两层,然后本身走了一步空步,还要再减掉一层 112 | tryScore = -searchFull(-beta, 1 - beta, depth - 3, NO_NULL); 113 | // 空步裁剪发现的胜利需要进一步验证 114 | if (tryScore >= WIN_SCORE) tryScore = beta; 115 | // 撤销空步 116 | unMakeNullMove(); 117 | 118 | // 如果足够好就可以发生截断,层数较大时要注意进行校验 119 | if (tryScore >= beta and ((depth < 14 and abs(beta) < WIN_SCORE) or 120 | searchFull(beta - 1, beta, depth - 2, NO_NULL) >= beta)) { 121 | return tryScore; 122 | } 123 | } 124 | } 125 | 126 | // 适用于PV节点的内部迭代加深启发 127 | else if (depth > 2 and move == INVALID_MOVE) { 128 | tryScore = searchFull(alpha, beta, depth >> 1, NO_NULL); 129 | if (tryScore <= alpha) tryScore = searchFull(LOSS_SCORE, beta, depth >> 1, NO_NULL); 130 | move = this->m_iidMove; 131 | } 132 | } 133 | 134 | // 搜索有限状态机 135 | SearchMachine search { this->m_chessboard, move, 136 | this->m_killerTable.getKiller(this->m_distance, 0), 137 | this->m_killerTable.getKiller(this->m_distance, 1) }; 138 | 139 | // 最佳走法的标志 140 | quint8 bestMoveHashFlag { HASH_ALPHA }; 141 | qint16 bestScore { LOSS_SCORE }; 142 | Move bestMove { INVALID_MOVE }; 143 | 144 | // 搜索计数器 145 | quint8 moveCount { 0 }; 146 | // 遍历所有走法 147 | while ((move = search.getNextMove()).isVaild()) { 148 | // 延迟走法裁剪 149 | if (notInCheck and not move.isCapture() and abs(bestScore) < WIN_SCORE and 150 | moveCount >= LMP_MOVE_COUNT[depth]) continue; 151 | 152 | // 如果被将军了就不搜索这一步 153 | if (makeMove(move)) { 154 | ++moveCount; 155 | // 不然就获得评分并更新最好的分数 156 | const HistoryMove &lastMove { this->m_chessboard.getLastMove() }; 157 | // 将军延伸,如果将军了对方就多搜几步 158 | qint8 newDepth = lastMove.isChecked() ? depth : depth - 1; 159 | 160 | // PVS,对于延迟走法的处理,要求没有被将军,没有将军别人,该步不是吃子步 161 | if (depth >= 3 and notInCheck and newDepth not_eq depth and not lastMove.isCapture()) { 162 | tryScore = -searchFull(-alpha - 1, -alpha, 163 | newDepth - REDUCTIONS[depth][moveCount]); 164 | } 165 | // 如果不满足条件就不衰减层数 166 | else tryScore = -searchFull(-alpha - 1, -alpha, newDepth); 167 | if (tryScore > alpha and tryScore < beta) tryScore = -searchFull(-beta, -alpha, newDepth); 168 | 169 | // 撤销走棋 170 | unMakeMove(); 171 | if (tryScore > bestScore) { 172 | // 找到最佳走法(但不能确定是Alpha、PV还是Beta走法) 173 | bestScore = tryScore; 174 | // 找到一个Beta走法 175 | if (tryScore >= beta) { 176 | // 更新走法标志 177 | bestMoveHashFlag = HASH_BETA; 178 | // Beta走法要保存到历史表 179 | bestMove = move; 180 | // Beta截断 181 | break; 182 | } 183 | // 找到一个PV走法 184 | if (tryScore > alpha) { 185 | // 更新走法标志 186 | bestMoveHashFlag = HASH_PV; 187 | // PV走法要保存到历史表 188 | bestMove = move; 189 | // 缩小Alpha-Beta边界 190 | alpha = tryScore; 191 | } 192 | } 193 | } 194 | } 195 | 196 | // 提供给内部迭代加深使用 197 | this->m_iidMove = bestMove; 198 | 199 | // 所有走法都搜索完了,把最佳走法(不能是Alpha走法)保存到历史表,返回最佳值。如果是杀棋,就根据杀棋步数给出评价 200 | if (bestScore == LOSS_SCORE) return LOSS_SCORE + this->m_distance; 201 | 202 | // 记录到置换表 203 | recordHash(bestMoveHashFlag, bestScore, depth, bestMove); 204 | // 如果不是Alpha走法,并且不是吃子走法,就将最佳走法保存到历史表、杀手表 205 | if (bestMove.isVaild() and not bestMove.isCapture()) setBestMove(bestMove, depth); 206 | 207 | return bestScore; 208 | } 209 | 210 | qint16 SearchInstance::searchQuiescence(qint16 alpha, const qint16 beta) { 211 | // 杀棋步数裁剪 212 | qint16 tryScore = LOSS_SCORE + this->m_distance; 213 | if (tryScore >= beta) return tryScore; 214 | 215 | // 先检查重复局面,获得重复局面标志 216 | auto repeatScore { this->m_chessboard.getRepeatScore(this->m_distance) }; 217 | // 如果有重复的情况,直接返回分数 218 | if (repeatScore.has_value()) return repeatScore.value(); 219 | 220 | qint16 bestScore { LOSS_SCORE }; 221 | 222 | // 差值裁剪的边界值 223 | qint16 deltaBase { LOSS_SCORE }; 224 | 225 | // 如果不被将军,先做局面评价,如果局面评价没有截断,再生成吃子走法 226 | bool notInCheck { not this->m_chessboard.getLastMove().isChecked() }; 227 | if (notInCheck) { 228 | bestScore = this->m_chessboard.score(); 229 | // Beta截断 230 | if (bestScore >= beta) return bestScore; 231 | 232 | // 缩小Alpha-Beta边界 233 | if (bestScore > alpha) alpha = bestScore; 234 | 235 | // 调整差值裁剪的边界 236 | deltaBase = bestScore + 155; 237 | } 238 | 239 | // 静态搜索有限状态机 240 | SearchQuiescenceMachine search { this->m_chessboard, notInCheck }; 241 | 242 | // 遍历所有走法 243 | Move move; 244 | while ((move = search.getNextMove()).isVaild()) { 245 | // 如果被将军了就不搜索这一步 246 | if (makeMove(move)) { 247 | // 首先进行差值裁剪,如果加上一定的值都不能超过alpha,就认为这个走法是无用的 248 | if (notInCheck and not this->m_chessboard.getLastMove().isChecked() and 249 | deltaBase + PIECE_VALUE[move.victim()] <= alpha) { 250 | tryScore = deltaBase + PIECE_VALUE[move.victim()]; 251 | } 252 | // 否则就获得评分并更新最好的分数 253 | else tryScore = -searchQuiescence(-beta, -alpha); 254 | 255 | // 撤销走棋 256 | unMakeMove(); 257 | if (tryScore > bestScore) { 258 | // 找到最佳走法(但不能确定是Alpha、PV还是Beta走法) 259 | bestScore = tryScore; 260 | // 找到一个Beta走法,Beta截断 261 | if (tryScore >= beta) return tryScore; 262 | // 找到一个PV走法,缩小Alpha-Beta边界 263 | if (tryScore > alpha) alpha = tryScore; 264 | } 265 | } 266 | } 267 | 268 | // 所有走法都搜索完了,返回最佳值。如果是杀棋,就根据杀棋步数给出评价 269 | if (bestScore == LOSS_SCORE) return LOSS_SCORE + this->m_distance; 270 | 271 | // 否则就返回最佳值 272 | return bestScore; 273 | } 274 | 275 | qint16 SearchInstance::bestScore() const { return this->m_bestScore; } 276 | 277 | Move SearchInstance::bestMove() const { return this->m_bestMove; } 278 | 279 | quint8 SearchInstance::legalMove() const { return this->m_legalMove; } 280 | 281 | void SearchInstance::stopSearch() { this->m_stop = true; } 282 | 283 | bool SearchInstance::isStopped() const { return this->m_stop; } 284 | 285 | bool SearchInstance::makeMove(Move &move) { 286 | bool result { this->m_chessboard.makeMove(move) }; 287 | if (result) ++this->m_distance; 288 | return result; 289 | } 290 | 291 | void SearchInstance::unMakeMove() { this->m_chessboard.unMakeMove(); --this->m_distance; } 292 | 293 | void SearchInstance::makeNullMove() { ++this->m_distance; this->m_chessboard.makeNullMove(); } 294 | 295 | void SearchInstance::unMakeNullMove() { 296 | --this->m_distance; 297 | this->m_chessboard.unMakeNullMove(); 298 | } 299 | 300 | qint16 SearchInstance::probeHash(qint16 alpha, qint16 beta, qint8 depth, Move &hashMove) { 301 | return this->m_hashTable.probeHash(this->m_distance, this->m_chessboard.zobrist(), 302 | alpha, beta, depth, hashMove); 303 | } 304 | 305 | void SearchInstance::recordHash(quint8 hashFlag, qint16 score, qint8 depth, const Move &move) { 306 | this->m_hashTable.recordHash(this->m_distance, this->m_chessboard.zobrist(), 307 | hashFlag, score, depth, move); 308 | } 309 | 310 | void SearchInstance::setBestMove(const Move &move, qint8 depth) { 311 | this->m_killerTable.updateKiller(this->m_distance, move); 312 | this->m_chessboard.updateHistoryValue(move, depth); 313 | } 314 | } 315 | -------------------------------------------------------------------------------- /src/board/chessboard.cpp: -------------------------------------------------------------------------------- 1 | #include "chessboard.h" 2 | 3 | namespace PikaChess { 4 | /** 用于解析fen的映射 */ 5 | QMap FEN_MAP { 6 | { 'R', RED_ROOK }, { 'N', RED_KNIGHT }, { 'C', RED_CANNON }, { 'B', RED_BISHOP }, 7 | { 'P', RED_PAWN }, { 'A', RED_ADVISOR }, { 'K', RED_KING }, 8 | { 'r', BLACK_ROOK }, { 'n', BLACK_KNIGHT }, { 'c', BLACK_CANNON }, { 'b', BLACK_BISHOP }, 9 | { 'p', BLACK_PAWN }, { 'a', BLACK_ADVISOR }, { 'k', BLACK_KING } 10 | }; 11 | 12 | /** 用于生成fen的映射 */ 13 | QMap FEN_REVERSE_MAP { 14 | { RED_ROOK, 'R' }, { RED_KNIGHT, 'N' }, { RED_CANNON, 'C' }, { RED_BISHOP, 'B' }, 15 | { RED_PAWN, 'P' }, { RED_ADVISOR, 'A' }, { RED_KING, 'K' }, 16 | { BLACK_ROOK, 'r' }, { BLACK_KNIGHT, 'n' }, { BLACK_CANNON, 'c' }, { BLACK_BISHOP, 'b' }, 17 | { BLACK_PAWN, 'p' }, { BLACK_ADVISOR, 'a' }, { BLACK_KING, 'k' } 18 | }; 19 | 20 | /** 判断两个位置是否同一行 */ 21 | bool SAME_RANK[90][90]; 22 | 23 | /** 判断是否需要检测被捉 */ 24 | quint8 CHASE_INFO[14][14]; 25 | 26 | void Chessboard::parseFen(const QString &fen) { 27 | // 先清空原有的棋盘信息 28 | for (auto &bitboard : this->m_bitboards) bitboard.clearAllBits(); 29 | this->m_redOccupancy.clearAllBits(); 30 | this->m_blackOccupancy.clearAllBits(); 31 | this->m_occupancy.clearAllBits(); 32 | this->m_piece = 0; 33 | memset(this->m_helperBoard, EMPTY, sizeof(this->m_helperBoard)); 34 | 35 | // 分割为棋盘和选边两部分 36 | QStringList fenList { fen.split(' ') }; 37 | 38 | // 当前位置的计数器 39 | quint8 count { 0 }; 40 | // 遍历整个棋盘fen,将对应的内容填入位棋盘和辅助棋盘中 41 | foreach (const auto &ch, fenList[0]) { 42 | switch (ch.toLatin1()) { 43 | case '/': continue; 44 | default: 45 | if (ch.isNumber()) { count += (ch.toLatin1() - '0'); continue; } 46 | ++this->m_piece; 47 | this->m_bitboards[FEN_MAP[ch]].setBit(count); 48 | this->m_helperBoard[count] = FEN_MAP[ch]; 49 | break; 50 | } 51 | if (ch.isLower()) this->m_blackOccupancy.setBit(count); 52 | else this->m_redOccupancy.setBit(count); 53 | this->m_occupancy.setBit(count); 54 | ++count; 55 | } 56 | 57 | // 决定选边 58 | if ("w" == fenList[1]) this->m_side = RED; 59 | else this->m_side = BLACK; 60 | 61 | // 重置步数计数器 62 | this->m_historyMovesCount = 1; 63 | 64 | // 刷新双方的初始累加器 65 | Accumulator &acc { this->getLastMove().m_acc }; 66 | qint32 featureIndexes[33]; 67 | 68 | // 刷新对方的累加器 69 | this->m_side ^= OPP_SIDE; 70 | acc.kingPos[this->m_side] = this->m_bitboards[KING + this->m_side].getLastBitIndex(); 71 | this->getAllFeatures(featureIndexes); 72 | featureTransformer->refreshAccumulator(acc, this->m_side, featureIndexes); 73 | 74 | // 刷新自己的累加器 75 | this->m_side ^= OPP_SIDE; 76 | acc.kingPos[this->m_side] = this->m_bitboards[KING + this->m_side].getLastBitIndex(); 77 | this->getAllFeatures(featureIndexes); 78 | featureTransformer->refreshAccumulator(acc, this->m_side, featureIndexes); 79 | } 80 | 81 | QString Chessboard::getFen() const { 82 | QString fen; 83 | 84 | // 空位置计数器 85 | quint8 count { 0 }; 86 | // 遍历辅助棋盘,取出棋子放入fen中 87 | for (quint8 rank { 0 }; rank < 10; ++rank) { 88 | for (quint8 file { 0 }; file < 9; ++file){ 89 | quint8 index = rank * 9 + file; 90 | if (this->m_helperBoard[index] not_eq EMPTY) { 91 | if (count) { fen += QString::number(count); count = 0; } 92 | fen += FEN_REVERSE_MAP[this->m_helperBoard[index]]; 93 | } else ++count; 94 | } 95 | 96 | // 一行结束了,清空空位置计数器 97 | if (count) { fen += QString::number(count); count = 0; } 98 | 99 | // 不是最后一行就补上/ 100 | if (9 not_eq rank) fen += '/'; 101 | } 102 | 103 | // 补充选边信息 104 | fen += " "; 105 | fen += RED == this->m_side ? 'w' : 'b'; 106 | 107 | return fen; 108 | } 109 | 110 | quint8 Chessboard::genCapMoves(ValuedMove *moveList) const { 111 | // 可用的位置 112 | Bitboard available { this->m_side == RED ? this->m_blackOccupancy : this->m_redOccupancy }; 113 | 114 | // 生成的走法数 115 | quint8 total { 0 }; 116 | 117 | // 遍历所有棋子,生成对应的走法 118 | Bitboard bitboard { this->m_side == RED ? this->m_redOccupancy : this->m_blackOccupancy }; 119 | 120 | // 遍历位棋盘上的每一个位 121 | quint8 index; 122 | while ((index = bitboard.getLastBitIndex()) < 90) { 123 | bitboard.clearBit(index); 124 | quint8 chess { this->m_helperBoard[index] }; 125 | 126 | // 获取这个位可以攻击到的位置 127 | Bitboard attack { PRE_GEN.getAttack(chess, index, this->m_occupancy) & available }; 128 | 129 | // 遍历可以攻击的位置,生成对应的走法 130 | quint8 victimIndex; 131 | while ((victimIndex = attack.getLastBitIndex()) < 90) { 132 | attack.clearBit(victimIndex); 133 | 134 | ValuedMove &move { moveList[total++] }; 135 | move.setMove(chess, this->m_helperBoard[victimIndex], index, victimIndex); 136 | // 计算棋子的MVVLVA 137 | qint8 score { MVVLVA[move.victim()] }; 138 | // 如果棋子被保护了还要减去攻击者的分值 139 | if (isProtected(move.to(), this->m_side)) score -= MVVLVA[chess]; 140 | move.setScore(score); 141 | } 142 | } 143 | 144 | return total; 145 | } 146 | 147 | quint8 Chessboard::genNonCapMoves(ValuedMove *moveList) const { 148 | // 可用的位置 149 | Bitboard available { ~this->m_occupancy }; 150 | 151 | // 生成的走法数 152 | quint8 total { 0 }; 153 | 154 | // 遍历所有棋子,生成对应的走法 155 | Bitboard bitboard { this->m_side == RED ? this->m_redOccupancy : this->m_blackOccupancy }; 156 | 157 | // 遍历位棋盘上的每一个位 158 | quint8 index; 159 | while ((index = bitboard.getLastBitIndex()) < 90) { 160 | bitboard.clearBit(index); 161 | quint8 chess { this->m_helperBoard[index] }; 162 | 163 | // 获取这个位可以攻击到的位置 164 | Bitboard attack { PRE_GEN.getAttack(chess, index, this->m_occupancy) & available }; 165 | // 遍历可以攻击的位置,生成对应的走法 166 | quint8 attackIndex; 167 | while ((attackIndex = attack.getLastBitIndex()) < 90) { 168 | attack.clearBit(attackIndex); 169 | ValuedMove &move { moveList[total++] }; 170 | move.setMove(chess, EMPTY, index, attackIndex); 171 | move.setScore(this->m_historyTable.getValue(move)); 172 | } 173 | } 174 | 175 | return total; 176 | } 177 | 178 | void Chessboard::getAllFeatures(qint32 *featureIndexes) const { 179 | // 获取当前走子方将的位置 180 | quint8 kingPos { this->getLastMove().m_acc.kingPos[this->m_side] }; 181 | 182 | // 遍历所有位置,提取特征 183 | Bitboard occupancy { this->m_occupancy }; 184 | 185 | quint8 index; 186 | while ((index = occupancy.getLastBitIndex()) < 90) { 187 | occupancy.clearBit(index); 188 | *featureIndexes++ = FeatureIndex(this->m_side, index, this->m_helperBoard[index], kingPos); 189 | } 190 | 191 | // 结束标志 192 | *featureIndexes = -1; 193 | } 194 | 195 | bool Chessboard::isChecked() const { 196 | // 获取对方的选边 197 | quint8 oppSide = this->m_side ^ OPP_SIDE; 198 | 199 | // 首先获取棋盘上的将的位置 200 | quint8 index { this->m_bitboards[KING + this->m_side].getLastBitIndex() }; 201 | 202 | return 203 | // 把将当作车来走,看能不能吃到对方的车或将(将帅对脸) 204 | ((PRE_GEN.getRookAttack(index, this->m_occupancy) & 205 | (this->m_bitboards[ROOK + oppSide] | this->m_bitboards[KING + oppSide])) or 206 | // 把将当作马来走,看能不能吃到对方的马 207 | (PRE_GEN.getAttackByKnight(index, this->m_occupancy) & 208 | this->m_bitboards[KNIGHT + oppSide]) or 209 | // 把将当作炮来走,看能不能吃到对方的炮 210 | (PRE_GEN.getCannonAttack(index, this->m_occupancy) & 211 | this->m_bitboards[CANNON + oppSide]) or 212 | // 把将当作兵来走,看能不能吃到对方的兵 213 | (PRE_GEN.getAttackByPawn(this->m_side, index) & this->m_bitboards[PAWN + oppSide])); 214 | } 215 | 216 | bool Chessboard::isProtected(quint8 index, quint8 side) const { 217 | // 获取对方的选边 218 | quint8 oppSide = side ^ OPP_SIDE; 219 | 220 | return 221 | // 看这个位置有没有被车保护 222 | (PRE_GEN.getRookAttack(index, this->m_occupancy) & 223 | this->m_bitboards[ROOK + oppSide]) or 224 | // 看这个位置有没有被炮保护 225 | (PRE_GEN.getCannonAttack(index, this->m_occupancy) & 226 | this->m_bitboards[CANNON + oppSide]) or 227 | // 看这个位置有没有被马保护 228 | (PRE_GEN.getAttackByKnight(index, this->m_occupancy) & 229 | this->m_bitboards[KNIGHT + oppSide]) or 230 | // 看这个位置有没有被兵保护 231 | (PRE_GEN.getAttackByPawn(side, index) & this->m_bitboards[PAWN + oppSide]) or 232 | // 看这个位置有没有被象保护 233 | (PRE_GEN.getBishopAttack(index, this->m_occupancy) & 234 | this->m_bitboards[BISHOP + oppSide]) or 235 | // 看这个位置有没有被士保护 236 | (PRE_GEN.getAdvisorAttack(index) & this->m_bitboards[ADVISOR + oppSide]) or 237 | // 看这个位置有没有被将保护 238 | (PRE_GEN.getKingAttack(index) & this->m_bitboards[KING + oppSide]); 239 | } 240 | 241 | bool Chessboard::isLegalMove(Move &move) const { 242 | // 看看这个地方是不是自己的棋子,检查所去之处是否有自己的棋子 243 | const Bitboard &occupancy { RED == this->m_side ? 244 | this->m_redOccupancy : this->m_blackOccupancy }; 245 | if (not occupancy[move.from()] or occupancy[move.to()]) return false; 246 | 247 | // 在符合的情况下首先检查该走法是否符合相关棋子的步法 248 | move.setChess(this->m_helperBoard[move.from()]); 249 | if (PRE_GEN.getAttack(move.chess(), move.from(), this->m_occupancy)[move.to()]) { 250 | // 修正吃子情况 251 | move.setVictim(this->m_helperBoard[move.to()]); 252 | return true; 253 | } 254 | 255 | return false; 256 | } 257 | 258 | std::optional Chessboard::getRepeatScore(quint8 distance) const { 259 | /* mySide代表的是是否是调用本函数的那一方(下称"我方") 260 | * 因为一调用搜索就马上调用了本函数,我方没有走棋 261 | * 所以在检查重复走法时,历史走法表中最后一项保存的是对方的最后一步 262 | * 所以这个变量的初始值为假,代表这一步不是我方,因为走法从后往前遍历 */ 263 | bool mySide { false }; 264 | 265 | // 双方的长打标志 266 | quint16 myFlag { 0x7fff }, oppFlag { 0x7fff }; 267 | 268 | // 指向历史走法表的最后一项,往前遍历 269 | const HistoryMove *move = &this->m_historyMoves[this->m_historyMovesCount - 1]; 270 | /* 必须保证步法有效,也就是没有到头部哨兵或者空步裁剪处 271 | * 如果遇到空步裁剪就不往下检测了,因为空步无法算作有效步 272 | * 并且要求不是吃子步,因为吃子就打破长打了 */ 273 | while (not move->isNullMove() and not move->isCapture()) { 274 | if (mySide) { 275 | // 如果是我方,更新我方长打信息 276 | myFlag &= move->getFlag(); 277 | 278 | // 如果检测到局面与当前局面重复就返回对应的分数 279 | if (move->zobrist() == this->m_zobrist) { 280 | myFlag = (myFlag & 0x3fff) == 0 ? myFlag : 0x3fff; 281 | oppFlag = (oppFlag & 0x3fff) == 0 ? oppFlag : 0x3fff; 282 | 283 | // 我方长打返回负分,对方长打返回正分,双方长打返回0分 284 | if (myFlag > oppFlag) return BAN_SCORE_LOSS + distance; 285 | else if (myFlag < oppFlag) return BAN_SCORE_MATE - distance; 286 | else return 0; 287 | } 288 | } 289 | // 如果是对方,更新对方的将军信息 290 | else oppFlag &= move->getFlag(); 291 | 292 | // 更新选边信息 293 | mySide = not mySide; 294 | 295 | // move指向前一个走法 296 | --move; 297 | } 298 | 299 | // 没有重复局面 300 | return std::nullopt; 301 | } 302 | 303 | bool Chessboard::makeMove(Move &move) { 304 | // 如果这步是吃子步,从被吃掉的棋子的位棋盘中移除对应的位 305 | if (move.isCapture()) { 306 | if (RED == this->m_side) this->m_blackOccupancy.clearBit(move.to()); 307 | else this->m_redOccupancy.clearBit(move.to()); 308 | this->m_bitboards[move.victim()].clearBit(move.to()); 309 | // 存活的子少了一个 310 | --this->m_piece; 311 | // 注意,这里不用移除occupancy中move.to()位,因为攻击的棋子会移动过来 312 | } 313 | 314 | // 将move.to()的位置设置上,并从原来的位置移除对应的位 315 | else this->m_occupancy.setBit(move.to()); 316 | this->m_occupancy.clearBit(move.from()); 317 | if (RED == this->m_side) { 318 | this->m_redOccupancy.setBit(move.to()); 319 | this->m_redOccupancy.clearBit(move.from()); 320 | } else { 321 | this->m_blackOccupancy.setBit(move.to()); 322 | this->m_blackOccupancy.clearBit(move.from()); 323 | } 324 | this->m_bitboards[move.chess()].setBit(move.to()); 325 | this->m_bitboards[move.chess()].clearBit(move.from()); 326 | 327 | // 检查走完之后是否被将军了,如果被将军了就撤销这一步 328 | if (isChecked()) { undoMove(move); return false; } 329 | 330 | // 走动辅助棋盘 331 | this->m_helperBoard[move.from()] = EMPTY; 332 | this->m_helperBoard[move.to()] = move.chess(); 333 | 334 | // 获取上一个累加器 335 | const Accumulator &lastAcc { this->getLastMove().m_acc }; 336 | 337 | // 在历史走法表中记录这一个走法 338 | HistoryMove &historyMove { this->m_historyMoves[this->m_historyMovesCount++] }; 339 | 340 | // 记录现在的走法 341 | historyMove.setMove(move); 342 | 343 | // 记录现在的Zobrist值 344 | historyMove.setZobrist(this->m_zobrist); 345 | 346 | // 计算新的Zobrist值 347 | this->m_zobrist ^= PRE_GEN.getSideZobrist(); 348 | this->m_zobrist ^= PRE_GEN.getZobrist(move.chess(), move.from()); 349 | this->m_zobrist ^= PRE_GEN.getZobrist(move.chess(), move.to()); 350 | // 吃子步需要把被吃的子的zobrist去除 351 | if (move.isCapture()) this->m_zobrist ^= PRE_GEN.getZobrist(move.victim(), move.to()); 352 | 353 | // 如果走动的是将,就刷新自己的累加器 354 | if (move.chess() == KING + this->m_side) { 355 | historyMove.m_acc.kingPos[this->m_side] = move.to(); 356 | qint32 featureIndexes[33]; 357 | this->getAllFeatures(featureIndexes); 358 | featureTransformer->refreshAccumulator(historyMove.m_acc, this->m_side, featureIndexes); 359 | } 360 | // 否则就更新自己的累加器 361 | else featureTransformer->updateAccumulator(lastAcc, historyMove.m_acc, this->m_side, move); 362 | 363 | // 换边 364 | this->m_side ^= OPP_SIDE; 365 | 366 | // 不要忘记另一边累加器的也要更新 367 | featureTransformer->updateAccumulator(lastAcc, historyMove.m_acc, this->m_side, move); 368 | 369 | // 补充对应的将军捉子信息 370 | if (isChecked()) historyMove.setChecked(); 371 | else historyMove.setChase(this->getChase()); 372 | 373 | // 走子成功 374 | return true; 375 | } 376 | 377 | void Chessboard::unMakeMove() { 378 | // 换边 379 | this->m_side ^= OPP_SIDE; 380 | 381 | // 从历史走法表中取出最后一个走法 382 | const HistoryMove &move { this->m_historyMoves[--this->m_historyMovesCount] }; 383 | 384 | // 还原辅助棋盘 385 | this->m_helperBoard[move.to()] = move.victim(); 386 | this->m_helperBoard[move.from()] = move.chess(); 387 | 388 | // 还原原来的Zobrist值 389 | this->m_zobrist = move.zobrist(); 390 | 391 | // 撤销这个走法 392 | undoMove(move); 393 | } 394 | 395 | void Chessboard::makeNullMove() { 396 | // 获取历史走法表项,并将自增走法历史表的大小 397 | const Accumulator &lastAcc { this->getLastMove().m_acc }; 398 | HistoryMove &move = this->m_historyMoves[this->m_historyMovesCount++]; 399 | // 设置空步信息 400 | move.setNullMove(); 401 | // 复制上一个累加器的内容 402 | this->getLastMove().m_acc.copyFrom(lastAcc); 403 | // 换边 404 | this->m_side ^= OPP_SIDE; 405 | // 计算新的Zobrist值 406 | this->m_zobrist ^= PRE_GEN.getSideZobrist(); 407 | } 408 | 409 | void Chessboard::unMakeNullMove() { 410 | // 自减历史走法表大小 411 | --this->m_historyMovesCount; 412 | // 换边 413 | this->m_side ^= OPP_SIDE; 414 | // 还原Zobrist 415 | this->m_zobrist ^= PRE_GEN.getSideZobrist(); 416 | } 417 | 418 | void Chessboard::updateHistoryValue(const Move &move, quint8 depth) { 419 | this->m_historyTable.updateValue(move, depth); 420 | } 421 | 422 | HistoryMove &Chessboard::getLastMove() { 423 | return this->m_historyMoves[this->m_historyMovesCount - 1]; 424 | } 425 | 426 | const HistoryMove &Chessboard::getLastMove() const { 427 | return this->m_historyMoves[this->m_historyMovesCount - 1]; 428 | } 429 | 430 | void Chessboard::undoMove(const Move &move) { 431 | // 恢复原来的位 432 | this->m_bitboards[move.chess()].clearBit(move.to()); 433 | this->m_bitboards[move.chess()].setBit(move.from()); 434 | if (RED == this->m_side) { 435 | this->m_redOccupancy.clearBit(move.to()); 436 | this->m_redOccupancy.setBit(move.from()); 437 | } else { 438 | this->m_blackOccupancy.clearBit(move.to()); 439 | this->m_blackOccupancy.setBit(move.from()); 440 | } 441 | 442 | // 如果这步是吃子步,就设置回对应的位,注意,这里不用再次设置occupancy,因为之前没有清除这个位 443 | if (move.isCapture()) { 444 | if (RED == this->m_side) this->m_blackOccupancy.setBit(move.to()); 445 | else this->m_redOccupancy.setBit(move.to()); 446 | this->m_bitboards[move.victim()].setBit(move.to()); 447 | // 恢复存活子 448 | ++this->m_piece; 449 | // 注意,如果是吃子步则不用清空to,因为这里原来有一个棋子 450 | } 451 | 452 | // 恢复原来的位 453 | else this->m_occupancy.clearBit(move.to()); 454 | this->m_occupancy.setBit(move.from()); 455 | } 456 | 457 | quint16 Chessboard::getChase() { 458 | quint8 side = this->m_side ^ OPP_SIDE; 459 | 460 | const HistoryMove &move { this->getLastMove() }; 461 | 462 | quint16 flag { 0 }; 463 | 464 | // 首先获取被抓的棋子 465 | Bitboard chase { RED == side ? this->m_blackOccupancy : this->m_redOccupancy }; 466 | switch(move.chess() - side) { 467 | case ROOK: 468 | chase &= PRE_GEN.getRookChase(move.to(), SAME_RANK[move.from()][move.to()], m_occupancy); 469 | break; 470 | case CANNON: 471 | chase &= PRE_GEN.getCannonChase(move.to(), SAME_RANK[move.from()][move.to()], m_occupancy); 472 | break; 473 | case KNIGHT: 474 | chase &= PRE_GEN.getKnightAttack(move.to(), this->m_occupancy); 475 | break; 476 | default: 477 | // 其余棋子不予考虑 478 | return 0; 479 | } 480 | 481 | // 接下来查表决定是否是捉 482 | quint8 index; 483 | while ((index = chase.getLastBitIndex()) < 90) { 484 | chase.clearBit(index); 485 | quint8 victim { this->m_helperBoard[index] }; 486 | switch (CHASE_INFO[move.chess()][victim]) { 487 | case 1: flag |= CHESS_FLAG[victim]; break; 488 | case 2: if (not this->isProtected(index, side)) flag |= CHESS_FLAG[victim]; break; 489 | case 3: 490 | if (PRE_GEN.getSide(side)[index] and not this->isProtected(index, side)) { 491 | flag |= CHESS_FLAG[victim]; 492 | } 493 | default: 494 | break; 495 | } 496 | } 497 | 498 | return flag; 499 | } 500 | 501 | quint8 Chessboard::side() const { return this->m_side; } 502 | 503 | void Chessboard::setSide(quint8 newSide) { this->m_side = newSide; } 504 | 505 | quint64 Chessboard::zobrist() const { return this->m_zobrist; } 506 | } 507 | -------------------------------------------------------------------------------- /ChineseChess.pro.user: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | EnvironmentId 7 | {11642ffd-1668-41a7-bf18-704cd22e117e} 8 | 9 | 10 | ProjectExplorer.Project.ActiveTarget 11 | 0 12 | 13 | 14 | ProjectExplorer.Project.EditorSettings 15 | 16 | true 17 | false 18 | true 19 | 20 | Cpp 21 | 22 | CppGlobal 23 | 24 | 25 | 26 | QmlJS 27 | 28 | QmlJSGlobal 29 | 30 | 31 | 32 | Nim 33 | 34 | NimGlobal 35 | 36 | 37 | 3 38 | UTF-8 39 | false 40 | 4 41 | false 42 | 80 43 | true 44 | true 45 | 1 46 | false 47 | true 48 | false 49 | 0 50 | true 51 | true 52 | 0 53 | 8 54 | true 55 | false 56 | 1 57 | true 58 | true 59 | true 60 | *.md, *.MD, Makefile 61 | false 62 | true 63 | true 64 | 65 | 66 | 67 | ProjectExplorer.Project.PluginSettings 68 | 69 | 70 | true 71 | false 72 | true 73 | true 74 | true 75 | true 76 | 77 | 78 | 0 79 | true 80 | 81 | -fno-delayed-template-parsing 82 | 83 | true 84 | Builtin.Questionable 85 | 86 | false 87 | true 88 | Builtin.DefaultTidyAndClazy 89 | 4 90 | 91 | 92 | 93 | true 94 | 95 | 96 | true 97 | 98 | 99 | 100 | 101 | ProjectExplorer.Project.Target.0 102 | 103 | Desktop 104 | Desktop Qt 6.4.2 MinGW 64-bit 105 | Desktop Qt 6.4.2 MinGW 64-bit 106 | qt.qt6.642.win64_mingw_kit 107 | 1 108 | 0 109 | 0 110 | 111 | 0 112 | D:\Qt\QtProjects\ChineseChess\..\build-ChineseChess-Desktop_Qt_6_4_2_MinGW_64_bit-Debug 113 | D:/Qt/QtProjects/build-ChineseChess-Desktop_Qt_6_4_2_MinGW_64_bit-Debug 114 | 115 | 116 | true 117 | QtProjectManager.QMakeBuildStep 118 | false 119 | 120 | 121 | 122 | true 123 | Qt4ProjectManager.MakeStep 124 | 125 | 2 126 | 构建 127 | 构建 128 | ProjectExplorer.BuildSteps.Build 129 | 130 | 131 | 132 | true 133 | Qt4ProjectManager.MakeStep 134 | clean 135 | 136 | 1 137 | Clean 138 | Clean 139 | ProjectExplorer.BuildSteps.Clean 140 | 141 | 2 142 | false 143 | 144 | false 145 | 146 | Debug 147 | Qt4ProjectManager.Qt4BuildConfiguration 148 | 2 149 | 150 | 151 | D:\Qt\QtProjects\ChineseChess\..\build-ChineseChess-Desktop_Qt_6_4_2_MinGW_64_bit-Release 152 | D:/Qt/QtProjects/build-ChineseChess-Desktop_Qt_6_4_2_MinGW_64_bit-Release 153 | 154 | 155 | true 156 | QtProjectManager.QMakeBuildStep 157 | false 158 | 159 | 160 | 161 | true 162 | Qt4ProjectManager.MakeStep 163 | 164 | 2 165 | 构建 166 | 构建 167 | ProjectExplorer.BuildSteps.Build 168 | 169 | 170 | 171 | true 172 | Qt4ProjectManager.MakeStep 173 | clean 174 | 175 | 1 176 | Clean 177 | Clean 178 | ProjectExplorer.BuildSteps.Clean 179 | 180 | 2 181 | false 182 | 183 | false 184 | 185 | Release 186 | Qt4ProjectManager.Qt4BuildConfiguration 187 | 0 188 | 0 189 | 190 | 191 | 0 192 | D:\Qt\QtProjects\ChineseChess\..\build-ChineseChess-Desktop_Qt_6_4_2_MinGW_64_bit-Profile 193 | D:/Qt/QtProjects/build-ChineseChess-Desktop_Qt_6_4_2_MinGW_64_bit-Profile 194 | 195 | 196 | true 197 | QtProjectManager.QMakeBuildStep 198 | false 199 | 200 | 201 | 202 | true 203 | Qt4ProjectManager.MakeStep 204 | 205 | 2 206 | 构建 207 | 构建 208 | ProjectExplorer.BuildSteps.Build 209 | 210 | 211 | 212 | true 213 | Qt4ProjectManager.MakeStep 214 | clean 215 | 216 | 1 217 | Clean 218 | Clean 219 | ProjectExplorer.BuildSteps.Clean 220 | 221 | 2 222 | false 223 | 224 | false 225 | 226 | Profile 227 | Qt4ProjectManager.Qt4BuildConfiguration 228 | 0 229 | 0 230 | 0 231 | 232 | 3 233 | 234 | 235 | 0 236 | 部署 237 | 部署 238 | ProjectExplorer.BuildSteps.Deploy 239 | 240 | 1 241 | 242 | false 243 | ProjectExplorer.DefaultDeployConfiguration 244 | 245 | 1 246 | 247 | true 248 | true 249 | true 250 | 251 | 2 252 | 253 | Qt4ProjectManager.Qt4RunConfiguration:D:/Qt/QtProjects/ChineseChess/ChineseChess.pro 254 | D:/Qt/QtProjects/ChineseChess/ChineseChess.pro 255 | false 256 | true 257 | true 258 | false 259 | true 260 | D:/Qt/QtProjects/build-ChineseChess-Desktop_Qt_6_4_2_MinGW_64_bit-Release 261 | 262 | 1 263 | 264 | 265 | 266 | ProjectExplorer.Project.TargetCount 267 | 1 268 | 269 | 270 | ProjectExplorer.Project.Updater.FileVersion 271 | 22 272 | 273 | 274 | Version 275 | 22 276 | 277 | 278 | -------------------------------------------------------------------------------- /src/table/pregen.cpp: -------------------------------------------------------------------------------- 1 | #include "pregen.h" 2 | 3 | namespace PikaChess { 4 | PreGen PRE_GEN; 5 | 6 | /** 位棋盘掩码 */ 7 | extern __m128i BITBOARD_MASK[90]; 8 | /** 位棋盘反掩码 */ 9 | extern __m128i BITBOARD_NOT_MASK[90]; 10 | 11 | /** 延迟走法衰减的衰减层数 */ 12 | extern quint16 REDUCTIONS[100][128]; 13 | 14 | /** 延迟走法裁剪的裁剪层数 [第几层] */ 15 | extern quint16 LMP_MOVE_COUNT[100]; 16 | 17 | PreGen::PreGen() { 18 | // 位棋盘掩码初始化 19 | for (quint8 index { 0 }; index < 90; ++index) { 20 | BITBOARD_MASK[index] = __m128i(__uint128_t(1) << index); 21 | BITBOARD_NOT_MASK[index] = ~BITBOARD_MASK[index]; 22 | } 23 | 24 | // 默认走法初始化 25 | INVALID_MOVE.setMove(EMPTY, EMPTY, 0, 0); 26 | 27 | // 同行判断初始化 28 | for (quint8 index { 0 }; index < 90; ++index) { 29 | quint8 start = index / 9 * 9; 30 | quint8 stop = start + 9; 31 | while (start < stop) SAME_RANK[index][start++] = true; 32 | } 33 | // 因为在车炮的判断中,车炮横向移动需要判断纵向,所以需要调转过来 34 | for (auto & i : SAME_RANK) for (auto &j : i) j = not j; 35 | 36 | // 初始化捉子信息 0代表不是捉,1代表是捉,2代表需要被捉子无根,3代表需要过河且被抓子无根 37 | for (const auto &side : { RED, BLACK }) { 38 | quint8 oppSide = side ^ OPP_SIDE; 39 | // 车抓无根炮、马、过河兵 40 | CHASE_INFO[ROOK + side][CANNON + oppSide] = 2; 41 | CHASE_INFO[ROOK + side][KNIGHT + oppSide] = 2; 42 | CHASE_INFO[ROOK + side][PAWN + oppSide] = 3; 43 | 44 | // 炮抓车 45 | CHASE_INFO[CANNON + side][ROOK + oppSide] = 1; 46 | // 炮抓无根马、过河兵 47 | CHASE_INFO[CANNON + side][KNIGHT + oppSide] = 2; 48 | CHASE_INFO[CANNON + side][PAWN + oppSide] = 3; 49 | 50 | // 马抓车 51 | CHASE_INFO[KNIGHT + side][ROOK + oppSide] = 1; 52 | // 马抓无根炮、过河兵 53 | CHASE_INFO[KNIGHT + side][CANNON + oppSide] = 2; 54 | CHASE_INFO[KNIGHT + side][PAWN + oppSide] = 3; 55 | } 56 | 57 | // 生成占用位 58 | genRookOccupancy(); 59 | genKnightOccupancy(); 60 | genCannonOccupancy(); 61 | genBishopOccupancy(); 62 | genAttackByKnightOccupancy(); 63 | genChaseOccupancy(); 64 | 65 | // 计算PEXT移位 66 | genShiftRook(); 67 | genShiftKnight(); 68 | genShiftCannon(); 69 | genShiftBishop(); 70 | genShiftAttackByKnight(); 71 | genShiftChase(); 72 | 73 | // 生成走法 74 | genRook(); 75 | genKnight(); 76 | genCannon(); 77 | genRedPawn(); 78 | genBlackPawn(); 79 | genBishop(); 80 | genAdvisor(); 81 | genKing(); 82 | genAttackByKnight(); 83 | genAttackByRedPawn(); 84 | genAttackByBlackPawn(); 85 | genChase(); 86 | genSide(); 87 | 88 | // 生成Zobrist值; 89 | genZobristValues(); 90 | 91 | // 生成LMR的衰减层数,LMP的裁剪走法个数 92 | quint16 reduce[128]; 93 | for (quint8 i { 1 }; i < 128; ++i) reduce[i] = int(21.9 * std::log(i)); 94 | 95 | for (quint8 depth = 1; depth < 100; ++depth) { 96 | LMP_MOVE_COUNT[depth] = 3 + depth * depth; 97 | for (quint8 moveCount = 1; moveCount < 128; ++moveCount) { 98 | int r = reduce[depth] * reduce[moveCount]; 99 | REDUCTIONS[depth][moveCount] = (r + 534) / 1024; 100 | } 101 | } 102 | } 103 | 104 | void PreGen::genRookOccupancy() { 105 | #pragma omp parallel for 106 | for (quint8 index = 0; index < 90; ++index) { 107 | qint8 rank = index / 9; 108 | qint8 file = index % 9; 109 | Bitboard bitboard { }; 110 | // 上下左右 111 | for (qint8 up = rank - 1; up > 0; --up) bitboard.setBit(up * 9 + file); 112 | for (qint8 down = rank + 1; down < 9; ++down) bitboard.setBit(down * 9 + file); 113 | for (qint8 left = file - 1; left > 0; --left) bitboard.setBit(rank * 9 + left); 114 | for (qint8 right = file + 1; right < 8; ++right) bitboard.setBit(rank * 9 + right); 115 | this->m_rookOccupancy[index][0] = bitboard.m_bitboard[0]; 116 | this->m_rookOccupancy[index][1] = bitboard.m_bitboard[1]; 117 | } 118 | } 119 | 120 | void PreGen::genKnightOccupancy() { 121 | #pragma omp parallel for 122 | for (quint8 index = 0; index < 90; ++index) { 123 | qint8 rank = index / 9; 124 | qint8 file = index % 9; 125 | Bitboard bitboard { }; 126 | // 马腿上下左右 127 | if (rank > 0) bitboard.setBit((rank - 1) * 9 + file); 128 | if (rank < 9) bitboard.setBit((rank + 1) * 9 + file); 129 | if (file > 0) bitboard.setBit(rank * 9 + file - 1); 130 | if (file < 8) bitboard.setBit(rank * 9 + file + 1); 131 | this->m_knightOccupancy[index][0] = bitboard.m_bitboard[0]; 132 | this->m_knightOccupancy[index][1] = bitboard.m_bitboard[1]; 133 | } 134 | } 135 | 136 | void PreGen::genCannonOccupancy() { 137 | #pragma omp parallel for 138 | for (quint8 index = 0; index < 90; ++index) { 139 | qint8 rank = index / 9; 140 | qint8 file = index % 9; 141 | Bitboard bitboard { }; 142 | // 上下左右 143 | for (qint8 up = rank - 1; up >= 0; --up) bitboard.setBit(up * 9 + file); 144 | for (qint8 down = rank + 1; down <= 9; ++down) bitboard.setBit(down * 9 + file); 145 | for (qint8 left = file - 1; left >= 0; --left) bitboard.setBit(rank * 9 + left); 146 | for (qint8 right = file + 1; right <= 8; ++right) bitboard.setBit(rank * 9 + right); 147 | this->m_cannonOccupancy[index][0] = bitboard.m_bitboard[0]; 148 | this->m_cannonOccupancy[index][1] = bitboard.m_bitboard[1]; 149 | } 150 | } 151 | 152 | void PreGen::genBishopOccupancy() { 153 | #pragma omp parallel for 154 | for (quint8 index = 0; index < 90; ++index) { 155 | qint8 rank = index / 9; 156 | qint8 file = index % 9; 157 | Bitboard bitboard { }; 158 | // 可以向上走的条件 159 | if (index == 63 or index == 67 or index == 71 or index == 83 or index == 87 or 160 | index == 18 or index == 22 or index == 26 or index == 38 or index == 42) { 161 | if (file > 0) bitboard.setBit((rank - 1) * 9 + file - 1); 162 | if (file < 8) bitboard.setBit((rank - 1) * 9 + file + 1); 163 | } 164 | // 可以往下走的条件 165 | if (index == 47 or index == 51 or index == 63 or index == 67 or index == 71 or 166 | index == 2 or index == 6 or index == 18 or index == 22 or index == 26) { 167 | if (file > 0) bitboard.setBit((rank + 1) * 9 + file - 1); 168 | if (file < 8) bitboard.setBit((rank + 1) * 9 + file + 1); 169 | } 170 | this->m_bishopOccupancy[index][0] = bitboard.m_bitboard[0]; 171 | this->m_bishopOccupancy[index][1] = bitboard.m_bitboard[1]; 172 | } 173 | } 174 | 175 | void PreGen::genAttackByKnightOccupancy() { 176 | #pragma omp parallel for 177 | for (quint8 index = 0; index < 90; ++index) { 178 | qint8 rank = index / 9; 179 | qint8 file = index % 9; 180 | Bitboard bitboard { }; 181 | // 可以向上走的条件 182 | if (rank > 0) { 183 | if (file > 0) bitboard.setBit((rank - 1) * 9 + file - 1); 184 | if (file < 8) bitboard.setBit((rank - 1) * 9 + file + 1); 185 | } 186 | // 可以往下走的条件 187 | if (rank < 9) { 188 | if (file > 0) bitboard.setBit((rank + 1) * 9 + file - 1); 189 | if (file < 8) bitboard.setBit((rank + 1) * 9 + file + 1); 190 | } 191 | this->m_attackByKnightOccupancy[index][0] = bitboard.m_bitboard[0]; 192 | this->m_attackByKnightOccupancy[index][1] = bitboard.m_bitboard[1]; 193 | } 194 | } 195 | 196 | void PreGen::genChaseOccupancy() { 197 | // 先生成行的占用位 198 | #pragma omp parallel for 199 | for (quint8 index = 0; index < 90; ++index) { 200 | Bitboard bitboard; 201 | quint8 start = index / 9 * 9; 202 | quint8 stop = start + 9; 203 | while (start < stop) bitboard.setBit(start++); 204 | bitboard.clearBit(index); 205 | this->m_chaseOccupancy[index][1][0] = bitboard.m_bitboard[0]; 206 | this->m_chaseOccupancy[index][1][1] = bitboard.m_bitboard[1]; 207 | } 208 | 209 | // 然后再生成列的占用位 210 | #pragma omp parallel for 211 | for (quint8 index = 0; index < 90; ++index) { 212 | Bitboard bitboard; 213 | quint8 start = index % 9; 214 | while (start < 90) bitboard.setBit(start), start += 9; 215 | bitboard.clearBit(index); 216 | this->m_chaseOccupancy[index][0][0] = bitboard.m_bitboard[0]; 217 | this->m_chaseOccupancy[index][0][1] = bitboard.m_bitboard[1]; 218 | } 219 | } 220 | 221 | Bitboard PreGen::getEnumOccupancy(const quint64 &occ0, const quint64 &occ1, quint32 count) { 222 | Bitboard occupancy { }; 223 | occupancy.m_bitboard[0] = occ0; 224 | occupancy.m_bitboard[1] = occ1; 225 | 226 | Bitboard bitboard; 227 | for (quint8 i { 0 }; i < 90; ++i) { 228 | if (occupancy[i]) { 229 | if (count & 1) bitboard.setBit(i); 230 | count >>= 1; 231 | } 232 | } 233 | 234 | return bitboard; 235 | } 236 | 237 | void PreGen::genShiftRook() { 238 | #pragma omp parallel for 239 | for (int i = 0; i < 90; ++i) { 240 | // 计算高位的位移 241 | this->m_rookShift[i] = _mm_popcnt_u64(this->m_rookOccupancy[i][1]); 242 | } 243 | } 244 | 245 | void PreGen::genShiftKnight() { 246 | #pragma omp parallel for 247 | for (int i = 0; i < 90; ++i) { 248 | // 计算高位的位移 249 | this->m_knightShift[i] = _mm_popcnt_u64(this->m_knightOccupancy[i][1]); 250 | } 251 | } 252 | 253 | void PreGen::genShiftCannon() { 254 | #pragma omp parallel for 255 | for (int i = 0; i < 90; ++i) { 256 | // 计算高位的位移 257 | this->m_cannonShift[i] = _mm_popcnt_u64(this->m_cannonOccupancy[i][1]); 258 | } 259 | } 260 | 261 | void PreGen::genShiftBishop() { 262 | #pragma omp parallel for 263 | for (int i = 0; i < 90; ++i) { 264 | // 计算高位的位移 265 | this->m_bishopShift[i] = _mm_popcnt_u64(this->m_bishopOccupancy[i][1]); 266 | } 267 | } 268 | 269 | void PreGen::genShiftAttackByKnight() { 270 | #pragma omp parallel for 271 | for (int i = 0; i < 90; ++i) { 272 | // 计算高位的位移 273 | this->m_attackByKnightShift[i] = _mm_popcnt_u64(this->m_attackByKnightOccupancy[i][1]); 274 | } 275 | } 276 | 277 | void PreGen::genShiftChase() { 278 | #pragma omp parallel for 279 | for (quint8 i = 0; i < 90; ++i) { 280 | // 计算高位的位移 281 | this->m_chaseShift[i][0] = _mm_popcnt_u64(this->m_chaseOccupancy[i][0][1]); 282 | this->m_chaseShift[i][1] = _mm_popcnt_u64(this->m_chaseOccupancy[i][1][1]); 283 | } 284 | } 285 | 286 | Bitboard PreGen::getPreRookAttack(qint8 pos, const Bitboard &occupancy) { 287 | qint8 rank = pos / 9; 288 | qint8 file = pos % 9; 289 | Bitboard bitboard; 290 | // 向上走的情况 291 | qint8 up = rank - 1; 292 | while (up > 0 and not occupancy[up * 9 + file]) { 293 | bitboard.setBit(up * 9 + file); 294 | --up; 295 | } 296 | if (up >= 0) bitboard.setBit(up * 9 + file); 297 | // 向下走的情况 298 | qint8 down = rank + 1; 299 | while (down < 9 and not occupancy[down * 9 + file]) { 300 | bitboard.setBit(down * 9 + file); 301 | ++down; 302 | } 303 | if (down <= 9) bitboard.setBit(down * 9 + file); 304 | // 向左走的情况 305 | qint8 left = file - 1; 306 | while (left > 0 and not occupancy[rank * 9 + left]) { 307 | bitboard.setBit(rank * 9 + left); 308 | --left; 309 | } 310 | if (left >= 0) bitboard.setBit(rank * 9 + left); 311 | // 向右走的情况 312 | qint8 right = file + 1; 313 | while (right < 8 and not occupancy[rank * 9 + right]) { 314 | bitboard.setBit(rank * 9 + right); 315 | ++right; 316 | } 317 | if (right <= 8) bitboard.setBit(rank * 9 + right); 318 | return bitboard; 319 | } 320 | 321 | Bitboard PreGen::getPreKnightAttack(qint8 pos, const Bitboard &occupancy) { 322 | qint8 rank = pos / 9; 323 | qint8 file = pos % 9; 324 | Bitboard bitboard; 325 | // 马腿上下左右 326 | if (rank > 1 and not occupancy[(rank - 1) * 9 + file]) { 327 | if (file > 0) bitboard.setBit((rank - 2) * 9 + file - 1); 328 | if (file < 8) bitboard.setBit((rank - 2) * 9 + file + 1); 329 | } 330 | if (rank < 8 and not occupancy[(rank + 1) * 9 + file]) { 331 | if (file > 0) bitboard.setBit((rank + 2) * 9 + file - 1); 332 | if (file < 8) bitboard.setBit((rank + 2) * 9 + file + 1); 333 | } 334 | if (file > 1 and not occupancy[rank * 9 + file - 1]) { 335 | if (rank > 0) bitboard.setBit((rank - 1) * 9 + file - 2); 336 | if (rank < 9) bitboard.setBit((rank + 1) * 9 + file - 2); 337 | } 338 | if (file < 7 and not occupancy[rank * 9 + file + 1]) { 339 | if (rank > 0) bitboard.setBit((rank - 1) * 9 + file + 2); 340 | if (rank < 9) bitboard.setBit((rank + 1) * 9 + file + 2); 341 | } 342 | return bitboard; 343 | } 344 | 345 | Bitboard PreGen::getPreCannonAttack(qint8 pos, const Bitboard &occupancy) { 346 | qint8 rank = pos / 9; 347 | qint8 file = pos % 9; 348 | Bitboard bitboard; 349 | // 向上走的情况 350 | qint8 up = rank - 1; 351 | while (up >= 0 and not occupancy[up * 9 + file]) { 352 | bitboard.setBit(up * 9 + file); 353 | --up; 354 | } 355 | --up; 356 | while (up >= 0 and not occupancy[up * 9 + file]) --up; 357 | if (up >= 0) bitboard.setBit(up * 9 + file); 358 | // 向下走的情况 359 | qint8 down = rank + 1; 360 | while (down <= 9 and not occupancy[down * 9 + file]) { 361 | bitboard.setBit(down * 9 + file); 362 | ++down; 363 | } 364 | ++down; 365 | while (down <= 9 and not occupancy[down * 9 + file]) ++down; 366 | if (down <= 9) bitboard.setBit(down * 9 + file); 367 | // 向左走的情况 368 | qint8 left = file - 1; 369 | while (left >= 0 and not occupancy[rank * 9 + left]) { 370 | bitboard.setBit(rank * 9 + left); 371 | --left; 372 | } 373 | --left; 374 | while (left >= 0 and not occupancy[rank * 9 + left]) --left; 375 | if (left >= 0) bitboard.setBit(rank * 9 + left); 376 | // 向右走的情况 377 | qint8 right = file + 1; 378 | while (right <= 8 and not occupancy[rank * 9 + right]) { 379 | bitboard.setBit(rank * 9 + right); 380 | ++right; 381 | } 382 | ++right; 383 | while (right <= 8 and not occupancy[rank * 9 + right]) ++right; 384 | if (right <= 8) bitboard.setBit(rank * 9 + right); 385 | return bitboard; 386 | } 387 | 388 | Bitboard PreGen::getPreBishopAttack(qint8 pos, const Bitboard &occupancy) { 389 | Bitboard bitboard; 390 | // 左上 391 | if (pos == 22 or pos == 26 or pos == 38 or pos == 42 or 392 | pos == 67 or pos == 71 or pos == 83 or pos == 87) { 393 | if (not occupancy[pos - 10]) bitboard.setBit(pos - 20); 394 | } 395 | // 右上 396 | if (pos == 18 or pos == 22 or pos == 38 or pos == 42 or 397 | pos == 63 or pos == 67 or pos == 83 or pos == 87) { 398 | if (not occupancy[pos - 8]) bitboard.setBit(pos - 16); 399 | } 400 | // 左下 401 | if (pos == 2 or pos == 6 or pos == 22 or pos == 26 or 402 | pos == 47 or pos == 51 or pos == 67 or pos == 71) { 403 | if (not occupancy[pos + 8]) bitboard.setBit(pos + 16); 404 | } 405 | // 右下 406 | if (pos == 2 or pos == 6 or pos == 18 or pos == 22 or 407 | pos == 47 or pos == 51 or pos == 63 or pos == 67) { 408 | if (not occupancy[pos + 10]) bitboard.setBit(pos + 20); 409 | } 410 | return bitboard; 411 | } 412 | 413 | Bitboard PreGen::getPreAttackByKnight(qint8 pos, const Bitboard &occupancy) { 414 | qint8 rank = pos / 9; 415 | qint8 file = pos % 9; 416 | Bitboard bitboard; 417 | // 马腿斜线四方 418 | if (rank > 0 and not occupancy[(rank - 1) * 9 + file - 1]) { 419 | bitboard.setBit((rank - 1) * 9 + file - 2); 420 | if (rank > 1) bitboard.setBit((rank - 2) * 9 + file - 1); 421 | } 422 | if (rank > 0 and not occupancy[(rank - 1) * 9 + file + 1]) { 423 | bitboard.setBit((rank - 1) * 9 + file + 2); 424 | if (rank > 1) bitboard.setBit((rank - 2) * 9 + file + 1); 425 | } 426 | if (rank < 9 and not occupancy[(rank + 1) * 9 + file - 1]) { 427 | bitboard.setBit((rank + 1) * 9 + file - 2); 428 | if (rank < 8) bitboard.setBit((rank + 2) * 9 + file - 1); 429 | } 430 | if (rank < 9 and not occupancy[(rank + 1) * 9 + file + 1]) { 431 | bitboard.setBit((rank + 1) * 9 + file + 2); 432 | if (rank < 8) bitboard.setBit((rank + 2) * 9 + file + 1); 433 | } 434 | return bitboard; 435 | } 436 | 437 | void PreGen::genRook() { 438 | #pragma omp parallel for 439 | for (quint8 index = 0; index < 90; ++index) { 440 | for (quint32 i { 0 }; i < 32768; ++i) { 441 | Bitboard occupancy { getEnumOccupancy(this->m_rookOccupancy[index][0], 442 | this->m_rookOccupancy[index][1], i) }; 443 | 444 | quint64 pextIndex = occupancy.getPextIndex(this->m_rookOccupancy[index][0], 445 | this->m_rookOccupancy[index][1], 446 | this->m_rookShift[index]); 447 | 448 | this->m_rookAttack[index][pextIndex] = getPreRookAttack(index, occupancy); 449 | } 450 | } 451 | } 452 | 453 | void PreGen::genKnight() { 454 | #pragma omp parallel for 455 | for (quint8 index = 0; index < 90; ++index) { 456 | for (quint32 i { 0 }; i < 16; ++i) { 457 | Bitboard occupancy { getEnumOccupancy(this->m_knightOccupancy[index][0], 458 | this->m_knightOccupancy[index][1], i) }; 459 | 460 | quint64 pextIndex = occupancy.getPextIndex(this->m_knightOccupancy[index][0], 461 | this->m_knightOccupancy[index][1], 462 | this->m_knightShift[index]); 463 | 464 | this->m_knightAttack[index][pextIndex] = getPreKnightAttack(index, occupancy); 465 | } 466 | } 467 | } 468 | 469 | void PreGen::genCannon() { 470 | #pragma omp parallel for 471 | for (quint8 index = 0; index < 90; ++index) { 472 | for (quint32 i { 0 }; i < 131072; ++i) { 473 | Bitboard occupancy { getEnumOccupancy(this->m_cannonOccupancy[index][0], 474 | this->m_cannonOccupancy[index][1], i) }; 475 | 476 | quint64 pextIndex = occupancy.getPextIndex(this->m_cannonOccupancy[index][0], 477 | this->m_cannonOccupancy[index][1], 478 | this->m_cannonShift[index]); 479 | 480 | this->m_cannonAttack[index][pextIndex] = getPreCannonAttack(index, occupancy); 481 | } 482 | } 483 | } 484 | 485 | void PreGen::genBishop() { 486 | std::vector poss { 2, 6, 18, 22, 26, 38, 42, 47, 51, 63, 67, 71, 83, 87 }; 487 | #pragma omp parallel for 488 | for (quint8 index : poss) { 489 | for (quint32 i { 0 }; i < 16; ++i) { 490 | Bitboard occupancy { getEnumOccupancy(this->m_bishopOccupancy[index][0], 491 | this->m_bishopOccupancy[index][1], i) }; 492 | 493 | quint64 pextIndex = occupancy.getPextIndex(this->m_bishopOccupancy[index][0], 494 | this->m_bishopOccupancy[index][1], 495 | this->m_bishopShift[index]); 496 | 497 | this->m_bishopAttack[index][pextIndex] = getPreBishopAttack(index, occupancy); 498 | } 499 | } 500 | } 501 | 502 | void PreGen::genRedPawn() { 503 | #pragma omp parallel for 504 | for (quint8 index = 0; index <= 62; ++index) { 505 | if (index == 46 or index == 55 or index == 48 or index == 57 or index == 50 or index == 59 506 | or index == 52 or index == 61) continue; 507 | qint8 rank = index / 9; 508 | qint8 file = index % 9; 509 | Bitboard &bitboard { this->m_redPawnAttack[index] }; 510 | // 前面的一个格子 511 | if (rank > 0) bitboard.setBit((rank - 1) * 9 + file); 512 | // 过河后左右格子也要看 513 | if (rank < 5) { 514 | if (file > 0) bitboard.setBit(rank * 9 + file - 1); 515 | if (file < 8) bitboard.setBit(rank * 9 + file + 1); 516 | } 517 | } 518 | } 519 | 520 | void PreGen::genBlackPawn() { 521 | #pragma omp parallel for 522 | for (quint8 index = 27; index < 90; ++index) { 523 | if (index == 28 or index == 37 or index == 30 or index == 39 or index == 32 or index == 41 524 | or index == 34 or index == 43) continue; 525 | qint8 rank = index / 9; 526 | qint8 file = index % 9; 527 | Bitboard &bitboard { this->m_blackPawnAttack[index] }; 528 | // 前面的一个格子 529 | if (rank < 9) bitboard.setBit((rank + 1) * 9 + file); 530 | // 过河后左右格子也要看 531 | if (rank > 4) { 532 | if (file > 0) bitboard.setBit(rank * 9 + file - 1); 533 | if (file < 8) bitboard.setBit(rank * 9 + file + 1); 534 | } 535 | } 536 | } 537 | 538 | void PreGen::genAdvisor() { 539 | #pragma omp parallel for 540 | for (quint8 index = 0; index < 90; ++index) { 541 | qint8 rank = index / 9; 542 | qint8 file = index % 9; 543 | Bitboard &bitboard { this->m_advisorAttack[index] }; 544 | // 可以向左上走的条件 545 | if (index == 13 or index == 23 or index == 76 or index == 86) { 546 | bitboard.setBit((rank - 1) * 9 + file - 1); 547 | } 548 | // 可以向右上走的条件 549 | if (index == 13 or index == 21 or index == 76 or index == 84) { 550 | bitboard.setBit((rank - 1) * 9 + file + 1); 551 | } 552 | // 可以往左下走的条件 553 | if (index == 5 or index == 13 or index == 68 or index == 76) { 554 | bitboard.setBit((rank + 1) * 9 + file - 1); 555 | } 556 | // 可以往右下走的条件 557 | if (index == 3 or index == 13 or index == 66 or index == 76) { 558 | bitboard.setBit((rank + 1) * 9 + file + 1); 559 | } 560 | } 561 | } 562 | 563 | void PreGen::genKing() { 564 | #pragma omp parallel for 565 | for (quint8 index = 0; index < 90; ++index) { 566 | qint8 rank = index / 9; 567 | qint8 file = index % 9; 568 | Bitboard &bitboard { this->m_kingAttack[index] }; 569 | // 可以往上走一个格子的情况 570 | if (index == 12 or index == 13 or index == 14 or 571 | index == 21 or index == 22 or index == 23 or 572 | index == 75 or index == 76 or index == 77 or 573 | index == 84 or index == 85 or index == 86) { 574 | bitboard.setBit((rank - 1) * 9 + file); 575 | } 576 | // 可以往下走一个格子的情况 577 | if (index == 3 or index == 4 or index == 5 or 578 | index == 12 or index == 13 or index == 14 or 579 | index == 66 or index == 67 or index == 68 or 580 | index == 75 or index == 76 or index == 77) { 581 | bitboard.setBit((rank + 1) * 9 + file); 582 | } 583 | // 可以往左走一个格子的情况 584 | if (index == 4 or index == 5 or index == 13 or 585 | index == 14 or index == 22 or index == 23 or 586 | index == 67 or index == 68 or index == 76 or 587 | index == 77 or index == 85 or index == 86) { 588 | bitboard.setBit(rank * 9 + file - 1); 589 | } 590 | // 可以往右走一个格子的情况 591 | if (index == 3 or index == 4 or index == 12 or 592 | index == 13 or index == 21 or index == 22 or 593 | index == 66 or index == 67 or index == 75 or 594 | index == 76 or index == 84 or index == 85) { 595 | bitboard.setBit(rank * 9 + file + 1); 596 | } 597 | } 598 | } 599 | 600 | void PreGen::genAttackByKnight() { 601 | #pragma omp parallel for 602 | for (quint8 index = 0; index < 90; ++index) { 603 | for (quint32 i { 0 }; i < 16; ++i) { 604 | Bitboard occupancy { getEnumOccupancy(this->m_attackByKnightOccupancy[index][0], 605 | this->m_attackByKnightOccupancy[index][1], i) }; 606 | 607 | quint64 pextIndex = occupancy.getPextIndex(this->m_attackByKnightOccupancy[index][0], 608 | this->m_attackByKnightOccupancy[index][1], 609 | this->m_attackByKnightShift[index]); 610 | 611 | this->m_attackByKnight[index][pextIndex] = getPreAttackByKnight(index, occupancy); 612 | } 613 | } 614 | } 615 | 616 | void PreGen::genAttackByRedPawn() { 617 | #pragma omp parallel for 618 | for (quint8 index = 0; index <= 62; ++index) { 619 | qint8 rank = index / 9; 620 | qint8 file = index % 9; 621 | Bitboard &bitboard { this->m_attackByRedPawn[index] }; 622 | // 后面的一个格子 623 | if (index <= 35 or index == 45 or index == 36 or 624 | index == 47 or index == 38 or 625 | index == 49 or index == 40 or 626 | index == 51 or index == 42 or 627 | index == 53 or index == 44) { 628 | bitboard.setBit((rank + 1) * 9 + file); 629 | } 630 | // 左右格子 631 | if (index <= 44) { 632 | if (file > 0) bitboard.setBit(rank * 9 + file - 1); 633 | if (file < 8) bitboard.setBit(rank * 9 + file + 1); 634 | } 635 | } 636 | } 637 | 638 | void PreGen::genAttackByBlackPawn() { 639 | #pragma omp parallel for 640 | for (quint8 index = 27; index < 90; ++index) { 641 | qint8 rank = index / 9; 642 | qint8 file = index % 9; 643 | Bitboard &bitboard { this->m_attackByBlackPawn[index] }; 644 | // 前面的一个格子 645 | if (index >= 54 or index == 45 or index == 36 or 646 | index == 47 or index == 38 or 647 | index == 49 or index == 40 or 648 | index == 51 or index == 42 or 649 | index == 53 or index == 44) { 650 | bitboard.setBit((rank - 1) * 9 + file); 651 | } 652 | // 左右格子 653 | if (index >= 45) { 654 | if (file > 0) bitboard.setBit(rank * 9 + file - 1); 655 | if (file < 8) bitboard.setBit(rank * 9 + file + 1); 656 | } 657 | } 658 | } 659 | 660 | void PreGen::genChase() { 661 | // 先生成列的捉位 662 | #pragma omp parallel for 663 | for (quint8 index = 0; index < 90; ++index) { 664 | for (quint16 i { 0 }; i < 1 << 9; ++i) { 665 | Bitboard occupancy { getEnumOccupancy(this->m_chaseOccupancy[index][0][0], 666 | this->m_chaseOccupancy[index][0][1], i) }; 667 | 668 | quint64 pextIndex = occupancy.getPextIndex(this->m_chaseOccupancy[index][0][0], 669 | this->m_chaseOccupancy[index][0][1], 670 | this->m_chaseShift[index][0]); 671 | 672 | this->m_rookChase[index][0][pextIndex] = getRookAttack(index, occupancy) & occupancy; 673 | 674 | // 炮的捉子另外处理 675 | qint8 rank = index / 9; 676 | qint8 file = index % 9; 677 | Bitboard bitboard; 678 | // 向上走的情况 679 | qint8 up = rank - 1; 680 | while (up >= 0 and not occupancy[up * 9 + file]) --up; 681 | --up; 682 | while (up >= 0 and not occupancy[up * 9 + file]) --up; 683 | if (up >= 0) bitboard.setBit(up * 9 + file); 684 | // 向下走的情况 685 | qint8 down = rank + 1; 686 | while (down <= 9 and not occupancy[down * 9 + file]) ++down; 687 | ++down; 688 | while (down <= 9 and not occupancy[down * 9 + file]) ++down; 689 | if (down <= 9) bitboard.setBit(down * 9 + file); 690 | this->m_cannonChase[index][0][pextIndex] = bitboard; 691 | } 692 | } 693 | 694 | // 再生成行的捉位 695 | #pragma omp parallel for 696 | for (quint8 index = 0; index < 90; ++index) { 697 | for (quint16 i { 0 }; i < 1 << 9; ++i) { 698 | Bitboard occupancy { getEnumOccupancy(this->m_chaseOccupancy[index][1][0], 699 | this->m_chaseOccupancy[index][1][1], i) }; 700 | 701 | quint64 pextIndex = occupancy.getPextIndex(this->m_chaseOccupancy[index][1][0], 702 | this->m_chaseOccupancy[index][1][1], 703 | this->m_chaseShift[index][1]); 704 | 705 | this->m_rookChase[index][1][pextIndex] = getRookAttack(index, occupancy) & occupancy; 706 | 707 | // 炮的捉子另外处理 708 | qint8 rank = index / 9; 709 | qint8 file = index % 9; 710 | Bitboard bitboard; 711 | // 向左走的情况 712 | qint8 left = file - 1; 713 | while (left >= 0 and not occupancy[rank * 9 + left]) --left; 714 | --left; 715 | while (left >= 0 and not occupancy[rank * 9 + left]) --left; 716 | if (left >= 0) bitboard.setBit(rank * 9 + left); 717 | // 向右走的情况 718 | qint8 right = file + 1; 719 | while (right <= 8 and not occupancy[rank * 9 + right]) ++right; 720 | ++right; 721 | while (right <= 8 and not occupancy[rank * 9 + right]) ++right; 722 | if (right <= 8) bitboard.setBit(rank * 9 + right); 723 | this->m_cannonChase[index][1][pextIndex] = bitboard; 724 | } 725 | } 726 | } 727 | 728 | void PreGen::genSide() { 729 | Bitboard bitboard; 730 | #pragma omp parallel for 731 | for (quint8 index = 45; index < 90; ++index) bitboard.setBit(index); 732 | this->m_redSide = bitboard; 733 | bitboard.clearAllBits(); 734 | #pragma omp parallel for 735 | for (quint8 index = 0; index < 45; ++index) bitboard.setBit(index); 736 | this->m_blackSide = bitboard; 737 | } 738 | 739 | void PreGen::genZobristValues() { 740 | // 随机数生成引擎 741 | std::mt19937_64 engine(time(NULL)); 742 | 743 | // 均匀分布 744 | std::uniform_int_distribution uniform; 745 | 746 | // 生成每一个位置的Zobrist值 747 | for (quint8 index { 0 }; index < 90; ++index) { 748 | // 生成这个位置上每一个棋子的Zobrist值 749 | for (quint8 chess { RED_ROOK }; chess <= BLACK_KING; ++chess) { 750 | this->m_zobrists[index][chess] = uniform(engine); 751 | } 752 | } 753 | 754 | // 生成选边的Zobrist值 755 | this->m_sideZobrist = uniform(engine); 756 | } 757 | 758 | Bitboard PreGen::getAttack(quint8 chessType, quint8 index, const Bitboard &occupancy) { 759 | // 保证case n(n连续),留给编译器使用函数指针数组来优化 760 | switch (chessType) { 761 | case RED_ROOK: return getRookAttack(index, occupancy); 762 | case RED_KNIGHT: return getKnightAttack(index, occupancy); 763 | case RED_CANNON: return getCannonAttack(index, occupancy); 764 | case RED_BISHOP: return getBishopAttack(index, occupancy); 765 | case RED_PAWN: return getRedPawnAttack(index); 766 | case RED_ADVISOR: return getAdvisorAttack(index); 767 | case RED_KING: return getKingAttack(index); 768 | case BLACK_ROOK: return getRookAttack(index, occupancy); 769 | case BLACK_KNIGHT: return getKnightAttack(index, occupancy); 770 | case BLACK_CANNON: return getCannonAttack(index, occupancy); 771 | case BLACK_BISHOP: return getBishopAttack(index, occupancy); 772 | case BLACK_PAWN: return getBlackPawnAttack(index); 773 | case BLACK_ADVISOR: return getAdvisorAttack(index); 774 | case BLACK_KING: return getKingAttack(index); 775 | } 776 | return { }; 777 | } 778 | 779 | Bitboard PreGen::getRookAttack(quint8 index, const Bitboard &occupancy) const { 780 | quint64 pextIndex { occupancy.getPextIndex(this->m_rookOccupancy[index][0], 781 | this->m_rookOccupancy[index][1], 782 | this->m_rookShift[index]) }; 783 | return this->m_rookAttack[index][pextIndex]; 784 | } 785 | 786 | Bitboard PreGen::getKnightAttack(quint8 index, const Bitboard &occupancy) const { 787 | quint64 pextIndex { occupancy.getPextIndex(this->m_knightOccupancy[index][0], 788 | this->m_knightOccupancy[index][1], 789 | this->m_knightShift[index]) }; 790 | return this->m_knightAttack[index][pextIndex]; 791 | } 792 | 793 | Bitboard PreGen::getCannonAttack(quint8 index, const Bitboard &occupancy) const { 794 | quint64 pextIndex { occupancy.getPextIndex(this->m_cannonOccupancy[index][0], 795 | this->m_cannonOccupancy[index][1], 796 | this->m_cannonShift[index]) }; 797 | return this->m_cannonAttack[index][pextIndex]; 798 | } 799 | 800 | Bitboard PreGen::getBishopAttack(quint8 index, const Bitboard &occupancy) const { 801 | quint64 pextIndex { occupancy.getPextIndex(this->m_bishopOccupancy[index][0], 802 | this->m_bishopOccupancy[index][1], 803 | this->m_bishopShift[index]) }; 804 | return this->m_bishopAttack[index][pextIndex]; 805 | } 806 | 807 | Bitboard PreGen::getRedPawnAttack(quint8 index) const { return this->m_redPawnAttack[index]; } 808 | 809 | Bitboard PreGen::getBlackPawnAttack(quint8 index) const { return this->m_blackPawnAttack[index]; } 810 | 811 | Bitboard PreGen::getAdvisorAttack(quint8 index) const { return this->m_advisorAttack[index]; } 812 | 813 | Bitboard PreGen::getKingAttack(quint8 index) const { return this->m_kingAttack[index]; } 814 | 815 | Bitboard PreGen::getAttackByKnight(quint8 index, const Bitboard &occupancy) const { 816 | quint64 pextIndex { occupancy.getPextIndex(this->m_attackByKnightOccupancy[index][0], 817 | this->m_attackByKnightOccupancy[index][1], 818 | this->m_attackByKnightShift[index]) }; 819 | return this->m_attackByKnight[index][pextIndex]; 820 | } 821 | 822 | Bitboard PreGen::getAttackByPawn(quint8 side, quint8 index) const { 823 | if (RED == side) return this->m_attackByBlackPawn[index]; 824 | else return this->m_attackByRedPawn[index]; 825 | } 826 | 827 | Bitboard PreGen::getRookChase(quint8 index, bool rank, const Bitboard &occupancy) const { 828 | quint64 pextIndex { occupancy.getPextIndex(this->m_chaseOccupancy[index][rank][0], 829 | this->m_chaseOccupancy[index][rank][1], 830 | this->m_chaseShift[index][rank]) }; 831 | return this->m_rookChase[index][rank][pextIndex]; 832 | } 833 | 834 | Bitboard PreGen::getCannonChase(quint8 index, bool rank, const Bitboard &occupancy) const { 835 | quint64 pextIndex { occupancy.getPextIndex(this->m_chaseOccupancy[index][rank][0], 836 | this->m_chaseOccupancy[index][rank][1], 837 | this->m_chaseShift[index][rank]) }; 838 | return this->m_cannonChase[index][rank][pextIndex]; 839 | } 840 | 841 | Bitboard PreGen::getSide(quint8 side) const { 842 | if (RED == side) return this->getRedSide(); 843 | else return this->getBlackSide(); 844 | } 845 | 846 | Bitboard PreGen::getRedSide() const { return this->m_redSide; } 847 | 848 | Bitboard PreGen::getBlackSide() const { return this->m_blackSide; } 849 | 850 | quint64 PreGen::getZobrist(quint8 chess, quint8 index) const { 851 | return this->m_zobrists[index][chess]; 852 | } 853 | 854 | quint64 PreGen::getSideZobrist() const { return this->m_sideZobrist; } 855 | } 856 | -------------------------------------------------------------------------------- /src/GUI/dialog.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | Dialog 4 | 5 | 6 | true 7 | 8 | 9 | 10 | 0 11 | 0 12 | 1000 13 | 940 14 | 15 | 16 | 17 | 18 | Microsoft YaHei Light 19 | -1 20 | 21 | 22 | 23 | 中国象棋 24 | 25 | 26 | *{ 27 | font-family:"Microsoft YaHei Light"; 28 | font-size:18px; 29 | } 30 | 31 | 32 | 33 | 34 | 10 35 | 10 36 | 980 37 | 920 38 | 39 | 40 | 41 | 42 | Microsoft YaHei Light 43 | -1 44 | 45 | 46 | 47 | Qt::ClickFocus 48 | 49 | 50 | #frame{ 51 | background-color: transparent; 52 | border-image: url(:/Images/bg.jpg); 53 | border-radius:6px; 54 | opacity:0.5; 55 | } 56 | 57 | 58 | QFrame::StyledPanel 59 | 60 | 61 | QFrame::Raised 62 | 63 | 64 | 65 | 66 | 0 67 | 0 68 | 70 69 | 70 70 | 71 | 72 | 73 | border-image: url(:ChessImage/ChessIcon.ico); 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 260 83 | 0 84 | 431 85 | 61 86 | 87 | 88 | 89 | font-size:40px 90 | 91 | 92 | 中国象棋 ChineseChess 93 | 94 | 95 | 96 | 97 | 98 | 887 99 | 0 100 | 48 101 | 48 102 | 103 | 104 | 105 | 106 | 0 107 | 0 108 | 109 | 110 | 111 | #MinButton{ 112 | border-image:url(:/Images/min.ico); 113 | border-radius:5px; 114 | } 115 | #MinButton:pressed{ 116 | border-image:url(:/Images/min_press.bmp); 117 | } 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 932 127 | 0 128 | 48 129 | 48 130 | 131 | 132 | 133 | 134 | 0 135 | 0 136 | 137 | 138 | 139 | 140 | 0 141 | 0 142 | 143 | 144 | 145 | #CloseButton{ 146 | border-image:url(:/Images/close.ico); 147 | border-radius:5px; 148 | } 149 | 150 | #CloseButton:pressed{ 151 | border-image:url(:/Images/close_press.ico); 152 | } 153 | 154 | 155 | 156 | 157 | 158 | false 159 | 160 | 161 | 162 | 163 | 164 | 20 165 | 80 166 | 741 167 | 821 168 | 169 | 170 | 171 | border-image: url(:/ChessImage/BOARD.BMP); 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 30 181 | 90 182 | 80 183 | 80 184 | 185 | 186 | 187 | border-image: url(:/ChessImage/br.ico); 188 | color:transparent; 189 | 190 | 191 | b 192 | 193 | 194 | Qt::AutoText 195 | 196 | 197 | false 198 | 199 | 200 | 201 | 202 | 203 | 110 204 | 90 205 | 80 206 | 80 207 | 208 | 209 | 210 | border-image: url(:/ChessImage/bn.ico); 211 | color:transparent; 212 | 213 | 214 | b 215 | 216 | 217 | Qt::AutoText 218 | 219 | 220 | 221 | 222 | 223 | 190 224 | 90 225 | 80 226 | 80 227 | 228 | 229 | 230 | border-image: url(:/ChessImage/bb.ico); 231 | color:transparent; 232 | 233 | 234 | b 235 | 236 | 237 | Qt::AutoText 238 | 239 | 240 | 241 | 242 | 243 | 270 244 | 90 245 | 80 246 | 80 247 | 248 | 249 | 250 | border-image: url(:/ChessImage/ba.ico); 251 | color:transparent; 252 | 253 | 254 | b 255 | 256 | 257 | Qt::AutoText 258 | 259 | 260 | 261 | 262 | 263 | 350 264 | 90 265 | 80 266 | 80 267 | 268 | 269 | 270 | border-image: url(:/ChessImage/bk.ico); 271 | color:transparent; 272 | 273 | 274 | b 275 | 276 | 277 | Qt::AutoText 278 | 279 | 280 | 281 | 282 | 283 | 430 284 | 90 285 | 80 286 | 80 287 | 288 | 289 | 290 | border-image: url(:/ChessImage/ba.ico); 291 | color:transparent; 292 | 293 | 294 | b 295 | 296 | 297 | Qt::AutoText 298 | 299 | 300 | 301 | 302 | 303 | 510 304 | 90 305 | 80 306 | 80 307 | 308 | 309 | 310 | border-image: url(:/ChessImage/bb.ico); 311 | color:transparent; 312 | 313 | 314 | b 315 | 316 | 317 | Qt::AutoText 318 | 319 | 320 | 321 | 322 | 323 | 590 324 | 90 325 | 80 326 | 80 327 | 328 | 329 | 330 | border-image: url(:/ChessImage/bn.ico); 331 | color:transparent; 332 | 333 | 334 | b 335 | 336 | 337 | Qt::AutoText 338 | 339 | 340 | 341 | 342 | 343 | 670 344 | 90 345 | 80 346 | 80 347 | 348 | 349 | 350 | border-image: url(:/ChessImage/br.ico); 351 | color:transparent; 352 | 353 | 354 | b 355 | 356 | 357 | Qt::AutoText 358 | 359 | 360 | 361 | 362 | 363 | 110 364 | 250 365 | 80 366 | 80 367 | 368 | 369 | 370 | border-image: url(:/ChessImage/bc.ico); 371 | color:transparent; 372 | 373 | 374 | b 375 | 376 | 377 | 378 | 379 | 380 | 590 381 | 250 382 | 80 383 | 80 384 | 385 | 386 | 387 | border-image: url(:/ChessImage/bc.ico); 388 | color:transparent; 389 | 390 | 391 | b 392 | 393 | 394 | 395 | 396 | 397 | 30 398 | 330 399 | 80 400 | 80 401 | 402 | 403 | 404 | border-image: url(:/ChessImage/bp.ico); 405 | color:transparent; 406 | 407 | 408 | b 409 | 410 | 411 | 412 | 413 | 414 | 190 415 | 330 416 | 80 417 | 80 418 | 419 | 420 | 421 | border-image: url(:/ChessImage/bp.ico); 422 | color:transparent; 423 | 424 | 425 | b 426 | 427 | 428 | 429 | 430 | 431 | 350 432 | 330 433 | 80 434 | 80 435 | 436 | 437 | 438 | border-image: url(:/ChessImage/bp.ico); 439 | color:transparent; 440 | 441 | 442 | b 443 | 444 | 445 | 446 | 447 | 448 | 510 449 | 330 450 | 80 451 | 80 452 | 453 | 454 | 455 | border-image: url(:/ChessImage/bp.ico); 456 | color:transparent; 457 | 458 | 459 | b 460 | 461 | 462 | 463 | 464 | 465 | 670 466 | 330 467 | 80 468 | 80 469 | 470 | 471 | 472 | border-image: url(:/ChessImage/bp.ico); 473 | color:transparent; 474 | 475 | 476 | b 477 | 478 | 479 | 480 | 481 | 482 | 30 483 | 570 484 | 80 485 | 80 486 | 487 | 488 | 489 | border-image: url(:/ChessImage/rp.ico); 490 | color:transparent; 491 | 492 | 493 | r 494 | 495 | 496 | 497 | 498 | 499 | 190 500 | 570 501 | 80 502 | 80 503 | 504 | 505 | 506 | border-image: url(:/ChessImage/rp.ico); 507 | color:transparent; 508 | 509 | 510 | r 511 | 512 | 513 | 514 | 515 | 516 | 350 517 | 570 518 | 80 519 | 80 520 | 521 | 522 | 523 | border-image: url(:/ChessImage/rp.ico); 524 | color:transparent; 525 | 526 | 527 | r 528 | 529 | 530 | 531 | 532 | 533 | 510 534 | 570 535 | 80 536 | 80 537 | 538 | 539 | 540 | border-image: url(:/ChessImage/rp.ico); 541 | color:transparent; 542 | 543 | 544 | r 545 | 546 | 547 | 548 | 549 | 550 | 670 551 | 570 552 | 80 553 | 80 554 | 555 | 556 | 557 | border-image: url(:/ChessImage/rp.ico); 558 | color:transparent; 559 | 560 | 561 | r 562 | 563 | 564 | 565 | 566 | 567 | 110 568 | 650 569 | 80 570 | 80 571 | 572 | 573 | 574 | border-image: url(:/ChessImage/rc.ico); 575 | color:transparent; 576 | 577 | 578 | r 579 | 580 | 581 | 582 | 583 | 584 | 590 585 | 650 586 | 80 587 | 80 588 | 589 | 590 | 591 | border-image: url(:/ChessImage/rc.ico); 592 | color:transparent; 593 | 594 | 595 | r 596 | 597 | 598 | Qt::RichText 599 | 600 | 601 | 602 | 603 | 604 | 30 605 | 810 606 | 80 607 | 80 608 | 609 | 610 | 611 | border-image: url(:/ChessImage/rr.ico); 612 | color:transparent; 613 | 614 | 615 | r 616 | 617 | 618 | 619 | 620 | 621 | 190 622 | 810 623 | 80 624 | 80 625 | 626 | 627 | 628 | border-image: url(:/ChessImage/rb.ico); 629 | color:transparent; 630 | 631 | 632 | r 633 | 634 | 635 | 636 | 637 | 638 | 110 639 | 810 640 | 80 641 | 80 642 | 643 | 644 | 645 | border-image: url(:/ChessImage/rn.ico); 646 | color:transparent; 647 | 648 | 649 | r 650 | 651 | 652 | 653 | 654 | 655 | 270 656 | 810 657 | 80 658 | 80 659 | 660 | 661 | 662 | border-image: url(:/ChessImage/ra.ico); 663 | color:transparent; 664 | 665 | 666 | r 667 | 668 | 669 | 670 | 671 | 672 | 350 673 | 810 674 | 80 675 | 80 676 | 677 | 678 | 679 | border-image: url(:/ChessImage/rk.ico); 680 | color:transparent; 681 | 682 | 683 | r 684 | 685 | 686 | 687 | 688 | 689 | 430 690 | 810 691 | 80 692 | 80 693 | 694 | 695 | 696 | border-image: url(:/ChessImage/ra.ico); 697 | color:transparent; 698 | 699 | 700 | r 701 | 702 | 703 | 704 | 705 | 706 | 510 707 | 810 708 | 80 709 | 80 710 | 711 | 712 | 713 | border-image: url(:/ChessImage/rb.ico); 714 | color:transparent; 715 | 716 | 717 | r 718 | 719 | 720 | 721 | 722 | 723 | 670 724 | 810 725 | 80 726 | 80 727 | 728 | 729 | 730 | border-image: url(:/ChessImage/rr.ico); 731 | color:transparent; 732 | 733 | 734 | r 735 | 736 | 737 | 738 | 739 | 740 | 590 741 | 810 742 | 80 743 | 80 744 | 745 | 746 | 747 | border-image: url(:/ChessImage/rn.ico); 748 | color:transparent; 749 | 750 | 751 | r 752 | 753 | 754 | 755 | 756 | 757 | 770 758 | 70 759 | 201 760 | 351 761 | 762 | 763 | 764 | Qt::LeftToRight 765 | 766 | 767 | 768 | 769 | 770 | 中国象棋云库 771 | 772 | 773 | Qt::AlignCenter 774 | 775 | 776 | 777 | 778 | 0 779 | 20 780 | 201 781 | 61 782 | 783 | 784 | 785 | font:40px; 786 | color:green; 787 | 788 | 789 | 云库出步 790 | 791 | 792 | Qt::AlignCenter 793 | 794 | 795 | 796 | 797 | 798 | 10 799 | 80 800 | 181 801 | 21 802 | 803 | 804 | 805 | Qt::Horizontal 806 | 807 | 808 | 809 | 810 | 811 | 10 812 | 100 813 | 181 814 | 231 815 | 816 | 817 | 818 | font:22px 819 | 820 | 821 | 默认由ChessDB提供的中国象棋云库出步。 822 | 823 | 云库无对应走法或获取走法失败时由引擎出步。 824 | 825 | 826 | Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter 827 | 828 | 829 | true 830 | 831 | 832 | 833 | 834 | 835 | 836 | 770 837 | 440 838 | 201 839 | 111 840 | 841 | 842 | 843 | 红黑方选择 844 | 845 | 846 | Qt::AlignCenter 847 | 848 | 849 | 850 | 851 | 110 852 | 40 853 | 71 854 | 51 855 | 856 | 857 | 858 | font:40px 859 | 860 | 861 | 862 | 863 | 864 | 865 | 866 | 867 | 868 | 869 | 870 | 871 | 872 | 873 | 874 | 20 875 | 40 876 | 81 877 | 51 878 | 879 | 880 | 881 | font:40px 882 | 883 | 884 | 我执 885 | 886 | 887 | 888 | 889 | 890 | 891 | 770 892 | 720 893 | 201 894 | 81 895 | 896 | 897 | 898 | font:30px; 899 | 900 | 901 | 🆕 重置棋盘 902 | 903 | 904 | 905 | 906 | 907 | 770 908 | 820 909 | 201 910 | 81 911 | 912 | 913 | 914 | font:30px; 915 | 916 | 917 | 🔃 翻转棋盘 918 | 919 | 920 | 921 | 922 | 923 | 350 924 | 450 925 | 80 926 | 80 927 | 928 | 929 | 930 | border-image: url(:/ChessImage/mask2.png); 931 | color:transparent; 932 | 933 | 934 | b 935 | 936 | 937 | 938 | 939 | 940 | 350 941 | 450 942 | 80 943 | 80 944 | 945 | 946 | 947 | border-image: url(:/ChessImage/mask.png); 948 | color:transparent; 949 | 950 | 951 | b 952 | 953 | 954 | 955 | 956 | 957 | 350 958 | 450 959 | 80 960 | 80 961 | 962 | 963 | 964 | border-image: url(:/ChessImage/mask.png); 965 | color:transparent; 966 | 967 | 968 | b 969 | 970 | 971 | 972 | 973 | 974 | 770 975 | 570 976 | 201 977 | 121 978 | 979 | 980 | 981 | 最少思考时间 982 | 983 | 984 | Qt::AlignCenter 985 | 986 | 987 | 988 | 989 | 10 990 | 40 991 | 181 992 | 61 993 | 994 | 995 | 996 | font:40px 997 | 998 | 999 | 2 1000 | 1001 | 1002 | 1003 | 1 秒 1004 | 1005 | 1006 | 1007 | 1008 | 2 秒 1009 | 1010 | 1011 | 1012 | 1013 | 3 秒 1014 | 1015 | 1016 | 1017 | 1018 | 4 秒 1019 | 1020 | 1021 | 1022 | 1023 | 5 秒 1024 | 1025 | 1026 | 1027 | 1028 | 6 秒 1029 | 1030 | 1031 | 1032 | 1033 | 7 秒 1034 | 1035 | 1036 | 1037 | 1038 | 8 秒 1039 | 1040 | 1041 | 1042 | 1043 | 9 秒 1044 | 1045 | 1046 | 1047 | 1048 | Icon 1049 | Title 1050 | MinButton 1051 | CloseButton 1052 | ChessBoard 1053 | BlackChe1 1054 | BlackMa1 1055 | BlackXiang1 1056 | BlackShi1 1057 | BlackJiang 1058 | BlackShi2 1059 | BlackXiang2 1060 | BlackMa2 1061 | BlackChe2 1062 | BlackPao1 1063 | BlackPao2 1064 | BlackBing1 1065 | BlackBing2 1066 | BlackBing3 1067 | BlackBing4 1068 | BlackBing5 1069 | RedBing1 1070 | RedBing2 1071 | RedBing3 1072 | RedBing4 1073 | RedBing5 1074 | RedPao1 1075 | RedPao2 1076 | RedChe1 1077 | RedXiang1 1078 | RedMa1 1079 | RedShi1 1080 | RedJiang 1081 | RedShi2 1082 | RedXiang2 1083 | RedChe2 1084 | RedMa2 1085 | ComputerScoreBox 1086 | SideSelectionBox 1087 | Flip 1088 | Reset 1089 | Mask1 1090 | Mask2 1091 | Mask3 1092 | HardSelectionBox 1093 | 1094 | 1095 | 1096 | 1097 | 1098 | --------------------------------------------------------------------------------