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
80 changes: 80 additions & 0 deletions attacks.go
Original file line number Diff line number Diff line change
@@ -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
}
32 changes: 32 additions & 0 deletions attacks_benchmark_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
})
}
}
113 changes: 113 additions & 0 deletions attacks_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
}