diff --git a/attacks.go b/attacks.go new file mode 100644 index 00000000..03352b4b --- /dev/null +++ b/attacks.go @@ -0,0 +1,80 @@ +package chess + +import "math/bits" + +// AttacksFrom returns the squares attacked by the piece on sq. +// +// Attacks are based only on piece movement and board occupancy. They are +// independent of the side to move, check, pins and other legal-move +// restrictions. +// +// For sliding pieces, the first occupied square in each direction is included, +// regardless of the occupying piece's color, and squares beyond it are excluded. +// +// An empty or invalid source square returns an empty slice. +func (b *Board) AttacksFrom(sq Square) []Square { + if b == nil || sq < A1 || sq > H8 { + return nil + } + + piece := b.Piece(sq) + if piece == NoPiece { + return nil + } + + attacks := bbForPieceAttacks(b, piece, sq) + + result := make([]Square, 0, bits.OnesCount64(uint64(attacks))) + for index := range numOfSquaresInBoard { + target := Square(index) + if attacks.Occupied(target) { + result = append(result, target) + } + } + + return result +} + +func bbForPieceAttacks(board *Board, piece Piece, sq Square) bitboard { + occupied := ^board.emptySqs + + switch piece.Type() { + case Pawn: + return pawnAttacksFrom(piece.Color(), sq) + + case Knight: + return bbKnightMoves[sq] + + case Bishop: + return diaAttack(occupied, sq) + + case Rook: + return hvAttack(occupied, sq) + + case Queen: + return diaAttack(occupied, sq) | + hvAttack(occupied, sq) + + case King: + return bbKingMoves[sq] + + default: + return 0 + } +} + +func pawnAttacksFrom(color Color, sq Square) bitboard { + source := bbForSquare(sq) + + if color == White { + captureRight := (source & ^bbFileH & ^bbRank8) >> 9 + captureLeft := (source & ^bbFileA & ^bbRank8) >> 7 + + return captureRight | captureLeft + } + + captureRight := (source & ^bbFileH & ^bbRank1) << 7 + captureLeft := (source & ^bbFileA & ^bbRank1) << 9 + + return captureRight | captureLeft +} diff --git a/attacks_benchmark_test.go b/attacks_benchmark_test.go new file mode 100644 index 00000000..1dc5f1be --- /dev/null +++ b/attacks_benchmark_test.go @@ -0,0 +1,32 @@ +package chess + +import "testing" + +func BenchmarkBoardAttacksFrom(b *testing.B) { + pos := mustPosition( + "r3k2r/ppp2ppp/2n1b3/3q4/3R4/2N1B3/PPP2PPP/R3K2R w KQkq - 0 1", + ) + board := pos.Board() + + tests := []struct { + name string + sq Square + }{ + {"Pawn", A2}, + {"Knight", C3}, + {"Bishop", E3}, + {"Rook", D4}, + {"Queen", D5}, + {"King", E1}, + } + + for _, test := range tests { + b.Run(test.name, func(b *testing.B) { + b.ReportAllocs() + + for i := 0; i < b.N; i++ { + _ = board.AttacksFrom(test.sq) + } + }) + } +} \ No newline at end of file diff --git a/attacks_test.go b/attacks_test.go new file mode 100644 index 00000000..6c79fb52 --- /dev/null +++ b/attacks_test.go @@ -0,0 +1,113 @@ +package chess + +import "testing" + +func assertSameSquares(t *testing.T, got []Square, want ...Square) { + t.Helper() + + if len(got) != len(want) { + t.Fatalf("got %v, want %v", got, want) + } + + gotSet := make(map[Square]bool, len(got)) + for _, sq := range got { + gotSet[sq] = true + } + + for _, sq := range want { + if !gotSet[sq] { + t.Errorf("missing expected square %s; got %v", sq, got) + } + } +} + +func TestBoardAttacksFromWhitePawn(t *testing.T) { + pos := mustPosition( + "4k3/8/8/8/4P3/8/8/4K3 w - - 0 1", + ) + + got := pos.Board().AttacksFrom(E4) + + assertSameSquares(t, got, D5, F5) +} + +func TestBoardAttacksFromBlackPawn(t *testing.T) { + pos := mustPosition( + "4k3/8/8/4p3/8/8/8/4K3 b - - 0 1", + ) + + got := pos.Board().AttacksFrom(E5) + + assertSameSquares(t, got, D4, F4) +} + +func TestBoardAttacksFromPawnOnAFile(t *testing.T) { + pos := mustPosition( + "4k3/8/8/8/P7/8/8/4K3 w - - 0 1", + ) + + got := pos.Board().AttacksFrom(A4) + + assertSameSquares(t, got, B5) +} + +func TestBoardAttacksFromRookStopsAfterBlockers(t *testing.T) { + pos := mustPosition( + "4k3/8/3P4/8/1n1R1b2/8/3p4/4K3 w - - 0 1", + ) + + got := pos.Board().AttacksFrom(D4) + + assertSameSquares( + t, + got, + D5, D6, + D3, D2, + C4, B4, + E4, F4, + ) +} + +func TestBoardAttacksFromEmptySquare(t *testing.T) { + board := StartingPosition().Board() + + got := board.AttacksFrom(E4) + + if len(got) != 0 { + t.Fatalf("expected no attacks, got %v", got) + } +} + +func TestBoardAttacksFromInvalidSquare(t *testing.T) { + board := StartingPosition().Board() + + for _, sq := range []Square{NoSquare, Square(64)} { + if got := board.AttacksFrom(sq); len(got) != 0 { + t.Errorf("AttacksFrom(%d) = %v, want empty", sq, got) + } + } +} + +func TestBoardAttacksFromIgnoresPins(t *testing.T) { + pos := mustPosition( + "4r1k1/8/8/8/8/8/4R3/4K3 w - - 0 1", + ) + + got := pos.Board().AttacksFrom(E2) + + // The rook is pinned against the white king, but it still attacks + // horizontally for attack-map purposes. + for _, expected := range []Square{A2, B2, C2, D2, F2, G2, H2} { + found := false + for _, actual := range got { + if actual == expected { + found = true + break + } + } + + if !found { + t.Errorf("expected pinned rook to attack %s", expected) + } + } +}