Как найти волшебные битборды?

const int BitTable[64] = {
  63, 30, 3, 32, 25, 41, 22, 33, 15, 50, 42, 13, 11, 53, 19, 34, 61, 29, 2,
  51, 21, 43, 45, 10, 18, 47, 1, 54, 9, 57, 0, 35, 62, 31, 40, 4, 49, 5, 52,
  26, 60, 6, 23, 44, 46, 27, 56, 16, 7, 39, 48, 24, 59, 14, 12, 55, 38, 28,
  58, 20, 37, 17, 36, 8
};

int pop_1st_bit(uint64 *bb) {
  uint64 b = *bb ^ (*bb - 1);
  unsigned int fold = (unsigned) ((b & 0xffffffff) ^ (b >> 32));
  *bb &= (*bb - 1);
  return BitTable[(fold * 0x783a9b23) >> 26];
}

uint64 index_to_uint64(int index, int bits, uint64 m) {
  int i, j;
  uint64 result = 0ULL;
  for(i = 0; i < bits; i++) {
    j = pop_1st_bit(&m);
    if(index & (1 << i)) result |= (1ULL << j);
  }
  return result;
}

Это из Wiki по программированию шахмат:
https://www.chessprogramming.org/Looking_for_Magics

Это часть некоторого кода для поиска магических чисел.

Аргумент uint64 m - это битборд, представляющий возможные блокированные квадраты для хода ладьи или слона. Пример ладьи на поле е4:

0 0 0 0 0 0 0 0 
0 0 0 0 1 0 0 0 
0 0 0 0 1 0 0 0 
0 0 0 0 1 0 0 0 
0 1 1 1 0 1 1 0 
0 0 0 0 1 0 0 0 
0 0 0 0 1 0 0 0 
0 0 0 0 0 0 0 0 

Квадратные ребра равны нулю, потому что они всегда блокируют, и уменьшение количества необходимых битов, по-видимому, полезно.

/* Bitboard, LSB to MSB, a1 through h8:
 * 56 - - - - - - 63
 *  - - - - - - - -
 *  - - - - - - - -
 *  - - - - - - - -
 *  - - - - - - - -
 *  - - - - - - - -
 *  - - - - - - - -
 *  0 - - - - - - 7
 */

Таким образом, в приведенном выше примере index_to_uint64 принимает индекс (от 0 до 2 ^ бит), а также количество битов, установленных в битовой доске (10) и битовой доске.

Затем это pops_1st_bit для каждого количества битов, за которым следует еще один сдвинутый бит кода. pops_1st_bit XOR - это битборд с самим собой минус один (почему?). Затем он с полными 32-битными битами, и где-то здесь мой мозг исчерпывает ОЗУ. Каким-то образом задействовано магическое шестнадцатеричное число 0x783a9b23 (это последовательность чисел из Lost?). И есть этот нелепый загадочный массив случайно упорядоченных чисел от 0 до 63 (BitTable[64]).

Ответы

Ответ 1

Хорошо, я понял.

Во-первых, некоторая терминология:

блокирующая маска. Битовая панель, содержащая все квадраты, которые могут блокировать кусок, для данного типа куска и квадрата. Он исключает завершающие квадраты краев, поскольку они всегда блокируются.

панель блокировщика. Битовая плата, содержащая занятые квадраты. Он имеет только квадраты, которые также находятся в маскировке блока.

панель перемещения. Битовая доска, содержащая все квадраты, на которые может перемещаться кусок, с учетом типа куска, квадрата и блокатора. Он включает в себя завершающие краевые квадраты, если кусок может двигаться туда.

Пример для ладью на квадрате e4, и на e2, e5, e7, b4 и c4 есть некоторые случайные фрагменты.

 The blocker mask        A blocker board         The move board
 0 0 0 0 0 0 0 0         0 0 0 0 0 0 0 0         0 0 0 0 0 0 0 0 
 0 0 0 0 1 0 0 0         0 0 0 0 1 0 0 0         0 0 0 0 0 0 0 0 
 0 0 0 0 1 0 0 0         0 0 0 0 0 0 0 0         0 0 0 0 0 0 0 0 
 0 0 0 0 1 0 0 0         0 0 0 0 1 0 0 0         0 0 0 0 1 0 0 0 
 0 1 1 1 0 1 1 0         0 1 1 0 0 0 0 0         0 0 1 1 0 1 1 1 
 0 0 0 0 1 0 0 0         0 0 0 0 0 0 0 0         0 0 0 0 1 0 0 0 
 0 0 0 0 1 0 0 0         0 0 0 0 1 0 0 0         0 0 0 0 1 0 0 0 
 0 0 0 0 0 0 0 0         0 0 0 0 0 0 0 0         0 0 0 0 0 0 0 0 

