Skip to content

Commit dc0186c

Browse files
authored
Add position.XFENString() (#126) (#57)
When working with 3rd party chess APIs these may require positions to be expressed in X-FEN notation rather than FEN. e.g. Notably while some of the lichess.org APIs work with either FEN or X-FEN, some of them require X-FEN. This commit adds position.XFENString() for this purpose. The key difference between X-FEN and FEN is the encoding of the en passant square. X-FEN will only specify it when an opposing pawn is in position to capture, while FEN will always specify it. (cherry picked from commit b30c7028492205fcea48ad31010dee4b26c80ea6)
1 parent 32ce606 commit dc0186c

File tree

2 files changed

+71
-1
lines changed

2 files changed

+71
-1
lines changed

fen_test.go

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,30 @@ var (
2121
"3r1rk1/p3qppp/2bb4/2p5/3p4/1P2P3/PBQN1PPP/2R2RK1 w - - 0 1",
2222
"4r1k1/1b3p1p/ppq3p1/2p5/8/1P3R1Q/PBP3PP/7K w - - 0 1",
2323
"5k2/ppp5/4P3/3R3p/6P1/1K2Nr2/PP3P2/8 b - - 1 32",
24+
"rnbqkbnr/pp1ppppp/8/8/1Pp1PP2/8/P1PP2PP/RNBQKBNR b KQkq b3 0 3",
25+
"rnbqkbnr/p1ppppp1/7p/Pp6/8/8/1PPPPPPP/RNBQKBNR w KQkq b6 0 3",
26+
"rnbqkbnr/1pppppp1/7p/pP6/8/8/P1PPPPPP/RNBQKBNR w KQkq a6 0 3",
27+
"rnbqkbnr/1pppppp1/7p/pP6/4P3/8/P1PP1PPP/RNBQKBNR b KQkq e3 0 3",
28+
}
29+
30+
validXFENs = []string{
31+
"rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1",
32+
"rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1",
33+
"rnbqkbnr/pp1ppppp/8/2p5/4P3/8/PPPP1PPP/RNBQKBNR w KQkq - 0 2",
34+
"rnbqkbnr/pp1ppppp/8/2p5/4P3/5N2/PPPP1PPP/RNBQKB1R b KQkq - 1 2",
35+
"7k/8/8/8/8/8/8/R6K w - - 0 1",
36+
"7k/8/8/8/8/8/8/2B1KB2 w - - 0 1",
37+
"8/8/8/4k3/8/8/8/R3K2R w KQ - 0 1",
38+
"8/8/8/8/4k3/8/3KP3/8 w - - 0 1",
39+
"8/8/5k2/8/5K2/8/4P3/8 w - - 0 1",
40+
"r4rk1/1b2bppp/ppq1p3/2pp3n/5P2/1P1BP3/PBPPQ1PP/R4RK1 w - - 0 1",
41+
"3r1rk1/p3qppp/2bb4/2p5/3p4/1P2P3/PBQN1PPP/2R2RK1 w - - 0 1",
42+
"4r1k1/1b3p1p/ppq3p1/2p5/8/1P3R1Q/PBP3PP/7K w - - 0 1",
43+
"5k2/ppp5/4P3/3R3p/6P1/1K2Nr2/PP3P2/8 b - - 1 32",
44+
"rnbqkbnr/pp1ppppp/8/8/1Pp1PP2/8/P1PP2PP/RNBQKBNR b KQkq b3 0 3",
45+
"rnbqkbnr/p1ppppp1/7p/Pp6/8/8/1PPPPPPP/RNBQKBNR w KQkq b6 0 3",
46+
"rnbqkbnr/1pppppp1/7p/pP6/8/8/P1PPPPPP/RNBQKBNR w KQkq a6 0 3",
47+
"rnbqkbnr/1pppppp1/7p/pP6/4P3/8/P1PP1PPP/RNBQKBNR b KQkq - 0 3",
2448
}
2549

2650
//nolint:gochecknoglobals // test data
@@ -40,14 +64,19 @@ var (
4064
)
4165

4266
func TestValidFENs(t *testing.T) {
43-
for _, f := range validFENs {
67+
for idx, f := range validFENs {
4468
state, err := decodeFEN(f)
4569
if err != nil {
4670
t.Fatal("recieved unexpected error", err)
4771
}
4872
if f != state.String() {
4973
t.Fatalf("fen expected board string %s but got %s", f, state.String())
5074
}
75+
xfen := state.XFENString()
76+
if xfen != validXFENs[idx] {
77+
t.Fatalf("xfen for fen %v (%v) was %v but expected %v", idx, f,
78+
xfen, validXFENs[idx])
79+
}
5180
}
5281
}
5382

position.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,47 @@ func (pos *Position) String() string {
214214
return fmt.Sprintf("%s %s %s %s %d %d", b, t, c, sq, pos.halfMoveClock, pos.moveCount)
215215
}
216216

217+
// XFENString() is similar to String() except that it returns a string with
218+
// the X-FEN format
219+
func (pos *Position) XFENString() string {
220+
b := pos.board.String()
221+
t := pos.turn.String()
222+
c := pos.castleRights.String()
223+
sq := "-"
224+
if pos.enPassantSquare != NoSquare {
225+
// Check if there is a pawn in a position to capture en passant
226+
var rank Rank
227+
if pos.turn == White {
228+
rank = Rank5
229+
} else {
230+
rank = Rank4
231+
}
232+
// The en passant target square will always be on the rank opposite the current turn's pawns
233+
file := pos.enPassantSquare.File()
234+
potentialPawnFiles := []File{file - 1, file + 1} // Pawns that could capture en passant will be on an adjacent file
235+
236+
for _, f := range potentialPawnFiles {
237+
if f < FileA || f > FileH { // Ensure file is within bounds
238+
continue
239+
}
240+
241+
potentialPawnSquare := NewSquare(f, rank)
242+
potentialPawn := pos.board.Piece(potentialPawnSquare)
243+
if potentialPawn == NoPiece {
244+
continue
245+
}
246+
if potentialPawn.Type() != Pawn {
247+
continue
248+
}
249+
if potentialPawn.Color() == pos.turn {
250+
sq = pos.enPassantSquare.String()
251+
break
252+
}
253+
}
254+
}
255+
return fmt.Sprintf("%s %s %s %s %d %d", b, t, c, sq, pos.halfMoveClock, pos.moveCount)
256+
}
257+
217258
// Hash returns a unique hash of the position.
218259
func (pos *Position) Hash() [16]byte {
219260
b, _ := pos.MarshalBinary()

0 commit comments

Comments
 (0)