src/main.c

changeset 130
3fc6b1d6cbe9
parent 129
189c7c77aaab
--- a/src/main.c	Thu May 28 12:15:26 2026 +0200
+++ b/src/main.c	Thu May 28 13:58:24 2026 +0200
@@ -44,7 +44,6 @@
 #include <unistd.h>
 
 typedef struct {
-    GameInfo gameinfo;
     /**
      * Server host address.
      * TCP: server address or \c NULL when we are the server
@@ -52,7 +51,12 @@
      */
     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;
@@ -60,29 +64,53 @@
     bool unicode;
 } Settings;
 
-int get_settings(int argc, char **argv, Settings *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:Fhp:rsS:t:uUv")) != -1 ;) {
+    for (int opt ; (opt = getopt(argc, argv, "a:bc:d:Fhp:rsS:t:uUv")) != -1 ;) {
         switch (opt) {
         case 'c':
-            settings->continuepgn = optarg;
+            settings.continuepgn = optarg;
             break;
         case 'b':
-            settings->gameinfo.servercolor = BLACK;
+            settings.servercolor = BLACK;
             break;
         case 'r':
-            settings->gameinfo.servercolor = rand() & 1 ? WHITE : BLACK;
+            settings.servercolor = rand() & 1 ? WHITE : BLACK;
             break;
         case 's':
-            settings->singlemachine = true;
+            settings.singlemachine = true;
             break;
         case 'F':
-            settings->disableflip = true;
+            settings.disableflip = true;
             break;
         case 'u':
             if (port_set) {
@@ -90,13 +118,14 @@
                         "when a TCP port was specified.\n");
                 return 1;
             }
-            settings->usedomainsocket = true;
+            settings.usedomainsocket = true;
             break;
         case 'U':
-            settings->unicode = false;
+            settings.unicode = false;
             break;
         case 't':
         case 'a':
+        case 'd':
             len = strlen(optarg);
             if (optarg[len-1] == 's') {
                 optarg[len-1] = '\0';
@@ -109,11 +138,13 @@
                     "- Maximum: 65535 seconds (1092 minutes)\n", optarg);
                 return 1;
             } else {
-                settings->gameinfo.timecontrol = 1;
+                settings.timecontrol = 1;
                 if (opt=='t') {
-                    settings->gameinfo.time = timeunit * time;
+                    settings.time = timeunit * time;
+                } else if (opt=='a') {
+                    settings.addtime = time;
                 } else {
-                    settings->gameinfo.addtime = time;
+                    settings.delay = time;
                 }
             }
             break;
@@ -122,7 +153,7 @@
                 fprintf(stderr, "Cannot use -p twice.\n");
                 return 1;
             }
-            if (settings->usedomainsocket) {
+            if (settings.usedomainsocket) {
                 fprintf(stderr, "Cannot specify TCP port "
                         "when using Unix domain sockets.\n");
                 return 1;
@@ -135,7 +166,7 @@
                     optarg);
                 return 1;
             } else {
-                settings->port = (short) port;
+                settings.port = (short) port;
                 port_set = true;
             }
             break;
@@ -155,18 +186,17 @@
 "  -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"
-"  -a <time>     Specifies the time to add after each move\n"
 "  -b            Server plays black pieces (default: white)\n"
 "  -r            Distribute color randomly\n"
-"  -t <time>     Specifies time limit (default: no limit)\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"
-"The time unit for -a is seconds and for -t minutes by default. To "
-"specify\nseconds for the -t option, use the s suffix.\n"
-"Example: -t 150s\n\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"
@@ -177,68 +207,52 @@
     }
     
     if (optind == argc - 1) {
-        settings->serverhost = argv[optind];
+        settings.serverhost = argv[optind];
     } else if (optind < argc - 1) {
         fprintf(stderr, "Too many arguments\n");
         return 1;
     }
     
         
-    if (settings->continuepgn) {
-        if (settings->serverhost) {
+    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";
+    if (settings.usedomainsocket) {
+        if (!settings.serverhost) {
+            settings.serverhost = "/tmp/chess.sock";
         }
         struct stat st;
-        if (stat(settings->serverhost, &st) == 0) {
+        if (stat(settings.serverhost, &st) == 0) {
             if (S_ISSOCK(st.st_mode)) {
-                settings->ishost = false;
+                settings.ishost = false;
             } else {
                 fprintf(stderr, "%s is not a Unix domain socket.\n",
-                    settings->serverhost);
+                    settings.serverhost);
                 return 1;
             }
         } else {
-            settings->ishost = true;
+            settings.ishost = true;
         }
     } else {
-        settings->ishost = !settings->serverhost;
+        settings.ishost = !settings.serverhost;
     }
     
     return 0;
 }
 
-static Settings settings;
-
-static void init_settings(void) {
-    memset(&settings, 0, sizeof(settings));
-    settings.gameinfo.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 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);
+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 = gameinfo->time >= 3600;
+        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);
@@ -263,9 +277,7 @@
     return 0;
 }
 
