bitmapbit-manipulationbit-shiftbitboard

Computationally feasible way of finding diagonals in a bitboard


I am struggling to find an efficient way of calculating a 64-bit representation of a square's diagonals.

The game in question is a board game called Othello or Reversi. I am currently working on a method to determine the stability of a piece. A stable piece can be defined as a piece which cannot be flipped. The current problem I'm working on can be defined by: "a piece cannot be flipped if all squares in all four directions are occupied."

Thus, for example, the piece labelled A cannot be captured if the board looks as follows:

/*      1 2 3 4 5 6 7 8 
 *   1  . . x . . . x . 
 *   2  . . x . . x . . 
 *   3  x . x . x . . . 
 *   4  . x x x . . . . 
 *   5  x x A x x x x x 
 *   6  . x x x . . . . 
 *   7  x . x . x . . . 
 *   8  . . x . . x . . 
 */

Where x denotes that there exists a piece in that posistion (regardless of its colour). Because the piece is surrounded by pieces in all directions it cannot be captured.

The vertical lines are easy to calculate. In a nested for-loop of 8x8, the next vertical line can be found by ver0 >> j, while the next horizontal line can be found by hor0 >> i*8.

const unsigned long long hor0 = 255ULL;
const unsigned long long ver0 = 72340172838076673ULL;
for (i = 0; i < 8; i++) {
    for (j = 0; j < 8; j++) {
    currHor = hor0 << i;
    currVer = ver0 << j;
    }
}

As a visual example, hor0 looks like:

/*      1 2 3 4 5 6 7 8 
 *   1  x x x x x x x x 
 *   2  . . . . . . . . 
 *   3  . . . . . . . . 
 *   4  . . . . . . . . 
 *   5  . . . . . . . . 
 *   6  . . . . . . . . 
 *   7  . . . . . . . . 
 *   8  . . . . . . . . 
 */

so a shift by 8 would move the line down by one.

ver0 looks like:

/*      1 2 3 4 5 6 7 8 
 *   1  x . . . . . . . 
 *   2  x . . . . . . . 
 *   3  x . . . . . . . 
 *   4  x . . . . . . . 
 *   5  x . . . . . . . 
 *   6  x . . . . . . . 
 *   7  x . . . . . . . 
 *   8  x . . . . . . . 
 */

So a shift by one would move the line right by one.

To find their combined cursor I simply OR the results:

currCursor = (currHor | currVer);

Now the real problem starts. In order to determine stability, I need all four directions. I'm not sure how to calculate the diagonal lines feasibly with only the (i, j) position.

My first attempt was to use bit shifts. This failed horribly as the shifts resulted in mirroring when I don't want garbage bits.

My second attempt was to simply place all of the diagonal lines in an array and use the indexes to find the corresponding line. The array is quite big but here is an example of how some of the elements look (only the left diagonals):

const unsigned long long rightDiagonals[15] = { \
    9241421688590303745ULL, \
/*      1 2 3 4 5 6 7 8 
 *   1  x . . . . . . . 
 *   2  . x . . . . . . 
 *   3  . . x . . . . . 
 *   4  . . . x . . . . 
 *   5  . . . . x . . . 
 *   6  . . . . . x . . 
 *   7  . . . . . . x . 
 *   8  . . . . . . . x
 *   [0] //This is the index in the array.
 */
    36099303471055874ULL, \
/*      1 2 3 4 5 6 7 8 
 *   1  . x . . . . . . 
 *   2  . . x . . . . . 
 *   3  . . . x . . . . 
 *   4  . . . . x . . . 
 *   5  . . . . . x . . 
 *   6  . . . . . . x . 
 *   7  . . . . . . . x 
 *   8  . . . . . . . . 
 *   [1] 
 */
...
    144396663052566528ULL, \
/*      1 2 3 4 5 6 7 8 
 *   1  . . . . . . . . 
 *   2  . . . . . . . . 
 *   3  . . . . . . . . 
 *   4  . . . . . . . . 
 *   5  . . . . . . . . 
 *   6  . . . . . . . . 
 *   7  x . . . . . . . 
 *   8  . x . . . . . . 
 *   [13] 
 */
    72057594037927936ULL}
/*      1 2 3 4 5 6 7 8 
 *   1  . . . . . . . . 
 *   2  . . . . . . . . 
 *   3  . . . . . . . . 
 *   4  . . . . . . . . 
 *   5  . . . . . . . . 
 *   6  . . . . . . . . 
 *   7  . . . . . . . . 
 *   8  x . . . . . . . 
 *   [14] 
 */

