Home Previous Bottom Next

Writing a chess program in 99 steps


step 79: search.cpp

#include <conio.h>
#include <stdio.h>
#include <iostream>
#include <iomanip>
#include "defines.h"
#include "extglobals.h"
#include "protos.h"
#include "board.h"
#include "timer.h"
 
Move Board::think()
{
 
//     ===========================================================================
//  This is the entry point for search, it is intended to drive iterative deepening
//  The search stops if (whatever comes first):
//  - there is no legal move (checkmate or stalemate)
//  - there is only one legal move (in this case we don't need to search)
//  **later** - time is up
//  - the search is interrupted by the user, or by winboard
//  - the search depth is reached
//     ===========================================================================
 
       int score, legalmoves, currentdepth;
       Move singlemove;
 
//     ===========================================================================
//     Check if the game has ended, or if there is only one legal move,
//  because then we don't need to search:
//     ===========================================================================
       if (isEndOfgame(legalmoves, singlemove)) return NOMOVE;
       if (legalmoves == 1)
       {
              std::cout << "forced move: "; displayMove(singlemove); std::cout << std::endl;
              return singlemove;
       }
 
//     ===========================================================================
//     There is more than legal 1 move, so prepare to search:
//     ===========================================================================
       lastPVLength = 0;
       memset(lastPV, 0 , sizeof(lastPV));
       memset(whiteHeuristics, 0, sizeof(whiteHeuristics));
       memset(blackHeuristics, 0, sizeof(blackHeuristics));
       inodes = 0;
       // display console header
       displaySearchStats(1, 0, 0); 
       timer.init();
       msStart = timer.getms();
 
       //  iterative deepening:
       for (currentdepth = 1; currentdepth <= searchDepth; currentdepth++)
       {
              //  clear the buffers:
              memset(moveBufLen, 0, sizeof(moveBufLen));
              memset(moveBuffer, 0, sizeof(moveBuffer));
              memset(triangularLength, 0, sizeof(triangularLength));
              memset(triangularArray, 0, sizeof(triangularArray));
              followpv = true;
              score = alphabetapvs(0, currentdepth, -LARGE_NUMBER, LARGE_NUMBER);
              msStop = timer.getms();
              displaySearchStats(2, currentdepth, score);
              // stop searching if the current depth leads to a forced mate:
              if ((score > (CHECKMATESCORE-currentdepth)) || (score < -(CHECKMATESCORE-currentdepth)))
                     currentdepth = searchDepth;
       }
       return (lastPV[0]);
}
 
int Board::alphabetapvs(int ply, int depth, int alpha, int beta)
{
       // PV search
 
       int i, j, movesfound, pvmovesfound, val;
       triangularLength[ply] = ply;
       if (depth == 0)
       {
              followpv = false;
              return board.eval();
       }
 
       // repetition check:
       if (repetitionCount() >= 3) return DRAWSCORE;
       movesfound = 0;
       pvmovesfound = 0;
       moveBufLen[ply+1] = movegen(moveBufLen[ply]);
       for (i = moveBufLen[ply]; i < moveBufLen[ply+1]; i++)
       {
              selectmove(ply, i, depth, followpv);
              makeMove(moveBuffer[i]);
              {
                     if (!isOtherKingAttacked())
                     {
                           inodes++;
                           movesfound++;
                           if (!ply) displaySearchStats(3, ply, i);
                           if (pvmovesfound)
                           {
                                  val = -alphabetapvs(ply+1, depth-1, -alpha-1, -alpha);
                          if ((val > alpha) && (val < beta))
                                  {
                                          // in case of failure, proceed with normal alphabeta
                                         val = -alphabetapvs(ply+1, depth - 1, -beta, -alpha);                        
                                  }
                           }
                           // normal alphabeta
                            else val = -alphabetapvs(ply+1, depth-1, -beta, -alpha);         
                           unmakeMove(moveBuffer[i]);
                           if (val >= beta)
                           {
                                  // update the history heuristic
                                  if (nextMove)
                                         blackHeuristics[moveBuffer[i].getFrom()][moveBuffer[i].getTosq()] += depth*depth;
                                  else
                                         whiteHeuristics[moveBuffer[i].getFrom()][moveBuffer[i].getTosq()] += depth*depth;
                                  return beta;
                           }
                           if (val > alpha)
                           {
                                  alpha = val;                                                      // both sides want to maximize from *their* perspective
                                  pvmovesfound++;
                                  triangularArray[ply][ply] = moveBuffer[i];                                  // save this move
                                  for (j = ply + 1; j < triangularLength[ply+1]; j++)
                                  {
                                         triangularArray[ply][j] = triangularArray[ply+1][j];   // and append the latest best PV from deeper plies
                                  }
                                  triangularLength[ply] = triangularLength[ply+1];
                                  if (!ply)
                                  {
                                         msStop = timer.getms();
                                         displaySearchStats(2, depth, val);
                                  }
                           }
                     }
                     else unmakeMove(moveBuffer[i]);
              }
       }
 
       // update the history heuristic
       if (pvmovesfound)
       {
              if (nextMove)
                     blackHeuristics[triangularArray[ply][ply].getFrom()][triangularArray[ply][ply].getTosq()] += depth*depth;
              else
                     whiteHeuristics[triangularArray[ply][ply].getFrom()][triangularArray[ply][ply].getTosq()] += depth*depth;
       }
 
       //     50-move rule:
       if (fiftyMove >= 100) return DRAWSCORE;
 
       //     Checkmate/stalemate detection:
       if (!movesfound)
       {
              if (isOwnKingAttacked())  return (-CHECKMATESCORE+ply-1);
              else  return (STALEMATESCORE);
       }
 
       return alpha;
}
 
