simplify code structure

Thu, 28 May 2026 12:15:26 +0200

author
Mike Becker <universe@uap-core.de>
date
Thu, 28 May 2026 12:15:26 +0200
changeset 129
189c7c77aaab
parent 128
ce38ee9bc3af
child 130
3fc6b1d6cbe9

simplify code structure

src/Makefile file | annotate | diff | comparison | revisions
src/chess/Makefile file | annotate | diff | comparison | revisions
src/chess/game-info.c file | annotate | diff | comparison | revisions
src/chess/game-info.h file | annotate | diff | comparison | revisions
src/chess/rules.c file | annotate | diff | comparison | revisions
src/chess/rules.h file | annotate | diff | comparison | revisions
src/client.c file | annotate | diff | comparison | revisions
src/game.c file | annotate | diff | comparison | revisions
src/game.h file | annotate | diff | comparison | revisions
src/main.c file | annotate | diff | comparison | revisions
src/server.c file | annotate | diff | comparison | revisions
--- a/src/Makefile	Tue May 26 15:29:00 2026 +0200
+++ b/src/Makefile	Thu May 28 12:15:26 2026 +0200
@@ -28,7 +28,7 @@
 
 include ../config.mk
 
-SRC  = main.c colors.c network.c input.c server.c client.c game.c
+SRC  = main.c colors.c network.c input.c
 OBJ = $(SRC:%.c=$(BUILDDIR)/%.o)
 
 all: $(BUILDDIR)/terminal-chess FORCE
@@ -41,26 +41,16 @@
 
 FORCE:
 
-$(BUILDDIR)/client.o: client.c input.h game.h chess/game-info.h network.h \
- chess/pgn.h chess/rules.h chess/game-info.h
-	@echo "Compiling $<"
-	$(CC) -o $@ $(CFLAGS) -c $<
-
 $(BUILDDIR)/colors.o: colors.c colors.h
 	@echo "Compiling $<"
 	$(CC) -o $@ $(CFLAGS) -c $<
 
-$(BUILDDIR)/game.o: game.c game.h chess/game-info.h network.h input.h \
- colors.h chess/rules.h chess/game-info.h chess/pgn.h chess/rules.h
-	@echo "Compiling $<"
-	$(CC) -o $@ $(CFLAGS) -c $<
-
 $(BUILDDIR)/input.o: input.c input.h
 	@echo "Compiling $<"
 	$(CC) -o $@ $(CFLAGS) -c $<
 
-$(BUILDDIR)/main.o: main.c game.h chess/game-info.h input.h network.h \
- colors.h
+$(BUILDDIR)/main.o: main.c chess/rules.h chess/pgn.h chess/rules.h \
+ input.h network.h colors.h
 	@echo "Compiling $<"
 	$(CC) -o $@ $(CFLAGS) -c $<
 
@@ -68,8 +58,3 @@
 	@echo "Compiling $<"
 	$(CC) -o $@ $(CFLAGS) -c $<
 
-$(BUILDDIR)/server.o: server.c network.h game.h chess/game-info.h \
- chess/pgn.h chess/rules.h chess/game-info.h
-	@echo "Compiling $<"
-	$(CC) -o $@ $(CFLAGS) -c $<
-
--- a/src/chess/Makefile	Tue May 26 15:29:00 2026 +0200
+++ b/src/chess/Makefile	Thu May 28 12:15:26 2026 +0200
@@ -28,7 +28,7 @@
 
 include ../../config.mk
 
-SRC = game-info.c pawn.c rook.c knight.c bishop.c queen.c king.c rules.c pgn.c
+SRC = pawn.c rook.c knight.c bishop.c queen.c king.c rules.c pgn.c
 OBJ = $(SRC:%.c=$(BUILDDIR)/%.o)
 
 all: $(BUILDDIR)/libchess$(LIB_EXT) FORCE
@@ -38,40 +38,36 @@
 
 FORCE:
 
-$(BUILDDIR)/bishop.o: bishop.c bishop.h rules.h game-info.h
+$(BUILDDIR)/bishop.o: bishop.c bishop.h rules.h
 	@echo "Compiling $<"
 	$(CC) -o $@ $(CFLAGS) -c $<
 
-$(BUILDDIR)/game-info.o: game-info.c game-info.h
+$(BUILDDIR)/king.o: king.c rules.h king.h
 	@echo "Compiling $<"
 	$(CC) -o $@ $(CFLAGS) -c $<
 
