src/main.c

changeset 129
189c7c77aaab
parent 94
864f59271974
child 130
3fc6b1d6cbe9
--- a/src/main.c	Tue May 26 15:29:00 2026 +0200
+++ b/src/main.c	Thu May 28 12:15:26 2026 +0200
@@ -29,7 +29,8 @@
 
 #define PROGRAM_VERSION "1.0 alpha"
 
-#include "game.h"
+#include "chess/rules.h"
+#include "chess/pgn.h"
 #include "input.h"
 #include "network.h"
 #include "colors.h"
@@ -38,6 +39,26 @@
 #include <getopt.h>
 #include <locale.h>
 #include <sys/stat.h>
+#include <signal.h>
+#include <errno.h>
+#include <unistd.h>
+
+typedef struct {
+    GameInfo gameinfo;
+    /**
+     * 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;
+    short port;
+    bool ishost;
+    bool usedomainsocket;
+    bool singlemachine;
+    bool disableflip;
+    bool unicode;
+} Settings;
 
 int get_settings(int argc, char **argv, Settings *settings) {
     char *valid;
@@ -209,6 +230,956 @@
     }
 }
 
+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.");
+        }
+    }
+}
+
+static 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);
+}
+
+static 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);
+}
+
+static 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);
+}
+
+static 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();
+}
+
+static 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();
+}
+
+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, Settings *settings) {
+    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(Settings *settings) {
+    Server server;
+
+    dump_gameinfo(&(settings->gameinfo));
+    GameState gamestate;
+    gamestate_init(&gamestate);
+    if (settings->continuepgn) {
+        /* preload PGN data before handshake */
+        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 1;
+            }
+            if (!is_game_running(&gamestate)) {
+                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;
+        }
+    }
+
+    if (server_open(&server, settings)) {
+        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, &(settings->gameinfo), 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,
+            &(settings->gameinfo), 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(settings, &gamestate, fd);
+        net_destroy(&server);
+        game_review(settings, &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, Settings *settings) {
+    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(Settings *settings) {
+    Server server;
+
+    if (client_connect(&server, settings)) {
+        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, &(settings->gameinfo), sizeof(GameInfo));
+        dump_gameinfo(&(settings->gameinfo));
+        if (prompt_yesno("Accept challenge")) {
+            net_send_code(server.fd, NETCODE_ACCEPT);
+            game_play(settings, &gamestate, server.fd);
+            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));
+        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);
+        addch('\n');
+        dump_moveinfo(&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);
+            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(settings, &gamestate);
+    }
+    gamestate_cleanup(&gamestate);
+
+    net_destroy(&server);
+    return 0;
+}
+
 int main(int argc, char **argv) {
     srand(time(NULL));
 

mercurial