diff -r ce38ee9bc3af -r 189c7c77aaab src/game.c --- a/src/game.c Tue May 26 15:29:00 2026 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,759 +0,0 @@ -/* - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. - * - * Copyright 2016 Mike Becker. All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE - * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - * - */ - -#include "game.h" -#include "network.h" -#include "input.h" -#include "colors.h" -#include "chess/rules.h" -#include "chess/pgn.h" -#include -#include -#include -#include - -static const uint8_t boardx = 4, boardy = 10; -static int inputy = 21; /* should be overridden on game startup */ - -static int timecontrol(GameState *gamestate, GameInfo *gameinfo) { - if (gameinfo->timecontrol) { - uint16_t white = remaining_movetime(gameinfo, gamestate, WHITE); - uint16_t black = remaining_movetime(gameinfo, gamestate, BLACK); - char clkstr[16]; - bool always_hours = gameinfo->time >= 3600; - print_clk(white, clkstr, always_hours); - mvprintw(boardy+4, boardx-1, "White time: %s", clkstr); - print_clk(black, clkstr, always_hours); - mvprintw(boardy+5, boardx-1, "Black time: %s", clkstr); - - if (white == 0) { - move(inputy, 0); - printw("Time is over - Black wins!"); - clrtobot(); - refresh(); - return 1; - } - if (black == 0) { - move(inputy, 0); - printw("Time is over - White wins!"); - clrtobot(); - refresh(); - return 1; - } - } - - return 0; -} - -static void draw_board(GameState *gamestate, - uint8_t perspective, - bool unicode) { - char fen[90]; - compute_fen(fen, gamestate); - mvaddstr(0, 0, fen); - - for (uint8_t y = 0 ; y < 8 ; y++) { - for (uint8_t x = 0 ; x < 8 ; x++) { - uint8_t col = gamestate->board[y][x] & COLOR_MASK; - uint8_t piece = gamestate->board[y][x]; - char piecestr[5]; - if (piece) { - if (unicode) { - char* uc = getpieceunicode(piece); - strncpy(piecestr, uc, 5); - } else { - piecestr[0] = (piece & PIECE_MASK) == PAWN - ? 'P' : getpiecechr(piece); - piecestr[1] = '\0'; - } - } else { - piecestr[0] = ' '; - piecestr[1] = '\0'; - } - - bool boardblack = (y&1)==(x&1); - attrset((col==WHITE ? A_BOLD : A_DIM)| - COLOR_PAIR(col == WHITE ? - (boardblack ? COL_WB : COL_WW) : - (boardblack ? COL_BB : COL_BW) - ) - ); - - int cy = perspective == WHITE ? boardy-y : boardy-7+y; - int cx = perspective == WHITE ? boardx+x*3 : boardx+21-x*3; - mvprintw(cy, cx, " %s ", piecestr); - } - } - - attrset(A_NORMAL); - for (uint8_t i = 0 ; i < 8 ; i++) { - int x = perspective == WHITE ? boardx+i*3+1 : boardx+22-i*3; - int y = perspective == WHITE ? boardy-i : boardy-7+i; - mvaddch(boardy+1, x, 'a'+i); - mvaddch(y, boardx-2, '1'+i); - } - - /* move log */ - uint8_t logy = 2; - const uint8_t logx = boardx + 28; - move(logy, logx); - - /* count full moves */ - unsigned int logi = 0; - - /* wrap log after 45 moves */ - while (gamestate->movecount/6-logi/3 >= 15) { - logi++; - } - - for (unsigned mi = logi*2 ; mi < gamestate->movecount ; mi++) { - bool iswhite = mi % 2 == 0; - if (iswhite) { - logi++; - printw("%d. ", logi); - } - - addstr(gamestate->moves[mi].string); - if (!iswhite && logi%3 == 0) { - move(++logy, logx); - } else { - addch(' '); - } - } -} - -static void eval_move_failed_msg(int code) { - switch (code) { - case AMBIGUOUS_MOVE: - printw("Ambiguous move - please specify the piece to move."); - break; - case INVALID_POSITION: - printw("No piece can be moved this way."); - break; - case NEED_PROMOTION: - printw("You need to promote the pawn (append \"=Q\" e.g.)!"); - break; - case KING_IN_CHECK: - printw("Your king is in check!"); - break; - case PIECE_PINNED: - printw("This piece is pinned!"); - break; - case INVALID_MOVE_SYNTAX: - printw("Can't interpret move - please use algebraic notation."); - break; - case RULES_VIOLATED: - printw("Move does not comply chess rules."); - break; - case KING_MOVES_INTO_CHECK: - printw("Can't move the king into a check position."); - break; - default: - printw("Unknown move parser error."); - } -} - -static void save_pgn(GameState *gamestate, GameInfo *gameinfo) { - int y = getcury(stdscr); - - /* ask for player names */ - { - char pname[PLAYER_NAME_BUFLEN]; - printw("\rWhite's name (%s): ", pgn_player_name(gamestate, WHITE)); - clrtoeol(); - if (getnstr(pname, PLAYER_NAME_BUFLEN) == OK && pname[0] != '\0') { - strncpy(gamestate->wname, pname, PLAYER_NAME_BUFLEN); - } - move(y, 0); - printw("\rBlack's name (%s): ", pgn_player_name(gamestate, BLACK)); - clrtoeol(); - if (getnstr(pname, PLAYER_NAME_BUFLEN) == OK && pname[0] != '\0') { - strncpy(gamestate->bname, pname, PLAYER_NAME_BUFLEN); - } - move(y, 0); - } - - bool export_comments = prompt_yesno("Export with comments"); - - printw("\rFilename: "); - clrtoeol(); - - char filename[64]; - if (getnstr(filename, 64) == OK && filename[0] != '\0') { - move(y, 0); - FILE *file = fopen(filename, "w"); - if (file) { - write_pgn(file, gamestate, gameinfo, export_comments); - fclose(file); - printw("File saved."); - } else { - printw("Can't write to file (%s).", strerror(errno)); - } - clrtoeol(); - } -} - -#define MOVESTR_BUFLEN 10 -static int domove_singlemachine(GameState *gamestate, - GameInfo *gameinfo, uint8_t curcolor) { - - - size_t bufpos = 0; - char movestr[MOVESTR_BUFLEN]; - - flushinp(); - while (1) { - const char *curcolorstr = curcolor == WHITE ? "White" : "Black"; - if (timecontrol(gamestate, gameinfo)) { - return 1; - } - move(inputy, 0); - printw( - "Use chess notation to enter your move.\n" - "Or use a command: remis, resign, savepgn\n\n" - "%s to move: ", curcolorstr); - clrtoeol(); - - if (asyncgetnstr(movestr, &bufpos, MOVESTR_BUFLEN)) { - if (strncmp(movestr, "resign", MOVESTR_BUFLEN) == 0) { - if (curcolor == WHITE) { - gamestate->wresign = true; - } else { - gamestate->bresign = true; - } - return 1; - } else if (strncmp(movestr, "remis", MOVESTR_BUFLEN) == 0) { - gamestate->remis = true; - return 1; - } else if (strncmp(movestr, "savepgn", MOVESTR_BUFLEN) == 0) { - save_pgn(gamestate, gameinfo); - } else if (movestr[0] == 0) { - /* ignore empty move strings and ask again */ - } else { - Move move; - int result = eval_move(gamestate, movestr, &move, curcolor); - if (result == VALID_MOVE_SYNTAX) { - result = validate_move(gamestate, &move); - if (result == VALID_MOVE_SEMANTICS) { - apply_move(gamestate, &move); - if (gamestate->checkmate) { - return 1; - } else if (gamestate->stalemate) { - return 1; - } else { - return 0; - } - } else { - eval_move_failed_msg(result); - } - } else { - eval_move_failed_msg(result); - } - clrtoeol(); - } - } - } -} - -static int sendmove(GameState *gamestate, GameInfo *gameinfo, - int opponent, uint8_t mycolor) { - - size_t bufpos = 0; - char movestr[MOVESTR_BUFLEN]; - bool remis_rejected = false; - bool remis_suggested = false; - bool resign_suggested = false; - bool use_premove = false; - uint8_t code; - - if (*gamestate->premove) { - use_premove = true; - const unsigned mlen = sizeof(gamestate->premove); - strncpy(movestr, gamestate->premove, mlen); - movestr[mlen] = '\0'; - memset(gamestate->premove, 0, mlen); - } - - flushinp(); - while (1) { - if (timecontrol(gamestate, gameinfo)) { - net_send_code(opponent, NETCODE_TIMEOVER); - return 1; - } - - move(inputy, 0); - printw("Use chess notation to enter your move.\n"); - if (resign_suggested) { - if (remis_suggested) { - printw("The opponent asks you to resign or accept remis. \n\n"); - } else { - printw("The opponent asks you to resign. \n\n"); - } - } else if (remis_suggested) { - printw("The opponent offers remis. Type remis to accept. \n\n"); - } else if (remis_rejected) { - printw("Remis offer rejected. \n\n"); - } else { - printw("Or use a command: remis, resign, savepgn \n\n"); - } - printw("Type your move: "); - clrtoeol(); - - /* check if the opponent sent us something */ - code = net_recieve_code_async(opponent); - switch (code) { - case NETCODE_REMIS: - remis_suggested = true; - break; - case NETCODE_TAUNT: - resign_suggested = true; - break; - case NETCODE_RESIGN: - if (mycolor == WHITE) { - gamestate->bresign = true; - } else { - gamestate->wresign = true; - } - return 1; - case NETCODE_CONNLOST: - gamestate->ragequit = true; - return 1; - case NETCODE_ERROR: - printw("\rCannot perform asynchronous network IO"); - cbreak(); getch(); - exit(EXIT_FAILURE); - case NETCODE_AGAIN: - /* try again */ - break; - default: - printw("\nThe opponent sent an invalid network pacakge."); - } - - /* read move */ - if (use_premove || asyncgetnstr(movestr, &bufpos, MOVESTR_BUFLEN)) { - bool was_premove = use_premove; - use_premove = false; - if (strncmp(movestr, "resign", MOVESTR_BUFLEN) == 0) { - if (mycolor == WHITE) { - gamestate->wresign = true; - } else { - gamestate->bresign = true; - } - net_send_code(opponent, NETCODE_RESIGN); - return 1; - } else if (strncmp(movestr, "savepgn", MOVESTR_BUFLEN) == 0) { - save_pgn(gamestate, gameinfo); - } else if (strncmp(movestr, "remis", MOVESTR_BUFLEN) == 0) { - if (remis_suggested) { - net_send_code(opponent, NETCODE_REMIS); - gamestate->remis = true; - return 1; - } if (!remis_rejected) { - net_send_code(opponent, NETCODE_REMIS); - printw("Remis offer sent - waiting for acceptance..."); - refresh(); - code = net_recieve_code(opponent); - if (code == NETCODE_ACCEPT) { - gamestate->remis = true; - return 1; - } else if (code == NETCODE_CONNLOST) { - gamestate->ragequit = true; - return 1; - } else { - remis_rejected = true; - } - } - } else if (movestr[0] == 0) { - /* ignore empty move strings and ask again */ - } else { - Move move; - int eval_result = eval_move(gamestate, movestr, &move, mycolor); - switch (eval_result) { - case VALID_MOVE_SYNTAX: - net_send_data(opponent, NETCODE_MOVE, &move, sizeof(Move)); - code = net_recieve_code(opponent); - move.check = code == NETCODE_CHECK || - code == NETCODE_CHECKMATE; - gamestate->checkmate = code == NETCODE_CHECKMATE; - gamestate->stalemate = code == NETCODE_STALEMATE; - if (code == NETCODE_DECLINE) { - uint32_t reason; - net_recieve_data(opponent, &reason, sizeof(uint32_t)); - reason = ntohl(reason); - eval_move_failed_msg(reason); - } else if (code == NETCODE_ACCEPT - || code == NETCODE_CHECK - || code == NETCODE_CHECKMATE - || code == NETCODE_STALEMATE) { - apply_move(gamestate, &move); - if (gamestate->checkmate || gamestate->stalemate) { - return 1; - } else { - return 0; - } - } else if (code == NETCODE_CONNLOST) { - printw("Your opponent left the game."); - return 1; - } else { - printw("Invalid network response."); - } - break; - default: - if (was_premove) { - printw("\nThe prepared move could not be executed."); - } else { - eval_move_failed_msg(eval_result); - } - } - clrtoeol(); - } - } - } -} - -static int recvmove(GameState *gamestate, GameInfo *gameinfo, - int opponent, uint8_t mycolor) { - memset(gamestate->premove, 0, sizeof(gamestate->premove)); - - size_t bufpos = 0; - char movestr[MOVESTR_BUFLEN]; - bool remis_suggested = false, resign_suggested = false; - while (1) { - timecontrol(gamestate, gameinfo); - - move(inputy, 0); - printw("Waiting for opponent. Use chess notation to prepare a move.\n"); - if (*gamestate->premove) { - printw("Current pre-move: %s \n\n", - gamestate->premove); - } else if (remis_suggested && !resign_suggested) { - printw("Suggested remis. \n\n"); - } else if (resign_suggested) { - if (remis_suggested) { - printw("Suggested to resign or at least to accept remis. \n\n"); - } else { - printw("Suggested to resign. \n\n"); - } - } else { - printw("Or use a command: remis, resign, taunt, savepgn \n\n"); - } - printw("Prepare your next move: "); - clrtoeol(); - refresh(); - - /* allow the player to prepare a move */ - if (asyncgetnstr(movestr, &bufpos, MOVESTR_BUFLEN)) { - if (strncmp(movestr, "resign", MOVESTR_BUFLEN) == 0) { - if (mycolor == WHITE) { - gamestate->wresign = true; - } else { - gamestate->bresign = true; - } - net_send_code(opponent, NETCODE_RESIGN); - return 1; - } else if (strncmp(movestr, "taunt", MOVESTR_BUFLEN) == 0) { - resign_suggested = true; - net_send_code(opponent, NETCODE_TAUNT); - } else if (strncmp(movestr, "remis", MOVESTR_BUFLEN) == 0) { - remis_suggested = true; - net_send_code(opponent, NETCODE_REMIS); - } else if (strncmp(movestr, "savepgn", MOVESTR_BUFLEN) == 0) { - save_pgn(gamestate, gameinfo); - } else if (movestr[0] == 0) { - memset(gamestate->premove, 0, sizeof(gamestate->premove)); - } else { - int res = check_move(movestr, mycolor); - if (res == VALID_MOVE_SYNTAX) { - strncpy(gamestate->premove, movestr, 8); - memset(movestr, 0, MOVESTR_BUFLEN); - bufpos = 0; - clrtobot(); - } else { - eval_move_failed_msg(res); - } - } - } - - /* read opponent's move */ - uint8_t code = net_recieve_code_async(opponent); - switch (code) { - case NETCODE_TIMEOVER: - /* redraw the time control */ - timecontrol(gamestate, gameinfo); - return 1; - case NETCODE_RESIGN: - if (mycolor == WHITE) { - gamestate->bresign = true; - } else { - gamestate->wresign = true; - } - return 1; - case NETCODE_CONNLOST: - gamestate->ragequit = true; - return 1; - case NETCODE_REMIS: - if (remis_suggested) { - gamestate->remis = true; - return 1; - } else { - if (prompt_yesno( - "\rYour opponent offers remis - do you accept")) { - gamestate->remis = true; - net_send_code(opponent, NETCODE_ACCEPT); - return 1; - } else { - net_send_code(opponent, NETCODE_DECLINE); - } - } - break; - case NETCODE_MOVE: { - Move move; - net_recieve_data(opponent, &move, sizeof(Move)); - code = validate_move(gamestate, &move); - if (code == VALID_MOVE_SEMANTICS) { - apply_move(gamestate, &move); - if (gamestate->checkmate) { - net_send_code(opponent, NETCODE_CHECKMATE); - return 1; - } else if (gamestate->stalemate) { - net_send_code(opponent, NETCODE_STALEMATE); - return 1; - } else if (move.check) { - net_send_code(opponent, NETCODE_CHECK); - } else { - net_send_code(opponent, NETCODE_ACCEPT); - } - return 0; - } else { - uint32_t reason = htonl(code); - net_send_data(opponent, NETCODE_DECLINE, - &reason, sizeof(uint32_t)); - } - break; - } - case NETCODE_ERROR: - printw("\rCannot perform asynchronous network IO"); - cbreak(); getch(); - exit(EXIT_FAILURE); - case NETCODE_AGAIN: - /* try again */ - break; - default: - printw("\nInvalid network request."); - } - } -} - -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; - - 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); - if (gamestate->wresign) { - addstr("White resigned.\n"); - } else if (gamestate->bresign) { - addstr("Black resigned.\n"); - } 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 was checkmated.\n", - gamestate->movecount % 2 == 0 ? "White" : "Black"); - } 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(&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(&viewedstate); -} - -void game_play_singlemachine(Settings *settings) { - inputy = getmaxy(stdscr) - 6; - - GameState gamestate; - gamestate_init(&gamestate); - uint8_t curcol = WHITE; - - if (settings->continuepgn) { - FILE *pgnfile = fopen(settings->continuepgn, "r"); - if (pgnfile) { - int result = read_pgn(pgnfile, &gamestate, &(settings->gameinfo)); - long position = ftell(pgnfile); - fclose(pgnfile); - if (result) { - printw("Invalid PGN file content at position %ld:\n%s\n", - position, pgn_error_str(result)); - return; - } - if (!is_game_running(&gamestate)) { - addstr("Game has ended. Use -S to analyze it.\n"); - return; - } - curcol = opponent_color(last_move(&gamestate).piece&COLOR_MASK); - } else { - printw("Can't read PGN file (%s)\n", strerror(errno)); - return; - } - } - - bool running; - do { - clear(); - uint8_t perspective = settings->disableflip ? WHITE : curcol; - draw_board(&gamestate, perspective, settings->unicode); - running = !domove_singlemachine(&gamestate, - &(settings->gameinfo), curcol); - curcol = opponent_color(curcol); - } while (running); - - game_review(settings, &gamestate); - gamestate_cleanup(&gamestate); -} - -void game_play(Settings *settings, GameState *gamestate, int opponent) { - inputy = getmaxy(stdscr) - 6; - - uint8_t mycolor = settings->gameinfo.servercolor; - if (!settings->ishost) { - mycolor = opponent_color(mycolor); - } - - bool myturn = (gamestate->movecount > 0 ? - (last_move(gamestate).piece & COLOR_MASK) : BLACK) != mycolor; - - bool running; - do { - clear(); - draw_board(gamestate, mycolor, settings->unicode); - if (myturn) { - running = !sendmove(gamestate, &(settings->gameinfo), - opponent, mycolor); - } else { - running = !recvmove(gamestate, &(settings->gameinfo), - opponent, mycolor); - } - myturn ^= true; - } while (running); -} - -void dump_gameinfo(GameInfo *gameinfo) { - int serverwhite = gameinfo->servercolor == WHITE; - attron(A_UNDERLINE); - printw("Game details\n"); - attroff(A_UNDERLINE); - printw(" Server: %s\n Client: %s\n", - serverwhite?"White":"Black", serverwhite?"Black":"White" - ); - if (gameinfo->timecontrol) { - if (gameinfo->time % 60) { - printw(" Time limit: %ds + %ds\n", - gameinfo->time, gameinfo->addtime); - } else { - printw(" Time limit: %dm + %ds\n", - gameinfo->time/60, gameinfo->addtime); - } - } else { - printw(" No time limit\n"); - } - refresh(); -} - -void dump_moveinfo(GameState *gamestate) { - for (unsigned i = 0 ; i < gamestate->movecount ; i++) { - if (i % 2 == 0) { - printw("%d. %s", 1+i/2, gamestate->moves[i].string); - } else { - printw("%s", gamestate->moves[i].string); - } - // only five moves reliably fit into one screen row - if ((i+1) % 10) { - addch(' '); - } else { - addch('\n'); - } - } - refresh(); -}