start fixing the network protocol default tip

Sat, 22 Aug 2026 16:22:05 +0200

author
Mike Becker <universe@uap-core.de>
date
Sat, 22 Aug 2026 16:22:05 +0200
changeset 182
04c65336777f
parent 181
8bda076d0a16

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);
 

mercurial