diff --git a/CHANGELOG.md b/CHANGELOG.md index c28d671..451bde6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,13 @@ # [unreleased] +### New Features * `BoardDelegate` now notifies when king is in check, and provides the color of the checked king, see [Issue #38](https://github.com/chesskit-app/chesskit-swift/issues/38). * `Move.checkState` and `Move.disambiguation` are now publicly accessible, see [Issue #38](https://github.com/chesskit-app/chesskit-swift/issues/38). +### Technical Changes +* Enable Swift 6 language mode package-wide. +* `Game` and `MoveTree` are now `Sendable` types. + # ChessKit 0.12.1 Released Wednesday, September 11, 2024. diff --git a/Package.swift b/Package.swift index 63c1b26..d29ffdc 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version: 5.9 +// swift-tools-version: 6.0 import PackageDescription @@ -12,22 +12,10 @@ let package = Package( .watchOS(.v6) ], products: [ - .library( - name: "ChessKit", - targets: ["ChessKit"] - ) + .library(name: "ChessKit", targets: ["ChessKit"]) ], targets: [ - .target( - name: "ChessKit", - dependencies: [], - swiftSettings: [ - .enableExperimentalFeature("StrictConcurrency") - ] - ), - .testTarget( - name: "ChessKitTests", - dependencies: ["ChessKit"] - ) + .target(name: "ChessKit"), + .testTarget(name: "ChessKitTests", dependencies: ["ChessKit"]) ] ) diff --git a/Sources/ChessKit/Board.swift b/Sources/ChessKit/Board.swift index c415306..5faeaa2 100644 --- a/Sources/ChessKit/Board.swift +++ b/Sources/ChessKit/Board.swift @@ -220,8 +220,10 @@ public struct Board: Sendable { let checkState = self.checkState(for: move.piece.color) processedMove.checkState = checkState + // draw by repetition positionHashCounts[position.hashValue, default: 0] += 1 + // board state notification if checkState == .checkmate { delegate?.didEnd(with: .win(move.piece.color)) } else if checkState == .stalemate { diff --git a/Sources/ChessKit/Game.swift b/Sources/ChessKit/Game.swift index 8ef185b..943bdb1 100644 --- a/Sources/ChessKit/Game.swift +++ b/Sources/ChessKit/Game.swift @@ -10,7 +10,7 @@ import Foundation /// This object is the entry point for interacting with a full /// chess game within `ChessKit`. It provides methods for /// making moves and publishes the played moves in an observable way. -public struct Game: Hashable { +public struct Game: Hashable, Sendable { // MARK: - Properties diff --git a/Sources/ChessKit/MoveTree/MoveTree+Index.swift b/Sources/ChessKit/MoveTree/MoveTree+Index.swift index e117f31..084a8f2 100644 --- a/Sources/ChessKit/MoveTree/MoveTree+Index.swift +++ b/Sources/ChessKit/MoveTree/MoveTree+Index.swift @@ -7,6 +7,7 @@ extension MoveTree { /// Object that represents the index of a node in the move tree. public struct Index: Hashable, Sendable { + /// Variation number corresponding to the main variation of the tree. public static let mainVariation = 0 /// The move number. @@ -86,6 +87,7 @@ extension MoveTree { } +// MARK: - Comparable extension MoveTree.Index: Comparable { public static func < (lhs: Self, rhs: Self) -> Bool { if lhs.variation == rhs.variation { diff --git a/Sources/ChessKit/MoveTree/MoveTree.swift b/Sources/ChessKit/MoveTree/MoveTree.swift index 1304b69..9eda050 100644 --- a/Sources/ChessKit/MoveTree/MoveTree.swift +++ b/Sources/ChessKit/MoveTree/MoveTree.swift @@ -3,11 +3,13 @@ // ChessKit // +import Foundation + /// A tree-like data structure that represents the moves of a chess game. /// /// The tree maintains the move order including variations and /// provides index-based access for any element in the tree. -public struct MoveTree: Hashable { +public struct MoveTree: Hashable, Sendable { /// The index of the root of the move tree. /// @@ -27,6 +29,10 @@ public struct MoveTree: Hashable { Array(dictionary.keys) } + /// Lock to restrict modification of tree nodes + /// to ensure `Sendable` conformance for ``Node``. + private static let nodeLock = NSLock() + /// Adds a move to the move tree. /// /// - parameter move: The move to add to the tree. @@ -67,7 +73,9 @@ public struct MoveTree: Hashable { } } - dictionary[newIndex] = newNode + Self.nodeLock.withLock { + dictionary[newIndex] = newNode + } newNode.index = newIndex if newIndex.variation == Index.mainVariation { @@ -257,8 +265,10 @@ public struct MoveTree: Hashable { assessment: Move.Assessment = .null, comment: String = "" ) -> Move? { - dictionary[index]?.move.assessment = assessment - dictionary[index]?.move.comment = comment + Self.nodeLock.withLock { + dictionary[index]?.move.assessment = assessment + dictionary[index]?.move.comment = comment + } return dictionary[index]?.move } @@ -343,7 +353,7 @@ extension MoveTree: Equatable { extension MoveTree { /// Object that represents a node in the move tree. - class Node: Hashable { + class Node: Hashable, @unchecked Sendable { /// The move for this node. var move: Move @@ -365,7 +375,7 @@ extension MoveTree { lhs.index == rhs.index && lhs.move == rhs.move } - // MARK: - Hashable + // MARK: Hashable func hash(into hasher: inout Hasher) { hasher.combine(move) hasher.combine(index) diff --git a/Tests/ChessKitTests/BoardTests.swift b/Tests/ChessKitTests/BoardTests.swift index 03d638d..11666b8 100644 --- a/Tests/ChessKitTests/BoardTests.swift +++ b/Tests/ChessKitTests/BoardTests.swift @@ -34,7 +34,7 @@ final class BoardTests: XCTestCase { XCTAssertTrue(board.position.enPassantIsPossible) } - func testWhitePromotion() { + @MainActor func testWhitePromotion() { let pawn = Piece(.pawn, color: .white, square: .e7) let queen = Piece(.queen, color: .white, square: .e8) var board = Board(position: .init(pieces: [pawn])) @@ -58,7 +58,7 @@ final class BoardTests: XCTestCase { XCTAssertEqual(promotionMove.end, .e8) } - func testBlackPromotion() { + @MainActor func testBlackPromotion() { let pawn = Piece(.pawn, color: .black, square: .e2) let queen = Piece(.queen, color: .black, square: .e1) var board = Board(position: .init(pieces: [pawn])) @@ -82,7 +82,7 @@ final class BoardTests: XCTestCase { XCTAssertEqual(promotionMove.end, .e1) } - func testFiftyMoveRule() { + @MainActor func testFiftyMoveRule() { var board = Board(position: .fiftyMove) nonisolated(unsafe) var expectation: XCTestExpectation? = self.expectation(description: "Board returns fifty move draw result") @@ -102,7 +102,7 @@ final class BoardTests: XCTestCase { waitForExpectations(timeout: 1.0) } - func testInsufficientMaterial() { + @MainActor func testInsufficientMaterial() { var board = Board(position: .init(fen: "k7/b6P/8/8/8/8/8/K7 w - - 0 1")!) nonisolated(unsafe) var expectation: XCTestExpectation? = self.expectation(description: "Board returns insufficient material draw result") @@ -165,7 +165,7 @@ final class BoardTests: XCTestCase { XCTAssertTrue(board4.position.hasInsufficientMaterial) } - func testThreefoldRepetition() { + @MainActor func testThreefoldRepetition() { var board = Board(position: .standard) board.move(pieceAt: .e2, to: .e4) @@ -350,7 +350,7 @@ final class BoardTests: XCTestCase { XCTAssertNil(move) } - func testCheckMove() { + @MainActor func testCheckMove() { var board = Board(position: .init(fen: "k7/7R/8/8/8/8/K7/8 w - - 0 1")!) nonisolated(unsafe) var expectation: XCTestExpectation? = self.expectation(description: "Board returns check result") @@ -371,7 +371,7 @@ final class BoardTests: XCTestCase { waitForExpectations(timeout: 1.0) } - func testCheckmateMove() { + @MainActor func testCheckmateMove() { var board = Board(position: .init(fen: "k7/7R/6R1/8/8/8/K7/8 w - - 0 1")!) nonisolated(unsafe) var expectation: XCTestExpectation? = self.expectation(description: "Board returns checkmate result") diff --git a/Tests/ChessKitTests/MoveTreeTests.swift b/Tests/ChessKitTests/MoveTreeTests.swift index 52cba2c..965a27f 100644 --- a/Tests/ChessKitTests/MoveTreeTests.swift +++ b/Tests/ChessKitTests/MoveTreeTests.swift @@ -27,6 +27,13 @@ final class MoveTreeTests: XCTestCase { XCTAssertEqual(moveTree[.minimum.next], e4) } + func testNodeHashValue() { + var moveTree = MoveTree() + let e4 = Move(san: "e4", position: .standard) + moveTree[.minimum.next] = e4 + XCTAssertNotNil(moveTree.dictionary[.minimum.next]?.hashValue) + } + func testSameVariationComparability() { let wIndex = MoveTree.Index(number: 4, color: .white, variation: 2) XCTAssertLessThan(wIndex, wIndex.next)