A position is a draw (i.e. a player can claim a draw) if it occurs
three times. The engine therefore has to check how often the
current position has occurred in the game (or in the search tree,
leading to this position) and if a position has occurred more than 2
times, then the search will return the draw score. 'Same
position' here means that the pieces on the board occupy the same
squares, the castling rights are the same and the en-passant target must
be the same.
An effective way of comparing 2
positions is to assign some unique 64-bit signature to each position, and
then compare the 64-bit signatures. We cannot capture all theoretically
possible chess positions with 64 bits, but for all practical purposes,
64 seems to be enough (provided that the Zobrist keys, described
below, are sufficiently random). Please refer to Robert
Hyatt's
on-line article on hash collisions if you want to understand why we
don't need more than 64 bits.
The 64-bit signature of the chess position (stored in
hashkey, a new member of the board
structure) is constructed as follows:
hashkey = 0;
for (square =
0; square < 64; square++)
if (piece(square)) hashkey ^=
ZOBRISTKEYS[square][piece(square)];
This shows how pieces on the board are folded into a 64-bit hash key
(unique signature). The ZOBRISTKEYS are 64-bit random numbers,
and initialized at program start-up. We also have to fold in the side to move, castling rights, and the
en-passant target square. The technique was proposed by Albert Zobrist in 1970. The XOR
operator has the useful
property that it acts like an on/off switch: if you XOR a value 2
times by the same bit pattern, it will return to its original value. We can
fold a white pawn on e2 into the key by:
hashkey ^= ZOBRISTKEYS[E2][WHITE_PAWN]
and we can remove the pawn by doing the same operation again.
So ZOBRISTKEYS[E2][WHITE_PAWN] is
an on/off switch for a white pawn on square e2. Note that hashing
is a one-way street: you can generate the signature for any given
position, but you cannot derive the chess position from a hash key (and
we don't need to).
Makemove and unmakemove
are now modified accordingly to update the position signature as pieces are moving
around and/or captured. C++ has a rand()function
that generates pseudo-random numbers, in the range 0 to 32767. We use it to generate 64-bit random numbers:
U64 rand64()
{
return
rand()^((U64)rand()<<15)^((U64)rand()<<30)^((U64)rand()<<45)^((U64)rand()<<60);
}
To make the series of random numbers even 'more random',
srand(seed) can be used to initialize
rand(). For every different seed value,
rand() will generate a
different series of random numbers. Using the current time (i.e. the
number of seconds that have elapsed since January 1, 1970) as seed for
srand(), we will never get the same series
of random numbers:
time_t now;
srand((unsigned
int)time(&now));
In order to be able to scan all previous signatures, the signature is
added to the GameLineRecord
structure. Before we start a new search loop over moves,
repetitionCount (a new function) will count
the number of times that the current position has occurred. If it's 3 or
more, the draw score is returned. All of this hardly affects speed,
because the computations to update the signature and to detect repetitions
are quite fast.
Also note that we don't have to go back all the way to the start
position for a repetition check. The
fiftyMove counter will tell us when the last
irreversible move was done: we don't need to go back further than that. Also, every other position can be skipped,
because positions with the other side to move cannot possibly be the same as the current position.
int repetitionCount()
{
rep = 1;
ilast = endOfSearch - fiftyMove;
for (i =
endOfSearch - 2; i >= ilast; i -= 2)
if (gameLine[i].key
== hashkey) rep++;
return rep;
}
One positive side-effect of having hash keys is that implementing a
hash table will become an almost trivial task.
#ifndef WINGLET_HASH_H_
#define WINGLET_HASH_H_
#include "defines.h"
// random 64-bit keys to give every
position an 'almost' unique signature:
struct HashKeys
{
// total size =
1093 * 8 = 8744 bytes (minimum required is 6312):
U64 keys[64][16];
// position, piece (only 12 out of 16 piece are values used)
U64 side;
// side to move (black)
U64 ep[64];
// ep targets (only 16 used)
U64 wk;
// white king-side castling right
U64 wq;
// white queen-side castling right
U64 bk;
// black king-side castling right
U64 bq;
// black queen-side castling right
void
init(); // initialize the random
data
U64 rand64();
// 64-bit random number generator
};
#endif
#include <ctime>
#include <iostream>
#include <stdlib.h>
#include "hash.h"
#include "timer.h"
#include "protos.h"
void HashKeys::init()
{
// initialize all
random 64-bit numbers
int i,j;
time_t now;
// use current time
(in seconds) as random seed:
srand((unsigned
int)time(&now));
for (i = 0; i
< 64; i++)
{
ep[i] = rand64();
for (j
= 0; j < 16; j++) keys[i][j] = rand64();
}
side = rand64();
wk = rand64();
wq = rand64();
bk = rand64();
bq = rand64();
return;
}
U64 HashKeys::rand64()
{
return
rand()^((U64)rand()<<15)^((U64)rand()<<30)^((U64)rand()<<45)^((U64)rand()<<60);
}
#ifndef WINGLET_GLOBALS_H
#define WINGLET_GLOBALS_H
#include
"defines.h"
#include
"board.h"
#include "hash.h"
char CMD_BUFF[MAX_CMD_BUFF];
(...)
// Search parameters start here:
int LARGE_NUMBER = KING_VALUE + 1;
int CHECKMATESCORE = KING_VALUE;
int STALEMATESCORE = 0;
int DRAWSCORE = 0;
Move NOMOVE;
HashKeys KEY;
#endif
#ifndef WINGLET_EXTGLOBALS_H
#define WINGLET_EXTGLOBALS_H
#include
"defines.h"
#include
"board.h"
#include "hash.h"
extern
char
CMD_BUFF[];
extern
int
CMD_BUFF_COUNT;
(...)
extern Move NOMOVE;
extern HashKeys KEY;
#endif
#include
<iostream>
#include
<iomanip>
#include
"defines.h"
#include
"protos.h"
#include
"extglobals.h"
void dataInit()
{
//
===========================================================================
// Initialization of global data at program startup.
// This function should be called only once (or else the
mirrored data will be
// mirrored back again!!)
//
===========================================================================
unsigned
char
CHARBITSET[8];
int i,
square, rank, file, arank, afile, state, slide, diaga1h8, diaga8h1,
attackbit;
unsigned
char
state6Bit, state8Bit, attack8Bit;
Move move;
(...)
NOMOVE.moveInt = 0;
KEY.init();
return;
}
void info()
{
// your playground... display
variables - meant for testing/verification purposes only
std::cout << std::endl
<< "============ info start
==============" << std::endl;
std::cout <<
"size of board, in bytes = "
<< sizeof(board)
<< std::endl;
std::cout <<
"Material value = "
<< board.Material << std::endl;
std::cout <<
"White castling rights = "
<< int(board.castleWhite)
<< std::endl;
std::cout <<
"Black castling rights = "
<< int(board.castleBlack)
<< std::endl;
std::cout <<
"En-passant square = "
<< board.epSquare << std::endl;
std::cout <<
"Fifty move count = "
<< board.fiftyMove << std::endl;
std::cout <<
"bitCnt of white pawns = "
<< bitCnt(board.whitePawns) << std::endl;
std::cout << std::endl
<< "bitmap of blackKnights |
board.whitePawns:" << std::endl;
displayBitmap(board.blackKnights |
board.whitePawns);
std::cout <<
"============ info end
================" << std::endl <<
std::endl;
return;
}
#ifndef WINGLET_BOARD_H_
#define WINGLET_BOARD_H_
#include
"defines.h"
#include
"move.h"
#include
"gameline.h"
#include
"timer.h"
struct Board
{
BitMap whiteKing, whiteQueens, whiteRooks,
whiteBishops, whiteKnights, whitePawns;
BitMap blackKing, blackQueens, blackRooks,
blackBishops, blackKnights, blackPawns;
BitMap whitePieces, blackPieces,
occupiedSquares;
unsigned
char
nextMove; // WHITE_MOVE
or BLACK_MOVE
unsigned
char
castleWhite; // White's
castle status, CANCASTLEOO = 1, CANCASTLEOOO = 2
unsigned
char
castleBlack; // Black's
castle status, CANCASTLEOO = 1, CANCASTLEOOO = 2
int
epSquare; //
En-passant target square after double pawn move
int
fiftyMove; //
Moves since the last pawn move or capture
U64 hashkey;
// Random 'almost' unique signature for
current board position.
// additional variables:
int
Material; //
incrementally updated, total material on board,
// in centipawns, from white’s side of
view
int
square[64]; //
incrementally updated, this array is usefull if we want to
// probe what kind of piece is on a
particular square.
BOOLTYPE viewRotated;
// only used for displaying the
board. TRUE or FALSE.
// storing moves:
Move
moveBuffer[MAX_MOV_BUFF]; // all
generated moves of the current search tree are stored in this array.
int
moveBufLen[MAX_PLY]; //
this arrays keeps track of which moves belong to which ply
int
endOfGame; //
index for board.gameLine
int
endOfSearch; //
index for board.gameLine
GameLineRecord gameLine[MAX_GAME_LINE];
// search variables:
int
triangularLength[MAX_PLY];
Move triangularArray[MAX_PLY][MAX_PLY];
Timer timer;
U64 msStart, msStop;
int
searchDepth;
U64 inodes;
void
init();
int eval();
Move think();
int
minimax(int
ply, int
depth);
int
alphabeta(int
ply, int
depth, int
alpha, int
beta);
int
alphabetapvs(int
ply, int
depth, int
alpha, int
beta);
void
displaySearchStats(int
mode, int
depth, int
score);
BOOLTYPE isEndOfgame(int
&legalmoves, Move &singlemove);
int
repetitionCount();
void
mirror();
void
initFromSquares(int
input[64], unsigned
char
next, int
fiftyM, int
castleW, int
castleB, int
epSq);
void
initFromFen(char
fen[], char
fencolor[], char
fencastling[], char
fenenpassant[], int
fenhalfmoveclock, int
fenfullmovenumber);
void
display();
};
#endif
|