基于Bitboard存储棋子攻击范围:如何实现兵等非骑士棋子的攻击存储?
棋子攻击范围预存方案(针对兵、王、车等)
问题背景
已知可以通过初始化一个包含64个uint64_t类型元素的数组,预存骑士所有位置的攻击范围,使用时直接通过索引获取对应掩码。但骑士的攻击是固定跳步、不受其他棋子影响的,兵、王、车等棋子的规则更复杂,需要不同的预存策略。
骑士攻击范围示例代码(修正补全)
#include <array> std::array<uint64_t, 64> getKnightAttacks() { const uint64_t notLeft2Mask = 0x3f3f3f3f3f3f3f3f; const uint64_t notLeftMask = 0x7f7f7f7f7f7f7f7f; const uint64_t notRight2Mask = 0xfcfcfcfcfcfcfcfc; const uint64_t notRightMask = 0xfefefefefefefefe; const uint64_t notBottomMask = 0xffffffffffffff00; const uint64_t notBottom2Mask = 0xffffffffffff0000; const uint64_t notTopMask = 0x00ffffffffffffff; const uint64_t notTop2Mask = 0x0000ffffffffffff; std::array<uint64_t, 64> knight_attacks{}; for (uint64_t i = 0; i < 64; i++) { uint64_t pos = 1ULL << i; uint64_t temp_attacks = 0; temp_attacks |= (pos << 6) & notBottomMask & notRight2Mask; // 左下跳 temp_attacks |= (pos << 15) & notBottom2Mask & notRightMask; // 左上跳 temp_attacks |= (pos << 17) & notBottom2Mask & notLeftMask; // 右上跳 temp_attacks |= (pos << 10) & notBottomMask & notLeft2Mask; // 右下跳 temp_attacks |= (pos >> 6) & notTopMask & notLeft2Mask; // 右上跳(黑方视角) temp_attacks |= (pos >> 15) & notTop2Mask & notLeftMask; // 右下跳 temp_attacks |= (pos >> 17) & notTop2Mask & notRightMask; // 左下跳 temp_attacks |= (pos >> 10) & notTopMask & notRight2Mask; // 左上跳 knight_attacks[i] = temp_attacks; } return knight_attacks; }
各棋子的预存实现方案
1. 兵的预存方案
兵的移动分攻击(斜向吃子)和前进(非吃子),且受阵营、初始位置限制,需要分阵营预存多组掩码:
攻击掩码(吃子范围)
- 白兵:斜向上左、斜向上右两个位置,需过滤第一行(无法向上)、最左/最右列(无对应斜向位置)
- 黑兵:斜向下左、斜向下右两个位置,需过滤第八行(无法向下)、最左/最右列
示例代码(白兵攻击掩码):
std::array<uint64_t, 64> getWhitePawnAttacks() { std::array<uint64_t, 64> pawn_attacks{}; const uint64_t notLeftMask = 0x7f7f7f7f7f7f7f7f; // 排除最左列 const uint64_t notRightMask = 0xfefefefefefefefe; // 排除最右列 const uint64_t notTopMask = 0x00ffffffffffffff; // 排除第一行 for (uint64_t i = 0; i < 64; ++i) { uint64_t pos = 1ULL << i; uint64_t attacks = 0; // 左斜上吃子 attacks |= (pos << 7) & notLeftMask & notTopMask; // 右斜上吃子 attacks |= (pos << 9) & notRightMask & notTopMask; pawn_attacks[i] = attacks; } return pawn_attacks; }
前进移动掩码
- 白兵单步前进:每个位置存正上方的位置(排除第一行)
- 白兵初始两步:仅第二行的位置存正上方两个位置的掩码
- 黑兵对应生成向下的掩码即可
2. 王的预存方案
王的移动是周围8格,无阻挡(仅走一步),和骑士类似可以直接预存64元素数组,用边界掩码过滤出界位置:
std::array<uint64_t, 64> getKingAttacks() { std::array<uint64_t, 64> king_attacks{}; const uint64_t notLeftMask = 0x7f7f7f7f7f7f7f7f; const uint64_t notRightMask = 0xfefefefefefefefe; const uint64_t notTopMask = 0x00ffffffffffffff; const uint64_t notBottomMask = 0xffffffffffffff00; for (uint64_t i = 0; i < 64; ++i) { uint64_t pos = 1ULL << i; uint64_t attacks = 0; // 上下左右 attacks |= (pos << 8) & notBottomMask; // 下 attacks |= (pos >> 8) & notTopMask; // 上 attacks |= (pos << 1) & notRightMask; // 右 attacks |= (pos >> 1) & notLeftMask; // 左 // 斜向 attacks |= (pos << 7) & notLeftMask & notBottomMask; // 左下 attacks |= (pos << 9) & notRightMask & notBottomMask; // 右下 attacks |= (pos >> 7) & notRightMask & notTopMask; // 右上 attacks |= (pos >> 9) & notLeftMask & notTopMask; // 左上 king_attacks[i] = attacks; } return king_attacks; }
3. 车、象、后的处理方案
这类棋子的移动是直线/斜线,会被其他棋子阻挡,无法预存固定的攻击范围,有两种常用方案:
方案一:动态生成攻击范围
每次需要时,从当前位置出发,沿各个方向遍历,直到遇到边界或棋子,生成掩码:
// 示例:生成车的攻击范围(结合当前棋盘占据情况) uint64_t getRookAttacks(uint64_t pos, uint64_t occupied) { uint64_t attacks = 0; int square = __builtin_ctzll(pos); // 获取当前位置索引(0-63) int rank = square / 8; int file = square % 8; // 向上遍历 for (int r = rank - 1; r >= 0; --r) { uint64_t target = 1ULL << (r * 8 + file); attacks |= target; if (occupied & target) break; // 遇到棋子停止 } // 向下遍历 for (int r = rank + 1; r < 8; ++r) { uint64_t target = 1ULL << (r * 8 + file); attacks |= target; if (occupied & target) break; } // 向左遍历 for (int f = file - 1; f >= 0; --f) { uint64_t target = 1ULL << (rank * 8 + f); attacks |= target; if (occupied & target) break; } // 向右遍历 for (int f = file + 1; f < 8; ++f) { uint64_t target = 1ULL << (rank * 8 + f); attacks |= target; if (occupied & target) break; } return attacks; }
方案二:预存射线掩码+位运算截断
预存每个位置沿各个方向的所有射线(比如车的四个方向射线,包含该方向所有格子),然后用当前棋盘的占据位快速截断射线,得到实际攻击范围。这是国际象棋引擎中常用的优化技巧:
// 预存车的射线掩码(示例:向上射线) std::array<uint64_t, 64> rookUpRays; // 初始化时生成射线掩码 void initRookRays() { for (int i = 0; i < 64; ++i) { int rank = i / 8; int file = i % 8; uint64_t ray = 0; for (int r = rank - 1; r >= 0; --r) { ray |= 1ULL << (r * 8 + file); } rookUpRays[i] = ray; } } // 实际计算时截断射线 uint64_t getRookUpAttacks(uint64_t pos, uint64_t occupied) { int square = __builtin_ctzll(pos); uint64_t ray = rookUpRays[square]; uint64_t blocker = ray & occupied; if (blocker) { // 找到第一个阻挡棋子,截断射线 int firstBlocker = __builtin_ctzll(blocker); ray &= ~(rookUpRays[firstBlocker]); } return ray; }
总结
- 骑士、王:攻击范围固定无阻挡,直接预存64元素数组即可
- 兵:按阵营预存攻击、移动多组掩码,适配其规则限制
- 车、象、后:因受阻挡影响,要么动态生成,要么预存射线掩码结合当前棋盘状态快速计算
内容的提问来源于stack exchange,提问作者ns8
相关产品推荐
相关产品推荐