int Board::alphabeta(int ply, int depth, int alpha, int beta)
{
       // Negascout
 
       int i, j, val;
 
       triangularLength[ply] = ply;
       if (depth == 0) return board.eval();
 
       moveBufLen[ply+1] = movegen(moveBufLen[ply]);
       for (i = moveBufLen[ply]; i < moveBufLen[ply+1]; i++)
       {
              makeMove(moveBuffer[i]);
              {
                     if (!isOtherKingAttacked())
                     {
                           inodes++;
                           if (!ply) displaySearchStats(3, ply, i);
                           val = -alphabeta(ply+1, depth-1, -beta, -alpha);
                           unmakeMove(moveBuffer[i]);
                           if (val >= beta)
                           {
                                  return beta;
                           }
                           if (val > alpha)
                           {
                                  alpha = val;                                                                              // both sides want to maximize from *their* perspective
                                  triangularArray[ply][ply] = moveBuffer[i];                                  // save this move
                                  for (j = ply + 1; j < triangularLength[ply + 1]; j++)
                                  {
                                         triangularArray[ply][j] = triangularArray[ply+1][j];   // and append the latest best PV from deeper plies
                                  }
                                  triangularLength[ply] = triangularLength[ply + 1];
                                  if (!ply)
                                  {
                                         msStop = timer.getms();
                                         displaySearchStats(2, depth, val);
                                  }
                           }
                     }
                     else unmakeMove(moveBuffer[i]);
              }
       }
       return alpha;
}
 
