Skip to content

Commit fee9636

Browse files
authored
Fix ability for king to castle while in check (#12)
* Added validation for king check state when determining ability to castle. * Added invalid castling unit tests to prevent issue in the future. * Minor documentation improvements in `Board.swift`. fixes #11
2 parents 8732e0e + 8f8f19d commit fee9636

File tree

4 files changed

+48
-14
lines changed

4 files changed

+48
-14
lines changed

CHANGELOG.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@
33
#### Improvements
44
* PGN parsing now supports tag pairs (for example `[Event "Name"]`) located at the top of the PGN format, see [Issue #8](https://github.com/chesskit-app/chesskit-swift/issues/8).
55

6-
### Breaking Changes
6+
#### Bug Fixes
7+
* Fix issue where king is allowed to castle in check, see [Issue #11](https://github.com/chesskit-app/chesskit-swift/issues/11).
8+
9+
#### Breaking Changes
710
* Remove `color` parameter from `Move.init(san:color:position:)` initializer.
811
* It was not being used, can be removed from any initializer call where it was included.
912
* The new initializer is simply `Move.init(san:position:)`.

README.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -83,16 +83,17 @@ print(board.legalMoves(forPieceAt: .e2)) // returns [.e3, .e4]
8383

8484
* Parse [FEN](https://en.wikipedia.org/wiki/Forsyth–Edwards_Notation) into a `Position` object
8585
``` swift
86-
// use Position initializer
86+
// parse FEN using Position initializer
8787
let fen = "rnbqkbnr/pp1ppppp/8/2p5/4P3/5N2/PPPP1PPP/RNBQKB1R b KQkq - 1 2"
8888
let position = Position(fen: fen)
8989

9090
// convert Position to FEN string
91-
let fenString = Position.standard.fen
91+
let fenString = position.fen
9292
```
9393

94-
* Similarly parse [PGN](https://en.wikipedia.org/wiki/Portable_Game_Notation) (into `Game`) or [SAN](https://en.wikipedia.org/wiki/Algebraic_notation_(chess)) (into `Move`).
94+
* Similarly, parse [PGN](https://en.wikipedia.org/wiki/Portable_Game_Notation) (into `Game`) or [SAN](https://en.wikipedia.org/wiki/Algebraic_notation_(chess)) (into `Move`).
9595
``` swift
96+
// parse PGN using Game initializer
9697
let game = Game(pgn: "1. e4 e5 2. Nf3")
9798

9899
// convert Game to PGN string

Sources/ChessKit/Board.swift

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ public struct Board {
3434
/// Initializes a board with the given `position`.
3535
///
3636
/// - parameter position: The starting position of the board.
37-
/// Defaults to `Position.standard` which is the starting position
37+
/// The default is `Position.standard` which is the starting position
3838
/// for a standard chess game.
3939
///
4040
public init(position: Position = .standard) {
@@ -48,6 +48,7 @@ public struct Board {
4848
///
4949
/// - parameter start: The starting square of the piece.
5050
/// - parameter end: The ending square of the piece.
51+
///
5152
/// - returns: The `Move` object representing the move.
5253
///
5354
/// If `start` doesn't contain a piece or `end` is not a valid legal move
@@ -155,7 +156,9 @@ public struct Board {
155156
///
156157
/// - parameter square: The square currently containing the piece.
157158
/// - parameter newSquare: The new square for the piece.
159+
///
158160
/// - returns: Whether or not the move is valid.
161+
///
159162
public func canMove(pieceAt square: Square, to newSquare: Square) -> Bool {
160163
guard let piece = set.get(square) else { return false }
161164
return legalMoves(for: piece, in: set) & newSquare.bb != 0
@@ -164,8 +167,10 @@ public struct Board {
164167
/// Returns the possible legal moves for a piece at a given square.
165168
///
166169
/// - parameter square: The square containing the piece to check.
170+
///
167171
/// - returns: An array of squares containing legal moves or an empty
168172
/// array if there are no legal moves or if there is no piece at `square`.
173+
///
169174
public func legalMoves(forPieceAt square: Square) -> [Square] {
170175
guard let piece = set.get(square) else { return [] }
171176
return legalMoves(for: piece, in: set).squares
@@ -175,6 +180,7 @@ public struct Board {
175180
///
176181
/// - parameter move: The move that triggered the promotion.
177182
/// - parameter kind: The piece kind to promote a pawn to.
183+
///
178184
/// - returns: The final move containing the promoted piece.
179185
///
180186
/// Call this when a pawn reaches the opposite side of the board
@@ -196,7 +202,6 @@ public struct Board {
196202

197203
/// Determines check state and handles pawn promotion for
198204
/// provided `move`.
199-
///
200205
private func process(move: Move) -> Move {
201206
var processedMove = move
202207

@@ -223,9 +228,7 @@ public struct Board {
223228
return processedMove
224229
}
225230

226-
/// Determines the current check state for the
227-
/// provided `color`.
228-
///
231+
/// Determines the current check state for the provided `color`.
229232
private func checkState(for color: Piece.Color) -> Move.CheckState {
230233
var checkState: Move.CheckState = .none
231234

@@ -281,6 +284,7 @@ public struct Board {
281284

282285
// MARK: - Move Validation
283286

287+
/// Determines the legal moves for the given `piece` in `set`.
284288
private func legalMoves(for piece: Piece, in set: PieceSet) -> Bitboard {
285289
let attacks = switch piece.kind {
286290
case .king:
@@ -312,6 +316,7 @@ public struct Board {
312316
///
313317
/// - parameter piece: The piece to move.
314318
/// - parameter square: The square to move the piece to.
319+
///
315320
/// - returns: Whether the move is valid.
316321
///
317322
private func validate(moveFor piece: Piece, to square: Square) -> Bool {
@@ -333,6 +338,7 @@ public struct Board {
333338
///
334339
/// - parameter sq: A bitboard corresponding to the square of interest.
335340
/// - parameter set: The piece set for which to calculate attackers.
341+
///
336342
/// - returns: A bitboard with the locations of the pieces in `set`
337343
/// that attack `sq`.
338344
///
@@ -354,6 +360,7 @@ public struct Board {
354360
///
355361
/// - parameter color: The color of the king.
356362
/// - parameter set: The set of pieces on the board.
363+
///
357364
/// - returns: Whether or not the king with `color` is in check.
358365
///
359366
private func isKingInCheck(_ color: Piece.Color, set: PieceSet) -> Bool {
@@ -395,10 +402,7 @@ public struct Board {
395402
let singleMove = movement(1)
396403

397404
// double pawn push for starting move
398-
var extraMove = Bitboard(0)
399-
if isOnStartingRank {
400-
extraMove = movement(2)
401-
}
405+
let extraMove = isOnStartingRank ? movement(2) : 0
402406

403407
// en passant move
404408
var enPassantMove = Bitboard(0)
@@ -418,6 +422,7 @@ public struct Board {
418422
/// - parameter color: The color of the pawn.
419423
/// - parameter sq: A bitboard representing the square the pawn is currently on.
420424
/// - parameter set: The set of pieces active on the board.
425+
///
421426
/// - returns: A bitboard of the possible capturing pawn moves.
422427
///
423428
/// For the purposes of `Board`, en-passant is not considered a capturing move.
@@ -437,6 +442,7 @@ public struct Board {
437442
/// - parameter color: The color of the pawn.
438443
/// - parameter sq: A bitboard representing the square the pawn is currently on.
439444
/// - parameter set: The set of pieces active on the board.
445+
///
440446
/// - returns: A bitboard of the possible pawn moves.
441447
///
442448
private func pawnAttacks(
@@ -501,7 +507,6 @@ public struct Board {
501507

502508
/// Determines whether the king of the provided `color` can
503509
/// castle according to `castling` given `set`.
504-
///
505510
private func canCastle(_ color: Piece.Color, castling: Castling, set: PieceSet) -> Bool {
506511
let us = set.get(color)
507512

@@ -512,10 +517,13 @@ public struct Board {
512517
attackers(to: $0.bb, set: set) & ~us == 0
513518
}
514519

520+
let notInCheck = !isKingInCheck(color, set: set)
521+
515522
return position.legalCastlings.contains(castling)
516523
&& validKing != 0
517524
&& validRook != 0
518525
&& notCastlingThroughCheck
526+
&& notInCheck
519527
}
520528

521529
}

Tests/ChessKitTests/BoardTests.swift

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,28 @@ class BoardTests: XCTestCase {
6161
XCTAssertEqual(bkMove.result, .castle(.bK))
6262
}
6363

64+
func testInvalidCastling() {
65+
let position = Position(
66+
pieces: [
67+
.init(.queen, color: .black, square: .e8),
68+
.init(.king, color: .white, square: .e1),
69+
.init(.rook, color: .white, square: .h1)
70+
]
71+
)
72+
var board = Board(position: position)
73+
74+
// attempt to castle while in check
75+
XCTAssertFalse(board.canMove(pieceAt: .e1, to: .g1))
76+
77+
// attempt to castle through check
78+
board.move(pieceAt: .e8, to: .f8)
79+
XCTAssertFalse(board.canMove(pieceAt: .e1, to: .g1))
80+
81+
// valid castling move
82+
board.move(pieceAt: .f8, to: .h8)
83+
XCTAssertTrue(board.canMove(pieceAt: .e1, to: .g1))
84+
}
85+
6486
func testPromotion() {
6587
let pawn = Piece(.pawn, color: .white, square: .e7)
6688
let queen = Piece(.queen, color: .white, square: .e8)

0 commit comments

Comments
 (0)