OK, this is as good as it gets:
At first glance it would seem convenient to assign all squares on an edge to a group of contiguous bits, since they can be brought all at once in the key by a single bit in the magic (either the whole edge appears, or it doesn't appear at all). This would make it possible to sparse out the remaining bits more. We have to be careful, however: to be able to share index tables, it is not only a matter of bringing the bits in the key, but you want corresponding squares of the occupancy set to end up in the same bit of the key. So an edge traversed in the opposite direction would have to need its bits sorted into opposite order, and this is impossible to do for a contiguous set.
To solve this problem, the edges are split in two half-edges of three squares. Each half-edge is assigned a group of 3 contiguous bits, and the two half-edges have their squares numbered in opposite direction (away from the corner). The msb of each group then corresponds to squares that are each image on (horizontal or vertical) reflection. So to get an equivalent representation of an edge traversed in the opposite direction, we only have to swap these blocks. By assigning bits that are sufficiently far apart, the two half-edges can be brought independently to any position in the key, so the swapping is easy.
Groups of 3 bits thus play an important role, and we will divide up the bitboard in 20 groups of 3 bits, plus 4 bits for the corners. If we number the edges 1-4, clockwise, we can assign the groups as:
- Code: Select all
board (each letter marks a 3x3 quadrant of inner squares):
1 1' 1 1'
---- ----
4'|A a| 2 4'| B| 2
4 | o| 2' 4 |p b| 2'
---- ----
3' 3 3' 3
1 1' 1 1'
---- ----
4'|s | 2 4'|d t| 2
4 |c C| 2' 4 |D | 2'
---- ----
3' 3 3' 3
bitboard (each letter represents 3 bits):
msb -4dDt3cCs2bBp1aAo4'3'2'1' lsb
The numbers thus represent the (half-)edges, the letters the inner squares. The corners get assigned to the most-significant 4 bits, since they never occur in any occupancy set, and it is favorable to have all used bits as much to the right as possible (since a multiplication can only shift them to the left).
If the attacker is in a corner, the occupancy set consists entirely of edges, two of them (say 1 and 2). These can be easily moved to the uppermost 12 bits (the key), by a magic with only 3 non-zero bits:
- Code: Select all
-. . ......2...1.....2'1' masked
2. . .1.....2'1' <<28
.. . 1.....2'1' <<31
.2'1' <<55
______________________________ +
22'1'1 key
- 4.. .........1....4'..1' masked
. 4.. .........1....4'..1' <<1
. .1. ...4'..1' <<33
4'..1' <<52
______________________________ +
4'411' key
Note that the exceptional case 1+4, where the primed half-edges did not have bordering bit assignments, does not have the outcome 11'4'4 expected from cyclic permutation. This would require shifting group 4 to the right, and is thus not possible through multiplication. Fortunately the situation is symmetric w.r.t. diagonal mirroring, which maps 4 <-> 1' etc. So all four corners can be mapped to fully equivalent 12-bit keys, which could share the index table.
When the attacker is on an edge square, the occupancy set consists of a single edge (with one square, the one the attacker is on, missing, plus a number of inner squares added). Because the assignment of square numbers is such that a +90-degree rotation simply maps the 1aAo1' bitgroups on the corresponding 2bBp2', we can make equivalent keys for the rotated states simply by shifting 2bBp to where 1aAo was, etc. The challenge, however, is to also find equivalent keys for the mirror-image positions (b1->g1, etc.). For a Rook on edge 2 this will definitely involve swapping 2 and 2'. The other edges will not appear in the occupancy set, but every other bit of the inner-square groups might appear, depending on where exactly the Rook is located on this edge. The two mappings could be:
- Code: Select all
case(1):
-..dDt.cCs2bBp.aAo..2'. masked (letter groups sparse!)
2bBp.aAo..2'. <<30
xxxxxxx other shifts to collect inner squares
.2'. <<54
______________________________ +
22'Bp key
case(2):
-..dDt.cCs2bBp.aAo..2'. masked (letter groups sparse!)
.2bBp.aAo..2'. <<27
xxxxxxx collect other inner squares, in particular:
bBp.aAo..2'. <<33 collect B
2'. <<57
______________________________ +
2'2bB key
Note, however, that in case 1 the group b collides with 2'. Bits in b can only be hit, though, if the Rook is in 2'. If the Rook is on 2, its non-edge ray only runs through the quadrant where it can hit B (and a, A, s, d, t), but not b or p, for which it has to be on 2' (where it can't hit B). And if the Rook is on 2', the square of the half-edge 2' it is on is not in the occupancy set, there is a 1-bit hole in 2' after masking. So we can arrange it such that the 3 squares mapping to bits in b in the 3x3 quadrant next to 2' precisely fill that hole. (i.e. put one such square on each ray perpendicular to the 2 edge). 2' and b thus don't really collide, they rather merge perfectly to a combined 3-bit group (2 bits from 2', one from b).
B has to contain the mirror-image squares of b, as B is to 2 what b is to 2'. So in case 2, when the Rook is on 2 and hence 2' is complete, b is clear (so still no collision between 2' and b). In this case we have to merge 2 with B, which we do by intentially shifting B above the appropriate key bits. Note that b and p, which then also map into the key, are clear in this situation. So making use of the knowledge that we use case (1) only if the Rook is on the 2' half-edge, and (2) if it is on 2, the cases look more like
- Code: Select all
case(1):
-..dDt.cCs2b.p.aAo..2'. masked (Attacker on 2')
2b.p.aAo..2'. <<30
xxxxxxx other shifts to collect inner squares
.2'. <<54
______________________________ +
22'.p key
case(2):
-..dDt.cCs2.B..aAo..2'. masked (Attacker on 2)
.2.B..aAo..2'. <<27
xxxxxxx collect other inner squares, in particular:
.B..aAo..2'. <<33 collect B
2'. <<57
______________________________ +
2'2.B key
Note that the extreme case of the Rook on edge 4 does not represent any special problems: In case (1), which is used if the Rook is on 4', the group 4' borders on o, rather than on another edge group. But the rays of a Rook on on 4' cannot intersect o. In case (2) 4' has to be shifted to the left of 4, (which itself cannot be right shifted!), but there are 4 empty (corner) bits to accomodate it. So for each pair of mirror-image attacker squares, we can create an equivalent key that has the 6 edge occupancies in the high-order 6 bits, the attacker itself being in the second half-edge, the hole created there filled by the occupancy of an equivalent square in the quadrant next to it. As far as the mapping of these squares into the key is concerned they could thus share an index table between all 8 symmetry-equivalent edge squares. If it can be done in practice also depends on the mappings of the 5 remaining inner squares, which varies case by case (i.e. b1 is different from c1, d1).
An example of an assignment (which, due to the way I assigned the bB bits to squares I refer to as the W system) could be:
- Code: Select all
1 1'
63,23,22,21, 0, 1, 2,60,
11,38,16,56,29,19,50,35,
4' 10,55,37,15,18,49,28,34, 2
9,17,54,36,48,27,20,33,
57,44,51,24,12,30,41, 3,
4 58,52,25,42,39,13,31, 4, 2'
59,26,43,53,32,40,14, 5,
62, 8, 7, 6,45,46,47,61
3' 3
1 1'
- 1 1 1 1'1'1'-
4's A d B a t 2
4' 4'd s A a t B 2 2
4'A d s t B a 2
4 c D p o b C 2'
4 4 D p c C o b 2' 2'
4 p c D b C o 2'
- 3'3'3'3 3 3 -
3' 3
The 3-bit groups are laid out such that there is never more than one square of them on a row or file, although a Rook could of course attack two of them from different directions if it is on an inner square. The occupancy set of a Rook on an inner square contains 10 inner squares out of 12 groups, so less than one per group on the average. In general, the inner-square bits are quite well spread out over the 45 high bits of the word if the Rook is on an inner square, and the unprimed half-edges cause an extra separation distance between the groups that map onto each other on rotation. The outermost groups are flanked by zeroes, so shifts could easily mimic periodic boundary conditions. This should make it very easy to find at least keys that allow sharing of index tables between the four squares related by rotation: the different keys should be cyclic permutations of each other (with width 48 bits). For the mirror images you will depend on clever keys. (The keys that do just one rotation group would be so simple that you could probably make them by hand...)
If you want to apply redundancy, the XOR patterns should similarly be rotated versions of each other, in their low-order 12 bits and the 48 bits above it, in steps of 3 and 12, respectively. In that case you will preserve the rotation symmetry.