diff --git a/domains/games/libs/toyfish/BUILD.bazel b/domains/games/libs/toyfish/BUILD.bazel index bd6abd26..9ebdb713 100644 --- a/domains/games/libs/toyfish/BUILD.bazel +++ b/domains/games/libs/toyfish/BUILD.bazel @@ -26,4 +26,8 @@ go_test( size = "small", srcs = ["types_test.go"], embed = [":types"], + deps = [ + "@com_github_stretchr_testify//assert", + "@com_github_stretchr_testify//require", + ], ) diff --git a/domains/games/libs/toyfish/types.go b/domains/games/libs/toyfish/types.go index 68c5eb21..a1ebd75b 100644 --- a/domains/games/libs/toyfish/types.go +++ b/domains/games/libs/toyfish/types.go @@ -52,7 +52,7 @@ func populatePieceMap() map[int8]*Piece { Side: White, Value: pieceInfo.S, } - lowerCased := int8(strings.ToLower(string(pieceInfo.F))[0]) + lowerCased := int8(strings.ToLower(string(rune(pieceInfo.F)))[0]) pieceMap[lowerCased] = &Piece{ FenRepr: lowerCased, Glyph: pieceInfo.G[1], @@ -65,10 +65,11 @@ func populatePieceMap() map[int8]*Piece { } type Game struct { - StartingFen string - Settings *s.Settings - Board Board - Side Color + StartingFen string + Settings *s.Settings + Board Board + Side Color + EnPassantSquare int } func FenToBoard(fen string, pieceMap map[int8]*Piece) (Board, error) { @@ -133,7 +134,7 @@ func (g *Game) GenerateMoves() []Move { for idx, piece := range g.Board { //square := g.Settings.Coordinates[idx] if piece != nil && piece.Side == g.Side { - for _, offset := range g.Settings.Directions[s.Piece(piece.FenRepr)] { + for _, offset := range g.Settings.Directions[s.Piece(rune(piece.FenRepr))] { //fmt.Printf("piece %c on square %s with offset %d\n", piece.Glyph, square, offset) targetSquare := idx for { @@ -151,10 +152,20 @@ func (g *Game) GenerateMoves() []Move { // *************************************************** // ******************* PAWN LOGIC ******************** // *************************************************** - if strings.Contains("pP", string(piece.FenRepr)) { + if strings.Contains("pP", string(rune(piece.FenRepr))) { // no en-passant on empty square if offset%10 != 0 && capturedPiece == nil { - break + if targetSquare == g.EnPassantSquare { + capturedPawnIdx := targetSquare + if piece.Side == White { + capturedPawnIdx += 10 + } else { + capturedPawnIdx -= 10 + } + capturedPiece = g.Board[capturedPawnIdx] + } else { + break + } } // pawns can't capture forward if offset%10 == 0 && capturedPiece != nil { @@ -178,14 +189,12 @@ func (g *Game) GenerateMoves() []Move { break } } - // TODO: implement es-passant capture suppoer - // TODO: implement castling rules // *************************************************** // ******************* CHECKMATE ********************* // *************************************************** - if capturedPiece != nil && strings.Contains("kK", string(capturedPiece.FenRepr)) { + if capturedPiece != nil && strings.Contains("kK", string(rune(capturedPiece.FenRepr))) { return nil } @@ -208,7 +217,7 @@ func (g *Game) GenerateMoves() []Move { } // pawn, knight, and king aren't sliding pieces (only one move at a time in each allowed direction) - if strings.Contains("pPnNkK", string(piece.FenRepr)) { + if strings.Contains("pPnNkK", string(rune(piece.FenRepr))) { break } } @@ -224,11 +233,27 @@ func NewGame(settings *s.Settings) (*Game, error) { if err != nil { return nil, err } + + enPassantSquare := -1 + parts := strings.Split(settings.Fen, " ") + if len(parts) > 3 { + epTarget := parts[3] + if epTarget != "-" { + for i, coord := range settings.Coordinates { + if coord == epTarget { + enPassantSquare = i + break + } + } + } + } + game := Game{ - StartingFen: settings.Fen, - Settings: settings, - Board: board, - Side: White, + StartingFen: settings.Fen, + Settings: settings, + Board: board, + Side: White, + EnPassantSquare: enPassantSquare, } return &game, nil diff --git a/domains/games/libs/toyfish/types_test.go b/domains/games/libs/toyfish/types_test.go index f3443b7a..f1fedfe3 100644 --- a/domains/games/libs/toyfish/types_test.go +++ b/domains/games/libs/toyfish/types_test.go @@ -1 +1,72 @@ package toyfish + +import ( + "testing" + s "github.com/muchq/moonbase/domains/games/libs/toyfish/settings" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func mockSettings(fen string) *s.Settings { + // Minimal settings for testing pawn moves + coords := make([]string, 120) + for i := range coords { + coords[i] = "xx" + } + + files := "abcdefgh" + ranks := "87654321" + + for r, rank := range ranks { + for f, file := range files { + idx := 21 + r*10 + f + coords[idx] = string(file) + string(rank) + } + } + + return &s.Settings{ + Fen: fen, + Coordinates: coords, + Directions: map[s.Piece][]int{ + "P": {-10, -20, -9, -11}, + "p": {10, 20, 9, 11}, + }, + Rank2: []int{81, 82, 83, 84, 85, 86, 87, 88}, + Rank7: []int{31, 32, 33, 34, 35, 36, 37, 38}, + } +} + +func TestEnPassant(t *testing.T) { + // FEN: White pawn at e5, Black pawn at d5 (just moved d7-d5). + // En Passant target is d6. + // e5 is index 55 (Rank 5, File e -> 5th char -> index 4). 21 + 3*10 + 4 = 55. Correct. + // d5 is index 54. + // d6 is index 44. + + fen := "8/8/8/3pP3/8/8/8/8 w - d6 0 1" + settings := mockSettings(fen) + + game, err := NewGame(settings) + require.NoError(t, err) + + // Check EnPassantSquare parsing + assert.Equal(t, 44, game.EnPassantSquare, "EnPassantSquare should be parsed correctly (d6 -> 44)") + + moves := game.GenerateMoves() + + // Look for move e5xd6 (55 -> 44) + found := false + for _, m := range moves { + if m.Source == 55 && m.Target == 44 { + found = true + assert.NotNil(t, m.CapturedPiece, "CapturedPiece should not be nil") + assert.Equal(t, int8('p'), m.CapturedPiece.FenRepr, "Captured piece should be black pawn") + // The captured piece is at d5 (54), not d6 (44) + // But Move struct doesn't expose captured piece location directly, + // however we can verify it's the correct piece object (or at least correct type/side). + break + } + } + + assert.True(t, found, "En Passant move e5xd6 should be generated") +}