Home Previous Bottom Next

Writing a chess program in 99 steps


21  Connecting to Winboard

Having a chess board representation on the console is OK for testing and developing purposes,  but if you want to do some serious game playing, or analyze a position, then a graphical user interface (GUI) is a the better option.   Actually, a GUI will also help in testing and developing, because it's easier & faster to read and set up positions, and easier to see how the engine behaves when analyzing different positions.  Fortunately we don't have to write the GUI ourselves.

An engine and the interface are two individual Windows programs that communicate: the engine needs to know what the GUI (or user) wants, and the GUI needs to know what the engine is doing.   There are a couple of free chess interfaces available that we can use, winglet will be supporting the winboard protocol.   WinBoard displays the chess board on the screen, accepts moves made with the mouse, and loads and saves game files in standard chess notation. Winboard serves as a front-end for many different chess engines. You can play a game against an engine, set up arbitrary positions, force variations, or watch a game between two engines. Winboard has an extensive help file.

Specifying and reading an initialization file as command line option

When winglet is connected to winboard, we don't have the console anymore to enter and change program settings.  Therefore, at program start-up, an initialization file is read (default filename is wingletx.ini) that contains a number of parameters.  The name of the initialization file can be specified at start-up (using this command syntax: "wingletx.exe i=filename"). So you can have different initialization files and choose which one to use when starting the program, or define different shortcuts, one for every initialization file.  In function main() you will find code that reads the command line parameters. readIniFile() is the function that reads the initialization file.

Think, analyze, ponder

One of the programmer's challenges when writing a driver is to figure out how communication is exactly done, in what order commands are being sent to the engine and what the engine needs to send back to winboard in response.  Another problem is when and how to process commands sent from winboard during search, ponder (thinking in the opponent's time) or analyzing positions.  Some commands can be dealt with without stopping the search, but other commands can only be executed after the search has been interrupted. Following table gives an overview of some of the differences between searching for the best move, pondering, and analyzing a position:

type of search: in a timed match? when to do it ? when to end? is it interruptable? make the best move when done/interrupted?
think yes in side to move's  time search depth is reached, or time is up no (i.e. normally not) yes
analyze no when users asks for it (command 'analyze') when the winboard user enters a move yes no
ponder yes in the opponents' time when the opponent makes a move yes no