-$(BUILDDIR)/king.o: king.c rules.h game-info.h king.h
+$(BUILDDIR)/knight.o: knight.c knight.h rules.h
 	@echo "Compiling $<"
 	$(CC) -o $@ $(CFLAGS) -c $<
 
-$(BUILDDIR)/knight.o: knight.c knight.h rules.h game-info.h
+$(BUILDDIR)/pawn.o: pawn.c pawn.h rules.h
 	@echo "Compiling $<"
 	$(CC) -o $@ $(CFLAGS) -c $<
 
-$(BUILDDIR)/pawn.o: pawn.c pawn.h rules.h game-info.h
+$(BUILDDIR)/pgn.o: pgn.c pgn.h rules.h
 	@echo "Compiling $<"
 	$(CC) -o $@ $(CFLAGS) -c $<
 
-$(BUILDDIR)/pgn.o: pgn.c pgn.h rules.h game-info.h
+$(BUILDDIR)/queen.o: queen.c rules.h rook.h bishop.h queen.h
 	@echo "Compiling $<"
 	$(CC) -o $@ $(CFLAGS) -c $<
 
-$(BUILDDIR)/queen.o: queen.c rules.h game-info.h rook.h bishop.h queen.h
+$(BUILDDIR)/rook.o: rook.c rules.h rook.h
 	@echo "Compiling $<"
 	$(CC) -o $@ $(CFLAGS) -c $<
 
-$(BUILDDIR)/rook.o: rook.c rules.h game-info.h rook.h
+$(BUILDDIR)/rules.o: rules.c rules.h pawn.h rook.h knight.h bishop.h \
+ queen.h king.h
 	@echo "Compiling $<"
 	$(CC) -o $@ $(CFLAGS) -c $<
 
