various improvements - chess-puzzles - chess puzzle book generator | |
git clone git://git.codemadness.org/chess-puzzles | |
Log | |
Files | |
Refs | |
README | |
LICENSE | |
--- | |
commit cd3acc17b7b82e112bd5abbc65324283a7c5bf7f | |
parent f1daa80c1d6cf199c57d820e117255f38bd3d55e | |
Author: Hiltjo Posthuma <[email protected]> | |
Date: Thu, 4 Jan 2024 16:44:29 +0100 | |
various improvements | |
Some are: | |
* Colour themes. | |
* Initial PGN output. | |
* PGN piece localization. | |
* generate.sh: dutch localization. | |
* Highlight check or checkmate in red. | |
* Add a struct board and localize states. | |
* Add a CGI mode. | |
* Lichess example script, show more information about the players. | |
* Add dark mode CSS. | |
* Add a script to generate animated gifs for solutions. | |
* Documentation improvements. | |
* Many bugfixes. | |
Diffstat: | |
M LICENSE | 2 +- | |
M TODO | 15 +++++++++------ | |
M docs/stream_lichess.sh | 49 +++++++++++++++++++++++------… | |
M fen.1 | 41 +++++++++++++++++++++++++----… | |
M fen.c | 1120 +++++++++++++++++++++--------… | |
M generate.sh | 119 +++++++++++++++++++++++------… | |
A gifs.sh | 50 +++++++++++++++++++++++++++++… | |
M tests.sh | 56 +++++++++++++++++++++++++++--… | |
8 files changed, 1047 insertions(+), 405 deletions(-) | |
--- | |
diff --git a/LICENSE b/LICENSE | |
@@ -1,6 +1,6 @@ | |
ISC License | |
-Copyright (c) 2023 Hiltjo Posthuma <[email protected]> | |
+Copyright (c) 2023-2024 Hiltjo Posthuma <[email protected]> | |
Permission to use, copy, modify, and/or distribute this software for any | |
purpose with or without fee is hereby granted, provided that the above | |
diff --git a/TODO b/TODO | |
@@ -1,3 +1,8 @@ | |
+? canpiecemove(): en passant take (if not in check afterwards). | |
+? ischeckmated(): check en passant take to defend checkmate. | |
+ | |
+- rename fen.c to be more unique so it can be installed in $PATH. | |
+ | |
- option for output for annotating moves in a human-like way (for screenreader… | |
https://en.wikipedia.org/wiki/Portable_Game_Notation | |
PGN: | |
@@ -7,11 +12,9 @@ if so, the piece's file letter, numerical rank, or the exact … | |
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. " | |
-- rename fen.c to be more unique so it can be installed in $PATH. | |
- | |
-? output for PGN notation for moves: | |
- - mainly for solutions page. | |
- - maybe add some option to show it in tty/ASCII mode. | |
- | |
+- 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. | |
diff --git a/docs/stream_lichess.sh b/docs/stream_lichess.sh | |
@@ -10,16 +10,15 @@ if [ "$1" = "" ]; then | |
fi | |
gameid="$1" | |
-token="" # API token here. | |
+token="${LICHESS_TOKEN}" # API token here. | |
url="https://lichess.org/api/board/game/stream/$gameid" | |
# start position of classical chess. | |
fen="rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1" | |
-white="Anonymous" | |
-black="Anonymous" | |
firstline=1 | |
+header="" | |
# -N disables cURL buffering, each line is streamed as JSON. | |
curl \ | |
@@ -30,22 +29,46 @@ curl \ | |
-H 'Accept: application/x-ndjson' "$url" | \ | |
while read -r json; do | |
if [ "$firstline" = "1" ]; then | |
+ firstline="0" | |
+ | |
str=$(printf '%s' "$json" | jaq '$1 == ".initialFen" { print $… | |
test "$str" != "" && fen="$str" # override | |
- str=$(printf '%s' "$json" | jaq '$1 == ".white.name" { print $… | |
- test "$str" != "" && white="$str" # override | |
- | |
- str=$(printf '%s' "$json" | jaq '$1 == ".black.name" { print $… | |
- test "$str" != "" && black="$str" # override | |
- | |
- firstline="0" | |
+ header=$(printf '%s' "$json" | jaq ' | |
+BEGIN { | |
+ white_name = "Anonymous"; | |
+ black_name = "Anonymous"; | |
+} | |
+$2 == "?" || $3 == "" { next; } | |
+$1 == ".white.name" { white_name = $3; } | |
+$1 == ".white.rating" { white_rating = $3; } | |
+$1 == ".white.provisional" && $3 == "true" { white_provisional = "?"; } | |
+$1 == ".black.name" { black_name = $3; } | |
+$1 == ".black.rating" { black_rating = $3; } | |
+$1 == ".black.provisional" && $3 == "true" { black_provisional = "?"; } | |
+$1 == ".white.aiLevel" { white_name = "AI level " $3; } | |
+$1 == ".black.aiLevel" { black_name = "AI level " $3; } | |
+END { | |
+ white = white_name; | |
+ if (white_rating != "") | |
+ white = white " (" white_rating white_provisional ")"; | |
+ black = black_name; | |
+ if (black_rating != "") | |
+ black = black " (" black_rating black_provisional ")"; | |
+ print white " vs " black; | |
+}') | |
fi | |
- moves=$(printf '%s' "$json" | jaq '$1 == ".moves" { print $3; }') | |
- test "$moves" = "" && continue | |
+ str=$(printf '%s' "$json" | jaq '$1 == ".moves" { print $3; }') | |
+ test "$str" != "" && moves="$str" # override | |
clear | |
- printf '%s vs %s\n\n' "$white" "$black" | |
+ printf '%s\n\n' "$header" | |
./fen -o tty "$fen" "$moves" | |
+ | |
+ printf '\nMoves:\n' | |
+ printf '%s\n' "$moves" | |
+ | |
+ printf '\nPGN:\n' | |
+ ./fen -o pgn "$fen" "$moves" | |
done | |
diff --git a/fen.1 b/fen.1 | |
@@ -1,20 +1,23 @@ | |
-.Dd December 21, 2023 | |
+.Dd January 4, 2024 | |
.Dt FEN 1 | |
.Os | |
.Sh NAME | |
.Nm fen | |
-.Nd parses FEN and some moves and writes output | |
+.Nd parses chess FEN, plays moves and writes output | |
.Sh SYNOPSIS | |
.Nm | |
.Op Fl cCfF | |
-.\" .Op Fl o Ar ascii | fen | pgn | svg | tty | |
-.Op Fl o Ar ascii | fen | svg | tty | |
+.Op Fl m mapping | |
+.Op Fl o Ar ascii | fen | pgn | svg | tty | |
+.Op Fl t theme | |
.Op Ar FEN | |
.Op Ar moves | |
.Sh DESCRIPTION | |
.Nm | |
-parses the Forsyth-Edwards Notation (FEN) and some moves and write the output | |
-to a chosen format. | |
+parses the Forsyth-Edwards Notation (FEN) to setup the chess board. | |
+It then plays some | |
+.Ar moves | |
+and writes the output to a chosen format. | |
The options are as follows: | |
.Bl -tag -width Ds | |
.It Fl c | |
@@ -25,6 +28,11 @@ Disable board coordinates. | |
Flip the board, default is off. | |
.It Fl F | |
Do not flip the board. | |
+.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: | |
+"KDTLP". | |
+The default is: "KQRBN". | |
.It Fl o Ar format | |
Output format to one of the following format: | |
.Bl -tag -width Ds | |
@@ -33,8 +41,8 @@ ASCII text representation of the board. | |
FEN of the board state after playing the moves. | |
.It fen | |
FEN of the board state after playing the moves. | |
-.\" .It pgn | |
-.\" PGN output of the moves for the board. | |
+.It pgn | |
+PGN output of the moves for the board. | |
.It svg | |
SVG image of the board. | |
.It tty | |
@@ -42,6 +50,19 @@ Text representation of the board suitable for a terminal. | |
The terminal requires UTF-8 support for chess symbols and it uses truecolor for | |
the board theme. | |
.El | |
+.It Fl t Ar theme | |
+Use a colour theme for certain output formats, supported are the names: brown | |
+(default), green, grey. | |
+.El | |
+.Sh ENVIRONMENT VARIABLES | |
+.Bl -tag -width Ds | |
+.It Ev QUERY_STRING | |
+If this option is set | |
+.Nm | |
+will run in "CGI" mode suitable for a web server / HTTP daemon. | |
+This accepts the parameters: fen, moves, flip, coords and theme, similar to the | |
+command-line flags. | |
+It will serve a SVG of the chess board and moves. | |
.El | |
.Sh EXIT STATUS | |
.Ex -std | |
@@ -57,3 +78,7 @@ fen startpos e2e4 > board.svg | |
.Nm | |
supports classical chess and chess960 only. | |
Input moves are not validated, they are assumed to be legal. | |
+.Sh BUGS | |
+I hope it covers the case of taking en passant during a blood moon while a leap | |
+second ellapses. | |
+If it does not, please report it. | |
diff --git a/fen.c b/fen.c | |
@@ -15,28 +15,115 @@ | |
#define SETFGCOLOR(r,g,b) printf("\x1b[38;2;%d;%d;%dm", r, g, b) | |
#define SETBGCOLOR(r,g,b) printf("\x1b[48;2;%d;%d;%dm", r, g, b) | |
-enum outputmode { ModeASCII = 0, ModeFEN, ModePGN, ModeTTY, ModeSVG }; | |
+enum outputmode { ModeASCII = 0, ModeCGI, ModeFEN, ModePGN, ModeTTY, ModeSVG }; | |
enum outputmode outputmode = ModeSVG; | |
-static char board[8][8]; | |
-static char highlight[8][8]; | |
+/* localization of letter for PGN pieces */ | |
+const char *pgn_piecemapping = ""; | |
+ | |
+typedef unsigned char Color; | |
+ | |
+struct theme { | |
+ const char *name; | |
+ /* RGB */ | |
+ Color border[3]; | |
+ Color darksquare[3]; | |
+ Color lightsquare[3]; | |
+ Color darksquarehi[3]; | |
+ Color lightsquarehi[3]; | |
+ Color lightsquarecheck[3]; | |
+ Color darksquarecheck[3]; | |
+}; | |
+ | |
+struct theme themes[] = { | |
+/* lichess default brown theme colors (red, green, blue) */ | |
+{ | |
+ .name = "default", | |
+ .border = { 0x70, 0x49, 0x2d }, | |
+ .darksquare = { 0xb5, 0x88, 0x63 }, | |
+ .lightsquare = { 0xf0, 0xd9, 0xb5 }, | |
+ .darksquarehi = { 0xaa, 0xa2, 0x3a }, | |
+ .lightsquarehi = { 0xcd, 0xd2, 0x6a }, | |
+ .lightsquarecheck = { 0xff, 0x6a, 0x6a }, | |
+ .darksquarecheck = { 0xff, 0x3a, 0x3a } | |
+}, | |
+/* lichess green theme */ | |
+{ | |
+ .name = "green", | |
+ .border = { 0x33, 0x33, 0x33 }, | |
+ .darksquare = { 0x86, 0xa6, 0x66 }, | |
+ .lightsquare = { 0xff, 0xff, 0xdd }, | |
+ .darksquarehi = { 0x4f, 0xa1, 0x8e }, | |
+ .lightsquarehi = { 0x96, 0xd6, 0xd4 }, | |
+ .lightsquarecheck = { 0xff, 0x6a, 0x6a }, | |
+ .darksquarecheck = { 0xff, 0x3a, 0x3a } | |
+}, | |
+/* greyscale theme, highlight is still green */ | |
+{ | |
+ .name = "grey", | |
+ .border = { 0x00, 0x00, 0x00 }, | |
+ .darksquare = { 0x66, 0x66, 0x66 }, | |
+ .lightsquare = { 0xaa, 0xaa, 0xaa }, | |
+ .darksquarehi = { 0x66, 0x61, 0x23 }, | |
+ .lightsquarehi = { 0xa8, 0xab, 0x55 }, | |
+ .lightsquarecheck = { 0xff, 0x6a, 0x6a }, | |
+ .darksquarecheck = { 0xff, 0x3a, 0x3a } | |
+} | |
+}; | |
+ | |
+struct board { | |
+ char tiles[8][8]; /* board tiles and piece placement */ | |
+ int enpassantsquare[2]; /* default: no: { -1, -1 } */ | |
+ | |
+ char highlight[8][8]; /* highlighted squares, (0 = none, 1 = highli… | |
+ | |
+ int side_to_move; /* default: white to move: 'w' */ | |
+ int white_can_castle[2]; /* allow king side, allow queen side? default… | |
+ int black_can_castle[2]; /* allow king side, allow queen side? default… | |
+ | |
+ int movenumber; /* default: 1 */ | |
+ int halfmove; /* default: 0 */ | |
-static int side_to_move = 'w'; /* default: white to move */ | |
-static int white_can_castle[2] = { 0, 0 }; /* allow king side, allow queen sid… | |
-static int black_can_castle[2] = { 0, 0 }; /* allow king side, allow queen sid… | |
-static int enpassantsquare[2] = { -1, -1 }; | |
-static int movenumber = 1; | |
-static int halfmove = 0; | |
+ int flipboard; /* flip board ? default: 0 */ | |
+ int showcoords; /* board coordinates? default: 1 */ | |
-/* lichess default theme colors */ | |
-static const int border[] = { 0x70, 0x49, 0x2d }; | |
-static const int darksquare[] = { 0xb5, 0x88, 0x63 }; | |
-static const int lightsquare[] = { 0xf0, 0xd9, 0xb5 }; | |
-static const int darksquarehi[] = { 0xaa, 0xa2, 0x3a }; | |
-static const int lightsquarehi[] = { 0xcd, 0xd2, 0x6a }; | |
+ /* board theme */ | |
+ struct theme *theme; | |
+}; | |
-static int showcoords = 1; /* config: show board coordinates? */ | |
-static int flipboard = 0; /* config: flip board ? */ | |
+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]; | |
+ return b->theme; | |
+ } | |
+ } | |
+ return NULL; | |
+} | |
+ | |
+void | |
+board_init(struct board *b) | |
+{ | |
+ memset(b, 0, sizeof(*b)); /* zeroed fields by default */ | |
+ b->side_to_move = 'w'; | |
+ b->enpassantsquare[0] = -1; /* no en passant */ | |
+ b->enpassantsquare[1] = -1; | |
+ b->movenumber = 1; | |
+ b->flipboard = 0; | |
+ b->showcoords = 1; | |
+ b->theme = &themes[0]; /* use first theme as default */ | |
+} | |
+ | |
+void | |
+board_copy(struct board *bd, struct board *bs) | |
+{ | |
+ memcpy(bd, bs, sizeof(*bd)); | |
+} | |
void | |
pgn(const char *fmt, ...) | |
@@ -51,6 +138,28 @@ pgn(const char *fmt, ...) | |
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) | |
{ | |
@@ -79,25 +188,6 @@ isvalidpiece(int c) | |
return strchr(pieces, c) ? 1 : 0; | |
} | |
-/* 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 | |
xtofile(int c) | |
{ | |
@@ -134,24 +224,72 @@ squaretoxy(const char *s, int *x, int *y) | |
return 0; | |
} | |
+/* place a piece, if possible */ | |
void | |
-highlightmove(int x, int y) | |
+place(struct board *b, int piece, int x, int y) | |
+{ | |
+ if (!isvalidsquare(x, y)) | |
+ return; | |
+ | |
+ b->tiles[y][x] = piece; | |
+} | |
+ | |
+/* get piece, if possible */ | |
+int | |
+getpiece(struct board *b, int x, int y) | |
+{ | |
+ if (!isvalidsquare(x, y)) | |
+ return 0; | |
+ return b->tiles[y][x]; | |
+} | |
+ | |
+void | |
+highlightmove(struct board *b, int x, int y) | |
{ | |
if (isvalidsquare(x, y)) | |
- highlight[y][x] = 1; | |
+ b->highlight[y][x] = 1; | |
} | |
void | |
-showboardfen(void) | |
+highlightcheck(struct board *b, int x, int y) | |
{ | |
- int x, y, piece, skip = 0; | |
+ if (isvalidsquare(x, y)) | |
+ b->highlight[y][x] = 2; | |
+} | |
+ | |
+Color * | |
+getsquarecolor(struct board *b, int x, int y, int invert) | |
+{ | |
+ struct theme *t; | |
+ | |
+ t = b->theme; | |
+ if (((x % 2) ^ (y % 2)) == invert) { | |
+ switch (b->highlight[y][x]) { | |
+ case 1: return t->lightsquarehi; | |
+ case 2: return t->lightsquarecheck; | |
+ default: return t->lightsquare; | |
+ } | |
+ } else { | |
+ switch (b->highlight[y][x]) { | |
+ case 1: return t->darksquarehi; | |
+ case 2: return t->darksquarecheck; | |
+ default: return t->darksquare; | |
+ } | |
+ } | |
+ return t->lightsquare; /* never happens */ | |
+} | |
+ | |
+void | |
+showboardfen(struct board *b) | |
+{ | |
+ int x, y, piece, skip; | |
for (y = 0; y < 8; y++) { | |
if (y > 0) | |
putchar('/'); | |
skip = 0; | |
for (x = 0; x < 8; x++) { | |
- piece = getpiece(x, y); | |
+ piece = getpiece(b, x, y); | |
if (piece) { | |
if (skip) | |
putchar(skip + '0'); | |
@@ -164,27 +302,27 @@ showboardfen(void) | |
if (skip) | |
putchar(skip + '0'); | |
} | |
- printf(" %c ", side_to_move); | |
- if (white_can_castle[0]) | |
+ printf(" %c ", b->side_to_move); | |
+ if (b->white_can_castle[0]) | |
putchar('K'); | |
- if (white_can_castle[1]) | |
+ if (b->white_can_castle[1]) | |
putchar('Q'); | |
- if (black_can_castle[0]) | |
+ if (b->black_can_castle[0]) | |
putchar('k'); | |
- if (black_can_castle[1]) | |
+ if (b->black_can_castle[1]) | |
putchar('q'); | |
- if ((white_can_castle[0] + white_can_castle[1] + | |
- black_can_castle[0] + black_can_castle[1]) == 0) | |
+ if ((b->white_can_castle[0] + b->white_can_castle[1] + | |
+ b->black_can_castle[0] + b->black_can_castle[1]) == 0) | |
putchar('-'); /* no castling for either side */ | |
putchar(' '); | |
- if (enpassantsquare[0] != -1 && enpassantsquare[1] != -1) { | |
- putchar(xtofile(enpassantsquare[0])); | |
- putchar(ytorank(enpassantsquare[1])); | |
+ if (b->enpassantsquare[0] != -1 && b->enpassantsquare[1] != -1) { | |
+ putchar(xtofile(b->enpassantsquare[0])); | |
+ putchar(ytorank(b->enpassantsquare[1])); | |
} else { | |
putchar('-'); | |
} | |
- printf(" %d %d\n", halfmove, movenumber); | |
+ printf(" %d %d\n", b->halfmove, b->movenumber); | |
} | |
void | |
@@ -214,12 +352,12 @@ showpiece_svg(int c) | |
} | |
void | |
-output_svg(void) | |
+output_svg(struct board *b) | |
{ | |
+ Color *color; | |
const char *s; | |
char pieces[] = "pPKQRBNkqrbn"; /* pieces, check if they are used for … | |
unsigned char pieceused[LEN("pPKQRBNkqrbn")] = { 0 }; | |
- const int *color; | |
int i, ix, iy, x, y, piece; | |
fputs("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n" | |
@@ -230,7 +368,7 @@ output_svg(void) | |
for (i = 0; i < LEN(pieces); i++) { | |
for (y = 0; y < 8 && !pieceused[i]; y++) { | |
for (x = 0; x < 8; x++) { | |
- if (getpiece(x, y) == pieces[i]) { | |
+ if (getpiece(b, x, y) == pieces[i]) { | |
pieceused[i] = 1; | |
break; | |
} | |
@@ -267,20 +405,16 @@ output_svg(void) | |
fputs("</defs>\n", stdout); | |
for (iy = 0; iy < 8; iy++) { | |
- y = flipboard ? 7 - iy : iy; | |
+ y = b->flipboard ? 7 - iy : iy; | |
for (ix = 0; ix < 8; ix++) { | |
- x = flipboard ? 7 - ix : ix; | |
- | |
- if (((x % 2) ^ (y % 2)) == 0) | |
- color = highlight[y][x] ? lightsquarehi : ligh… | |
- else | |
- color = highlight[y][x] ? darksquarehi : darks… | |
+ x = b->flipboard ? 7 - ix : ix; | |
+ color = getsquarecolor(b, x, y, 0); | |
printf("<g><rect x=\"%d\" y=\"%d\" width=\"45\" height… | |
ix * 45, iy * 45, color[0], color[1], color[2]… | |
- piece = getpiece(x, y); | |
+ piece = getpiece(b, x, y); | |
if (piece) { | |
printf("<g transform=\"translate(%d %d)\">", i… | |
showpiece_svg(piece); | |
@@ -289,38 +423,23 @@ output_svg(void) | |
} | |
} | |
- if (showcoords) { | |
+ if (b->showcoords) { | |
ix = 7; | |
- x = flipboard ? 0 : 7; | |
+ x = b->flipboard ? 0 : 7; | |
for (iy = 0; iy < 8; iy++) { | |
- y = flipboard ? 7 - iy : iy; | |
- | |
+ y = b->flipboard ? 7 - iy : iy; | |
/* inverse square color for text */ | |
- if (((x % 2) ^ (y % 2)) == 0) | |
- color = highlight[y][x] ? darksquarehi : darks… | |
- else | |
- color = highlight[y][x] ? lightsquarehi : ligh… | |
+ color = getsquarecolor(b, x, y, 1); | |
printf("<text x=\"%d\" y=\"%d\" fill=\"#%02x%02x%02x\"… | |
(ix + 1) * 45 - 2, (iy * 45) + 10, color[0], c… | |
} | |
iy = 7; | |
- y = flipboard ? 0 : 7; | |
+ y = b->flipboard ? 0 : 7; | |
for (ix = 0; ix < 8; ix++) { | |
- x = flipboard ? 7 - ix : ix; | |
- | |
+ x = b->flipboard ? 7 - ix : ix; | |
/* inverse square color for text */ | |
- if (x % 2 == 0) { | |
- if (y % 2 == 0) | |
- color = highlight[y][x] ? darksquarehi… | |
- else | |
- color = highlight[y][x] ? lightsquareh… | |
- } else { | |
- if (y % 2 == 0) | |
- color = highlight[y][x] ? lightsquareh… | |
- else | |
- color = highlight[y][x] ? darksquarehi… | |
- } | |
+ color = getsquarecolor(b, x, y, 1); | |
printf("<text x=\"%d\" y=\"%d\" fill=\"#%02x%02x%02x\"… | |
(ix * 45) + 2, (iy + 1) * 45 - 3, color[0], co… | |
@@ -335,12 +454,7 @@ showpiece_tty(int c) | |
{ | |
const char *s = ""; | |
- /* simple or use unicode character */ | |
-#if 0 | |
- putchar(c); | |
- return; | |
-#endif | |
- | |
+ /* unicode characters */ | |
switch (c) { | |
case 'K': s = "â™”"; break; | |
case 'Q': s = "♕"; break; | |
@@ -362,34 +476,33 @@ showpiece_tty(int c) | |
/* show board */ | |
void | |
-output_tty(void) | |
+output_tty(struct board *b) | |
{ | |
- const int *color; | |
+ struct theme *t; | |
+ Color *color; | |
int ix, iy, x, y, piece; | |
- SETBGCOLOR(border[0], border[1], border[2]); | |
+ t = b->theme; | |
+ | |
+ SETBGCOLOR(t->border[0], t->border[1], t->border[2]); | |
fputs(" ", stdout); | |
printf("\x1b[0m"); /* reset */ | |
putchar('\n'); | |
for (iy = 0; iy < 8; iy++) { | |
- y = flipboard ? 7 - iy : iy; | |
+ y = b->flipboard ? 7 - iy : iy; | |
- SETBGCOLOR(border[0], border[1], border[2]); | |
+ SETBGCOLOR(t->border[0], t->border[1], t->border[2]); | |
fputs("\x1b[97m", stdout); /* bright white */ | |
fputs(" ", stdout); | |
for (ix = 0; ix < 8; ix++) { | |
- x = flipboard ? 7 - ix : ix; | |
- | |
- if (((x % 2) ^ (y % 2)) == 0) | |
- color = highlight[y][x] ? lightsquarehi : ligh… | |
- else | |
- color = highlight[y][x] ? darksquarehi : darks… | |
+ x = b->flipboard ? 7 - ix : ix; | |
+ color = getsquarecolor(b, x, y, 0); | |
SETBGCOLOR(color[0], color[1], color[2]); | |
fputs(" ", stdout); | |
- piece = getpiece(x, y); | |
+ piece = getpiece(b, x, y); | |
if (piece) { | |
if (piece >= 'A' && piece <= 'Z') | |
fputs("\x1b[97m", stdout); /* bright w… | |
@@ -405,9 +518,9 @@ output_tty(void) | |
} | |
printf("\x1b[0m"); /* reset */ | |
- color = border; | |
+ color = t->border; | |
SETBGCOLOR(color[0], color[1], color[2]); | |
- if (showcoords) { | |
+ if (b->showcoords) { | |
fputs("\x1b[97m", stdout); /* bright white */ | |
putchar(ytorank(y)); | |
putchar(' '); | |
@@ -418,14 +531,17 @@ output_tty(void) | |
printf("\x1b[0m"); /* reset */ | |
putchar('\n'); | |
} | |
- color = border; | |
+ color = t->border; | |
SETBGCOLOR(color[0], color[1], color[2]); | |
fputs("\x1b[97m", stdout); /* bright white */ | |
- if (showcoords) { | |
- if (flipboard) | |
- fputs(" h g f e d c b a ", stdout); | |
- else | |
- fputs(" a b c d e f g h ", stdout); | |
+ if (b->showcoords) { | |
+ fputs(" ", stdout); | |
+ for (iy = 0; iy < 8; iy++) { | |
+ y = b->flipboard ? 7 - iy : iy; | |
+ putchar(xtofile(y)); | |
+ fputs(" ", stdout); | |
+ } | |
+ fputs(" ", stdout); | |
} else { | |
fputs(" ", stdout); | |
} | |
@@ -441,36 +557,37 @@ showpiece_ascii(int c) | |
/* OnlyFENs */ | |
void | |
-output_fen(void) | |
+output_fen(struct board *b) | |
{ | |
- showboardfen(); | |
+ showboardfen(b); | |
} | |
/* show board */ | |
void | |
-output_ascii(void) | |
+output_ascii(struct board *b) | |
{ | |
- int hi[3] = { '>', ' ', '<' }; | |
- int dark[3] = { '.', '.', '.' }; | |
- int light[3] = { ' ', ' ', ' ' }; | |
- int *color, ix, iy, x, y, piece; | |
+ unsigned char hi[3] = { '>', ' ', '<' }; | |
+ unsigned char dark[3] = { '.', '.', '.' }; | |
+ unsigned char light[3] = { ' ', ' ', ' ' }; | |
+ unsigned char *color; | |
+ int ix, iy, x, y, piece; | |
for (iy = 0; iy < 8; iy++) { | |
- y = flipboard ? 7 - iy : iy; | |
+ y = b->flipboard ? 7 - iy : iy; | |
fputs("+---+---+---+---+---+---+---+---+\n", stdout); | |
for (ix = 0; ix < 8; ix++) { | |
- x = flipboard ? 7 - ix : ix; | |
+ x = b->flipboard ? 7 - ix : ix; | |
if (((x % 2) ^ (y % 2)) == 0) | |
- color = highlight[y][x] ? hi : light; | |
+ color = b->highlight[y][x] ? hi : light; | |
else | |
- color = highlight[y][x] ? hi : dark; | |
+ color = b->highlight[y][x] ? hi : dark; | |
if (ix == 0) | |
putchar('|'); | |
putchar(color[0]); | |
- piece = getpiece(x, y); | |
+ piece = getpiece(b, x, y); | |
if (piece) | |
showpiece_ascii(piece); | |
else | |
@@ -478,25 +595,29 @@ output_ascii(void) | |
putchar(color[2]); | |
putchar('|'); | |
} | |
- if (showcoords) { | |
+ if (b->showcoords) { | |
putchar(' '); | |
putchar(ytorank(y)); | |
} | |
putchar('\n'); | |
} | |
fputs("+---+---+---+---+---+---+---+---+\n", stdout); | |
- if (showcoords) { | |
- if (flipboard) | |
- printf(" h | g | f | e | d | c | b | a |\n"); | |
- else | |
- printf(" a | b | c | d | e | f | g | h |\n"); | |
+ if (b->showcoords) { | |
+ fputs(" ", stdout); | |
+ for (iy = 0; iy < 8; iy++) { | |
+ if (iy) | |
+ fputs(" | ", stdout); | |
+ y = b->flipboard ? 7 - iy : iy; | |
+ putchar(xtofile(y)); | |
+ } | |
+ fputs(" |\n", stdout); | |
} | |
fputs("\n", stdout); | |
} | |
int | |
-findking(int side, int *kingx, int *kingy) | |
+findking(struct board *b, int side, int *kingx, int *kingy) | |
{ | |
int king, x, y; | |
@@ -507,7 +628,7 @@ findking(int side, int *kingx, int *kingy) | |
/* find king */ | |
for (y = 0; y < 8; y++) { | |
for (x = 0; x < 8; x++) { | |
- if (getpiece(x, y) == king) { | |
+ if (getpiece(b, x, y) == king) { | |
*kingx = x; | |
*kingy = y; | |
return 1; | |
@@ -518,8 +639,9 @@ findking(int side, int *kingx, int *kingy) | |
} | |
int | |
-ischecked(int side) | |
+isincheck(struct board *b, int side) | |
{ | |
+ 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 }; | |
int line[] = { 1, 0, 0, 1, -1, 0, 0, -1 }; | |
int knight[] = { -1, -2, 1, -2, -1, 2, 1, 2, | |
@@ -530,18 +652,27 @@ ischecked(int side) | |
int piece; | |
/* find our king */ | |
- if (!findking(side, &kingx, &kingy)) | |
+ if (!findking(b, side, &kingx, &kingy)) | |
return 0; /* should not happen */ | |
+ /* check kings (illegal) */ | |
+ for (j = 0; j < LEN(king); j += 2) { | |
+ x = kingx + king[j]; | |
+ y = kingy + king[j + 1]; | |
+ piece = getpiece(b, x, y); | |
+ if (piece && strchr("kK", piece)) | |
+ return 1; | |
+ } | |
+ | |
/* check files and ranks (for queen and rook) */ | |
for (j = 0; j < LEN(line); j += 2) { | |
- for (i = 1; i < 8; i ++) { | |
+ for (i = 1; i < 8; i++) { | |
x = kingx + (i * line[j]); | |
y = kingy + (i * line[j + 1]); | |
- if (!(piece = getpiece(x, y))) | |
+ if (!(piece = getpiece(b, x, y))) | |
continue; | |
/* a piece is in front of it */ | |
- if (piece && strchr("bBnNpP", piece)) | |
+ if (piece && strchr("kKbBnNpP", piece)) | |
break; | |
/* own piece blocking/defending it */ | |
if ((side == 'w' && iswhitepiece(piece)) || | |
@@ -556,10 +687,10 @@ ischecked(int side) | |
for (i = 1; i < 8; i++) { | |
x = kingx + (i * diag[j]); | |
y = kingy + (i * diag[j + 1]); | |
- if (!(piece = getpiece(x, y))) | |
+ if (!(piece = getpiece(b, x, y))) | |
continue; | |
/* a piece is in front of it */ | |
- if (piece && strchr("rRnNpP", piece)) | |
+ if (piece && strchr("kKrRnNpP", piece)) | |
break; | |
/* own piece blocking/defending it */ | |
if ((side == 'w' && iswhitepiece(piece)) || | |
@@ -574,55 +705,189 @@ ischecked(int side) | |
for (j = 0; j < LEN(knight); j += 2) { | |
x = kingx + knight[j]; | |
y = kingy + knight[j + 1]; | |
-// highlightmove(x, y); /* DEBUG */ | |
- if (getpiece(x, y) == piece) | |
+ if (getpiece(b, x, y) == piece) | |
return 1; | |
} | |
/* check pawns */ | |
if (side == 'w') { | |
- if (getpiece(kingx - 1, kingy - 1) == 'p' || | |
- getpiece(kingx + 1, kingy - 1) == 'p') | |
+ if (getpiece(b, kingx - 1, kingy - 1) == 'p' || | |
+ getpiece(b, kingx + 1, kingy - 1) == 'p') | |
return 1; | |
} else if (side == 'b') { | |
- if (getpiece(kingx - 1, kingy + 1) == 'P' || | |
- getpiece(kingx + 1, kingy + 1) == 'P') | |
+ if (getpiece(b, kingx - 1, kingy + 1) == 'P' || | |
+ getpiece(b, kingx + 1, kingy + 1) == 'P') | |
return 1; | |
} | |
return 0; | |
} | |
-/* TODO */ | |
int | |
-ischeckmated(int side) | |
+trypiecemove(struct board *b, int side, int piece, int x1, int y1, int x2, int… | |
{ | |
- int kingx, kingy; | |
+ struct board tb; | |
+ | |
+ board_copy(&tb, b); | |
+ place(&tb, 0, x1, y1); | |
+ place(&tb, piece, x2, y2); | |
+ | |
+ /* would put in check / still in check, not allowed */ | |
+ return !isincheck(&tb, side); | |
+} | |
+ | |
+/* 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 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 }; | |
+ int line[] = { 1, 0, 0, 1, -1, 0, 0, -1 }; | |
+ int knight[] = { -1, -2, 1, -2, -1, 2, 1, 2, | |
+ -2, -1, 2, -1, -2, 1, 2, 1 | |
+ }; | |
+ int i, j, dir, x, y; | |
+ int piece, takepiece; | |
- // TODO: can king move out check, without being checked? | |
- // TODO: can a piece defend it. | |
- // TODO: can the checking piece get taken without still being checked? | |
- // TODO: handle double-checks. | |
- // TODO: separate board state or just move and undo move? | |
+ if (!(piece = getpiece(b, x1, y1))) | |
+ return 0; /* theres no piece so it cannot be moved */ | |
- if (!ischecked(side)) | |
+ /* can't move opponent piece */ | |
+ if ((side == 'w' && isblackpiece(piece)) || | |
+ (side == 'b' && iswhitepiece(piece))) | |
return 0; | |
- /* find our king */ | |
- if (!findking(side, &kingx, &kingy)) | |
- return 0; /* should not happen */ | |
+ if ((takepiece = getpiece(b, x2, y2))) { | |
+ /* can't take your own piece */ | |
+ if ((side == 'w' && iswhitepiece(takepiece)) || | |
+ (side == 'b' && isblackpiece(takepiece))) | |
+ return 0; | |
+ } | |
+ | |
+ /* king movement */ | |
+ if (piece == 'K' || piece == 'k') { | |
+ for (j = 0; j < LEN(king); j += 2) { | |
+ if (x1 + king[j] == x2 && y1 + king[j + 1] == y2) | |
+ goto trymove; | |
+ } | |
+ } | |
+ | |
+ /* check files and ranks (for queen and rook) */ | |
+ if (piece == 'Q' || piece == 'q' || piece == 'R' || piece == 'r') { | |
+ for (j = 0; j < LEN(line); j += 2) { | |
+ for (i = 1; i < 8; i++) { | |
+ x = x1 + (i * line[j]); | |
+ y = y1 + (i * line[j + 1]); | |
+ | |
+ if (x == x2 && y == y2 && | |
+ trypiecemove(b, side, piece, x1, y1, x2, y… | |
+ return 1; | |
+ | |
+ /* a piece is in front of it: stop this checki… | |
+ if (getpiece(b, x, y)) | |
+ break; | |
+ } | |
+ } | |
+ } | |
+ | |
+ /* check diagonals (queen and bishop) */ | |
+ if (piece == 'Q' || piece == 'q' || piece == 'B' || piece == 'b') { | |
+ for (j = 0; j < LEN(diag); j += 2) { | |
+ for (i = 1; i < 8; i++) { | |
+ x = x1 + (i * diag[j]); | |
+ y = y1 + (i * diag[j + 1]); | |
+ if (x == x2 && y == y2 && | |
+ trypiecemove(b, side, piece, x1, y1, x2, y… | |
+ return 1; | |
+ | |
+ /* a piece is in front of it: stop this checki… | |
+ if (getpiece(b, x, y)) | |
+ break; | |
+ } | |
+ } | |
+ } | |
+ | |
+ /* knight movement */ | |
+ if (piece == 'N' || piece == 'n') { | |
+ for (j = 0; j < LEN(knight); j += 2) { | |
+ if (x1 + knight[j] == x2 && y1 + knight[j + 1] == y2) | |
+ goto trymove; | |
+ } | |
+ } | |
+ /* pawn move */ | |
+ if (piece == 'P' || piece == 'p') { | |
+ /* direction */ | |
+ dir = piece == 'P' ? -1 : +1; | |
+ j = piece == 'P' ? 6 : 1; /* start row */ | |
+ | |
+ if (takepiece == 0) { | |
+ if (x1 != x2) | |
+ return 0; /* move on same file */ | |
+ /* start move: can be 2 moves */ | |
+ if (y1 == j && y2 == y1 + (2 * dir)) { | |
+ /* square must be empty */ | |
+ if (getpiece(b, x1, y1 + dir)) | |
+ return 0; | |
+ goto trymove; | |
+ } | |
+ /* normal: check for one move */ | |
+ if (y2 != y1 + dir) | |
+ return 0; | |
+ goto trymove; | |
+ } else { | |
+ /* pawn takes, normal case */ | |
+ if ((x2 == x1 - 1 || x2 == x1 + 1) && | |
+ (y2 == y1 + dir)) | |
+ goto trymove; | |
+ } | |
+ } | |
return 0; | |
+ | |
+/* previous checks for move succeeded, actually try move with the current | |
+ board state */ | |
+trymove: | |
+ return trypiecemove(b, side, piece, x1, y1, x2, y2); | |
+} | |
+ | |
+int | |
+ischeckmated(struct board *b, int side) | |
+{ | |
+ int x, y, x2, y2, piece; | |
+ | |
+ /* check pieces that can block or take a piece that removes the check … | |
+ for (y = 0; y < 8; y++) { | |
+ for (x = 0; x < 8; x++) { | |
+ piece = getpiece(b, x, y); | |
+ if ((side == 'w' && !iswhitepiece(piece)) || | |
+ (side == 'b' && !isblackpiece(piece))) | |
+ continue; | |
+ | |
+ for (y2 = 0; y2 < 8; y2++) { | |
+ for (x2 = 0; x2 < 8; x2++) { | |
+ /* can piece move and afterwards we ar… | |
+ if (canpiecemove(b, side, x, y, x2, y2… | |
+ return 0; | |
+ } | |
+ } | |
+ } | |
+ } | |
+ | |
+ return 1; | |
} | |
void | |
-parsefen(const char *fen) | |
+board_setup_fen(struct board *b, const char *fen) | |
{ | |
char square[3]; | |
const char *s; | |
long l; | |
int x, y, field; | |
+ if (!strcmp(fen, "startpos")) | |
+ fen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 … | |
+ | |
square[2] = '\0'; | |
/* initial board state, FEN format */ | |
@@ -643,26 +908,26 @@ parsefen(const char *fen) | |
} | |
/* is piece? place it */ | |
if (isvalidpiece(*s)) | |
- place(*s, x++, y); | |
+ place(b, *s, x++, y); | |
break; | |
case 1: /* active color */ | |
if (*s == 'w' || *s == 'b') | |
- side_to_move = *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; | |
+ b->white_can_castle[0] = 0; | |
+ b->white_can_castle[1] = 0; | |
+ b->black_can_castle[0] = 0; | |
+ b->black_can_castle[1] = 0; | |
} else if (*s == 'K') { | |
- white_can_castle[0] = 1; | |
+ b->white_can_castle[0] = 1; | |
} else if (*s == 'Q') { | |
- white_can_castle[1] = 1; | |
+ b->white_can_castle[1] = 1; | |
} else if (*s == 'k') { | |
- black_can_castle[0] = 1; | |
+ b->black_can_castle[0] = 1; | |
} else if (*s == 'q') { | |
- black_can_castle[1] = 1; | |
+ b->black_can_castle[1] = 1; | |
} | |
break; | |
case 3: /* en passant square */ | |
@@ -672,8 +937,8 @@ parsefen(const char *fen) | |
square[1] = *(s + 1); | |
squaretoxy(square, &x, &y); | |
- enpassantsquare[0] = x; | |
- enpassantsquare[1] = y; | |
+ b->enpassantsquare[0] = x; | |
+ b->enpassantsquare[1] = y; | |
} | |
break; | |
case 4: /* halfmove */ | |
@@ -682,7 +947,7 @@ parsefen(const char *fen) | |
l = strtol(s, NULL, 10); | |
if (l >= 0 && l < 32767) { | |
- halfmove = l; | |
+ b->halfmove = l; | |
for (; *s && isdigit((unsigned char)*s); s++) | |
; | |
@@ -694,7 +959,7 @@ parsefen(const char *fen) | |
l = strtol(s, NULL, 10); | |
if (l >= 0 && l < 32767) { | |
- movenumber = (int)l; | |
+ b->movenumber = (int)l; | |
for (; *s && isdigit((unsigned char)*s); s++) | |
; | |
} | |
@@ -714,178 +979,216 @@ parsefen(const char *fen) | |
} | |
void | |
-parsemoves(const char *moves) | |
+board_playmoves(struct board *b, const char *moves) | |
{ | |
char square[3]; | |
const char *castled, *s; | |
- int firstmove, i, x, y, x2, y2, piece, takepiece, tookpiece; | |
+ int firstmove, i, j, x, y, x2, y2, otherside, piece, takepiece, tookpi… | |
+ int needfile, needrank, promote; | |
/* process moves */ | |
square[2] = '\0'; | |
x = y = x2 = y2 = -1; | |
firstmove = 1; | |
+ /* clear previous highlights */ | |
+ memset(&(b->highlight), 0, sizeof(b->highlight)); | |
for (s = moves; *s; s++) { | |
if (*s == ' ') | |
continue; | |
- if ((*s >= 'a' && *s <= 'h') && | |
+ if (!((*s >= 'a' && *s <= 'h') && | |
(*(s + 1) >= '1' && *(s + 1) <= '8') && | |
(*(s + 2) >= 'a' && *(s + 2) <= 'h') && | |
- (*(s + 3) >= '1' && *(s + 3) <= '8')) { | |
- /* TODO: if first move but its blacks turn, prefix PGN | |
- with "..." or something, since the white move was u… | |
+ (*(s + 3) >= '1' && *(s + 3) <= '8'))) | |
+ continue; | |
- if (firstmove) | |
- firstmove = 0; | |
- else | |
- pgn(" "); | |
+ otherside = b->side_to_move == 'b' ? 'w' : 'b'; | |
- square[0] = *s; | |
- square[1] = *(s + 1); | |
+ /* if first move and it is blacks turn, prefix | |
+ with "...", because the white move was unknown */ | |
+ if (firstmove && b->side_to_move == 'b') | |
+ pgn("%d. ... ", b->movenumber); | |
- s += 2; | |
- squaretoxy(square, &x, &y); | |
- piece = getpiece(x, y); | |
+ if (firstmove) | |
+ firstmove = 0; | |
+ else | |
+ pgn(" "); | |
- /* target location */ | |
- square[0] = *s; | |
- square[1] = *(s + 1); | |
- squaretoxy(square, &x2, &y2); | |
+ square[0] = *s; | |
+ square[1] = *(s + 1); | |
- /* place piece at new location */ | |
- place(0, x, y); /* clear previous square */ | |
- /* take piece (can be your own) */ | |
- takepiece = getpiece(x2, y2); | |
+ s += 2; | |
+ squaretoxy(square, &x, &y); | |
+ piece = getpiece(b, x, y); | |
- s += 2; | |
+ /* target location */ | |
+ square[0] = *s; | |
+ square[1] = *(s + 1); | |
+ squaretoxy(square, &x2, &y2); | |
- /* took piece of opponent */ | |
- tookpiece = (side_to_move == 'w' && isblackpiece(takep… | |
- (side_to_move == 'b' && iswhitepiece(takep… | |
+ /* take piece (can be your own) */ | |
+ takepiece = getpiece(b, x2, y2); | |
- /* if pawn move or taken a piece increase halfmove cou… | |
- if (piece == 'p' || piece == 'P' || tookpiece) | |
- halfmove = 0; | |
- else | |
- halfmove++; | |
- | |
- if (side_to_move == 'w') | |
- pgn("%d. ", movenumber); | |
- | |
- /* castled this move? for PGN */ | |
- castled = NULL; | |
- | |
- /* castling */ | |
- if (piece == 'K' && y == 7 && y2 == 7) { | |
- /* white: kingside castling */ | |
- if (x2 > x + 1 || takepiece == 'R') { | |
- for (i = x2; i < 8; i++) { | |
- if (getpiece(i, y2) == 'R') { | |
- place(0, i, y2); /* cl… | |
- place('R', x2 - 1, y2)… | |
- castled = "O-O"; | |
- break; | |
- } | |
- } | |
- } else if (x2 < x - 1 || takepiece == 'R') { | |
- /* white: queenside castling */ | |
- for (i = x2; i >= 0; i--) { | |
- if (getpiece(i, y2) == 'R') { | |
- place('R', x2 + 1, y2)… | |
- place(0, i, y2); /* cl… | |
- castled = "O-O-O"; | |
- break; | |
- } | |
+ s += 2; | |
+ | |
+ promote = 0; | |
+ if (*s == 'q' || *s == 'b' || *s == 'n') { | |
+ promote = *s; | |
+ s++; | |
+ } | |
+ | |
+ /* took piece of opponent */ | |
+ tookpiece = (b->side_to_move == 'w' && isblackpiece(takepiece)… | |
+ (b->side_to_move == 'b' && iswhitepiece(takepiece)… | |
+ | |
+ /* if pawn move or taken a piece increase halfmove counter */ | |
+ if (piece == 'p' || piece == 'P' || tookpiece) | |
+ b->halfmove = 0; | |
+ else | |
+ b->halfmove++; | |
+ | |
+ if (b->side_to_move == 'w') | |
+ pgn("%d. ", b->movenumber); | |
+ | |
+ /* castled this move? */ | |
+ castled = NULL; | |
+ | |
+ /* castling */ | |
+ if (piece == 'K' && y == 7 && y2 == 7) { | |
+ /* white: kingside castling */ | |
+ if (x2 > x + 1 || takepiece == 'R') { | |
+ for (i = x2; i < 8; i++) { | |
+ if (getpiece(b, i, y2) == 'R') { | |
+ place(b, 0, x, y); /* clear pr… | |
+ place(b, 0, i, y2); /* clear r… | |
+ place(b, 'R', x2 - 1, y2); /* … | |
+ castled = "O-O"; | |
+ break; | |
} | |
} | |
- } else if (piece == 'k' && y == 0 && y2 == 0) { | |
- /* black: kingside castling */ | |
- if (x2 > x + 1 || takepiece == 'r') { | |
- for (i = x2; i < 8; i++) { | |
- if (getpiece(i, y2) == 'r') { | |
- place(0, i, y2); /* cl… | |
- place('r', x2 - 1, y2)… | |
- castled = "O-O"; | |
- break; | |
- } | |
- } | |
- } else if (x2 < x - 1 || takepiece == 'R') { | |
- /* black: queenside castling */ | |
- for (i = x2; i >= 0; i--) { | |
- if (getpiece(i, y2) == 'r') { | |
- place('r', x2 + 1, y2)… | |
- place(0, i, y2); /* cl… | |
- castled = "O-O-O"; | |
- break; | |
- } | |
+ } else if (x2 < x - 1 || takepiece == 'R') { | |
+ /* white: queenside castling */ | |
+ for (i = x2; i >= 0; i--) { | |
+ if (getpiece(b, i, y2) == 'R') { | |
+ place(b, 0, x, y); /* clear pr… | |
+ place(b, 'R', x2 + 1, y2); /* … | |
+ place(b, 0, i, y2); /* clear r… | |
+ castled = "O-O-O"; | |
+ break; | |
} | |
} | |
} | |
- | |
- /* remove the ability to castle */ | |
- if (piece == 'K') { | |
- white_can_castle[0] = white_can_castle[1] = 0; | |
- } else if (piece == 'k') { | |
- black_can_castle[0] = black_can_castle[1] = 0; | |
- } else if (piece == 'R' && y == 7) { | |
- for (i = 0; i < 8; i++) { | |
- if (getpiece(i, y) == 'K') { | |
- if (i < x) | |
- white_can_castle[0] = … | |
- else if (i > x) | |
- white_can_castle[1] = … | |
+ } else if (piece == 'k' && y == 0 && y2 == 0) { | |
+ /* black: kingside castling */ | |
+ if (x2 > x + 1 || takepiece == 'r') { | |
+ for (i = x2; i < 8; i++) { | |
+ if (getpiece(b, i, y2) == 'r') { | |
+ place(b, 0, x, y); /* clear pr… | |
+ place(b, 0, i, y2); /* clear r… | |
+ place(b, 'r', x2 - 1, y2); /* … | |
+ castled = "O-O"; | |
break; | |
} | |
} | |
- } else if (piece == 'r' && y == 0) { | |
- for (i = 0; i < 8; i++) { | |
- if (getpiece(i, y) == 'k') { | |
- if (i > x) | |
- black_can_castle[1] = … | |
- else if (i < x) | |
- black_can_castle[0] = … | |
+ } else if (x2 < x - 1 || takepiece == 'R') { | |
+ /* black: queenside castling */ | |
+ for (i = x2; i >= 0; i--) { | |
+ if (getpiece(b, i, y2) == 'r') { | |
+ place(b, 0, x, y); /* clear pr… | |
+ place(b, 'r', x2 + 1, y2); /* … | |
+ place(b, 0, i, y2); /* clear r… | |
+ castled = "O-O-O"; | |
break; | |
} | |
} | |
} | |
+ } | |
- /* taken en passant? */ | |
- if (x2 == enpassantsquare[0] && y2 == enpassantsquare[… | |
- place(0, x2, piece == 'P' ? y2 + 1 : y2 - 1); | |
- | |
- /* the en passant square resets after a move */ | |
- enpassantsquare[0] = -1; | |
- enpassantsquare[1] = -1; | |
- /* moved 2 squares and there is an opponent pawn next … | |
- if (piece == 'P' && y == 6 && y2 == 4) { | |
- if (getpiece(x - 1, y2) == 'p' || | |
- getpiece(x + 1, y2) == 'p') { | |
- enpassantsquare[0] = x; | |
- enpassantsquare[1] = 5; | |
+ /* remove the ability to castle */ | |
+ if (piece == 'K') { | |
+ b->white_can_castle[0] = b->white_can_castle[1] = 0; | |
+ } else if (piece == 'k') { | |
+ b->black_can_castle[0] = b->black_can_castle[1] = 0; | |
+ } else if (piece == 'R' && y == 7) { | |
+ for (i = 0; i < 8; i++) { | |
+ if (getpiece(b, i, y) == 'K') { | |
+ if (i < x) | |
+ b->white_can_castle[0] = 0; | |
+ else if (i > x) | |
+ b->white_can_castle[1] = 0; | |
+ break; | |
} | |
- } else if (piece == 'p' && y == 1 && y2 == 3) { | |
- if (getpiece(x - 1, y2) == 'P' || | |
- getpiece(x + 1, y2) == 'P') { | |
- enpassantsquare[0] = x; | |
- enpassantsquare[1] = 2; | |
+ } | |
+ } else if (piece == 'r' && y == 0) { | |
+ for (i = 0; i < 8; i++) { | |
+ if (getpiece(b, i, y) == 'k') { | |
+ if (i > x) | |
+ b->black_can_castle[1] = 0; | |
+ else if (i < x) | |
+ b->black_can_castle[0] = 0; | |
+ break; | |
} | |
} | |
+ } | |
- /* place piece */ | |
- place(piece, x2, y2); | |
+ /* taken en passant? */ | |
+ if (x2 == b->enpassantsquare[0] && y2 == b->enpassantsquare[1]) | |
+ place(b, 0, x2, piece == 'P' ? y2 + 1 : y2 - 1); | |
+ | |
+ /* the en passant square resets after a move */ | |
+ b->enpassantsquare[0] = -1; | |
+ 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; | |
+ } | |
+ } 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 output is PGN, show piece movement, else skip this step … | |
+ if (outputmode == ModePGN) { | |
/* PGN for move */ | |
if (castled) { | |
pgn("%s", castled); | |
} else { | |
/* pawn move needs no notation */ | |
if (piece != 'p' && piece != 'P') { | |
- pgn("%c", toupper(piece)); | |
- /* TODO: check ambiguity for certain p… | |
- pgn("%c%c", xtofile(x), ytorank(y)); | |
+ pgn("%c", pgnpiece(piece)); | |
+ | |
+ /* check ambiguity for certain pieces … | |
+ 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) … | |
+ continue; | |
+ | |
+ if (canpiecemove(b, b-… | |
+ needfile = 1; | |
+ if (i == x) | |
+ needra… | |
+ } | |
+ } | |
+ } | |
+ if (needfile) { | |
+ pgn("%c", xtofile(x)); | |
+ if (needrank) | |
+ pgn("%c", ytorank(y)); | |
+ } | |
} | |
if (tookpiece) { | |
- /* pawn captures are prefixed by the f… | |
+ /* pawn captures are prefixed by the f… | |
if (piece == 'p' || piece == 'P') | |
pgn("%c", xtofile(x)); | |
pgn("x"); | |
@@ -893,51 +1196,177 @@ parsemoves(const char *moves) | |
pgn("%c%c", xtofile(x2), ytorank(y2)); | |
/* possible promotion: queen, knight, bishop */ | |
- if (*s == 'q' || *s == 'n' || *s == 'b') { | |
- if (side_to_move == 'w') | |
- piece = toupper((unsigned char… | |
+ if (promote) { | |
+ if (b->side_to_move == 'w') | |
+ piece = toupper(promote); | |
else | |
- piece = *s; | |
- place(piece, x2, y2); | |
- s++; | |
- pgn("=%c", toupper(piece)); | |
+ piece = tolower(promote); | |
+ place(b, piece, x2, y2); | |
+ | |
+ pgn("=%c", pgnpiece(piece)); | |
} | |
} | |
- if (ischeckmated(side_to_move == 'b' ? 'w' : 'b')) | |
- pgn("#"); | |
- else if (ischecked(side_to_move == 'b' ? 'w' : 'b')) | |
- pgn("+"); | |
+ } | |
- /* a move by black increases the move number */ | |
- if (side_to_move == 'b') | |
- movenumber++; | |
+ /* clear previous square (if not castled) */ | |
+ if (!castled) | |
+ place(b, 0, x, y); | |
+ /* place piece or new promoted piece */ | |
+ place(b, piece, x2, y2); | |
- /* switch which side it is to move */ | |
- side_to_move = side_to_move == 'b' ? 'w' : 'b'; | |
- } | |
+ if (ischeckmated(b, otherside)) | |
+ pgn("#"); | |
+ else if (isincheck(b, otherside)) | |
+ pgn("+"); | |
+ | |
+ /* a move by black increases the move number */ | |
+ if (b->side_to_move == 'b') | |
+ b->movenumber++; | |
+ | |
+ /* switch which side it is to move */ | |
+ b->side_to_move = otherside; | |
+ | |
+ if (!*s) | |
+ break; | |
} | |
- pgn("\n"); | |
-// // DEBUG | |
-// ischecked('w'); | |
-// ischecked('b'); | |
+ if (!firstmove) | |
+ pgn("\n"); | |
/* highlight last move */ | |
- highlightmove(x, y); | |
- highlightmove(x2, y2); | |
+ highlightmove(b, x, y); | |
+ highlightmove(b, x2, y2); | |
+ | |
+ /* highlight king in check or mate */ | |
+ if (isincheck(b, b->side_to_move) && | |
+ findking(b, b->side_to_move, &x, &y)) | |
+ highlightcheck(b, x, y); | |
} | |
void | |
usage(char *argv0) | |
{ | |
-// fprintf(stderr, "usage: %s [-cCfF] [-o ascii|fen|pgn|svg|tty] [FEN] … | |
- fprintf(stderr, "usage: %s [-cCfF] [-o ascii|fen|svg|tty] [FEN] [moves… | |
+ fprintf(stderr, "usage: %s [-cCfF] [-m mapping] [-o ascii|fen|pgn|svg|… | |
exit(1); | |
} | |
+/* CGI: get parameter */ | |
+char * | |
+getparam(const char *query, const char *s) | |
+{ | |
+ const char *p, *last = NULL; | |
+ size_t len; | |
+ | |
+ len = strlen(s); | |
+ for (p = query; (p = strstr(p, s)); p += len) { | |
+ if (p[len] == '=' && (p == query || p[-1] == '&' || p[-1] == '… | |
+ last = p + len + 1; | |
+ } | |
+ | |
+ return (char *)last; | |
+} | |
+ | |
+int | |
+hexdigit(int c) | |
+{ | |
+ if (c >= '0' && c <= '9') | |
+ return c - '0'; | |
+ else if (c >= 'A' && c <= 'F') | |
+ return c - 'A' + 10; | |
+ else if (c >= 'a' && c <= 'f') | |
+ return c - 'a' + 10; | |
+ | |
+ return 0; | |
+} | |
+ | |
+/* CGI: decode until NUL separator or end of "key". */ | |
+int | |
+decodeparam(char *buf, size_t bufsiz, const char *s) | |
+{ | |
+ size_t i; | |
+ | |
+ if (!bufsiz) | |
+ return -1; | |
+ | |
+ for (i = 0; *s && *s != '&'; s++) { | |
+ switch (*s) { | |
+ case '%': | |
+ if (i + 3 >= bufsiz) | |
+ return -1; | |
+ if (!isxdigit((unsigned char)*(s+1)) || | |
+ !isxdigit((unsigned char)*(s+2))) | |
+ return -1; | |
+ buf[i++] = hexdigit(*(s+1)) * 16 + hexdigit(*(s+2)); | |
+ s += 2; | |
+ break; | |
+ case '+': | |
+ if (i + 1 >= bufsiz) | |
+ return -1; | |
+ buf[i++] = ' '; | |
+ break; | |
+ default: | |
+ if (i + 1 >= bufsiz) | |
+ return -1; | |
+ buf[i++] = *s; | |
+ break; | |
+ } | |
+ } | |
+ buf[i] = '\0'; | |
+ | |
+ return i; | |
+} | |
+ | |
+/* CGI mode */ | |
+int | |
+cgi_mode(void) | |
+{ | |
+ struct board board; | |
+ char *query, *p; | |
+ char buf[4096]; | |
+ | |
+ board_init(&board); | |
+ | |
+ query = getenv("QUERY_STRING"); | |
+ if ((p = getparam(query, "flip")) && (*p == '0' || *p == '1')) | |
+ board.flipboard = *p == '1' ? 1 : 0; | |
+ if ((p = getparam(query, "coords")) && (*p == '0' || *p == '1')) | |
+ board.showcoords = *p == '1' ? 1 : 0; | |
+ if ((p = getparam(query, "theme"))) { | |
+ if (decodeparam(buf, sizeof(buf), p) == -1) | |
+ goto badrequest; | |
+ board_set_theme(&board, buf); | |
+ } | |
+ if ((p = getparam(query, "fen"))) { | |
+ if (decodeparam(buf, sizeof(buf), p) == -1) | |
+ goto badrequest; | |
+ board_setup_fen(&board, buf); | |
+ } else { | |
+ board_setup_fen(&board, "startpos"); | |
+ } | |
+ if ((p = getparam(query, "moves"))) { | |
+ if (decodeparam(buf, sizeof(buf), p) == -1) | |
+ goto badrequest; | |
+ board_playmoves(&board, buf); | |
+ } | |
+ | |
+ fputs("Status: 200 OK\r\nContent-Type: image/svg+xml\r\n\r\n", stdout); | |
+ output_svg(&board); | |
+ | |
+ return 0; | |
+ | |
+badrequest: | |
+ fputs("Status: 400 Bad Request\r\n", stdout); | |
+ fputs("Content-Type: text/plain\r\n", stdout); | |
+ fputs("\r\n", stdout); | |
+ fputs("Bad request: fen parameter use a valid format or \"startpos\" (… | |
+ | |
+ return 1; | |
+} | |
+ | |
int | |
main(int argc, char *argv[]) | |
{ | |
+ struct board board; | |
const char *fen, *moves; | |
int i, j; | |
@@ -946,6 +1375,10 @@ main(int argc, char *argv[]) | |
err(1, "pledge"); | |
#endif | |
+ if (getenv("QUERY_STRING")) | |
+ return cgi_mode(); | |
+ | |
+ board_init(&board); | |
fen = "startpos"; | |
moves = ""; | |
@@ -955,11 +1388,19 @@ main(int argc, char *argv[]) | |
for (j = 1; argv[i][j]; j++) { | |
switch (argv[i][j]) { | |
- case 'c': showcoords = 1; break; | |
- case 'C': showcoords = 0; break; | |
- case 'f': flipboard = 1; break; | |
- case 'F': flipboard = 0; break; | |
- case 'o': | |
+ case 'c': board.showcoords = 1; break; | |
+ case 'C': board.showcoords = 0; break; | |
+ case 'f': board.flipboard = 1; break; | |
+ case 'F': board.flipboard = 0; break; | |
+ case 'm': /* remap PGN */ | |
+ if (i + 1 >= argc) | |
+ usage(argv[0]); | |
+ i++; | |
+ if (strlen(argv[i]) != 5) | |
+ usage(argv[0]); | |
+ pgn_piecemapping = argv[i]; | |
+ goto next; | |
+ case 'o': /* output format */ | |
if (i + 1 >= argc) | |
usage(argv[0]); | |
i++; | |
@@ -967,8 +1408,8 @@ main(int argc, char *argv[]) | |
outputmode = ModeASCII; | |
else if (!strcmp(argv[i], "fen")) | |
outputmode = ModeFEN; | |
-// else if (!strcmp(argv[i], "pgn")) | |
-// outputmode = ModePGN; | |
+ else if (!strcmp(argv[i], "pgn")) | |
+ outputmode = ModePGN; | |
else if (!strcmp(argv[i], "svg")) | |
outputmode = ModeSVG; | |
else if (!strcmp(argv[i], "tty")) | |
@@ -976,6 +1417,12 @@ main(int argc, char *argv[]) | |
else | |
usage(argv[0]); | |
goto next; | |
+ case 't': /* theme name */ | |
+ if (i + 1 >= argc) | |
+ usage(argv[0]); | |
+ i++; | |
+ board_set_theme(&board, argv[i]); | |
+ goto next; | |
default: | |
usage(argv[0]); | |
break; | |
@@ -993,20 +1440,15 @@ next: | |
i++; | |
} | |
- if (!strcmp(fen, "startpos")) | |
- fen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 … | |
- | |
- parsefen(fen); | |
- parsemoves(moves); | |
- | |
-// outputmode = ModeTTY; /* DEBUG */ | |
+ board_setup_fen(&board, fen); | |
+ board_playmoves(&board, moves); | |
switch (outputmode) { | |
- case ModeASCII: output_ascii(); break; | |
- case ModeFEN: output_fen(); break; | |
-// case ModePGN: break; /* handled in parsemoves() */ | |
- case ModeSVG: output_svg(); break; | |
- case ModeTTY: output_tty(); break; | |
+ case ModeASCII: output_ascii(&board); break; | |
+ case ModeFEN: output_fen(&board); break; | |
+ case ModePGN: break; /* handled in parsemoves(… | |
+ case ModeSVG: output_svg(&board); break; | |
+ case ModeTTY: output_tty(&board); break; | |
default: usage(argv[0]); break; | |
} | |
diff --git a/generate.sh b/generate.sh | |
@@ -2,6 +2,36 @@ | |
fenbin="./fen" | |
db="lichess_db_puzzle.csv" | |
+# default, green, grey | |
+theme="default" | |
+ | |
+# texts / localization. | |
+# English | |
+text_solutions="Solutions" | |
+text_puzzles="Puzzles" | |
+text_puzzle="Puzzle" | |
+text_puzzlerating="Puzzel rating" | |
+text_point="point" | |
+text_points="points" | |
+text_whitetomove="white to move" | |
+text_blacktomove="black to move" | |
+text_title="${text_puzzles}" | |
+text_header="${text_puzzles}!" | |
+pgnmapping="KQRBN" | |
+ | |
+# 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}!" | |
+# Dutch: (K)oning, (D)ame, (T)oren, (L)oper, (P)aard. | |
+#pgnmapping="KDTLP" | |
if ! test -f "$db"; then | |
printf 'File "%s" not found, run `make db` to update it\n' "$db" >&2 | |
@@ -14,7 +44,7 @@ mkdir -p puzzles/solutions | |
solutions="$(mktemp)" | |
seedfile="$(mktemp)" | |
-seed=20231221 # must be a integer value | |
+seed=20240101 # must be a integer value | |
# seed for random sorting, makes it deterministic for the same system | |
# seed must be sufficiently long. | |
echo "${seed}_chess_puzzles" > "$seedfile" | |
@@ -48,7 +78,7 @@ cat > "$index" <<! | |
<html> | |
<head> | |
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> | |
-<title>Puzzles</title> | |
+<title>${text_title}</title> | |
<style type="text/css"> | |
body { | |
font-family: sans-serif; | |
@@ -73,11 +103,20 @@ footer { | |
details summary { | |
cursor: pointer; /* show hand */ | |
} | |
+@media (prefers-color-scheme: dark) { | |
+ body { | |
+ background-color: #000; | |
+ color: #bdbdbd; | |
+ } | |
+ h2 a { | |
+ color: #bdbdbd; | |
+ } | |
+} | |
</style> | |
</head> | |
<body> | |
<header> | |
-<h1>Puzzles, happy christmas mating!</h1> | |
+<h1>${text_header}</h1> | |
</header> | |
<main> | |
! | |
@@ -98,13 +137,13 @@ LC_ALL=C awk -F ',' 'int($4) >= 2000 { print $0 }' "$group… | |
LC_ALL=C awk -F ',' 'int($4) >= 2700 { print $0 }' "$groupsdir/matein5.csv" > … | |
( | |
-shuffle "$groupsdir/matein1.csv" 100 | sed 10q | LC_ALL=C awk '{ print $0 ",1 … | |
-shuffle "$groupsdir/matein2.csv" 100 | sed 10q | LC_ALL=C awk '{ print $0 ",2 … | |
-shuffle "$groupsdir/matein3.csv" 100 | sed 10q | LC_ALL=C awk '{ print $0 ",3 … | |
-shuffle "$groupsdir/matein4.csv" 100 | sed 10q | LC_ALL=C awk '{ print $0 ",4 … | |
-shuffle "$groupsdir/matein5_lt_2000.csv" 100 | sed 5q | LC_ALL=C awk '{ print … | |
-shuffle "$groupsdir/matein5_ge_2000.csv" 100 | sed 3q | LC_ALL=C awk '{ print … | |
-shuffle "$groupsdir/matein5_ge_2700.csv" 100 | sed 2q | LC_ALL=C awk '{ print … | |
+shuffle "$groupsdir/matein1.csv" 100 | sed 10q | LC_ALL=C awk '{ print $0 ",1"… | |
+shuffle "$groupsdir/matein2.csv" 100 | sed 10q | LC_ALL=C awk '{ print $0 ",2"… | |
+shuffle "$groupsdir/matein3.csv" 100 | sed 10q | LC_ALL=C awk '{ print $0 ",3"… | |
+shuffle "$groupsdir/matein4.csv" 100 | sed 10q | LC_ALL=C awk '{ print $0 ",4"… | |
+shuffle "$groupsdir/matein5_lt_2000.csv" 100 | sed 5q | LC_ALL=C awk '{ print … | |
+shuffle "$groupsdir/matein5_ge_2000.csv" 100 | sed 3q | LC_ALL=C awk '{ print … | |
+shuffle "$groupsdir/matein5_ge_2700.csv" 100 | sed 2q | LC_ALL=C awk '{ print … | |
rm -rf "$groupsdir" | |
) | \ | |
while read -r line; do | |
@@ -130,27 +169,35 @@ while read -r line; do | |
# added field: points | |
points=$(printf '%s' "$line" | cut -f "11" -d ',') | |
+ if [ "$points" = "1" ]; then | |
+ points="$points ${text_point}" | |
+ else | |
+ points="$points ${text_points}" | |
+ fi | |
img="$i.svg" | |
txt="$i.txt" | |
vt="$i.vt" | |
+ destfen="puzzles/$i.fen" | |
destsvg="puzzles/$img" | |
desttxt="puzzles/$txt" | |
destvt="puzzles/$vt" | |
- "$fenbin" $flip -o svg "$fen" "$firstmove" > "$destsvg" | |
- "$fenbin" $flip -o ascii "$fen" "$firstmove" > "$desttxt" | |
- "$fenbin" $flip -o tty "$fen" "$firstmove" > "$destvt" | |
+ "$fenbin" -m "$pgnmapping" -t "$theme" $flip -o svg "$fen" "$firstmove… | |
+ "$fenbin" -m "$pgnmapping" -t "$theme" $flip -o ascii "$fen" "$firstmo… | |
+ "$fenbin" -m "$pgnmapping" -t "$theme" $flip -o tty "$fen" "$firstmove… | |
+ "$fenbin" -m "$pgnmapping" -t "$theme" $flip -o fen "$fen" "$firstmove… | |
+ pgn=$("$fenbin" -m "$pgnmapping" -o pgn "$fen" "$firstmove") | |
printf '<div class="puzzle" id="puzzle-%s">\n' "$i" >> "$index" | |
- printf '<h2><a href="#puzzle-%s">Puzzle %s</a></h2>\n' "$i" "$i" >> "$… | |
+ printf '<h2><a href="#puzzle-%s">%s %s</a></h2>\n' "$i" "${text_puzzle… | |
test "$lichess" != "" && printf '<a href="%s">' "$lichess" >> "$index" | |
title="" | |
- test "$rating" != "" && title="Puzzle rating: $rating" | |
+ test "$rating" != "" && title="${text_puzzlerating}: $rating" | |
- printf '<img src="%s" alt="Puzzle #%s" title="%s" width="360" height="… | |
- "$img" "$i" "$title" >> "$index" | |
+ printf '<img src="%s" alt="%s #%s" title="%s" width="360" height="360"… | |
+ "$img" "${text_puzzle}" "$i" "$title" >> "$index" | |
test "$lichess" != "" && printf '</a>' >> "$index" | |
echo "" >> "$index" | |
@@ -158,13 +205,13 @@ while read -r line; do | |
# if there is a first move, inverse to move. | |
if test "$firstmove" != ""; then | |
case "$tomove" in | |
- "w") movetext=", black to move";; | |
- "b") movetext=", white to move";; | |
+ "w") movetext=", ${text_blacktomove}";; | |
+ "b") movetext=", ${text_whitetomove}";; | |
esac | |
else | |
case "$tomove" in | |
- "w") movetext=", white to move";; | |
- "b") movetext=", black to move";; | |
+ "w") movetext=", ${text_whitetomove}";; | |
+ "b") movetext=", ${text_blacktomove}";; | |
esac | |
fi | |
@@ -174,7 +221,7 @@ while read -r line; do | |
# solutions per puzzle. | |
printf '<div class="puzzle-solution">\n' >> "$solutions" | |
- printf '<h2><a href="#puzzle-%s">Puzzle %s</a></h2>\n' "$i" "$i" >> "$… | |
+ printf '<h2><a href="#puzzle-%s">%s %s</a></h2>\n' "$i" "${text_puzzle… | |
m="${allmoves}" | |
movecount=0 | |
@@ -182,8 +229,16 @@ while read -r line; do | |
# the solution images. | |
# add initial puzzle aswell for context. | |
- printf '<img src="%s" width="180" height="180" loading="lazy" />\n' \ | |
- "${i}.svg" >> "$solutions" | |
+ printf '<img src="%s" width="180" height="180" loading="lazy" title="%… | |
+ "${i}.svg" "$pgn" >> "$solutions" | |
+ | |
+ # solution PGN | |
+ pgn_solution="$($fenbin -m "$pgnmapping" -o pgn "$fen" "$allmoves")" | |
+ | |
+ destsolpgn="puzzles/solutions/${i}.pgn" | |
+ printf '%s\n' "$pgn_solution" > "$destsolpgn" | |
+ | |
+# printf 'DEBUG: #%s: "%s" "%s"\n' "$i" "$fen" "$allmoves" >&2 | |
while [ "$m" != "" ]; do | |
prevmoves="$m" | |
@@ -205,25 +260,27 @@ while read -r line; do | |
# process move list in sequence. | |
destsolsvg="puzzles/solutions/${i}_${movecount}.svg" | |
- "$fenbin" $flip -o svg "$fen" "$movelist" > "$destsolsvg" | |
+ "$fenbin" -m "$pgnmapping" -t "$theme" $flip -o svg "$fen" "$m… | |
+ | |
+ # PGN of moves so far. | |
+ pgn="$($fenbin -m "$pgnmapping" -o pgn "$fen" "$movelist")" | |
- printf '<img src="%s" width="180" height="180" loading="lazy" … | |
- "solutions/${i}_${movecount}.svg" >> "$solutions" | |
+ printf '<img src="%s" width="180" height="180" loading="lazy" … | |
+ "solutions/${i}_${movecount}.svg" "$pgn" >> "$solution… | |
movecount=$((movecount + 1)) | |
done | |
+ | |
+ printf '<p><b>PGN:</b> %s</p>\n' "${pgn_solution}" >> "$solutions" | |
printf '</div>\n' >> "$solutions" | |
printf '</div>\n' >> "$index" | |
- # DEBUG | |
- #echo "$count: $line" >&2 | |
- | |
count=$((count + 1)) | |
done | |
# solutions / spoilers | |
-echo "<footer><br/><br/><details>\n<summary>Solutions</summary>\n" >> "$index" | |
+printf '<footer><br/><br/><details>\n<summary>%s</summary>\n' "$text_solutions… | |
cat "$solutions" >> "$index" | |
echo "</details>\n<br/><br/></footer>\n" >> "$index" | |
diff --git a/gifs.sh b/gifs.sh | |
@@ -0,0 +1,50 @@ | |
+#!/bin/sh | |
+# create animated gifs for puzzle solutions. | |
+# Dependencies: ffmpeg, ImageMagick, etc. | |
+ | |
+tmppal="$(mktemp '/tmp/palette_XXXXXXXX.png')" | |
+ | |
+n=1 | |
+while :; do | |
+ f="puzzles/${n}.svg" | |
+ test -f "$f" || break | |
+ | |
+ tmpdir="$(mktemp -d '/tmp/puzzleanim_XXXXXXXX')" | |
+ | |
+ # initial puzzle state also | |
+ dest="$tmpdir/0.png" | |
+ convert "$f" "$dest" | |
+ | |
+ # solution | |
+ i=1 | |
+ while :; do | |
+ f="puzzles/solutions/${n}_${i}.svg" | |
+ test -f "$f" || break | |
+ | |
+ dest="$tmpdir/$i.png" | |
+ convert "$f" "$dest" | |
+ i=$((i+1)) | |
+ done | |
+ | |
+ # create video / animation. | |
+ out="puzzles/solutions/$n.gif" | |
+ rm -f "$out" | |
+ | |
+ # generate palette for gif. | |
+ rm -f "$tmppal" | |
+ ffmpeg -loglevel error -stats -i "$tmpdir/%d.png"\ | |
+ -vf palettegen "$tmppal" | |
+ | |
+ # wait longer for last frame. | |
+ ffmpeg -loglevel error -stats -framerate 1\ | |
+ -i "$tmpdir/%d.png" \ | |
+ -i "$tmppal" \ | |
+ -lavfi 'tpad=stop_mode=clone:stop_duration=4[v];[v]paletteuse[… | |
+ -map '[out]' \ | |
+ "$out" | |
+ | |
+ rm -rf "$tmpdir" | |
+ rm -f "$tmppal" | |
+ | |
+ n=$((n + 1)) | |
+done | |
diff --git a/tests.sh b/tests.sh | |
@@ -158,6 +158,12 @@ testfen 'white takes en passant: should reset halfmove cou… | |
'rnbqkbnr/pppp1pp1/8/3Pp2p/8/8/PPP1PPPP/RNBQKBNR w KQkq e6 1 3'\ | |
'd5e6' | |
+# check halfmove counter on check, white checks black: should increase halfmov… | |
+testfen 'check halfmove counter on check, white checks black: should increase … | |
+ 'rnbqkbnr/ppp1pp1p/6p1/1B1p4/3P4/4P3/PPP2PPP/RNBQK1NR b KQkq - 21 1'\ | |
+ 'rnbqkbnr/ppp1pp1p/6p1/3p4/3P4/4P3/PPP2PPP/RNBQKBNR w KQkq - 20 1'\ | |
+ 'f1b5' | |
+ | |
# 960 white queenside castle | |
testfen '960 white queenside castle'\ | |
'qrn1bk1n/ppb1pprp/2p3p1/3p4/1B1P4/3NPB2/PPP2PPP/Q1KR2RN b q - 5 6'\ | |
@@ -220,7 +226,7 @@ testfen '960, castle king on queenside with many empty squa… | |
tests_pgn() { | |
testpgn 'simple pawn move'\ | |
- 'e5'\ | |
+ '2. ... e5'\ | |
'rnbqkbnr/ppp1pppp/8/3p4/3PP3/8/PPP2PPP/RNBQKBNR b KQkq - 0 2'\ | |
'e7e5' | |
@@ -229,24 +235,60 @@ testpgn 'check: check with white pawn'\ | |
'rnbqkbnr/pppp1ppp/6P1/8/8/4p3/PPPPPP1P/RNBQKBNR w KQkq - 0 4'\ | |
'g6f7' | |
testpgn 'check: check with black pawn'\ | |
- 'exf2+'\ | |
+ '4. ... exf2+'\ | |
'rnbqkbnr/pppp1ppP/8/8/8/4p3/PPPPPP1P/RNBQKBNR b KQkq - 0 4'\ | |
'e3f2' | |
testpgn 'check: check with bishop'\ | |
- '3. Bf1b5+'\ | |
+ '3. Bb5+'\ | |
'rnbqkbnr/ppp2ppp/8/3pp3/3PP3/8/PPP2PPP/RNBQKBNR w KQkq - 0 3'\ | |
'f1b5' | |
testpgn 'check: check with white queen, open file'\ | |
- '6. Qd4e4+'\ | |
+ '6. Qe4+'\ | |
'rnbqkbnr/1ppp1pp1/p5Pp/8/3Q4/4P3/PPP1PP1P/RNB1KBNR w KQkq - 0 6'\ | |
'd4e4' | |
-testpgn 'check: check with white knight'\ | |
- '8. Nd5xc7+'\ | |
+testpgn 'check: check and takes with white knight'\ | |
+ '8. Nxc7+'\ | |
'rnbqkbn1/pppp1ppr/6P1/3N4/7p/4PN2/PPP1PP1P/R1BQKB1R w KQq - 2 8'\ | |
'd5c7' | |
+ | |
+testpgn 'black moves, but the move from white is unknown, use "..."'\ | |
+ '1. ... e5'\ | |
+ 'rnbqkbnr/pppppppp/8/8/3P4/8/PPP1PPPP/RNBQKBNR b KQkq - 0 1'\ | |
+ 'e7e5' | |
+ | |
+testpgn 'black moves with knight, ambigous move, needs file name'\ | |
+ '8. ... Nbd7'\ | |
+ 'rn2kb1r/pp4pp/1qp1pn2/3p4/3P1B2/1P1BP3/P1P2P1P/RN1QK1NR b KQkq - 0 8'\ | |
+ 'b8d7' | |
+ | |
+testpgn 'black moves with knight, non-ambigous move'\ | |
+ '8. ... Nd7'\ | |
+ 'rn2kb1r/pp4pp/1qp1p3/3p4/3P1B2/1P1BP3/P1P2P1P/RN1QK1NR b KQkq - 0 8'\ | |
+ 'b8d7' | |
+ | |
+testpgn '2 queens, ambigous move, needs file and rank'\ | |
+ '8. Qh3g3'\ | |
+ 'rn2kb1r/pp4pp/1qp1p3/3p4/3P1B1Q/1P1BP2Q/P1P2P1P/RN2K1NR w KQkq - 0 8'\ | |
+ 'h3g3' | |
+ | |
+testpgn 'king moves into range of king (illegal move), but played, so notate i… | |
+ '1. Kd5+'\ | |
+ '8/8/3k4/8/3K2R1/8/8/8 w - - 0 1'\ | |
+ 'd4d5' | |
+ | |
+# Long list of moves, checkmate at the end. | |
+testpgn 'Long list of moves, compared to Lichess board analyzer'\ | |
+ '1. d4 d5 2. Bf4 c6 3. e3 Bf5 4. Bd3 e6 5. g4 Nf6 6. gxf5 Qb6 7. fxe6 … | |
+ 'startpos'\ | |
+ 'd2d4 d7d5 c1f4 c7c6 e2e3 c8f5 f1d3 e7e6 g2g4 g8f6 g4f5 d8b6 f5e6 f7e6… | |
+ | |
+testpgn 'Rook on same file, only one is legal, Rd7d1+ -> Rd1+'\ | |
+ '34. Rc6 Rd1+ 35. Rxd1 Rxd1#'\ | |
+ '3r2k1/3r2p1/R6p/4bp2/2P1p2P/7N/2P2PP1/2R3K1 w - - 1 34'\ | |
+ 'a6c6 d7d1 c1d1 d8d1' | |
} | |
tests_fen | |
-#tests_pgn | |
+tests_pgn |