--- 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,