Changes

Jump to: navigation, search

CPW-Engine search

23,912 bytes added, 16:55, 18 December 2018
Created page with "'''Home * Engines * CPW-Engine * Search''' For function definitions, see CPW-Engine_search_h =Search= <pre> #include "stdafx.h" #include "searc..."
'''[[Main Page|Home]] * [[Engines]] * [[CPW-Engine]] * Search'''

For function definitions, see [[CPW-Engine_search_h]]

=[[Search]]=
<pre>
#include "stdafx.h"
#include "search.h"
#include "transposition.h"


#define MAX_DEPTH 100

/* symbols used to enhance readability */
#define DO_NULL 1
#define NO_NULL 0
#define IS_PV 1
#define NO_PV 0

sSearchDriver sd;

int draw_opening = -10; // middlegame draw value
int draw_endgame = 0; // endgame draw value
int ASPIRATION = 50; // size of the aspiration window ( val-ASPITATION, val+ASPIRATION )

bool time_over = 0;

enum eproto {
PROTO_NOTHING,
PROTO_XBOARD,
PROTO_UCI
} extern mode;

U8 bestmove; // move id passed between iterations for sorting purposes
smove move_to_make; // move to be returned when search runs out of time

/***************************************************************
* search_run() is the interface of all the search functions, *
* the only function called outside search.cpp. It does some *
* preparatory work, and then calls search_iterate(); *
***************************************************************/

void search_run() {

if (chronos.flags & (FTIME | FINC | FMOVESTOGO)) {
if (getBookMove(BOOK_BROAD)) return;
}

search_clearDriver();
time_calc_movetime();
ageHistoryTable();
if (mode == PROTO_NOTHING) printSearchHeader();

search_iterate();
}

void search_clearDriver() {
sd.myside = b.stm; // remember color - needed in contempt()
sd.starttime = gettime();
sd.movetime = 0;
sd.depth = 0;

// now clear all the statistical data
sd.nodes = 0;
sd.q_nodes = 0;
}

/**************************************************************
* search_iterate() calls search_root() with increasing depth *
* until allocated time is exhausted. *
**************************************************************/

void search_iterate() {
int val, temp;

// check the exact number of legal moves in the current position

int move_count = move_countLegal();

// do a full-window 1-ply search to get the first estimate of val

sd.depth = 1;
val = search_root(sd.depth, -INF, INF);

// main loop, increasing deph in steps of 1

for (sd.depth = 2; sd.depth <= MAX_DEPTH; sd.depth++) {

// breaking conditions - either expired time
// or just one legal reply and position searched to depth 4

if (time_stop_root() || time_over) break;
if (move_count == 1 && sd.depth == 5) break;

// this function deals with aspiration window
val = search_widen(sd.depth, val);
}

// after the loop has finished, send the move to the interface
com_sendmove(move_to_make);
}

int search_widen(int depth, int val) {
int temp = val,
alpha = val - 50,
beta = val + 50;

temp = search_root(sd.depth, alpha, beta);
if (temp <= alpha || temp >= beta)
temp = search_root(sd.depth, -INF, INF);
return temp;
}

int search_root(U8 depth, int alpha, int beta) {

int flagInCheck;
smove movelist[256];
int val = 0;

U8 currmove_legal = 0;

/* Check extension is done also at the root */

flagInCheck = isAttacked(!b.stm, b.KingLoc[b.stm]);
if (flagInCheck) ++depth;

U8 mcount = movegen(movelist, bestmove);

for (U8 i = 0; i < mcount; i++) {

movegen_sort(mcount, movelist, i);

if (movelist[i].piece_cap == KING) {
alpha = INF;
bestmove = movelist[i].id;
}

move_make(movelist[i]);

// filter out illegal moves
if (isAttacked(b.stm, b.KingLoc[!b.stm])) {
move_unmake(movelist[i]);
continue;
}

// if ( mode == PROTO_UCI )
// info_currmove( movelist[i], currmove_legal );

currmove_legal++;

/* the "if" clause introduces PVS at root */

if ((i == 0) ||
(-Search(depth - 1, 0, -alpha - 1, -alpha, DO_NULL, NO_PV) > alpha))
val = -Search(depth - 1, 0, -beta, -alpha, DO_NULL, IS_PV);

move_unmake(movelist[i]);

if (time_over) break;

if (val > alpha) {

bestmove = movelist[i].id;
move_to_make = movelist[i];

if (val > beta) { // should be >=, see post
tt_save(depth, beta, TT_BETA, bestmove);
info_pv(beta);
return beta;
}

alpha = val;
tt_save(depth, alpha, TT_ALPHA, bestmove);

info_pv(val);
} // changing node value finished
}

tt_save(depth, alpha, TT_EXACT, bestmove);
return alpha;
}

