From d38200dce57c2cab106bbe4de565ce5d6a82e2f5 Mon Sep 17 00:00:00 2001 From: CalciteM Team Date: Sun, 7 Jul 2019 01:04:43 +0800 Subject: [PATCH] =?UTF-8?q?=E6=AF=8F=E8=B5=B0=E4=B8=80=E6=AD=A5=E6=A3=8B?= =?UTF-8?q?=E6=9B=B4=E6=96=B0=20Hash=20=E5=80=BC=E4=BB=A5=E6=8F=90?= =?UTF-8?q?=E5=8D=87=E6=95=88=E7=8E=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 已知问题: 棋力明显下滑,待调试 --- NineChess/src/config.h | 6 +- NineChess/src/ninechess.cpp | 167 ++++++++++++++++++++++++------- NineChess/src/ninechess.h | 46 ++++++++- NineChess/src/ninechessai_ab.cpp | 15 ++- NineChess/src/ninechessai_ab.h | 3 +- 5 files changed, 190 insertions(+), 47 deletions(-) diff --git a/NineChess/src/config.h b/NineChess/src/config.h index c63d713c..d23f4416 100644 --- a/NineChess/src/config.h +++ b/NineChess/src/config.h @@ -1,13 +1,13 @@ #ifndef CONFIG_H #define CONFIG_H -//#define DEBUG +#define DEBUG -#define DEAL_WITH_HORIZON_EFFECT +//#define DEAL_WITH_HORIZON_EFFECT //#define RANDOM_BEST_MOVE -//#define HASH_MAP_ENABLE +#define HASH_MAP_ENABLE #ifdef DEBUG #define DONOT_PLAY_SOUND diff --git a/NineChess/src/ninechess.cpp b/NineChess/src/ninechess.cpp index 158d4a9b..993fac50 100644 --- a/NineChess/src/ninechess.cpp +++ b/NineChess/src/ninechess.cpp @@ -112,15 +112,20 @@ const int NineChess::onBoard[(N_RINGS + 2) * N_SEATS] = { }; // 着法表 -int NineChess::moveTable[(N_RINGS + 2) * N_SEATS][N_MOVE_DIRECTIONS] = { 0 }; +int NineChess::moveTable[N_POINTS][N_MOVE_DIRECTIONS] = { 0 }; // 成三表 -int NineChess::millTable[(N_RINGS + 2) * N_SEATS][N_DIRECTIONS][N_RINGS - 1] = { 0 }; +int NineChess::millTable[N_POINTS][N_DIRECTIONS][N_RINGS - 1] = { 0 }; NineChess::NineChess() { - // 单独提出 board,免得每次都写 context.board; + // 单独提出 board 等数据,免得每次都写 context.board; board_ = context.board; + //hash_ = &context.hash; + //zobrist_ = &context.zobrist; + + // 创建哈希数据 + constructHash(); // 默认选择第1号规则,即“打三棋” setContext(&RULES[1]); @@ -171,6 +176,24 @@ NineChess::~NineChess() { } +void NineChess::constructHash() +{ + context.hash = 0; + +#if 0 + gameMovingHash = rand64(); + player2sTurnHash = rand64(); + + uint64_t zobrist[N_POINTS][POINT_TYPE_COUNT]; + + for (int p = 0; p < N_POINTS; p++) { + for (int t = NineChess::POINT_TYPE_EMPTY; t <= NineChess::POINT_TYPE_FORBIDDEN; t++) { + zobrist[p][t] = rand64(); + } + } +#endif +} + NineChess::Player NineChess::getOpponent(NineChess::Player player) { switch (player) @@ -301,7 +324,7 @@ void NineChess::createMillTable() // 设置棋局状态和棋盘数据,用于初始化 bool NineChess::setContext(const struct Rule *rule, int maxStepsLedToDraw, int maxTimeLedToLose, int initialStep, int flags, const char *board, - int nPiecesInHand_1, int nPiecesInHand_2, int nPiecesNeedRemove) + int nPiecesInHand_1, int nPiecesInHand_2, int nPiecesNeedRemove, uint64_t hash) { // 有效性判断 if (maxStepsLedToDraw < 0 || maxTimeLedToLose < 0 || initialStep < 0 || @@ -320,24 +343,39 @@ bool NineChess::setContext(const struct Rule *rule, int maxStepsLedToDraw, int m this->currentStep = initialStep; // 局面阶段标识 - if (flags & GAME_NOTSTARTED) + if (flags & GAME_NOTSTARTED) { context.stage = GAME_NOTSTARTED; - else if (flags & GAME_PLACING) + } + else if (flags & GAME_PLACING) { context.stage = GAME_PLACING; - else if (flags & GAME_MOVING) + } + else if (flags & GAME_MOVING) { context.stage = GAME_MOVING; - else if (flags & GAME_OVER) + //context.hash ^= // TODO + } + else if (flags & GAME_OVER) { context.stage = GAME_OVER; - else + } + else { return false; + } // 轮流状态标识 - if (flags & PLAYER1) + if (flags & PLAYER1) { +// if (context.turn == PLAYER2) { +// context.hash ^= player2sTurnHash; +// } context.turn = PLAYER1; - else if (flags & PLAYER2) + } + else if (flags & PLAYER2) { +// if (context.turn == PLAYER1) { +// context.hash ^= player2sTurnHash; +// } context.turn = PLAYER2; - else + } + else { return false; + } // 动作状态标识 if (flags & ACTION_CHOOSE) @@ -352,19 +390,37 @@ bool NineChess::setContext(const struct Rule *rule, int maxStepsLedToDraw, int m // 当前棋局(3×8) if (board == nullptr) { memset(context.board, 0, sizeof(context.board)); + context.hash = 0; } else { memcpy(context.board, board, sizeof(context.board)); + context.hash = hash; } // 计算盘面子数 + // 棋局,抽象为一个(5×8)的数组,上下两行留空 + /* + 0x00 代表无棋子 + 0x0F 代表禁点 + 0x11~0x1C 代表先手第 1~12 子 + 0x21~0x2C 代表后手第 1~12 子 + 判断棋子是先手的用 (board[i] & 0x10) + 判断棋子是后手的用 (board[i] & 0x20) + */ context.nPiecesOnBoard_1 = context.nPiecesOnBoard_2 = 0; for (int r = 1; r < N_RINGS + 2; r++) { for (int s = 0; s < N_SEATS; s++) { - if (context.board[r * N_SEATS + s] & '\x10') + int pos = r * N_SEATS + s; + if (context.board[pos] & '\x10') { context.nPiecesOnBoard_1++; - else if (context.board[r * N_SEATS + s] & '\x20') { + } + else if (context.board[pos] & '\x20') { context.nPiecesOnBoard_2++; } + else if (context.board[pos] & '\x0F') { + // 不计算盘面子数 + } + + //updateHash(pos); } } @@ -434,7 +490,8 @@ bool NineChess::setContext(const struct Rule *rule, int maxStepsLedToDraw, int m } void NineChess::getContext(struct Rule &rule, int &step, int &flags, - int *&board, int &nPiecesInHand_1, int &nPiecesInHand_2, int &num_NeedRemove) + int *&board, int &nPiecesInHand_1, int &nPiecesInHand_2, int &num_NeedRemove, + uint64_t &hash) { rule = this->currentRule; step = this->currentStep; @@ -443,6 +500,7 @@ void NineChess::getContext(struct Rule &rule, int &step, int &flags, nPiecesInHand_1 = context.nPiecesInHand_1; nPiecesInHand_2 = context.nPiecesInHand_2; num_NeedRemove = context.nPiecesNeedRemove; + hash = context.hash; } bool NineChess::reset() @@ -486,6 +544,9 @@ bool NineChess::reset() // 用时置零 elapsedMS_1 = elapsedMS_2 = 0; + // 哈希归零 + context.hash = 0; + // 提示 setTips(); @@ -548,7 +609,7 @@ bool NineChess::getPieceCP(const Player &player, const int &number, int &c, int else return false; - for (int i = N_SEATS; i < N_SEATS * (N_RINGS + 1); i++) { + for (int i = POS_BEGIN; i < POS_END; i++) { if (board_[i] == piece) { pos2cp(i, c, p); return true; @@ -578,7 +639,7 @@ bool NineChess::getCurrentPiece(Player &player, int &number) bool NineChess::pos2cp(const int pos, int &c, int &p) { - if (pos < N_SEATS || pos >= N_SEATS * (N_RINGS + 1)) + if (pos < POS_BEGIN || POS_END <= pos) return false; c = pos / N_SEATS; @@ -608,10 +669,11 @@ bool NineChess::place(int c, int p, long time_p /* = -1*/) // 如非“落子”状态,返回false if (context.action != ACTION_PLACE) return false; - - // 如果落子位置在棋盘外、已有子点或禁点,返回false + + // 转换为 pos int pos = cp2pos(c, p); + // 如果落子位置在棋盘外、已有子点或禁点,返回false if (!onBoard[pos] || board_[pos]) return false; @@ -637,6 +699,7 @@ bool NineChess::place(int c, int p, long time_p /* = -1*/) } board_[pos] = piece; + updateHash(pos); move_ = pos; player_ms = update(time_p); sprintf(cmdline, "(%1u,%1u) %02u:%02u.%03u", @@ -725,7 +788,9 @@ bool NineChess::place(int c, int p, long time_p /* = -1*/) c, p, player_ms / 60000, (player_ms % 60000) / 1000, player_ms % 1000); cmdlist.push_back(string(cmdline)); board_[pos] = board_[currentPos]; + updateHash(pos); board_[currentPos] = '\x00'; + updateHash(currentPos); currentPos = pos; currentStep++; n = addMills(currentPos); @@ -811,6 +876,7 @@ bool NineChess::capture(int c, int p, long time_p /* = -1*/) currentPos = 0; context.nPiecesNeedRemove--; currentStep++; + updateHash(pos); // 去子完成 // 如果决出胜负 @@ -937,6 +1003,7 @@ bool NineChess::place(int pos) // 如非“落子”状态,返回false if (context.action != ACTION_PLACE) return false; + // 如果落子位置在棋盘外、已有子点或禁点,返回false if (!onBoard[pos] || board_[pos]) return false; @@ -959,6 +1026,7 @@ bool NineChess::place(int pos) } board_[pos] = piece; + updateHash(pos); move_ = pos; currentPos = pos; //step++; @@ -1034,7 +1102,9 @@ bool NineChess::place(int pos) // 移子 move_ = (currentPos << 8) + pos; board_[pos] = board_[currentPos]; + updateHash(pos); board_[currentPos] = '\x00'; + updateHash(currentPos); currentPos = pos; //step++; n = addMills(currentPos); @@ -1111,6 +1181,7 @@ bool NineChess::capture(int pos) move_ = -pos; currentPos = 0; context.nPiecesNeedRemove--; + updateHash(pos); //step++; // 去子完成 @@ -1215,36 +1286,47 @@ bool NineChess::choose(int pos) return false; } +uint64_t NineChess::getHash() +{ + return context.hash; +} + // hash函数,对应可重复去子的规则 -uint64_t NineChess::chessHash() +uint64_t NineChess::updateHash(int pos) { /* * hash各数据位详解(名为hash,但实际并无冲突,是算法用到的棋局数据的完全表示) - * 57-64位:空白不用,全为0 - * 56位:轮流标识,0为先手,1为后手 - * 55位:动作标识,落子(选子移动)为0,1为去子 - * 7-54位(共48位):从棋盘第一个位置点到最后一个位置点的棋子,每个点用2个二进制位表示,共24个位置点,即48位。 + * 56-63位:空白不用,全为0 + * 55位:轮流标识,0为先手,1为后手 + * 54位:动作标识,落子(选子移动)为0,1为去子 + * 6-53位(共48位):从棋盘第一个位置点到最后一个位置点的棋子,每个点用2个二进制位表示,共24个位置点,即48位。 * 0b00表示空白,0b01表示先手棋子,0b10表示后手棋子,0b11表示禁点 - * 5-6位(共2位):待去子数,最大为3,用2个二进制位表示即可 - * 1-4位:player1的手棋数,不需要player2的(可计算出) - */ + * 4-5位(共2位):待去子数,最大为3,用2个二进制位表示即可 + * 0-3位:player1的手棋数,不需要player2的(可计算出) + */ + +#if 0 uint64_t hash = 0ull; for (int i = POS_BEGIN; i < POS_END; i++) { hash |= board_[i] & 0x30; hash <<= 2; } +#endif + + uint64_t temp = board_[pos] & 0x30 >> 4; + context.hash |= (temp) << ((pos - 8) * 2 + 6); if (context.turn == PLAYER2) - hash |= 1ull << 55; + context.hash |= 1ull << 55; if (context.action == ACTION_CAPTURE) - hash |= 1ull << 54; + context.hash |= 1ull << 54; - hash |= (uint64_t)context.nPiecesNeedRemove << 4; - hash |= context.nPiecesInHand_1; + context.hash |= (uint64_t)context.nPiecesNeedRemove << 4; + context.hash |= context.nPiecesInHand_1; - return hash; + return context.hash; } bool NineChess::giveup(Player loser) @@ -1673,10 +1755,15 @@ bool NineChess::isAllSurrounded(enum Player ply) void NineChess::cleanForbiddenPoints() { + int pos = 0; + for (int i = 1; i <= N_RINGS; i++) { for (int j = 0; j < N_SEATS; j++) { - if (board_[i * N_SEATS + j] == '\x0f') - board_[i * N_SEATS + j] = '\x00'; + pos = i * N_SEATS + j; + if (board_[pos] == '\x0f') { + board_[pos] = '\x00'; + updateHash(pos); + } } } } @@ -1774,7 +1861,9 @@ void NineChess::mirror(bool cmdChange /*= true*/) for (j = 1; j < N_SEATS / 2; j++) { ch = board_[i * N_SEATS + j]; board_[i * N_SEATS + j] = board_[(i + 1) * N_SEATS - j]; + //updateHash(i * N_SEATS + j); board_[(i + 1) * N_SEATS - j] = ch; + //updateHash((i + 1) * N_SEATS - j); } } @@ -1891,7 +1980,9 @@ void NineChess::turn(bool cmdChange /*= true*/) for (i = 0; i < N_SEATS; i++) { ch = board_[N_SEATS + i]; board_[N_SEATS + i] = board_[N_SEATS * N_RINGS + i]; + //updateHash(N_SEATS + i); board_[N_SEATS * N_RINGS + i] = ch; + //updateHash(N_SEATS * N_RINGS + i); } uint64_t llp1, llp2, llp3; @@ -2080,9 +2171,12 @@ void NineChess::rotate(int degrees, bool cmdChange /*= true*/) ch2 = board_[i * N_SEATS + 1]; for (j = 0; j < N_SEATS - 2; j++) { board_[i * N_SEATS + j] = board_[i * N_SEATS + j + 2]; + //updateHash(i * N_SEATS + j); } board_[i * N_SEATS + 6] = ch1; + //updateHash(i * N_SEATS + 6); board_[i * N_SEATS + 7] = ch2; + //updateHash(i * N_SEATS + 7); } } else if (degrees == 6) { for (i = 1; i <= N_RINGS; i++) { @@ -2090,16 +2184,21 @@ void NineChess::rotate(int degrees, bool cmdChange /*= true*/) ch2 = board_[i * N_SEATS + 6]; for (j = N_SEATS - 1; j >= 2; j--) { board_[i * N_SEATS + j] = board_[i * N_SEATS + j - 2]; + //updateHash(i * N_SEATS + j); } board_[i * N_SEATS + 1] = ch1; + //updateHash(i * N_SEATS + 1); board_[i * N_SEATS] = ch2; + //updateHash(i * N_SEATS); } } else if (degrees == 4) { for (i = 1; i <= N_RINGS; i++) { for (j = 0; j < N_SEATS / 2; j++) { ch1 = board_[i * N_SEATS + j]; board_[i * N_SEATS + j] = board_[i * N_SEATS + j + 4]; + //updateHash(i * N_SEATS + j); board_[i * N_SEATS + j + 4] = ch1; + //updateHash(i * N_SEATS + j + 4); } } } else diff --git a/NineChess/src/ninechess.h b/NineChess/src/ninechess.h index 28d1883e..78b98bd9 100644 --- a/NineChess/src/ninechess.h +++ b/NineChess/src/ninechess.h @@ -36,6 +36,9 @@ public: // 横直斜3个方向,禁止修改! static const int N_DIRECTIONS = 3; + // 棋盘点的个数:40 + static const int N_POINTS = (NineChess::N_RINGS + 2) * NineChess::N_SEATS; + // 移动方向,包括顺时针、逆时针、向内、向外4个方向 enum MoveDirection { @@ -123,6 +126,14 @@ public: GAME_OVER = 0x0008 // 结局 }; + uint64_t rand64(void) { + return rand() ^ + ((uint64_t)rand() << 15) ^ + ((uint64_t)rand() << 30) ^ + ((uint64_t)rand() << 45) ^ + ((uint64_t)rand() << 60); + } + // 玩家标识, 轮流状态, 胜负标识 enum Player : uint16_t { @@ -143,6 +154,16 @@ public: ACTION_CAPTURE = 0x0400 // 提子 }; + // 棋盘点上棋子的类型 + enum PointType : uint16_t + { + POINT_TYPE_EMPTY = 0, // 没有棋子 + POINT_TYPE_PLAYER1 = 1, // 先手的子 + POINT_TYPE_PLAYER2 = 2, // 后手的子 + POINT_TYPE_FORBIDDEN = 3, // 禁点 + POINT_TYPE_COUNT = 4 + }; + // 棋局结构体,算法相关,包含当前棋盘数据 // 单独分离出来供AI判断局面用,生成置换表时使用 struct ChessContext @@ -156,7 +177,13 @@ public: 判断棋子是先手的用 (board[i] & 0x10) 判断棋子是后手的用 (board[i] & 0x20) */ - int board[(NineChess::N_RINGS + 2) * NineChess::N_SEATS]; + int board[N_POINTS]; + + // 局面哈希值 + uint64_t hash; + + // Zobrist 数组 + //uint64_t zobrist[N_POINTS][POINT_TYPE_COUNT]; // 局面阶段标识 enum NineChess::GameStage stage; @@ -219,6 +246,9 @@ private: // 生成成三表 void createMillTable(); + // 创建哈希值 + void constructHash(); + public: explicit NineChess(); virtual ~NineChess(); @@ -238,12 +268,14 @@ public: const char *board = nullptr, // 默认空棋盘 int nPiecesInHand_1 = 12, // 玩家1剩余未放置子数 int nPiecesInHand_2 = 12, // 玩家2剩余未放置子数 - int nPiecesNeedRemove = 0 // 尚待去除的子数 + int nPiecesNeedRemove = 0, // 尚待去除的子数 + uint64_t hash = 0ull // Hash 为0 ); // 获取棋局状态和棋盘上下文 void getContext(struct Rule &rule, int &step, int &flags, int *&board, - int &nPiecesInHand_1, int &p2_nPiecesInHand_2InHand, int &nPiecesNeedRemove); + int &nPiecesInHand_1, int &p2_nPiecesInHand_2InHand, int &nPiecesNeedRemove, + uint64_t &hash); // 获取当前规则 const struct Rule *getRule() const @@ -448,7 +480,8 @@ protected: bool capture(int pos); // hash函数 - uint64_t chessHash(); + uint64_t getHash(); + uint64_t updateHash(int pos); private: // 当前使用的规则 @@ -457,9 +490,12 @@ private: // 棋局上下文 struct ChessContext context; - // 棋局数据中的棋盘数据,单独提出来 + // 棋局上下文中的棋盘数据,单独提出来 int *board_; + // 棋局哈希值 + // uint64_t hash; + // 选中的棋子在board中的位置 int currentPos; diff --git a/NineChess/src/ninechessai_ab.cpp b/NineChess/src/ninechessai_ab.cpp index b2124f37..b6312b83 100644 --- a/NineChess/src/ninechessai_ab.cpp +++ b/NineChess/src/ninechessai_ab.cpp @@ -447,8 +447,8 @@ int NineChessAi_ab::changeDepth(int originalDepth) //int depthTable[] = { 2, 11, 11, 11, 11, 10, 9, 8, 8, 8, 7, 7, 1 }; int depthTable[] = { 2, 12, 12, 12, 12, 11, 10, 9, 9, 9, 8, 7, 1 }; #else - //int depthTable[] = { 2, 12, 12, 12, 12, 11, 10, 9, 8, 8, 8, 7, 1 }; - int depthTable[] = { 2, 12, 12, 12, 12, 11, 10, 9, 9, 9, 8, 7, 1 }; + int depthTable[] = { 2, 12, 12, 12, 12, 11, 10, 9, 8, 8, 8, 7, 1 }; + //int depthTable[] = { 2, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 1 }; #endif // DEAL_WITH_HORIZON_EFFECT newDepth = depthTable[chessTemp.getPiecesInHandCount_1()]; #elif defined GAME_PLACING_FIXED_DEPTH @@ -516,6 +516,7 @@ int NineChessAi_ab::alphaBetaPruning(int depth, int alpha, int beta, Node *node) // 初始化 node->isLeaf = false; node->isTimeout = false; + node->isHash = false; #endif // 搜索到叶子节点(决胜局面) @@ -557,7 +558,7 @@ int NineChessAi_ab::alphaBetaPruning(int depth, int alpha, int beta, Node *node) #ifdef HASH_MAP_ENABLE // 检索 hashmap - uint64_t hash = chessTemp.chessHash(); + uint64_t hash = chessTemp.getHash(); node->hash = hash; hashMapMutex.lock(); @@ -567,6 +568,11 @@ int NineChessAi_ab::alphaBetaPruning(int depth, int alpha, int beta, Node *node) if (node != rootNode && iter != hashmap.end() && iter->second.depth >= depth) { +#ifdef DEBUG_AB_TREE + node->isHash = true; +#endif + + // TODO: 处理 Alpha/Beta 确切值 node->value = iter->second.value; if (chessContext->turn == NineChess::PLAYER1) @@ -791,6 +797,7 @@ const char *NineChessAi_ab::move2string(int move) unordered_map::iterator NineChessAi_ab::findHash(uint64_t hash) { auto iter = hashmap.find(hash); + if (iter != hashmap.end()) return iter; @@ -805,7 +812,7 @@ unordered_map::iterator NineChessAi_ab::fin chessTempShift.turn(false); for (int k = 0; k < 4; k++) { chessTempShift.rotate(k * 90, false); - iter = hashmap.find(chessTempShift.chessHash()); + iter = hashmap.find(chessTempShift.getHash()); if (iter != hashmap.end()) return iter; } diff --git a/NineChess/src/ninechessai_ab.h b/NineChess/src/ninechessai_ab.h index 0615e672..2b7a108b 100644 --- a/NineChess/src/ninechessai_ab.h +++ b/NineChess/src/ninechessai_ab.h @@ -48,12 +48,13 @@ public: #ifdef DEBUG_AB_TREE string cmd; enum NineChess::Player player; // 此招是谁下的 - int depth; + int depth; // 深度 bool evaluated; // 是否评估过局面 int alpha; // 当前搜索结点走棋方搜索到的最好值,任何比它小的值对当前结点的走棋方都没有意义。当函数递归时 Alpha 和 Beta 不但取负数而且要交换位置 int beta; // 表示对手目前的劣势,这是对手所能承受的最坏结果,Beta 值越大,表示对手劣势越明显,如果当前结点返回 Beta 或比 Beta 更好的值,作为父结点的对方就绝对不会选择这种策略 bool isTimeout; // 是否遍历到此结点时因为超时而被迫退出 bool isLeaf; // 是否为叶子结点, 叶子结点是决胜局面 + bool isHash; // 是否从 Hash 读取 NineChess::GameStage stage; // 摆棋阶段还是走棋阶段 NineChess::Action action; // 动作状态 int nPiecesOnBoardDiff; // 场上棋子个数和对手的差值