Tue, 19 May 2026 18:03:06 +0200
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; }