Fonction de Zobrist


Pour le jeu d'échecs, il n'est pas rare que la recherche du meilleur coup, qui s'effectue sur plusieurs coups de profondeur, génère plusieurs fois les mêmes positions. Plutôt que de recalculer le mérite d'une position plusieurs fois, on va storer la position déjà examinée dans une table de transposition. La table de transposition n'est en fait qu'une table de hash où sont entreposées les positions déjà visitées ainsi que toute information nécessaire aux heuristiques de décision.

Comme la rapidité est primordiale — plus le programme peut examiner de positions, meilleure sera son estimation du jeu — Zobrist propose une fonction de hash très simple. Pour chaque case de l'échiquier on a un tableau de 13 valeurs aléatoires (une pour chaque état possible de l'échiquier, à savoir vide, ou contenant une des 12 pièces, noires et blanches.) Cela fait une matrice de 64 × 13 valeurs aléatoires. Pour calculer le hash, il suffit de combiner, grâce au ou-exclusif, les valeurs indicées par la case et la pièce qui l'occupe, soit:

H( p ) = i Zi, pi

p est la position, et pi la pièce sur la case i de la position; Z est la matrice qui contient les valeurs aléatoires, et enfin, ⊕ est l'opérateur ou-exclusif, comme Σ est l'opérateur de somme.

Les valeurs de la matrice Z sont des nombres pseudo-aléatoires tirés d'une loi uniforme. Ils doivent tous être distincts et tirés d'un générateur fort. Dans le cas des échecs, la taille de la matrice est modérée: 64 × 16 (si on veut conserver un alignement en mémoire) ce qui ne fait que 1024 valeurs, pour un total de 4096 octets si on manipule des entiers 32 bits.

La propriété la plus intéressante de la fonction de Zobrist est sans contredit le fait qu'elle puisse être calculée de façon incrémentale: il est en effet possible de trouver le hash d'une nouvelle position en utilisant le hash de la position dont elle est issue. Il suffit en effet de « retirer » une pièce en effectuant le ou-exclusive entre la valeur de la matrice Z de la pièce que l'on retire — car (hz) ⊕ z = h — et en ajoutant, toujours avec le ou-exclusif, la valeur de la matrice Z de la pièce que l'on ajoute. Cela réduit le temps de calcul de la fonction de hash de façon significative.

Pour en faire une fonction de hash universelle, on considérera 256 valeurs par « case » pour accommoder toutes les valeurs possibles pour les octets. Enfin, on pourra augmenter le nombre de « cases » à un nombre quelconque mais pratique sachant que chaque case comporte maintenant 256 valeurs possibles. Une implémentation de cette fonction de Zobrist donnerait ceci:




 inline unsigned HashZobrist(const unsigned char *buffer, 
                         int size)
  {
   static const unsigned ZOBRIST[64][256]=
    #include "HashZobristTable"
    ;

   unsigned h=0;

   for (int i=0; i<size; i++)
    h ^= ZOBRIST[i & 0x3fu][buffer[i]];

   return h;
  }




Les tests (que nous présenterons dans une section ultérieure) montrent que la fonction de Zobrist donne des hashes très raisonnables.




( steven.pigeon@videotron.ca )