Некоторые примечания:

  • Маска-блокиратор всегда одинакова для заданного квадрата и куска (ладья или слон).
  • Блокирующие доски включают дружественные и вражеские части, и это подмножество маскирования блока.
  • Результирующая доска для перемещения может включать в себя перемещения, которые захватывают ваши собственные части, однако эти перемещения легко удаляются впоследствии: moveboard &= ~friendly_pieces)

Цель метода магических чисел заключается в том, чтобы очень быстро просмотреть предварительно рассчитанную плату перемещения для данной блокаторного блока. В противном случае вам придется (медленно) вычислять плату перемещения каждый раз. Это касается только скользящих частей, а именно ладьи и слона. Королева - всего лишь комбинация ладьи и слона.

Магические числа можно найти для каждой комбинации квадратов и кусков. Для этого вам нужно рассчитать все возможные варианты блокатора для каждого квадратного/комбинированного комбо. Это то, что делает этот код. Как он это делает, для меня все еще немного, но что также кажется очевидным для оригинального автора, Мэтта Тейлора. (Спасибо @Pradhan за ссылку)

Итак, я сделал повторный ввод кода для генерации всех возможных вариантов блокаторов. Он использует другую технику, и хотя он немного медленнее, его гораздо легче читать и понимать. Тот факт, что он немного медленнее, не является проблемой, потому что этот код не критичен по скорости. Программа должна выполнить ее только один раз при запуске программы, и для двухъядерного i5 требуется только микросекунды.

/* Generate a unique blocker board, given an index (0..2^bits) and the blocker mask 
 * for the piece/square. Each index will give a unique blocker board. */
static uint64_t gen_blockerboard (int index, uint64_t blockermask) 
{
    /* Start with a blockerboard identical to the mask. */
    uint64_t blockerboard = blockermask;

    /* Loop through the blockermask to find the indices of all set bits. */
    int8_t bitindex = 0;
    for (int8_t i=0; i<64; i++) {
        /* Check if the i'th bit is set in the mask (and thus a potential blocker). */
        if ( blockermask & (1ULL<<i) ) {
            /* Clear the i'th bit in the blockerboard if it clear in the index at bitindex. */
            if ( !(index & (1<<bitindex)) ) {
                blockerboard &= ~(1ULL<<i); //Clear the bit.
            }
            /* Increment the bit index in the 0-4096 index, so each bit in index will correspond 
             * to each set bit in blockermask. */
            bitindex++;
        }
    }
    return blockerboard;
}

Чтобы использовать его, сделайте следующее:

int bits = count_bits( RookBlockermask[square] );
/* Generate all (2^bits) blocker boards. */ 
for (int i=0; i < (1<<bits); i++) {
    RookBlockerboard[square][i] = gen_blockerboard( i, RookBlockermask[square] );
}

Как это работает: Есть 2 ^ бит блокаторов, где bits - это число 1 в маске блокатора, которые являются единственными соответствующими битами. Кроме того, каждое целое число от 0 до 2 ^ бит имеет уникальную последовательность из 1 и 0 длины bits. Таким образом, эта функция просто соответствует каждому биту в данном целочисленном значении соответствующему биту в маске блокатора и отключает его/в соответствии с созданием уникальной панели блокатора.

Он не такой умный или быстрый, но читаемый.

Ответ 2

Хорошо, я попытаюсь пройти через это.

index_to_uint64( 7, 10, m ); 

7 - это просто случайно выбранное число между 0 и 2 ^ 10, а 10 - количество бит, установленных в m. m может быть представлено четырьмя способами:

bitboard:
0 0 0 0 0 0 0 0 
0 0 0 0 1 0 0 0 
0 0 0 0 1 0 0 0 
0 0 0 0 1 0 0 0 
0 1 1 1 0 1 1 0 
0 0 0 0 1 0 0 0 
0 0 0 0 1 0 0 0 
0 0 0 0 0 0 0 0 
dec: 4521262379438080
hex: 0x1010106e101000
bin: 0000 0000 0001 0000 0001 0000 0001 0000 0110 1110 0001 0000 0001 0000 0000 0000

Перемещение. Это будет называться 10 раз. Он имеет возвращаемое значение и изменяет m.

pop_1st_bit(&m);

В pop_1st_bit, m называется bb. Я поменяю его на m для ясности.

