various improvements and features - 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 1f750e11a33213399d0ae7f8817d52f35cc8b6b8
 (DIR) parent 0fe1f6ac19819dd3954b8520db31a66f76d7cf87
 (HTM) Author: Hiltjo Posthuma <hiltjo@codemadness.org>
       Date:   Sat,  6 Jan 2024 15:40:35 +0100
       
       various improvements and features
       
       ... do all the things and do them meh.
       
       - ambigous moves improvements and fixes.
       - add "speak" mode, describe a move in human-like language (dutch and english).
       - add -l option to only output the PGN or description of the last move, useful
         for speech engines.
       - FEN output: reset en passant square on checkmate (like Lichess does).
       - lichess stream script: add description of the move and use espeak to say it.
       - add a page of the puzzles intended for the tty (index.vt).
       - index.html improvements:
         - CSS tweaks to better align 2 puzzles in the center.
         - hyperlink color.
         - add simple plain-text listing in solutions.txt file.
         - add title and alt of the solutions moves in PGN and as a description.
       - tests:
         - add more of them.
         - return exitcode 0 or 1 on a failure.
         - prefix type of test: PGN or FEN.
       
       Diffstat:
         M TODO                                |      19 +++++++------------
         M docs/stream_lichess.sh              |      16 +++++++++++++++-
         M fen.1                               |      11 +++++++++--
         M fen.c                               |     376 ++++++++++++++++++++++---------
         M generate.sh                         |      97 ++++++++++++++++++++++---------
         M tests.sh                            |     185 +++++++++++++++++++++++++++++--
       
       6 files changed, 546 insertions(+), 158 deletions(-)
       ---
 (DIR) diff --git a/TODO b/TODO
       @@ -1,12 +1,7 @@
       -arbitrary test: en-passant defend against mate
       -https://lichess.org/editor/rnbqkbnr/pppppppp/8/1P1PP3/2PKQ3/2PQQ3/P1PP1PPP/RNBQ1BNR_w_HAkq_-_0_1?color=white
       -
       -
       -another with pawn removed: cannot defend because then we are in check
       -https://lichess.org/editor/rnbqkbnr/pppppppp/8/3PP3/2PKQ3/2PQQ3/P1PP1PPP/RNBQ1BNR_w_HAkq_-_0_1?color=white
       -
       -
       -
       +? pgnnotation make function?
       +? PGN output: add game termination state?
       +        - PGN output: add stalemate?
       +        - PGN output: but what if resign, time-out, draw offer?
        
        ? canpiecemove(): en passant take (if not in check afterwards).
        ? ischeckmated(): check en passant take to defend checkmate.
       @@ -23,11 +18,11 @@ after the moving piece's name (in that order of preference). Thus, Nge2
        specifies that the knight originally on the g-file moves to e2. "
        
        ? read PGN to moves?
       +        - input and output piece mapping?
        
       -x add a format" parameter for the CGI mode: vt, pgn, svg, ascii.
       -
       -- improve documentation.
        - more tests.
                - piece ambiguity.
                - en passant (in check), etc.
                - in check, checkmate.
       +        - test more chess960 black kingside and queenside castling
       +        - test more long sequence and halfmove and movenumber counts
 (DIR) diff --git a/docs/stream_lichess.sh b/docs/stream_lichess.sh
       @@ -28,6 +28,7 @@ curl \
        -H "Authorization: Bearer $token" \
        -H 'Accept: application/x-ndjson' "$url" | \
        while read -r json; do
       +        moveplayed="0"
                if [ "$firstline" = "1" ]; then
                        firstline="0"
        
       @@ -60,15 +61,28 @@ END {
                fi
        
                str=$(printf '%s' "$json" | jaq '$1 == ".moves" { print $3; }')
       -        test "$str" != "" && moves="$str" # override
       +        if [ "$str" != "" ]; then
       +                moves="$str" # override
       +                moveplayed="1"
       +        fi
        
                clear
                printf '%s\n\n' "$header"
                ./fen -o tty "$fen" "$moves"
        
       +        if [ "$moveplayed" = "1" ]; then
       +                speaktext="$(./fen -l -o speak "$fen" "$moves")"
       +        fi
       +        printf '\n%s\n' "$speaktext"
       +
                printf '\nMoves:\n'
                printf '%s\n' "$moves"
        
                printf '\nPGN:\n'
                ./fen -o pgn "$fen" "$moves"
       +
       +        # audio
       +        if [ "$moveplayed" = "1" ]; then
       +                (printf '%s\n' "$speaktext" | espeak) &
       +        fi
        done
 (DIR) diff --git a/fen.1 b/fen.1
       @@ -1,4 +1,4 @@
       -.Dd January 4, 2024
       +.Dd January 5, 2024
        .Dt FEN 1
        .Os
        .Sh NAME
       @@ -7,8 +7,9 @@
        .Sh SYNOPSIS
        .Nm
        .Op Fl cCfF
       +.Op Fl l
        .Op Fl m mapping
       -.Op Fl o Ar ascii | fen | pgn | svg | tty
       +.Op Fl o Ar ascii | fen | pgn | speak | svg | tty
        .Op Fl t theme
        .Op Ar FEN
        .Op Ar moves
       @@ -28,6 +29,9 @@ Disable board coordinates.
        Flip the board, default is off.
        .It Fl F
        Do not flip the board.
       +.It Fl l
       +For PGN and speak mode only output the last move.
       +For PGN this will not prefix the move number.
        .It Fl m Ar mapping
        Specify a mapping to remap the piece letters to a localized PGN format.
        For example for dutch: (K)oning, (D)ame, (T)oren, (L)oper, (P)aard it could be:
       @@ -43,6 +47,9 @@ FEN of the board state after playing the moves.
        FEN of the board state after playing the moves.
        .It pgn
        PGN output of the moves for the board.
       +.It speak
       +Write each move per line as text to stdout.
       +Intended to be piped to speech applications.
        .It svg
        SVG image of the board.
        .It tty
 (DIR) diff --git a/fen.c b/fen.c
       @@ -15,9 +15,11 @@
        #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 { ModeInvalid = 0, ModeASCII, ModeCGI, ModeFEN, ModePGN, ModeTTY, ModeSVG };
       +enum outputmode { ModeInvalid = 0, ModeASCII, ModeCGI, ModeFEN, ModePGN, ModeTTY, ModeSVG, ModeSpeak };
        enum outputmode outputmode = ModeSVG;
        
       +static int onlylastmove = 0, silent = 0, dutchmode = 0;
       +
        /* localization of letter for PGN pieces */
        const char *pgn_piecemapping = "";
        
       @@ -91,12 +93,12 @@ struct board {
                struct theme *theme;
        };
        
       +/* set theme by name */
        struct theme *
        board_set_theme(struct board *b, const char *name)
        {
                int i;
        
       -        /* lookup theme by name */
                for (i = 0; i < LEN(themes); i++) {
                        if (!strcmp(themes[i].name, name)) {
                                b->theme = &themes[i];
       @@ -109,7 +111,7 @@ board_set_theme(struct board *b, const char *name)
        void
        board_init(struct board *b)
        {
       -        memset(b, 0, sizeof(*b)); /* zeroed fields by default */
       +        memset(b, 0, sizeof(*b)); /* zero fields by default */
                b->side_to_move = 'w';
                b->enpassantsquare[0] = -1; /* no en passant */
                b->enpassantsquare[1] = -1;
       @@ -125,41 +127,6 @@ board_copy(struct board *bd, struct board *bs)
                memcpy(bd, bs, sizeof(*bd));
        }
        
       -void
       -pgn(const char *fmt, ...)
       -{
       -        va_list ap;
       -
       -        if (outputmode != ModePGN)
       -                return;
       -
       -        va_start(ap, fmt);
       -        vprintf(fmt, ap);
       -        va_end(ap);
       -}
       -
       -/* remap letter for PGN pieces, default: "KQRBN"
       -   Dutch: (K)oning, (D)ame, (T)oren, (L)oper, (P)aard: "KDTLP" */
       -int
       -pgnpiece(int piece)
       -{
       -        piece = toupper(piece);
       -
       -        /* no mapping */
       -        if (!pgn_piecemapping[0])
       -                return piece;
       -
       -        switch (piece) {
       -        case 'K': piece = pgn_piecemapping[0]; break;
       -        case 'Q': piece = pgn_piecemapping[1]; break;
       -        case 'R': piece = pgn_piecemapping[2]; break;
       -        case 'B': piece = pgn_piecemapping[3]; break;
       -        case 'N': piece = pgn_piecemapping[4]; break;
       -        }
       -
       -        return piece;
       -}
       -
        int
        isvalidsquare(int x, int y)
        {
       @@ -224,6 +191,67 @@ squaretoxy(const char *s, int *x, int *y)
                return 0;
        }
        
       +void
       +pgn(const char *fmt, ...)
       +{
       +        va_list ap;
       +
       +        if (outputmode != ModePGN || silent)
       +                return;
       +
       +        va_start(ap, fmt);
       +        vprintf(fmt, ap);
       +        va_end(ap);
       +}
       +
       +/* remap letter for PGN pieces, default: "KQRBN"
       +   Dutch: (K)oning, (D)ame, (T)oren, (L)oper, (P)aard: "KDTLP" */
       +int
       +pgnpiece(int piece)
       +{
       +        piece = toupper(piece);
       +
       +        /* no mapping */
       +        if (!pgn_piecemapping[0])
       +                return piece;
       +
       +        switch (piece) {
       +        case 'K': piece = pgn_piecemapping[0]; break;
       +        case 'Q': piece = pgn_piecemapping[1]; break;
       +        case 'R': piece = pgn_piecemapping[2]; break;
       +        case 'B': piece = pgn_piecemapping[3]; break;
       +        case 'N': piece = pgn_piecemapping[4]; break;
       +        }
       +
       +        return piece;
       +}
       +
       +void
       +speak(const char *fmt, ...)
       +{
       +        va_list ap;
       +
       +        if (outputmode != ModeSpeak || silent)
       +                return;
       +
       +        va_start(ap, fmt);
       +        vprintf(fmt, ap);
       +        va_end(ap);
       +}
       +
       +void
       +speakpiece(int piece)
       +{
       +        switch (piece) {
       +        case 'K': case 'k': speak(dutchmode ? "koning " : "king "); break;
       +        case 'Q': case 'q': speak(dutchmode ? "dame " : "queen "); break;
       +        case 'R': case 'r': speak(dutchmode ? "toren " : "rook "); break;
       +        case 'B': case 'b': speak(dutchmode ? "loper " : "bishop "); break;
       +        case 'N': case 'n': speak(dutchmode ? "paard " : "knight "); break;
       +        case 'P': case 'p': speak(dutchmode ? "pion " : "pawn "); break;
       +        }
       +}
       +
        /* place a piece, if possible */
        void
        place(struct board *b, int piece, int x, int y)
       @@ -639,6 +667,19 @@ findking(struct board *b, int side, int *kingx, int *kingy)
        }
        
        int
       +isenpassantplayed(struct board *b, int side, int x, int y)
       +{
       +        if (side == 'w') {
       +                return (getpiece(b, x - 1, y) == 'p' ||
       +                        getpiece(b, x + 1, y) == 'p');
       +        } else if (side == 'b') {
       +                return (getpiece(b, x - 1, y) == 'P' ||
       +                        getpiece(b, x + 1, y) == 'P');
       +        }
       +        return 0;
       +}
       +
       +int
        isincheck(struct board *b, int side)
        {
                int king[]   = { -1, -1, -1, 0, -1, 1, 0, -1, 0, 1, 1, -1, 1, 0, 1, 1 };
       @@ -724,11 +765,17 @@ isincheck(struct board *b, int side)
        }
        
        int
       -trypiecemove(struct board *b, int side, int piece, int x1, int y1, int x2, int y2)
       +trypiecemove(struct board *b, int side, int piece,
       +             int x1, int y1, int x2, int y2, int px, int py)
        {
                struct board tb;
        
                board_copy(&tb, b);
       +
       +        /* taken en passant? remove pawn */
       +        if (x2 == px && y2 == py)
       +                place(&tb, 0, x2, piece == 'P' ? y2 + 1 : y2 - 1);
       +
                place(&tb, 0, x1, y1);
                place(&tb, piece, x2, y2);
        
       @@ -739,7 +786,8 @@ trypiecemove(struct board *b, int side, int piece, int x1, int y1, int x2, int y
        /* can piece move from (x, y), to (x, y)?
           en passant square if any is (px, py), otherwise (-1, -1) */
        int
       -canpiecemove(struct board *b, int side, int x1, int y1, int x2, int y2, int px, int py)
       +canpiecemove(struct board *b, int side, int piece,
       +             int x1, int y1, int x2, int y2, int px, int py)
        {
                int king[]   = { -1, -1, -1, 0, -1, 1, 0, -1, 0, 1, 1, -1, 1, 0, 1, 1 };
                int diag[]   = { -1, -1,  1, 1, -1, 1, 1, -1 };
       @@ -748,9 +796,9 @@ canpiecemove(struct board *b, int side, int x1, int y1, int x2, int y2, int px, 
                                 -2, -1,  2, -1, -2, 1, 2, 1
                               };
                int i, j, dir, x, y;
       -        int piece, takepiece;
       +        int takepiece;
        
       -        if (!(piece = getpiece(b, x1, y1)))
       +        if (!piece)
                        return 0; /* theres no piece so it cannot be moved */
        
                /* can't move opponent piece */
       @@ -781,7 +829,7 @@ canpiecemove(struct board *b, int side, int x1, int y1, int x2, int y2, int px, 
                                        y = y1 + (i * line[j + 1]);
        
                                        if (x == x2 && y == y2 &&
       -                                    trypiecemove(b, side, piece, x1, y1, x2, y2))
       +                                    trypiecemove(b, side, piece, x1, y1, x2, y2, px, py))
                                                return 1;
        
                                        /* a piece is in front of it: stop this checking this direction */
       @@ -798,7 +846,7 @@ canpiecemove(struct board *b, int side, int x1, int y1, int x2, int y2, int px, 
                                        x = x1 + (i * diag[j]);
                                        y = y1 + (i * diag[j + 1]);
                                        if (x == x2 && y == y2 &&
       -                                    trypiecemove(b, side, piece, x1, y1, x2, y2))
       +                                    trypiecemove(b, side, piece, x1, y1, x2, y2, px, py))
                                                return 1;
        
                                        /* a piece is in front of it: stop this checking this direction */
       @@ -822,6 +870,15 @@ canpiecemove(struct board *b, int side, int x1, int y1, int x2, int y2, int px, 
                        dir = piece == 'P' ? -1 : +1;
                        j = piece == 'P' ? 6 : 1; /* start row */
        
       +                /* can move to en passant square? */
       +                /* en passant set? try it if possible */
       +                if (px == x2 && py == y2 &&
       +                    (py == y1 + dir) &&
       +                    ((px == x1 - 1) || px == x1 + 1)) {
       +                        if (isenpassantplayed(b, side == 'w' ? 'b' : 'w', px, y1))
       +                                return trypiecemove(b, side, piece, x1, y1, x2, y2, px, py);
       +                }
       +
                        if (takepiece == 0) {
                                if (x1 != x2)
                                        return 0; /* move on same file */
       @@ -848,13 +905,16 @@ canpiecemove(struct board *b, int side, int x1, int y1, int x2, int y2, int px, 
        /* previous checks for move succeeded, actually try move with the current
           board state */
        trymove:
       -        return trypiecemove(b, side, piece, x1, y1, x2, y2);
       +        return trypiecemove(b, side, piece, x1, y1, x2, y2, px, py);
        }
        
        int
        ischeckmated(struct board *b, int side)
        {
       -        int x, y, x2, y2, piece;
       +        int x, y, x2, y2, px, py, piece;
       +
       +        px = b->enpassantsquare[0];
       +        py = b->enpassantsquare[0];
        
                /* check pieces that can block or take a piece that removes the check */
                for (y = 0; y < 8; y++) {
       @@ -867,7 +927,7 @@ ischeckmated(struct board *b, int side)
                                for (y2 = 0; y2 < 8; y2++) {
                                        for (x2 = 0; x2 < 8; x2++) {
                                                /* can piece move and afterwards we are not in check? */
       -                                        if (canpiecemove(b, side, x, y, x2, y2, -1, -1))
       +                                        if (canpiecemove(b, side, piece, x, y, x2, y2, px, py))
                                                        return 0;
                                        }
                                }
       @@ -978,13 +1038,54 @@ board_setup_fen(struct board *b, const char *fen)
                }
        }
        
       +/* count ambiguity for piece moves, used to make the notation shorter */
       +void
       +countambigousmoves(struct board *b, int side, int piece,
       +        int x, int y, int x2, int y2, int px, int py,
       +        int *countfile, int *countrank, int *countboard)
       +{
       +        int cf = 0, cr = 0, cb = 0, i, j;
       +
       +        /* check same file */
       +        for (i = 0; i < 8; i++) {
       +                if (getpiece(b, i, y) == piece &&
       +                    canpiecemove(b, side, piece, i, y, x2, y2, px, py))
       +                        cf++;
       +        }
       +
       +        /* check same rank */
       +        for (i = 0; i < 8; i++) {
       +                if (getpiece(b, x, i) == piece &&
       +                    canpiecemove(b, side, piece, x, i, x2, y2, px, py))
       +                        cr++;
       +        }
       +
       +        /* check whole board */
       +        if (cf <= 1 && cr <= 1) {
       +                /* check the whole board if there is any piece
       +                   that can move to the same square */
       +                for (i = 0; i < 8; i++) {
       +                        for (j = 0; j < 8; j++) {
       +                                if (getpiece(b, i, j) == piece &&
       +                                    canpiecemove(b, side, piece, i, j, x2, y2, px, py))
       +                                        cb++;
       +                        }
       +                }
       +        }
       +
       +        *countfile = cf;
       +        *countrank = cr;
       +        *countboard = cb;
       +}
       +
        void
        board_playmoves(struct board *b, const char *moves)
        {
                char square[3];
                const char *castled, *s;
       -        int firstmove, i, j, x, y, x2, y2, otherside, piece, takepiece, tookpiece;
       -        int needfile, needrank, promote;
       +        int firstmove, i, x, y, x2, y2, side, otherside, piece, takepiece, tookpiece;
       +        int countfile, countrank, countboard, px, py;
       +        int promote, tookeps;
        
                /* process moves */
                square[2] = '\0';
       @@ -1002,17 +1103,24 @@ board_playmoves(struct board *b, const char *moves)
                            (*(s + 3) >= '1' && *(s + 3) <= '8')))
                                continue;
        
       -                otherside = b->side_to_move == 'b' ? 'w' : 'b';
       +                /* is last move in this sequence? */
       +                if (onlylastmove && !strchr(s, ' '))
       +                        silent = 0;
       +
       +                side = b->side_to_move;
       +                otherside = side == 'b' ? 'w' : 'b';
        
                        /* if first move and it is blacks turn, prefix
                           with "...", because the white move was unknown */
       -                if (firstmove && b->side_to_move == 'b')
       +                if (!onlylastmove && firstmove && side == 'b')
                                pgn("%d. ... ", b->movenumber);
        
       -                if (firstmove)
       +                if (firstmove && !silent) {
                                firstmove = 0;
       -                else
       +                } else {
                                pgn(" ");
       +                        speak("\n");
       +                }
        
                        square[0] = *s;
                        square[1] = *(s + 1);
       @@ -1038,8 +1146,8 @@ board_playmoves(struct board *b, const char *moves)
                        }
        
                        /* took piece of opponent */
       -                tookpiece = (b->side_to_move == 'w' && isblackpiece(takepiece)) ||
       -                            (b->side_to_move == 'b' && iswhitepiece(takepiece));
       +                tookpiece = (side == 'w' && isblackpiece(takepiece)) ||
       +                            (side == 'b' && iswhitepiece(takepiece));
        
                        /* if pawn move or taken a piece increase halfmove counter */
                        if (piece == 'p' || piece == 'P' || tookpiece)
       @@ -1047,7 +1155,7 @@ board_playmoves(struct board *b, const char *moves)
                        else
                                b->halfmove++;
        
       -                if (b->side_to_move == 'w')
       +                if (!onlylastmove && side == 'w')
                                pgn("%d. ", b->movenumber);
        
                        /* castled this move? */
       @@ -1061,7 +1169,7 @@ board_playmoves(struct board *b, const char *moves)
                                                if (getpiece(b, i, y2) == 'R') {
                                                        place(b, 0, x, y); /* clear previous square */
                                                        place(b, 0, i, y2); /* clear rook square */
       -                                                place(b, 'R', x2 - 1, y2); /* next to king */
       +                                                place(b, 'R', x2 - 1, y2); /* rook next to king */
                                                        castled = "O-O";
                                                        break;
                                                }
       @@ -1071,8 +1179,8 @@ board_playmoves(struct board *b, const char *moves)
                                        for (i = x2; i >= 0; i--) {
                                                if (getpiece(b, i, y2) == 'R') {
                                                        place(b, 0, x, y); /* clear previous square */
       -                                                place(b, 'R', x2 + 1, y2); /* next to king */
                                                        place(b, 0, i, y2); /* clear rook square */
       +                                                place(b, 'R', x2 + 1, y2); /* rook next to king */
                                                        castled = "O-O-O";
                                                        break;
                                                }
       @@ -1085,7 +1193,7 @@ board_playmoves(struct board *b, const char *moves)
                                                if (getpiece(b, i, y2) == 'r') {
                                                        place(b, 0, x, y); /* clear previous square */
                                                        place(b, 0, i, y2); /* clear rook square */
       -                                                place(b, 'r', x2 - 1, y2); /* next to king */
       +                                                place(b, 'r', x2 - 1, y2); /* rook next to king */
                                                        castled = "O-O";
                                                        break;
                                                }
       @@ -1095,14 +1203,20 @@ board_playmoves(struct board *b, const char *moves)
                                        for (i = x2; i >= 0; i--) {
                                                if (getpiece(b, i, y2) == 'r') {
                                                        place(b, 0, x, y); /* clear previous square */
       -                                                place(b, 'r', x2 + 1, y2); /* next to king */
                                                        place(b, 0, i, y2); /* clear rook square */
       +                                                place(b, 'r', x2 + 1, y2); /* rook next to king */
                                                        castled = "O-O-O";
                                                        break;
                                                }
                                        }
                                }
                        }
       +                if (castled) {
       +                        /* set previous move square (for highlight) */
       +                        x = i;
       +                        y = y2;
       +                        place(b, piece, x2, y2); /* place king */
       +                }
        
                        /* remove the ability to castle */
                        if (piece == 'K') {
       @@ -1132,95 +1246,139 @@ board_playmoves(struct board *b, const char *moves)
                        }
        
                        /* taken en passant? */
       -                if (x2 == b->enpassantsquare[0] && y2 == b->enpassantsquare[1])
       +                tookeps = 0;
       +                if (x2 == b->enpassantsquare[0] && y2 == b->enpassantsquare[1] &&
       +                    (piece == 'P' || piece == 'p')) {
       +                        /* clear square */
                                place(b, 0, x2, piece == 'P' ? y2 + 1 : y2 - 1);
       +                        /* set a piece is taken */
       +                        tookpiece = 1;
       +                        takepiece = piece == 'P' ? 'p' : 'P';
       +                        tookeps = 1;
       +                }
        
                        /* the en passant square resets after a move */
       -                b->enpassantsquare[0] = -1;
       -                b->enpassantsquare[1] = -1;
       +                px = b->enpassantsquare[0] = -1;
       +                py = b->enpassantsquare[1] = -1;
        
                        /* set en passant square:
                           moved 2 squares and there is an opponent pawn next to it */
                        if (piece == 'P' && y == 6 && y2 == 4) {
       -                        if (getpiece(b, x - 1, y2) == 'p' ||
       -                            getpiece(b, x + 1, y2) == 'p') {
       -                                b->enpassantsquare[0] = x;
       -                                b->enpassantsquare[1] = 5;
       +                        if (isenpassantplayed(b, side, x, y2)) {
       +                                px = b->enpassantsquare[0] = x;
       +                                py = b->enpassantsquare[1] = 5;
                                }
                        } else if (piece == 'p' && y == 1 && y2 == 3) {
       -                        if (getpiece(b, x - 1, y2) == 'P' ||
       -                            getpiece(b, x + 1, y2) == 'P') {
       -                                b->enpassantsquare[0] = x;
       -                                b->enpassantsquare[1] = 2;
       +                        if (isenpassantplayed(b, side, x, y2)) {
       +                                px = b->enpassantsquare[0] = x;
       +                                py = b->enpassantsquare[1] = 2;
                                }
                        }
        
       -                /* if output is PGN, show piece movement, else skip this step */
       -                if (outputmode == ModePGN) {
       -                        /* PGN for move */
       +                /* PGN for move, if output is not PGN then skip this step */
       +                if (outputmode == ModePGN || outputmode == ModeSpeak) {
                                if (castled) {
                                        pgn("%s", castled);
       +
       +                                if (side == 'w')
       +                                        speak(dutchmode ? "wit " : "white ");
       +                                else if (side == 'b')
       +                                        speak(dutchmode ? "zwart " : "black ");
       +
       +                                if (!strcmp(castled, "O-O"))
       +                                        speak(dutchmode ? "rokeert aan koningszijde " : "castled kingside ");
       +                                else
       +                                        speak(dutchmode ? "rokeert aan damezijde " : "castled queenside ");
                                } else {
       +                                if (side == 'w')
       +                                        speak(dutchmode ? "witte " : "white ");
       +                                else if (side == 'b')
       +                                        speak(dutchmode ? "zwarte " : "black ");
       +
       +                                if (!tookpiece) {
       +                                        if (!dutchmode)
       +                                                speak("moves ");
       +                                        speakpiece(piece);
       +                                }
       +
                                        /* pawn move needs no notation */
       -                                if (piece != 'p' && piece != 'P') {
       +                                if (piece != 'p' && piece != 'P')
                                                pgn("%c", pgnpiece(piece));
        
       -                                        /* check ambiguity for certain pieces and make the notation shorter */
       -                                        needfile = needrank = 0;
       -                                        for (i = 0; i < 8; i++) {
       -                                                for (j = 0; j < 8; j++) {
       -                                                        if (x == i && j == y)
       -                                                                continue;
       -                                                        if (getpiece(b, i, j) != piece)
       -                                                                continue;
       -
       -                                                        if (canpiecemove(b, b->side_to_move, i, j, x2, y2, -1, -1)) {
       -                                                                needfile = 1;
       -                                                                if (i == x)
       -                                                                        needrank = 1;
       -                                                        }
       -                                                }
       -                                        }
       -                                        if (needfile) {
       -                                                pgn("%c", xtofile(x));
       -                                                if (needrank)
       -                                                        pgn("%c", ytorank(y));
       -                                        }
       +                                /* check ambiguity for certain pieces and make the notation shorter */
       +                                countambigousmoves(b, side, piece, x, y, x2, y2, px, py,
       +                                        &countfile, &countrank, &countboard);
       +
       +                                if (countfile > 1) {
       +                                        pgn("%c", xtofile(x));
       +                                        speak("%c", xtofile(x));
                                        }
       +                                if (countrank > 1) {
       +                                        pgn("%c", ytorank(y));
       +                                        speak("%c", ytorank(y));
       +                                }
       +                                if (countboard > 1) {
       +                                        pgn("%c", xtofile(x));
       +                                        speak("%c", xtofile(x));
       +                                }
       +                                if (countfile > 1 || countrank > 1 || countboard > 1)
       +                                        speak(" ");
       +
                                        if (tookpiece) {
                                                /* pawn captures are prefixed by the file letter (no more needed) */
                                                if (piece == 'p' || piece == 'P')
                                                        pgn("%c", xtofile(x));
                                                pgn("x");
       +                                        speakpiece(piece);
       +                                        speak(dutchmode ? "slaat " : "takes ");
       +                                        speakpiece(takepiece);
       +                                        speak(dutchmode ? "op " : "on ");
       +                                        speak("%c%c ", xtofile(x2), ytorank(y2));
       +                                        if (tookeps)
       +                                                speak("en passant ");
       +                                } else {
       +                                        speak(dutchmode ? "naar " : "to ");
       +                                        speak("%c%c ", xtofile(x2), ytorank(y2));
                                        }
                                        pgn("%c%c", xtofile(x2), ytorank(y2));
        
       -                                /* possible promotion: queen, knight, bishop */
       +                                /* possible promotion: queen, rook, bishop, knight */
                                        if (promote) {
       -                                        if (b->side_to_move == 'w')
       +                                        if (side == 'w')
                                                        piece = toupper(promote);
                                                else
                                                        piece = tolower(promote);
                                                place(b, piece, x2, y2);
        
       +                                        speak(dutchmode ? "en promoot naar " : "and promotes to ");
       +                                        speakpiece(promote);
       +
                                                pgn("=%c", pgnpiece(piece));
                                        }
                                }
                        }
        
                        /* clear previous square (if not castled) */
       -                if (!castled)
       +                if (!castled) {
                                place(b, 0, x, y);
       -                /* place piece or new promoted piece */
       -                place(b, piece, x2, y2);
       +                        /* place piece or new promoted piece */
       +                        place(b, piece, x2, y2);
       +                }
       +
       +                if (ischeckmated(b, otherside)) {
       +                        /* reset en passant square on checkmate */
       +                        b->enpassantsquare[0] = -1;
       +                        b->enpassantsquare[1] = -1;
        
       -                if (ischeckmated(b, otherside))
                                pgn("#");
       -                else if (isincheck(b, otherside))
       +                        speak(dutchmode ? "mat" : "checkmate");
       +                } else if (isincheck(b, otherside)) {
                                pgn("+");
       +                        speak(dutchmode ? "schaak" : "check");
       +                }
        
                        /* a move by black increases the move number */
       -                if (b->side_to_move == 'b')
       +                if (side == 'b')
                                b->movenumber++;
        
                        /* switch which side it is to move */
       @@ -1230,8 +1388,10 @@ board_playmoves(struct board *b, const char *moves)
                                break;
                }
        
       -        if (!firstmove)
       +        if (!firstmove) {
                        pgn("\n");
       +                speak("\n");
       +        }
        
                /* highlight last move */
                highlightmove(b, x, y);
       @@ -1246,7 +1406,7 @@ board_playmoves(struct board *b, const char *moves)
        void
        usage(char *argv0)
        {
       -        fprintf(stderr, "usage: %s [-cCfF] [-m mapping] [-o ascii|fen|pgn|svg|tty] [-t default|green|grey] [FEN] [moves]\n", argv0);
       +        fprintf(stderr, "usage: %s [-cCfF] [-l] [-m mapping] [-o ascii|fen|pgn|speak|svg|tty] [-t default|green|grey] [FEN] [moves]\n", argv0);
                exit(1);
        }
        
       @@ -1325,6 +1485,8 @@ outputnametomode(const char *s)
                        return ModeFEN;
                else if (!strcmp(s, "pgn"))
                        return ModePGN;
       +        else if (!strcmp(s, "speak"))
       +                return ModeSpeak;
                else if (!strcmp(s, "svg"))
                        return ModeSVG;
                else if (!strcmp(s, "tty"))
       @@ -1433,8 +1595,10 @@ main(int argc, char *argv[])
                                switch (argv[i][j]) {
                                case 'c': board.showcoords = 1; break;
                                case 'C': board.showcoords = 0; break;
       +                        case 'd': dutchmode = 1; break; /* top secret dutch mode for "speak" */
                                case 'f': board.flipboard = 1; break;
                                case 'F': board.flipboard = 0; break;
       +                        case 'l': onlylastmove = 1; silent = 1; break;
                                case 'm': /* remap PGN */
                                        if (i + 1 >= argc)
                                                usage(argv[0]);
 (DIR) diff --git a/generate.sh b/generate.sh
       @@ -4,10 +4,14 @@ fenbin="./fen"
        db="lichess_db_puzzle.csv"
        # default, green, grey
        theme="default"
       +lang="en" # en, nl
       +#fenopts="-d" # dutch mode (for speak output)
        
        # texts / localization.
        # English
       +if [ "$lang" = "en" ]; then
        text_solutions="Solutions"
       +text_solutionstxtlabel="Text listing of solutions"
        text_puzzles="Puzzles"
        text_puzzle="Puzzle"
        text_puzzlerating="Puzzel rating"
       @@ -18,20 +22,24 @@ text_blacktomove="black to move"
        text_title="${text_puzzles}"
        text_header="${text_puzzles}!"
        pgnmapping="KQRBN"
       +fi
        
        # Dutch
       -#text_solutions="Oplossingen"
       -#text_puzzles="Puzzels"
       -#text_puzzle="Puzzel"
       -#text_puzzlerating="Puzzel moeilijkheidsgraad"
       -#text_point="punt"
       -#text_points="punten"
       -#text_whitetomove="wit aan zet"
       -#text_blacktomove="zwart aan zet"
       -#text_title="${text_puzzles}"
       -#text_header="${text_puzzles}!"
       +if [ "$lang" = "nl" ]; then
       +text_solutions="Oplossingen"
       +text_solutionstxtlabel="Tekstbestand, lijst met oplossingen"
       +text_puzzles="Puzzels"
       +text_puzzle="Puzzel"
       +text_puzzlerating="Puzzel moeilijkheidsgraad"
       +text_point="punt"
       +text_points="punten"
       +text_whitetomove="wit aan zet"
       +text_blacktomove="zwart aan zet"
       +text_title="${text_puzzles}"
       +text_header="${text_puzzles}!"
        # Dutch: (K)oning, (D)ame, (T)oren, (L)oper, (P)aard.
       -#pgnmapping="KDTLP"
       +pgnmapping="KDTLP"
       +fi
        
        if ! test -f "$db"; then
                printf 'File "%s" not found, run `make db` to update it\n' "$db" >&2
       @@ -39,6 +47,9 @@ if ! test -f "$db"; then
        fi
        
        index="puzzles/index.html"
       +indexvt="puzzles/index.vt"
       +
       +# clean previous files.
        rm -rf puzzles
        mkdir -p puzzles/solutions
        
       @@ -73,19 +84,31 @@ sel[NR] {
                rm -f "$results"
        }
        
       +# solutions.txt header.
       +solutionstxt="puzzles/solutions.txt"
       +printf '%s\n\n' "${text_solutions}" >> "$solutionstxt"
       +
       +cat > "$indexvt" <<!
       +${text_header}
       +
       +!
       +
        cat > "$index" <<!
        <!DOCTYPE html>
       -<html>
       +<html dir="ltr" lang="${lang}">
        <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
        <title>${text_title}</title>
        <style type="text/css">
        body {
                font-family: sans-serif;
       -        width: 960px;
       +        width: 775px;
                margin: 0 auto;
                padding: 0 10px;
        }
       +a {
       +        color: #000;
       +}
        h2 a {
                color: #000;
                text-decoration: none;
       @@ -108,7 +131,7 @@ details summary {
                        background-color: #000;
                        color: #bdbdbd;
                }
       -        h2 a {
       +        h2 a, a {
                        color: #bdbdbd;
                }
        }
       @@ -183,11 +206,11 @@ while read -r line; do
                desttxt="puzzles/$txt"
                destvt="puzzles/$vt"
        
       -        "$fenbin" -m "$pgnmapping" -t "$theme" $flip -o svg "$fen" "$firstmove" > "$destsvg"
       -        "$fenbin" -m "$pgnmapping" -t "$theme" $flip -o ascii "$fen" "$firstmove" > "$desttxt"
       -        "$fenbin" -m "$pgnmapping" -t "$theme" $flip -o tty "$fen" "$firstmove" > "$destvt"
       -        "$fenbin" -m "$pgnmapping" -t "$theme" $flip -o fen "$fen" "$firstmove" > "$destfen"
       -        pgn=$("$fenbin" -m "$pgnmapping" -o pgn "$fen" "$firstmove")
       +        "$fenbin" $fenopts -m "$pgnmapping" -t "$theme" $flip -o svg "$fen" "$firstmove" > "$destsvg"
       +        "$fenbin" $fenopts -m "$pgnmapping" -t "$theme" $flip -o ascii "$fen" "$firstmove" > "$desttxt"
       +        "$fenbin" $fenopts -m "$pgnmapping" -t "$theme" $flip -o tty "$fen" "$firstmove" > "$destvt"
       +        "$fenbin" $fenopts -m "$pgnmapping" -t "$theme" $flip -o fen "$fen" "$firstmove" > "$destfen"
       +        pgn=$("$fenbin" $fenopts -l -m "$pgnmapping" -o pgn "$fen" "$firstmove")
        
                printf '<div class="puzzle" id="puzzle-%s">\n' "$i" >> "$index"
                printf '<h2><a href="#puzzle-%s">%s %s</a></h2>\n' "$i" "${text_puzzle}" "$i" >> "$index"
       @@ -219,6 +242,11 @@ while read -r line; do
                printf '%s%s\n' "$points" "$movetext" >> "$desttxt"
                printf '\n%s%s\n' "$points" "$movetext" >> "$destvt"
        
       +        # vt
       +        printf 'Puzzle %s\n\n' "$i" >> "$indexvt"
       +        cat "$destvt" >> "$indexvt"
       +        printf '\n\n' >> "$indexvt"
       +
                # solutions per puzzle.
                printf '<div class="puzzle-solution">\n' >> "$solutions"
                printf '<h2><a href="#puzzle-%s">%s %s</a></h2>\n' "$i" "${text_puzzle}" "$i" >> "$solutions"
       @@ -229,11 +257,12 @@ while read -r line; do
                # the solution images.
        
                # add initial puzzle aswell for context.
       -        printf '<img src="%s" width="180" height="180" loading="lazy" title="%s" />\n' \
       -                "${i}.svg" "$pgn" >> "$solutions"
       +        ptitlespeak="$("$fenbin" $fenopts -l -o speak "$fen" "$firstmove")"
       +        printf '<img src="%s" width="180" height="180" loading="lazy" alt="%s" title="%s" />\n' \
       +                "${i}.svg" "$ptitlespeak" "$pgn, $ptitlespeak" >> "$solutions"
        
                # solution PGN
       -        pgn_solution="$($fenbin -m "$pgnmapping" -o pgn "$fen" "$allmoves")"
       +        pgn_solution="$("$fenbin" $fenopts -m "$pgnmapping" -o pgn "$fen" "$allmoves")"
        
                destsolpgn="puzzles/solutions/${i}.pgn"
                printf '%s\n' "$pgn_solution" > "$destsolpgn"
       @@ -260,13 +289,14 @@ while read -r line; do
        
                        # process move list in sequence.
                        destsolsvg="puzzles/solutions/${i}_${movecount}.svg"
       -                "$fenbin" -m "$pgnmapping" -t "$theme" $flip -o svg "$fen" "$movelist" > "$destsolsvg"
       +                "$fenbin" $fenopts -m "$pgnmapping" -t "$theme" $flip -o svg "$fen" "$movelist" > "$destsolsvg"
        
                        # PGN of moves so far.
       -                pgn="$($fenbin -m "$pgnmapping" -o pgn "$fen" "$movelist")"
       +                pgn="$("$fenbin" $fenopts $fenopts -l -m "$pgnmapping" -o pgn "$fen" "$movelist")"
       +                ptitlespeak="$("$fenbin" $fenopts -l -o speak "$fen" "$movelist")"
        
       -                printf '<img src="%s" width="180" height="180" loading="lazy" title="%s" />\n' \
       -                        "solutions/${i}_${movecount}.svg" "$pgn" >> "$solutions"
       +                printf '<img src="%s" width="180" height="180" loading="lazy" alt="%s" title="%s" />\n' \
       +                        "solutions/${i}_${movecount}.svg" "$ptitlespeak" "$pgn, $ptitlespeak" >> "$solutions"
        
                        movecount=$((movecount + 1))
                done
       @@ -276,19 +306,30 @@ while read -r line; do
        
                printf '</div>\n' >> "$index"
        
       +        # add PGN solution to solutions text file.
       +        printf '%s. %s\n' "$i" "${pgn_solution}" >> "$solutionstxt"
       +
                count=$((count + 1))
        done
        
        # solutions / spoilers
        printf '<footer><br/><br/><details>\n<summary>%s</summary>\n' "$text_solutions" >> "$index"
       +printf '<p><a href="solutions.txt">%s</a></p>\n' "${text_solutionstxtlabel}" >> "$index"
       +
       +# add solutions HTML to index page.
        cat "$solutions" >> "$index"
        echo "</details>\n<br/><br/></footer>\n" >> "$index"
        
       +# add solutions to vt index page.
       +printf '\n\n\n\n\n\n\n\n\n\n' >> "$indexvt"
       +printf '\n\n\n\n\n\n\n\n\n\n' >> "$indexvt"
       +printf '\n\n\n\n\n' >> "$indexvt"
       +cat "$solutionstxt" >> "$indexvt"
       +
        cat >> "$index" <<!
        </main>
        </body>
        </html>
        !
        
       -rm -f "$solutions"
       -rm -f "$seedfile"
       +rm -f "$solutions" "$seedfile"
 (DIR) diff --git a/tests.sh b/tests.sh
       @@ -1,5 +1,8 @@
        #!/bin/sh
        
       +statuscode=0
       +failed=0
       +
        # testfen(name, expect, fen, moves)
        testfen() {
                name="$1"
       @@ -7,14 +10,24 @@ testfen() {
                fen="$3"
                moves="$4"
        
       +        # input FEN with no moves should match output FEN (except "startpos").
       +        output=$(./fen -o fen "$fen" "")
       +        if test "$fen" != "startpos" && test "$fen" != "$output"; then
       +                printf '[FEN] Fail: %s, input FEN does not match output FEN\n' "$name"
       +                statuscode=1
       +                failed=$((failed+1))
       +        fi
       +
                output=$(./fen -o fen "$fen" "$moves")
                if test "$output" = "$expect"; then
       -                printf 'OK: %s\n' "$name"
       +                printf '[FEN] OK: %s\n' "$name"
                else
       -                printf 'Fail: %s\n' "$name"
       +                printf '[FEN] Fail: %s\n' "$name"
                        printf '\texpected: %s\n' "$expect"
                        printf '\tgot:      %s\n' "$output"
                        printf '\tInput FEN, moves: "%s" "%s"\n' "$fen" "$moves"
       +                statuscode=1
       +                failed=$((failed+1))
                fi
        }
        
       @@ -27,12 +40,14 @@ testpgn() {
        
                output=$(./fen -o pgn "$fen" "$moves")
                if test "$output" = "$expect"; then
       -                printf 'OK: %s\n' "$name"
       +                printf '[PGN] OK: %s\n' "$name"
                else
       -                printf 'Fail: %s\n' "$name"
       +                printf '[PGN] Fail: %s\n' "$name"
                        printf '\texpected: %s\n' "$expect"
                        printf '\tgot:      %s\n' "$output"
                        printf '\tInput FEN, moves: "%s" "%s"\n' "$fen" "$moves"
       +                statuscode=1
       +                failed=$((failed+1))
                fi
        }
        
       @@ -232,8 +247,15 @@ testfen 'black moves pawn en passant into checkmate (cant take en passant to def
                'rnbqkbnr/p1p1pppp/1p6/3PP3/2PKQ3/2PQQ3/P1PP1PPP/RNBQ1BNR b kq - 0 1'\
                'c7c5'
        
       -# TODO: test more chess960 black kingside and queenside castling
       -# TODO: test more long sequence and halfmove and movenumber counts
       +testfen 'white is checkmated (en passant square is not set), do not remove pawn (illegal move though)'\
       +        'rnbqkbnr/p2ppppp/1pP5/1Pp1P3/2PKQ3/2PQQ3/P1PP1PPP/RNBQ1BNR b kq - 0 1'\
       +        'rnbqkbnr/p2ppppp/1p6/1PpPP3/2PKQ3/2PQQ3/P1PP1PPP/RNBQ1BNR w kq - 0 1'\
       +        'd5c6'
       +
       +testfen 'white is not checkmated (en passant square is set and can be played), remove pawn'\
       +        'rnbqkbnr/p2ppppp/1pP5/1P2P3/2PKQ3/2PQQ3/P1PP1PPP/RNBQ1BNR b kq - 0 1'\
       +        'rnbqkbnr/p2ppppp/1p6/1PpPP3/2PKQ3/2PQQ3/P1PP1PPP/RNBQ1BNR w kq c6 0 1'\
       +        'd5c6'
        }
        
        tests_pgn() {
       @@ -280,8 +302,8 @@ testpgn 'black moves with knight, non-ambigous move'\
                'rn2kb1r/pp4pp/1qp1p3/3p4/3P1B2/1P1BP3/P1P2P1P/RN1QK1NR b KQkq - 0 8'\
                'b8d7'
        
       -testpgn '2 queens, ambigous move, needs file and rank'\
       -        '8. Qh3g3'\
       +testpgn '2 queens, ambigous move, needs rank'\
       +        '8. Q3g3'\
                'rn2kb1r/pp4pp/1qp1p3/3p4/3P1B1Q/1P1BP2Q/P1P2P1P/RN2K1NR w KQkq - 0 8'\
                'h3g3'
        
       @@ -314,11 +336,156 @@ testpgn 'black moves pawn en passant into checkmate (cant take en passant to def
                'c7c5'
        
        # check also if the en passant square is set (but it is not legal to play).
       -testpgn 'black moves pawn en passant into checkmate (cant take en passant to defend)'\
       +testpgn 'black moves pawn en passant into checkmate (cant take en passant to defend), part 2'\
                '1. ... c5#'\
                'rnbqkbnr/p1p1pppp/1p6/3PP3/2PKQ3/2PQQ3/P1PP1PPP/RNBQ1BNR b kq c6 0 1'\
                'c7c5'
       +
       +testpgn 'Knights on the same rank can move to the same square'\
       +        '1. Nde5'\
       +        'rnbqkbnr/pppppppp/8/8/8/3N1N2/PPPPPPPP/R1BQKB1R w KQkq - 0 1'\
       +        'd3e5'
       +
       +testpgn 'Knights on the same rank can move to the same square, part 2'\
       +        '1. Nfe5'\
       +        'rnbqkbnr/pppppppp/8/8/8/3N1N2/PPPPPPPP/R1BQKB1R w KQkq - 0 1'\
       +        'f3e5'
       +
       +testpgn 'Knights on the same same file can move to the same square'\
       +        '1. N3c4'\
       +        'rnbqkbnr/pppppppp/8/4N3/8/4N3/PPPPPPPP/R1BQKB1R w KQkq - 0 1'\
       +        'e3c4'
       +
       +testpgn 'Knights on the same same file can move to the same square, part 2'\
       +        '1. N5c4'\
       +        'rnbqkbnr/pppppppp/8/4N3/8/4N3/PPPPPPPP/R1BQKB1R w KQkq - 0 1'\
       +        'e5c4'
       +
       +testpgn 'Knights on the same same file can move to the same square, part 3'\
       +        '1. N5g4'\
       +        'rnbqkbnr/pppppppp/8/4N3/8/4N3/PPPPPPPP/R1BQKB1R w KQkq - 0 1'\
       +        'e5g4'
       +
       +testpgn 'Rook on the same file can move to the same square'\
       +        '1. R3d4'\
       +        'rnbqkbnr/pppppppp/3R4/8/8/3R4/PPPPPPPP/1NBQKBN1 w kq - 0 1'\
       +        'd3d4'
       +
       +testpgn 'Rook on the same file can move to the same square, part 2'\
       +        '1. R6d4'\
       +        'rnbqkbnr/pppppppp/3R4/8/8/3R4/PPPPPPPP/1NBQKBN1 w kq - 0 1'\
       +        'd6d4'
       +
       +testpgn 'Rook on the same rank can move to the same square'\
       +        '1. Rge4'\
       +        'rnbqkbnr/pppppppp/8/8/2R3R1/8/PPPPPPPP/1NBQKBN1 w kq - 0 1'\
       +        'g4e4'
       +
       +testpgn 'Rook on the same rank can move to the same square, part 2'\
       +        '1. Rce4'\
       +        'rnbqkbnr/pppppppp/8/8/2R3R1/8/PPPPPPPP/1NBQKBN1 w kq - 0 1'\
       +        'c4e4'
       +
       +testpgn 'Rook on the same rank can take on the same square (with check), part 3'\
       +        '1. Rgxe4+'\
       +        'rnbqkbnr/pppp1ppp/8/8/2R1p1R1/8/PPPPPPPP/1NBQKBN1 w kq - 0 1'\
       +        'g4e4'
       +
       +testpgn 'Rook on the same rank can take on the same square (with check), part 4'\
       +        '1. Rcxe4+'\
       +        'rnbqkbnr/pppp1ppp/8/8/2R1p1R1/8/PPPPPPPP/1NBQKBN1 w kq - 0 1'\
       +        'c4e4'
       +
       +testpgn 'Knights on the same same file can take on the same square'\
       +        '1. N3xc4'\
       +        'rnbqkbnr/pppppppp/8/4N3/2p5/4N3/PPPPPPPP/R1BQKB1R w KQkq - 0 1'\
       +        'e3c4'
       +
       +testpgn 'Knights on the same same file can take on the same square, part 2'\
       +        '1. N5xc4'\
       +        'rnbqkbnr/pppppppp/8/4N3/2p5/4N3/PPPPPPPP/R1BQKB1R w KQkq - 0 1'\
       +        'e5c4'
       +
       +testpgn 'Knights on same files and ranks move to same square'\
       +        '1. Nf3e5'\
       +        'rnbqkbnr/pppNpNpp/8/8/8/3N1N2/PPPPPPPP/R1BQKB1R w KQkq - 0 1'\
       +        'f3e5'
       +
       +testpgn 'Knights on same files and ranks move to same square, part 2'\
       +        '1. Nd7e5'\
       +        'rnbqkbnr/pppNpNpp/8/8/8/3N1N2/PPPPPPPP/R1BQKB1R w KQkq - 0 1'\
       +        'd7e5'
       +
       +testpgn 'Knights on same files and ranks move to same square, part 3'\
       +        '1. Nd3e5'\
       +        'rnbqkbnr/pppNpNpp/8/8/8/3N1N2/PPPPPPPP/R1BQKB1R w KQkq - 0 1'\
       +        'd3e5'
       +
       +testpgn '4 knights that can move to the same square, but not on the same file or ranks'\
       +        '1. Nfe5'\
       +        'rnbqkbnr/ppp1pNpp/2N5/8/6N1/3N4/PPPPPPPP/R1BQKB1R w KQkq - 0 1'\
       +        'f7e5'
       +
       +testpgn '4 knights that can move to the same square, but not on the same file or ranks, part 2'\
       +        '1. Nce5'\
       +        'rnbqkbnr/ppp1pNpp/2N5/8/6N1/3N4/PPPPPPPP/R1BQKB1R w KQkq - 0 1'\
       +        'c6e5'
       +
       +testpgn '4 knights that can move to the same square, but not on the same file or ranks, part 3'\
       +        '1. Nde5'\
       +        'rnbqkbnr/ppp1pNpp/2N5/8/6N1/3N4/PPPPPPPP/R1BQKB1R w KQkq - 0 1'\
       +        'd3e5'
       +
       +testpgn '4 knights that can move to the same square, but not on the same file or ranks, part 4'\
       +        '1. Nge5'\
       +        'rnbqkbnr/ppp1pNpp/2N5/8/6N1/3N4/PPPPPPPP/R1BQKB1R w KQkq - 0 1'\
       +        'g4e5'
       +
       +testpgn 'Move bishop, 2 bishops can move to the same square'\
       +        '1. Bge4'\
       +        'rnbqkbnr/pppppppp/6B1/8/8/3B4/PPPP1PPP/RNBQK1NR w KQkq - 0 1'\
       +        'g6e4'
       +
       +testpgn 'Move bishop, 2 bishops can move to the same square, part 2'\
       +        '1. Bde4'\
       +        'rnbqkbnr/pppppppp/6B1/8/8/3B4/PPPP1PPP/RNBQK1NR w KQkq - 0 1'\
       +        'd3e4'
       +
       +testpgn 'Move bishop, 2 bishops can move to the same square, part 3'\
       +        '1. Bde4'\
       +        'rnbqkbnr/pppppppp/2B3B1/8/8/2BB4/PPPP1PPP/RNBQK1NR w KQkq - 0 1'\
       +        'd3e4'
       +
       +# white takes en passant
       +testpgn 'white takes en passant'\
       +        '3. dxe6'\
       +        'rnbqkbnr/pppp1pp1/8/3Pp2p/8/8/PPP1PPPP/RNBQKBNR w KQkq e6 0 3'\
       +        'd5e6'
       +# black takes en passant
       +testpgn 'black takes en passant'\
       +        '3. ... hxg3'\
       +        'rnbqkbnr/ppppppp1/8/8/3P2Pp/4P3/PPP2P1P/RNBQKBNR b KQkq g3 0 3'\
       +        'h4g3'
       +
       +# NOTE: this is ambigous, but this is not dc6, because the en passant square is not set either.
       +testpgn 'white is checkmated (en passant square is not set), do not remove pawn (illegal move though)'\
       +        '1. c6'\
       +        'rnbqkbnr/p2ppppp/1p6/1PpPP3/2PKQ3/2PQQ3/P1PP1PPP/RNBQ1BNR w kq - 0 1'\
       +        'd5c6'
       +
       +testpgn 'white is not checkmated (en passant square is set and can be played), remove pawn'\
       +        '1. dxc6'\
       +        'rnbqkbnr/p2ppppp/1p6/1PpPP3/2PKQ3/2PQQ3/P1PP1PPP/RNBQ1BNR w kq c6 0 1'\
       +        'd5c6'
        }
        
        tests_fen
        tests_pgn
       +
       +if test "$statuscode" = "1"; then
       +        echo "$failed tests failed"
       +else
       +        echo "All tests OK"
       +fi
       +
       +exit "$statuscode"