implement interactive game review

Tue, 19 May 2026 18:03:06 +0200

author
Mike Becker <universe@uap-core.de>
date
Tue, 19 May 2026 18:03:06 +0200
changeset 122
e65d9b5e9324
parent 121
53f714ac783d
child 123
0f5a1cda8f60

implement interactive game review

resolves #813 but creates issue #844
(at least it makes #844 noticeable)

src/chess/game-info.h file | annotate | diff | comparison | revisions
src/chess/rules.c file | annotate | diff | comparison | revisions
src/chess/rules.h file | annotate | diff | comparison | revisions
src/client.c file | annotate | diff | comparison | revisions
src/game.c file | annotate | diff | comparison | revisions
src/server.c file | annotate | diff | comparison | revisions
--- a/src/chess/game-info.h	Tue May 19 17:59:24 2026 +0200
+++ b/src/chess/game-info.h	Tue May 19 18:03:06 2026 +0200
@@ -101,11 +101,15 @@
     bool stalemate;
     bool remis;
     bool resign;
+    /** this flag is only supposed to be set when the opponent disconnects */
+    bool ragequit;
+    bool review;
 } GameState;
 
 
 #define is_game_running(gamestate) !((gamestate)->checkmate || \
-    (gamestate)->resign || (gamestate)->stalemate || (gamestate)->remis)
+    (gamestate)->resign || (gamestate)->stalemate || (gamestate)->remis || \
+    (gamestate)->review)
 
 #define last_move(gamestate) \
     ((gamestate)->moves[(gamestate)->movecount-1])
--- a/src/chess/rules.c	Tue May 19 17:59:24 2026 +0200
+++ b/src/chess/rules.c	Tue May 19 18:03:06 2026 +0200
@@ -144,6 +144,32 @@
     }
 }
 
