Skip to content

Commit

Permalink
Implement threefold repetition check (#37)
Browse files Browse the repository at this point in the history
* LegalCastlings and PieceSet are now Hashable

* Implemented "occurred" function to check position repetition

* Implemented position hashed value

* Added gamePositions dicitonary to "Board" and check for Threefold repetition

* Added test to check Threefold repetition

* Re-added Sendable to LegalCastlings

* Position now conforms to Hashable protocol

* Moved part of Threefold repetition check logic from Position to Board

* Added sideSquares property to Piece

* Fixed a bug where enPassant pawn would be written inside the FEN notation every time a pawn moved by 2 squares

* Changed Threefold repetition test according to the new structure

* Fixed boardPermormanceTest according to the FEN notation change

* Implemented check to avoid writing enPassant square into FEN notation when it would lead to a check

* Changed test according to new check

* Added check to avoid writing enPassant square in FEN notation when the nearby pawn is not of the opposite color

* Removed sideSquares from "Piece.swift"

* Renamed `canBeCaptured` function to `couldBeCaptured`

* Re-inserted all the enPassant capture square inside the  different FEN notations

* Added "enPassantIsPossible" var to Position

* Added left and right Square variables

* Added function "validateEnPassant" and removed all the old part

* Added test for double enPassant
One pawn can execute it, one cannot

* Now `couldBeCaptured(...)` function is again Internal

* Square `left` and `right` matches the project style

* Removed `board.hashedPositions()` and changed test accordingly

* Indentation changes
  • Loading branch information
joee-ca authored Aug 3, 2024
1 parent cf39da1 commit e3bceb0
Show file tree
Hide file tree
Showing 8 changed files with 129 additions and 27 deletions.
2 changes: 1 addition & 1 deletion Sources/ChessKit/Bitboards/PieceSet.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
///
/// Also contains convenient amalgamations
/// of different combinations of pieces.
struct PieceSet: Equatable, Sendable {
struct PieceSet: Equatable, Hashable, Sendable {
/// Bitboard for black king pieces.
var k: Bitboard = 0
/// Bitboard for black queen pieces.
Expand Down
39 changes: 36 additions & 3 deletions Sources/ChessKit/Board.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ public struct Board: Sendable {
/// The current position represented on the board.
public var position: Position

/// All the positions occurred during the game and how many times they appeared
private var positionHashCounts: [Int: Int]

/// Convenience accessor for the pieces in `position`.
private var set: PieceSet {
position.pieceSet
Expand All @@ -38,6 +41,7 @@ public struct Board: Sendable {
public init(position: Position = .standard) {
Attacks.create()
self.position = position
self.positionHashCounts = [:]
}

// MARK: - Public
Expand Down Expand Up @@ -79,6 +83,7 @@ public struct Board: Sendable {
return process(move: Move(result: .capture(enPassant.pawn), piece: piece, start: start, end: end))
} else {
position.enPassant = nil // prevent en passant on next turn
position.enPassantIsPossible = false
}

// castling
Expand Down Expand Up @@ -142,6 +147,11 @@ public struct Board: Sendable {

if abs(start.rank.value - end.rank.value) == 2 {
position.enPassant = EnPassant(pawn: updatedPiece)

if validateEnPassant() {
position.enPassantIsPossible = true
}

}
}

Expand Down Expand Up @@ -198,13 +208,15 @@ public struct Board: Sendable {

/// Determines end game state and
/// handles pawn promotion for provided `move`.
private func process(move: Move) -> Move {
private mutating func process(move: Move) -> Move {
var processedMove = move

// checks & mate
let checkState = self.checkState(for: move.piece.color)
processedMove.checkState = checkState

positionHashCounts[position.hashValue, default: 0] += 1

if checkState == .checkmate {
delegate?.didEnd(with: .win(move.piece.color))
} else if checkState == .stalemate {
Expand All @@ -213,6 +225,8 @@ public struct Board: Sendable {
delegate?.didEnd(with: .draw(.fiftyMoves))
} else if position.hasInsufficientMaterial {
delegate?.didEnd(with: .draw(.insufficientMaterial))
} else if positionHashCounts[position.hashValue] == 3 {
delegate?.didEnd(with: .draw(.repetition))
}

// pawn promotion
Expand Down Expand Up @@ -326,14 +340,33 @@ public struct Board: Sendable {
testSet.add(movedPiece)

if let enPassant = position.enPassant {
if enPassant.canBeCaptured(by: piece) && enPassant.captureSquare == square {
if enPassant.couldBeCaptured(by: piece) && enPassant.captureSquare == square {
testSet.remove(enPassant.pawn)
}
}

return !isKingInCheck(piece.color, set: testSet)
}

/// Determines if there is an actualy possibility to execute
/// the enPassant.
///
private func validateEnPassant() -> Bool {
if let ep = position.enPassant {
let sideSquares = [ep.pawn.square.left, ep.pawn.square.right]

for square in sideSquares {
if let piece = position.piece(at: square),
ep.couldBeCaptured(by: piece),
validate(moveFor: piece, to: ep.captureSquare) {
return true
}
}
}

return false
}

/// Determines the positions of pieces that attack a given square.
///
/// - parameter sq: A bitboard corresponding to the square of interest.
Expand Down Expand Up @@ -409,7 +442,7 @@ public struct Board: Sendable {
if let enPassant = position.enPassant,
let square = Square(sq),
let piece = set.get(square),
enPassant.canBeCaptured(by: piece) {
enPassant.couldBeCaptured(by: piece) {
enPassantMove = enPassant.captureSquare.bb
}

Expand Down
18 changes: 15 additions & 3 deletions Sources/ChessKit/Position.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
//

/// Represents the collection of pieces on the chess board.
public struct Position: Equatable, Sendable {
public struct Position: Equatable, Sendable, Hashable {

/// The pieces currently existing on the board in this position.
public var pieces: [Piece] {
Expand All @@ -25,10 +25,20 @@ public struct Position: Equatable, Sendable {

/// Contains information about a pawn that can be captured by en passant.
///
/// This property is `nil` if there is no pawn capable of being captured by
/// en passant.
/// This property is set whenever a pawn moves by 2 squares.
var enPassant: EnPassant?

/// States the possibility to actually execute the enPassant
var enPassantIsPossible: Bool

/// Conforming Position class to Hashable
public func hash(into hasher: inout Hasher) {
hasher.combine(pieceSet)
hasher.combine(sideToMove)
hasher.combine(legalCastlings)
hasher.combine(enPassantIsPossible)
}

/// Keeps track of the number of moves in a game for the current position.
public private(set) var clock: Clock

Expand All @@ -38,12 +48,14 @@ public struct Position: Equatable, Sendable {
sideToMove: Piece.Color = .white,
legalCastlings: LegalCastlings = LegalCastlings(),
enPassant: EnPassant? = nil,
enPassantIsPossible: Bool = false,
clock: Clock = Clock()
) {
self.pieceSet = .init(pieces: pieces)
self.sideToMove = sideToMove
self.legalCastlings = legalCastlings
self.enPassant = enPassant
self.enPassantIsPossible = enPassantIsPossible
self.clock = clock
}

Expand Down
2 changes: 1 addition & 1 deletion Sources/ChessKit/Special Moves/Castling.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
//

/// Structure that captures legal castling moves.
struct LegalCastlings: Equatable, Sendable {
struct LegalCastlings: Equatable, Hashable, Sendable {

private var legal: [Castling]

Expand Down
6 changes: 3 additions & 3 deletions Sources/ChessKit/Special Moves/EnPassant.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,16 @@ struct EnPassant: Equatable, Hashable, Sendable {
Square(pawn.square.file, pawn.color == .white ? 3 : 6)
}

/// Determines whether or not the pawn can be captured by en passant.
/// Determines whether or not the pawn could be captured by en passant.
///
/// - parameter capturingPiece: The piece that is capturing the contained pawn.
/// - returns: Whether `capturingPiece` can capture `pawn`.
/// - returns: Whether `capturingPiece` could capture `pawn`.
///
/// `capturingPiece` must be an opposite color pawn that is on the
/// same rank as the target pawn and exactly 1 file away from the
/// target pawn for this method to return `true`, otherwise `false`
/// is returned.
func canBeCaptured(by capturingPiece: Piece) -> Bool {
func couldBeCaptured(by capturingPiece: Piece) -> Bool {
capturingPiece.kind == .pawn &&
capturingPiece.color == pawn.color.opposite &&
capturingPiece.square.rank == pawn.square.rank &&
Expand Down
10 changes: 10 additions & 0 deletions Sources/ChessKit/Square.swift
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,16 @@ public enum Square: Int, Equatable, CaseIterable, Sendable {
}
}

/// The Square at the left of the current one.
public var left: Square {
Square(File(file.number - 1), rank)
}

/// The Square at the right of the current one.
public var right: Square {
Square(File(file.number + 1), rank)
}

// MARK: - Squares

case a1, b1, c1, d1, e1, f1, g1, h1
Expand Down
55 changes: 51 additions & 4 deletions Tests/ChessKitTests/BoardTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,8 @@ class BoardTests: XCTestCase {
let ep = board.position.enPassant!

let capturingPiece = board.position.piece(at: .f4)!
XCTAssertTrue(ep.canBeCaptured(by: capturingPiece))

XCTAssertTrue(ep.couldBeCaptured(by: capturingPiece))
let move = board.move(pieceAt: .f4, to: ep.captureSquare)!
XCTAssertEqual(move.result, .capture(ep.pawn))
}
Expand All @@ -61,7 +61,16 @@ class BoardTests: XCTestCase {
XCTAssertFalse(board.canMove(pieceAt: .c5, to: .d6))
}

func testPromotion() {
func testDoubleEnPassant() {
var board = Board(position: .init(fen: "kr6/2p5/8/1P1P4/8/1K6/8/8 b - - 0 1")!)
board.move(pieceAt: .c7, to: .c5)
// after this move only 1 out of 2 pawns can execute enPassant
XCTAssertFalse(board.canMove(pieceAt: .b5, to: .c6))
XCTAssertTrue(board.canMove(pieceAt: .d5, to: .c6))
XCTAssertTrue(board.position.enPassantIsPossible)
}

func testPromotion() {
let pawn = Piece(.pawn, color: .white, square: .e7)
let queen = Piece(.queen, color: .white, square: .e8)
var board = Board(position: .init(pieces: [pawn]))
Expand Down Expand Up @@ -167,7 +176,45 @@ class BoardTests: XCTestCase {
board4.move(pieceAt: .a8, to: .b7)
XCTAssertTrue(board4.position.hasInsufficientMaterial)
}


func testThreefoldRepetition() {

var board = Board(position: .standard)

board.move(pieceAt: .e2, to: .e4)
board.move(pieceAt: .e7, to: .e5) // 1st time position occurs

board.move(pieceAt: .g1, to: .f3)
board.move(pieceAt: .g8, to: .f6)

board.move(pieceAt: .f3, to: .g1)
board.move(pieceAt: .f6, to: .g8) // 2nd time position occurs

board.move(pieceAt: .g1, to: .f3)
board.move(pieceAt: .g8, to: .f6)

board.move(pieceAt: .f3, to: .g1)


nonisolated(unsafe) var expectation: XCTestExpectation? = self.expectation(description: "Board returns draw by repetition result")

let delegate = MockBoardDelegate(didEnd: { result in
if case .draw(let drawType) = result {
if drawType == .repetition {
expectation?.fulfill()
expectation = nil
}
} else {
XCTFail()
}
})

board.delegate = delegate
board.move(pieceAt: .f6, to: .g8) // 3rd time position occurs

waitForExpectations(timeout: 1.0)
}

func testLegalMovesForNonexistentPiece() {
let board = Board(position: .standard)
// no piece at d4
Expand Down
24 changes: 12 additions & 12 deletions Tests/ChessKitTests/SpecialMoveTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -62,22 +62,22 @@ class SpecialMoveTests: XCTestCase {
let blackPawn = Piece(.pawn, color: .black, square: .d5)
let blackEnPassant = EnPassant(pawn: blackPawn)
XCTAssertEqual(blackEnPassant.captureSquare, Square.d6)
XCTAssertTrue(blackEnPassant.canBeCaptured(by: Piece(.pawn, color: .white, square: .e5)))
XCTAssertTrue(blackEnPassant.canBeCaptured(by: Piece(.pawn, color: .white, square: .c5)))
XCTAssertFalse(blackEnPassant.canBeCaptured(by: Piece(.pawn, color: .black, square: .e5)))
XCTAssertFalse(blackEnPassant.canBeCaptured(by: Piece(.pawn, color: .white, square: .f5)))
XCTAssertFalse(blackEnPassant.canBeCaptured(by: Piece(.pawn, color: .white, square: .b5)))
XCTAssertFalse(blackEnPassant.canBeCaptured(by: Piece(.bishop, color: .white, square: .c5)))
XCTAssertTrue(blackEnPassant.couldBeCaptured(by: Piece(.pawn, color: .white, square: .e5)))
XCTAssertTrue(blackEnPassant.couldBeCaptured(by: Piece(.pawn, color: .white, square: .c5)))
XCTAssertFalse(blackEnPassant.couldBeCaptured(by: Piece(.pawn, color: .black, square: .e5)))
XCTAssertFalse(blackEnPassant.couldBeCaptured(by: Piece(.pawn, color: .white, square: .f5)))
XCTAssertFalse(blackEnPassant.couldBeCaptured(by: Piece(.pawn, color: .white, square: .b5)))
XCTAssertFalse(blackEnPassant.couldBeCaptured(by: Piece(.bishop, color: .white, square: .c5)))

let whitePawn = Piece(.pawn, color: .white, square: .d4)
let whiteEnPassant = EnPassant(pawn: whitePawn)
XCTAssertEqual(whiteEnPassant.captureSquare, Square.d3)
XCTAssertTrue(whiteEnPassant.canBeCaptured(by: Piece(.pawn, color: .black, square: .e4)))
XCTAssertTrue(whiteEnPassant.canBeCaptured(by: Piece(.pawn, color: .black, square: .c4)))
XCTAssertFalse(whiteEnPassant.canBeCaptured(by: Piece(.pawn, color: .white, square: .e4)))
XCTAssertFalse(whiteEnPassant.canBeCaptured(by: Piece(.pawn, color: .black, square: .f4)))
XCTAssertFalse(whiteEnPassant.canBeCaptured(by: Piece(.pawn, color: .black, square: .b4)))
XCTAssertFalse(whiteEnPassant.canBeCaptured(by: Piece(.bishop, color: .black, square: .c4)))
XCTAssertTrue(whiteEnPassant.couldBeCaptured(by: Piece(.pawn, color: .black, square: .e4)))
XCTAssertTrue(whiteEnPassant.couldBeCaptured(by: Piece(.pawn, color: .black, square: .c4)))
XCTAssertFalse(whiteEnPassant.couldBeCaptured(by: Piece(.pawn, color: .white, square: .e4)))
XCTAssertFalse(whiteEnPassant.couldBeCaptured(by: Piece(.pawn, color: .black, square: .f4)))
XCTAssertFalse(whiteEnPassant.couldBeCaptured(by: Piece(.pawn, color: .black, square: .b4)))
XCTAssertFalse(whiteEnPassant.couldBeCaptured(by: Piece(.bishop, color: .black, square: .c4)))
}

}

0 comments on commit e3bceb0

Please sign in to comment.