Allow null move in analysis and edit-game mode
[xboard.git] / backend.c
index a1c3c86..a304ca5 100644 (file)
--- a/backend.c
+++ b/backend.c
@@ -63,6 +63,7 @@ int flock(int f, int code);
 
 #else
 
+#include <sys/file.h>
 #define DoSleep( n ) if( (n) >= 0) sleep(n)
 #define SLASH '/'
 
@@ -4860,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);
     }
@@ -4900,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
@@ -5075,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 {
@@ -6080,7 +6088,7 @@ DefaultPromoChoice(int white)
 static int autoQueen; // [HGM] oneclick
 
 int
-HasPromotionChoice(int fromX, int fromY, int toX, int toY, char *promoChoice)
+HasPromotionChoice(int fromX, int fromY, int toX, int toY, char *promoChoice, int sweepSelect)
 {
     /* [HGM] rewritten IsPromotion to only flag promotions that offer a choice */
     /* [HGM] add Shogi promotions */
@@ -6162,9 +6170,9 @@ HasPromotionChoice(int fromX, int fromY, int toX, int toY, char *promoChoice)
     }
     // give caller the default choice even if we will not make it
     *promoChoice = ToLower(PieceToChar(defaultPromoChoice));
-    if(gameInfo.variant == VariantShogi) *promoChoice = '+';
-    if(appData.sweepSelect && gameInfo.variant != VariantGreat
-                          && gameInfo.variant != VariantShogi
+    if(gameInfo.variant == VariantShogi) *promoChoice = (defaultPromoChoice == piece ? '=' : '+');
+    if(        sweepSelect && gameInfo.variant != VariantGreat
+                          && gameInfo.variant != VariantGrand
                           && gameInfo.variant != VariantSuper) return FALSE;
     if(autoQueen) return FALSE; // predetermined
 
@@ -6526,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) {
@@ -6667,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;
@@ -6744,7 +6758,7 @@ void
 MarkTargetSquares(int clear)
 {
   int x, y;
-  if(!appData.markers || !appData.highlightDragging ||
+  if(!appData.markers || !appData.highlightDragging || appData.icsActive && gameInfo.variant < VariantShogi ||
      !appData.testLegality || gameMode == EditPosition) return;
   if(clear) {
     for(x=0; x<BOARD_WIDTH; x++) for(y=0; y<BOARD_HEIGHT; y++) marker[y][x] = 0;
@@ -6805,7 +6819,6 @@ void LeftClick(ClickType clickType, int xPix, int yPix)
     }
 
     if (clickType == Press) ErrorPopDown();
-    MarkTargetSquares(1);
 
     x = EventToSquare(xPix, BOARD_WIDTH);
     y = EventToSquare(yPix, BOARD_HEIGHT);
@@ -6820,7 +6833,7 @@ void LeftClick(ClickType clickType, int xPix, int yPix)
        defaultPromoChoice = promoSweep;
        promoSweep = EmptySquare;   // terminate sweep
        promoDefaultAltered = TRUE;
-       if(!selectFlag) x = fromX, y = fromY; // and fake up-click on same square if we were still selecting
+       if(!selectFlag && (x != toX || y != toY)) x = fromX, y = fromY; // and fake up-click on same square if we were still selecting
     }
 
     if(promotionChoice) { // we are waiting for a click to indicate promotion piece
@@ -6873,7 +6886,7 @@ void LeftClick(ClickType clickType, int xPix, int yPix)
        }
        return;
       }
-      fromX = x; fromY = y;
+      fromX = x; fromY = y; toX = toY = -1;
       if(!appData.oneClick || !OnlyMove(&x, &y, FALSE) ||
         // even if only move, we treat as normal when this would trigger a promotion popup, to allow sweep selection
         appData.sweepSelect && CanPromote(boards[currentMove][fromY][fromX], fromY) && originalY != y) {
@@ -6881,7 +6894,7 @@ void LeftClick(ClickType clickType, int xPix, int yPix)
            if (OKToStartUserMove(fromX, fromY)) {
                second = 0;
                MarkTargetSquares(0);
-               DragPieceBegin(xPix, yPix); dragging = 1;
+               DragPieceBegin(xPix, yPix, FALSE); dragging = 1;
                if(appData.sweepSelect && CanPromote(piece = boards[currentMove][fromY][fromX], fromY)) {
                    promoSweep = defaultPromoChoice;
                    selectFlag = 0; lastX = xPix; lastY = yPix;
@@ -6920,6 +6933,7 @@ void LeftClick(ClickType clickType, int xPix, int yPix)
            /* Clicked again on same color piece -- changed his mind */
            second = (x == fromX && y == fromY);
            promoDefaultAltered = FALSE;
+           MarkTargetSquares(1);
           if(!second || appData.oneClick && !OnlyMove(&x, &y, TRUE)) {
            if (appData.highlightDragging) {
                SetHighlights(x, y, -1, -1);
@@ -6935,7 +6949,7 @@ void LeftClick(ClickType clickType, int xPix, int yPix)
                fromX = x;
                fromY = y; dragging = 1;
                MarkTargetSquares(0);
-               DragPieceBegin(xPix, yPix);
+               DragPieceBegin(xPix, yPix, FALSE);
                if(appData.sweepSelect && CanPromote(piece = boards[currentMove][y][x], y)) {
                    promoSweep = defaultPromoChoice;
                    selectFlag = 0; lastX = xPix; lastY = yPix;
@@ -6986,13 +7000,25 @@ void LeftClick(ClickType clickType, int xPix, int yPix)
     toX = x;
     toY = y;
     saveAnimate = appData.animate;
+    MarkTargetSquares(1);
     if (clickType == Press) {
        if(gameMode == EditPosition && boards[currentMove][fromY][fromX] == EmptySquare) {
            // must be Edit Position mode with empty-square selected
-           fromX = x; fromY = y; DragPieceBegin(xPix, yPix); dragging = 1; // consider this a new attempt to drag
+           fromX = x; fromY = y; DragPieceBegin(xPix, yPix, FALSE); dragging = 1; // consider this a new attempt to drag
            if(x >= BOARD_LEFT && x < BOARD_RGHT) clearFlag = 1; // and defer click-click move of empty-square to up-click
            return;
        }
+       if(appData.sweepSelect && HasPromotionChoice(fromX, fromY, toX, toY, &promoChoice, FALSE)) {
+           ChessSquare piece = boards[currentMove][fromY][fromX];
+           DragPieceBegin(xPix, yPix, TRUE); dragging = 1;
+           promoSweep = defaultPromoChoice;
+           if(PieceToChar(PROMOTED piece) == '+') promoSweep = PROMOTED piece;
+           selectFlag = 0; lastX = xPix; lastY = yPix;
+           Sweep(0); // Pawn that is going to promote: preview promotion piece
+           DisplayMessage("", _("Pull pawn backwards to under-promote"));
+           DrawPosition(FALSE, boards[currentMove]);
+           return;
+       }
        /* Finish clickclick move */
        if (appData.animate || appData.highlightLastMove) {
            SetHighlights(fromX, fromY, toX, toY);
@@ -7043,7 +7069,7 @@ void LeftClick(ClickType clickType, int xPix, int yPix)
 
     if(gatingPiece != EmptySquare) promoChoice = ToLower(PieceToChar(gatingPiece));
 
-    if (HasPromotionChoice(fromX, fromY, toX, toY, &promoChoice)) {
+    if (HasPromotionChoice(fromX, fromY, toX, toY, &promoChoice, appData.sweepSelect)) {
        SetHighlights(fromX, fromY, toX, toY);
        if(gameInfo.variant == VariantSuper || gameInfo.variant == VariantGreat || gameInfo.variant == VariantGrand) {
            // [HGM] super: promotion to captured piece selected from holdings
@@ -7124,7 +7150,7 @@ int RightClick(ClickType action, int x, int y, int *fromX, int *fromY)
        toX = xSqr; toY = ySqr; lastX = x, lastY = y;
        if(flipView) toX = BOARD_WIDTH - 1 - toX; else toY = BOARD_HEIGHT - 1 - toY;
        NextPiece(0);
-       return -2;
+       return 2; // grab
       case IcsObserving:
        if(!appData.icsEngineAnalyze) return -1;
       case IcsPlayingWhite:
@@ -8993,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
@@ -9683,7 +9713,7 @@ WriteTourneyFile(char *results)
        fprintf(f, "-loadPositionIndex %d\n", appData.loadPositionIndex);
        fprintf(f, "-rewindIndex %d\n", appData.rewindIndex);
        if(searchTime > 0)
-               fprintf(f, "-searchTime \"%s\"\n", appData.searchTime);
+               fprintf(f, "-searchTime \"%d:%02d\"\n", searchTime/60, searchTime%60);
        else {
                fprintf(f, "-mps %d\n", appData.movesPerSession);
                fprintf(f, "-tc %s\n", appData.timeControl);
@@ -13546,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) {
@@ -13556,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) {
@@ -13833,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--;
        }
@@ -15199,8 +15243,6 @@ DisplayComment(moveNumber, text)
      char *text;
 {
     char title[MSG_SIZ];
-    char buf[8000]; // comment can be long!
-    int score, depth;
 
     if (moveNumber < 0 || parseList[moveNumber][0] == NULLCHAR) {
       safeStrCpy(title, "Comment", sizeof(title)/sizeof(title[0]));
@@ -15209,14 +15251,6 @@ DisplayComment(moveNumber, text)
              WhiteOnMove(moveNumber) ? " " : ".. ",
              parseList[moveNumber]);
     }
-    // [HGM] PV info: display PV info together with (or as) comment
-    if(moveNumber >= 0 && (depth = pvInfoList[moveNumber].depth) > 0) {
-      if(text == NULL) text = "";
-      score = pvInfoList[moveNumber].score;
-      snprintf(buf,sizeof(buf)/sizeof(buf[0]), "%s%.2f/%d %d\n%s", score>0 ? "+" : "", score/100.,
-             depth, (pvInfoList[moveNumber].time+50)/100, text);
-      text = buf;
-    }
     if (text != NULL && (appData.autoDisplayComment || commentUp))
         CommentPopUp(title, text);
 }