Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions domains/games/libs/toyfish/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -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",
],
)
57 changes: 41 additions & 16 deletions domains/games/libs/toyfish/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand All @@ -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) {
Expand Down Expand Up @@ -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 {
Expand All @@ -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 {
Expand All @@ -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
}

Expand All @@ -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
}
}
Expand All @@ -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
Expand Down
71 changes: 71 additions & 0 deletions domains/games/libs/toyfish/types_test.go
Original file line number Diff line number Diff line change
@@ -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")
}
Loading