fen: various code improvements, initial code for PGN output (disabled for now) … | |
git clone git://git.codemadness.org/chess-puzzles | |
Log | |
Files | |
Refs | |
README | |
LICENSE | |
--- | |
commit e536a441d52c33adea06f8ed451bd890a3ebdb9d | |
parent 4f6db6f748b3760f1f991e46bbb37246d201d4c8 | |
Author: Hiltjo Posthuma <[email protected]> | |
Date: Sat, 23 Dec 2023 18:59:46 +0100 | |
fen: various code improvements, initial code for PGN output (disabled for now) | |
- Reduce SVG output size by reusing the draw paths for pawns (saves ~4KB per | |
image on average). Of course this depends. On a board with no pawns it can be | |
~300 bytes larger. | |
- Remove Board FEN header for tty and ASCII outputs. | |
- Make the highlight function more generic (for one move). | |
- Improve outputs parsing, an invalid option now shows the usage(). | |
- Rename the output functions to output_<outputname>(). | |
- Add initial code for PGN output, needs some work, disabled for now. | |
... work in progress | |
Diffstat: | |
M fen.1 | 4 +++- | |
M fen.c | 289 +++++++++++++++++++++++++----… | |
M tests.sh | 53 ++++++++++++++++++++++++++++++ | |
3 files changed, 294 insertions(+), 52 deletions(-) | |
--- | |
diff --git a/fen.1 b/fen.1 | |
@@ -7,7 +7,7 @@ | |
.Sh SYNOPSIS | |
.Nm | |
.Op Fl cCfF | |
-.Op Fl o Ar ascii | fen | svg | tty | |
+.Op Fl o Ar ascii | fen | pgn | svg | tty | |
.Op Ar FEN | |
.Op Ar moves | |
.Sh DESCRIPTION | |
@@ -32,6 +32,8 @@ ASCII text representation of the board. | |
FEN of the board state after playing the moves. | |
.It fen | |
FEN of the board state after playing the moves. | |
+.It pgn | |
+PGN output of the moves for the board. | |
.It svg | |
SVG image of the board. | |
.It tty | |
diff --git a/fen.c b/fen.c | |
@@ -1,4 +1,5 @@ | |
#include <ctype.h> | |
+#include <stdarg.h> | |
#include <stdio.h> | |
#include <stdlib.h> | |
#include <string.h> | |
@@ -12,6 +13,9 @@ | |
#define SETFGCOLOR(r,g,b) printf("\x1b[38;2;%d;%d;%dm", r, g, b) | |
#define SETBGCOLOR(r,g,b) printf("\x1b[48;2;%d;%d;%dm", r, g, b) | |
+enum outputmode { ModeASCII = 0, ModeFEN, ModePGN, ModeTTY, ModeSVG }; | |
+enum outputmode outputmode = ModeSVG; | |
+ | |
static char board[8][8]; | |
static char highlight[8][8]; | |
@@ -32,6 +36,19 @@ static const int lightsquarehi[] = { 0xcd, 0xd2, 0x6a }; | |
static int showcoords = 1; /* config: show board coordinates? */ | |
static int flipboard = 0; /* config: flip board ? */ | |
+void | |
+pgn(const char *fmt, ...) | |
+{ | |
+ va_list ap; | |
+ | |
+ if (outputmode != ModePGN) | |
+ return; | |
+ | |
+ va_start(ap, fmt); | |
+ vprintf(fmt, ap); | |
+ va_end(ap); | |
+} | |
+ | |
int | |
isvalidsquare(int x, int y) | |
{ | |
@@ -92,13 +109,10 @@ squaretoxy(const char *s, int *x, int *y) | |
} | |
void | |
-highlightmove(int x1, int y1, int x2, int y2) | |
+highlightmove(int x, int y) | |
{ | |
- if (isvalidsquare(x1, y1)) | |
- highlight[y1][x1] = 1; | |
- | |
- if (isvalidsquare(x2, y2)) | |
- highlight[y2][x2] = 1; | |
+ if (isvalidsquare(x, y)) | |
+ highlight[y][x] = 1; | |
} | |
void | |
@@ -144,7 +158,7 @@ showboardfen(void) | |
} else { | |
putchar('-'); | |
} | |
- printf(" %d %d", halfmove, movenumber); | |
+ printf(" %d %d\n", halfmove, movenumber); | |
} | |
void | |
@@ -160,13 +174,13 @@ showpiece_svg(int c) | |
case 'R': s = "<g fill=\"#fff\" fill-rule=\"evenodd\" stroke=\"#000\" … | |
case 'B': s = "<g fill=\"none\" fill-rule=\"evenodd\" stroke=\"#000\" … | |
case 'N': s = "<g fill=\"none\" fill-rule=\"evenodd\" stroke=\"#000\" … | |
- case 'P': s = "<path d=\"M22.5 9c-2.21 0-4 1.79-4 4 0 .89.29 1.71.78 2… | |
+ case 'P': s = "<use href=\"#pawn\" fill=\"#fff\"/>"; break; | |
case 'k': s = "<g fill=\"none\" fill-rule=\"evenodd\" stroke=\"#000\" … | |
case 'q': s = "<g fill-rule=\"evenodd\" stroke=\"#000\" stroke-width=\… | |
case 'r': s = "<g fill-rule=\"evenodd\" stroke=\"#000\" stroke-width=\… | |
case 'b': s = "<g fill=\"none\" fill-rule=\"evenodd\" stroke=\"#000\" … | |
case 'n': s = "<g fill=\"none\" fill-rule=\"evenodd\" stroke=\"#000\" … | |
- case 'p': s = "<path d=\"M22.5 9c-2.21 0-4 1.79-4 4 0 .89.29 1.71.78 2… | |
+ case 'p': s = "<use href=\"#pawn\" fill=\"#000\"/>"; break; | |
} | |
if (*s) | |
@@ -174,7 +188,7 @@ showpiece_svg(int c) | |
} | |
void | |
-showboard_svg(void) | |
+output_svg(void) | |
{ | |
const int *color; | |
int ix, iy, x, y, piece; | |
@@ -184,9 +198,9 @@ showboard_svg(void) | |
"<svg width=\"360\" height=\"360\" viewBox=\"0 0 360 360\" xml… | |
"<rect fill=\"#fff\" stroke=\"#000\" x=\"0\" y=\"0\" width=\"3… | |
- fputs("<!-- Board FEN: ", stdout); | |
- showboardfen(); | |
- fputs(" -->\n", stdout); | |
+ fputs("<defs>\n", stdout); | |
+ fputs("<path id=\"pawn\" d=\"M22.5 9c-2.21 0-4 1.79-4 4 0 .89.29 1.71.… | |
+ fputs("</defs>\n", stdout); | |
for (iy = 0; iy < 8; iy++) { | |
y = flipboard ? 7 - iy : iy; | |
@@ -284,15 +298,11 @@ showpiece_tty(int c) | |
/* show board */ | |
void | |
-showboard_tty(void) | |
+output_tty(void) | |
{ | |
const int *color; | |
int ix, iy, x, y, piece; | |
- printf("Board FEN:\n"); | |
- showboardfen(); | |
- printf("\n\n"); | |
- | |
SETBGCOLOR(border[0], border[1], border[2]); | |
fputs(" ", stdout); | |
printf("\x1b[0m"); /* reset */ | |
@@ -367,25 +377,20 @@ showpiece_ascii(int c) | |
/* OnlyFENs */ | |
void | |
-showboard_fen(void) | |
+output_fen(void) | |
{ | |
showboardfen(); | |
- printf("\n"); | |
} | |
/* show board */ | |
void | |
-showboard_ascii(void) | |
+output_ascii(void) | |
{ | |
int hi[3] = { '>', ' ', '<' }; | |
int dark[3] = { '.', '.', '.' }; | |
int light[3] = { ' ', ' ', ' ' }; | |
int *color, ix, iy, x, y, piece; | |
- printf("Board FEN:\n"); | |
- showboardfen(); | |
- printf("\n\n"); | |
- | |
for (iy = 0; iy < 8; iy++) { | |
y = flipboard ? 7 - iy : iy; | |
@@ -426,6 +431,124 @@ showboard_ascii(void) | |
fputs("\n", stdout); | |
} | |
+int | |
+findking(int side, int *kingx, int *kingy) | |
+{ | |
+ int king, x, y; | |
+ | |
+ king = side == 'w' ? 'K' : 'k'; | |
+ *kingx = -1; | |
+ *kingy = -1; | |
+ | |
+ /* find king */ | |
+ for (y = 0; y < 8; y++) { | |
+ for (x = 0; x < 8; x++) { | |
+ if (getpiece(x, y) == king) { | |
+ *kingx = x; | |
+ *kingy = y; | |
+ return 1; | |
+ } | |
+ } | |
+ } | |
+ return 0; | |
+} | |
+ | |
+int | |
+ischecked(int side) | |
+{ | |
+ int diag[] = { -1, -1, 1, 1, -1, 1, 1, -1 }; | |
+ int line[] = { 1, 0, 0, 1, -1, 0, 0, -1 }; | |
+ int knight[] = { -1, -2, 1, -2, -1, 2, 1, 2, | |
+ -2, -1, 2, -1, -2, 1, 2, 1 | |
+ }; | |
+ int i, j, x, y; | |
+ int kingx, kingy; | |
+ int piece; | |
+ | |
+ /* find our king */ | |
+ if (!findking(side, &kingx, &kingy)) | |
+ return 0; /* should not happen */ | |
+ | |
+ /* check files and ranks (for queen and rook) */ | |
+ for (j = 0; j < 8; j += 2) { | |
+ for (i = 1; i < 8; i ++) { | |
+ x = kingx + (i * line[j]); | |
+ y = kingy + (i * line[j + 1]); | |
+ if (!(piece = getpiece(x, y))) | |
+ continue; | |
+ /* a piece is in front of it */ | |
+ if (piece && strchr("bBnNpP", piece)) | |
+ break; | |
+ /* own piece blocking/defending it */ | |
+ if ((side == 'w' && iswhitepiece(piece)) || | |
+ (side == 'b' && isblackpiece(piece))) | |
+ break; | |
+ return 1; | |
+ } | |
+ } | |
+ | |
+ /* check diagonals (queen and bishop) */ | |
+ for (j = 0; j < 8; j += 2) { | |
+ for (i = 1; i < 8; i ++) { | |
+ x = kingx + (i * diag[j]); | |
+ y = kingy + (i * diag[j + 1]); | |
+ if (!(piece = getpiece(x, y))) | |
+ continue; | |
+ /* a piece is in front of it */ | |
+ if (piece && strchr("rRnNpP", piece)) | |
+ break; | |
+ /* own piece blocking/defending it */ | |
+ if ((side == 'w' && iswhitepiece(piece)) || | |
+ (side == 'b' && isblackpiece(piece))) | |
+ break; | |
+ return 1; | |
+ } | |
+ } | |
+ | |
+ /* check knights */ | |
+ piece = side == 'w' ? 'n' : 'N'; | |
+ for (j = 0; j < 16; j += 2) { | |
+ x = kingx + knight[j]; | |
+ y = kingy + knight[j + 1]; | |
+// highlightmove(x, y); /* DEBUG */ | |
+ if (getpiece(x, y) == piece) | |
+ return 1; | |
+ } | |
+ | |
+ /* check pawns */ | |
+ if (side == 'w') { | |
+ if (getpiece(kingx - 1, kingy - 1) == 'p' || | |
+ getpiece(kingx + 1, kingy - 1) == 'p') | |
+ return 1; | |
+ } else if (side == 'b') { | |
+ if (getpiece(kingx - 1, kingy + 1) == 'P' || | |
+ getpiece(kingx + 1, kingy + 1) == 'P') | |
+ return 1; | |
+ } | |
+ | |
+ return 0; | |
+} | |
+ | |
+int | |
+ischeckmated(int side) | |
+{ | |
+ int kingx, kingy; | |
+ | |
+ // TODO: can king move out check, without being checked? | |
+ | |
+ if (!ischecked(side)) | |
+ return 0; | |
+ | |
+ /* find our king */ | |
+ if (!findking(side, &kingx, &kingy)) | |
+ return 0; /* should not happen */ | |
+ | |
+ // TODO: can the king move? move it and check if it is not checked | |
+ // TODO: separate board state or just move and undo move? | |
+ | |
+ return 0; // TODO | |
+} | |
+ | |
void | |
parsefen(const char *fen) | |
{ | |
@@ -528,12 +651,13 @@ void | |
parsemoves(const char *moves) | |
{ | |
char square[3]; | |
- const char *s; | |
- int i, x, y, x2, y2, piece, takepiece; | |
+ const char *castled, *s; | |
+ int firstmove, i, x, y, x2, y2, piece, takepiece, tookpiece; | |
/* process moves */ | |
square[2] = '\0'; | |
x = y = x2 = y2 = -1; | |
+ firstmove = 1; | |
for (s = moves; *s; s++) { | |
if (*s == ' ') | |
@@ -542,6 +666,14 @@ parsemoves(const char *moves) | |
(*(s + 1) >= '1' && *(s + 1) <= '8') && | |
(*(s + 2) >= 'a' && *(s + 2) <= 'h') && | |
(*(s + 3) >= '1' && *(s + 3) <= '8')) { | |
+ /* TODO: if first move but its blacks turn, prefix PGN | |
+ with "..." or something, since the white move was u… | |
+ | |
+ if (firstmove) | |
+ firstmove = 0; | |
+ else | |
+ pgn(" "); | |
+ | |
square[0] = *s; | |
square[1] = *(s + 1); | |
@@ -561,14 +693,22 @@ parsemoves(const char *moves) | |
s += 2; | |
+ /* took piece of opponent */ | |
+ tookpiece = (side_to_move == 'w' && isblackpiece(takep… | |
+ (side_to_move == 'b' && iswhitepiece(takep… | |
+ | |
/* if pawn move or taken a piece increase halfmove cou… | |
- if (piece == 'p' || piece == 'P' || | |
- (side_to_move == 'w' && isblackpiece(takepiece)) || | |
- (side_to_move == 'b' && iswhitepiece(takepiece))) | |
+ if (piece == 'p' || piece == 'P' || tookpiece) | |
halfmove = 0; | |
else | |
halfmove++; | |
+ if (side_to_move == 'w') | |
+ pgn("%d. ", movenumber); | |
+ | |
+ /* castled this move? for PGN */ | |
+ castled = NULL; | |
+ | |
/* castling */ | |
if (piece == 'K' && y == 7 && y2 == 7) { | |
/* white: kingside castling */ | |
@@ -577,6 +717,7 @@ parsemoves(const char *moves) | |
if (getpiece(i, y2) == 'R') { | |
place(0, i, y2); /* cl… | |
place('R', x2 - 1, y2)… | |
+ castled = "O-O"; | |
break; | |
} | |
} | |
@@ -586,6 +727,7 @@ parsemoves(const char *moves) | |
if (getpiece(i, y2) == 'R') { | |
place('R', x2 + 1, y2)… | |
place(0, i, y2); /* cl… | |
+ castled = "O-O-O"; | |
break; | |
} | |
} | |
@@ -597,6 +739,7 @@ parsemoves(const char *moves) | |
if (getpiece(i, y2) == 'r') { | |
place(0, i, y2); /* cl… | |
place('r', x2 - 1, y2)… | |
+ castled = "O-O"; | |
break; | |
} | |
} | |
@@ -606,6 +749,7 @@ parsemoves(const char *moves) | |
if (getpiece(i, y2) == 'r') { | |
place('r', x2 + 1, y2)… | |
place(0, i, y2); /* cl… | |
+ castled = "O-O-O"; | |
break; | |
} | |
} | |
@@ -664,15 +808,39 @@ parsemoves(const char *moves) | |
/* place piece */ | |
place(piece, x2, y2); | |
- /* possible promotion: queen, knight, bishop */ | |
- if (*s == 'q' || *s == 'n' || *s == 'b') { | |
- if (side_to_move == 'w') | |
- piece = toupper((unsigned char)*s); | |
- else | |
- piece = *s; | |
- place(piece, x2, y2); | |
- s++; | |
+ /* PGN for move */ | |
+ if (castled) { | |
+ pgn("%s", castled); | |
+ } else { | |
+ /* pawn move needs no notation */ | |
+ if (piece != 'p' && piece != 'P') { | |
+ pgn("%c", toupper(piece)); | |
+ /* TODO: check ambiguity for certain p… | |
+ pgn("%c%c", 'a' + x, '8' - y); | |
+ } | |
+ if (tookpiece) { | |
+ /* pawn captures are prefixed by the f… | |
+ if (piece == 'p' || piece == 'P') | |
+ pgn("%c", 'a' + x); | |
+ pgn("x"); | |
+ } | |
+ pgn("%c%c", 'a' + x2, '8' - y2); | |
+ | |
+ /* possible promotion: queen, knight, bishop */ | |
+ if (*s == 'q' || *s == 'n' || *s == 'b') { | |
+ if (side_to_move == 'w') | |
+ piece = toupper((unsigned char… | |
+ else | |
+ piece = *s; | |
+ place(piece, x2, y2); | |
+ s++; | |
+ pgn("=%c", toupper(piece)); | |
+ } | |
} | |
+ if (ischeckmated(side_to_move == 'b' ? 'w' : 'b')) | |
+ pgn("#"); | |
+ else if (ischecked(side_to_move == 'b' ? 'w' : 'b')) | |
+ pgn("+"); | |
/* a move by black increases the move number */ | |
if (side_to_move == 'b') | |
@@ -682,14 +850,21 @@ parsemoves(const char *moves) | |
side_to_move = side_to_move == 'b' ? 'w' : 'b'; | |
} | |
} | |
+ pgn("\n"); | |
+ | |
+// // DEBUG | |
+// ischecked('w'); | |
+// ischecked('b'); | |
/* highlight last move */ | |
- highlightmove(x, y, x2, y2); | |
+ highlightmove(x, y); | |
+ highlightmove(x2, y2); | |
} | |
void | |
usage(char *argv0) | |
{ | |
+// fprintf(stderr, "usage: %s [-cCfF] [-o ascii|fen|pgn|svg|tty] [FEN] … | |
fprintf(stderr, "usage: %s [-cCfF] [-o ascii|fen|svg|tty] [FEN] [moves… | |
exit(1); | |
} | |
@@ -697,7 +872,7 @@ usage(char *argv0) | |
int | |
main(int argc, char *argv[]) | |
{ | |
- const char *fen, *moves, *output = "svg"; | |
+ const char *fen, *moves; | |
int i, j; | |
#ifdef __OpenBSD__ | |
@@ -721,7 +896,19 @@ main(int argc, char *argv[]) | |
case 'o': | |
if (i + 1 >= argc) | |
usage(argv[0]); | |
- output = argv[++i]; | |
+ i++; | |
+ if (!strcmp(argv[i], "ascii")) | |
+ outputmode = ModeASCII; | |
+ else if (!strcmp(argv[i], "fen")) | |
+ outputmode = ModeFEN; | |
+// else if (!strcmp(argv[i], "pgn")) | |
+// outputmode = ModePGN; | |
+ else if (!strcmp(argv[i], "svg")) | |
+ outputmode = ModeSVG; | |
+ else if (!strcmp(argv[i], "tty")) | |
+ outputmode = ModeTTY; | |
+ else | |
+ usage(argv[0]); | |
goto next; | |
default: | |
usage(argv[0]); | |
@@ -746,16 +933,16 @@ next: | |
parsefen(fen); | |
parsemoves(moves); | |
- if (!strcmp(output, "ascii")) | |
- showboard_ascii(); | |
- else if (!strcmp(output, "fen")) | |
- showboard_fen(); | |
- else if (!strcmp(output, "svg")) | |
- showboard_svg(); | |
- else if (!strcmp(output, "tty")) | |
- showboard_tty(); | |
- else | |
- usage(argv[0]); | |
+// outputmode = ModeTTY; /* DEBUG */ | |
+ | |
+ switch (outputmode) { | |
+ case ModeASCII: output_ascii(); break; | |
+ case ModeFEN: output_fen(); break; | |
+// case ModePGN: break; /* handled in parsemoves() */ | |
+ case ModeSVG: output_svg(); break; | |
+ case ModeTTY: output_tty(); break; | |
+ default: usage(argv[0]); break; | |
+ } | |
return 0; | |
} | |
diff --git a/tests.sh b/tests.sh | |
@@ -18,6 +18,25 @@ testfen() { | |
fi | |
} | |
+# testpgn(name, expect, fen, moves) | |
+testpgn() { | |
+ name="$1" | |
+ expect="$2" | |
+ fen="$3" | |
+ moves="$4" | |
+ | |
+ output=$(./fen -o pgn "$fen" "$moves") | |
+ if test "$output" = "$expect"; then | |
+ printf 'OK: %s\n' "$name" | |
+ else | |
+ printf 'Fail: %s\n' "$name" | |
+ printf '\texpected: %s\n' "$expect" | |
+ printf '\tgot: %s\n' "$output" | |
+ printf '\tInput FEN, moves: "%s" "%s"\n' "$fen" "$moves" | |
+ fi | |
+} | |
+ | |
+tests_fen() { | |
testfen 'startpos'\ | |
'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1' \ | |
"startpos"\ | |
@@ -197,3 +216,37 @@ testfen '960, castle king on queenside with many empty squ… | |
# TODO: test more chess960 black kingside and queenside castling | |
# TODO: test more long sequence and halfmove and movenumber counts | |
+} | |
+ | |
+tests_pgn() { | |
+testpgn 'simple pawn move'\ | |
+ 'e5'\ | |
+ 'rnbqkbnr/ppp1pppp/8/3p4/3PP3/8/PPP2PPP/RNBQKBNR b KQkq - 0 2'\ | |
+ 'e7e5' | |
+ | |
+testpgn 'check: check with white pawn'\ | |
+ '4. gxf7+'\ | |
+ 'rnbqkbnr/pppp1ppp/6P1/8/8/4p3/PPPPPP1P/RNBQKBNR w KQkq - 0 4'\ | |
+ 'g6f7' | |
+testpgn 'check: check with black pawn'\ | |
+ 'exf2+'\ | |
+ 'rnbqkbnr/pppp1ppP/8/8/8/4p3/PPPPPP1P/RNBQKBNR b KQkq - 0 4'\ | |
+ 'e3f2' | |
+ | |
+testpgn 'check: check with bishop'\ | |
+ '3. Bf1b5+'\ | |
+ 'rnbqkbnr/ppp2ppp/8/3pp3/3PP3/8/PPP2PPP/RNBQKBNR w KQkq - 0 3'\ | |
+ 'f1b5' | |
+testpgn 'check: check with white queen, open file'\ | |
+ '6. Qd4e4+'\ | |
+ 'rnbqkbnr/1ppp1pp1/p5Pp/8/3Q4/4P3/PPP1PP1P/RNB1KBNR w KQkq - 0 6'\ | |
+ 'd4e4' | |
+ | |
+testpgn 'check: check with white knight'\ | |
+ '8. Nd5xc7+'\ | |
+ 'rnbqkbn1/pppp1ppr/6P1/3N4/7p/4PN2/PPP1PP1P/R1BQKB1R w KQq - 2 8'\ | |
+ 'd5c7' | |
+} | |
+ | |
+tests_fen | |
+#tests_pgn |