fen: various code improvements, initial code for PGN output (disabled for now) - chess-puzzles - chess puzzle book generator
 (HTM) git clone git://git.codemadness.org/chess-puzzles
 (DIR) Log
 (DIR) Files
 (DIR) Refs
 (DIR) README
 (DIR) LICENSE
       ---
 (DIR) commit e536a441d52c33adea06f8ed451bd890a3ebdb9d
 (DIR) parent 4f6db6f748b3760f1f991e46bbb37246d201d4c8
 (HTM) Author: Hiltjo Posthuma <hiltjo@codemadness.org>
       Date:   Sat, 23 Dec 2023 18:59:46 +0100
       
       fen: various code improvements, initial code for PGN output (disabled for now)
       
       - Reduce SVG output size by reusing the draw paths for pawns (saves ~4KB per
         image on average). Of course this depends. On a board with no pawns it can be
         ~300 bytes larger.
       - Remove Board FEN header for tty and ASCII outputs.
       - Make the highlight function more generic (for one move).
       - Improve outputs parsing, an invalid option now shows the usage().
       - Rename the output functions to output_<outputname>().
       - Add initial code for PGN output, needs some work, disabled for now.
       
       ... work in progress
       
       Diffstat:
         M fen.1                               |       4 +++-
         M fen.c                               |     289 +++++++++++++++++++++++++------
         M tests.sh                            |      53 ++++++++++++++++++++++++++++++
       
       3 files changed, 294 insertions(+), 52 deletions(-)
       ---
 (DIR) diff --git a/fen.1 b/fen.1
       @@ -7,7 +7,7 @@
        .Sh SYNOPSIS
        .Nm
        .Op Fl cCfF
       -.Op Fl o Ar ascii | fen | svg | tty
       +.Op Fl o Ar ascii | fen | pgn | svg | tty
        .Op Ar FEN
        .Op Ar moves
        .Sh DESCRIPTION
       @@ -32,6 +32,8 @@ ASCII text representation of the board.
        FEN of the board state after playing the moves.
        .It fen
        FEN of the board state after playing the moves.
       +.It pgn
       +PGN output of the moves for the board.
        .It svg
        SVG image of the board.
        .It tty
 (DIR) diff --git a/fen.c b/fen.c
       @@ -1,4 +1,5 @@
        #include <ctype.h>
       +#include <stdarg.h>
        #include <stdio.h>
        #include <stdlib.h>
        #include <string.h>
       @@ -12,6 +13,9 @@
        #define SETFGCOLOR(r,g,b)    printf("\x1b[38;2;%d;%d;%dm", r, g, b)
        #define SETBGCOLOR(r,g,b)    printf("\x1b[48;2;%d;%d;%dm", r, g, b)
        
       +enum outputmode { ModeASCII = 0, ModeFEN, ModePGN, ModeTTY, ModeSVG };
       +enum outputmode outputmode = ModeSVG;
       +
        static char board[8][8];
        static char highlight[8][8];
        
       @@ -32,6 +36,19 @@ static const int lightsquarehi[] = { 0xcd, 0xd2, 0x6a };
        static int showcoords = 1; /* config: show board coordinates? */
        static int flipboard = 0; /* config: flip board ? */
        
       +void
       +pgn(const char *fmt, ...)
       +{
       +        va_list ap;
       +
       +        if (outputmode != ModePGN)
       +                return;
       +
       +        va_start(ap, fmt);
       +        vprintf(fmt, ap);
       +        va_end(ap);
       +}
       +
        int
        isvalidsquare(int x, int y)
        {
       @@ -92,13 +109,10 @@ squaretoxy(const char *s, int *x, int *y)
        }
        
        void
       -highlightmove(int x1, int y1, int x2, int y2)
       +highlightmove(int x, int y)
        {
       -        if (isvalidsquare(x1, y1))
       -                highlight[y1][x1] = 1;
       -
       -        if (isvalidsquare(x2, y2))
       -                highlight[y2][x2] = 1;
       +        if (isvalidsquare(x, y))
       +                highlight[y][x] = 1;
        }
        
        void
       @@ -144,7 +158,7 @@ showboardfen(void)
                } else {
                        putchar('-');
                }
       -        printf(" %d %d", halfmove, movenumber);
       +        printf(" %d %d\n", halfmove, movenumber);
        }
        
        void
       @@ -160,13 +174,13 @@ showpiece_svg(int c)
                case 'R': s = "<g fill=\"#fff\" fill-rule=\"evenodd\" stroke=\"#000\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M9 39h27v-3H9v3zm3-3v-4h21v4H12zm-1-22V9h4v2h5V9h5v2h5V9h4v5\" stroke-linecap=\"butt\"/><path d=\"M34 14l-3 3H14l-3-3\"/><path d=\"M31 17v12.5H14V17\" stroke-linecap=\"butt\" stroke-linejoin=\"miter\"/><path d=\"M31 29.5l1.5 2.5h-20l1.5-2.5\"/><path d=\"M11 14h23\" fill=\"none\" stroke-linejoin=\"miter\"/></g>"; break;
                case 'B': s = "<g fill=\"none\" fill-rule=\"evenodd\" stroke=\"#000\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><g fill=\"#fff\" stroke-linecap=\"butt\"><path d=\"M9 36c3.39-.97 10.11.43 13.5-2 3.39 2.43 10.11 1.03 13.5 2 0 0 1.65.54 3 2-.68.97-1.65.99-3 .5-3.39-.97-10.11.46-13.5-1-3.39 1.46-10.11.03-13.5 1-1.354.49-2.323.47-3-.5 1.354-1.94 3-2 3-2z\"/><path d=\"M15 32c2.5 2.5 12.5 2.5 15 0 .5-1.5 0-2 0-2 0-2.5-2.5-4-2.5-4 5.5-1.5 6-11.5-5-15.5-11 4-10.5 14-5 15.5 0 0-2.5 1.5-2.5 4 0 0-.5.5 0 2z\"/><path d=\"M25 8a2.5 2.5 0 1 1-5 0 2.5 2.5 0 1 1 5 0z\"/></g><path d=\"M17.5 26h10M15 30h15m-7.5-14.5v5M20 18h5\" stroke-linejoin=\"miter\"/></g>"; break;
                case 'N': s = "<g fill=\"none\" fill-rule=\"evenodd\" stroke=\"#000\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M22 10c10.5 1 16.5 8 16 29H15c0-9 10-6.5 8-21\" fill=\"#fff\"/><path d=\"M24 18c.38 2.91-5.55 7.37-8 9-3 2-2.82 4.34-5 4-1.042-.94 1.41-3.04 0-3-1 0 .19 1.23-1 2-1 0-4.003 1-4-4 0-2 6-12 6-12s1.89-1.9 2-3.5c-.73-.994-.5-2-.5-3 1-1 3 2.5 3 2.5h2s.78-1.992 2.5-3c1 0 1 3 1 3\" fill=\"#fff\"/><path d=\"M9.5 25.5a.5.5 0 1 1-1 0 .5.5 0 1 1 1 0zm5.433-9.75a.5 1.5 30 1 1-.866-.5.5 1.5 30 1 1 .866.5z\" fill=\"#000\"/></g>"; break;
       -        case 'P': s = "<path d=\"M22.5 9c-2.21 0-4 1.79-4 4 0 .89.29 1.71.78 2.38C17.33 16.5 16 18.59 16 21c0 2.03.94 3.84 2.41 5.03-3 1.06-7.41 5.55-7.41 13.47h23c0-7.92-4.41-12.41-7.41-13.47 1.47-1.19 2.41-3 2.41-5.03 0-2.41-1.33-4.5-3.28-5.62.49-.67.78-1.49.78-2.38 0-2.21-1.79-4-4-4z\" fill=\"#fff\" stroke=\"#000\" stroke-width=\"1.5\" stroke-linecap=\"round\"/>"; break;
       +        case 'P': s = "<use href=\"#pawn\" fill=\"#fff\"/>"; break;
                case 'k': s = "<g fill=\"none\" fill-rule=\"evenodd\" stroke=\"#000\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M22.5 11.63V6\" stroke-linejoin=\"miter\"/><path d=\"M22.5 25s4.5-7.5 3-10.5c0 0-1-2.5-3-2.5s-3 2.5-3 2.5c-1.5 3 3 10.5 3 10.5\" fill=\"#000\" stroke-linecap=\"butt\" stroke-linejoin=\"miter\"/><path d=\"M11.5 37c5.5 3.5 15.5 3.5 21 0v-7s9-4.5 6-10.5c-4-6.5-13.5-3.5-16 4V27v-3.5c-3.5-7.5-13-10.5-16-4-3 6 5 10 5 10V37z\" fill=\"#000\"/><path d=\"M20 8h5\" stroke-linejoin=\"miter\"/><path d=\"M32 29.5s8.5-4 6.03-9.65C34.15 14 25 18 22.5 24.5l.01 2.1-.01-2.1C20 18 9.906 14 6.997 19.85c-2.497 5.65 4.853 9 4.853 9\" stroke=\"#ececec\"/><path d=\"M11.5 30c5.5-3 15.5-3 21 0m-21 3.5c5.5-3 15.5-3 21 0m-21 3.5c5.5-3 15.5-3 21 0\" stroke=\"#ececec\"/></g>"; break;
                case 'q': s = "<g fill-rule=\"evenodd\" stroke=\"#000\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><g stroke=\"none\"><circle cx=\"6\" cy=\"12\" r=\"2.75\"/><circle cx=\"14\" cy=\"9\" r=\"2.75\"/><circle cx=\"22.5\" cy=\"8\" r=\"2.75\"/><circle cx=\"31\" cy=\"9\" r=\"2.75\"/><circle cx=\"39\" cy=\"12\" r=\"2.75\"/></g><path d=\"M9 26c8.5-1.5 21-1.5 27 0l2.5-12.5L31 25l-.3-14.1-5.2 13.6-3-14.5-3 14.5-5.2-13.6L14 25 6.5 13.5 9 26z\" stroke-linecap=\"butt\"/><path d=\"M9 26c0 2 1.5 2 2.5 4 1 1.5 1 1 .5 3.5-1.5 1-1.5 2.5-1.5 2.5-1.5 1.5.5 2.5.5 2.5 6.5 1 16.5 1 23 0 0 0 1.5-1 0-2.5 0 0 .5-1.5-1-2.5-.5-2.5-.5-2 .5-3.5 1-2 2.5-2 2.5-4-8.5-1.5-18.5-1.5-27 0z\" stroke-linecap=\"butt\"/><path d=\"M11 38.5a35 35 1 0 0 23 0\" fill=\"none\" stroke-linecap=\"butt\"/><path d=\"M11 29a35 35 1 0 1 23 0m-21.5 2.5h20m-21 3a35 35 1 0 0 22 0m-23 3a35 35 1 0 0 24 0\" fill=\"none\" stroke=\"#ececec\"/></g>"; break;
                case 'r': s = "<g fill-rule=\"evenodd\" stroke=\"#000\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M9 39h27v-3H9v3zm3.5-7l1.5-2.5h17l1.5 2.5h-20zm-.5 4v-4h21v4H12z\" stroke-linecap=\"butt\"/><path d=\"M14 29.5v-13h17v13H14z\" stroke-linecap=\"butt\" stroke-linejoin=\"miter\"/><path d=\"M14 16.5L11 14h23l-3 2.5H14zM11 14V9h4v2h5V9h5v2h5V9h4v5H11z\" stroke-linecap=\"butt\"/><path d=\"M12 35.5h21m-20-4h19m-18-2h17m-17-13h17M11 14h23\" fill=\"none\" stroke=\"#ececec\" stroke-width=\"1\" stroke-linejoin=\"miter\"/></g>"; break;
                case 'b': s = "<g fill=\"none\" fill-rule=\"evenodd\" stroke=\"#000\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><g fill=\"#000\" stroke-linecap=\"butt\"><path d=\"M9 36c3.39-.97 10.11.43 13.5-2 3.39 2.43 10.11 1.03 13.5 2 0 0 1.65.54 3 2-.68.97-1.65.99-3 .5-3.39-.97-10.11.46-13.5-1-3.39 1.46-10.11.03-13.5 1-1.354.49-2.323.47-3-.5 1.354-1.94 3-2 3-2z\"/><path d=\"M15 32c2.5 2.5 12.5 2.5 15 0 .5-1.5 0-2 0-2 0-2.5-2.5-4-2.5-4 5.5-1.5 6-11.5-5-15.5-11 4-10.5 14-5 15.5 0 0-2.5 1.5-2.5 4 0 0-.5.5 0 2z\"/><path d=\"M25 8a2.5 2.5 0 1 1-5 0 2.5 2.5 0 1 1 5 0z\"/></g><path d=\"M17.5 26h10M15 30h15m-7.5-14.5v5M20 18h5\" stroke=\"#ececec\" stroke-linejoin=\"miter\"/></g>"; break;
                case 'n': s = "<g fill=\"none\" fill-rule=\"evenodd\" stroke=\"#000\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M22 10c10.5 1 16.5 8 16 29H15c0-9 10-6.5 8-21\" fill=\"#000\"/><path d=\"M24 18c.38 2.91-5.55 7.37-8 9-3 2-2.82 4.34-5 4-1.042-.94 1.41-3.04 0-3-1 0 .19 1.23-1 2-1 0-4.003 1-4-4 0-2 6-12 6-12s1.89-1.9 2-3.5c-.73-.994-.5-2-.5-3 1-1 3 2.5 3 2.5h2s.78-1.992 2.5-3c1 0 1 3 1 3\" fill=\"#000\"/><path d=\"M9.5 25.5a.5.5 0 1 1-1 0 .5.5 0 1 1 1 0zm5.433-9.75a.5 1.5 30 1 1-.866-.5.5 1.5 30 1 1 .866.5z\" fill=\"#ececec\" stroke=\"#ececec\"/><path d=\"M24.55 10.4l-.45 1.45.5.15c3.15 1 5.65 2.49 7.9 6.75S35.75 29.06 35.25 39l-.05.5h2.25l.05-.5c.5-10.06-.88-16.85-3.25-21.34-2.37-4.49-5.79-6.64-9.19-7.16l-.51-.1z\" fill=\"#ececec\" stroke=\"none\"/></g>"; break;
       -        case 'p': s = "<path d=\"M22.5 9c-2.21 0-4 1.79-4 4 0 .89.29 1.71.78 2.38C17.33 16.5 16 18.59 16 21c0 2.03.94 3.84 2.41 5.03-3 1.06-7.41 5.55-7.41 13.47h23c0-7.92-4.41-12.41-7.41-13.47 1.47-1.19 2.41-3 2.41-5.03 0-2.41-1.33-4.5-3.28-5.62.49-.67.78-1.49.78-2.38 0-2.21-1.79-4-4-4z\" stroke=\"#000\" stroke-width=\"1.5\" stroke-linecap=\"round\"/>"; break;
       +        case 'p': s = "<use href=\"#pawn\" fill=\"#000\"/>"; break;
                }
        
                if (*s)
       @@ -174,7 +188,7 @@ showpiece_svg(int c)
        }
        
        void
       -showboard_svg(void)
       +output_svg(void)
        {
                const int *color;
                int ix, iy, x, y, piece;
       @@ -184,9 +198,9 @@ showboard_svg(void)
                        "<svg width=\"360\" height=\"360\" viewBox=\"0 0 360 360\" xmlns=\"http://www.w3.org/2000/svg\">\n"
                        "<rect fill=\"#fff\" stroke=\"#000\" x=\"0\" y=\"0\" width=\"360\" height=\"360\"/>\n", stdout);
        
       -        fputs("<!-- Board FEN: ", stdout);
       -        showboardfen();
       -        fputs(" -->\n", stdout);
       +        fputs("<defs>\n", stdout);
       +        fputs("<path id=\"pawn\" d=\"M22.5 9c-2.21 0-4 1.79-4 4 0 .89.29 1.71.78 2.38C17.33 16.5 16 18.59 16 21c0 2.03.94 3.84 2.41 5.03-3 1.06-7.41 5.55-7.41 13.47h23c0-7.92-4.41-12.41-7.41-13.47 1.47-1.19 2.41-3 2.41-5.03 0-2.41-1.33-4.5-3.28-5.62.49-.67.78-1.49.78-2.38 0-2.21-1.79-4-4-4z\" stroke=\"#000\" stroke-width=\"1.5\" stroke-linecap=\"round\"/>", stdout);
       +        fputs("</defs>\n", stdout);
        
                for (iy = 0; iy < 8; iy++) {
                        y = flipboard ? 7 - iy : iy;
       @@ -284,15 +298,11 @@ showpiece_tty(int c)
        
        /* show board */
        void
       -showboard_tty(void)
       +output_tty(void)
        {
                const int *color;
                int ix, iy, x, y, piece;
        
       -        printf("Board FEN:\n");
       -        showboardfen();
       -        printf("\n\n");
       -
                SETBGCOLOR(border[0], border[1], border[2]);
                fputs("                            ", stdout);
                printf("\x1b[0m"); /* reset */
       @@ -367,25 +377,20 @@ showpiece_ascii(int c)
        
        /* OnlyFENs */
        void
       -showboard_fen(void)
       +output_fen(void)
        {
                showboardfen();
       -        printf("\n");
        }
        
        /* show board */
        void
       -showboard_ascii(void)
       +output_ascii(void)
        {
                int hi[3] = { '>', ' ', '<' };
                int dark[3] = { '.', '.', '.' };
                int light[3] = { ' ', ' ', ' ' };
                int *color, ix, iy, x, y, piece;
        
       -        printf("Board FEN:\n");
       -        showboardfen();
       -        printf("\n\n");
       -
                for (iy = 0; iy < 8; iy++) {
                        y = flipboard ? 7 - iy : iy;
        
       @@ -426,6 +431,124 @@ showboard_ascii(void)
                fputs("\n", stdout);
        }
        
       +int
       +findking(int side, int *kingx, int *kingy)
       +{
       +        int king, x, y;
       +
       +        king = side == 'w' ? 'K' : 'k';
       +        *kingx = -1;
       +        *kingy = -1;
       +
       +        /* find king */
       +        for (y = 0; y < 8; y++) {
       +                for (x = 0; x < 8; x++) {
       +                        if (getpiece(x, y) == king) {
       +                                *kingx = x;
       +                                *kingy = y;
       +                                return 1;
       +                        }
       +                }
       +        }
       +        return 0;
       +}
       +
       +int
       +ischecked(int side)
       +{
       +        int diag[]   = { -1, -1,  1, 1, -1, 1, 1, -1 };
       +        int line[]   = {  1,  0,  0, 1, -1, 0, 0, -1 };
       +        int knight[] = { -1, -2,  1, -2, -1, 2, 1, 2,
       +                         -2, -1,  2, -1, -2, 1, 2, 1
       +                       };
       +        int i, j, x, y;
       +        int kingx, kingy;
       +        int piece;
       +
       +        /* find our king */
       +        if (!findking(side, &kingx, &kingy))
       +                return 0; /* should not happen */
       +
       +        /* check files and ranks (for queen and rook) */
       +        for (j = 0; j < 8; j += 2) {
       +                for (i = 1; i < 8; i ++) {
       +                        x = kingx + (i * line[j]);
       +                        y = kingy + (i * line[j + 1]);
       +                        if (!(piece = getpiece(x, y)))
       +                                continue;
       +                        /* a piece is in front of it */
       +                        if (piece && strchr("bBnNpP", piece))
       +                                break;
       +                        /* own piece blocking/defending it */
       +                        if ((side == 'w' && iswhitepiece(piece)) ||
       +                            (side == 'b' && isblackpiece(piece)))
       +                                break;
       +                        return 1;
       +                }
       +        }
       +
       +        /* check diagonals (queen and bishop) */
       +        for (j = 0; j < 8; j += 2) {
       +                for (i = 1; i < 8; i ++) {
       +                        x = kingx + (i * diag[j]);
       +                        y = kingy + (i * diag[j + 1]);
       +                        if (!(piece = getpiece(x, y)))
       +                                continue;
       +                        /* a piece is in front of it */
       +                        if (piece && strchr("rRnNpP", piece))
       +                                break;
       +                        /* own piece blocking/defending it */
       +                        if ((side == 'w' && iswhitepiece(piece)) ||
       +                            (side == 'b' && isblackpiece(piece)))
       +                                break;
       +                        return 1;
       +                }
       +        }
       +
       +        /* check knights */
       +        piece = side == 'w' ? 'n' : 'N';
       +        for (j = 0; j < 16; j += 2) {
       +                x = kingx + knight[j];
       +                y = kingy + knight[j + 1];
       +//                highlightmove(x, y); /* DEBUG */
       +                if (getpiece(x, y) == piece)
       +                        return 1;
       +        }
       +
       +        /* check pawns */
       +        if (side == 'w') {
       +                if (getpiece(kingx - 1, kingy - 1) == 'p' ||
       +                    getpiece(kingx + 1, kingy - 1) == 'p')
       +                        return 1;
       +        } else if (side == 'b') {
       +                if (getpiece(kingx - 1, kingy + 1) == 'P' ||
       +                    getpiece(kingx + 1, kingy + 1) == 'P')
       +                        return 1;
       +        }
       +
       +        return 0;
       +}
       +
       +int
       +ischeckmated(int side)
       +{
       +        int kingx, kingy;
       +
       +        // TODO: can king move out check, without being checked?
       +
       +        if (!ischecked(side))
       +                return 0;
       +
       +        /* find our king */
       +        if (!findking(side, &kingx, &kingy))
       +                return 0; /* should not happen */
       +
       +        // TODO: can the king move? move it and check if it is not checked
       +        // TODO: separate board state or just move and undo move?
       +
       +        return 0; // TODO
       +}
       +
        void
        parsefen(const char *fen)
        {
       @@ -528,12 +651,13 @@ void
        parsemoves(const char *moves)
        {
                char square[3];
       -        const char *s;
       -        int i, x, y, x2, y2, piece, takepiece;
       +        const char *castled, *s;
       +        int firstmove, i, x, y, x2, y2, piece, takepiece, tookpiece;
        
                /* process moves */
                square[2] = '\0';
                x = y = x2 = y2 = -1;
       +        firstmove = 1;
        
                for (s = moves; *s; s++) {
                        if (*s == ' ')
       @@ -542,6 +666,14 @@ parsemoves(const char *moves)
                            (*(s + 1) >= '1' && *(s + 1) <= '8') &&
                            (*(s + 2) >= 'a' && *(s + 2) <= 'h') &&
                            (*(s + 3) >= '1' && *(s + 3) <= '8')) {
       +                        /* TODO: if first move but its blacks turn, prefix PGN
       +                           with "..." or something, since the white move was unknown */
       +
       +                        if (firstmove)
       +                                firstmove = 0;
       +                        else
       +                                pgn(" ");
       +
                                square[0] = *s;
                                square[1] = *(s + 1);
        
       @@ -561,14 +693,22 @@ parsemoves(const char *moves)
        
                                s += 2;
        
       +                        /* took piece of opponent */
       +                        tookpiece = (side_to_move == 'w' && isblackpiece(takepiece)) ||
       +                                    (side_to_move == 'b' && iswhitepiece(takepiece));
       +
                                /* if pawn move or taken a piece increase halfmove counter */
       -                        if (piece == 'p' || piece == 'P' ||
       -                            (side_to_move == 'w' && isblackpiece(takepiece)) ||
       -                            (side_to_move == 'b' && iswhitepiece(takepiece)))
       +                        if (piece == 'p' || piece == 'P' || tookpiece)
                                        halfmove = 0;
                                else
                                        halfmove++;
        
       +                        if (side_to_move == 'w')
       +                                pgn("%d. ", movenumber);
       +
       +                        /* castled this move? for PGN */
       +                        castled = NULL;
       +
                                /* castling */
                                if (piece == 'K' && y == 7 && y2 == 7) {
                                        /* white: kingside castling */
       @@ -577,6 +717,7 @@ parsemoves(const char *moves)
                                                        if (getpiece(i, y2) == 'R') {
                                                                place(0, i, y2); /* clear rook square */
                                                                place('R', x2 - 1, y2); /* next to king */
       +                                                        castled = "O-O";
                                                                break;
                                                        }
                                                }
       @@ -586,6 +727,7 @@ parsemoves(const char *moves)
                                                        if (getpiece(i, y2) == 'R') {
                                                                place('R', x2 + 1, y2); /* next to king */
                                                                place(0, i, y2); /* clear rook square */
       +                                                        castled = "O-O-O";
                                                                break;
                                                        }
                                                }
       @@ -597,6 +739,7 @@ parsemoves(const char *moves)
                                                        if (getpiece(i, y2) == 'r') {
                                                                place(0, i, y2); /* clear rook square */
                                                                place('r', x2 - 1, y2); /* next to king */
       +                                                        castled = "O-O";
                                                                break;
                                                        }
                                                }
       @@ -606,6 +749,7 @@ parsemoves(const char *moves)
                                                        if (getpiece(i, y2) == 'r') {
                                                                place('r', x2 + 1, y2); /* next to king */
                                                                place(0, i, y2); /* clear rook square */
       +                                                        castled = "O-O-O";
                                                                break;
                                                        }
                                                }
       @@ -664,15 +808,39 @@ parsemoves(const char *moves)
                                /* place piece */
                                place(piece, x2, y2);
        
       -                        /* possible promotion: queen, knight, bishop */
       -                        if (*s == 'q' || *s == 'n' || *s == 'b') {
       -                                if (side_to_move == 'w')
       -                                        piece = toupper((unsigned char)*s);
       -                                else
       -                                        piece = *s;
       -                                place(piece, x2, y2);
       -                                s++;
       +                        /* PGN for move */
       +                        if (castled) {
       +                                pgn("%s", castled);
       +                        } else {
       +                                /* pawn move needs no notation */
       +                                if (piece != 'p' && piece != 'P') {
       +                                        pgn("%c", toupper(piece));
       +                                        /* TODO: check ambiguity for certain pieces and make the notation shorter */
       +                                        pgn("%c%c", 'a' + x, '8' - y);
       +                                }
       +                                if (tookpiece) {
       +                                        /* pawn captures are prefixed by the file letter */
       +                                        if (piece == 'p' || piece == 'P')
       +                                                pgn("%c", 'a' + x);
       +                                        pgn("x");
       +                                }
       +                                pgn("%c%c", 'a' + x2, '8' - y2);
       +
       +                                /* possible promotion: queen, knight, bishop */
       +                                if (*s == 'q' || *s == 'n' || *s == 'b') {
       +                                        if (side_to_move == 'w')
       +                                                piece = toupper((unsigned char)*s);
       +                                        else
       +                                                piece = *s;
       +                                        place(piece, x2, y2);
       +                                        s++;
       +                                        pgn("=%c", toupper(piece));
       +                                }
                                }
       +                        if (ischeckmated(side_to_move == 'b' ? 'w' : 'b'))
       +                                pgn("#");
       +                        else if (ischecked(side_to_move == 'b' ? 'w' : 'b'))
       +                                pgn("+");
        
                                /* a move by black increases the move number */
                                if (side_to_move == 'b')
       @@ -682,14 +850,21 @@ parsemoves(const char *moves)
                                side_to_move = side_to_move == 'b' ? 'w' : 'b';
                        }
                }
       +        pgn("\n");
       +
       +//        // DEBUG
       +//        ischecked('w');
       +//        ischecked('b');
        
                /* highlight last move */
       -        highlightmove(x, y, x2, y2);
       +        highlightmove(x, y);
       +        highlightmove(x2, y2);
        }
        
        void
        usage(char *argv0)
        {
       +//        fprintf(stderr, "usage: %s [-cCfF] [-o ascii|fen|pgn|svg|tty] [FEN] [moves]\n", argv0);
                fprintf(stderr, "usage: %s [-cCfF] [-o ascii|fen|svg|tty] [FEN] [moves]\n", argv0);
                exit(1);
        }
       @@ -697,7 +872,7 @@ usage(char *argv0)
        int
        main(int argc, char *argv[])
        {
       -        const char *fen, *moves, *output = "svg";
       +        const char *fen, *moves;
                int i, j;
        
        #ifdef __OpenBSD__
       @@ -721,7 +896,19 @@ main(int argc, char *argv[])
                                case 'o':
                                        if (i + 1 >= argc)
                                                usage(argv[0]);
       -                                output = argv[++i];
       +                                i++;
       +                                if (!strcmp(argv[i], "ascii"))
       +                                        outputmode = ModeASCII;
       +                                else if (!strcmp(argv[i], "fen"))
       +                                        outputmode = ModeFEN;
       +//                                else if (!strcmp(argv[i], "pgn"))
       +//                                        outputmode = ModePGN;
       +                                else if (!strcmp(argv[i], "svg"))
       +                                        outputmode = ModeSVG;
       +                                else if (!strcmp(argv[i], "tty"))
       +                                        outputmode = ModeTTY;
       +                                else
       +                                        usage(argv[0]);
                                        goto next;
                                default:
                                        usage(argv[0]);
       @@ -746,16 +933,16 @@ next:
                parsefen(fen);
                parsemoves(moves);
        
       -        if (!strcmp(output, "ascii"))
       -                showboard_ascii();
       -        else if (!strcmp(output, "fen"))
       -                showboard_fen();
       -        else if (!strcmp(output, "svg"))
       -                showboard_svg();
       -        else if (!strcmp(output, "tty"))
       -                showboard_tty();
       -        else
       -                usage(argv[0]);
       +//        outputmode = ModeTTY; /* DEBUG */
       +
       +        switch (outputmode) {
       +        case ModeASCII: output_ascii(); break;
       +        case ModeFEN:   output_fen();        break;
       +//        case ModePGN:                   break; /* handled in parsemoves() */
       +        case ModeSVG:   output_svg();   break;
       +        case ModeTTY:   output_tty();   break;
       +        default:        usage(argv[0]); break;
       +        }
        
                return 0;
        }
 (DIR) diff --git a/tests.sh b/tests.sh
       @@ -18,6 +18,25 @@ testfen() {
                fi
        }
        
       +# testpgn(name, expect, fen, moves)
       +testpgn() {
       +        name="$1"
       +        expect="$2"
       +        fen="$3"
       +        moves="$4"
       +
       +        output=$(./fen -o pgn "$fen" "$moves")
       +        if test "$output" = "$expect"; then
       +                printf 'OK: %s\n' "$name"
       +        else
       +                printf 'Fail: %s\n' "$name"
       +                printf '\texpected: %s\n' "$expect"
       +                printf '\tgot:      %s\n' "$output"
       +                printf '\tInput FEN, moves: "%s" "%s"\n' "$fen" "$moves"
       +        fi
       +}
       +
       +tests_fen() {
        testfen 'startpos'\
                'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1' \
                "startpos"\
       @@ -197,3 +216,37 @@ testfen '960, castle king on queenside with many empty squares between them'\
        
        # TODO: test more chess960 black kingside and queenside castling
        # TODO: test more long sequence and halfmove and movenumber counts
       +}
       +
       +tests_pgn() {
       +testpgn 'simple pawn move'\
       +        'e5'\
       +        'rnbqkbnr/ppp1pppp/8/3p4/3PP3/8/PPP2PPP/RNBQKBNR b KQkq - 0 2'\
       +        'e7e5'
       +
       +testpgn 'check: check with white pawn'\
       +        '4. gxf7+'\
       +        'rnbqkbnr/pppp1ppp/6P1/8/8/4p3/PPPPPP1P/RNBQKBNR w KQkq - 0 4'\
       +        'g6f7'
       +testpgn 'check: check with black pawn'\
       +        'exf2+'\
       +        'rnbqkbnr/pppp1ppP/8/8/8/4p3/PPPPPP1P/RNBQKBNR b KQkq - 0 4'\
       +        'e3f2'
       +
       +testpgn 'check: check with bishop'\
       +        '3. Bf1b5+'\
       +        'rnbqkbnr/ppp2ppp/8/3pp3/3PP3/8/PPP2PPP/RNBQKBNR w KQkq - 0 3'\
       +        'f1b5'
       +testpgn 'check: check with white queen, open file'\
       +        '6. Qd4e4+'\
       +        'rnbqkbnr/1ppp1pp1/p5Pp/8/3Q4/4P3/PPP1PP1P/RNB1KBNR w KQkq - 0 6'\
       +        'd4e4'
       +
       +testpgn 'check: check with white knight'\
       +        '8. Nd5xc7+'\
       +        'rnbqkbn1/pppp1ppr/6P1/3N4/7p/4PN2/PPP1PP1P/R1BQKB1R w KQq - 2 8'\
       +        'd5c7'
       +}
       +
       +tests_fen
       +#tests_pgn