I don't know how to match the index of the array with the (i, j) index. For example, if a square is on the index (2, 1), the corresponding index should be [1] in the array. But if a square is on (3, 2) the index is the array should also be [1]. And if a square is on (1,1), ... (7, 7), (8, 8), the index should be [0].

I can't determine a way to easily find the line. One thing I thought of is to get the 64-bit representation of the current square and OR it with the intersection of two lines. The problem is that this would require a for loop and this operation will be executed thousands of times, which is computationally not optimal.

Does anybody know of a way to compute diagonals from a single square or a method to calculate the corresponding index in the array of diagonals?

Your help is greatly appreciated.


Solution

  • I don't know how to match the index of the array with the (i, j) index. For example, if a square is on the index (2, 1), the corresponding index should be [1] in the array. But if a square is on (3, 2) the index is the array should also be [1]. And if a square is on (1,1), ... (7, 7), (8, 8), the index should be [0].

    There is a simple pattern in the pairs you have given, it may be easier to spot it geometrically. Have a look at the grid below and see if you can figure it out before reading on, try to think about what you need (how can you make each x, y pair on the line produce the same number):

        0 1 2 3 4 5 6 7
    
    0   . . x . . . . .
    1   . . . x . . . .
    2   . . . . x . . .
    3   . . . . . x . .
    4   . . . . . . x .
    5   . . . . . . . x
    6   . . . . . . . .
    7   . . . . . . . .
    

    If you draw a line from the left edge, to the diagonal, to the bottom edge (the manhattan distance), you will notice all points on the line have the same distance 7 - x + y. Similarly we can do x - y for the distance from the "primary diagonal". The following illustrates this for all points:

        0 1 2 3 4 5 6 7
    
    0   0 1 2 3 4 5 6 7
    1  -1 0 1 2 3 4 5 6
    2  -2-1 0 1 2 3 4 5
    3  -3-2-1 0 1 2 3 4
    4  -4-3-2-1 0 1 2 3
    5  -5-4-3-2-1 0 1 2
    6  -6-5-4-3-2-1 0 1
    7  -7-6-5-4-3-2-1 0
    

    At this point you could go and map your 16 precomputed diagonals to x - y... However we can do much better. Your original idea of shifting the diagonals is good, but it takes some effort to figure out how to efficiently avoid extraneous bits from wrapping around the grid.

    The key thing to notice is that moving the primary diagonal right on the grid is equivalent to moving it up, and moving it left is equivalent to moving it down (assuming bits were to just fall off the grid). Of course when we actually use a bitwise shift left or right we get bits wrapping around, however when we shift up or down (using a multiple of 8 with left/right), the extra bits always get pushed off the ends of the word. The same is true of the secondary diagonal but with inverse equivalence.

    If we start with the "primary diagonal" (top-left to bottom-right) and "secondary diagonal" (top-right to bottom-left), we can shift them up and down using using x - y to produce all other combinations of diagonals, then OR them together just like you are doing with the orthogonal lines:

    const uint64_t
        HP = 0xff00000000000000, // horizontal primary
        VP = 0x8080808080808080, // vertical primary
        DP = 0x8040201008040201, // diagonal primary
        DS = 0x0102040810204080; // diagonal secondary
    
    uint64_t stable (int x, int y) {
        uint64_t m =
            VP >> x | HP >> (y << 3);
        if (x >= y) {
            m |= DP << ((x - y) << 3);
        } else {
            m |= DP >> ((y - x) << 3);
        }
        int z = 7 - x;
        if (z >= y) {
            m |= DS << ((z - y) << 3);
        } else {
            m |= DS >> ((y - z) << 3);
        }
        return m;
    }
    
    void main () {
        for (int y = 0; y < 8; y ++) {
            for (int x = 0; x < 8; x ++) {
                uint64_t m = stable(x, y);
                printf("\n%d,%d:\n", x, y);
                for (int i = 7; i >= 0; i --) {
                    int line = m >> (i << 3);
                    printf("%c %c %c %c %c %c %c %c\n",
                        line & 0x80 ? 'x' : '.',
                        line & 0x40 ? 'x' : '.',
                        line & 0x20 ? 'x' : '.',
                        line & 0x10 ? 'x' : '.',
                        line & 0x08 ? 'x' : '.',
                        line & 0x04 ? 'x' : '.',
                        line & 0x02 ? 'x' : '.',
                        line & 0x01 ? 'x' : '.'
                    );
                }
            }
        }
    }
    

    All of the << 3 are just for efficiency, it's equivalent to * 8.

    I'm guessing you will want to mask your board value with m to see if it's "stable" like so board & m == m?

    Fun little problem, thanks :)