+static void calc_movetime(GameState *gamestate, Move *move) {
+    struct timeval curtimestamp;
+    gettimeofday(&curtimestamp, NULL);
+    move->timestamp.tv_sec = curtimestamp.tv_sec;
+    move->timestamp.tv_usec = (int32_t) curtimestamp.tv_usec;
+
+    if (gamestate->movecount > 1) {
+        struct movetimeval lasttstamp = last_move(gamestate).timestamp;
+        uint64_t sec = curtimestamp.tv_sec - lasttstamp.tv_sec;
+        suseconds_t micros;
+        if (curtimestamp.tv_usec < lasttstamp.tv_usec) {
+            micros = 1000000-(lasttstamp.tv_usec - curtimestamp.tv_usec);
+            sec--;
+        } else {
+            micros = curtimestamp.tv_usec - lasttstamp.tv_usec;
+        }
+
+        move->movetime.tv_sec = sec;
+        move->movetime.tv_usec = (int32_t) micros;
+    } else {
+        /* no move time for the first move of both white and black */
+        move->movetime.tv_usec = 0;
+        move->movetime.tv_sec = 0;
+    }
+}
+
 static void addmove(GameState* gamestate, Move *data) {
     if (gamestate->movecount == gamestate->movecapacity) {
         gamestate->movecapacity += 64; /* 32 more full moves */
@@ -154,29 +180,12 @@
     Move *move = &gamestate->moves[gamestate->movecount];
     *move = *data;
 
-    struct timeval curtimestamp;
-    gettimeofday(&curtimestamp, NULL);
-    move->timestamp.tv_sec = curtimestamp.tv_sec;
-    move->timestamp.tv_usec = (int32_t) curtimestamp.tv_usec;
-    
-    if (gamestate->movecount > 1) {
-        struct movetimeval lasttstamp = last_move(gamestate).timestamp;
-        uint64_t sec = curtimestamp.tv_sec - lasttstamp.tv_sec;
-        suseconds_t micros;
-        if (curtimestamp.tv_usec < lasttstamp.tv_usec) {
-            micros = 1000000-(lasttstamp.tv_usec - curtimestamp.tv_usec);
-            sec--;
-        } else {
-            micros = curtimestamp.tv_usec - lasttstamp.tv_usec;
-        }
-        
-        move->movetime.tv_sec = sec;
-        move->movetime.tv_usec = (int32_t) micros;
-    } else {
-        /* no move time for the first move of both white and black */
-        move->movetime.tv_usec = 0;
-        move->movetime.tv_sec = 0;
+    /* only if move has no time info, compute it */
+    if (move->movetime.tv_sec == 0 && move->movetime.tv_usec == 0) {
+        calc_movetime(gamestate, move);
     }
+
+    /* important: only "add" the move after calculating the time! */
     gamestate->movecount++;
 }
 
@@ -280,7 +289,19 @@
 }
 
 void apply_move(GameState *gamestate, Move *move) {
-    apply_move_impl(gamestate, move, 0);
+    apply_move_impl(gamestate, move, false);
+}
+
+void gamestate_at_move(GameState *gamestate,
+        unsigned move_number, GameState *replay) {
+    gamestate_init(replay);
+    replay->review = true;
+    if (move_number > gamestate->movecount) {
+        move_number = gamestate->movecount;
+    }
+    for (unsigned i = 0 ; i < move_number ; i++) {
+        apply_move_impl(replay, &(gamestate->moves[i]), true);
+    }
 }
 
 static int validate_move_rules(GameState *gamestate, Move *move) {
@@ -357,7 +378,7 @@
     /* simulate move for check validation */
     GameState simulation = gamestate_copy_sim(gamestate);
     Move simmove = *move;
-    apply_move_impl(&simulation, &simmove, 1);
+    apply_move_impl(&simulation, &simmove, true);
     
     /* find kings for check validation */
     uint8_t piececolor = (move->piece & COLOR_MASK);
--- a/src/chess/rules.h	Tue May 19 17:59:24 2026 +0200
+++ b/src/chess/rules.h	Tue May 19 18:03:06 2026 +0200
@@ -249,6 +249,18 @@
 void apply_move(GameState *gamestate, Move *move);
 
 /**
+ * Copies the state of the game at the specified move number.
+ *
+ * This function is helpful to generate a game state for reviewing past moves.
+ *
+ * @param gamestate the current game state
+ * @param move_number the half-move that would now be played
+ * @param replay the struct to populate with the state at the specified move
+ */
+void gamestate_at_move(GameState *gamestate,
+        unsigned move_number, GameState *replay);
+
+/**
  * Returns the remaining time on the clock for the specified
  * half-move number.
  *
--- a/src/client.c	Tue May 19 17:59:24 2026 +0200
+++ b/src/client.c	Tue May 19 18:03:06 2026 +0200
@@ -122,6 +122,7 @@
     if (played) {
         game_review(settings, &gamestate);
     }
+    gamestate_cleanup(&gamestate);
 
     net_destroy(&server);
     return 0;
--- a/src/game.c	Tue May 19 17:59:24 2026 +0200
+++ b/src/game.c	Tue May 19 18:03:06 2026 +0200
@@ -185,7 +185,6 @@
 
     printw("\rFilename: ");
     clrtoeol();
-    refresh();
     
     char filename[64];
     int y = getcury(stdscr);
@@ -226,16 +225,10 @@
         
         if (asyncgetnstr(movestr, &bufpos, MOVESTR_BUFLEN)) {
             if (strncmp(movestr, "resign", MOVESTR_BUFLEN) == 0) {
-                gamestate->resign = 1;
-                printw("%s resigned!", curcolorstr);
-                clrtobot();
-                refresh();
+                gamestate->resign = true;
                 return 1;
             } else if (strncmp(movestr, "remis", MOVESTR_BUFLEN) == 0) {
-                gamestate->remis = 1;
-                printw("Game ends remis.");
-                clrtobot();
-                refresh();
+                gamestate->remis = true;
                 return 1;
             } else if (strncmp(movestr, "savepgn", MOVESTR_BUFLEN) == 0) {
                 save_pgn(gamestate, gameinfo);
@@ -247,12 +240,8 @@
                     if (result == VALID_MOVE_SEMANTICS) {
                         apply_move(gamestate, &move);
                         if (gamestate->checkmate) {
-                            printw("Checkmate!");
-                            clrtoeol();
                             return 1;
                         } else if (gamestate->stalemate) {
-                            printw("Stalemate!");
-                            clrtoeol();
                             return 1;
                         } else {
                             return 0;
@@ -323,13 +312,10 @@
                 resign_suggested = true;
                 break;
             case NETCODE_RESIGN:
-                gamestate->resign = 1;
-                printw("\rYour opponent resigned!");
-                clrtoeol();
+                gamestate->resign = true;
                 return 1;
             case NETCODE_CONNLOST:
-                printw("\rYour opponent has left the game.");
-                clrtoeol();
+                gamestate->ragequit = true;
                 return 1;
             case NETCODE_ERROR:
                 printw("\rCannot perform asynchronous network IO");
@@ -347,10 +333,7 @@
             bool was_premove = use_premove;
             use_premove = false;
             if (strncmp(movestr, "resign", MOVESTR_BUFLEN) == 0) {
-                gamestate->resign = 1;
-                printw("You resigned!");
-                clrtoeol();
-                refresh();
+                gamestate->resign = true;
                 net_send_code(opponent, NETCODE_RESIGN);
                 return 1;
             } else if (strncmp(movestr, "savepgn", MOVESTR_BUFLEN) == 0) {
@@ -358,10 +341,7 @@
             } else if (strncmp(movestr, "remis", MOVESTR_BUFLEN) == 0) {
                 if (remis_suggested) {
                     net_send_code(opponent, NETCODE_REMIS);
-                    gamestate->remis = 1;
-                    printw("\rRemis accepted!");
-                    clrtoeol();
-                    refresh();
+                    gamestate->remis = true;
                     return 1;
                 } if (!remis_rejected) {
                     net_send_code(opponent, NETCODE_REMIS);
@@ -369,15 +349,10 @@
                     refresh();
                     code = net_recieve_code(opponent);
                     if (code == NETCODE_ACCEPT) {
-                        gamestate->remis = 1;
-                        printw("\rRemis accepted!");
-                        clrtoeol();
-                        refresh();
+                        gamestate->remis = true;
                         return 1;
                     } else if (code == NETCODE_CONNLOST) {
-                        printw("\rYour opponent left the game.");
-                        clrtoeol();
-                        refresh();
+                        gamestate->ragequit = true;
                         return 1;
                     } else {
                         remis_rejected = true;
@@ -404,13 +379,7 @@
                             || code == NETCODE_CHECKMATE
                             || code == NETCODE_STALEMATE) {
                         apply_move(gamestate, &move);
-                        if (gamestate->checkmate) {
-                            printw("Checkmate!");
-                            clrtoeol();
-                            return 1;
-                        } else if (gamestate->stalemate) {
-                            printw("Stalemate!");
-                            clrtoeol();
+                        if (gamestate->checkmate || gamestate->stalemate) {
                             return 1;
                         } else {
                             return 0;
@@ -468,10 +437,7 @@
         /* allow the player to prepare a move */
         if (asyncgetnstr(movestr, &bufpos, MOVESTR_BUFLEN)) {
             if (strncmp(movestr, "resign", MOVESTR_BUFLEN) == 0) {
-                gamestate->resign = 1;
-                printw("You resigned!");
-                clrtoeol();
-                refresh();
+                gamestate->resign = true;
                 net_send_code(opponent, NETCODE_RESIGN);
                 return 1;
             } else if (strncmp(movestr, "taunt", MOVESTR_BUFLEN) == 0) {
@@ -505,26 +471,19 @@
             timecontrol(gamestate, gameinfo);
             return 1;
         case NETCODE_RESIGN:
-            gamestate->resign = 1;
-            printw("\rYour opponent resigned!");
-            clrtoeol();
+            gamestate->resign = true;
             return 1;
         case NETCODE_CONNLOST:
-            printw("\rYour opponent has left the game.");
-            clrtoeol();
+            gamestate->ragequit = true;
             return 1;
         case NETCODE_REMIS:
             if (remis_suggested) {
-                gamestate->remis = 1;
-                printw("\rRemis accepted!");
-                clrtoeol();
+                gamestate->remis = true;
                 return 1;
             } else {
                 if (prompt_yesno(
                     "\rYour opponent offers remis - do you accept")) {
-                    gamestate->remis = 1;
-                    printw("\rRemis accepted!");
-                    clrtoeol();
+                    gamestate->remis = true;
                     net_send_code(opponent, NETCODE_ACCEPT);
                     return 1;
                 } else {
@@ -540,13 +499,9 @@
                 apply_move(gamestate, &move);
                 if (gamestate->checkmate) {
                     net_send_code(opponent, NETCODE_CHECKMATE);
-                    printw("\rCheckmate!");
-                    clrtoeol();
                     return 1;
                 } else if (gamestate->stalemate) {
                     net_send_code(opponent, NETCODE_STALEMATE);
-                    printw("\rStalemate!");
-                    clrtoeol();
                     return 1;
                 } else if (move.check) {
                     net_send_code(opponent, NETCODE_CHECK);
@@ -575,31 +530,82 @@
 }
 
 void game_review(Settings* settings, GameState *gamestate) {
+    const unsigned page_moves = 10;
     GameInfo *gameinfo = &(settings->gameinfo);
+    GameState viewedstate = {0};
+    unsigned viewedmove = gamestate->movecount;
+    bool redraw = true;
 
-    move(0,0);
-    draw_board(gamestate, WHITE, settings->unicode);
-    
-    mvaddstr(getmaxy(stdscr)-1, 0,
-        "Press 'q' to quit or 's' to save a PGN file...");
-    refresh();
-    flushinp();
-    
     noecho();
     int c;
     do {
+        if (redraw) {
+            gamestate_cleanup(&viewedstate);
+            gamestate_at_move(gamestate, viewedmove, &viewedstate);
+
+            erase(); /* don't use clear() to avoid flickering */
+            draw_board(&viewedstate, WHITE, settings->unicode);
+            timecontrol(&viewedstate, gameinfo);
+
+            move(getmaxy(stdscr)-5, 0);
+            const char *curcolorstr =
+                gamestate->movecount % 2 == 0 ? "White" : "Black";
+            if (gamestate->resign) {
+                printw("%s resigned.\n", curcolorstr);
+            } else if (gamestate->remis) {
+                addstr("The game ended remis.\n");
+            } else if (gamestate->stalemate) {
+                addstr("The game ended in a stalemate.\n");
+            } else if (gamestate->checkmate) {
+                printw("%s has lost the game.\n", curcolorstr);
+            } else if (gamestate->ragequit) {
+                printw("Your opponent disconnected.\n");
+            }
+            addstr("\nPress 'q' to quit, 's' to save the position as PGN, or\n"
+                "arrow keys, home/end, page up/down to review the game.\n");
+            flushinp();
+            redraw = false;
+        }
         c = getch();
         if (c == 's') {
             addch('\r');
             echo();
-            save_pgn(gamestate, gameinfo);
-            addstr(" Press 'q' to quit...");
+            save_pgn(&viewedstate, gameinfo);
             noecho();
+            redraw = true;
+        } else if (c == KEY_UP || c == KEY_LEFT) {
+            if (viewedmove > 0) {
+                viewedmove--;
+                redraw = true;
+            }
+        } else if (c == KEY_DOWN || c == KEY_RIGHT) {
+            if (viewedmove < gamestate->movecount) {
+                viewedmove++;
+                redraw = true;
+            }
+        } else if (c == KEY_HOME) {
+            viewedmove = 0;
+            redraw = true;
+        } else if (c == KEY_END) {
+            viewedmove = gamestate->movecount;
+            redraw = true;
+        } else if (c == KEY_PPAGE) {
+            if (viewedmove > page_moves) {
+                viewedmove -= page_moves;
+            } else {
+                viewedmove = 0;
+            }
+            redraw = true;
+        } else if (c == KEY_NPAGE) {
+            viewedmove += page_moves;
+            if (viewedmove > gamestate->movecount) {
+                viewedmove = gamestate->movecount;
+            }
+            redraw = true;
         }
     } while (c != 'q');
     echo();
-    
-    gamestate_cleanup(gamestate);
+    gamestate_cleanup(&viewedstate);
 }
 
 void game_play_singlemachine(Settings *settings) {
@@ -642,6 +648,7 @@
     }  while (running);
     
     game_review(settings, &gamestate);
+    gamestate_cleanup(&gamestate);
 }
 
 void game_play(Settings *settings, GameState *gamestate, int opponent) {
--- a/src/server.c	Tue May 19 17:59:24 2026 +0200
+++ b/src/server.c	Tue May 19 18:03:06 2026 +0200
@@ -147,28 +147,28 @@
     addstr("\rClient connected - awaiting challenge acceptance...");
     refresh();
     int code = net_recieve_code(fd);
+    int exitcode = 0;
     if (code == NETCODE_ACCEPT) {
         addstr("\rClient connected - challenge accepted.");
         clrtoeol();
         game_play(settings, &gamestate, fd);
         net_destroy(&server);
         game_review(settings, &gamestate);
-        return 0;
     } else if (code == NETCODE_DECLINE) {
         addstr("\rClient connected - challenge declined.");
         clrtoeol();
         net_destroy(&server);
-        return 0;
     } else if (code == NETCODE_CONNLOST) {
         addstr("\rClient connected - but gave no response.");
         clrtoeol();
         net_destroy(&server);
-        return 0;
     } else {
         addstr("\rInvalid client response");
         clrtoeol();
         
         net_destroy(&server);
-        return 1;
+        exitcode = 1;
     }
+    gamestate_cleanup(&gamestate);
+    return exitcode;
 }

mercurial