int Board::minimax(int ply, int depth)
{
       // Negamax
 
       int i, j, val, best;
       best = -LARGE_NUMBER;
       triangularLength[ply] = ply;
       if (depth == 0) return board.eval();
       moveBufLen[ply+1] = movegen(moveBufLen[ply]);
       for (i = moveBufLen[ply]; i < moveBufLen[ply+1]; i++)
       {
              makeMove(moveBuffer[i]);
              {
                     if (!isOtherKingAttacked())
                     {
                           inodes++;
                           if (!ply) displaySearchStats(3, ply, i);
                           val = -minimax(ply+1, depth-1);                                 // note the minus sign
                           unmakeMove(moveBuffer[i]);
                           if (val > best)                                                 // both sides want to maximize from *their* perspective
                           {
                                  best = val;
                                  triangularArray[ply][ply] = moveBuffer[i];                                  // save this move
                                  for (j = ply + 1; j < triangularLength[ply + 1]; j++)
                                  {
                                         triangularArray[ply][j] = triangularArray[ply+1][j];   // and append the latest best PV from deeper plies
                                  }
                                  triangularLength[ply] = triangularLength[ply + 1];
                                  if (!ply)
                                  {
                                         msStop = timer.getms();
                                         displaySearchStats(2, depth, val);
                                  }
                           }
                     }
                     else unmakeMove(moveBuffer[i]);
              }
       }
       return best;
}
 
 
void Board::displaySearchStats(int mode, int depth, int score)
{
       // displays various search statistics
       // only to be used when ply = 0
       // mode = 1 : display header
       // mode = 2 : display full stats, including score and latest PV
       // mode = 3 : display current root move that is being searched
       //                     depth = ply, score = loop counter in the search move list
 
       char sanMove[12];
       U64 dt, hh, mm, ss;
 
       switch (mode)
       {
              case 1:
                           std::cout << "depth  score   nodes     time  knps PV" << std::endl;
                           break;
 
              case 2:
                           dt = msStop - msStart;
 
                           // depth
                           printf("%5d ", depth);
 
                           // score
                           printf("%+6.2f ", float(score/100.0));
 
                           // nodes searched
                            if      (inodes > 100000000) printf("%6.0f%c ", float(inodes/1000000.0), 'M');
                           else if (inodes > 10000000) printf("%6.2f%c ", float(inodes/1000000.0), 'M');
                           else if (inodes > 1000000) printf("%6.0f%c ", float(inodes/1000.0),    'K');
                           else if (inodes > 100000)  printf("%6.1f%c ", float(inodes/1000.0),    'K');
                           else if (inodes > 10000)   printf("%6.2f%c ", float(inodes/1000.0),    'K');
                           else                                     printf("%7d ", inodes);
 
                           // search time
                           if (dt > 3600000)
                           {     
                                  hh = dt/3600000;
                                  mm = (dt - 3600000*hh)/60000;
                                  ss = (dt - 3600000*hh - 60000*mm)/1000;
                                  printf("%02d%c", hh, ':');
                                  printf("%02d%c", mm, ':');
                                  printf("%02d ", ss);
                           }
                           else if (dt > 60000)
                           {     
                                  mm = dt/60000;
                                  ss = (dt - 60000*mm)/1000;
                                  printf("   %02d%c", mm, ':');
                                  printf("%02d ", ss);
                           }
                           else if (dt > 10000)       printf(" %6.1f%c ", float(dt/1000.0), 's');
                           else if (dt > 1000)               printf(" %6.2f%c ", float(dt/1000.0), 's');
                           else if (dt > 0)                  printf(" %5dms ", dt);
                           else                                     printf("     0ms ");
 
                           // search speed
                           if (dt > 0) std::cout << std::setw(5) << (inodes/dt) << " ";
                           else          std::cout << "    - ";
 
                           // store this PV:
                           rememberPV();
 
                           // display the PV
                           displayPV();
                           break;
 
              case 3: // Note that the numbers refer to pseudo-legal moves:
                           printf("%12s (%2d/%2d)%16s", " ", score+1, moveBufLen[depth+1]-moveBufLen[depth], " ");
                           unmakeMove(moveBuffer[score]);
                           toSan(moveBuffer[score], sanMove);
                          std::cout << sanMove;
                           makeMove(moveBuffer[score]);
//                         printf("...    \n");
                           printf("...    \r");
                           std::cout.flush();
                           break;
 
              default: break;
       }
 
       return;
}
 