-$(BUILDDIR)/rules.o: rules.c rules.h game-info.h pawn.h rook.h knight.h \
- bishop.h queen.h king.h
-	@echo "Compiling $<"
-	$(CC) -o $@ $(CFLAGS) -c $<
-
--- a/src/chess/game-info.c	Tue May 26 15:29:00 2026 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,53 +0,0 @@
-/*
- * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
- *
- * Copyright 2016 Mike Becker. All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- *   1. Redistributions of source code must retain the above copyright
- *      notice, this list of conditions and the following disclaimer.
- *
- *   2. Redistributions in binary form must reproduce the above copyright
- *      notice, this list of conditions and the following disclaimer in the
- *      documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
- * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
- * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
- * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
- * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
- * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
- * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
- * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
- * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
- * POSSIBILITY OF SUCH DAMAGE.
- *
- */
-
-#include "game-info.h"
-#include <string.h>
-#include <stdlib.h>
-
-void gamestate_init(GameState *gamestate) {
-    memset(gamestate, 0, sizeof(GameState));
-
-    Board initboard = {
-        {WROOK, WKNIGHT, WBISHOP, WQUEEN, WKING, WBISHOP, WKNIGHT, WROOK},
-        {WPAWN, WPAWN,   WPAWN,   WPAWN,  WPAWN, WPAWN,   WPAWN,   WPAWN},
-        {0,     0,       0,       0,      0,     0,       0,       0},
-        {0,     0,       0,       0,      0,     0,       0,       0},
-        {0,     0,       0,       0,      0,     0,       0,       0},
-        {0,     0,       0,       0,      0,     0,       0,       0},
-        {BPAWN, BPAWN,   BPAWN,   BPAWN,  BPAWN, BPAWN,   BPAWN,   BPAWN},
-        {BROOK, BKNIGHT, BBISHOP, BQUEEN, BKING, BBISHOP, BKNIGHT, BROOK}
-    };
-    memcpy(gamestate->board, initboard, sizeof(Board));
-}
-
-void gamestate_cleanup(GameState *gamestate) {
-    free(gamestate->moves);
-    gamestate->movecount = gamestate->movecapacity = 0;
-}
\ No newline at end of file
--- a/src/chess/game-info.h	Tue May 26 15:29:00 2026 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,137 +0,0 @@
-/*
- * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
- *
- * Copyright 2016 Mike Becker. All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- *   1. Redistributions of source code must retain the above copyright
- *      notice, this list of conditions and the following disclaimer.
- *
- *   2. Redistributions in binary form must reproduce the above copyright
- *      notice, this list of conditions and the following disclaimer in the
- *      documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
- * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
- * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
- * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
- * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
- * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
- * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
- * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
- * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
- * POSSIBILITY OF SUCH DAMAGE.
- *
- */
-
-#ifndef GAME_INFO_H
-#define GAME_INFO_H
-
-#include <stdint.h>
-#include <stdbool.h>
-
-#define WHITE 0x10
-#define BLACK 0x20
-#define opponent_color(color) ((color)==WHITE?BLACK:WHITE)
-
-#define PIECE_MASK 0x0F
-#define COLOR_MASK 0x30
-
-#define PAWN   0x01
-#define ROOK   0x02
-#define KNIGHT 0x03
-#define BISHOP 0x04
-#define QUEEN  0x05
-#define KING   0x06
-
-#define WPAWN   (WHITE|PAWN)
-#define WROOK   (WHITE|ROOK)
-#define WKNIGHT (WHITE|KNIGHT)
-#define WBISHOP (WHITE|BISHOP)
-#define WQUEEN  (WHITE|QUEEN)
-#define WKING   (WHITE|KING)
-#define BPAWN   (BLACK|PAWN)
-#define BROOK   (BLACK|ROOK)
-#define BKNIGHT (BLACK|KNIGHT)
-#define BBISHOP (BLACK|BISHOP)
-#define BQUEEN  (BLACK|QUEEN)
-#define BKING   (BLACK|KING)
-
-typedef uint8_t Board[8][8];
-
-struct movetimeval {
-    uint64_t tv_sec;
-    int32_t tv_usec;
-};
-
-typedef struct {
-    uint8_t piece;
-    uint8_t fromfile;
-    uint8_t fromrow;
-    uint8_t tofile;
-    uint8_t torow;
-    uint8_t promotion;
-    uint8_t check;
-    uint8_t capture;
-    struct movetimeval timestamp;
-    struct movetimeval movetime;
-    char string[8];
-} Move;
-
-typedef struct {
-    uint8_t servercolor;
-    uint8_t timecontrol;
-    uint16_t time;
-    uint16_t addtime;
-} GameInfo;
-
-/** The buffer length for player names in GameInfo structures. */
-#define PLAYER_NAME_BUFLEN 32
-
-typedef struct {
-    Board board;
-    Move* moves;
-    /** optional name of the white player */
-    char wname[PLAYER_NAME_BUFLEN];
-    /** optional name of the black player */
-    char bname[PLAYER_NAME_BUFLEN];
-    /** capacity of the move array */
-    unsigned movecapacity;
-    /** number of (half-)moves (counting BOTH colors) */
-    unsigned int movecount;
-    /** a premove that shall be evaluated next time it's our turn */
-    char premove[8];
-    bool checkmate;
-    bool stalemate;
-    bool remis;
-    bool wresign;
-    bool bresign;
-    /** this flag is only supposed to be set when the opponent disconnects */
-    bool ragequit;
-    bool review;
-} GameState;
-
-
-#define is_game_running(gamestate) !((gamestate)->checkmate || \
-    (gamestate)->wresign || (gamestate)->bresign || \
-    (gamestate)->stalemate || (gamestate)->remis || (gamestate)->review)
-
-#define last_move(gamestate) \
-    ((gamestate)->moves[(gamestate)->movecount-1])
-
-/**
- * Initializes a game state and prepares the chess board.
- * @param gamestate the game state to initialize
- */
-void gamestate_init(GameState *gamestate);
-
-/**
- * Cleans up a game state and frees the memory for the movement list.
- * @param gamestate the game state to clean up
- */
-void gamestate_cleanup(GameState *gamestate);
-
-#endif
--- a/src/chess/rules.c	Tue May 26 15:29:00 2026 +0200
+++ b/src/chess/rules.c	Thu May 28 12:15:26 2026 +0200
@@ -40,6 +40,27 @@
 #include <stdlib.h>
 #include <sys/time.h>
 
