The alpha-beta algorithm needs good moves to start with, or else
there won't be many of cut-offs, and the search speed will be quite
slow. So how do we determine what moves should be tried first?
This is a chicken and egg situation, we need to know good moves to find
the best move! Obviously, if we already know what moves are good,
then what's the point of searching in the first place?
'Good moves' are for instance moves from the principal variation,
moves that cause beta cut-offs, and moves that improve alpha.
An effective way to get good moves is to start with a shallow search,
remember which moves were good, and use that information in a deeper
search. This is the general idea of iterative deepening.
If we do a 9-ply search, then we start with a 1-ply search, remember the
good moves, use that information (also called 'history
heuristics') for the 2-ply search, and so on, until we arrive at
ply=9:
ply = 0;
for (depth =
1; depth <= maxdepth; depth++)
{
score = alphabetapvs(ply, depth, -LARGE_NUMBER,
LARGE_NUMBER);
}
Of course we need some mechanism to store good moves, and use that
information to sort the moves.
The first couple of iterations are going to be very fast, and the
added bonus of this technique is that there is always (almost
immediately) a 'best move' available to reply, in case time runs out.
You might be surprised to see that a 9-ply search with iterative
deepening (so start with a 1-ply search, then a 2-ply search etc, until
9) turns out to be significantly faster then doing a plain 9-ply search
without iterative deepening. The only reason for this
speedup is that iterative deepening gives us the information we need to
order moves, resulting in a lot more cut-offs than an alpha-beta search
without move ordering. If move ordering is very bad, a 9-ply search takes about 35x longer
than an 8-ply search (if the branching factor is 35). If move
ordering is good, this factor will be closer to 6 (or lower).
Winglet's history heuristics and move ordering is very basic. The PV
from a previous iteration is stored and tried first at the next
iteration. Moves that caused a beta cut-off, and moves that improved
alpha are tried next. These moves are stored in two databases (one for
white and one for black), indexed by the from
and to field, and containing 'move scores'
that can be used for sorting:
unsigned
int whiteHeuristics[from][to];
unsigned
int blackHeuristics[from][to];
This is how the database is updated during the
search, if a beta cut-off is found:
if (val >=
beta)
{
if (nextMove)
blackHeuristics[move.from][move.to] += depth*depth;
else
whiteHeuristics[move.from][move.to] += depth*depth;
}
Incrementing the score with 'depth*depth'
will create a preference for moves that caused cut-offs at higher
remaining depth values, because these moves are cutting larger parts of
the search tree.
The move sorting algorithm is actually selecting
the single next best move on the move list, rather than doing a full
sort. This is done in the search algorithm, after move generation
and right before making the move:
movegen();
for (i =
ifirst; i < ilast; i++)
{
selectmove(i);
makeMove(i);
etc...
We are saving a lot of time by selecting the next move, instead of doing
an expensive sort of all moves. Remember that most moves from the
move generator will never be made (due to cut-offs).
#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;
int
lastPVLength;
Move lastPV[MAX_PLY];
unsigned
int whiteHeuristics[64][64];
unsigned
int blackHeuristics[64][64];
BOOLTYPE followpv;
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();
void
rememberPV();
void
selectmove(int &ply,
int &i, int
&depth, BOOLTYPE &followpv);
};
#endif
#include <iostream>
#include "defines.h"
#include "protos.h"
#include "board.h"
void Board::selectmove(int
&ply, int &i,
int &depth, BOOLTYPE &followpv)
{
int j, k;
unsigned
int best;
Move temp;
// re-orders the
move list so that the best move is selected as the next move to try.
if (followpv
&& depth > 1)
{
for (j
= i; j < moveBufLen[ply+1]; j++)
{
if
(moveBuffer[j].moveInt == lastPV[ply].moveInt)
{
temp.moveInt =
moveBuffer[j].moveInt;
moveBuffer[j].moveInt =
moveBuffer[i].moveInt;
moveBuffer[i].moveInt =
temp.moveInt;
return;
}
}
}
if (nextMove)
{
best =
blackHeuristics[moveBuffer[i].getFrom()][moveBuffer[i].getTosq()];
j = i;
for (k
= i + 1; k < moveBufLen[ply+1]; k++)
{
if
(blackHeuristics[moveBuffer[k].getFrom()][moveBuffer[k].getTosq()] >
best)
{
best =
blackHeuristics[moveBuffer[k].getFrom()][moveBuffer[k].getTosq()];
j =
k;
}
}
if (j
> i)
{
temp.moveInt =
moveBuffer[j].moveInt;
moveBuffer[j].moveInt =
moveBuffer[i].moveInt;
moveBuffer[i].moveInt =
temp.moveInt;
}
}
else
{
best =
whiteHeuristics[moveBuffer[i].getFrom()][moveBuffer[i].getTosq()];
j = i;
for (k
= i + 1; k < moveBufLen[ply+1]; k++)
{
if
(whiteHeuristics[moveBuffer[k].getFrom()][moveBuffer[k].getTosq()] >
best)
{
best =
whiteHeuristics[moveBuffer[k].getFrom()][moveBuffer[k].getTosq()];
j =
k;
}
}
if (j
> i)
{
temp.moveInt =
moveBuffer[j].moveInt;
moveBuffer[j].moveInt =
moveBuffer[i].moveInt;
moveBuffer[i].moveInt =
temp.moveInt;
}
}
return;
}
|