Sat, 22 Aug 2026 16:22:05 +0200
start fixing the network protocol
relates to #976
documentation issue #938
| PROTOCOL.md | file | annotate | diff | comparison | revisions | |
| src/chess/pgn.c | file | annotate | diff | comparison | revisions | |
| src/chess/rules.c | file | annotate | diff | comparison | revisions | |
| src/chess/rules.h | file | annotate | diff | comparison | revisions | |
| src/main.c | file | annotate | diff | comparison | revisions | |
| src/network.h | file | annotate | diff | comparison | revisions |
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/PROTOCOL.md Sat Aug 22 16:22:05 2026 +0200 @@ -0,0 +1,141 @@ +# terminal-chess Network Protocol + +The network protocol for terminal-chess is designed in an interoperable manner, +allowing for playing chess with other clients that implement the same protocol. + +This document describes the protocol in detail, including the message formats +and the expected behavior of clients and the server. + +The current protocol version is 24. + +## Message Format + +The terminal-chess network protocol has two different message types: + +* a code consisting of a single byte +* a message composed of a single byte indicating the type plus a payload + +An example implementation covering both types is the following struct: + +```C +struct message { + unsigned char code; + unsigned char message[]; // flexible array member +} +``` + +A table with all message codes can be found [below](#message-code-table). + +The datatypes used in the message payloads are as follows: + +* BYTE — a single byte +* WORD — two bytes integer (big-endian) +* DWORD — four bytes integer (big-endian) +* MOVE — a data structure describing a move, consisting of + * a DWORD for the elapsed move time in seconds + * a DWORD for additional elapsed move time in microseconds + * a BYTE describing the moved piece (see below) + * a BYTE index for the file the piece was moved from + * a BYTE index for the row the piece was moved from + * a BYTE index for the file the piece was moved to + * a BYTE index for the row the piece was moved to + * a BYTE describing the selected piece for promotion (otherwise zero) + +The following table shows the possible values for the BYTEs describing a piece. + +| Piece | Value (Hex) | +|--------------|-------------| +| White Pawn | 0x11 | +| White Rook | 0x12 | +| White Knight | 0x13 | +| White Bishop | 0x14 | +| White Queen | 0x15 | +| White King | 0x16 | +| Black Pawn | 0x21 | +| Black Rook | 0x22 | +| Black Knight | 0x23 | +| Black Bishop | 0x24 | +| Black Queen | 0x25 | +| Black King | 0x26 | + +## Handshake + +Immediately after a connection between a server and a client is established, +the server SHALL send one single byte denoting the protocol version to the +client. +The client SHALL then immediately answer with their protocol version. +After both parties have sent their version to the other party, they SHALL both +compare their own version with the received version. +When the versions do not match, both parties SHALL terminate the connection. +The users SHOULD be informed about the protocol mismatch. + +## Game Setup + +After a successful handshake, the server SHALL send the client information about +the game they want to play. The server can set up a completely new game or +continue an unfinished game. + +### Starting a New Game + +When the server wants to start a new game, they SHALL send a `NETCODE_GAMEINFO` +message. The payload of this message is as follows: +* one BYTE denoting the color the server wants to play + (16 or 0x10 = white, 32 or 0x20 = black) +* one BYTE indicating if the game is played with time control (1 = yes, 0 = no) +* one WORD for the initial clock time in seconds +* one WORD for the number of seconds that are added per move +* one WORD for the delay in seconds for each move + +When the game is played without time control, the last three WORDs MAY be +left uninitialized, but they SHALL be sent anyway. + +### Continuing a Game + +When the server wants to continue a game, they SHALL send a `NETCODE_PGNDATA` +message. The payload of this message is as follows: +* the same two BYTES and three WORDS as in `NETCODE_GAMEINFO`, followed by +* + +### Clock Setup + +For clock synchronization, the following rules SHALL be applied: + +* the clock of each player starts after their first move +* the _increment_ is added _after_ a move, except… +* … when the initial clock is zero, then the clock starts with the increment +* when the used time for a move is less than the delay, the move costs no time +* the delay is always subtracted from the actual move time + +## Message Code Table + +The following table shows the definitions of the network codes. + +| Code Name | Byte | With Payload | +|----------------------|------|--------------| +| NETCODE_ACCEPT | 0x02 | no | +| NETCODE_DECLINE | 0x04 | no | +| NETCODE_DECLINE_MOVE | 0x05 | yes | +| NETCODE_GAMEINFO | 0x10 | yes | +| NETCODE_PGNDATA | 0x11 | yes | +| NETCODE_MOVE | 0x20 | yes | +| NETCODE_CHECK | 0x22 | no | +| NETCODE_CHECKMATE | 0x23 | no | +| NETCODE_STALEMATE | 0x28 | no | +| NETCODE_NOMATERIAL | 0x29 | no | +| NETCODE_THREEFOLD | 0x30 | no | +| NETCODE_RESIGN | 0x41 | no | +| NETCODE_REMIS | 0x42 | no | +| NETCODE_TAUNT | 0x43 | no | +| NETCODE_TIMEOVER | 0x44 | no | + +The following codes are reserved for implementation. +They are not used during transmissions and can be used as return values +for functions to indicate errors. + +| Code Name | Byte | +|----------------------|------| +| NETCODE_AGAIN | 0x70 | +| NETCODE_CONNLOST | 0x80 | +| NETCODE_ERROR | 0xFF | + +
--- a/src/chess/pgn.c Sat Aug 22 15:15:52 2026 +0200 +++ b/src/chess/pgn.c Sat Aug 22 16:22:05 2026 +0200 @@ -147,7 +147,7 @@ } } while (!isspace(c = *(pgndata++))); movestr[i] = '\0'; - if (eval_move(gamestate, movestr, curcol, &move) + if (eval_move_strict(gamestate, movestr, curcol, &move) != VALID_MOVE_SYNTAX) { return(pgn_error_move_syntax); }
--- a/src/chess/rules.c Sat Aug 22 15:15:52 2026 +0200 +++ b/src/chess/rules.c Sat Aug 22 16:22:05 2026 +0200 @@ -106,94 +106,6 @@ return (gamestate->movecount % 2 == 0) ? WHITE : BLACK; } -/* MUST be called BETWEEN validating AND applying a move to work correctly */ -static void format_move(const GameState *gamestate, Move *move) { - char *string = &(move->string[0]); - - /* at least 8 characters should be available, wipe them out */ - memset(string, 0, 8); - - unsigned int idx; - if (piece_type(move->piece) == KING && - abs(move->tofile-move->fromfile) == 2) { - /* special formats for castling */ - if (move->tofile==fileidx('c')) { - memcpy(string, "O-O-O", 5); - idx = 5; - } else { - memcpy(string, "O-O", 3); - idx = 3; - } - } else { - /* start by notating the piece character */ - string[0] = getpiecechr(move->piece); - idx = string[0] ? 1 : 0; - - /* find out how many source information we do need */ - if (piece_type(move->piece) == PAWN) { - if (move->capture) { - string[idx++] = filechr(move->fromfile); - } - } else if (piece_type(move->piece) != KING) { - /* resolve ambiguities, if any */ - Move candidates[16]; - size_t ccount; - if (get_real_candidates(gamestate, move->torow, move->tofile, - piece_color(move->piece), candidates, &ccount)) { - unsigned int ambrows = 0, ambfiles = 0, ambpiece = 0; - for (size_t i = 0 ; i < ccount ; i++) { - if (candidates[i].piece == move->piece) { - ambpiece++; - if (candidates[i].fromrow == move->fromrow) { - ambrows++; - } - if (candidates[i].fromfile == move->fromfile) { - ambfiles++; - } - } - } - /* neither file, nor row are ambiguous, name file */ - if (ambpiece > 1 && ambrows == 1 && ambfiles == 1) { - /* this is most likely the case with Knights - * in diagonal opposition */ - string[idx++] = filechr(move->fromfile); - } else { - /* ambiguous row, name file */ - if (ambrows > 1) { - string[idx++] = filechr(move->fromfile); - } - /* ambiguous file, name row */ - if (ambfiles > 1) { - string[idx++] = rowchr(move->fromrow); - } - } - } - } - - /* capturing? */ - if (move->capture) { - string[idx++] = 'x'; - } - - /* destination */ - string[idx++] = filechr(move->tofile); - string[idx++] = rowchr(move->torow); - - /* promotion? */ - if (move->promotion) { - string[idx++] = '='; - string[idx++] = getpiecechr(move->promotion); - } - } - - /* check? */ - if (move->checkmate) { - string[idx++] = '#'; - } else if (move->check) { - string[idx++] = '+'; - } -} - static void calc_movetime(GameState *gamestate, Move *move) { /* only if move has no time info, compute it */ if (move->movetime > 0) return; @@ -622,6 +534,106 @@ return canescape ? 1 : 2; } + +static void format_move_impl(const GameState *gamestate, Move *move) { + char *string = &(move->string[0]); + + /* at least 8 characters should be available, wipe them out */ + memset(string, 0, 8); + + unsigned int idx; + if (piece_type(move->piece) == KING && + abs(move->tofile-move->fromfile) == 2) { + /* special formats for castling */ + if (move->tofile==fileidx('c')) { + memcpy(string, "O-O-O", 5); + idx = 5; + } else { + memcpy(string, "O-O", 3); + idx = 3; + } + } else { + /* start by notating the piece character */ + string[0] = getpiecechr(move->piece); + idx = string[0] ? 1 : 0; + + /* find out how many source information we do need */ + if (piece_type(move->piece) == PAWN) { + if (move->capture) { + string[idx++] = filechr(move->fromfile); + } + } else if (piece_type(move->piece) != KING) { + /* resolve ambiguities, if any */ + Move candidates[16]; + size_t ccount; + if (get_real_candidates(gamestate, move->torow, move->tofile, + piece_color(move->piece), candidates, &ccount)) { + unsigned int ambrows = 0, ambfiles = 0, ambpiece = 0; + for (size_t i = 0 ; i < ccount ; i++) { + if (candidates[i].piece == move->piece) { + ambpiece++; + if (candidates[i].fromrow == move->fromrow) { + ambrows++; + } + if (candidates[i].fromfile == move->fromfile) { + ambfiles++; + } + } + } + /* neither file, nor row are ambiguous, name file */ + if (ambpiece > 1 && ambrows == 1 && ambfiles == 1) { + /* this is most likely the case with Knights + * in diagonal opposition */ + string[idx++] = filechr(move->fromfile); + } else { + /* ambiguous row, name file */ + if (ambrows > 1) { + string[idx++] = filechr(move->fromfile); + } + /* ambiguous file, name row */ + if (ambfiles > 1) { + string[idx++] = rowchr(move->fromrow); + } + } + } + } + + /* capturing? */ + if (move->capture) { + string[idx++] = 'x'; + } + + /* destination */ + string[idx++] = filechr(move->tofile); + string[idx++] = rowchr(move->torow); + + /* promotion? */ + if (move->promotion) { + string[idx++] = '='; + string[idx++] = getpiecechr(move->promotion); + } + } + + /* check? */ + if (move->checkmate) { + string[idx++] = '#'; + } else if (move->check) { + string[idx++] = '+'; + } +} + +void format_move(const GameState *gamestate, Move *move) { + /* correct check/checkmate flags */ + move->check = move->checkmate = false; + switch (determine_check_or_checkmate(gamestate, move)) { + case 2: move->checkmate = true; + case 1: move->check = true; + } + + /* format the move string */ + format_move_impl(gamestate, move); +} + static int validate_move_rules(const GameState *gamestate, const Move *move) { assert((move->piece & ~(PIECE_MASK|COLOR_MASK)) == 0); @@ -1162,18 +1174,18 @@ } /* format the move string */ - format_move(gamestate, move); + format_move_impl(gamestate, move); } } return result; } -int eval_move_lazy(const GameState *gamestate, +int eval_move(const GameState *gamestate, const char *mstr, Color color, Move *move) { return eval_move2(gamestate, mstr, color, move, true); } -int eval_move(const GameState *gamestate, +int eval_move_strict(const GameState *gamestate, const char *mstr, Color color, Move *move) { return eval_move2(gamestate, mstr, color, move, false); }
--- a/src/chess/rules.h Sat Aug 22 15:15:52 2026 +0200 +++ b/src/chess/rules.h Sat Aug 22 16:22:05 2026 +0200 @@ -91,19 +91,18 @@ }; typedef struct { + char string[8]; + struct movetimeval timestamp; /* TODO: remove this from the struct */ + uint64_t movetime; /* the time for this move in microseconds */ Piece piece; File fromfile; Row fromrow; File tofile; Row torow; Piece promotion; - uint8_t check; /* must always be set if checkmate is set */ - uint8_t checkmate; - uint8_t capture; - uint8_t padding[7]; /* necessary for stable ABI across networks */ - struct movetimeval timestamp; /* TODO: remove this from the struct */ - uint64_t movetime; /* the time for this move in microseconds */ - char string[8]; + bool check; /* must always be set if checkmate is set */ + bool checkmate; + bool capture; } Move; typedef struct { @@ -489,7 +488,7 @@ * evaluating the allowed moves according to the current game state. * * This function expects correct notation of check and checkmate indicators. - * For a more lazy evaluation, use eval_move_lazy(). + * For a more lazy evaluation, use eval_move(). * * For a purely syntactic check, regardless of whether a piece exists that is * allowed to move that way, use check_move(). @@ -500,30 +499,42 @@ * @param move a pointer to object where the move data shall be stored * @return status code (see macros in this file for the list of codes) */ +int eval_move_strict(const GameState *gamestate, + const char *mstr, Color color, Move *move); + +/** + * Evaluates a move syntactically and stores the move data in the specified + * object. + * + * When short algebraic notation is used, the source position is determined by + * evaluating the allowed moves according to the current game state. + * + * This function automatically corrects missing or incorrect check/checkmate + * indicators. Use eval_move_strict() if you want to keep the original notation. + * + * For a purely syntactic check, regardless of whether a piece exists that is + * allowed to move that way, use check_move(). + * + * @param gamestate the current game state + * @param mstr the input string to parse + * @param color the color of the player to evaluate the move for + * @param move a pointer to object where the move data shall be stored + * @return status code (see macros in this file for the list of codes) + */ int eval_move(const GameState *gamestate, const char *mstr, Color color, Move *move); /** - * Evaluates a move syntactically and stores the move data in the specified - * object. - * - * When short algebraic notation is used, the source position is determined by - * evaluating the allowed moves according to the current game state. + * Calculates the move string within the specified move. * - * This function automatically corrects missing or incorrect check/checkmate - * indicators. Use eval_move() if you want to keep the original notation. + * This corrects any missing check / checkmate flags, first. * - * For a purely syntactic check, regardless of whether a piece exists that is - * allowed to move that way, use check_move(). + * Do not call this function after eval_move() or eval_move_strict(). * * @param gamestate the current game state - * @param mstr the input string to parse - * @param color the color of the player to evaluate the move for - * @param move a pointer to object where the move data shall be stored - * @return status code (see macros in this file for the list of codes) + * @param move the move data */ -int eval_move_lazy(const GameState *gamestate, - const char *mstr, Color color, Move *move); +void format_move(const GameState *gamestate, Move *move); /** * Syntactically checks a move without verifying that a piece exists that is
--- a/src/main.c Sat Aug 22 15:15:52 2026 +0200 +++ b/src/main.c Sat Aug 22 16:22:05 2026 +0200 @@ -473,7 +473,7 @@ /* ignore empty move strings and ask again */ } else { Move move; - int result = eval_move_lazy(gamestate, + int result = eval_move(gamestate, movestr, curcolor, &move); if (result == VALID_MOVE_SYNTAX) { result = validate_move(gamestate, &move); @@ -496,6 +496,31 @@ } } +static NetMove hton_move(const Move *move) { + return (NetMove) { + .mtime_sec = htonl(move->movetime / 1000000ull), + .mtime_usec = htonl(move->movetime % 1000000ull), + .piece = move->piece, + .fromfile = move->fromfile, .fromrow = move->fromrow, + .tofile = move->tofile, .torow = move->torow, + .promotion = move->promotion + }; +} + +static Move ntoh_move(const NetMove *move) { + return (Move) { + .string = {0}, /* calculated before applying the move */ + .timestamp = {0}, /* planned to be removed */ + .movetime = (uint64_t)move->mtime_sec * 1000000ull + move->mtime_usec, + .piece = move->piece, + .fromfile = move->fromfile, .fromrow = move->fromrow, + .tofile = move->tofile, .torow = move->torow, + .promotion = move->promotion, + /* flags are also calculated before applying the move */ + 0 + }; +} + static int sendmove(GameState *gamestate, int opponent, uint8_t mycolor) { size_t bufpos = 0; @@ -607,11 +632,12 @@ /* ignore empty move strings and ask again */ } else { Move move; - int eval_result = eval_move_lazy(gamestate, + int eval_result = eval_move(gamestate, movestr, mycolor, &move); switch (eval_result) { case VALID_MOVE_SYNTAX: - net_send_data(opponent, NETCODE_MOVE, &move, sizeof(Move)); + NetMove nmove = hton_move(&move); + net_send_data(opponent, NETCODE_MOVE, &nmove, NETMOVE_LEN); code = net_recieve_code(opponent); /* we could validate the move's check/checkmate flag with * the network response code, but we choose not to do it */ @@ -760,8 +786,10 @@ } break; case NETCODE_MOVE: { - Move move; - net_recieve_data(opponent, &move, sizeof(Move)); + NetMove nmove; + net_recieve_data(opponent, &nmove, NETMOVE_LEN); + Move move = ntoh_move(&nmove); + format_move(gamestate, &move); code = validate_move(gamestate, &move); if (code == VALID_MOVE_SEMANTICS) { apply_move(gamestate, &move); @@ -1049,13 +1077,18 @@ if (settings.continuepgn) { /* Continue game, send PGN data */ uint16_t mc = gamestate->movecount; - size_t pgndata_size = sizeof(GameInfo)+sizeof(mc)+mc*sizeof(Move); + size_t pgndata_size = sizeof(GameInfo) + 2 + mc * NETMOVE_LEN; char *pgndata = malloc(pgndata_size); memcpy(pgndata, &(gamestate->info), sizeof(GameInfo)); unsigned offset = sizeof(GameInfo); - memcpy(pgndata+offset, &mc, sizeof(mc)); - offset += sizeof(mc); - memcpy(pgndata+offset, gamestate->moves, mc*sizeof(Move)); + uint16_t mcn = htons(mc); + memcpy(pgndata+offset, &mcn, 2); + offset += 2; + for (unsigned i = 0 ; i < mc ; i++) { + NetMove nmove = hton_move(gamestate->moves + i); + memcpy(pgndata+offset, &nmove, NETMOVE_LEN); + offset += NETMOVE_LEN; + } net_send_data(fd, NETCODE_PGNDATA, pgndata, pgndata_size); free(pgndata); } else { @@ -1150,12 +1183,19 @@ net_recieve_data(server.fd, &(gamestate->info), sizeof(GameInfo)); uint16_t mc; net_recieve_data(server.fd, &mc, sizeof(mc)); - Move *moves = calloc(mc, sizeof(Move)); - net_recieve_data(server.fd, moves, mc*sizeof(Move)); + mc = ntohs(mc); + char *movedata = malloc(mc * NETMOVE_LEN); + net_recieve_data(server.fd, movedata, mc*NETMOVE_LEN); + unsigned offset = 0; for (size_t i = 0 ; i < mc ; i++) { - apply_move(gamestate, &(moves[i])); + NetMove nmove; + memcpy(&nmove, movedata+offset, NETMOVE_LEN); + offset += NETMOVE_LEN; + Move move = ntoh_move(&nmove); + format_move(gamestate, &move); + apply_move(gamestate, &move); } - free(moves); + free(movedata); dump_gameinfo(gamestate); if (prompt_yesno( "\n\nServer wants to continue a game. Accept challenge")) {
--- a/src/network.h Sat Aug 22 15:15:52 2026 +0200 +++ b/src/network.h Sat Aug 22 16:22:05 2026 +0200 @@ -70,6 +70,19 @@ Client *client; } Server; +typedef struct { + uint32_t mtime_sec; + uint32_t mtime_usec; + uint8_t piece; + uint8_t fromfile; + uint8_t fromrow; + uint8_t tofile; + uint8_t torow; + uint8_t promotion; +} NetMove; + +#define NETMOVE_LEN 14 + int net_create_tcp(Server *server, short port); int net_find_tcp(Server *server, char* host, short port);