# HG changeset patch # User Mike Becker # Date 1779963326 -7200 # Node ID 189c7c77aaabc48b2ef4ed7e9344f1ee354dc4fd # Parent ce38ee9bc3afcf5be14b538270bc6364cf18179c simplify code structure diff -r ce38ee9bc3af -r 189c7c77aaab src/Makefile --- 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 $< - diff -r ce38ee9bc3af -r 189c7c77aaab src/chess/Makefile --- 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 $< - diff -r ce38ee9bc3af -r 189c7c77aaab src/chess/game-info.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 -#include - -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 diff -r ce38ee9bc3af -r 189c7c77aaab src/chess/game-info.h --- 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 -#include - -#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 diff -r ce38ee9bc3af -r 189c7c77aaab src/chess/rules.c --- 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 #include +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; diff -r ce38ee9bc3af -r 189c7c77aaab src/chess/rules.h --- 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 #include @@ -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. * diff -r ce38ee9bc3af -r 189c7c77aaab src/client.c --- 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 -#include - -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; -} diff -r ce38ee9bc3af -r 189c7c77aaab src/game.c --- 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 -#include -#include -#include - -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(); -} diff -r ce38ee9bc3af -r 189c7c77aaab src/game.h --- 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 - -#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 */ - diff -r ce38ee9bc3af -r 189c7c77aaab src/main.c --- a/src/main.c Tue May 26 15:29:00 2026 +0200 +++ b/src/main.c Thu May 28 12:15:26 2026 +0200 @@ -29,7 +29,8 @@ #define PROGRAM_VERSION "1.0 alpha" -#include "game.h" +#include "chess/rules.h" +#include "chess/pgn.h" #include "input.h" #include "network.h" #include "colors.h" @@ -38,6 +39,26 @@ #include #include #include +#include +#include +#include + +typedef struct { + GameInfo gameinfo; + /** + * Server host address. + * TCP: server address or \c NULL when we are the server + * Domain Socket: the path to the domain socket + */ + char* serverhost; + char* continuepgn; + short port; + bool ishost; + bool usedomainsocket; + bool singlemachine; + bool disableflip; + bool unicode; +} Settings; int get_settings(int argc, char **argv, Settings *settings) { char *valid; @@ -209,6 +230,956 @@ } } +static const uint8_t boardx = 4, boardy = 10; +static int inputy = 21; /* should be overridden on game startup */ + +static int timecontrol(GameState *gamestate, GameInfo *gameinfo) { + if (gameinfo->timecontrol) { + uint16_t white = remaining_movetime(gameinfo, gamestate, WHITE); + uint16_t black = remaining_movetime(gameinfo, gamestate, BLACK); + char clkstr[16]; + bool always_hours = gameinfo->time >= 3600; + print_clk(white, clkstr, always_hours); + mvprintw(boardy+4, boardx-1, "White time: %s", clkstr); + print_clk(black, clkstr, always_hours); + mvprintw(boardy+5, boardx-1, "Black time: %s", clkstr); + + if (white == 0) { + move(inputy, 0); + printw("Time is over - Black wins!"); + clrtobot(); + refresh(); + return 1; + } + if (black == 0) { + move(inputy, 0); + printw("Time is over - White wins!"); + clrtobot(); + refresh(); + return 1; + } + } + + return 0; +} + +static void draw_board(GameState *gamestate, + uint8_t perspective, + bool unicode) { + char fen[90]; + compute_fen(fen, gamestate); + mvaddstr(0, 0, fen); + + for (uint8_t y = 0 ; y < 8 ; y++) { + for (uint8_t x = 0 ; x < 8 ; x++) { + uint8_t col = gamestate->board[y][x] & COLOR_MASK; + uint8_t piece = gamestate->board[y][x]; + char piecestr[5]; + if (piece) { + if (unicode) { + char* uc = getpieceunicode(piece); + strncpy(piecestr, uc, 5); + } else { + piecestr[0] = (piece & PIECE_MASK) == PAWN + ? 'P' : getpiecechr(piece); + piecestr[1] = '\0'; + } + } else { + piecestr[0] = ' '; + piecestr[1] = '\0'; + } + + bool boardblack = (y&1)==(x&1); + attrset((col==WHITE ? A_BOLD : A_DIM)| + COLOR_PAIR(col == WHITE ? + (boardblack ? COL_WB : COL_WW) : + (boardblack ? COL_BB : COL_BW) + ) + ); + + int cy = perspective == WHITE ? boardy-y : boardy-7+y; + int cx = perspective == WHITE ? boardx+x*3 : boardx+21-x*3; + mvprintw(cy, cx, " %s ", piecestr); + } + } + + attrset(A_NORMAL); + for (uint8_t i = 0 ; i < 8 ; i++) { + int x = perspective == WHITE ? boardx+i*3+1 : boardx+22-i*3; + int y = perspective == WHITE ? boardy-i : boardy-7+i; + mvaddch(boardy+1, x, 'a'+i); + mvaddch(y, boardx-2, '1'+i); + } + + /* move log */ + uint8_t logy = 2; + const uint8_t logx = boardx + 28; + move(logy, logx); + + /* count full moves */ + unsigned int logi = 0; + + /* wrap log after 45 moves */ + while (gamestate->movecount/6-logi/3 >= 15) { + logi++; + } + + for (unsigned mi = logi*2 ; mi < gamestate->movecount ; mi++) { + bool iswhite = mi % 2 == 0; + if (iswhite) { + logi++; + printw("%d. ", logi); + } + + addstr(gamestate->moves[mi].string); + if (!iswhite && logi%3 == 0) { + move(++logy, logx); + } else { + addch(' '); + } + } +} + +static void eval_move_failed_msg(int code) { + switch (code) { + case AMBIGUOUS_MOVE: + printw("Ambiguous move - please specify the piece to move."); + break; + case INVALID_POSITION: + printw("No piece can be moved this way."); + break; + case NEED_PROMOTION: + printw("You need to promote the pawn (append \"=Q\" e.g.)!"); + break; + case KING_IN_CHECK: + printw("Your king is in check!"); + break; + case PIECE_PINNED: + printw("This piece is pinned!"); + break; + case INVALID_MOVE_SYNTAX: + printw("Can't interpret move - please use algebraic notation."); + break; + case RULES_VIOLATED: + printw("Move does not comply chess rules."); + break; + case KING_MOVES_INTO_CHECK: + printw("Can't move the king into a check position."); + break; + default: + printw("Unknown move parser error."); + } +} + +static void save_pgn(GameState *gamestate, GameInfo *gameinfo) { + int y = getcury(stdscr); + + /* ask for player names */ + { + char pname[PLAYER_NAME_BUFLEN]; + printw("\rWhite's name (%s): ", pgn_player_name(gamestate, WHITE)); + clrtoeol(); + if (getnstr(pname, PLAYER_NAME_BUFLEN) == OK && pname[0] != '\0') { + strncpy(gamestate->wname, pname, PLAYER_NAME_BUFLEN); + } + move(y, 0); + printw("\rBlack's name (%s): ", pgn_player_name(gamestate, BLACK)); + clrtoeol(); + if (getnstr(pname, PLAYER_NAME_BUFLEN) == OK && pname[0] != '\0') { + strncpy(gamestate->bname, pname, PLAYER_NAME_BUFLEN); + } + move(y, 0); + } + + bool export_comments = prompt_yesno("Export with comments"); + + printw("\rFilename: "); + clrtoeol(); + + char filename[64]; + if (getnstr(filename, 64) == OK && filename[0] != '\0') { + move(y, 0); + FILE *file = fopen(filename, "w"); + if (file) { + write_pgn(file, gamestate, gameinfo, export_comments); + fclose(file); + printw("File saved."); + } else { + printw("Can't write to file (%s).", strerror(errno)); + } + clrtoeol(); + } +} + +#define MOVESTR_BUFLEN 10 +static int domove_singlemachine(GameState *gamestate, + GameInfo *gameinfo, uint8_t curcolor) { + + + size_t bufpos = 0; + char movestr[MOVESTR_BUFLEN]; + + flushinp(); + while (1) { + const char *curcolorstr = curcolor == WHITE ? "White" : "Black"; + if (timecontrol(gamestate, gameinfo)) { + return 1; + } + move(inputy, 0); + printw( + "Use chess notation to enter your move.\n" + "Or use a command: remis, resign, savepgn\n\n" + "%s to move: ", curcolorstr); + clrtoeol(); + + if (asyncgetnstr(movestr, &bufpos, MOVESTR_BUFLEN)) { + if (strncmp(movestr, "resign", MOVESTR_BUFLEN) == 0) { + if (curcolor == WHITE) { + gamestate->wresign = true; + } else { + gamestate->bresign = true; + } + return 1; + } else if (strncmp(movestr, "remis", MOVESTR_BUFLEN) == 0) { + gamestate->remis = true; + return 1; + } else if (strncmp(movestr, "savepgn", MOVESTR_BUFLEN) == 0) { + save_pgn(gamestate, gameinfo); + } else if (movestr[0] == 0) { + /* ignore empty move strings and ask again */ + } else { + Move move; + int result = eval_move(gamestate, movestr, &move, curcolor); + if (result == VALID_MOVE_SYNTAX) { + result = validate_move(gamestate, &move); + if (result == VALID_MOVE_SEMANTICS) { + apply_move(gamestate, &move); + if (gamestate->checkmate) { + return 1; + } else if (gamestate->stalemate) { + return 1; + } else { + return 0; + } + } else { + eval_move_failed_msg(result); + } + } else { + eval_move_failed_msg(result); + } + clrtoeol(); + } + } + } +} + +static int sendmove(GameState *gamestate, GameInfo *gameinfo, + int opponent, uint8_t mycolor) { + + size_t bufpos = 0; + char movestr[MOVESTR_BUFLEN]; + bool remis_rejected = false; + bool remis_suggested = false; + bool resign_suggested = false; + bool use_premove = false; + uint8_t code; + + if (*gamestate->premove) { + use_premove = true; + const unsigned mlen = sizeof(gamestate->premove); + strncpy(movestr, gamestate->premove, mlen); + movestr[mlen] = '\0'; + memset(gamestate->premove, 0, mlen); + } + + flushinp(); + while (1) { + if (timecontrol(gamestate, gameinfo)) { + net_send_code(opponent, NETCODE_TIMEOVER); + return 1; + } + + move(inputy, 0); + printw("Use chess notation to enter your move.\n"); + if (resign_suggested) { + if (remis_suggested) { + printw("The opponent asks you to resign or accept remis. \n\n"); + } else { + printw("The opponent asks you to resign. \n\n"); + } + } else if (remis_suggested) { + printw("The opponent offers remis. Type remis to accept. \n\n"); + } else if (remis_rejected) { + printw("Remis offer rejected. \n\n"); + } else { + printw("Or use a command: remis, resign, savepgn \n\n"); + } + printw("Type your move: "); + clrtoeol(); + + /* check if the opponent sent us something */ + code = net_recieve_code_async(opponent); + switch (code) { + case NETCODE_REMIS: + remis_suggested = true; + break; + case NETCODE_TAUNT: + resign_suggested = true; + break; + case NETCODE_RESIGN: + if (mycolor == WHITE) { + gamestate->bresign = true; + } else { + gamestate->wresign = true; + } + return 1; + case NETCODE_CONNLOST: + gamestate->ragequit = true; + return 1; + case NETCODE_ERROR: + printw("\rCannot perform asynchronous network IO"); + cbreak(); getch(); + exit(EXIT_FAILURE); + case NETCODE_AGAIN: + /* try again */ + break; + default: + printw("\nThe opponent sent an invalid network pacakge."); + } + + /* read move */ + if (use_premove || asyncgetnstr(movestr, &bufpos, MOVESTR_BUFLEN)) { + bool was_premove = use_premove; + use_premove = false; + if (strncmp(movestr, "resign", MOVESTR_BUFLEN) == 0) { + if (mycolor == WHITE) { + gamestate->wresign = true; + } else { + gamestate->bresign = true; + } + net_send_code(opponent, NETCODE_RESIGN); + return 1; + } else if (strncmp(movestr, "savepgn", MOVESTR_BUFLEN) == 0) { + save_pgn(gamestate, gameinfo); + } else if (strncmp(movestr, "remis", MOVESTR_BUFLEN) == 0) { + if (remis_suggested) { + net_send_code(opponent, NETCODE_REMIS); + gamestate->remis = true; + return 1; + } if (!remis_rejected) { + net_send_code(opponent, NETCODE_REMIS); + printw("Remis offer sent - waiting for acceptance..."); + refresh(); + code = net_recieve_code(opponent); + if (code == NETCODE_ACCEPT) { + gamestate->remis = true; + return 1; + } else if (code == NETCODE_CONNLOST) { + gamestate->ragequit = true; + return 1; + } else { + remis_rejected = true; + } + } + } else if (movestr[0] == 0) { + /* ignore empty move strings and ask again */ + } else { + Move move; + int eval_result = eval_move(gamestate, movestr, &move, mycolor); + switch (eval_result) { + case VALID_MOVE_SYNTAX: + net_send_data(opponent, NETCODE_MOVE, &move, sizeof(Move)); + code = net_recieve_code(opponent); + move.check = code == NETCODE_CHECK || + code == NETCODE_CHECKMATE; + gamestate->checkmate = code == NETCODE_CHECKMATE; + gamestate->stalemate = code == NETCODE_STALEMATE; + if (code == NETCODE_DECLINE) { + uint32_t reason; + net_recieve_data(opponent, &reason, sizeof(uint32_t)); + reason = ntohl(reason); + eval_move_failed_msg(reason); + } else if (code == NETCODE_ACCEPT + || code == NETCODE_CHECK + || code == NETCODE_CHECKMATE + || code == NETCODE_STALEMATE) { + apply_move(gamestate, &move); + if (gamestate->checkmate || gamestate->stalemate) { + return 1; + } else { + return 0; + } + } else if (code == NETCODE_CONNLOST) { + printw("Your opponent left the game."); + return 1; + } else { + printw("Invalid network response."); + } + break; + default: + if (was_premove) { + printw("\nThe prepared move could not be executed."); + } else { + eval_move_failed_msg(eval_result); + } + } + clrtoeol(); + } + } + } +} + +static int recvmove(GameState *gamestate, GameInfo *gameinfo, + int opponent, uint8_t mycolor) { + memset(gamestate->premove, 0, sizeof(gamestate->premove)); + + size_t bufpos = 0; + char movestr[MOVESTR_BUFLEN]; + bool remis_suggested = false, resign_suggested = false; + while (1) { + timecontrol(gamestate, gameinfo); + + move(inputy, 0); + printw("Waiting for opponent. Use chess notation to prepare a move.\n"); + if (*gamestate->premove) { + printw("Current pre-move: %s \n\n", + gamestate->premove); + } else if (remis_suggested && !resign_suggested) { + printw("Suggested remis. \n\n"); + } else if (resign_suggested) { + if (remis_suggested) { + printw("Suggested to resign or at least to accept remis. \n\n"); + } else { + printw("Suggested to resign. \n\n"); + } + } else { + printw("Or use a command: remis, resign, taunt, savepgn \n\n"); + } + printw("Prepare your next move: "); + clrtoeol(); + refresh(); + + /* allow the player to prepare a move */ + if (asyncgetnstr(movestr, &bufpos, MOVESTR_BUFLEN)) { + if (strncmp(movestr, "resign", MOVESTR_BUFLEN) == 0) { + if (mycolor == WHITE) { + gamestate->wresign = true; + } else { + gamestate->bresign = true; + } + net_send_code(opponent, NETCODE_RESIGN); + return 1; + } else if (strncmp(movestr, "taunt", MOVESTR_BUFLEN) == 0) { + resign_suggested = true; + net_send_code(opponent, NETCODE_TAUNT); + } else if (strncmp(movestr, "remis", MOVESTR_BUFLEN) == 0) { + remis_suggested = true; + net_send_code(opponent, NETCODE_REMIS); + } else if (strncmp(movestr, "savepgn", MOVESTR_BUFLEN) == 0) { + save_pgn(gamestate, gameinfo); + } else if (movestr[0] == 0) { + memset(gamestate->premove, 0, sizeof(gamestate->premove)); + } else { + int res = check_move(movestr, mycolor); + if (res == VALID_MOVE_SYNTAX) { + strncpy(gamestate->premove, movestr, 8); + memset(movestr, 0, MOVESTR_BUFLEN); + bufpos = 0; + clrtobot(); + } else { + eval_move_failed_msg(res); + } + } + } + + /* read opponent's move */ + uint8_t code = net_recieve_code_async(opponent); + switch (code) { + case NETCODE_TIMEOVER: + /* redraw the time control */ + timecontrol(gamestate, gameinfo); + return 1; + case NETCODE_RESIGN: + if (mycolor == WHITE) { + gamestate->bresign = true; + } else { + gamestate->wresign = true; + } + return 1; + case NETCODE_CONNLOST: + gamestate->ragequit = true; + return 1; + case NETCODE_REMIS: + if (remis_suggested) { + gamestate->remis = true; + return 1; + } else { + if (prompt_yesno( + "\rYour opponent offers remis - do you accept")) { + gamestate->remis = true; + net_send_code(opponent, NETCODE_ACCEPT); + return 1; + } else { + net_send_code(opponent, NETCODE_DECLINE); + } + } + break; + case NETCODE_MOVE: { + Move move; + net_recieve_data(opponent, &move, sizeof(Move)); + code = validate_move(gamestate, &move); + if (code == VALID_MOVE_SEMANTICS) { + apply_move(gamestate, &move); + if (gamestate->checkmate) { + net_send_code(opponent, NETCODE_CHECKMATE); + return 1; + } else if (gamestate->stalemate) { + net_send_code(opponent, NETCODE_STALEMATE); + return 1; + } else if (move.check) { + net_send_code(opponent, NETCODE_CHECK); + } else { + net_send_code(opponent, NETCODE_ACCEPT); + } + return 0; + } else { + uint32_t reason = htonl(code); + net_send_data(opponent, NETCODE_DECLINE, + &reason, sizeof(uint32_t)); + } + break; + } + case NETCODE_ERROR: + printw("\rCannot perform asynchronous network IO"); + cbreak(); getch(); + exit(EXIT_FAILURE); + case NETCODE_AGAIN: + /* try again */ + break; + default: + printw("\nInvalid network request."); + } + } +} + +static void game_review(Settings* settings, GameState *gamestate) { + const unsigned page_moves = 10; + GameInfo *gameinfo = &(settings->gameinfo); + GameState viewedstate = {0}; + unsigned viewedmove = gamestate->movecount; + bool redraw = true; + + noecho(); + int c; + do { + if (redraw) { + gamestate_cleanup(&viewedstate); + gamestate_at_move(gamestate, viewedmove, &viewedstate); + + erase(); /* don't use clear() to avoid flickering */ + draw_board(&viewedstate, WHITE, settings->unicode); + timecontrol(&viewedstate, gameinfo); + + move(getmaxy(stdscr)-5, 0); + if (gamestate->wresign) { + addstr("White resigned.\n"); + } else if (gamestate->bresign) { + addstr("Black resigned.\n"); + } else if (gamestate->remis) { + addstr("The game ended remis.\n"); + } else if (gamestate->stalemate) { + addstr("The game ended in a stalemate.\n"); + } else if (gamestate->checkmate) { + printw("%s was checkmated.\n", + gamestate->movecount % 2 == 0 ? "White" : "Black"); + } else if (gamestate->ragequit) { + printw("Your opponent disconnected.\n"); + } + addstr("\nPress 'q' to quit, 's' to save the position as PGN, or\n" + "arrow keys, home/end, page up/down to review the game.\n"); + flushinp(); + redraw = false; + } + c = getch(); + if (c == 's') { + addch('\r'); + echo(); + save_pgn(&viewedstate, gameinfo); + noecho(); + redraw = true; + } else if (c == KEY_UP || c == KEY_LEFT) { + if (viewedmove > 0) { + viewedmove--; + redraw = true; + } + } else if (c == KEY_DOWN || c == KEY_RIGHT) { + if (viewedmove < gamestate->movecount) { + viewedmove++; + redraw = true; + } + } else if (c == KEY_HOME) { + viewedmove = 0; + redraw = true; + } else if (c == KEY_END) { + viewedmove = gamestate->movecount; + redraw = true; + } else if (c == KEY_PPAGE) { + if (viewedmove > page_moves) { + viewedmove -= page_moves; + } else { + viewedmove = 0; + } + redraw = true; + } else if (c == KEY_NPAGE) { + viewedmove += page_moves; + if (viewedmove > gamestate->movecount) { + viewedmove = gamestate->movecount; + } + redraw = true; + } + } while (c != 'q'); + echo(); + gamestate_cleanup(&viewedstate); +} + +static void game_play_singlemachine(Settings *settings) { + inputy = getmaxy(stdscr) - 6; + + GameState gamestate; + gamestate_init(&gamestate); + uint8_t curcol = WHITE; + + if (settings->continuepgn) { + FILE *pgnfile = fopen(settings->continuepgn, "r"); + if (pgnfile) { + int result = read_pgn(pgnfile, &gamestate, &(settings->gameinfo)); + long position = ftell(pgnfile); + fclose(pgnfile); + if (result) { + printw("Invalid PGN file content at position %ld:\n%s\n", + position, pgn_error_str(result)); + return; + } + if (!is_game_running(&gamestate)) { + addstr("Game has ended. Use -S to analyze it.\n"); + return; + } + curcol = opponent_color(last_move(&gamestate).piece&COLOR_MASK); + } else { + printw("Can't read PGN file (%s)\n", strerror(errno)); + return; + } + } + + bool running; + do { + clear(); + uint8_t perspective = settings->disableflip ? WHITE : curcol; + draw_board(&gamestate, perspective, settings->unicode); + running = !domove_singlemachine(&gamestate, + &(settings->gameinfo), curcol); + curcol = opponent_color(curcol); + } while (running); + + game_review(settings, &gamestate); + gamestate_cleanup(&gamestate); +} + +static void game_play(Settings *settings, GameState *gamestate, int opponent) { + inputy = getmaxy(stdscr) - 6; + + uint8_t mycolor = settings->gameinfo.servercolor; + if (!settings->ishost) { + mycolor = opponent_color(mycolor); + } + + bool myturn = (gamestate->movecount > 0 ? + (last_move(gamestate).piece & COLOR_MASK) : BLACK) != mycolor; + + bool running; + do { + clear(); + draw_board(gamestate, mycolor, settings->unicode); + if (myturn) { + running = !sendmove(gamestate, &(settings->gameinfo), + opponent, mycolor); + } else { + running = !recvmove(gamestate, &(settings->gameinfo), + opponent, mycolor); + } + myturn ^= true; + } while (running); +} + +static void dump_gameinfo(GameInfo *gameinfo) { + int serverwhite = gameinfo->servercolor == WHITE; + attron(A_UNDERLINE); + printw("Game details\n"); + attroff(A_UNDERLINE); + printw(" Server: %s\n Client: %s\n", + serverwhite?"White":"Black", serverwhite?"Black":"White" + ); + if (gameinfo->timecontrol) { + if (gameinfo->time % 60) { + printw(" Time limit: %ds + %ds\n", + gameinfo->time, gameinfo->addtime); + } else { + printw(" Time limit: %dm + %ds\n", + gameinfo->time/60, gameinfo->addtime); + } + } else { + printw(" No time limit\n"); + } + refresh(); +} + +static void dump_moveinfo(GameState *gamestate) { + for (unsigned i = 0 ; i < gamestate->movecount ; i++) { + if (i % 2 == 0) { + printw("%d. %s", 1+i/2, gamestate->moves[i].string); + } else { + printw("%s", gamestate->moves[i].string); + } + // only five moves reliably fit into one screen row + if ((i+1) % 10) { + addch(' '); + } else { + addch('\n'); + } + } + refresh(); +} + +static int server_fd = -1; +static void interrupt_listen(int sig) { + if (server_fd > -1) { + // this interrupts + close(server_fd); + } +} + +static int server_open(Server *server, Settings *settings) { + printw("\nListening for client...\n"); + refresh(); + if (settings->usedomainsocket + ? net_create_sock(server, settings->serverhost) + : net_create_tcp(server, settings->port)) { + addstr("Server creation failed"); + return 1; + } + + // allow Ctrl+C to interrupt the listening process + server_fd = server->fd; + signal(SIGINT, interrupt_listen); + + if (net_listen(server)) { + addstr("Listening for client failed or interrupted"); + return 1; + } + + // restore default action + signal(SIGINT, SIG_DFL); + + return 0; +} + +static int server_handshake(Client *client) { + net_send_code(client->fd, NETCODE_VERSION); + if (net_recieve_code(client->fd) != NETCODE_VERSION) { + addstr("Client uses an incompatible software version."); + return 1; + } + + addstr("Client connected - transmitting gameinfo..."); + refresh(); + + return 0; +} + +static int server_run(Settings *settings) { + Server server; + + dump_gameinfo(&(settings->gameinfo)); + GameState gamestate; + gamestate_init(&gamestate); + if (settings->continuepgn) { + /* preload PGN data before handshake */ + FILE *pgnfile = fopen(settings->continuepgn, "r"); + if (pgnfile) { + int result = read_pgn(pgnfile, &gamestate, + &(settings->gameinfo)); + long position = ftell(pgnfile); + fclose(pgnfile); + if (result) { + printw("Invalid PGN file content at position %ld:\n%s\n", + position, pgn_error_str(result)); + return 1; + } + if (!is_game_running(&gamestate)) { + addstr("Game has ended. Use -s to analyze it locally.\n"); + return 1; + } + addch('\n'); + dump_moveinfo(&gamestate); + addch('\n'); + } else { + printw("Can't read PGN file (%s)\n", strerror(errno)); + return 1; + } + } + + if (server_open(&server, settings)) { + net_destroy(&server); + return 1; + } + + if (server_handshake(server.client)) { + net_destroy(&server); + return 1; + } + + int fd = server.client->fd; + if (settings->continuepgn) { + /* Continue game, send PGN data */ + uint16_t mc = gamestate.movecount; + size_t pgndata_size = sizeof(GameInfo)+sizeof(mc)+mc*sizeof(Move); + char *pgndata = malloc(pgndata_size); + memcpy(pgndata, &(settings->gameinfo), sizeof(GameInfo)); + unsigned offset = sizeof(GameInfo); + memcpy(pgndata+offset, &mc, sizeof(mc)); + offset += sizeof(mc); + memcpy(pgndata+offset, gamestate.moves, mc*sizeof(Move)); + net_send_data(fd, NETCODE_PGNDATA, pgndata, pgndata_size); + free(pgndata); + } else { + /* Start new game */ + net_send_data(fd, NETCODE_GAMEINFO, + &(settings->gameinfo), sizeof(GameInfo)); + } + addstr("\rClient connected - awaiting challenge acceptance..."); + refresh(); + int code = net_recieve_code(fd); + int exitcode = 0; + if (code == NETCODE_ACCEPT) { + addstr("\rClient connected - challenge accepted."); + clrtoeol(); + game_play(settings, &gamestate, fd); + net_destroy(&server); + game_review(settings, &gamestate); + } else if (code == NETCODE_DECLINE) { + addstr("\rClient connected - challenge declined."); + clrtoeol(); + net_destroy(&server); + } else if (code == NETCODE_CONNLOST) { + addstr("\rClient connected - but gave no response."); + clrtoeol(); + net_destroy(&server); + } else { + addstr("\rInvalid client response"); + clrtoeol(); + + net_destroy(&server); + exitcode = 1; + } + gamestate_cleanup(&gamestate); + return exitcode; +} + + +static int client_connect(Server *server, Settings *settings) { + if (settings->usedomainsocket + ? net_find_sock(server, settings->serverhost) + : net_find_tcp(server, settings->serverhost, settings->port)) { + addstr("Can't find server"); + return 1; + } + + if (net_connect(server)) { + addstr("Can't connect to server"); + return 1; + } + + return 0; +} + +static int client_handshake(Server *server) { + if (net_recieve_code(server->fd) != NETCODE_VERSION) { + addstr("Server uses an incompatible software version."); + return 1; + } else { + net_send_code(server->fd, NETCODE_VERSION); + } + + printw("Connection established!\n\n"); + refresh(); + + return 0; +} + +static int client_run(Settings *settings) { + Server server; + + if (client_connect(&server, settings)) { + net_destroy(&server); + return 1; + } + + if (client_handshake(&server)) { + net_destroy(&server); + return 1; + } + + uint8_t code = net_recieve_code(server.fd); + GameState gamestate; + gamestate_init(&gamestate); + bool played = false; + if (code == NETCODE_GAMEINFO) { + /* Start new game */ + net_recieve_data(server.fd, &(settings->gameinfo), sizeof(GameInfo)); + dump_gameinfo(&(settings->gameinfo)); + if (prompt_yesno("Accept challenge")) { + net_send_code(server.fd, NETCODE_ACCEPT); + game_play(settings, &gamestate, server.fd); + played = true; + } else { + net_send_code(server.fd, NETCODE_DECLINE); + } + } else if (code == NETCODE_PGNDATA) { + net_recieve_data(server.fd, &(settings->gameinfo), sizeof(GameInfo)); + dump_gameinfo(&(settings->gameinfo)); + uint16_t mc; + net_recieve_data(server.fd, &mc, sizeof(mc)); + Move *moves = calloc(mc, sizeof(Move)); + net_recieve_data(server.fd, moves, mc*sizeof(Move)); + for (size_t i = 0 ; i < mc ; i++) { + apply_move(&gamestate, &(moves[i])); + } + free(moves); + addch('\n'); + dump_moveinfo(&gamestate); + if (prompt_yesno( + "\n\nServer wants to continue a game. Accept challenge")) { + net_send_code(server.fd, NETCODE_ACCEPT); + game_play(settings, &gamestate, server.fd); + played = true; + } else { + net_send_code(server.fd, NETCODE_DECLINE); + } + } else { + addstr("Server sent invalid gameinfo."); + net_destroy(&server); + return 1; + } + + if (played) { + game_review(settings, &gamestate); + } + gamestate_cleanup(&gamestate); + + net_destroy(&server); + return 0; +} + int main(int argc, char **argv) { srand(time(NULL)); diff -r ce38ee9bc3af -r 189c7c77aaab src/server.c --- 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 -#include -#include -#include -#include -#include - -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; -}