Skip to content
This repository has been archived by the owner on Jan 25, 2025. It is now read-only.

Commit

Permalink
Add position.XFENString() (#126)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
mikeb26 authored Nov 25, 2024
1 parent c78e63f commit b30c702
Show file tree
Hide file tree
Showing 2 changed files with 71 additions and 1 deletion.
31 changes: 30 additions & 1 deletion fen_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,30 @@ var (
"3r1rk1/p3qppp/2bb4/2p5/3p4/1P2P3/PBQN1PPP/2R2RK1 w - - 0 1",
"4r1k1/1b3p1p/ppq3p1/2p5/8/1P3R1Q/PBP3PP/7K w - - 0 1",
"5k2/ppp5/4P3/3R3p/6P1/1K2Nr2/PP3P2/8 b - - 1 32",
"rnbqkbnr/pp1ppppp/8/8/1Pp1PP2/8/P1PP2PP/RNBQKBNR b KQkq b3 0 3",
"rnbqkbnr/p1ppppp1/7p/Pp6/8/8/1PPPPPPP/RNBQKBNR w KQkq b6 0 3",
"rnbqkbnr/1pppppp1/7p/pP6/8/8/P1PPPPPP/RNBQKBNR w KQkq a6 0 3",
"rnbqkbnr/1pppppp1/7p/pP6/4P3/8/P1PP1PPP/RNBQKBNR b KQkq e3 0 3",
}

validXFENs = []string{
"rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1",
"rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1",
"rnbqkbnr/pp1ppppp/8/2p5/4P3/8/PPPP1PPP/RNBQKBNR w KQkq - 0 2",
"rnbqkbnr/pp1ppppp/8/2p5/4P3/5N2/PPPP1PPP/RNBQKB1R b KQkq - 1 2",
"7k/8/8/8/8/8/8/R6K w - - 0 1",
"7k/8/8/8/8/8/8/2B1KB2 w - - 0 1",
"8/8/8/4k3/8/8/8/R3K2R w KQ - 0 1",
"8/8/8/8/4k3/8/3KP3/8 w - - 0 1",
"8/8/5k2/8/5K2/8/4P3/8 w - - 0 1",
"r4rk1/1b2bppp/ppq1p3/2pp3n/5P2/1P1BP3/PBPPQ1PP/R4RK1 w - - 0 1",
"3r1rk1/p3qppp/2bb4/2p5/3p4/1P2P3/PBQN1PPP/2R2RK1 w - - 0 1",
"4r1k1/1b3p1p/ppq3p1/2p5/8/1P3R1Q/PBP3PP/7K w - - 0 1",
"5k2/ppp5/4P3/3R3p/6P1/1K2Nr2/PP3P2/8 b - - 1 32",
"rnbqkbnr/pp1ppppp/8/8/1Pp1PP2/8/P1PP2PP/RNBQKBNR b KQkq b3 0 3",
"rnbqkbnr/p1ppppp1/7p/Pp6/8/8/1PPPPPPP/RNBQKBNR w KQkq b6 0 3",
"rnbqkbnr/1pppppp1/7p/pP6/8/8/P1PPPPPP/RNBQKBNR w KQkq a6 0 3",
"rnbqkbnr/1pppppp1/7p/pP6/4P3/8/P1PP1PPP/RNBQKBNR b KQkq - 0 3",
}

invalidFENs = []string{
Expand All @@ -34,14 +58,19 @@ var (
)

func TestValidFENs(t *testing.T) {
for _, f := range validFENs {
for idx, f := range validFENs {
state, err := decodeFEN(f)
if err != nil {
t.Fatal("recieved unexpected error", err)
}
if f != state.String() {
t.Fatalf("fen expected board string %s but got %s", f, state.String())
}
xfen := state.XFENString()
if xfen != validXFENs[idx] {
t.Fatalf("xfen for fen %v (%v) was %v but expected %v", idx, f,
xfen, validXFENs[idx])
}
}
}

Expand Down
41 changes: 41 additions & 0 deletions position.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,47 @@ func (pos *Position) String() string {
return fmt.Sprintf("%s %s %s %s %d %d", b, t, c, sq, pos.halfMoveClock, pos.moveCount)
}

// XFENString() is similar to String() except that it returns a string with
// the X-FEN format
func (pos *Position) XFENString() string {
b := pos.board.String()
t := pos.turn.String()
c := pos.castleRights.String()
sq := "-"
if pos.enPassantSquare != NoSquare {
// Check if there is a pawn in a position to capture en passant
var rank Rank
if pos.turn == White {
rank = Rank5
} else {
rank = Rank4
}
// The en passant target square will always be on the rank opposite the current turn's pawns
file := pos.enPassantSquare.File()
potentialPawnFiles := []File{file - 1, file + 1} // Pawns that could capture en passant will be on an adjacent file

for _, f := range potentialPawnFiles {
if f < FileA || f > FileH { // Ensure file is within bounds
continue
}

potentialPawnSquare := NewSquare(f, rank)
potentialPawn := pos.board.Piece(potentialPawnSquare)
if potentialPawn == NoPiece {
continue
}
if potentialPawn.Type() != Pawn {
continue
}
if potentialPawn.Color() == pos.turn {
sq = pos.enPassantSquare.String()
break
}
}
}
return fmt.Sprintf("%s %s %s %s %d %d", b, t, c, sq, pos.halfMoveClock, pos.moveCount)
}

// Hash returns a unique hash of the position
func (pos *Position) Hash() [16]byte {
b, _ := pos.MarshalBinary()
Expand Down

0 comments on commit b30c702

Please sign in to comment.