Allow null move in analysis and edit-game mode
authorH.G. Muller <h.g.muller@hccnet.nl>
Wed, 19 Jan 2011 10:46:41 +0000 (11:46 +0100)
committerH.G. Muller <h.g.muller@hccnet.nl>
Sat, 6 Aug 2011 16:40:51 +0000 (18:40 +0200)
In some games it is allowed to pass your turn. The internal
representation picked for such a null move is the drop of an empty
square, (EmptySquare,DROP_RANK,0,0). Computer-algebraic form is "@@@@",
SAN form is "--", (for compatibility with ChessBase / SCID), and the
non-compliant forms "null", "pass"and "Z0" are recognized in
the parser as well. ApplyMove() has to test for this special case
(to not actually clear a1), and a way to enter the pass for the user had
to be found (clicking the opponent clock, like in EditPosition mode).
 The null move is made move irreversible to prevent repetition draws
spanning null moves being adjudicated.
  Such entry of null move is now allowed in AnalyzeMode and EditGame mode.
Because Chess engines will not accept null moves, SendMoveToProgram had
to be adapted to send not the move, but the position after it in
case of a null move. This erases the move history in the engine,
so it will not react to 'undo' for that null move, so BackwardInner
had to be adapted to test for intervening null moves, and if one is
found, approach the target from the other side after loading the
earliest position before it not crossing another null move, and
then loading the moves upto the target.
  XBoard clock clicks had to be fixed, to pay attention to the shift key.

backend.c
moves.c
parser.c
xboard.c

