diff --git a/fen_test.go b/fen_test.go index ee6dd98..85bf7e9 100644 --- a/fen_test.go +++ b/fen_test.go @@ -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{ @@ -34,7 +58,7 @@ 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) @@ -42,6 +66,11 @@ func TestValidFENs(t *testing.T) { 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]) + } } } diff --git a/position.go b/position.go index ee67fa9..33cfdb5 100644 --- a/position.go +++ b/position.go @@ -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()