int Search(U8 depth, U8 ply, int alpha, int beta, int can_null, int is_pv) {

int val = -INF;
char bestmove;
char tt_move = INVALID;
char tt_flag = TT_ALPHA;
int flagInCheck;
int legal_move = 0;
int raised_alpha = 0;
int f_prune = 0;
int reduction_depth = 0;
int moves_tried = 0;
int new_depth;
int mate_value = INF - ply; // will be used in mate distance pruning
smove move;

/************************************************************************
* Probably later we will want to probe the transposition table. *
* Tell the cpu to prepare for that event. This is just a minor *
* speed optimization and program would run fine without that. *
************************************************************************/

_mm_prefetch((char *)&tt[b.hash & tt_size], _MM_HINT_NTA);

/************************************************************************
* Check for timeout. This is quite time-consuming, so we do it only *
* every so often. The side effect is that if we want to limit search *
* by number of nodes, it will be slightly inexact. *
************************************************************************/

if (!time_over && !(sd.nodes & 4095))
time_over = time_stop();
if (time_over) return 0;

/************************************************************************
* MATE DISTANCE PRUNING - a minor improvement that helps to shave off *
* some nodes when the checkmate is near. Basically it prevents looking *
* for checkmates taking longer than one we have already found. No Elo *
* gain expected, but it's a nice feature. Don't use it at the root, *
* since this code doesn't return a move, only a value. *
************************************************************************/

if (alpha < -mate_value) alpha = -mate_value;
if (beta > mate_value - 1) beta = mate_value - 1;
if (alpha >= beta) return alpha;

/************************************************************************
* Are we in check? If so, extend. It also means that program will *
* never enter quiescence search while in check. *
************************************************************************/

flagInCheck = (isAttacked(!b.stm, b.KingLoc[b.stm]));
if (flagInCheck) ++depth;

/************************************************************************
* At leaf nodes we do quiescence search (captures only) to make sure *
* that only relatively quiet positions with no hanging pieces will be *
* evaluated. *
************************************************************************/

if (depth == 0) return Quiesce(alpha, beta);

sd.nodes++;

if (isRepetition()) return contempt();

/************************************************************************
* Read the transposition table. We may have already searched current *
* position. If depth was sufficient, then we might use the score *
* of that search. If not, hash move still is expected to be good *
* and should be sorted first. *
* *
* NOTE: current implementation is sub-standard, since tt_move is just *
* an index showing move's location on a move list. We should be able *
* to retrieve move without generating full move list instead. *
************************************************************************/

if ((val = tt_probe(depth, alpha, beta, &tt_move)) != INVALID) {
// in pv nodes we return only in case of an exact hash hit
if (!is_pv || (val > alpha && val < beta))
return val;
}

/************************************************************************
* EVAL PRUNING / STATIC NULL MOVE *
************************************************************************/

if (depth < 3
&& (!is_pv)
&& (!flagInCheck)
&& (abs(beta - 1) > -INF + 100))
{
int static_eval = eval(alpha, beta, 1);

int eval_margin = 120 * depth;
if (static_eval - eval_margin >= beta)
return static_eval - eval_margin;
}

/************************************************************************
* Here we introduce NULL MOVE PRUNING. It means allowing opponent *
* to execute two moves in a row, i.e. capturing something and escaping *
* a recapture. If this cannot wreck our position, then it is so good *
* that there's no point in searching further. The flag "can_null" *
* ensures we don't do two null moves in a row. Null move is not used *
* in the endgame because of the risk of zugzwang. *
************************************************************************/

if ((depth > 2)
&& (can_null)
&& (!is_pv)
&& (eval(alpha, beta, 1) > beta) //should be >=, see post
&& (b.PieceMaterial[b.stm] > e.ENDGAME_MAT)
&& (!flagInCheck))
{
char ep_old = b.ep;
move_makeNull();

/********************************************************************
* We use so-called adaptative null move pruning. Size of reduction *
* depends on remaining depth. *
********************************************************************/

char R = 2;
if (depth > 6) R = 3;

val = -Search(depth - R - 1, ply, -beta, -beta + 1, NO_NULL, NO_PV);

move_unmakeNull(ep_old);

if (time_over) return 0;
if (val >= beta) return beta;
}

/************************************************************************
* Decide if FUTILITY PRUNING is applicable. If we are not in check, *
* not searching for a checkmate and eval is below (alpha - margin), *
* it might mean that searching non-tactical moves at low depths *
* is futile, so we set a flag allowing this pruning. *
************************************************************************/

int fmargin[4] = { 0, 200, 300, 500 };

if (depth <= 3
&& !is_pv
&& !flagInCheck
&& abs(alpha) < 9000
&& eval(alpha, beta, 1) + fmargin[depth] <= alpha)
f_prune = 1;

/* generate moves */

smove movelist[256];
U8 mcount = movegen(movelist, tt_move);

ReorderMoves(movelist, mcount, ply);

bestmove = movelist[0].id;

/************************************************************************
* Now it's time to loop through the move list. *
************************************************************************/

for (int i = 0; i < mcount; i++) {

movegen_sort(mcount, movelist, i); // pick the best of untried moves
move = movelist[i];
move_make(move);

// filter out illegal moves
if (isAttacked(b.stm, b.KingLoc[!b.stm])) {
move_unmake(move);
continue;
}
moves_tried++;

/********************************************************************
* When the futility pruning flag is set, prune moves which do not *
* give check and do not change material balance. Some programs *
* prune insufficient captures as well, but that seems too risky. *
********************************************************************/

if (f_prune
&& legal_move
&& !move_iscapt(move)
&& !move_isprom(move)
&& !isAttacked(!b.stm, b.KingLoc[b.stm])) {
move_unmake(move);
continue;
}

reduction_depth = 0; // this move has not been reduced yet
new_depth = depth - 1; // decrease depth by one ply

/********************************************************************
* Late move reduction. Typically a cutoff occurs on trying one of *
* the first moves. If it doesn't, we are probably in an all-node, *
* which means that all moves will fail low. So we might as well *
* spare some effort, searching to reduced depth. Of course this is *
* not a foolproof method, but it works more often than not. Still, *
* we need to exclude certain moves from reduction, in order to *
* filter out tactical moves that may cause a late cutoff. *
********************************************************************/

if (!is_pv
&& new_depth > 3
&& legal_move
&& moves_tried > 3
&& !isAttacked(!b.stm, b.KingLoc[b.stm])
&& !flagInCheck
&& (move.from != sd.killers[0][ply].from || move.to != sd.killers[0][ply].to)
&& (move.from != sd.killers[1][ply].from || move.to != sd.killers[1][ply].to)
&& !move_iscapt(move)
&& !move_isprom(move)) {

/****************************************************************
* Real programs tend use more advanced formulas to calculate *
* reduction depth. Typically they calculate it from both *
* remaining depth and move count. Formula used here is very *
* basic and gives only a minimal improvement over uniform *
* one ply reduction, and is included for the sake of complete- *
* ness only. *
****************************************************************/

reduction_depth = 1;
if (moves_tried > 8) reduction_depth += 1;

new_depth -= reduction_depth;
}

re_search:

/********************************************************************
* The code below introduces principal variation search. It means *
* that once we are in a PV-node (indicated by IS_PV flag) and we *
* have found a move that raises alpha, we assume that the rest *
* of moves ought to be refuted. This is done relatively cheaply *
* by using a null-window search centered around alpha. Only if *
* this search fails high, we are forced repeat it with full window.*
* *
* Understanding the shorthand in the first two lines is a bit *
* tricky. If alpha has not been raised, we might be either in *
* a zero window (scout) node or in an open window (pv) node, *
* entered after a scout search failed high. In both cases, we *
* need to search with the same alpha, the same beta AND the same *
* node type. *
********************************************************************/

if (!raised_alpha)
val = -Search(new_depth, ply + 1, -beta, -alpha, DO_NULL, is_pv);
else {
// first try to refute a move - if this fails, do a real search
if (-Search(new_depth, ply + 1, -alpha - 1, -alpha, DO_NULL, NO_PV) > alpha)
val = -Search(new_depth, ply + 1, -beta, -alpha, DO_NULL, IS_PV);
}

/********************************************************************
* Sometimes reduced search brings us above alpha. This is unusual, *
* since we expected reduced move to be bad in first place. It is *
* not certain now, so let's search to the full, unreduced depth. *
********************************************************************/

if (reduction_depth && val > alpha) {
new_depth += reduction_depth;
reduction_depth = 0;
goto re_search;
}

move_unmake(move);

/********************************************************************
* If the move doesn't return -INF, it means that the King *
* couldn't be captured immediately. So the move was legal. In this *
* case we increase the legal_move counter, to look afterwards, *
* whether there were any legal moves on the board at all. *
********************************************************************/

legal_move += (val != -INF);

if (time_over) return 0;

/********************************************************************
* We can improve over alpha, so we change the node value together *
* with the expected move. Also the raised_alpha flag, needed to *
* control PVS, is set. In case of a beta cuoff, when our position *
* is so good that the score will not be accepted one ply before, *
* we return it immediately. *
********************************************************************/

if (val > alpha) {

bestmove = movelist[i].id;

if (val >= beta) {

/*************************************************************
* On a quiet move update killer moves and history table *
* in order to enhance move ordering. *
*************************************************************/

if (!move_iscapt(move)
&& !move_isprom(move)) {
setKillers(movelist[i], ply);
sd.history[move.from][move.to] += depth*depth;

/*********************************************************
* With super deep search history table would overflow *
* - let's prevent it. *
*********************************************************/

if (sd.history[move.from][move.to] > SORT_KILL) {
for (int a = 0; a < 128; a++)
for (int b = 0; b < 128; b++) {
sd.history[a][b] = sd.history[a][b] / 2;
}
}
}
tt_flag = TT_BETA;
alpha = beta;
break; // no need to search any further
}

raised_alpha = 1;
tt_flag = TT_EXACT;
alpha = val;

} // changing the node value is finished

} // end of looping through the moves

/************************************************************************
* Checkmate and stalemate detection: if we can't find a legal move *
* in the current position, we test if we are in check. If so, mate *
* score relative to search depth is returned. If not, we use draw *
* evaluation provided by contempt() function. *
************************************************************************/

if (!legal_move) {
bestmove = -1;

if (flagInCheck) alpha = -INF + ply;
else alpha = contempt();
}

/* tt_save() does not save anything when the search is timed out */
tt_save(depth, alpha, tt_flag, bestmove);

return alpha;
}