index f0289bc..a304ca5 100644 (file)
--- a/backend.c
+++ b/backend.c
@@ -4861,6 +4861,11 @@ SendMoveToProgram(moveNum, cps)
 {
     char buf[MSG_SIZ];
 
+    if(moveList[moveNum][1] == '@' && moveList[moveNum][0] == '@') {
+       // null move in variant where engine does not understand it (for analysis purposes)
+       SendBoard(cps, moveNum + 1); // send position after move in stead.
+       return;
+    }
     if (cps->useUsermove) {
       SendToProgram("usermove ", cps);
     }
@@ -4901,6 +4906,7 @@ SendMoveToProgram(moveNum, cps)
       } else
       if(BOARD_HEIGHT > 10) { // [HGM] big: convert ranks to double-digit where needed
        if(moveList[moveNum][1] == '@' && (BOARD_HEIGHT < 16 || moveList[moveNum][0] <= 'Z')) { // drop move
+         if(moveList[moveNum][0]== '@') snprintf(buf, MSG_SIZ, "@@@@\n"); else
          snprintf(buf, MSG_SIZ, "%c@%c%d%s", moveList[moveNum][0],
                                              moveList[moveNum][2], moveList[moveNum][3] - '0', moveList[moveNum]+4);
        } else
@@ -5076,6 +5082,7 @@ CoordsToComputerAlgebraic(rf, ff, rt, ft, promoChar, move)
      char move[7];
 {
     if (rf == DROP_RANK) {
+      if(ff == EmptySquare) sprintf(move, "@@@@\n"); else // [HGM] pass
       sprintf(move, "%c@%c%c\n",
                 ToUpper(PieceToChar((ChessSquare) ff)), AAA + ft, ONE + rt);
     } else {
@@ -6527,6 +6534,9 @@ UserMoveEvent(fromX, fromY, toX, toY, promoChar)
     /* [HGM] always test for legality, to get promotion info */
     moveType = LegalityTest(boards[currentMove], PosFlags(currentMove),
                                          fromY, fromX, toY, toX, promoChar);
+
+    if(fromY == DROP_RANK && fromX == EmptySquare && (gameMode == AnalyzeMode || gameMode == EditGame)) moveType = NormalMove;
+
     /* [HGM] but possibly ignore an IllegalMove result */
     if (appData.testLegality) {
        if (moveType == IllegalMove || moveType == ImpossibleMove) {
@@ -6668,6 +6678,9 @@ FinishMove(moveType, fromX, fromY, toX, toY, promoChar)
         // [HGM] book: if program might be playing, let it use book
        bookHit = SendMoveToBookUser(forwardMostMove-1, &first, FALSE);
        first.maybeThinking = TRUE;
+    } else if(fromY == DROP_RANK && fromX == EmptySquare) {
+       if(!first.useSetboard) SendToProgram("undo\n", &first); // kludge to change stm in engines that do not support setboard
+       SendBoard(&first, currentMove+1);
     } else SendMoveToProgram(forwardMostMove-1, &first);
     if (currentMove == cmailOldMove + 1) {
       cmailMoveType[lastLoadGameNumber - 1] = CMAIL_MOVE;
@@ -9006,15 +9019,19 @@ ApplyMove(fromX, fromY, toX, toY, promoChar, board)
       oldEP = (signed char)board[EP_STATUS];
       board[EP_STATUS] = EP_NONE;
 
-      if( board[toY][toX] != EmptySquare )
-           board[EP_STATUS] = EP_CAPTURE;
-
   if (fromY == DROP_RANK) {
        /* must be first */
+        if(fromX == EmptySquare) { // [HGM] pass: empty drop encodes null move; nothing to change.
+           board[EP_STATUS] = EP_CAPTURE; // null move considered irreversible
+           return;
+       }
         piece = board[toY][toX] = (ChessSquare) fromX;
   } else {
       int i;
 
+      if( board[toY][toX] != EmptySquare )
+           board[EP_STATUS] = EP_CAPTURE;
+
       if( board[fromY][fromX] == WhiteLance || board[fromY][fromX] == BlackLance ) {
            if( gameInfo.variant != VariantSuper && gameInfo.variant != VariantShogi )
                board[EP_STATUS] = EP_PAWN_MOVE; // Lance is Pawn-like in most variants
@@ -13559,7 +13576,9 @@ ClockClick(int which)
          if (gameMode == EditPosition || gameMode == IcsExamining) {
            if(!appData.pieceMenu && blackPlaysFirst) EditPositionMenuEvent(ClearBoard, 0, 0);
            SetBlackToPlayEvent();
-         } else if (gameMode == EditGame || shiftKey) {
+         } else if ((gameMode == AnalyzeMode || gameMode == EditGame) && !blackFlag && WhiteOnMove(currentMove)) {
+          UserMoveEvent((int)EmptySquare, DROP_RANK, 0, 0, 0); // [HGM] multi-move: if not out of time, enters null move
+         } else if (shiftKey) {
            AdjustClock(which, -1);
          } else if (gameMode == IcsPlayingWhite ||
                     gameMode == MachinePlaysBlack) {
@@ -13569,7 +13588,9 @@ ClockClick(int which)
          if (gameMode == EditPosition || gameMode == IcsExamining) {
            if(!appData.pieceMenu && !blackPlaysFirst) EditPositionMenuEvent(ClearBoard, 0, 0);
            SetWhiteToPlayEvent();
-         } else if (gameMode == EditGame || shiftKey) {
+         } else if ((gameMode == AnalyzeMode || gameMode == EditGame) && !whiteFlag && !WhiteOnMove(currentMove)) {
+          UserMoveEvent((int)EmptySquare, DROP_RANK, 0, 0, 0); // [HGM] multi-move
+         } else if (shiftKey) {
            AdjustClock(which, -1);
          } else if (gameMode == IcsPlayingBlack ||
                   gameMode == MachinePlaysWhite) {
@@ -13846,6 +13867,16 @@ BackwardInner(target)
     if (gameMode == EditGame || gameMode==AnalyzeMode ||
        gameMode == PlayFromGameFile || gameMode == AnalyzeFile) {
        while (currentMove > target) {
+           if(moveList[currentMove-1][1] == '@' && moveList[currentMove-1][0] == '@') {
+               // null move cannot be undone. Reload program with move history before it.
+               int i;
+               for(i=target; i>backwardMostMove; i--) { // seek back to start or previous null move
+                   if(moveList[i-1][1] == '@' && moveList[i-1][0] == '@') break;
+               }
+               SendBoard(&first, i); 
+               for(currentMove=i; currentMove<target; currentMove++) SendMoveToProgram(currentMove, &first);
+               break;
+           }
            SendToProgram("undo\n", &first);
            currentMove--;
        }
diff --git a/moves.c b/moves.c
index 89728e1..2a8e187 100644 (file)
--- a/moves.c
+++ b/moves.c
@@ -1500,6 +1500,7 @@ ChessMove CoordsToAlgebraic(board, flags, rf, ff, rt, ft, promoChar, out)
     CoordsToAlgebraicClosure cl;
 
     if (rf == DROP_RANK) {
+       if(ff == EmptySquare) { strncpy(outp, "--",3); return NormalMove; } // [HGM] pass
        /* Bughouse piece drop */
        *outp++ = ToUpper(PieceToChar((ChessSquare) ff));
        *outp++ = '@';
index 0a6549e..b2dfe88 100644 (file)
--- a/parser.c
+++ b/parser.c
@@ -489,6 +489,10 @@ badMove:// we failed to find algebraic move
            *p = oldp; // we might need to re-match the skipped stuff
        }
 
+       if(Match("@@@@", p) || Match("--", p) || Match("Z0", p) || Match("pass", p) || Match("null", p)) {
+           strncpy(currentMoveString, "@@@@", 5);
+           return yyboardindex & F_WHITE_ON_MOVE ? WhiteDrop : BlackDrop;
+       }
 
        // ********* Efficient skipping of (mostly) alphabetic chatter **********
        while(isdigit(**p) || isalpha(**p) || **p == '-') (*p)++;
index a676d24..89fcb70 100644 (file)
--- a/xboard.c
+++ b/xboard.c
@@ -1130,8 +1130,12 @@ char boardTranslations[] =
    Any<Btn3Down>: XawPositionSimpleMenu(menuB) XawPositionSimpleMenu(menuD) \
                  PieceMenuPopup(menuB) \n";
 
-char whiteTranslations[] = "<BtnDown>: WhiteClock()\n";
-char blackTranslations[] = "<BtnDown>: BlackClock()\n";
+char whiteTranslations[] =
+   "Shift<BtnDown>: WhiteClock(1)\n \
+   <BtnDown>: WhiteClock(0)\n";
+char blackTranslations[] =
+   "Shift<BtnDown>: BlackClock(1)\n \
+   <BtnDown>: BlackClock(0)\n";
 
 char ICSInputTranslations[] =
     "<Key>Up: UpKeyProc() \n "
@@ -4141,6 +4145,7 @@ void WhiteClock(w, event, prms, nprms)
      String *prms;
      Cardinal *nprms;
 {
+    shiftKey = prms[0][0] & 1;
     ClockClick(0);
 }
 
@@ -4150,6 +4155,7 @@ void BlackClock(w, event, prms, nprms)
      String *prms;
      Cardinal *nprms;
 {
+    shiftKey = prms[0][0] & 1;
     ClockClick(1);
 }