Skip to content

Commit

Permalink
#186: Add attackers() (#456)
Browse files Browse the repository at this point in the history
Add attackers() method
  • Loading branch information
kylebatucal committed May 10, 2024
1 parent 0762754 commit 52f7579
Show file tree
Hide file tree
Showing 3 changed files with 292 additions and 9 deletions.
32 changes: 31 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,36 @@ chess.ascii()
// a b c d e f g h'
```

### .attackers(square, [ color ])

Returns a list of squares that have pieces belonging to the side to move that
can attack the given square. This function takes an optional parameter which can
change which color the pieces should belong to.

```ts
const chess = new Chess()

chess.attackers('f3')
// -> ['e2', 'g2', 'g1'] (empty squares can be attacked)

chess.attackers('e2')
// -> ['d1', 'e1', 'f1', 'g1'] (we can attack our own pieces)

chess.attackers('f6')
// -> [] (squares not attacked by the side to move will return an empty list)

chess.move('e4')
chess.attackers('f6')
// -> ['g8', 'e7', 'g7'] (return value changes depending on side to move)

chess.attackers('f3', WHITE)
// -> ['g2', 'd1', 'g1'] (side to move can be ignored by specifying a color)

chess.load('4k3/4n3/8/8/8/8/4R3/4K3 w - - 0 1')
chess.attackers('c6', BLACK)
// -> ['e7'] (pieces still attack a square even if they are pinned)
```

### .board()

Returns an 2D array representation of the current position. Empty squares are
Expand Down Expand Up @@ -436,7 +466,7 @@ chess.isAttacked('e2', WHITE)

chess.load('4k3/4n3/8/8/8/8/4R3/4K3 w - - 0 1')
chess.isAttacked('c6', BLACK)
// -> true (pieces still attack a square if even they are pinned)
// -> true (pieces still attack a square even if they are pinned)
```

### .isCheckmate()
Expand Down
218 changes: 218 additions & 0 deletions __tests__/attackers.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
import { Chess, Color, SQUARES, WHITE, BLACK } from '../src/chess'
import 'jest-extended'

function getAttackerCount(chess: Chess, color: Color) {
return Array.from(
{ length: 64 },
(_, i) => chess.attackers(SQUARES[i], color).length,
)
}

test('attackers - attacker count in default position', () => {
const chess = new Chess()

// prettier-ignore
const expectedWhiteAttackerCount = [
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
2, 2, 3, 2, 2, 3, 2, 2,
1, 1, 1, 4, 4, 1, 1, 1,
0, 1, 1, 1, 1, 1, 1, 0,
]
expect(getAttackerCount(chess, WHITE)).toEqual(expectedWhiteAttackerCount)

// prettier-ignore
const expectedBlackAttackerCount = [
0, 1, 1, 1, 1, 1, 1, 0,
1, 1, 1, 4, 4, 1, 1, 1,
2, 2, 3, 2, 2, 3, 2, 2,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
]
expect(getAttackerCount(chess, BLACK)).toEqual(expectedBlackAttackerCount)
})

test('attackers - attacker count in middlegame position', () => {
const chess = new Chess(
'r3kb1r/1b3ppp/pqnppn2/1p6/4PBP1/PNN5/1PPQBP1P/2KR3R b kq - 0 1',
) // Gujrathi–Firouzja, round 6

// prettier-ignore
const expectedWhiteAttackerCount = [
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 2, 0, 0, 0, 1,
1, 2, 1, 3, 1, 2, 1, 1,
1, 1, 1, 2, 1, 1, 1, 0,
1, 1, 2, 3, 3, 1, 3, 0,
1, 1, 2, 4, 2, 0, 0, 2,
1, 2, 3, 5, 3, 3, 2, 1,
]
expect(getAttackerCount(chess, WHITE)).toEqual(expectedWhiteAttackerCount)

// prettier-ignore
const expectedBlackAttackerCount = [
1, 2, 2, 4, 2, 2, 2, 0,
3, 1, 1, 2, 3, 1, 1, 2,
3, 0, 2, 1, 1, 1, 2, 1,
2, 2, 2, 2, 2, 1, 0, 1,
1, 1, 1, 2, 1, 0, 1, 0,
0, 0, 0, 0, 1, 0, 0, 0,
0, 0, 0, 0, 0, 1, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
]
expect(getAttackerCount(chess, BLACK)).toEqual(expectedBlackAttackerCount)
})

