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"