-static void draw_board(GameState *gamestate,
-		       uint8_t perspective,
-		       bool unicode) {
+static void draw_board(GameState *gamestate, uint8_t perspective) {
     char fen[90];
     compute_fen(fen, gamestate);
     mvaddstr(0, 0, fen);
@@ -276,7 +288,7 @@
             uint8_t piece = gamestate->board[y][x];
             char piecestr[5];
             if (piece) {
-                if (unicode) {
+                if (settings.unicode) {
                     char* uc = getpieceunicode(piece);
                     strncpy(piecestr, uc, 5);
                 } else {
@@ -371,7 +383,7 @@
     }
 }
 
-static void save_pgn(GameState *gamestate, GameInfo *gameinfo) {
+static void save_pgn(GameState *gamestate) {
     int y = getcury(stdscr);
 
     /* ask for player names */
@@ -401,7 +413,7 @@
         move(y, 0);
         FILE *file = fopen(filename, "w");
         if (file) {
-            write_pgn(file, gamestate, gameinfo, export_comments);
+            write_pgn(file, gamestate, export_comments);
             fclose(file);
             printw("File saved.");
         } else {
@@ -412,17 +424,14 @@
 }
 
 #define MOVESTR_BUFLEN 10
-static int domove_singlemachine(GameState *gamestate,
-        GameInfo *gameinfo, uint8_t curcolor) {
-
-
+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, gameinfo)) {
+        if (timecontrol(gamestate)) {
             return 1;
         }
         move(inputy, 0);
@@ -444,7 +453,7 @@
                 gamestate->remis = true;
                 return 1;
             } else if (strncmp(movestr, "savepgn", MOVESTR_BUFLEN) == 0) {
-                save_pgn(gamestate, gameinfo);
+                save_pgn(gamestate);
             } else if (movestr[0] == 0) {
                 /* ignore empty move strings and ask again */
             } else {
@@ -473,8 +482,7 @@
     }
 }
 
-static int sendmove(GameState *gamestate, GameInfo *gameinfo,
-        int opponent, uint8_t mycolor) {
+static int sendmove(GameState *gamestate, int opponent, uint8_t mycolor) {
 
     size_t bufpos = 0;
     char movestr[MOVESTR_BUFLEN];
@@ -494,7 +502,7 @@
 
     flushinp();
     while (1) {
-        if (timecontrol(gamestate, gameinfo)) {
+        if (timecontrol(gamestate)) {
             net_send_code(opponent, NETCODE_TIMEOVER);
             return 1;
         }
@@ -560,7 +568,7 @@
                 net_send_code(opponent, NETCODE_RESIGN);
                 return 1;
             } else if (strncmp(movestr, "savepgn", MOVESTR_BUFLEN) == 0) {
-                save_pgn(gamestate, gameinfo);
+                save_pgn(gamestate);
             } else if (strncmp(movestr, "remis", MOVESTR_BUFLEN) == 0) {
                 if (remis_suggested) {
                     net_send_code(opponent, NETCODE_REMIS);
@@ -629,15 +637,14 @@
     }
 }
 
-static int recvmove(GameState *gamestate, GameInfo *gameinfo,
-        int opponent, uint8_t mycolor) {
+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, gameinfo);
+        timecontrol(gamestate);
 
         move(inputy, 0);
         printw("Waiting for opponent. Use chess notation to prepare a move.\n");
@@ -676,7 +683,7 @@
                 remis_suggested = true;
                 net_send_code(opponent, NETCODE_REMIS);
             } else if (strncmp(movestr, "savepgn", MOVESTR_BUFLEN) == 0) {
-                save_pgn(gamestate, gameinfo);
+                save_pgn(gamestate);
             } else if (movestr[0] == 0) {
                 memset(gamestate->premove, 0, sizeof(gamestate->premove));
             } else {
@@ -697,7 +704,7 @@
         switch (code) {
         case NETCODE_TIMEOVER:
             /* redraw the time control */
-            timecontrol(gamestate, gameinfo);
+            timecontrol(gamestate);
             return 1;
         case NETCODE_RESIGN:
             if (mycolor == WHITE) {
@@ -762,9 +769,8 @@
     }
 }
 
-static void game_review(Settings* settings, GameState *gamestate) {
+static void game_review(GameState *gamestate) {
     const unsigned page_moves = 10;
-    GameInfo *gameinfo = &(settings->gameinfo);
     GameState viewedstate = {0};
     unsigned viewedmove = gamestate->movecount;
     bool redraw = true;
@@ -777,8 +783,8 @@
             gamestate_at_move(gamestate, viewedmove, &viewedstate);
 
             erase(); /* don't use clear() to avoid flickering */
-            draw_board(&viewedstate, WHITE, settings->unicode);
-            timecontrol(&viewedstate, gameinfo);
+            draw_board(&viewedstate, WHITE);
+            timecontrol(&viewedstate);
 
             move(getmaxy(stdscr)-5, 0);
             if (gamestate->wresign) {
@@ -804,7 +810,7 @@
         if (c == 's') {
             addch('\r');
             echo();
-            save_pgn(&viewedstate, gameinfo);
+            save_pgn(&viewedstate);
             noecho();
             redraw = true;
         } else if (c == KEY_UP || c == KEY_LEFT) {
@@ -842,17 +848,18 @@
     gamestate_cleanup(&viewedstate);
 }
 
-static void game_play_singlemachine(Settings *settings) {
+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 (settings.continuepgn) {
+        FILE *pgnfile = fopen(settings.continuepgn, "r");
         if (pgnfile) {
-            int result = read_pgn(pgnfile, &gamestate, &(settings->gameinfo));
+            int result = read_pgn(pgnfile, &gamestate);
             long position = ftell(pgnfile);
             fclose(pgnfile);
             if (result) {
@@ -874,22 +881,21 @@
     bool running;
     do {
         clear();
-        uint8_t perspective = settings->disableflip ? WHITE : curcol;
-        draw_board(&gamestate, perspective, settings->unicode);
-        running = !domove_singlemachine(&gamestate,
-            &(settings->gameinfo), curcol);
+        uint8_t perspective = settings.disableflip ? WHITE : curcol;
+        draw_board(&gamestate, perspective);
+        running = !domove_singlemachine(&gamestate, curcol);
         curcol = opponent_color(curcol);
     }  while (running);
 
-    game_review(settings, &gamestate);
+    game_review(&gamestate);
     gamestate_cleanup(&gamestate);
 }
 
-static void game_play(Settings *settings, GameState *gamestate, int opponent) {
+static void game_play(GameState *gamestate, int opponent, bool as_client) {
     inputy = getmaxy(stdscr) - 6;
 
-    uint8_t mycolor = settings->gameinfo.servercolor;
-    if (!settings->ishost) {
+    uint8_t mycolor = gamestate->info.servercolor;
+    if (as_client) {
         mycolor = opponent_color(mycolor);
     }
 
@@ -899,19 +905,18 @@
     bool running;
     do {
         clear();
-        draw_board(gamestate, mycolor, settings->unicode);
+        draw_board(gamestate, mycolor);
         if (myturn) {
-            running = !sendmove(gamestate, &(settings->gameinfo),
-                opponent, mycolor);
+            running = !sendmove(gamestate, opponent, mycolor);
         } else {
-            running = !recvmove(gamestate, &(settings->gameinfo),
-                opponent, mycolor);
+            running = !recvmove(gamestate, opponent, mycolor);
         }
         myturn ^= true;
     }  while (running);
 }
 
-static void dump_gameinfo(GameInfo *gameinfo) {
+static void dump_gameinfo(GameState *gamestate) {
+    GameInfo *gameinfo = &gamestate->info;
     int serverwhite = gameinfo->servercolor == WHITE;
     attron(A_UNDERLINE);
     printw("Game details\n");
@@ -921,19 +926,20 @@
     );
     if (gameinfo->timecontrol) {
         if (gameinfo->time % 60) {
-            printw("  Time limit: %ds + %ds\n",
+            printw("  Time limit: %us + %us",
                 gameinfo->time, gameinfo->addtime);
         } else {
-            printw("  Time limit: %dm + %ds\n",
+            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");
     }
-    refresh();
-}
-
-static void dump_moveinfo(GameState *gamestate) {
+    addch('\n');
     for (unsigned i = 0 ; i < gamestate->movecount ; i++) {
         if (i % 2 == 0) {
             printw("%d. %s", 1+i/2, gamestate->moves[i].string);
@@ -958,12 +964,12 @@
     }
 }
 
-static int server_open(Server *server, Settings *settings) {
+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)) {
+    if (settings.usedomainsocket
+            ? net_create_sock(server, settings.serverhost)
+            : net_create_tcp(server, settings.port)) {
         addstr("Server creation failed");
         return 1;
     }
@@ -996,18 +1002,17 @@
     return 0;
 }
 
-static int server_run(Settings *settings) {
+static int server_run(void) {
     Server server;
 
-    dump_gameinfo(&(settings->gameinfo));
     GameState gamestate;
     gamestate_init(&gamestate);
-    if (settings->continuepgn) {
+    settings_apply(&gamestate);
+    if (settings.continuepgn) {
         /* preload PGN data before handshake */
-        FILE *pgnfile = fopen(settings->continuepgn, "r");
+        FILE *pgnfile = fopen(settings.continuepgn, "r");
         if (pgnfile) {
-            int result = read_pgn(pgnfile, &gamestate,
-                &(settings->gameinfo));
+            int result = read_pgn(pgnfile, &gamestate);
             long position = ftell(pgnfile);
             fclose(pgnfile);
             if (result) {
@@ -1019,16 +1024,14 @@
                 addstr("Game has ended. Use -s to analyze it locally.\n");
                 return 1;
             }
-            addch('\n');
-            dump_moveinfo(&gamestate);
-            addch('\n');
         } else {
             printw("Can't read PGN file (%s)\n", strerror(errno));
             return 1;
         }
     }
+    dump_gameinfo(&gamestate);
 
-    if (server_open(&server, settings)) {
+    if (server_open(&server)) {
         net_destroy(&server);
         return 1;
     }
@@ -1039,12 +1042,12 @@
     }
 
     int fd = server.client->fd;
-    if (settings->continuepgn) {
+    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, &(settings->gameinfo), sizeof(GameInfo));
+        memcpy(pgndata, &(gamestate.info), sizeof(GameInfo));
         unsigned offset = sizeof(GameInfo);
         memcpy(pgndata+offset, &mc, sizeof(mc));
         offset += sizeof(mc);
@@ -1054,7 +1057,7 @@
     } else {
         /* Start new game */
         net_send_data(fd, NETCODE_GAMEINFO,
-            &(settings->gameinfo), sizeof(GameInfo));
+            &(gamestate.info), sizeof(GameInfo));
     }
     addstr("\rClient connected - awaiting challenge acceptance...");
     refresh();
@@ -1063,9 +1066,9 @@
     if (code == NETCODE_ACCEPT) {
         addstr("\rClient connected - challenge accepted.");
         clrtoeol();
-        game_play(settings, &gamestate, fd);
+        game_play(&gamestate, fd, false);
         net_destroy(&server);
-        game_review(settings, &gamestate);
+        game_review(&gamestate);
     } else if (code == NETCODE_DECLINE) {
         addstr("\rClient connected - challenge declined.");
         clrtoeol();
@@ -1086,10 +1089,10 @@
 }
 
 
-static int client_connect(Server *server, Settings *settings) {
-    if (settings->usedomainsocket
-            ? net_find_sock(server, settings->serverhost)
-            : net_find_tcp(server, settings->serverhost, settings->port)) {
+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;
     }
@@ -1116,10 +1119,10 @@
     return 0;
 }
 
-static int client_run(Settings *settings) {
+static int client_run(void) {
     Server server;
 
-    if (client_connect(&server, settings)) {
+    if (client_connect(&server)) {
         net_destroy(&server);
         return 1;
     }
@@ -1135,18 +1138,17 @@
     bool played = false;
     if (code == NETCODE_GAMEINFO) {
         /* Start new game */
-        net_recieve_data(server.fd, &(settings->gameinfo), sizeof(GameInfo));
-        dump_gameinfo(&(settings->gameinfo));
+        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(settings, &gamestate, server.fd);
+            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, &(settings->gameinfo), sizeof(GameInfo));
-        dump_gameinfo(&(settings->gameinfo));
+        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));
@@ -1155,12 +1157,11 @@
             apply_move(&gamestate, &(moves[i]));
         }
         free(moves);
-        addch('\n');
-        dump_moveinfo(&gamestate);
+        dump_gameinfo(&gamestate);
         if (prompt_yesno(
                 "\n\nServer wants to continue a game. Accept challenge")) {
             net_send_code(server.fd, NETCODE_ACCEPT);
-            game_play(settings, &gamestate, server.fd);
+            game_play(&gamestate, server.fd, true);
             played = true;
         } else {
             net_send_code(server.fd, NETCODE_DECLINE);
@@ -1172,7 +1173,7 @@
     }
 
     if (played) {
-        game_review(settings, &gamestate);
+        game_review(&gamestate);
     }
     gamestate_cleanup(&gamestate);
 
@@ -1184,7 +1185,7 @@
     srand(time(NULL));
 
     init_settings();
-    if (get_settings(argc, argv, &settings)) {
+    if (get_settings(argc, argv)) {
         return 1;
     }
 
@@ -1204,11 +1205,10 @@
     
     int exitcode;
     if (settings.singlemachine) {
-        game_play_singlemachine(&settings);
+        game_play_singlemachine();
         exitcode = EXIT_SUCCESS;
     } else {
-        exitcode = settings.ishost ?
-            server_run(&settings) : client_run(&settings);
+        exitcode = settings.ishost ? server_run() : client_run();
     }
     
     mvaddstr(getmaxy(stdscr)-1, 0,

mercurial