8

My brain is smoking trying to understand the mechanics of this bitboard technique. In order to make it simple, lets imagine that, instead of chess and a lot of complex piece movements, we have a game with only two pieces and one a row of 8 positions. One piece is a triangle and the other is a circle, like this:

┌───┬───┬───┬───┬───┬───┬───┬───┐
│   │   │ ▲ │   │   │ ● │   │   │
└───┴───┴───┴───┴───┴───┴───┴───┘ 

The triangle can move like a rook. Any amount of positions horizontally but cannot jump over the circle.

Now imagine that the user moves the triangle to the last position, like this:

┌───┬───┬───┬───┬───┬───┬───┬───┐
│   │   │   │   │   │ ● │   │ ▲ │
└───┴───┴───┴───┴───┴───┴───┴───┘

For this example the triangle move bitboard is

1 1 0 1 1 1 1 1

and the circle position mask is

0 0 0 0 0 1 0 0

Obviously the move is illegal, because the triangle cannot jump over the circle but how the software can check if the move is legal using the magic bitboard technique?

Duck
  • 32,792
  • 46
  • 221
  • 426

1 Answers1

11

You are right that it's not possible to determine valid moves for sliding pieces by using only bitwise operations. You'll need bitwise operations and precomputed lookup tables.

The Chess case

Most recent chess engines are using the technique known as Magic Bitboards.

The implementations vary, but the basic principle is always the same:

  1. Isolate the squares that a given piece may reach from a given position, without taking board occupancy into account. This gives us a 64-bit bitmask of potential target squares. Let's call it T (for Target).

  2. Perform a bitwise AND of T with the bitmask of occupied squares on the board. Let's call the latter O (for Occupied).

  3. Multiply the result by a magic value M and shift the result to the right by a magic amount S. This gives us I (for Index).

  4. Use I as an index in a lookup table to retrieve the bitmask of squares that can actually be reached with this configuration.

To sum it up:

I = ((T & O) * M) >> S
reachable_squares = lookup[I]

T, M, S and lookup are all precomputed and depend on the position of the piece (P = 0 ... 63). So, a more accurate formula would be:

I = ((T[P] & O) * M[P]) >> S[P]
reachable_squares = lookup[P][I]

The purpose of step #3 is to transform the 64-bit value T & O into a much smaller one, so that a table of a reasonable size can be used. What we get by computing ((T & O) * M) >> S is essentially a random sequence of bits, and we want to map each of these sequences to a unique bitmask of reachable target squares.

The 'magic' part in this algorithm is to determine the M and S values that will produce a collision-free lookup table as small as possible. As noticed by Bo Persson in the comments, this is a Perfect Hash Function problem. However, no perfect hashing has been found for magic bitboards so far, which means that the lookup tables in use typically contain many unused 'holes'. Most of the time, they are built by running an extensive brute-force search.

Your test case

Now going back to your example:

┌───┬───┬───┬───┬───┬───┬───┬───┐
│   │   │ ▲ │   │   │ ● │   │   │
└───┴───┴───┴───┴───┴───┴───┴───┘ 
  7   6   5   4   3   2   1   0