void setKillers(smove m, U8 ply) {

/* if a move isn't a capture, save it as a killer move */
if (m.piece_cap == PIECE_EMPTY) {

/* make sure killer moves will be different
before saving secondary killer move */
if (m.from != sd.killers[ply][0].from ||
m.to != sd.killers[ply][0].to
)
sd.killers[ply][1] = sd.killers[ply][0];

/* save primary killer move */
sd.killers[ply][0] = m;
}
}

void ReorderMoves(smove * m, U8 mcount, U8 ply) {

for (int j = 0; j<mcount; j++) {
if ((m[j].from == sd.killers[ply][1].from)
&& (m[j].to == sd.killers[ply][1].to)
&& (m[j].score < SORT_KILL - 1)) {
m[j].score = SORT_KILL - 1;
}

if ((m[j].from == sd.killers[ply][0].from)
&& (m[j].to == sd.killers[ply][0].to)
&& (m[j].score < SORT_KILL)) {
m[j].score = SORT_KILL;
}
}
}

int info_currmove(smove m, int nr) {

switch (mode) {
case PROTO_UCI:

char buffer[64];
char move[6];

algebraic_writemove(m, move);
sprintf(buffer, "info depth %d currmove %s currmovenumber %d", sd.depth, move, nr + 1);

com_send(buffer);
}
return 0;
}