+void gamestate_init(GameState *gamestate) {
+    memset(gamestate, 0, sizeof(GameState));
+
+    Board initboard = {
+        {WROOK, WKNIGHT, WBISHOP, WQUEEN, WKING, WBISHOP, WKNIGHT, WROOK},
+        {WPAWN, WPAWN,   WPAWN,   WPAWN,  WPAWN, WPAWN,   WPAWN,   WPAWN},
+        {0,     0,       0,       0,      0,     0,       0,       0},
+        {0,     0,       0,       0,      0,     0,       0,       0},
+        {0,     0,       0,       0,      0,     0,       0,       0},
+        {0,     0,       0,       0,      0,     0,       0,       0},
+        {BPAWN, BPAWN,   BPAWN,   BPAWN,  BPAWN, BPAWN,   BPAWN,   BPAWN},
+        {BROOK, BKNIGHT, BBISHOP, BQUEEN, BKING, BBISHOP, BKNIGHT, BROOK}
+    };
+    memcpy(gamestate->board, initboard, sizeof(Board));
+}
+
+void gamestate_cleanup(GameState *gamestate) {
+    free(gamestate->moves);
+    gamestate->movecount = gamestate->movecapacity = 0;
+}
+
 static GameState gamestate_copy_sim(GameState *gamestate) {
     GameState simulation = *gamestate;
 
--- a/src/chess/rules.h	Tue May 26 15:29:00 2026 +0200
+++ b/src/chess/rules.h	Thu May 28 12:15:26 2026 +0200
@@ -30,8 +30,6 @@
 #ifndef RULES_H
 #define	RULES_H
 
-#include "game-info.h"
-
 #include <stdint.h>
 #include <stdbool.h>
 
@@ -48,6 +46,94 @@
 
 #define ENPASSANT_THREAT 0x40
 
+#define WHITE 0x10
+#define BLACK 0x20
+#define opponent_color(color) ((color)==WHITE?BLACK:WHITE)
+
+#define PIECE_MASK 0x0F
+#define COLOR_MASK 0x30
+
+#define PAWN   0x01
+#define ROOK   0x02
+#define KNIGHT 0x03
+#define BISHOP 0x04
+#define QUEEN  0x05
+#define KING   0x06
+
+#define WPAWN   (WHITE|PAWN)
+#define WROOK   (WHITE|ROOK)
+#define WKNIGHT (WHITE|KNIGHT)
+#define WBISHOP (WHITE|BISHOP)
+#define WQUEEN  (WHITE|QUEEN)
+#define WKING   (WHITE|KING)
+#define BPAWN   (BLACK|PAWN)
+#define BROOK   (BLACK|ROOK)
+#define BKNIGHT (BLACK|KNIGHT)
+#define BBISHOP (BLACK|BISHOP)
+#define BQUEEN  (BLACK|QUEEN)
+#define BKING   (BLACK|KING)
+
+typedef uint8_t Board[8][8];
+
+struct movetimeval {
+    uint64_t tv_sec;
+    int32_t tv_usec;
+};
+
+typedef struct {
+    uint8_t piece;
+    uint8_t fromfile;
+    uint8_t fromrow;
+    uint8_t tofile;
+    uint8_t torow;
+    uint8_t promotion;
+    uint8_t check;
+    uint8_t capture;
+    struct movetimeval timestamp;
+    struct movetimeval movetime;
+    char string[8];
+} Move;
+
+typedef struct {
+    uint8_t servercolor;
+    uint8_t timecontrol;
+    uint16_t time;
+    uint16_t addtime;
+} GameInfo;
+
+/** The buffer length for player names in GameInfo structures. */
+#define PLAYER_NAME_BUFLEN 32
+
+typedef struct {
+    Board board;
+    Move* moves;
+    /** optional name of the white player */
+    char wname[PLAYER_NAME_BUFLEN];
+    /** optional name of the black player */
+    char bname[PLAYER_NAME_BUFLEN];
+    /** capacity of the move array */
+    unsigned movecapacity;
+    /** number of (half-)moves (counting BOTH colors) */
+    unsigned int movecount;
+    /** a premove that shall be evaluated next time it's our turn */
+    char premove[8];
+    bool checkmate;
+    bool stalemate;
+    bool remis;
+    bool wresign;
+    bool bresign;
+    /** this flag is only supposed to be set when the opponent disconnects */
+    bool ragequit;
+    bool review;
+} GameState;
+
+#define is_game_running(gamestate) !((gamestate)->checkmate || \
+    (gamestate)->wresign || (gamestate)->bresign || \
+    (gamestate)->stalemate || (gamestate)->remis || (gamestate)->review)
+
+#define last_move(gamestate) \
+    ((gamestate)->moves[(gamestate)->movecount-1])
+
 #define POS_UNSPECIFIED UINT8_MAX
 #define mdst(b,m) b[(m)->torow][(m)->tofile]
 #define msrc(b,m) b[(m)->fromrow][(m)->fromfile]
@@ -71,6 +157,19 @@
 #define fileidx_s(c) (isfile(c)?fileidx(c):POS_UNSPECIFIED)
 #define rowidx_s(c) (isrow(c)?rowidx(c):POS_UNSPECIFIED)
 
