Code of search method does not fit on the screen. I tried to use more high level subroutines with unclear names and large parameter lists but that was a failure for you have to look up all the time.
Current lines of code for search is about 190.
Search readability
Moderators: hgm, Rebel, chrisw
-
- Posts: 433
- Joined: Fri Jan 16, 2015 4:02 pm
Re: Search readability
My main search function has 200 lines too. Recursive functions are very hard to split, especially when you have such a large state per call (alpha, beta, score, depth, moves, lmr / nmp flags, in check, best score, best move, etc).
But of course, I do my best to simplify the code all the time, even if it's only a couple of lines. This recent commit [1] cut 4 lines from the search function. It's very important to maintain a mental model of the engine and with chess engines it's easy to have an exploding complexity.
https://bitbucket.org/zurichess/zuriche ... 267c6f8fff
But of course, I do my best to simplify the code all the time, even if it's only a couple of lines. This recent commit [1] cut 4 lines from the search function. It's very important to maintain a mental model of the engine and with chess engines it's easy to have an exploding complexity.
https://bitbucket.org/zurichess/zuriche ... 267c6f8fff
zurichess - http://www.zurichess.xyz
-
- Posts: 7216
- Joined: Mon May 27, 2013 10:31 am
Re: Search readability
If code maintainability gets worse and worse then end of project is near.
So it should or must have highest priority.
So it should or must have highest priority.
-
- Posts: 4052
- Joined: Thu May 15, 2008 9:57 pm
- Location: Berlin, Germany
- Full name: Sven Schüle
Re: Search readability
This is a well-known problem. Many engines have huge and monolithic search functions. There are various approaches to deal with it, e.g.:Henk wrote:Code of search method does not fit on the screen. I tried to use more high level subroutines with unclear names and large parameter lists but that was a failure for you have to look up all the time.
Current lines of code for search is about 190.
- Keep a clear structure and use lots of comments. Examples: Stockfish, Crafty.
- Use various different search functions for specific purposes where each function is much smaller. Problem: code duplication. Advantage: you can understand each function separately (hopefully).
- Use subroutines for some common parts. I have done this in some my engines. Example: a family of "expand subtree of move" subroutines (or one with several parameters) that cover just makeMove(), the recursive search call, and unmakeMove(), and return the score from the recursive call. And another subroutine that does the typical "if (score >= beta) DO_THE_CUTOFF ...; if (score > bestScore) bestScore = score;" bookkeeping stuff, and returns a boolean indicating whether a beta cutoff has been detected. Also everything that checks for one of several possible draw situations could be combined in one function.
The last one is a possible way to keep the search function, or all of the different search functions, slightly shorter in terms of "lines of code". But it is not clear whether this actually helps to keep the whole search code simple and easy to understand so I do not explicitly encourage everyone to follow this approach (nor do I want to discourage anyone to do so, of course!). So maybe the two other approaches are best.
With null move, LMR, futility pruning, PVS, and possibly other search features the problem becomes more urgent but even without that it is not trivial to keep search functions easy to read.
If you feel your (search) code has become very hard to maintain then consider a rewrite. Many people have successfully done so before.
-
- Posts: 4366
- Joined: Fri Mar 10, 2006 5:23 am
- Location: http://www.arasanchess.org
Re: Search readability
I dunno. There are static analysis tools that will measure function complexity and tell you to break up big routines. On the other hand, complexity is not a sign of problems necessarily. I have worked on commercial systems that were huge and complex by every measure.
--Jon
--Jon
-
- Posts: 7216
- Joined: Mon May 27, 2013 10:31 am
Re: Search readability
I remember I had to develop a component to test a generic library written by others. Only after two months they admitted that it was not possible to use the library for that component. For functionality was not there.
So each time they told me I had to use this or that functionality or I should re-use another component.
Perhaps Russian politics. At least they were Russian.
Also the one who gave me that task knew in advance that it would probably fail. But he could not proof that they were lying. Still don't understand why he did not fire me in the first week when there is no appropriate work.
Though it might have been that it was all a misunderstanding.
So never trust generic libraries or tools. Usually it is all crap unless there are very many users.
So each time they told me I had to use this or that functionality or I should re-use another component.
Perhaps Russian politics. At least they were Russian.
Also the one who gave me that task knew in advance that it would probably fail. But he could not proof that they were lying. Still don't understand why he did not fire me in the first week when there is no appropriate work.
Though it might have been that it was all a misunderstanding.
So never trust generic libraries or tools. Usually it is all crap unless there are very many users.
-
- Posts: 1563
- Joined: Thu Jul 16, 2009 10:47 am
- Location: Almere, The Netherlands
Re: Search readability
Henk, I fully agree with you on this, I try to avoid libraries written by others as the plague.Henk wrote: So never trust generic libraries or tools. Usually it is all crap unless there are very many users.
Most open source stuff you'll find on the internet is very messy and buggy with maybe a few exceptions.
-
- Posts: 2929
- Joined: Sat Jan 22, 2011 12:42 am
- Location: NL
Re: Search readability
In my more recent endeavours the search looks like this:
It doesn't have as many bells and whistles, and of course it has qsearch as a separate function, but overall I quite like the structure I have here. I don't use a separate root search function.
Code: Select all
int search(game_t *game, int alpha, int beta, int draft, int ply)
{
assert(game);
assert(beta > alpha);
/* Truncate the principle variation at this depth here, otherwise we may
* end up with rubbish at the end of the PV.
*/
truncate_principle_variation(game, ply);
/* The very first thing we do is to check if this position has occurred
* three times in the game tree, or whether no irreversible move has
* been played in 50 moves (100 ply). Either of these conditions means
* we should flag a draw and return immediately.
* During the search, we simply score positions as a draw if they have
* occurred at least once before. From the top level, we don't claim a
* draw until three repetitions have been made.
*/
if (ply > 0) {
if (legal_draw(game) || position_repeated_once(game))
return 0;
}
/* Check whether our search time for this move has expired */
check_clock();
if (abort_search) return 0;
/* Check whether we're looking for a checkmate.
* If so, prune branches that are worse than the best mate found so far
* (mate distance pruning).
*/
alpha = max(alpha, -CHECKMATE + ply);
beta = min(beta, CHECKMATE - ply - 1);
if (alpha >= beta)
return alpha;
if (draft <= 0 || ply > MAX_SEARCH_DEPTH)
return qsearch(game, alpha, beta, draft, ply);
/* Look up this position in the transposition table */
move_t hash_move = 0;
int hash_score_upper = 0;
int hash_score_lower = 0;
int hash_depth_upper = 0;
int hash_depth_lower = 0;
bool have_hash = retrieve_table(game->transposition_table, game->board.hash, &hash_depth_lower, &hash_score_lower, &hash_depth_upper, &hash_score_upper, &hash_move);
game->hash_found += have_hash;
/* Internal iterative deepening */
if (!have_hash && alpha != beta-1 && draft > 1) {
search(game, alpha, beta, draft-1, ply);
have_hash = retrieve_table(game->transposition_table, game->board.hash, &hash_depth_lower, &hash_score_lower, &hash_depth_upper, &hash_score_upper, &hash_move);
}
if (have_hash && game->board.fifty_counter < 80) {
if (hash_depth_lower>0) hash_score_lower = score_from_hashtable(hash_score_lower, ply);
if (hash_depth_upper>0) hash_score_upper = score_from_hashtable(hash_score_upper, ply);
if ((hash_depth_lower >= draft || is_mate_score(hash_score_lower)) && hash_depth_lower>0 && hash_score_lower >= beta) {
game->hash_cut++;
return hash_score_lower;
}
if ((hash_depth_upper >= draft || is_mate_score(hash_score_upper)) && hash_depth_upper>0 && hash_score_upper <= alpha) {
game->hash_cut++;
return hash_score_upper;
}
if (hash_score_upper == hash_score_lower && alpha != beta-1 && hash_depth_upper>0 && (hash_depth_upper >= draft || is_mate_score(hash_score_upper))) {
game->hash_cut++;
return hash_score_upper;
}
}
/* First try a null-move.
* This serves a double-purpose: it is also used as a check-detection.
*/
bool check = game->board.check;
int static_score = (check) ? -CHECKMATE
: evaluate(game);
move_t prev_move = game->moves_played ? game->move_list[game->moves_played-1] : 0;
bool avoid_null = beta > alpha+1 ||
check ||
draft < 4 ||
prev_move == 0 ||
zugzwang_threat(&game->board) ||
static_score < beta ||
is_mate_score(beta);
if (!avoid_null) {
int r = 2 + draft / 4;
int score;
play_null_move(game);
game->clock.nodes_searched++;
score = -search(game, -beta, -(beta-1), draft - 1 - r, ply+1);
takeback_null_move(game);
if (score >= beta && !is_mate_score(score) && !abort_search)
return score;
}
/* Futility pruning */
if (!check && draft == 1 && ((static_score + 700) < alpha || (static_score - 700) > beta) && !is_mate_score(alpha))
return static_score;
/* Generate moves - not needed in the root, where it was already done by
* the iterative deepening loop.
* If the other side is in check, we return a -ILLEGAL score which is
* caught one level up to mark the preceding move as illegal.
*/
movelist_t *movelist = game->movelist + ply;
bool legal = true;
if (ply == 0)
rewind_movelist(movelist);
else
legal = movegen(movelist, &game->board);
if (!legal) return -ILLEGAL;
int legal_moves = movelist->num_moves;
/* Score the moves */
score_moves(game, movelist, ply, hash_move);
/* Search the first move */
int score = -CHECKMATE;
move_t move = 0;
while (legal_moves) {
move = next_move(movelist);
game->best_move[ply] = move;
playmove(game, move);
game->clock.nodes_searched++;
game->board.check = in_check(&game->board, game->board.side_to_move);
int e = draft <= 1 && game->board.check && see(&game->board, move) > 0;
score = -search(game, -beta, -alpha, draft-1 + e, ply+1);
takeback(game);
if (score == ILLEGAL) {
legal_moves--;
continue;
}
break;
}
/* If there are no legal moves, the game is over */
if (legal_moves == 0) {
score = (check || game->stale_win) ? (-CHECKMATE + ply) : STALEMATE;
return score;
}
if (score > alpha) { /* New best line found */
alpha = score;
adjust_history_score(game, move, draft * draft);
/* Update and store principle variation */
backup_principle_variation(game, ply, move);
}
/* Search remaining moves; we know there is at least one legal move at
* this point, so don't bother tracking those anymore.
*/
move_t best_move = move;
int moves_searched = 1;
int best_score = score;
while (alpha < beta && movelist->cur_move < movelist->num_moves) {
move = next_move(movelist);
/* Futility pruning */
if (draft == 1 && !check && !is_mate_score(beta) && static_score < beta) {
/* Losing captures */
if (is_capture_move(&game->board, move) && get_move_score(movelist) < 1000) continue;
}
playmove(game, move);
game->clock.nodes_searched++;
int e = get_extension(move, get_move_score(movelist));
int r = (e == 0) ? get_reduction(move, get_move_score(movelist), moves_searched, draft) : 0;
/* Check extension: extend checking moves */
game->board.check = in_check(&game->board, game->board.side_to_move);
if (game->board.check) {
e = 1;
r = 0;
}
score = -search(game, -(alpha+1), -alpha, draft-1 + e - r, ply+1);
/* Verification search, only useful when the window is not a null
* window.
*/
if (score > alpha && score < beta)
score = -search(game, -beta, -alpha, draft-1 + e, ply+1);
takeback(game);
if (score == ILLEGAL) continue;
if (score > best_score) {
best_score = score;
best_move = game->best_move[ply] = move;
}
if (score > alpha) { /* New best line found */
alpha = score;
best_move = move;
adjust_history_score(game, move, draft * draft);
/* Update and store principle variation */
backup_principle_variation(game, ply, move);
}
moves_searched++;
}
if (alpha >= beta) { /* Beta cutoff */
/* Store good moves in the killer slots, but only if:
* - we were not in check at the beginning of this move
* - the move is not a promotion (already high in the list)
* - the move was not a capture (already high in the list)
*/
store_killer(game, best_move, ply);
store_mate_killer(game, best_move, ply, best_score);
if (prev_move == 0)
store_null_killer(game, ply, best_move);
game->branches_pruned_1st += (moves_searched == 1);
game->branches_pruned_lt3rd += (moves_searched < 3);
game->branches_pruned++;
for (int n=0; n<movelist->cur_move-1; n++)
adjust_history_score(game, movelist->move[n], -draft * draft);
}
/* Abort if we've exceeded our allowed time to think */
if (abort_search) return 0;
/* Store best move for retrieval from one level up in the search (for IID) */
game->best_move[ply] = best_move;
/* store evaluation in the transposition table */
if (!have_hash) {
hash_move = 0;
hash_score_upper = CHECKMATE;
hash_score_lower = -CHECKMATE;
hash_depth_upper = 0;
hash_depth_lower = 0;
}
if (best_score < beta) {
hash_score_upper = best_score;
hash_depth_upper = draft;
hash_move = 0;
}
if (best_score > alpha) {
hash_score_lower = best_score;
hash_depth_lower = draft;
hash_move = best_move;
}
if (hash_depth_lower>0) hash_score_lower = score_to_hashtable(hash_score_lower, ply);
if (hash_depth_upper>0) hash_score_upper = score_to_hashtable(hash_score_upper, ply);
store_table_entry(game->transposition_table, game->board.hash, hash_depth_lower, hash_score_lower, hash_depth_upper, hash_score_upper, hash_move);
return best_score;
}
-
- Posts: 1563
- Joined: Thu Jul 16, 2009 10:47 am
- Location: Almere, The Netherlands
Re: Search readability
In Nightmare there are many functions in the search() about 1100 lines in total.Henk wrote:Code of search method does not fit on the screen. I tried to use more high level subroutines with unclear names and large parameter lists but that was a failure for you have to look up all the time.
Current lines of code for search is about 190.
Basically they are:
search_root();
search_pv();
search_zw();
search_pv_smp();
search_zw_smp();
quiescence_pv();
quiescence_zw();
quiescence_evasions_pv();
quiescence_evasions_zw();
The search could be made smaller by merging things but that will make it less easy to read.
The most annoying thing is that some parts of my main search are mirrored in my smp search and that I have to make changes to both of them, this is something I forgot a few times in the past.
-
- Posts: 20943
- Joined: Mon Feb 27, 2006 7:30 pm
- Location: Birmingham, AL
Re: Search readability
That is THE reason I discontinued this bad practice several years ago.Joost Buijs wrote:In Nightmare there are many functions in the search() about 1100 lines in total.Henk wrote:Code of search method does not fit on the screen. I tried to use more high level subroutines with unclear names and large parameter lists but that was a failure for you have to look up all the time.
Current lines of code for search is about 190.
Basically they are:
search_root();
search_pv();
search_zw();
search_pv_smp();
search_zw_smp();
quiescence_pv();
quiescence_zw();
quiescence_evasions_pv();
quiescence_evasions_zw();
The search could be made smaller by merging things but that will make it less easy to read.
The most annoying thing is that some parts of my main search are mirrored in my smp search and that I have to make changes to both of them, this is something I forgot a few times in the past.
I have a single Search() procedure, and a simple Quiesce() procedure. I have one special case, QuiesceEvasions() that I will get rid of pretty soon...