Writing a chess program in 99 steps |
![]() |
||||
12 Making the movesMaking and unmaking moves is mostly about good housekeeping & administration of the board variables. Not all chess moves follow the same rules, and there are quite some exceptions that need to be dealt with (think of pawn moves, promotions, castlings, etc). This means that testing and verification is very important at this stage (using perft and special purpose debugging functions) - it's just too easy to make mistakes here. Solely for the purpose of debugging (un)makemove, I have added a function debugMoves, to checks consistency of most board variables after making and unmaking moves. This function can be activated at compile time with by uncommenting the compiler directive WINGLET_DEBUG_MOVES. How moves are storedWe need to store all generated moves at different search depths (plies), so we can trace our way back to the root and search another branch of the tree without having to generate the moves again. There is always going to be one set of moves for each ply that we need to store (see diagram below). To keep track of which moves in moveBuffer belong to which ply, another array is introduced: moveBufLen This array is populated in such a way that we can easily find the moves for a specific ply, as follows:
The move list is made part of the board structure. Keeping all search related data together would make it easier later to implement a parallel search algorithm (in case you want to embark on such a project). The current search line plus moves that have actually been played (the game line) are stored in a separate array, gameLine. This array also keeps track of the en-passant square and castling rights, so they can be copied back into the board structure when backing up moves.
Adding timersAfter the makeMove & unmakeMove algorithms have been implemented, it's good to have a way of measuring the 'back-bone speed' of the engine, i.e. how long a 'perft' (see below) takes in real time. I added a timer structure with corresponding methods. Using it is easy, you call timer.getms() before and after the section you want to time, and then calculate the time difference: "nodes = " << perftcount << ", " << msStop - msStart << " ms, "; Don't get too excited when you see high knods/s counts for perft (it's not unusual to see more than 10 million nodes per second for perft, or more than 10000 knods/s, depending on your hardware). This speed is going to drop significantly when doing real searches and evaluating leaf nodes. step 37: timer.h#ifndef WINGLET_TIMER_H#define WINGLET_TIMER_H#include <sys/timeb.h>#include "defines.h"struct Timer{U64 startTime;U64 stopTime;U64 currentTime;U64 stopTimeDelta;timeb startBuffer;timeb stopBuffer;timeb currentBuffer;BOOLTYPE running;void init(); // start the timervoid stop(); // stop the timervoid reset(); // reset the timervoid display(); // display time in seconds with 2 decimalsvoid displayhms(); // display time in hh:mm:ss.ddU64 getms(); // return time in millisecondsU64 getsysms(); // return system timeTimer();};#endif
step 38: timer.cpp#include <sys/timeb.h>#include <stdio.h>#include "timer.h"#pragma warning (disable: 4244)Timer::Timer(){running = false;startTime = 0;stopTime = 0;stopTimeDelta = 0;}void Timer::init(){if (!running){running = true;ftime(&startBuffer);startTime = startBuffer.time * 1000 + startBuffer.millitm + stopTimeDelta;}return;}void Timer::stop(){if (running){running = false;ftime(&stopBuffer);stopTime = stopBuffer.time * 1000 + stopBuffer.millitm;stopTimeDelta = startTime - stopTime;}return;}void Timer::reset(){if (running){ftime(&startBuffer);startTime = startBuffer.time * 1000 + startBuffer.millitm;}else{startTime = stopTime;stopTimeDelta = 0;}return;}void Timer::display(){if (running){ftime(¤tBuffer);currentTime = currentBuffer.time * 1000 + currentBuffer.millitm;printf("%6.2f", (currentTime - startTime)/1000.0);}elseprintf("%6.2f", (stopTime - startTime)/1000.0);return;}void Timer::displayhms(){int hh, mm, ss;if (running){ftime(¤tBuffer);currentTime = currentBuffer.time * 1000 + currentBuffer.millitm;hh = (currentTime - startTime)/1000/3600;mm = ((currentTime - startTime)-hh*3600000)/1000/60;ss = ((currentTime - startTime)-hh*3600000-mm*60000)/1000;printf("%02d:%02d:%02d", hh, mm, ss);}else{hh = (stopTime - startTime)/1000/3600;mm = ((stopTime - startTime)-hh*3600000)/1000/60;ss = ((stopTime - startTime)-hh*3600000-mm*60000)/1000;printf("%02d:%02d:%02d", hh, mm, ss);}return;}U64 Timer::getms(){if (running){ftime(¤tBuffer);currentTime = currentBuffer.time * 1000 + currentBuffer.millitm;return (currentTime - startTime) ;}elsereturn (stopTime - startTime);}U64 Timer::getsysms(){ftime(¤tBuffer);return (currentBuffer.time * 1000 + currentBuffer.millitm);}
Perft, measuring speed and verify correctnessPerft was first introduced in an early version of Crafty and since then has become a 'standard' test for chess engines. The original intent was to verify correctness of the move generator and (un)makemove, but it is nowadays also used to compare computational speed of different move and board implementations. Perft does a raw node count, up to some specified depth, by doing a full tree search. The procedure is very similar to a search (except for the absence of tree cut-offs), and instead of evaluating at the leaves, we just count the leaf nodes. Here are some test positions with results. step 39: perft.cpp#include <iostream>#include "defines.h"#include "extglobals.h"#include "protos.h"U64 perft(int ply, int depth){// Raw node count, up to depth, doing a full tree search.// perft is very similar to the search algorithm - instead of evaluating the leaves, we count them.//// Be carefull when calling this function with depths > 7, because it can take a very long// time before the result is returned: the average branching factor in chess is 35, so every// increment in depth will require 35x more computer time.//// perft is a good way of verifying correctness of the movegenerator and (un)makeMove,// because you can compare the results with published results for certain test positions.//// perft is also used to measure the performance of the move generator and (un)makeMove in terms// of speed, and to compare different implementations of generating, storing and (un)making moves.U64 retVal = 0;int i;// count this nodeif (depth == 0){return 1;}// generate moves from this positionboard.moveBufLen[ply+1] = movegen(board.moveBufLen[ply]);// loop over moves:for (i = board.moveBufLen[ply]; i < board.moveBufLen[ply+1]; i++){makeMove(board.moveBuffer[i]);{if (!isOtherKingAttacked()){// recursively call perftretVal += perft(ply + 1, depth-1);#ifdef WINGLET_DEBUG_PERFTif (depth == 1){if (board.moveBuffer[i].isCapture()) ICAPT++;if (board.moveBuffer[i].isEnpassant()) IEP++;if (board.moveBuffer[i].isPromotion()) IPROM++;if (board.moveBuffer[i].isCastleOO()) ICASTLOO++;if (board.moveBuffer[i].isCastleOOO()) ICASTLOOO++;if (isOwnKingAttacked()) ICHECK++;}#endif}}unmakeMove(board.moveBuffer[i]);}return retVal;}
|
|||||
|
|
|||||
|
last update: Saturday 11 June 2011 |
||||