BOOLTYPE Board::isEndOfgame(int &legalmoves, Move &singlemove)
{
       // Checks if the current position is end-of-game due to:
       // checkmate, stalemate, 50-move rule, or insufficient material
 
       int whiteknights, whitebishops, whiterooks, whitequeens, whitetotalmat;
       int blackknights, blackbishops, blackrooks, blackqueens, blacktotalmat;
 
       // are we checkmating the other side?
       int i;
       if (isOtherKingAttacked())
       {
              if (nextMove) std::cout << "1-0 {Black mates}" << std::endl;
              else std::cout << "1-0 {White mates}" << std::endl;
              return true;
       }
 
       // how many legal moves do we have?
       legalmoves = 0;
       moveBufLen[0] = 0;
       moveBufLen[1] = movegen(moveBufLen[0]);
       for (i = moveBufLen[0]; i < moveBufLen[1]; i++)
       {
              makeMove(moveBuffer[i]);
              if (!isOtherKingAttacked())
              {
                     legalmoves++;
                     singlemove = moveBuffer[i];
              }
              unmakeMove(moveBuffer[i]);
       }
 
       // checkmate or stalemate?
       if (!legalmoves)
       {
              if (isOwnKingAttacked())
              {
                     if (nextMove) std::cout << "1-0 {White mates}" << std::endl;
                     else std::cout << "1-0 {Black mates}" << std::endl;
              }
              else std::cout << "1/2-1/2 {stalemate}" << std::endl;
              return true;
       }
 
       // draw due to insufficient material:
    if (!whitePawns && !blackPawns)
       {
              whiteknights = bitCnt(whiteKnights);
              whitebishops = bitCnt(whiteBishops);
              whiterooks = bitCnt(whiteRooks);
              whitequeens = bitCnt(whiteQueens);
              whitetotalmat = 3 * whiteknights + 3 * whitebishops + 5 * whiterooks + 10 * whitequeens;
              blackknights = bitCnt(blackKnights);
              blackbishops = bitCnt(blackBishops);
              blackrooks = bitCnt(blackRooks);
              blackqueens = bitCnt(blackQueens);
              blacktotalmat = 3 * blackknights + 3 * blackbishops + 5 * blackrooks + 10 * blackqueens;
 
              // king versus king:
              if ((whitetotalmat == 0) && (blacktotalmat == 0))
              {
                     std::cout << "1/2-1/2 {material}" << std::endl;
                     return true;
              }
 
              // king and knight versus king:
              if (((whitetotalmat == 3) && (whiteknights == 1) && (blacktotalmat == 0)) ||
                    ((blacktotalmat == 3) && (blackknights == 1) && (whitetotalmat == 0)))
              {
                     std::cout << "1/2-1/2 {material}" << std::endl;
                     return true;
              }
 
        // 2 kings with one or more bishops, all bishops on the same colour:
              if ((whitebishops + blackbishops) > 0)
              {
                     if ((whiteknights == 0) && (whiterooks == 0) && (whitequeens == 0) &&
                           (blackknights == 0) && (blackrooks == 0) && (blackqueens == 0))
                     {
                           if (!((whiteBishops | blackBishops) & WHITE_SQUARES) ||
                !((whiteBishops | blackBishops) & BLACK_SQUARES))
                           {
                                  std::cout << "1/2-1/2 {material}" << std::endl;
                                  return true;
                           }
                     }
              }
       }
 
       // draw due to repetition:
       if (repetitionCount() >= 3)
       {
              std::cout << "1/2-1/2 {repetition}" << std::endl;
              return true;
 
       }
 
       // draw due to 50 move rule:
       if (fiftyMove >= 100)
       {
              std::cout << "1/2-1/2 {50-move rule}" << std::endl;
              return true;
       }
 
       return false;
}
 
int Board::repetitionCount()
{
       //     repetitionCount is used to detect threefold repetitions of the current position
 
       int i, ilast, rep;
       rep = 1;                                              // current position has occurred once, namely now!
       ilast = board.endOfSearch - board.fiftyMove;          // we don't need to go back all the way
       for (i = board.endOfSearch - 2; i >= ilast; i -= 2)   // we can skip every other position
       {
              if (gameLine[i].key == board.hashkey) rep++;
       }
       return rep;
}
 
void Board::rememberPV()
{
       // remember the last PV
       int i;
       lastPVLength = board.triangularLength[0];
       for (i = 0; i < board.triangularLength[0]; i++)
       {
              lastPV[i] = board.triangularArray[0][i];
       }
}

 

  try the improved search speed for different positions, and compare the total nodes visited with a perft for the same position & depth, chances are that less than 0.1% (!) of the total nodes in the full tree are actually visited during the search, without loss of accuracy.
  you can calculate the effective branching factor in different situations, with this formula:
                                             branching factor = nodes (1/depth)
 
This should be roughly equal to the time factor between successive iterations. Without hash tables, an effective branching factor of 6 is good.


Home Previous Top Next

last update: Sunday 26 June 2011