Note that pondering is implemented, but it really does not make a lot of sense now because the engine does not have transposition tables.  The idea of pondering is to use the opponents time, continue searching the tree and store the information in a large lookup table for later use (when it's our turn again).  This will save time when it's our turn.

A complete description of the winboard protocol can be found here.   H.G. Muller has also published a model driver for the winboard protocol in the Winboard Forum.    I am listing this model driver verbatim, just for the sake of advertising of how a winboard driver is supposed to be set up. Winglet's console command processing and winboard driver is based on this driver:

model WinBoard protocol driver by H.G. Muller:

    /********************************************************/
    /* Example of a WinBoard-protocol driver, by H.G.Muller */
    /********************************************************/
 
    #include <stdio.h>
 
    // four different constants, with values for WHITE and BLACK that suit your engine
    #define WHITE   1
    #define BLACK   2
    #define NONE    0
    #define ANALYZE  3
 
    // some value that cannot occur as a valid move
    #define INVALID 666
 
    // some parameter of your engine
    #define MAXMOVES 500  /* maximum game length  */
    #define MAXPLY   60   /* maximum search depth */
 
    #define OFF 0
    #define ON  1
 
    #define DEFAULT_FEN "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"
 
    typedef int MOVE;        // in this example moves are encoded as an int
 
    int moveNr;              // part of game state; incremented by MakeMove
    MOVE gameMove[MAXMOVES]; // holds the game history
 
    // Some routines your engine should have to do the various essential things
    int  MakeMove(int stm, MOVE move);      // performs move, and returns new side to move
    void UnMake(MOVE move);                 // unmakes the move;
    int  Setup(char *fen);                  // sets up the position from the given FEN, and returns the new side to move
    void SetMemorySize(int n);              // if n is different from last time, resize all tables to make memory usage below n MB
    char *MoveToText(MOVE move);            // converts the move from your internal format to text like e2e2, e1g1, a7a8q.
    MOVE ParseMove(char *moveText);         // converts a long-algebraic text move to your internal move format
    int  SearchBestMove(int stm, int timeLeft, int mps, int timeControl, int inc, int timePerMove, MOVE *move, MOVE *ponderMove);
    void PonderUntilInput(int stm);         // Search current position for stm, deepening forever until there is input.
 
    // Some global variables that control your engine's behavior
    int ponder;
    int randomize;
    int postThinking;
    int resign;         // engine-defined option
    int contemptFactor; // likewise
 
    int TakeBack(int n)
    { // reset the game and then replay it to the desired point
      int last, stm;
      stm = Setup(NULL);
      last = moveNr - n; if(last < 0) last = 0;
      for(moveNr=0; moveNr<last; moveNr++) stm = MakeMove(stm, gameMove[moveNr]);
    }
 
    void PrintResult(int stm, int score)
    {
        if(score == 0) printf("1/2-1/2\n");
        if(score > 0 && stm == WHITE || score < 0 && stm == BLACK) printf("1-0\n");
      else printf("0-1\n");
    }
 
    main()
    {
      int stm;                                 // side to move
      int engineSide=NONE;                     // side played by engine
      int timeLeft;                            // timeleft on engine's clock
      int mps, timeControl, inc, timePerMove;  // time-control parameters, to be used by Search
      int maxDepth;                            // used by search
      MOVE move, ponderMove;
      int i, score;
      char inBuf[80], command[80];
 
      while(1) { // infinite loop
 
        fflush(stdout);                 // make sure everything is printed before we do something that might take time
 
        if(stm == engineSide) {         // if it is the engine's turn to move, set it thinking, and let it move
    
          score = SearchBestMove(stm, timeLeft, mps, timeControl, inc, timePerMove, &move, &ponderMove);
 
          if(move == INVALID) {         // game apparently ended
            engineSide = NONE;          // so stop playing
            PrintResult(stm, score);
          } else {
            stm = MakeMove(stm, move);  // assumes MakeMove returns new side to move
            gameMove[moveNr++] = move;  // remember game
       printf("move %s\n", MoveToText(move));
          }
        }
 
        fflush(stdout); // make sure everything is printed before we do something that might take time
 
        // now it is not our turn (anymore)
        if(engineSide == ANALYZE) {       // in analysis, we always ponder the position
            PonderUntilInput(stm);
        } else
        if(engineSide != NONE && ponder == ON && moveNr != 0) { // ponder while waiting for input
          if(ponderMove == INVALID) {     // if we have no move to ponder on, ponder the position
            PonderUntilInput(stm);
          } else {
            int newStm = MakeMove(stm, ponderMove);
            PonderUntilInput(newStm);
            UnMake(ponderMove);
          }
        }
 
      noPonder:
        // wait for input, and read it until we have collected a complete line
        for(i = 0; (inBuf[i] = getchar()) != '\n'; i++);
        inBuf[i+1] = 0;
 
        // extract the first word
        sscanf(inBuf, "%s", command);
 
        // recognize the command,and execute it
        if(!strcmp(command, "quit"))    { break; } // breaks out of infinite loop
        if(!strcmp(command, "force"))   { engineSide = NONE;    continue; }
        if(!strcmp(command, "analyze")) { engineSide = ANALYZE; continue; }
        if(!strcmp(command, "exit"))    { engineSide = NONE;    continue; }
        if(!strcmp(command, "otim"))    { goto noPonder; } // do not start pondering after receiving time commands, as move will follow immediately
        if(!strcmp(command, "time"))    { sscanf(inBuf, "time %d", &timeLeft); goto noPonder; }
        if(!strcmp(command, "level"))   {
          int min, sec=0;
          sscanf(inBuf, "level %d %d %d", &mps, &min, &inc) == 3 ||  // if this does not work, it must be min:sec format
          sscanf(inBuf, "level %d %d:%d %d", &mps, &min, &sec, &inc);
          timeControl = 60*min + sec; timePerMove = -1;
          continue;
        }
        if(!strcmp(command, "protover")){
          printf("feature ping=1 setboard=1 colors=0 usermove=1 memory=1 debug=1");
          printf("feature option=\"Resign -check 0\"");           // example of an engine-defined option
          printf("feature option=\"Contempt -spin 0 -200 200\""); // and another one
          printf("feature done=1");
          continue;
        }
        if(!strcmp(command, "option")) { // setting of engine-define option; find out which
          if(sscanf(inBuf+7, "Resign=%d",   &resign)         == 1) continue;
          if(sscanf(inBuf+7, "Contempt=%d", &contemptFactor) == 1) continue;
          continue;
        }
        if(!strcmp(command, "sd"))      { sscanf(inBuf, "sd %d", &maxDepth);    continue; }
        if(!strcmp(command, "st"))      { sscanf(inBuf, "st %d", &timePerMove); continue; }
        if(!strcmp(command, "memory"))  { SetMemorySize(atoi(inBuf+7)); continue; }
        if(!strcmp(command, "ping"))    { printf("pong%s", inBuf+4); continue; }
    //  if(!strcmp(command, ""))        { sscanf(inBuf, " %d", &); continue; }
        if(!strcmp(command, "new"))     { engineSide = BLACK; stm = Setup(DEFAULT_FEN); maxDepth = MAXPLY; randomize = OFF; continue; }
        if(!strcmp(command, "setboard")){ engineSide = NONE;  stm = Setup(inBuf+9); continue; }
        if(!strcmp(command, "easy"))    { ponder = OFF; continue; }
        if(!strcmp(command, "hard"))    { ponder = ON;  continue; }
        if(!strcmp(command, "undo"))    { stm = TakeBack(1); continue; }
        if(!strcmp(command, "remove"))  { stm = TakeBack(2); continue; }
        if(!strcmp(command, "go"))      { engineSide = stm;  continue; }
        if(!strcmp(command, "post"))    { postThinking = ON; continue; }
        if(!strcmp(command, "nopost"))  { postThinking = OFF;continue; }
        if(!strcmp(command, "random"))  { randomize = ON;    continue; }
        if(!strcmp(command, "hint"))    { if(ponderMove != INVALID) printf("Hint: %s\n", MoveToText(ponderMove)); continue; }
        if(!strcmp(command, "book"))    {  continue; }
        // ignored commands:
        if(!strcmp(command, "xboard"))  { continue; }
        if(!strcmp(command, "computer")){ continue; }
        if(!strcmp(command, "name"))    { continue; }
        if(!strcmp(command, "ics"))     { continue; }
        if(!strcmp(command, "accepted")){ continue; }
        if(!strcmp(command, "rejected")){ continue; }
        if(!strcmp(command, "variant")) { continue; }
        if(!strcmp(command, ""))  {  continue; }
        if(!strcmp(command, "usermove")){
          int move = ParseMove(inBuf+9);
          if(move == INVALID) printf("Illegal move\n");
          else {
            stm = MakeMove(stm, move);
       ponderMove = INVALID;
            gameMove[moveNr++] = move;  // remember game
          }
          continue;
        }
        printf("Error: unknown command\n");
      }
    }

 

Peeking for input during search

We already have a way to peek at the clock during searches, see readClockAndInput.  All we need to add is checking for input from winboard.  Because the communication between winboard and winglet is done using a pipe , this check ends up being a little more complex than simply reading input from a keyboard.  PeekNamedPipe is a Windows system call that checks if there is data waiting in the communication pipe between winboard and winglet:

       if ((XB_MODE) && (PeekNamedPipe(GetStdHandle(STD_INPUT_HANDLE), NULL, 0, NULL, &nchar, NULL)))
       {
              for (CMD_BUFF_COUNT = 0; CMD_BUFF_COUNT < (int)nchar; CMD_BUFF_COUNT++)
              {
                     CMD_BUFF[CMD_BUFF_COUNT] = getc(stdin);
                     // sometimes we do not receive a newline character
                     if (((CMD_BUFF_COUNT+1)==(int)nchar) || CMD_BUFF[CMD_BUFF_COUNT] == '\n'
                     {
                           if (CMD_BUFF[CMD_BUFF_COUNT] == '\n') CMD_BUFF[CMD_BUFF_COUNT] = '\0';
                            else CMD_BUFF[CMD_BUFF_COUNT+1] = '\0';
 
                           if (CMD_BUFF=="" || !CMD_BUFF_COUNT) return;
 
                           sscanf(CMD_BUFF, "%s", command);

                         etc..

 

Time control in matches

Winboard will tell the side to move how much time is left on the clock (for both sides). This is very convenient because it means we don't have to keep track of the two clocks this ourselves.  Before the search starts, this information is used to calculate how much time winglet can consume (maximally) for the next move,  see function timeControl() for details. 

First, we make an estimation of how many moves are left in the game, then we simply divide (the total time left) by (the estimated moves left).  If however we have a time advantage over the opponent (i.e. the opponents has less time), then we will also allocate part of that time advantage for the next move.  Finally there are some checks done and limits put on the calculated time.

Setting up Winboard

There are a couple of different ways to add an engine to winboard. Winboard's help file will explain the details.  I'll explain two methods. My personal preference is to define a shortcut on the desktop (or in the startmenu):

Shortcut method:

  • Right-click on the desktop, select New - Shortcut, and then fill in the following
    (this is an example, you might have to change the winboard and/or winglet locations): 
    C:\WinBoard-4.5.2\WinBoard\winboard.exe /debug /cp /fcp "E:\Backup\winglet\wingletx\x64\Release\wingletx.exe" /fd "E:\Backup\winglet\wingletx"
  • Explanation: C:\WinBoard-4.5.2\WinBoard\winboard.exe is the location of winboard
    /debug tells winboard to write debugging information to a file. This is used to diagnose problems in the interaction between the engine and winboard.  It is an optional switch.
    /cp tells winboard to start in chess program mode.
    /fcp is the first program location, it is followed by winglets executable location
    /fd is the first program's working directory, this is where wingletx.ini file and logo.bmp reside.
  • You can optionally add a second engine (and this can be winglet too), by using the /scp and /sd options.
  • In case you are using non-default initialization files for winglet, then just change the /fcp parameter (for example):
    "E:\Backup\winglet\wingletx\x64\Release\wingletx.exe -i test.ini"

Or, edit the winboard.ini file:

This is done by editing the winboard.ini master settings file in the winboard installation folder (e.g. "C:\WinBoard-4.5.2\WinBoard\winboard.ini")
Open this file (with Notepad) and add winglet to /firstChessProgramNames, as follows:

/firstChessProgramNames={wingletx /fd "E:\Backup\winglet\wingletx\x64\Release\"
fmax /fd="..\Fairy-Max" /firstXBook
fruit_21 /fd="../Fruit" /fUCI
"polyglot _PG/fruit.ini"
"Pulsar2009-9b 2" /fd="..\Pulsar"
"ShaMax" /fd="..\Fairy-Max" /variant=shatranj
"MaxQi 22" /fd="..\Fairy-Max" /variant=xiangqi
haqikid /fd="..\HaQi" /firstXBook /variant=xiangqi
"UCCI2WB -noini ..\EleEye\eleeye.exe" /firstLogo="..\EleEye\logo.bmp" /variant=xiangqi
sdk09s /fd=..\Shokidoki /variant=shogi
}

/secondChessProgramNames={wingletx /sd "E:\Backup\winglet\wingletx\x64\Release\"
fruit_21 /sd="../Fruit" /sUCI
"fmax" /sd="..\Fairy-Max" /secondXBook

Optionally you can also add winglet to /secondChessProgramNames. In this installation option, Winglet's initialization file needs to reside in the /sd folder.

A L E R T: Note that, for these changes to take effect, you will have to delete your private winboard.ini user settings files, normally these private settings can be found in the "Application Data" folder (e.g. "C:\Documents and Settings\{user name}\Application Data\winboard.ini".). However, on my system I found this file in: C:\Users\user\AppData\Roaming.


Here you see winglet in action, playing black against Fairy-Max 4.8R:


 


Home Previous Top Next

last update: Friday 09 September 2011