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.
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.
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");
}
}
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..
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.
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:

|