uint64 b = m^(m-1);

Часть m-1 принимает младший значащий бит, который установлен и переворачивает его, и все биты под ним. После XOR все эти измененные биты теперь установлены на 1, а все старшие биты установлены на 0.

m  : 0000 0000 0001 0000 0001 0000 0001 0000 0110 1110 0001 0000 0001 0000 0000 0000 
m-1: 0000 0000 0001 0000 0001 0000 0001 0000 0110 1110 0001 0000 0000 1111 1111 1111
b  : 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0001 1111 1111 1111

Далее:

unsigned int fold = (unsigned) ((b & 0xffffffff) ^ (b >> 32));

Часть AND (b & 0xffffffff) с более низкими 32 установленными битами. Таким образом, это по существу очищает любые биты в верхней половине b.

(b & 0xffffffff)
b: 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0001 1111 1111 1111
&: 0000 0000 0000 0000 0000 0000 0000 0000 1111 1111 1111 1111 1111 1111 1111 1111
=: 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0001 1111 1111 1111

Часть ... ^ (b >> 32) сдвигает верхнюю половину b в нижнюю половину, затем XOR выводит результат предыдущей операции. Таким образом, в основном XOR составляет верхнюю половину b с нижней половиной b. Это не имеет никакого эффекта в этом случае, потому что верхняя половина b была пуста для начала.

>> :0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 
^  :0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0001 1111 1111 1111 

uint fold = 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0001 1111 1111 1111

Я не понимаю смысла этого "складывания", даже если в верхней половине b были бит.

В любом случае, двигайтесь дальше. Эта следующая строка фактически изменяет m, снимая младший бит. Это имеет смысл.

m &= (m - 1);
m  : 0000 0000 0001 0000 0001 0000 0001 0000 0110 1110 0001 0000 0001 0000 0000 0000 
m-1: 0000 0000 0001 0000 0001 0000 0001 0000 0110 1110 0001 0000 0000 1111 1111 1111
&  : 0000 0000 0001 0000 0001 0000 0001 0000 0110 1110 0001 0000 0000 0000 0000 0000 

Эта следующая часть умножает fold на какое-либо шестнадцатеричное число (простое?), справа сдвигает произведение 26 и использует его как индекс в BitTable, наш таинственный массив случайно упорядоченных чисел 0-63. На данный момент я подозреваю, что автор может писать генератор псевдослучайных чисел.

return BitTable[(fold * 0x783a9b23) >> 26];

Это завершает pop_1st_bit. Все это делается 10 раз (один раз для каждого бит, первоначально установленного в м). Каждый из 10 вызовов pop_1st_bit возвращает число 0-63.

j = pop_1st_bit(&m);
if(index & (1 << i)) result |= (1ULL << j);

В приведенных выше двух строках i - текущий бит, на котором мы находимся, 0-9. Поэтому, если число index (7, первоначально переданное как аргумент index_to_uint64), имеет бит i-го бита, затем установите j-й бит в результате, где j было возвращаемым значением 0-63 из pop_1st_bit.

И это! Я все еще смущен: (

Ответ 3

Когда вы смотрели серию видео на шахматных машинах на YouTube, у меня были те же вопросы, что и paulwal222. Кажется, что есть некоторые математики высокого уровня. Лучшие ссылки, объясняющие предысторию этого сложного вопроса, https://chessprogramming.wikispaces.com/Matt+Taylor и https://chessprogramming.wikispaces.com/BitScan. Похоже, что Мэтт Тейлор в 2003 году в google.group(https://groups.google.com/forum/#!topic/comp.lang.asm.x86/3pVGzQGb1ys) (также найденный pradhan) придумал что-то, что сейчас называемый складной трюк Мэтта Тейлора, 32-битная дружественная реализация, чтобы найти бит-индекс LS1B (https://en.wikipedia.org/wiki/Find_first_set). Тейлор складной трюк, по-видимому, является адаптацией De Bruijn (https://en.wikipedia.org/wiki/Nicolaas_Govert_de_Bruijn), который был разработан в 1997 году, согласно Дональду Кнуту Мартином Ляутером определите индекс LS1B минимальным совершенным хешированием (https://en.wikipedia.org/wiki/Perfect_hash_function). Числа BitTable (63, 30,..) и складки в PopBit (0x783a9b23), вероятно, являются так называемыми магическими числами (уникально?), Связанными с 32-битным трюком для Matt Taylor. Этот складной трюк кажется очень быстрым, потому что многие двигатели скопировали этот подход (f.i Stockfish).