+
+/**
+ * Initializes a game state and prepares the chess board.
+ * @param gamestate the game state to initialize
+ */
+void gamestate_init(GameState *gamestate);
+
+/**
+ * Cleans up a game state and frees the memory for the movement list.
+ * @param gamestate the game state to clean up
+ */
+void gamestate_cleanup(GameState *gamestate);
+
 /**
  * Maps a character to a piece.
  * 
--- a/src/client.c	Tue May 26 15:29:00 2026 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,129 +0,0 @@
-/*
- * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
- *
- * Copyright 2016 Mike Becker. All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- *   1. Redistributions of source code must retain the above copyright
- *      notice, this list of conditions and the following disclaimer.
- *
- *   2. Redistributions in binary form must reproduce the above copyright
- *      notice, this list of conditions and the following disclaimer in the
- *      documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
- * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
- * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
- * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
- * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
- * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
- * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
- * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
- * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
- * POSSIBILITY OF SUCH DAMAGE.
- *
- */
-
-#include "input.h"
-#include "game.h"
-#include "network.h"
-#include "chess/pgn.h"
-#include <ncurses.h>
-#include <stdlib.h>
-
-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;
-}
-
-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;
-}
--- a/src/game.c	Tue May 26 15:29:00 2026 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,759 +0,0 @@
-/*
- * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
- *
- * Copyright 2016 Mike Becker. All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- *   1. Redistributions of source code must retain the above copyright
- *      notice, this list of conditions and the following disclaimer.
- *
- *   2. Redistributions in binary form must reproduce the above copyright
- *      notice, this list of conditions and the following disclaimer in the
- *      documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
- * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
- * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
- * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
- * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
- * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
- * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
- * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
- * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
- * POSSIBILITY OF SUCH DAMAGE.
- *
- */
-
-#include "game.h"
-#include "network.h"
-#include "input.h"
-#include "colors.h"
-#include "chess/rules.h"
-#include "chess/pgn.h"
-#include <ncurses.h>
-#include <string.h>
-#include <stdio.h>
-#include <errno.h>
-
-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.");
-        }
-    }
-}
-
-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);
-}
-
-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);
-}
-
-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);
-}
-
-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();
-}
-
-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();
-}
--- a/src/game.h	Tue May 26 15:29:00 2026 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,80 +0,0 @@
-/*
- * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
- *
- * Copyright 2016 Mike Becker. All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- *   1. Redistributions of source code must retain the above copyright
- *      notice, this list of conditions and the following disclaimer.
- *
- *   2. Redistributions in binary form must reproduce the above copyright
- *      notice, this list of conditions and the following disclaimer in the
- *      documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
- * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
- * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
- * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
- * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
- * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
- * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
- * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
- * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
- * POSSIBILITY OF SUCH DAMAGE.
- *
- */
-
-#ifndef GAME_H
-#define	GAME_H
-
-#include "chess/game-info.h"
-#include <stdbool.h>
-
-#ifdef	__cplusplus
-extern "C" {
-#endif
-
-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;
-
-void game_play_singlemachine(Settings *settings);
-
-/**
- * Plays a network game of chess.
- *
- * @param settings the game settings
- * @param gamestate the current game state
- * @param opponent file descriptor for the opponent
- */
-void game_play(Settings *settings, GameState *gamestate, int opponent);
-void game_review(Settings* settings, GameState *gamestate);
-
-int server_run(Settings* settings);
-int client_run(Settings* settings);
-
-void dump_moveinfo(GameState *gamestate);
-void dump_gameinfo(GameInfo *gameinfo);
-
-#ifdef	__cplusplus
-}
-#endif
-
-#endif	/* GAME_H */
-
--- 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));
 
--- a/src/server.c	Tue May 26 15:29:00 2026 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,173 +0,0 @@
-/*
- * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
- *
- * Copyright 2016 Mike Becker. All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- *   1. Redistributions of source code must retain the above copyright
- *      notice, this list of conditions and the following disclaimer.
- *
- *   2. Redistributions in binary form must reproduce the above copyright
- *      notice, this list of conditions and the following disclaimer in the
- *      documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
- * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
- * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
- * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
- * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
- * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
- * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
- * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
- * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
- * POSSIBILITY OF SUCH DAMAGE.
- *
- */
-
-#include "network.h"
-#include "game.h"
-#include "chess/pgn.h"
-#include <ncurses.h>
-#include <errno.h>
-#include <string.h>
-#include <stdlib.h>
-#include <unistd.h>
-#include <signal.h>
-
-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;
-}
-
-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;
-}

mercurial