int info_pv(int val) {
char buffer[2048];
char score[10];
char pv[2048];

if (abs(val) < INF - 2000) {
sprintf(score, "cp %d", val);
}
else {
//the mating value is returned in moves not plies ( thats why /2+1)
if (val > 0)
sprintf(score, "mate %d", (INF - val) / 2 + 1);
else
sprintf(score, "mate %d", -(INF + val) / 2 - 1);
}

U32 nodes = (U32)sd.nodes;
U32 time = gettime() - sd.starttime;

util_pv(pv);

if (mode == PROTO_NOTHING)
sprintf(buffer, " %2d. %9d %5d %5d %s", sd.depth, nodes, time / 10, val, pv);
else
sprintf(buffer, "info depth %d score %s time %u nodes %u nps %u pv %s", sd.depth, score, time, nodes, countNps(nodes, time), pv);

com_send(buffer);

return 0;
}

/***********************************************************
* countNps() guards against overflow and thus cares for *
* displaying correct nps during longer searches. Node *
* count is converted from U64 to unsigned int because of *
* some problems with output. *
***********************************************************/

unsigned int countNps(unsigned int nodes, unsigned int time) {
if (time == 0) return 0;

if (time > 20000)
return nodes / (time / 1000);
else
return (nodes * 1000) / time;
}

