commit c8aed5b7783cae9c06f50dfcb5ab0af851edf54a
Author: Hiltjo Posthuma <hiltjo@codemadness.org>
Date: Sun, 17 Dec 2023 21:17:57 +0100
initial repo
Diffstat:
A | LICENSE | | | 15 | +++++++++++++++ |
A | Makefile | | | 11 | +++++++++++ |
A | README | | | 37 | +++++++++++++++++++++++++++++++++++++ |
A | fen_to_ascii.c | | | 276 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | fen_to_svg.c | | | 287 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | generate.sh | | | 138 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
6 files changed, 764 insertions(+), 0 deletions(-)
diff --git a/LICENSE b/LICENSE
@@ -0,0 +1,15 @@
+ISC License
+
+Copyright (c) 2023 Hiltjo Posthuma <hiltjo@codemadness.org>
+
+Permission to use, copy, modify, and/or distribute this software for any
+purpose with or without fee is hereby granted, provided that the above
+copyright notice and this permission notice appear in all copies.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
diff --git a/Makefile b/Makefile
@@ -0,0 +1,11 @@
+build: clean
+ ${CC} -o fen_to_svg fen_to_svg.c ${CFLAGS} ${LDFLAGS}
+ ${CC} -o fen_to_ascii fen_to_ascii.c ${CFLAGS} ${LDFLAGS}
+
+db:
+ rm -f lichess_db_puzzle.csv.zst lichess_db_puzzle.csv
+ curl -O 'https://database.lichess.org/lichess_db_puzzle.csv.zst'
+ zstd -d < lichess_db_puzzle.csv.zst > lichess_db_puzzle.csv
+
+clean:
+ rm -f fen_to_svg fen_to_ascii
diff --git a/README b/README
@@ -0,0 +1,37 @@
+chess puzzle book generator
+---------------------------
+
+This was a christmas hack for fun and non-profit.
+
+
+The generate.sh script generates puzzles.
+The puzzles used are from the lichess.org puzzle database.
+This database is a big CSV file.
+
+The generate index page is a HTML page, it lists the puzzles.
+Each puzzle is an SVG image.
+
+
+Files
+-----
+
+* generate.sh:
+ Read puzzles, shuffle them. Do some sorting based on difficulty and
+ assign score points.
+* fen_to_svg.c:
+ Read FEN and a few moves and generate an SVG image of the board.
+* fen_to_ascii.c:
+ Read FEN and a few moves and generate a text representation of the board.
+
+
+References
+----------
+
+* SVG of the pieces:
+ https://github.com/lichess-org/lila/tree/master/public/piece/cburnett
+
+* lichess FEN puzzle database
+ https://database.lichess.org/#puzzles
+
+* lichess.org
+ https://lichess.org/
diff --git a/fen_to_ascii.c b/fen_to_ascii.c
@@ -0,0 +1,276 @@
+/* TODO: option to flip board? */
+
+#include <stdio.h>
+#include <string.h>
+
+static char board[8][8];
+static char highlight[8][8];
+
+static int side_to_move = 'w'; /* default: white to move */
+static int white_can_castle[2] = { 0, 0 }; /* allow king side, allow queen side */
+static int black_can_castle[2] = { 0, 0 }; /* allow king side, allow queen side */
+
+static const int showcoords = 1; /* config: show board coordinates? */
+
+int
+isvalidsquare(int x, int y)
+{
+ return !(x < 0 || x >= 8 || y < 0 || y >= 8);
+}
+
+/* place a piece, if possible */
+void
+place(int piece, int x, int y)
+{
+ if (!isvalidsquare(x, y))
+ return;
+
+ board[y][x] = piece;
+}
+
+/* get piece, if possible */
+int
+getpiece(int x, int y)
+{
+ if (!isvalidsquare(x, y))
+ return 0;
+ return board[y][x];
+}
+
+int
+squaretoxy(const char *s, int *x, int *y)
+{
+ if (*s >= 'a' && *s <= 'h' &&
+ *(s + 1) >= '1' && *(s + 1) <= '8') {
+ *x = *s - 'a';
+ *y = '8' - *(s + 1);
+ return 1;
+ }
+ return 0;
+}
+
+void
+highlightmove(int x1, int y1, int x2, int y2)
+{
+ if (isvalidsquare(x1, y1))
+ highlight[y1][x1] = 1;
+
+ if (isvalidsquare(x2, y2))
+ highlight[y2][x2] = 1;
+}
+
+void
+showpiece(int c)
+{
+ const char *s = "";
+
+ /* simple or use unicode character */
+#if 1
+ putchar(c);
+ return;
+#endif
+
+ switch (c) {
+ case 'K': s = "♔"; break;
+ case 'Q': s = "♕"; break;
+ case 'R': s = "♖"; break;
+ case 'B': s = "♗"; break;
+ case 'N': s = "♘"; break;
+ case 'P': s = "♙"; break;
+ case 'k': s = "♚"; break;
+ case 'q': s = "♛"; break;
+ case 'r': s = "♜"; break;
+ case 'b': s = "♝"; break;
+ case 'n': s = "♞"; break;
+ case 'p': s = "♟"; break;
+ }
+
+ if (*s)
+ fputs(s, stdout);
+}
+
+void
+showboardfen(void)
+{
+ int x, y, piece, skip = 0;
+
+ for (y = 0; y < 8; y++) {
+ if (y > 0)
+ putchar('/');
+ skip = 0;
+ for (x = 0; x < 8; x++) {
+ piece = getpiece(x, y);
+ if (piece) {
+ if (skip)
+ putchar(skip + '0');
+ putchar(piece);
+ skip = 0;
+ } else {
+ skip++;
+ }
+ }
+ if (skip)
+ putchar(skip + '0');
+ }
+
+ /* ? TODO: detect en passant, invalid castling etc? */
+}
+
+/* show board */
+/* TODO: show fancier, unicode and background square color */
+/* TODO: use the output format similar to stockfish "d" command */
+void
+showboard(void)
+{
+ int x, y, piece;
+
+ printf("Board FEN:\n");
+ showboardfen();
+ printf("\n\n");
+
+ for (y = 0; y < 8; y++) {
+ printf("+---+---+---+---+---+---+---+---+\n");
+ for (x = 0; x < 8; x++) {
+ if (x == 0)
+ putchar('|');
+ fputs(" ", stdout);
+ piece = getpiece(x, y);
+ if (piece)
+ showpiece(piece);
+ else
+ fputs(" ", stdout);
+ fputs(" ", stdout);
+ putchar('|');
+ }
+ if (showcoords) {
+ putchar(' ');
+ putchar('8' - y);
+ }
+ putchar('\n');
+ }
+ printf("+---+---+---+---+---+---+---+---+\n");
+ if (showcoords)
+ printf(" a | b | c | d | e | f | g | h |\n");
+
+ fputs("\n", stdout);
+
+#if 0
+ if (side_to_move == 'w') {
+ fputs("White to move\n", stdout);
+ } else if (side_to_move == 'b')
+ fputs("Black to move\n", stdout);
+
+ if (white_can_castle[0])
+ fputs("White can castle king side\n", stdout);
+ if (white_can_castle[1])
+ fputs("White can castle queen side\n", stdout);
+ if (black_can_castle[0])
+ fputs("Black can castle king side\n", stdout);
+ if (black_can_castle[1])
+ fputs("Black can castle queen side\n", stdout);
+#endif
+}
+
+int
+main(int argc, char *argv[])
+{
+ const char *fen, *moves, *s;
+ int x, y, x2, y2, field, piece;
+ char pieces[] = "PNBRQKpnbrqk", square[3];
+
+ if (argc != 3) {
+ fprintf(stderr, "usage: %s <FEN> <moves>\n", argv[0]);
+ return 1;
+ }
+
+ fen = argv[1];
+ moves = argv[2];
+
+ /* initial board state, FEN format */
+ x = y = field = 0;
+ for (s = fen; *s; s++) {
+ /* next field, fields are: piece placement data, active color,
+ Castling availability, En passant target square,
+ Halfmove clock, Fullmove number */
+ if (*s == ' ') {
+ field++;
+ continue;
+ }
+
+ switch (field) {
+ case 0: /* piece placement data */
+ /* skip square */
+ if (*s >= '1' && *s <= '9') {
+ x += (*s - '0');
+ continue;
+ }
+ /* next rank */
+ if (*s == '/') {
+ x = 0;
+ y++;
+ continue;
+ }
+ /* is piece? place it */
+ if (strchr(pieces, *s))
+ place(*s, x++, y);
+ break;
+ case 1: /* active color */
+ if (*s == 'w' || *s == 'b')
+ side_to_move = *s;
+ break;
+ case 2: /* castling availability */
+ if (*s == '-') {
+ white_can_castle[0] = 0;
+ white_can_castle[1] = 0;
+ black_can_castle[0] = 0;
+ black_can_castle[1] = 0;
+ } else if (*s == 'K') {
+ white_can_castle[0] = 1;
+ } else if (*s == 'Q') {
+ white_can_castle[1] = 1;
+ } else if (*s == 'k') {
+ black_can_castle[0] = 1;
+ } else if (*s == 'q') {
+ black_can_castle[1] = 1;
+ }
+ break;
+ case 3: /* TODO: en-passant square, rest of the fields */
+ break;
+ }
+ /* TODO: parse which side to move, en-passant, etc */
+ }
+
+ /* process moves */
+ square[2] = '\0';
+ x = y = x2 = y2 = -1;
+ for (s = moves; *s; s++) {
+ if (*s == ' ')
+ continue;
+ if ((*s >= 'a' && *s <= 'h') &&
+ (*(s + 1) >= '1' && *(s + 1) <= '8') &&
+ (*(s + 2) >= 'a' && *(s + 2) <= 'h') &&
+ (*(s + 3) >= '1' && *(s + 3) <= '8')) {
+ square[0] = *s;
+ square[1] = *(s + 1);
+
+ s += 2;
+ squaretoxy(square, &x, &y);
+ piece = getpiece(x, y);
+
+ place(0, x, y); /* clear square */
+
+ /* place piece at new location */
+ square[0] = *s;
+ square[1] = *(s + 1);
+ squaretoxy(square, &x2, &y2);
+ place(piece, x2, y2);
+ s += 2;
+ }
+ }
+ /* highlight last move */
+ highlightmove(x, y, x2, y2);
+
+ showboard();
+
+ return 0;
+}
diff --git a/fen_to_svg.c b/fen_to_svg.c
@@ -0,0 +1,287 @@
+/* TODO: option to flip board? */
+
+#include <stdio.h>
+#include <string.h>
+
+static char board[8][8];
+static char highlight[8][8];
+
+static int side_to_move = 'w'; /* default: white to move */
+static int white_can_castle[2] = { 0, 0 }; /* allow king side, allow queen side */
+static int black_can_castle[2] = { 0, 0 }; /* allow king side, allow queen side */
+
+static const int showcoords = 1; /* config: show board coordinates? */
+
+int
+isvalidsquare(int x, int y)
+{
+ return !(x < 0 || x >= 8 || y < 0 || y >= 8);
+}
+
+/* place a piece, if possible */
+void
+place(int piece, int x, int y)
+{
+ if (!isvalidsquare(x, y))
+ return;
+
+ board[y][x] = piece;
+}
+
+/* get piece, if possible */
+int
+getpiece(int x, int y)
+{
+ if (!isvalidsquare(x, y))
+ return 0;
+ return board[y][x];
+}
+
+int
+squaretoxy(const char *s, int *x, int *y)
+{
+ if (*s >= 'a' && *s <= 'h' &&
+ *(s + 1) >= '1' && *(s + 1) <= '8') {
+ *x = *s - 'a';
+ *y = '8' - *(s + 1);
+ return 1;
+ }
+ return 0;
+}
+
+void
+highlightmove(int x1, int y1, int x2, int y2)
+{
+ if (isvalidsquare(x1, y1))
+ highlight[y1][x1] = 1;
+
+ if (isvalidsquare(x2, y2))
+ highlight[y2][x2] = 1;
+}
+
+void
+showpiece(int c)
+{
+ const char *s = "";
+
+ /* lichess default set,
+ extracted from https://github.com/lichess-org/lila/tree/master/public/piece/cburnett */
+ switch (c) {
+ case 'K': s = "<g fill=\"none\" fill-rule=\"evenodd\" stroke=\"#000\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M22.5 11.63V6M20 8h5\" stroke-linejoin=\"miter\"/><path d=\"M22.5 25s4.5-7.5 3-10.5c0 0-1-2.5-3-2.5s-3 2.5-3 2.5c-1.5 3 3 10.5 3 10.5\" fill=\"#fff\" stroke-linecap=\"butt\" stroke-linejoin=\"miter\"/><path d=\"M11.5 37c5.5 3.5 15.5 3.5 21 0v-7s9-4.5 6-10.5c-4-6.5-13.5-3.5-16 4V27v-3.5c-3.5-7.5-13-10.5-16-4-3 6 5 10 5 10V37z\" fill=\"#fff\"/><path d=\"M11.5 30c5.5-3 15.5-3 21 0m-21 3.5c5.5-3 15.5-3 21 0m-21 3.5c5.5-3 15.5-3 21 0\"/></g>"; break;
+ case 'Q': s = "<g fill=\"#fff\" fill-rule=\"evenodd\" stroke=\"#000\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M8 12a2 2 0 1 1-4 0 2 2 0 1 1 4 0zm16.5-4.5a2 2 0 1 1-4 0 2 2 0 1 1 4 0zM41 12a2 2 0 1 1-4 0 2 2 0 1 1 4 0zM16 8.5a2 2 0 1 1-4 0 2 2 0 1 1 4 0zM33 9a2 2 0 1 1-4 0 2 2 0 1 1 4 0z\"/><path d=\"M9 26c8.5-1.5 21-1.5 27 0l2-12-7 11V11l-5.5 13.5-3-15-3 15-5.5-14V25L7 14l2 12z\" stroke-linecap=\"butt\"/><path d=\"M9 26c0 2 1.5 2 2.5 4 1 1.5 1 1 .5 3.5-1.5 1-1.5 2.5-1.5 2.5-1.5 1.5.5 2.5.5 2.5 6.5 1 16.5 1 23 0 0 0 1.5-1 0-2.5 0 0 .5-1.5-1-2.5-.5-2.5-.5-2 .5-3.5 1-2 2.5-2 2.5-4-8.5-1.5-18.5-1.5-27 0z\" stroke-linecap=\"butt\"/><path d=\"M11.5 30c3.5-1 18.5-1 22 0M12 33.5c6-1 15-1 21 0\" fill=\"none\"/></g>"; break;
+ case 'R': s = "<g fill=\"#fff\" fill-rule=\"evenodd\" stroke=\"#000\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M9 39h27v-3H9v3zm3-3v-4h21v4H12zm-1-22V9h4v2h5V9h5v2h5V9h4v5\" stroke-linecap=\"butt\"/><path d=\"M34 14l-3 3H14l-3-3\"/><path d=\"M31 17v12.5H14V17\" stroke-linecap=\"butt\" stroke-linejoin=\"miter\"/><path d=\"M31 29.5l1.5 2.5h-20l1.5-2.5\"/><path d=\"M11 14h23\" fill=\"none\" stroke-linejoin=\"miter\"/></g>"; break;
+ case 'B': s = "<g fill=\"none\" fill-rule=\"evenodd\" stroke=\"#000\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><g fill=\"#fff\" stroke-linecap=\"butt\"><path d=\"M9 36c3.39-.97 10.11.43 13.5-2 3.39 2.43 10.11 1.03 13.5 2 0 0 1.65.54 3 2-.68.97-1.65.99-3 .5-3.39-.97-10.11.46-13.5-1-3.39 1.46-10.11.03-13.5 1-1.354.49-2.323.47-3-.5 1.354-1.94 3-2 3-2z\"/><path d=\"M15 32c2.5 2.5 12.5 2.5 15 0 .5-1.5 0-2 0-2 0-2.5-2.5-4-2.5-4 5.5-1.5 6-11.5-5-15.5-11 4-10.5 14-5 15.5 0 0-2.5 1.5-2.5 4 0 0-.5.5 0 2z\"/><path d=\"M25 8a2.5 2.5 0 1 1-5 0 2.5 2.5 0 1 1 5 0z\"/></g><path d=\"M17.5 26h10M15 30h15m-7.5-14.5v5M20 18h5\" stroke-linejoin=\"miter\"/></g>"; break;
+ case 'N': s = "<g fill=\"none\" fill-rule=\"evenodd\" stroke=\"#000\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M22 10c10.5 1 16.5 8 16 29H15c0-9 10-6.5 8-21\" fill=\"#fff\"/><path d=\"M24 18c.38 2.91-5.55 7.37-8 9-3 2-2.82 4.34-5 4-1.042-.94 1.41-3.04 0-3-1 0 .19 1.23-1 2-1 0-4.003 1-4-4 0-2 6-12 6-12s1.89-1.9 2-3.5c-.73-.994-.5-2-.5-3 1-1 3 2.5 3 2.5h2s.78-1.992 2.5-3c1 0 1 3 1 3\" fill=\"#fff\"/><path d=\"M9.5 25.5a.5.5 0 1 1-1 0 .5.5 0 1 1 1 0zm5.433-9.75a.5 1.5 30 1 1-.866-.5.5 1.5 30 1 1 .866.5z\" fill=\"#000\"/></g>"; break;
+ case 'P': s = "<path d=\"M22.5 9c-2.21 0-4 1.79-4 4 0 .89.29 1.71.78 2.38C17.33 16.5 16 18.59 16 21c0 2.03.94 3.84 2.41 5.03-3 1.06-7.41 5.55-7.41 13.47h23c0-7.92-4.41-12.41-7.41-13.47 1.47-1.19 2.41-3 2.41-5.03 0-2.41-1.33-4.5-3.28-5.62.49-.67.78-1.49.78-2.38 0-2.21-1.79-4-4-4z\" fill=\"#fff\" stroke=\"#000\" stroke-width=\"1.5\" stroke-linecap=\"round\"/>"; break;
+ case 'k': s = "<g fill=\"none\" fill-rule=\"evenodd\" stroke=\"#000\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M22.5 11.63V6\" stroke-linejoin=\"miter\"/><path d=\"M22.5 25s4.5-7.5 3-10.5c0 0-1-2.5-3-2.5s-3 2.5-3 2.5c-1.5 3 3 10.5 3 10.5\" fill=\"#000\" stroke-linecap=\"butt\" stroke-linejoin=\"miter\"/><path d=\"M11.5 37c5.5 3.5 15.5 3.5 21 0v-7s9-4.5 6-10.5c-4-6.5-13.5-3.5-16 4V27v-3.5c-3.5-7.5-13-10.5-16-4-3 6 5 10 5 10V37z\" fill=\"#000\"/><path d=\"M20 8h5\" stroke-linejoin=\"miter\"/><path d=\"M32 29.5s8.5-4 6.03-9.65C34.15 14 25 18 22.5 24.5l.01 2.1-.01-2.1C20 18 9.906 14 6.997 19.85c-2.497 5.65 4.853 9 4.853 9\" stroke=\"#ececec\"/><path d=\"M11.5 30c5.5-3 15.5-3 21 0m-21 3.5c5.5-3 15.5-3 21 0m-21 3.5c5.5-3 15.5-3 21 0\" stroke=\"#ececec\"/></g>"; break;
+ case 'q': s = "<g fill-rule=\"evenodd\" stroke=\"#000\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><g stroke=\"none\"><circle cx=\"6\" cy=\"12\" r=\"2.75\"/><circle cx=\"14\" cy=\"9\" r=\"2.75\"/><circle cx=\"22.5\" cy=\"8\" r=\"2.75\"/><circle cx=\"31\" cy=\"9\" r=\"2.75\"/><circle cx=\"39\" cy=\"12\" r=\"2.75\"/></g><path d=\"M9 26c8.5-1.5 21-1.5 27 0l2.5-12.5L31 25l-.3-14.1-5.2 13.6-3-14.5-3 14.5-5.2-13.6L14 25 6.5 13.5 9 26z\" stroke-linecap=\"butt\"/><path d=\"M9 26c0 2 1.5 2 2.5 4 1 1.5 1 1 .5 3.5-1.5 1-1.5 2.5-1.5 2.5-1.5 1.5.5 2.5.5 2.5 6.5 1 16.5 1 23 0 0 0 1.5-1 0-2.5 0 0 .5-1.5-1-2.5-.5-2.5-.5-2 .5-3.5 1-2 2.5-2 2.5-4-8.5-1.5-18.5-1.5-27 0z\" stroke-linecap=\"butt\"/><path d=\"M11 38.5a35 35 1 0 0 23 0\" fill=\"none\" stroke-linecap=\"butt\"/><path d=\"M11 29a35 35 1 0 1 23 0m-21.5 2.5h20m-21 3a35 35 1 0 0 22 0m-23 3a35 35 1 0 0 24 0\" fill=\"none\" stroke=\"#ececec\"/></g>"; break;
+ case 'r': s = "<g fill-rule=\"evenodd\" stroke=\"#000\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M9 39h27v-3H9v3zm3.5-7l1.5-2.5h17l1.5 2.5h-20zm-.5 4v-4h21v4H12z\" stroke-linecap=\"butt\"/><path d=\"M14 29.5v-13h17v13H14z\" stroke-linecap=\"butt\" stroke-linejoin=\"miter\"/><path d=\"M14 16.5L11 14h23l-3 2.5H14zM11 14V9h4v2h5V9h5v2h5V9h4v5H11z\" stroke-linecap=\"butt\"/><path d=\"M12 35.5h21m-20-4h19m-18-2h17m-17-13h17M11 14h23\" fill=\"none\" stroke=\"#ececec\" stroke-width=\"1\" stroke-linejoin=\"miter\"/></g>"; break;
+ case 'b': s = "<g fill=\"none\" fill-rule=\"evenodd\" stroke=\"#000\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><g fill=\"#000\" stroke-linecap=\"butt\"><path d=\"M9 36c3.39-.97 10.11.43 13.5-2 3.39 2.43 10.11 1.03 13.5 2 0 0 1.65.54 3 2-.68.97-1.65.99-3 .5-3.39-.97-10.11.46-13.5-1-3.39 1.46-10.11.03-13.5 1-1.354.49-2.323.47-3-.5 1.354-1.94 3-2 3-2z\"/><path d=\"M15 32c2.5 2.5 12.5 2.5 15 0 .5-1.5 0-2 0-2 0-2.5-2.5-4-2.5-4 5.5-1.5 6-11.5-5-15.5-11 4-10.5 14-5 15.5 0 0-2.5 1.5-2.5 4 0 0-.5.5 0 2z\"/><path d=\"M25 8a2.5 2.5 0 1 1-5 0 2.5 2.5 0 1 1 5 0z\"/></g><path d=\"M17.5 26h10M15 30h15m-7.5-14.5v5M20 18h5\" stroke=\"#ececec\" stroke-linejoin=\"miter\"/></g>"; break;
+ case 'n': s = "<g fill=\"none\" fill-rule=\"evenodd\" stroke=\"#000\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M22 10c10.5 1 16.5 8 16 29H15c0-9 10-6.5 8-21\" fill=\"#000\"/><path d=\"M24 18c.38 2.91-5.55 7.37-8 9-3 2-2.82 4.34-5 4-1.042-.94 1.41-3.04 0-3-1 0 .19 1.23-1 2-1 0-4.003 1-4-4 0-2 6-12 6-12s1.89-1.9 2-3.5c-.73-.994-.5-2-.5-3 1-1 3 2.5 3 2.5h2s.78-1.992 2.5-3c1 0 1 3 1 3\" fill=\"#000\"/><path d=\"M9.5 25.5a.5.5 0 1 1-1 0 .5.5 0 1 1 1 0zm5.433-9.75a.5 1.5 30 1 1-.866-.5.5 1.5 30 1 1 .866.5z\" fill=\"#ececec\" stroke=\"#ececec\"/><path d=\"M24.55 10.4l-.45 1.45.5.15c3.15 1 5.65 2.49 7.9 6.75S35.75 29.06 35.25 39l-.05.5h2.25l.05-.5c.5-10.06-.88-16.85-3.25-21.34-2.37-4.49-5.79-6.64-9.19-7.16l-.51-.1z\" fill=\"#ececec\" stroke=\"none\"/></g>"; break;
+ case 'p': s = "<path d=\"M22.5 9c-2.21 0-4 1.79-4 4 0 .89.29 1.71.78 2.38C17.33 16.5 16 18.59 16 21c0 2.03.94 3.84 2.41 5.03-3 1.06-7.41 5.55-7.41 13.47h23c0-7.92-4.41-12.41-7.41-13.47 1.47-1.19 2.41-3 2.41-5.03 0-2.41-1.33-4.5-3.28-5.62.49-.67.78-1.49.78-2.38 0-2.21-1.79-4-4-4z\" stroke=\"#000\" stroke-width=\"1.5\" stroke-linecap=\"round\"/>"; break;
+ }
+
+ if (*s)
+ fputs(s, stdout);
+}
+
+void
+showboardfen(void)
+{
+ int x, y, piece, skip = 0;
+
+ for (y = 0; y < 8; y++) {
+ if (y > 0)
+ putchar('/');
+ skip = 0;
+ for (x = 0; x < 8; x++) {
+ piece = getpiece(x, y);
+ if (piece) {
+ if (skip)
+ putchar(skip + '0');
+ putchar(piece);
+ skip = 0;
+ } else {
+ skip++;
+ }
+ }
+ if (skip)
+ putchar(skip + '0');
+ }
+
+ /* ? TODO: detect en passant, invalid castling etc? */
+}
+
+void
+showboard(void)
+{
+ /* lichess default theme colors */
+ const char *darksquare = "#b58863";
+ const char *lightsquare = "#f0d9b5";
+ const char *darksquarehi = "#aaa23a";
+ const char *lightsquarehi = "#cdd26a";
+ const char *color;
+ int x, y, piece;
+
+ fputs("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n"
+ "<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\n"
+ "<svg width=\"360\" height=\"360\" viewBox=\"0 0 360 360\" xmlns=\"http://www.w3.org/2000/svg\">\n"
+ "<rect fill=\"#fff\" stroke=\"#000\" x=\"0\" y=\"0\" width=\"360\" height=\"360\"/>\n", stdout);
+
+ fputs("<!-- Board FEN: ", stdout);
+ showboardfen();
+ fputs(" -->\n", stdout);
+
+ for (y = 0; y < 8; y++) {
+ for (x = 0; x < 8; x++) {
+ if (x % 2 == 0) {
+ if (y % 2 == 0)
+ color = highlight[y][x] ? lightsquarehi : lightsquare;
+ else
+ color = highlight[y][x] ? darksquarehi : darksquare;
+ } else {
+ if (y % 2 == 0)
+ color = highlight[y][x] ? darksquarehi : darksquare;
+ else
+ color = highlight[y][x] ? lightsquarehi : lightsquare;
+ }
+
+ printf("<g><rect x=\"%d\" y=\"%d\" width=\"45\" height=\"45\" fill=\"%s\"/></g>\n",
+ x * 45, y * 45, color);
+
+ piece = getpiece(x, y);
+ if (piece) {
+ printf("<g transform=\"translate(%d %d)\">", x * 45, y * 45);
+ showpiece(piece);
+ fputs("</g>\n", stdout);
+ }
+ }
+ }
+
+ if (showcoords) {
+ x = 7;
+ for (y = 0; y < 8; y++) {
+ if (y % 2 == 0)
+ color = highlight[y][x] ? lightsquarehi : lightsquare;
+ else
+ color = highlight[y][x] ? darksquarehi : darksquare;
+ printf("<text x=\"%d\" y=\"%d\" fill=\"%s\" dominant-baseline=\"hanging\" text-anchor=\"text-top\" style=\"font-family: sans-serif; font-size: 10px\">%c</text>\n",
+ (x * 45) + 37, (y * 45) + 3, color, '8' - y);
+ }
+ y = 7;
+ for (x = 0; x < 8; x++) {
+ if (x % 2 == 0)
+ color = highlight[y][x] ? lightsquarehi : lightsquare;
+ else
+ color = highlight[y][x] ? darksquarehi : darksquare;
+ printf("<text x=\"%d\" y=\"%d\" fill=\"%s\" dominant-baseline=\"text-bottom\" text-anchor=\"text-bottom\" style=\"font-family: sans-serif; font-size: 10px\">%c</text>\n",
+ (x * 45) + 2, (y + 1) * 45 - 3, color, x + 'a');
+ }
+ }
+
+ fputs("</svg>\n", stdout);
+}
+
+int
+main(int argc, char *argv[])
+{
+ const char *fen, *moves, *s;
+ int x, y, x2, y2, field, piece;
+ char pieces[] = "PNBRQKpnbrqk", square[3];
+
+ if (argc != 3) {
+ fprintf(stderr, "usage: %s <FEN> <moves>\n", argv[0]);
+ return 1;
+ }
+
+ fen = argv[1];
+ moves = argv[2];
+
+ /* initial board state, FEN format */
+ x = y = field = 0;
+ for (s = fen; *s; s++) {
+ /* next field, fields are: piece placement data, active color,
+ Castling availability, En passant target square,
+ Halfmove clock, Fullmove number */
+ if (*s == ' ') {
+ field++;
+ continue;
+ }
+
+ switch (field) {
+ case 0: /* piece placement data */
+ /* skip square */
+ if (*s >= '1' && *s <= '9') {
+ x += (*s - '0');
+ continue;
+ }
+ /* next rank */
+ if (*s == '/') {
+ x = 0;
+ y++;
+ continue;
+ }
+ /* is piece? place it */
+ if (strchr(pieces, *s))
+ place(*s, x++, y);
+ break;
+ case 1: /* active color */
+ if (*s == 'w' || *s == 'b')
+ side_to_move = *s;
+ break;
+ case 2: /* castling availability */
+ if (*s == '-') {
+ white_can_castle[0] = 0;
+ white_can_castle[1] = 0;
+ black_can_castle[0] = 0;
+ black_can_castle[1] = 0;
+ } else if (*s == 'K') {
+ white_can_castle[0] = 1;
+ } else if (*s == 'Q') {
+ white_can_castle[1] = 1;
+ } else if (*s == 'k') {
+ black_can_castle[0] = 1;
+ } else if (*s == 'q') {
+ black_can_castle[1] = 1;
+ }
+ break;
+ case 3: /* TODO: en-passant square, rest of the fields */
+ break;
+ }
+ /* TODO: parse which side to move, en-passant, etc */
+ }
+
+ /* process moves */
+ square[2] = '\0';
+ x = y = x2 = y2 = -1;
+ for (s = moves; *s; s++) {
+ if (*s == ' ')
+ continue;
+ if ((*s >= 'a' && *s <= 'h') &&
+ (*(s + 1) >= '1' && *(s + 1) <= '8') &&
+ (*(s + 2) >= 'a' && *(s + 2) <= 'h') &&
+ (*(s + 3) >= '1' && *(s + 3) <= '8')) {
+ square[0] = *s;
+ square[1] = *(s + 1);
+
+ s += 2;
+ squaretoxy(square, &x, &y);
+ piece = getpiece(x, y);
+
+ place(0, x, y); /* clear square */
+
+ /* place piece at new location */
+ square[0] = *s;
+ square[1] = *(s + 1);
+ squaretoxy(square, &x2, &y2);
+ place(piece, x2, y2);
+ s += 2;
+ }
+ }
+ /* highlight last move */
+ highlightmove(x, y, x2, y2);
+
+ showboard();
+
+ return 0;
+}
diff --git a/generate.sh b/generate.sh
@@ -0,0 +1,138 @@
+#!/bin/sh
+
+index="puzzles/index.html"
+mkdir -p puzzles
+
+cat > "$index" <<!
+<!DOCTYPE html>
+<html>
+<head>
+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+<title>Puzzles</title>
+<style type="text/css">
+body {
+ font-family: sans-serif;
+ width: 960px;
+ margin: 0 auto;
+ padding: 0 10px;
+}
+.puzzle {
+ float: left;
+ margin-right: 25px;
+}
+</style>
+</head>
+<body>
+<header>
+<h1>Puzzles, happy christmas mating!</h1>
+<!--<p>View the bottom of the SVG source for <a href="https://en.wikipedia.org/wiki/Forsyth%E2%80%93Edwards_Notation">OnlyFENs</a> :)</p>-->
+</header>
+<main>
+!
+
+# shuffle, some sort of order and point system based on rating of puzzle.
+
+db="lichess_db_puzzle.csv"
+count=1
+
+(grep 'mateIn1' < "$db" | shuf -n 100 | sed 10q
+grep 'mateIn2' < "$db" | shuf -n 100 | sed 10q
+grep 'mateIn3' < "$db" | shuf -n 100 | sed 10q
+grep 'mateIn4' < "$db" | shuf -n 100 | sed 10q
+LC_ALL=C awk -F ',' '(" " $8 " ") ~ / mateIn5 / && int($4) < 2000 { print $0 }' "$db" | shuf -n 100 | sed 5q
+LC_ALL=C awk -F ',' '(" " $8 " ") ~ / mateIn5 / && int($4) >= 2000 { print $0 }' "$db" | shuf -n 100 | sed 3q
+LC_ALL=C awk -F ',' '(" " $8 " ") ~ / mateIn5 / && int($4) >= 2700 { print $0 }' "$db" | shuf -n 100 | sed 2q
+) |
+LC_ALL=C awk -F ',' '
+{
+ points="1 point"; # default
+}
+(" " $8 " ") ~ / mateIn2 / {
+ points="2 points";
+}
+(" " $8 " ") ~ / mateIn3 / {
+ points="3 points";
+}
+(" " $8 " ") ~ / mateIn4 / {
+ points="4 points";
+}
+(" " $8 " ") ~ / mateIn5 / && int($4) < 2000 {
+ points="5 points";
+}
+(" " $8 " ") ~ / mateIn5 / && int($4) >= 2000 {
+ points="7 points";
+}
+(" " $8 " ") ~ / mateIn5 / && int($4) >= 2700 {
+ points="10 points";
+}
+{
+ print $0 "," points;
+}' | \
+while read -r line; do
+ i="$count"
+ fen=$(printf '%s' "$line" | cut -f 2 -d ',')
+ tomove=$(printf '%s' "$line" | cut -f 2 -d ',' | cut -f 2 -d ' ')
+ moves=$(printf '%s' "$line" | cut -f 3 -d ',' | cut -b 1-4 ) # first move only.
+ rating=$(printf '%s' "$line" | cut -f 4 -d ',')
+ ratingdev=$(printf '%s' "$line" | cut -f 5 -d ',')
+ lichess=$(printf '%s' "$line" | cut -f 9 -d ',')
+
+ # added field
+ points=$(printf '%s' "$line" | cut -f "11" -d ',')
+
+ img="$i.svg"
+ txt="$i.txt"
+ destsvg="puzzles/$img"
+ desttxt="puzzles/$txt"
+
+ ./fen_to_svg "$fen" "$moves" > "$destsvg"
+ ./fen_to_ascii "$fen" "$moves" > "$desttxt"
+
+ printf '<div class="puzzle">' >> "$index"
+ printf '<h2>Puzzle %s</h2>\n' "$i" >> "$index"
+ test "$lichess" != "" && printf '<a href="%s">' "$lichess" >> "$index"
+
+ title=""
+ test "$rating" != "" && title="Puzzle rating: $rating"
+
+ printf '<img src="%s" alt="Puzzle #%s" title="%s" width="360" height="360" loading="lazy" />' \
+ "$img" "$i" "$title" >> "$index"
+ test "$lichess" != "" && printf '</a>' >> "$index"
+ echo "" >> "$index"
+
+ case "$tomove" in
+ "w") tomove="w";;
+ "b") tomove="b";;
+ *) tomove="w";; # default
+ esac
+
+ movetext=""
+ # if there is a first move, inverse to move.
+ if test "moves" != ""; then
+ case "$tomove" in
+ "w") movetext=", black to move";;
+ "b") movetext=", white to move";;
+ esac
+ else
+ case "$tomove" in
+ "w") movetext=", white to move";;
+ "b") movetext=", black to move";;
+ esac
+ fi
+
+ printf '<p><b>%s</b>%s</p>\n' "$points" "$movetext" >> "$index"
+ printf '%s%s\n' "$points" "$movetext" >> "$desttxt"
+
+ printf '</div>\n' >> "$index"
+
+ # DEBUG
+ #echo "$count" >&2
+
+ count=$((count + 1))
+done
+
+cat >> "$index" <<!
+</main>
+</body>
+</html>
+!