Thu, 28 May 2026 13:58:24 +0200
implement optional delay - resolves #820
/* * 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. * */ #define PROGRAM_VERSION "1.0 alpha" #include "chess/rules.h" #include "chess/pgn.h" #include "input.h" #include "network.h" #include "colors.h" #include <string.h> #include <time.h> #include <getopt.h> #include <locale.h> #include <sys/stat.h> #include <signal.h> #include <errno.h> #include <unistd.h> typedef struct { /** * Server host address. * TCP: server address or \c NULL when we are the server * Domain Socket: the path to the domain socket */ char* serverhost; char* continuepgn; unsigned short time; unsigned short addtime; unsigned short delay; short port; uint8_t servercolor; bool timecontrol; bool ishost; bool usedomainsocket; bool singlemachine; bool disableflip; bool unicode; } Settings; static Settings settings; static void init_settings(void) { memset(&settings, 0, sizeof(settings)); settings.servercolor = WHITE; settings.port = 27015; settings.unicode = !!setlocale(LC_CTYPE, "C.UTF-8"); } static void cleanup() { endwin(); if (settings.usedomainsocket && settings.ishost) { remove(settings.serverhost); } } static void settings_apply(GameState *gamestate) { gamestate->info.servercolor = settings.servercolor; gamestate->info.timecontrol = settings.timecontrol; gamestate->info.time = settings.time; gamestate->info.addtime = settings.addtime; gamestate->info.delay = settings.delay; } static int get_settings(int argc, char **argv) { char *valid; unsigned long int time, port; uint8_t timeunit = 60; size_t len; bool port_set = false; for (int opt ; (opt = getopt(argc, argv, "a:bc:d:Fhp:rsS:t:uUv")) != -1 ;) { switch (opt) { case 'c': settings.continuepgn = optarg; break; case 'b': settings.servercolor = BLACK; break; case 'r': settings.servercolor = rand() & 1 ? WHITE : BLACK; break; case 's': settings.singlemachine = true; break; case 'F': settings.disableflip = true; break; case 'u': if (port_set) { fprintf(stderr, "Cannot use Unix domain sockets " "when a TCP port was specified.\n"); return 1; } settings.usedomainsocket = true; break; case 'U': settings.unicode = false; break; case 't': case 'a': case 'd': len = strlen(optarg); if (optarg[len-1] == 's') { optarg[len-1] = '\0'; timeunit = 1; } if ((time = strtoul(optarg, &valid, 10))*timeunit > UINT16_MAX || *valid != '\0') { fprintf(stderr, "Specified time is invalid (%s)" "- Maximum: 65535 seconds (1092 minutes)\n", optarg); return 1; } else { settings.timecontrol = 1; if (opt=='t') { settings.time = timeunit * time; } else if (opt=='a') { settings.addtime = time; } else { settings.delay = time; } } break; case 'p': if (port_set) { fprintf(stderr, "Cannot use -p twice.\n"); return 1; } if (settings.usedomainsocket) { fprintf(stderr, "Cannot specify TCP port " "when using Unix domain sockets.\n"); return 1; } port = strtol(optarg, &valid, 10); if (port < 1025 || port > 65535 || *valid != '\0') { fprintf(stderr, "Invalid port number (%s) - choose a number between " "1025 and 65535\n", optarg); return 1; } else { settings.port = (short) port; port_set = true; } break; case 'v': printf("terminal-chess : Version %s (Netcode Version %d)\n", PROGRAM_VERSION, NETCODE_VERSION); exit(0); case 'h': case '?': printf( "Usage: terminal-chess [OPTION]... [HOST]\n" "Starts/joins a network chess game\n" "\nGeneral options\n" " -c <PGN file> Continue the specified game\n" " -h This help page\n" " -p TCP port to use (default: 27015)\n" " -u Use Unix domain socket instead of TCP\n" " -U Disables unicode pieces\n" " -v Print version information and exits\n" "\nTime control (default: disabled)\n" " -t <time> Time limit in minutes (or seconds when used with 's' suffix)\n" " -a <time> The time in seconds to add after each move\n" " -d <time> A delay in seconds before the clock starts ticking each move\n" "\nServer options\n" " -b Server plays black pieces (default: white)\n" " -r Distribute color randomly\n" "\nHot seat\n" " -s Play a hot seat game (network options are ignored)\n" " -F Do not automatically flip the board in hot seat games\n" "\nNotes\n" "Use '-' for PGN files to read PGN data from standard input\n\n" "When playing over Unix domain socket, the HOST denotes the socket path.\n" "When the path doest not exist, a game is created. Otherwise, the program\n" "joins the existing game. When HOST is omitted, /tmp/chess.sock is used.\n" ); exit(0); } } if (optind == argc - 1) { settings.serverhost = argv[optind]; } else if (optind < argc - 1) { fprintf(stderr, "Too many arguments\n"); return 1; } if (settings.continuepgn) { if (settings.serverhost) { fprintf(stderr, "Can't continue a game when joining a server.\n"); return 1; } } if (settings.usedomainsocket) { if (!settings.serverhost) { settings.serverhost = "/tmp/chess.sock"; } struct stat st; if (stat(settings.serverhost, &st) == 0) { if (S_ISSOCK(st.st_mode)) { settings.ishost = false; } else { fprintf(stderr, "%s is not a Unix domain socket.\n", settings.serverhost); return 1; } } else { settings.ishost = true; } } else { settings.ishost = !settings.serverhost; } return 0; } static const uint8_t boardx = 4, boardy = 10; static int inputy = 21; /* should be overridden on game startup */ static int timecontrol(GameState *gamestate) { if (gamestate->info.timecontrol) { uint16_t white = remaining_movetime(gamestate, WHITE); uint16_t black = remaining_movetime(gamestate, BLACK); char clkstr[16]; bool always_hours = gamestate->info.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) { 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 (settings.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) { 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, 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, uint8_t curcolor) { size_t bufpos = 0; char movestr[MOVESTR_BUFLEN]; flushinp(); while (1) { const char *curcolorstr = curcolor == WHITE ? "White" : "Black"; if (timecontrol(gamestate)) { 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); } 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, 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)) { 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); } 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, 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); 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); } 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); 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."); } } } static void game_review(GameState *gamestate) { const unsigned page_moves = 10; 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); timecontrol(&viewedstate); 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); 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); } static void game_play_singlemachine(void) { inputy = getmaxy(stdscr) - 6; GameState gamestate; gamestate_init(&gamestate); settings_apply(&gamestate); uint8_t curcol = WHITE; if (settings.continuepgn) { FILE *pgnfile = fopen(settings.continuepgn, "r"); if (pgnfile) { int result = read_pgn(pgnfile, &gamestate); 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); running = !domove_singlemachine(&gamestate, curcol); curcol = opponent_color(curcol); } while (running); game_review(&gamestate); gamestate_cleanup(&gamestate); } static void game_play(GameState *gamestate, int opponent, bool as_client) { inputy = getmaxy(stdscr) - 6; uint8_t mycolor = gamestate->info.servercolor; if (as_client) { 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); if (myturn) { running = !sendmove(gamestate, opponent, mycolor); } else { running = !recvmove(gamestate, opponent, mycolor); } myturn ^= true; } while (running); } static void dump_gameinfo(GameState *gamestate) { GameInfo *gameinfo = &gamestate->info; 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: %us + %us", gameinfo->time, gameinfo->addtime); } else { printw(" Time limit: %um + %us", gameinfo->time/60, gameinfo->addtime); } if (gameinfo->delay) { printw(" (with %us delay)", gameinfo->delay); } addch('\n'); } else { printw(" No time limit\n"); } addch('\n'); 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(); } static int server_fd = -1; static void interrupt_listen(int sig) { if (server_fd > -1) { // this interrupts close(server_fd); } } static int server_open(Server *server) { printw("\nListening for client...\n"); refresh(); if (settings.usedomainsocket ? net_create_sock(server, settings.serverhost) : net_create_tcp(server, settings.port)) { addstr("Server creation failed"); return 1; } // allow Ctrl+C to interrupt the listening process server_fd = server->fd; signal(SIGINT, interrupt_listen); if (net_listen(server)) { addstr("Listening for client failed or interrupted"); return 1; } // restore default action signal(SIGINT, SIG_DFL); return 0; } static int server_handshake(Client *client) { net_send_code(client->fd, NETCODE_VERSION); if (net_recieve_code(client->fd) != NETCODE_VERSION) { addstr("Client uses an incompatible software version."); return 1; } addstr("Client connected - transmitting gameinfo..."); refresh(); return 0; } static int server_run(void) { Server server; GameState gamestate; gamestate_init(&gamestate); settings_apply(&gamestate); if (settings.continuepgn) { /* preload PGN data before handshake */ FILE *pgnfile = fopen(settings.continuepgn, "r"); if (pgnfile) { int result = read_pgn(pgnfile, &gamestate); 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 1; } if (!is_game_running(&gamestate)) { addstr("Game has ended. Use -s to analyze it locally.\n"); return 1; } } else { printw("Can't read PGN file (%s)\n", strerror(errno)); return 1; } } dump_gameinfo(&gamestate); if (server_open(&server)) { net_destroy(&server); return 1; } if (server_handshake(server.client)) { net_destroy(&server); return 1; } int fd = server.client->fd; if (settings.continuepgn) { /* Continue game, send PGN data */ uint16_t mc = gamestate.movecount; size_t pgndata_size = sizeof(GameInfo)+sizeof(mc)+mc*sizeof(Move); char *pgndata = malloc(pgndata_size); memcpy(pgndata, &(gamestate.info), sizeof(GameInfo)); unsigned offset = sizeof(GameInfo); memcpy(pgndata+offset, &mc, sizeof(mc)); offset += sizeof(mc); memcpy(pgndata+offset, gamestate.moves, mc*sizeof(Move)); net_send_data(fd, NETCODE_PGNDATA, pgndata, pgndata_size); free(pgndata); } else { /* Start new game */ net_send_data(fd, NETCODE_GAMEINFO, &(gamestate.info), sizeof(GameInfo)); } 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(&gamestate, fd, false); net_destroy(&server); game_review(&gamestate); } else if (code == NETCODE_DECLINE) { addstr("\rClient connected - challenge declined."); clrtoeol(); net_destroy(&server); } else if (code == NETCODE_CONNLOST) { addstr("\rClient connected - but gave no response."); clrtoeol(); net_destroy(&server); } else { addstr("\rInvalid client response"); clrtoeol(); net_destroy(&server); exitcode = 1; } gamestate_cleanup(&gamestate); return exitcode; } static int client_connect(Server *server) { if (settings.usedomainsocket ? net_find_sock(server, settings.serverhost) : net_find_tcp(server, settings.serverhost, settings.port)) { addstr("Can't find server"); return 1; } if (net_connect(server)) { addstr("Can't connect to server"); return 1; } return 0; } static int client_handshake(Server *server) { if (net_recieve_code(server->fd) != NETCODE_VERSION) { addstr("Server uses an incompatible software version."); return 1; } else { net_send_code(server->fd, NETCODE_VERSION); } printw("Connection established!\n\n"); refresh(); return 0; } static int client_run(void) { Server server; if (client_connect(&server)) { net_destroy(&server); return 1; } if (client_handshake(&server)) { net_destroy(&server); return 1; } uint8_t code = net_recieve_code(server.fd); GameState gamestate; gamestate_init(&gamestate); bool played = false; if (code == NETCODE_GAMEINFO) { /* Start new game */ net_recieve_data(server.fd, &(gamestate.info), sizeof(GameInfo)); dump_gameinfo(&gamestate); if (prompt_yesno("Accept challenge")) { net_send_code(server.fd, NETCODE_ACCEPT); game_play(&gamestate, server.fd, true); played = true; } else { net_send_code(server.fd, NETCODE_DECLINE); } } else if (code == NETCODE_PGNDATA) { net_recieve_data(server.fd, &(gamestate.info), sizeof(GameInfo)); uint16_t mc; net_recieve_data(server.fd, &mc, sizeof(mc)); Move *moves = calloc(mc, sizeof(Move)); net_recieve_data(server.fd, moves, mc*sizeof(Move)); for (size_t i = 0 ; i < mc ; i++) { apply_move(&gamestate, &(moves[i])); } free(moves); dump_gameinfo(&gamestate); if (prompt_yesno( "\n\nServer wants to continue a game. Accept challenge")) { net_send_code(server.fd, NETCODE_ACCEPT); game_play(&gamestate, server.fd, true); played = true; } else { net_send_code(server.fd, NETCODE_DECLINE); } } else { addstr("Server sent invalid gameinfo."); net_destroy(&server); return 1; } if (played) { game_review(&gamestate); } gamestate_cleanup(&gamestate); net_destroy(&server); return 0; } int main(int argc, char **argv) { srand(time(NULL)); init_settings(); if (get_settings(argc, argv)) { return 1; } initscr(); halfdelay(1); keypad(stdscr, true); if (has_colors()) { start_color(); init_colorpairs(); bkgd(COLOR_PAIR(COL_APP)); } else { fprintf(stderr, "Non-colored terminals are not supported yet."); endwin(); return EXIT_FAILURE; } atexit(cleanup); int exitcode; if (settings.singlemachine) { game_play_singlemachine(); exitcode = EXIT_SUCCESS; } else { exitcode = settings.ishost ? server_run() : client_run(); } mvaddstr(getmaxy(stdscr)-1, 0, "Game has ended. Press any key to leave..."); clrtoeol(); refresh(); cbreak(); flushinp(); getch(); return exitcode; }