Well, many details are still missing. And most things I had already been thinking about; it was just the idea to make the move list a single 256-bit set in MVV/LVA order that was new, and made it all come together. Before I was thinking about a two-level approach, keeping the set of attackers per square, instead of per victim (which would make inheriting each others attackers easier), and a bitmap of victims that would indicate which victims were attacked (which would then cause their attackers sets to be examined). I guess for the game with 96 pieces that I am really interested in I still would have to do something that, as a set of 96x96 bits would be so large that even attacking the least-significant set bit would require quite inefficient looping. I am not even sure this would not be a problem for the 256-bit sets.
And I already found an error: the numbering of attackers and victims is not consistent, and one of those would have to be inverted. (i.e. initializing the victims[] array the other way around). Low move numbers would be extracted first, and should thus correspond to high victims or low attackers.
The nice thing about this approach is not only that it is fast, but that it can be made even faster by skipping parts of it. After the moves of the captured piece have been deleted, and its slider moves have been extended, captures[his] has been fully updated. So you could extract the first opponent move at that point, to check the value of the victim, and thus whether he has a capture that could possibly push you below beta. If he has not, and it is obvious even without calculating the mobility term that stand pat also won't do it, the move can be discarded before updating captures[mine] (i.e. extending your own sliders, letting the mover inherit the attacks on the victim, calculating its new moves...)
If the opponent has no non-futile captures, but might conceivably be able to stand pat, you would have to evaluate the mobility term to make sure. In that case you would have to extend your own sliders, to get their mobility gain, and calculate the new moves of the mover to get its mobility. So you could not skip anything, but even then the calculation gives you the mobility as a side effect very cheaply.
The new moves of the mover can be efficiently calculated by means of the nearest-neighbor table:
Code: Select all
range[NPIECES][8]; // range in each of the 8 major directions of the given piece
// generate moves of piece from its new location
for(dir=0; dir<8; dir++) {
int dist = view[to][dir];
if(dist <= range[piece][dir]) {
dest = to + dist*step[dir];
target = board[dest];
if(COLOR(target) == his)
captures[mine] |= victimMask[piece] & attackerMask[target];
else if(COLOR(target) == mine)
protects[mine] |= victimMask[piece] & attackerMask[target];
}
}
So even for slides there is no ray scan, but the single piece you have to actually generate moves for 'leaps' directly on the first obstruction along the ray. It is also easily adapted for finite-range sliders, which is important for Shogi variants, where many pieces have slides of maximally 2, 3 or 5 squares in some directions. King and Pawns can have range = 1 (we are only using this for their captures moves!). Only Knight moves do not fit in this scheme. So you would have to test first if 'piece' is a Knight, and use a different piece of code to generate the moves when it is.
Also note that getting the bit describing a single move as victimMask[piece] & attackersMask[victim], ANDing 256-bit sets, could be replaced by a shift: (int256) attackerBit[piece] << shift[victim]. The latter might be less taxing on the memory system, as the attackerBit could be tabulated as a 32-bit int.