Here, the position of the piece is in [0 ... 7] and the occupancy bitmask is in [0x00 ... 0xFF] (as it's 8-bit wide).

Therefore, it's entirely feasible to build a direct lookup table based on the position and the current board without applying the 'magic' part.

We'd have:

reachable_squares = lookup[P][board]

This will result in a lookup table containing:

8 * 2^8 = 2048 entries

Obviously we cannot do that for chess, as it would contain:

64 * 2^64 = 1,180,591,620,717,411,303,424 entries

Hence the need for the magic multiply and shift operations to store the data in a more compact manner.

Below is a JS snippet to illustrate that method. Click on the board to toggle the enemy pieces.

var xPos = 5,          // position of the 'X' piece
    board = 1 << xPos, // initial board
    lookup = [];       // lookup table

function buildLookup() {
  var i, pos, msk;

  // iterate on all possible positions
  for(pos = 0; pos < 8; pos++) {
    // iterate on all possible occupancy masks
    for(lookup[pos] = [], msk = 0; msk < 0x100; msk++) {
      lookup[pos][msk] = 0;

      // compute valid moves to the left
      for(i = pos + 1; i < 8 && !(msk & (1 << i)); i++) {
        lookup[pos][msk] |= 1 << i;
      }
      // compute valid moves to the right
      for(i = pos - 1; i >= 0 && !(msk & (1 << i)); i--) {
        lookup[pos][msk] |= 1 << i;
      }
    }
  }
}

function update() {
  // get valid target squares from the lookup table
  var target = lookup[xPos][board];

  // redraw board
  for(var n = 0; n < 8; n++) {
    if(n != xPos) {
      $('td').eq(7 - n)
        .html(board & (1 << n) ? 'O' : '')
        .toggleClass('reachable', !!(target & (1 << n)));
    }
  }
}

$('td').eq(7 - xPos).html('X');

$('td').click(function() {
  var n = 7 - $('td').index($(this));
  n != xPos && (board ^= 1 << n);
  update();
});

buildLookup();
update();
td { width:16px;border:1px solid #777;text-align:center;cursor:pointer }
.reachable { background-color:#8f8 }
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<table>
  <tr><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr>
</table>
Arnauld
  • 5,055
  • 2
  • 13
  • 26
  • OK, thanks for the explanation. Is the best explanation so far but I cannot understand a few items: 1) you say "so a table of a reasonable size can be used". A table to store what? 2) can you give one example of a piece, a M, a S and a Table value and how you calculate that? If doing that for chess is complex, please use my example. Formulas are good but I need to se a binary example of what is going on and what I am retrieving on that table, so I can fully understand. THANKS! – Duck Aug 03 '16 at 09:40
  • @SpaceDog I'd recommend to refer to the [Chess Programming Wiki](https://chessprogramming.wikispaces.com/Magic+Bitboards) for more details about the chess implementation. Please see my updated answer for a simpler use of a lookup table based on your example. – Arnauld Aug 03 '16 at 10:07
  • @SpaceDog - The technique is related to a [Perfect Hash Function](https://en.wikipedia.org/wiki/Perfect_hash_function) that, by being collision free over a known domain, creates a one-to-one mapping from your position to a set of moves. The *hard* part is finding such a function. – Bo Persson Aug 03 '16 at 10:52
  • Thanks @BoPersson. I've added this reference in the answer. – Arnauld Aug 03 '16 at 11:03
  • @SpaceDog We can't apply the 'magic stuff' at all on your example, as the lookup table already is a perfect mapping in that case. We _need_ it for chess because the target squares are not represented as consecutive bits (except for rook & queen moves along ranks). – Arnauld Aug 03 '16 at 13:57
  • sorry but I am still not understanding it...I still do not understand what this table will contain. Why I need it? What is this table storing? Consider my example. You say my example will contain 2048 entries? What entries are that? – Duck Aug 03 '16 at 17:54
  • The table is storing what you've called the 'move bitboards' in your question. All of them. For each possible position of the triangle piece (or 'X' piece in my demo code) and each possible board (from 00000000 to 11111111). – Arnauld Aug 03 '16 at 21:43
  • are you telling me that for chess one has to store all the possibilities of all sliding pieces, against any combination of other pieces, in all 64 positions on a table that will be used to see which one is happening right now? For example, for a rook at A1 (index 0) one has to store all possibilities of pieces at rank 1, file 1, of other pieces? like, no pieces, one piece (b1), two pieces (b1 and c1)... one piece at b1 another at a2 and so one? and then another table for the attack, like, if the rook is at A1 and there is no pieces at its rays except for an enemy piece at h1, then the... – Duck Aug 04 '16 at 15:23
  • ... bitmask for the attack of that rook with one piece at h1 will be `0b0000000000000000000000000000000000000000000000000000000010000000` or in other words, decimal 128? and this value has also to be stored? amazing! – Duck Aug 04 '16 at 15:25
  • ... (see previous parts)... now this is starting to make sense. I have not digested it yet but... thanks. I have accepted your answer and upvoted you. But I still don't know how I will calculate those damn magic numbers... – Duck Aug 04 '16 at 15:26
  • @SpaceDog You are correct. For chess, the final tables are a bit bulky, but not as much as one could expect. Quoting the Chess Programming Wiki: _"Recent table sizes were about 38 KByte for the bishop attacks, but still about 800 KByte for rook attacks"_ – Arnauld Aug 04 '16 at 15:31
  • thanks! now I have to invent a way to create those tables! – Duck Aug 04 '16 at 15:33