test('attackers - attacker count when all but one square is covered', () => {
const chess = new Chess('Q4K1k/1Q5p/2Q5/3Q4/4Q3/5Q2/6Q1/7Q w - - 0 1')

// prettier-ignore
const expectedWhiteAttackerCount = [
1, 2, 3, 2, 4, 2, 3, 0,
2, 2, 2, 3, 3, 4, 3, 3,
3, 2, 2, 2, 3, 2, 3, 2,
2, 3, 2, 2, 2, 3, 2, 3,
3, 2, 3, 2, 2, 2, 3, 2,
2, 3, 2, 3, 2, 2, 2, 3,
3, 2, 3, 2, 3, 2, 2, 2,
2, 3, 2, 3, 2, 3, 2, 1,
]
expect(getAttackerCount(chess, WHITE)).toEqual(expectedWhiteAttackerCount)

// prettier-ignore
const expectedBlackAttackerCount = [
0, 0, 0, 0, 0, 0, 1, 0,
0, 0, 0, 0, 0, 0, 1, 1,
0, 0, 0, 0, 0, 0, 1, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
]
expect(getAttackerCount(chess, BLACK)).toEqual(expectedBlackAttackerCount)
})

test('attackers - return value depends on side to move', () => {
const chess = new Chess()
expect(chess.attackers('c3')).toIncludeSameMembers(['b1', 'b2', 'd2'])
expect(chess.attackers('c6')).toEqual([])

chess.move('e4')
expect(chess.attackers('c3')).toEqual([])
expect(chess.attackers('c6')).toIncludeSameMembers(['b7', 'b8', 'd7'])

chess.move('e5')
expect(chess.attackers('c3')).toIncludeSameMembers(['b1', 'b2', 'd2'])
expect(chess.attackers('c6')).toEqual([])
})

test('attackers - every piece attacking empty square', () => {
const chess = new Chess('2b5/4kp2/2r5/3q2n1/8/8/4P3/4K3 w - - 0 1')
expect(chess.attackers('e6', BLACK)).toIncludeSameMembers([
'c6',
'c8',
'd5',
'e7',
'f7',
'g5',
])
})

test('attackers - every piece attacking another piece', () => {
const chess = new Chess('4k3/8/8/8/5Q2/5p1R/4PK2/4N2B w - - 0 1')
expect(chess.attackers('f3')).toIncludeSameMembers([
'e1',
'e2',
'f2',
'f4',
'h1',
'h3',
])
})

test('attackers - every piece defending empty square', () => {
const chess = new Chess('B3k3/8/8/2K4R/3QPN2/8/8/8 w - - 0 1')
expect(chess.attackers('d5', WHITE)).toIncludeSameMembers([
'a8',
'c5',
'd4',
'e4',
'f4',
'h5',
])
})

test('attackers - every piece defending another piece', () => {
const chess = new Chess('2r5/1b1p4/1kp1q3/4n3/8/8/8/4K3 b - - 0 1')
expect(chess.attackers('c6')).toIncludeSameMembers([
'b6',
'b7',
'c8',
'd7',
'e5',
'e6',
])
})

test('attackers - pinned pieces still attack and defend', () => {
// knight on c3 is pinned, but it is still attacking d4 and defending e5
const chess = new Chess(
'r1bqkbnr/ppp2ppp/2np4/1B2p3/3PP3/5N2/PPP2PPP/RNBQK2R b KQkq - 0 4',
)
expect(chess.attackers('d4', BLACK)).toIncludeSameMembers(['c6', 'e5'])
expect(chess.attackers('e5', BLACK)).toIncludeSameMembers(['c6', 'd6'])
})

