diff -r ce38ee9bc3af -r 189c7c77aaab src/main.c --- 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 #include #include +#include +#include +#include + +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));