/***********************************************************
* Checking if the current position has been already *
* encountered on the current search path. Function *
* does NOT check the actual number of repetitions. *
***********************************************************/

int isRepetition() {

for (int i = 0; i < b.rep_index; i++) {
if (b.rep_stack[i] == b.hash)
return 1;
}

return 0;
}

/************************************************************
* Clearing the history table is needed at the beginning *
* of a search starting from a new position, like at the *
* beginning of a new game. *
************************************************************/

void clearHistoryTable() {
for (int i = 0; i < 128; i++)
for (int j = 0; j < 128; j++) {
sd.history[i][j] = 0;
}
}

/************************************************************
* ageHistoryTable() is run between searches to decrease *
* the history values used for move sorting. This causes *
* obsolete information to disappear gradually. Clearing *
* the table was worse for the move ordering. *
************************************************************/

void ageHistoryTable() {
for (int i = 0; i < 128; i++)
for (int j = 0; j < 128; j++) {
sd.history[i][j] = sd.history[i][j] / 8;
}
}

/************************************************************
* contempt() returns a draw value (which may be non-zero) *
* relative to the side to move and to the game stage. *
* This way we may make our program play for a draw or *
* strive to avoid it. *
************************************************************/

int contempt() {
int value = draw_opening;

if (b.PieceMaterial[sd.myside] < e.ENDGAME_MAT)
value = draw_endgame;

if (b.stm == sd.myside) return value;
else return -value;
}
</pre>
=Forum Posts=
* [http://www.talkchess.com/forum/viewtopic.php?t=56647 value > beta] by [[Robert Pope]], [[CCC]], June 10, 2015
* [http://www.open-chess.org/viewtopic.php?f=5&t=3056 Static NULL Move] by thevinenator, [[Computer Chess Forums|OpenChess Forum]], December 26, 2016 » [[Reverse Futility Pruning]]
* [http://www.open-chess.org/viewtopic.php?f=5&t=3074 NULL Move code question] by thevinenator, [[Computer Chess Forums|OpenChess Forum]], January 27, 2017 » [[Null Move Pruning]]

'''[[CPW-Engine|Up one Level]]'''

Navigation menu