test('attackers - king can "attack" defended piece', () => {
const chess = new Chess('3k4/8/8/8/3b4/3R4/4Pq2/4K3 w - - 0 1')
expect(chess.attackers('f2', WHITE)).toIncludeSameMembers(['e1'])
})

test('attackers - a lot of attackers', () => {
const chess = new Chess(
'5k2/8/3N1N2/2NBQQN1/3R1R2/2NPRPN1/3N1N2/4K3 w - - 0 1',
)
expect(chess.attackers('e4', WHITE)).toIncludeSameMembers([
'c3',
'c5',
'd2',
'd3',
'd4',
'd5',
'd6',
'e3',
'e5',
'f2',
'f3',
'f4',
'f5',
'f6',
'g3',
'g5',
])
})

test('attackers - no attackers', () => {
const chess = new Chess()
expect(chess.attackers('e4', WHITE)).toEqual([])
})

test('attackers - readme tests', () => {
const chess = new Chess()
expect(chess.attackers('f3')).toEqual(['e2', 'g2', 'g1'])
expect(chess.attackers('e2')).toEqual(['d1', 'e1', 'f1', 'g1'])
expect(chess.attackers('f6')).toEqual([])
chess.move('e4')
expect(chess.attackers('f6')).toEqual(['g8', 'e7', 'g7'])
expect(chess.attackers('f3', WHITE)).toEqual(['g2', 'd1', 'g1'])
chess.load('4k3/4n3/8/8/8/8/4R3/4K3 w - - 0 1')
expect(chess.attackers('c6', BLACK)).toEqual(['e7'])
})
51 changes: 43 additions & 8 deletions src/chess.ts
Original file line number Diff line number Diff line change
Expand Up @@ -906,7 +906,11 @@ export class Chess {
}
}

private _attacked(color: Color, square: number) {
private _attacked(color: Color, square: number): boolean
private _attacked(color: Color, square: number, verbose: false): boolean
private _attacked(color: Color, square: number, verbose: true): Square[]
private _attacked(color: Color, square: number, verbose?: boolean) {
const attackers: Square[] = []
for (let i = Ox88.a8; i <= Ox88.h1; i++) {
// did we run off the end of the board
if (i & 0x88) {
Expand All @@ -931,16 +935,28 @@ export class Chess {

if (ATTACKS[index] & PIECE_MASKS[piece.type]) {
if (piece.type === PAWN) {
if (difference > 0) {
if (piece.color === WHITE) return true
} else {
if (piece.color === BLACK) return true
if (
(difference > 0 && piece.color === WHITE) ||
(difference <= 0 && piece.color === BLACK)
) {
if (!verbose) {
return true
} else {
attackers.push(algebraic(i))
}
}
continue
}

// if the piece is a knight or a king
if (piece.type === 'n' || piece.type === 'k') return true
if (piece.type === 'n' || piece.type === 'k') {
if (!verbose) {
return true
} else {
attackers.push(algebraic(i))
continue
}
}

const offset = RAYS[index]
let j = i + offset
Expand All @@ -954,11 +970,30 @@ export class Chess {
j += offset
}

if (!blocked) return true
if (!blocked) {
if (!verbose) {
return true
} else {
attackers.push(algebraic(i))
continue
}
}
}
}

return false
if (verbose) {
return attackers
} else {
return false
}
}

attackers(square: Square, attackedBy?: Color) {
if (!attackedBy) {
return this._attacked(this._turn, Ox88[square], true)
} else {
return this._attacked(attackedBy, Ox88[square], true)
}
}

private _isKingAttacked(color: Color) {
Expand Down

0 comments on commit 52f7579

Please sign in to comment.