├── .codecov.yaml ├── .git-blame-ignore-revs ├── .github └── workflows │ └── checks.yaml ├── .gitignore ├── .periphery.yaml ├── .swift-format ├── CHANGELOG.md ├── LICENSE ├── Package.swift ├── README.md ├── Sources └── ChessKit │ ├── Bitboards │ ├── Attacks.swift │ ├── Bitboard.swift │ ├── PieceSet.swift │ └── Square+BB.swift │ ├── Board.swift │ ├── Clock.swift │ ├── Configuration.swift │ ├── Game.swift │ ├── Move.swift │ ├── MoveTree │ ├── MoveTree+Collection.swift │ ├── MoveTree+Deprecated.swift │ ├── MoveTree+Index.swift │ └── MoveTree.swift │ ├── Parsers │ ├── EngineLANParser+Regex.swift │ ├── EngineLANParser.swift │ ├── FENParser.swift │ ├── PGNParser │ │ ├── PGNParser+Deprecated.swift │ │ ├── PGNParser+MoveText.swift │ │ ├── PGNParser+Tags.swift │ │ └── PGNParser.swift │ ├── SANParser+Regex.swift │ └── SANParser.swift │ ├── Piece.swift │ ├── Position.swift │ ├── Special Moves │ ├── Castling.swift │ └── EnPassant.swift │ ├── Square.swift │ └── Utilities │ ├── Comparable+Bounded.swift │ └── Stack.swift └── Tests └── ChessKitTests ├── BoardTests.swift ├── CastlingTests.swift ├── GameTests.swift ├── MoveTests.swift ├── MoveTreeTests.swift ├── Parsers ├── EngineLANParserTests.swift ├── FENParserTests.swift ├── PGNParserTests.swift └── SANParserTests.swift ├── Performance ├── BoardPerformanceTests.swift └── PGNParserPerformanceTests.swift ├── PieceTests.swift ├── PositionTests.swift ├── SpecialMoveTests.swift ├── SquareTests.swift └── Utilities ├── MockBoardDelegate.swift ├── SampleGames.swift └── SamplePositions.swift /.codecov.yaml: -------------------------------------------------------------------------------- 1 | comment: false 2 | ignore: 3 | - "Tests" 4 | -------------------------------------------------------------------------------- /.git-blame-ignore-revs: -------------------------------------------------------------------------------- 1 | # List of commits to ignore on git-blame. 2 | # 3 | # To activate run `git config blame.ignoreRevsFile .git-blame-ignore-revs` 4 | 5 | # swift format 6 | 873661ac5bc64f0194a4a0addbc40c2d83d01b50 7 | -------------------------------------------------------------------------------- /.github/workflows/checks.yaml: -------------------------------------------------------------------------------- 1 | 2 | name: checks 3 | 4 | on: 5 | push: 6 | branches: master 7 | pull_request: 8 | branches: master 9 | 10 | jobs: 11 | check: 12 | uses: chesskit-app/workflows/.github/workflows/check-swift-package.yaml@master 13 | secrets: inherit 14 | with: 15 | test_bundle: ChessKitPackageTests 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | # SPM 4 | .swiftpm 5 | .build 6 | Packages/ 7 | 8 | # Xcode 9 | /*.xcodeproj 10 | xcuserdata/ 11 | /*.playground/* 12 | -------------------------------------------------------------------------------- /.periphery.yaml: -------------------------------------------------------------------------------- 1 | retain_public: true 2 | exclude_tests: true 3 | -------------------------------------------------------------------------------- /.swift-format: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "indentation": { 4 | "spaces": 2 5 | }, 6 | "indentConditionalCompilationBlocks": false, 7 | "lineBreakBetweenDeclarationAttributes": false, 8 | "lineLength": 200, 9 | "multiElementCollectionTrailingCommas": false, 10 | "respectsExistingLineBreaks": true, 11 | "rules": { 12 | "NoAccessLevelOnExtensionDeclaration": false, 13 | "UseWhereClausesInForLoops": true 14 | } 15 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # [unreleased] 2 | 3 | ### New Features 4 | * `Position` now includes an `assessment` property for positional assessments. 5 | * Possible values for `assessment` are part of the new `Position.Assessment` enum. 6 | * These are based on the standardized [Numerical Annotation Glyphs](https://en.wikipedia.org/wiki/Portable_Game_Notation#Numeric_Annotation_Glyphs) (also used for `Move.Assessment`). 7 | * `Game` and `MoveTree` have methods to update positional assessments. 8 | 9 | ### Improvements 10 | * Rewrote `PGNParser` to be more efficient and reliable. 11 | * Now parses PGN text more flexibly, accounting nested variations. 12 | * `PGNParser.parse(game:)` has replaced the now deprecated `PGNParser.parse(game:startingWith:)` (position is now inferred from the PGN tags). 13 | * `parse(game:)` is now a throwing method with a robust bank of possible errors for better error handling and understanding of why the PGN could not be parsed. 14 | * `Game(pgn:)` is now also a throwing initializer, passing on the error from `PGNParser`. 15 | * Now parses `Position.Assessment` (see above). 16 | * Add `Move.Assessment(notation:)` initializer. 17 | * Add `all` property to `Game.Tags` to return array of all named tags. 18 | * Add `CustomStringConvertible` conformance to `Game` and `Move`. 19 | 20 | ### Bug Fixes 21 | * Fix `SANParser` not parsing en passant captures properly (by Rob Raese). 22 | * Fix parsing of `"PlyCount"`, `"TimeControl"`, and `"SetUp"` PGN tags. 23 | 24 | # ChessKit 0.14.0 25 | Released Tuesday, June 3, 2025. 26 | 27 | ### New Features 28 | * Add `willPromote()` to `BoardDelegate` (by [@Amir-Zucker](https://github.com/Amir-Zucker)). 29 | * Called when pawn reaches last rank but before `Board.completePromotion()` is called. 30 | 31 | ### Improvements 32 | * `PGNParser` now parses `*` game results (indicating game in progress, abandoned, unknown, etc.). 33 | * Add `CustomStringConvertible` conformance to `Piece`, `Piece.Color`, and `Piece.Kind`. 34 | * Migrate all unit tests (expect performance tests) from `XCTest` to `Swift Testing`. 35 | 36 | ### Bug Fixes 37 | * Fix issue where `MoveTree.endIndex` is not properly updated after the first half move (by [@Amir-Zucker](https://github.com/Amir-Zucker)). 38 | * Fix issue where `BoardDelegate.didPromote()` was called before and after promotion (by [@Amir-Zucker](https://github.com/Amir-Zucker)). 39 | * Fix issue where pawns on the starting rank could hop over pieces in front of them, see [Issue #49](https://github.com/chesskit-app/chesskit-swift/issues/49) (by [@joee-ca](https://github.com/joee-ca)). 40 | * Fix `SANParser` (and `PGNParser`) accepting invalid SANs when they started with a valid SAN (e.g. `"e44"` which starts with the valid `"e4"`). 41 | 42 | ### Breaking Changes 43 | * `Game(pgn:)` is no longer a failable initializer, it will now always create a non-`nil` `Game`. 44 | 45 | # ChessKit 0.13.0 46 | Released Thursday, October 3, 2024. 47 | 48 | ### New Features 49 | * `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). 50 | * `Move.checkState` and `Move.disambiguation` are now publicly accessible, see [Issue #38](https://github.com/chesskit-app/chesskit-swift/issues/38). 51 | 52 | ### Technical Changes 53 | * Enable Swift 6 language mode package-wide. 54 | * `Game` and `MoveTree` are now `Sendable` types. 55 | 56 | # ChessKit 0.12.1 57 | Released Wednesday, September 11, 2024. 58 | 59 | ### Bug Fixes 60 | * Fix `MoveTree.fullVariation(for:)` returning blank array when `.minimum` is passed as the index. 61 | * If this index is passed, it will automatically start from the next (valid) index and return the moves for that index. 62 | * In practice this will mean the main variation will be returned (starting from white's first move). 63 | 64 | # ChessKit 0.12.0 65 | Released Wednesday, August 21, 2024. 66 | 67 | ### Improvements 68 | * Conform more types to `Hashable` such as `Game` and `MoveTree`. 69 | * `Game.Tag` now publicly exposes `name`. 70 | * `Game.Tags` is now `Hashable` and `Sendable`. 71 | 72 | # ChessKit 0.11.0 73 | Released Monday, August 5, 2024. 74 | 75 | ### New Features 76 | * A draw result is now published by `BoardDelegate` when the board encounters a threefold repetition (by [@joee-ca](https://github.com/joee-ca)). 77 | * Convenience directional properties added to `Square` such as `up`, `down`, `left`, and `right` to obtain squares in relation to the given `Square`. 78 | 79 | ### Bug Fixes 80 | * `File.init(_ number: Int)` now correctly bounds invalid values. 81 | * i.e. Values less than 1 become `File.a` and values greater than 8 become `File.h`. 82 | 83 | ### Technical Changes 84 | * Test coverage has been improved. 85 | * Parsers (`EngineLANParser`, `FENParser`, `PGNParser`, `SANParser`) have been converted from classes to caseless enums. 86 | * This should have no effect on existing code since the class versions had private initializers. 87 | 88 | # ChessKit 0.10.0 89 | Released Friday, June 21, 2024. 90 | 91 | ### Improvements 92 | * Update tools version to Swift 5.9 (requires Xcode 15.0 or greater). 93 | * Conform to Swift strict concurrency and add `Sendable` conformance to most objects 94 | 95 | ### Breaking Changes 96 | * `Game` is now a `struct` and no longer conforms to `ObservableObject`. 97 | * If observation semantics are required, consider using `didSet` property observers or an object that utilizes the `@Observable` macro. 98 | 99 | # ChessKit 0.9.0 100 | Released Saturday, June 15, 2024. 101 | 102 | ### Improvements 103 | * `MoveTree` now conforms to `BidirectionalCollection`, allowing for more standard collection-based semantics in Swift. 104 | * Should not affect any existing functionality or API usage. 105 | * Several methods on `MoveTree` have been deprecated in favor of their `Collection` counterparts: 106 | * `previousIndex(for:)` → `index(before:)` / `hasIndex(before:)` 107 | * `nextIndex(for:)` → `index(after:)` / `hasIndex(after:)` 108 | * `move(at:)` → `subscript(_:)` (e.g. `tree[index]`) 109 | * `MoveTree.annotate()` now optionally returns the `Move` object after annotation. 110 | * `MoveTree.path()` now returns tuple with named parameters (`direction` and `index`). 111 | 112 | ### Bug Fixes 113 | * Removed `CustomDebugStringConvertible` conformance from `Bitboard` to avoid affecting all `UInt64` debug prints. 114 | * To print the string representation of `Bitboard` use `Bitboard.chessString()`. 115 | 116 | # ChessKit 0.8.0 117 | Released Friday, June 7, 2024. 118 | 119 | ### Improvements 120 | * Add support for draw by insufficient material (by [@joee-ca](https://github.com/joee-ca)). 121 | * Once this condition is reached `.draw(.insufficientMaterial)` will be published via the `BoardDelegate.didEnd(with:)` method. 122 | * Add unicode variant selector when printing black pawn icon to avoid displaying emoji (by [@joee-ca](https://github.com/joee-ca)). 123 | 124 | ### Bug Fixes 125 | * Fix issue where king could castle through other pieces (by [@TigranSaakyan](https://github.com/TigranSaakyan)). 126 | 127 | # ChessKit 0.7.1 128 | Released Monday, May 6, 2024. 129 | 130 | * Fix `MoveTree.previousIndex(for:)` when provided index is one after `minimumIndex`. 131 | 132 | # ChessKit 0.7.0 133 | Released Monday, April 29, 2024. 134 | 135 | ### Improvements 136 | * Add `startingIndex` and `startingPosition` to `Game`. 137 | * `startingIndex` takes into account the `sideToMove` of `startingPosition`. 138 | 139 | ### Bug Fixes 140 | * Fix rare en passant issue that could allow the king to be left in check, see [Issue #18](https://github.com/chesskit-app/chesskit-swift/issues/18). 141 | 142 | # ChessKit 0.6.0 143 | Released Friday, April 19, 2024. 144 | 145 | ### Improvements 146 | * Enable `chesskit-swift` to run on oldest platform possible without code changes. 147 | * Now works on iOS 13+, macOS 10.15+, tvOS 13+, watchOS 6+. 148 | * Annotations on moves in the `MoveTree` can now also be updated via `Game.annotate(moveAt:assessment:comment:)`. 149 | 150 | ### Bug Fixes 151 | * Fix `MoveTree` not properly publishing changes via `Game`. 152 | * Fix `Board.EndResult.repetition` spelling. 153 | * This isn't made available yet but will be implemented in an upcoming release. 154 | 155 | # ChessKit 0.5.0 156 | Released Sunday, April 14, 2024. 157 | 158 | ### Improvements 159 | * 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). 160 | 161 | ### Bug Fixes 162 | * Fix issue where king is allowed to castle in check, see [Issue #11](https://github.com/chesskit-app/chesskit-swift/issues/11). 163 | 164 | ### Breaking Changes 165 | * Remove `color` parameter from `Move.init(san:color:position:)` initializer. 166 | * It was not being used, can be removed from any initializer call where it was included. 167 | * The new initializer is simply `Move.init(san:position:)`. 168 | 169 | # ChessKit 0.4.0 170 | Released Saturday, April 13, 2024. 171 | 172 | ### Improvements 173 | * `Board` move calculation and validation performance has greatly increased. 174 | * Performance has improved by over 250x when simulating a full game using `Board`. 175 | * Underlying board representation has been replaced with much faster bitboard structures and algorithms. 176 | * Add `CustomStringConvertible` conformance to `Board` and `Position` to allow for printing chess board representations, useful for debugging. 177 | * Add `ChessKitConfiguration` with static configuration properties for the package. 178 | * Currently the only option is `printMode` to determine how pieces should be represented when printing `Board` and `Position` objects (see previous item). 179 | 180 | ### Breaking Changes 181 | * `EnPassant` has been made an `internal struct`. It is used interally by `Position` and `Board`. 182 | 183 | ### Deprecations 184 | * `Position.toggleSideToMove()` is now private and handled automatically when calling `move()`. The public-facing `toggleSideToMove()` has been deprecated. 185 | 186 | # ChessKit 0.3.2 187 | Released Saturday, December 2, 2023. 188 | 189 | ### Fixes 190 | * Made `file` and `rank` public properties of `Square`. 191 | 192 | # ChessKit 0.3.1 193 | Released Friday, November 24, 2023. 194 | 195 | ### Improvements 196 | * Add `CaseIterable` conformance to several `Piece` and `Square` enums: 197 | * `Piece.Color` 198 | * `Piece.Kind` 199 | * `Square.Color` 200 | 201 | # ChessKit 0.3.0 202 | Released Wednesday, June 21, 2023. 203 | 204 | ### New Features 205 | * Add `future(for:)` and `fullVariation(for:)` methods to `MoveTree`. 206 | * `future(for:)` returns the future moves for a given 207 | index. 208 | * `fullVariation(for:)` returns the sum of `history(for:)` and `future(for:)`. 209 | 210 | ### Improvements 211 | * Simplify `PGNElement` to just contain a single `.move` case. 212 | * i.e. `.whiteMove` and `blackMove` have been removed and consolidated. 213 | 214 | ### Fixes 215 | * Fix behavior of `previousIndex(for:)` and `nextIndex(for:)` in `MoveTree`. 216 | * Especially when the provided `index` is equal to `.minimum`. 217 | 218 | # ChessKit 0.2.0 219 | Released Wednesday, May 31, 2023. 220 | 221 | ### New Features 222 | * `MoveTree` and `MoveTree.Index` objects to track move turns and variations. 223 | * `Game.moves` is now a `MoveTree` object instead of `[Int: MovePair]` 224 | * `MoveTree.Index` includes piece color and variation so it can be used to directly identify any single move within a game 225 | * Use the properties and functions of `MoveTree` to retrieve moves within the tree as needed 226 | 227 | * `make(move:index:)` and `make(moves:index:)` with ability to make moves on `Game` with SAN strings for convenience 228 | * For example: `game.make(moves: ["e4", "e5"])` 229 | 230 | * `PGNParser.convert(game:)` now returns the PGN string for a given game, including variations. 231 | * Note: `PGNParser.parse(pgn:)` still does not work with variations, this is coming in a future update. 232 | 233 | * `Game.positions` is now public 234 | * Contains a dictionary of all positions in the game by `MoveTree.Index`, including variations 235 | 236 | ### Removed 237 | * `Game.annotateMove` 238 | * Modify `Move.assessment` and `Move.comment` directly instead 239 | * `MovePair` 240 | * Use `Move` in conjuction with `MoveTree.Index` to track move indicies 241 | * `color` parameter from `SANParser.parse()` 242 | * The color is now obtained from the `sideToMove` in the provided `position` 243 | 244 | # ChessKit 0.1.2 245 | Released Thursday, May 11, 2023. 246 | 247 | * Add documentation for all public members 248 | * Add default starting position for `Game` initializer 249 | * Add ability to annotate moves via `Game` 250 | 251 | # ChessKit 0.1.1 252 | Released Wednesday, April 12, 2023. 253 | 254 | * Downgrade required Swift version to 5.7 255 | * Allows use with Xcode 14.2 on GitHub Actions 256 | 257 | # ChessKit 0.1.0 258 | Released Tuesday, April 11, 2023. 259 | 260 | * Initial release 261 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 ChessKit (https://github.com/chesskit-app) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 6.0 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "ChessKit", 7 | platforms: [ 8 | .iOS(.v12), 9 | .macCatalyst(.v13), 10 | .macOS(.v10_13), 11 | .tvOS(.v12), 12 | .watchOS(.v4), 13 | .visionOS(.v1) 14 | ], 15 | products: [ 16 | .library(name: "ChessKit", targets: ["ChessKit"]) 17 | ], 18 | targets: [ 19 | .target(name: "ChessKit"), 20 | .testTarget(name: "ChessKitTests", dependencies: ["ChessKit"]) 21 | ] 22 | ) 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ♟️ ChessKit 2 | 3 | [![checks](https://github.com/chesskit-app/chesskit-swift/actions/workflows/checks.yaml/badge.svg)](https://github.com/chesskit-app/chesskit-swift/actions/workflows/checks.yaml) [![codecov](https://codecov.io/gh/chesskit-app/chesskit-swift/branch/master/graph/badge.svg?token=676EP0N8XF)](https://codecov.io/gh/chesskit-app/chesskit-swift) 4 | 5 | A Swift package for efficiently implementing chess logic. 6 | 7 | For a related Swift package that contains chess engines such as [Stockfish](https://stockfishchess.org), see [chesskit-engine](https://github.com/chesskit-app/chesskit-engine). 8 | 9 | ## Usage 10 | 11 | 1. Add `chesskit-swift` as a dependency 12 | * In an [app built in Xcode](https://developer.apple.com/documentation/xcode/adding-package-dependencies-to-your-app), 13 | * or [as a dependency to another Swift Package](https://www.swift.org/documentation/package-manager/#importing-dependencies). 14 | 15 | 2. Next, import `ChessKit` to use it in Swift code: 16 | ``` swift 17 | import ChessKit 18 | 19 | // ... 20 | 21 | ``` 22 | 23 | ## Features 24 | 25 | * Representation of chess elements 26 | * `Piece`: represents a single piece on the board, given by its color, type, and location on the board 27 | * `Square`: represents a square on the board 28 | * `Move`: represents a piece move, along with other metadata such as captures, annotations, and disambiguations 29 | * `Position`: represents a single position on the chess board 30 | * `Board`: validate and make moves in accordance with chess rules 31 | * `Game`: manage positions throughout a game and PGN tags 32 | * Special moves (castling, en passant): handled automatically by `Position` and `Board` 33 | * Move validation 34 | * Implemented using highly performant `UInt64` [bitboards](https://www.chessprogramming.org/Bitboards). 35 | * Move branching and variations 36 | * Implemented using a performant tree-like data structure `MoveTree`. 37 | * Pawn promotion handling 38 | * Game states (check, stalemate, checkmate, draws) 39 | * Chess notation string parsing 40 | * `PGNParser` 41 | * `FENParser` 42 | * `SANParser` 43 | * `EngineLANParser` (for use with [UCI](https://www.wbec-ridderkerk.nl/html/UCIProtocol.html) engines) 44 | 45 | ## Examples 46 | 47 | * Create a board with the standard starting position: 48 | ``` swift 49 | let board = Board() 50 | ``` 51 | 52 | * Create a board with a custom starting position using [FEN](https://en.wikipedia.org/wiki/Forsyth–Edwards_Notation): 53 | ``` swift 54 | if let position = Position(fen: "rnbqkbnr/pp1ppppp/8/2p5/4P3/5N2/PPPP1PPP/RNBQKB1R b KQkq - 1 2") { 55 | let board = Board(position: position) 56 | print(board) 57 | } 58 | 59 | // 8 ♜ ♞ ♝ ♛ ♚ ♝ ♞ ♜ 60 | // 7 ♟ ♟ · ♟ ♟ ♟ ♟ ♟ 61 | // 6 · · · · · · · · 62 | // 5 · · ♟ · · · · · 63 | // 4 · · · · ♙ · · · 64 | // 3 · · · · · ♘ · · 65 | // 2 ♙ ♙ ♙ ♙ · ♙ ♙ ♙ 66 | // 1 ♖ ♘ ♗ ♕ ♔ ♗ · ♖ 67 | // a b c d e f g h 68 | // 69 | // (see `ChessKitConfiguration` for printing options) 70 | ``` 71 | 72 | * Move pieces on the board 73 | ``` swift 74 | let board = Board() 75 | // 8 ♜ ♞ ♝ ♛ ♚ ♝ ♞ ♜ 76 | // 7 ♟ ♟ ♟ ♟ ♟ ♟ ♟ ♟ 77 | // 6 · · · · · · · · 78 | // 5 · · · · · · · · 79 | // 4 · · · · · · · · 80 | // 3 · · · · · · · · 81 | // 2 ♙ ♙ ♙ ♙ ♙ ♙ ♙ ♙ 82 | // 1 ♖ ♘ ♗ ♕ ♔ ♗ ♘ ♖ 83 | // a b c d e f g h 84 | 85 | // move pawn at e2 to e4 86 | board.move(pieceAt: .e2, to: .e4) 87 | // 8 ♜ ♞ ♝ ♛ ♚ ♝ ♞ ♜ 88 | // 7 ♟ ♟ ♟ ♟ ♟ ♟ ♟ ♟ 89 | // 6 · · · · · · · · 90 | // 5 · · · · · · · · 91 | // 4 · · · · ♙ · · · 92 | // 3 · · · · · · · · 93 | // 2 ♙ ♙ ♙ ♙ · ♙ ♙ ♙ 94 | // 1 ♖ ♘ ♗ ♕ ♔ ♗ ♘ ♖ 95 | // a b c d e f g h 96 | ``` 97 | 98 | * Check move legality 99 | ``` swift 100 | let board = Board() 101 | print(board.canMove(pieceAt: .a1, to: .a8)) // false 102 | ``` 103 | 104 | * Check legal moves 105 | ``` swift 106 | let board = Board() 107 | print(board.legalMoves(forPieceAt: .e2)) // [.e3, .e4] 108 | ``` 109 | 110 | * Parse [FEN](https://en.wikipedia.org/wiki/Forsyth–Edwards_Notation) into a `Position` object, [PGN](https://en.wikipedia.org/wiki/Portable_Game_Notation) (into `Game`), or [SAN](https://en.wikipedia.org/wiki/Algebraic_notation_(chess)) (into `Move`). 111 | ``` swift 112 | // parse FEN using Position initializer 113 | let fen = "rnbqkbnr/pp1ppppp/8/2p5/4P3/5N2/PPPP1PPP/RNBQKB1R b KQkq - 1 2" 114 | let position = Position(fen: fen) 115 | 116 | // convert Position to FEN string 117 | let fenString = position.fen 118 | 119 | // parse PGN using Game initializer 120 | let game = Game(pgn: "1. e4 e5 2. Nf3") 121 | 122 | // convert Game to PGN string 123 | let pgnString = game.pgn 124 | 125 | // parse the move text "e4" from the starting position 126 | let move = Move(san: "e4", in: .standard) 127 | 128 | // convert Move to SAN string 129 | let sanString = move.san 130 | ``` 131 | 132 | ## License 133 | 134 | `ChessKit` is distributed under the [MIT License](https://github.com/chesskit-app/chesskit-swift/blob/master/LICENSE). 135 | -------------------------------------------------------------------------------- /Sources/ChessKit/Bitboards/Attacks.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Attacks.swift 3 | // ChessKit 4 | // 5 | 6 | import Foundation 7 | 8 | /// Stores pre-generated pseudo-legal attack bitboards 9 | /// for non-pawn piece types. 10 | struct Attacks: Sendable { 11 | 12 | /// Cached king attacks, the dictionary key 13 | /// corresponds to `Square.bb`. 14 | private(set) nonisolated(unsafe) static var kings = [Bitboard: Bitboard]() 15 | /// Cached rook attacks, the array index corresponds 16 | /// to `Square.rawValue`. 17 | private(set) nonisolated(unsafe) static var rooks = [Magic]() 18 | /// Cached bishop attacks, the array index corresponds 19 | /// to `Square.rawValue`. 20 | private(set) nonisolated(unsafe) static var bishops = [Magic]() 21 | /// Cached king attacks, the dictionary key 22 | /// corresponds to `Square.bb`. 23 | private(set) nonisolated(unsafe) static var knights = [Bitboard: Bitboard]() 24 | 25 | /// Lock to restrict modification of cached attacks 26 | /// to ensure `Sendable` conformance. 27 | private static let lock = NSLock() 28 | 29 | /// Generates and caches attack bitboards for all piece kinds. 30 | static func create() { 31 | Piece.Kind.allCases.forEach(create) 32 | } 33 | 34 | private static func create(for kind: Piece.Kind) { 35 | lock.withLock { 36 | switch kind { 37 | case .king: 38 | createKingAttacks() 39 | case .queen: 40 | break // uses (rooks | bishops) 41 | case .rook: 42 | createMagics(for: .rook) 43 | case .bishop: 44 | createMagics(for: .bishop) 45 | case .knight: 46 | createKnightAttacks() 47 | case .pawn: 48 | break 49 | } 50 | } 51 | } 52 | 53 | private static func createKingAttacks() { 54 | // ensure attacks are only initialized once 55 | guard kings.isEmpty else { return } 56 | 57 | /// King attack bit shifts 58 | /// ``` 59 | /// +---+---+---+---+---+---+---+---+ 60 | /// 8 | | | | | | | | | 61 | /// +---+---+---+---+---+---+---+---+ 62 | /// 7 | | | | | | | | | 63 | /// +---+---+---+---+---+---+---+---+ 64 | /// 6 | | | | | | | | | 65 | /// +---+---+---+---+---+---+---+---+ 66 | /// 5 | | | +7| +8| +9| | | | 67 | /// +---+---+---+---+---+---+---+---+ 68 | /// 4 | | | -1| 0| +1| | | | 69 | /// +---+---+---+---+---+---+---+---+ 70 | /// 3 | | | -7| -8| -9| | | | 71 | /// +---+---+---+---+---+---+---+---+ 72 | /// 2 | | | | | | | | | 73 | /// +---+---+---+---+---+---+---+---+ 74 | /// 1 | | | | | | | | | 75 | /// +---+---+---+---+---+---+---+---+ 76 | /// a b c d e f g h 77 | /// ``` 78 | Square.allCases.forEach { square in 79 | let sq = square.bb 80 | 81 | var attacks = sq.east() | sq.west() 82 | let horizontal = sq | attacks 83 | attacks |= horizontal.north() | horizontal.south() 84 | 85 | kings[sq] = attacks 86 | } 87 | } 88 | 89 | private static func createKnightAttacks() { 90 | // ensure attacks are only initialized once 91 | guard knights.isEmpty else { return } 92 | 93 | /// Knight attack bit shifts 94 | /// ``` 95 | /// +---+---+---+---+---+---+---+---+ 96 | /// 8 | | | | | | | | | 97 | /// +---+---+---+---+---+---+---+---+ 98 | /// 7 | | | | | | | | | 99 | /// +---+---+---+---+---+---+---+---+ 100 | /// 6 | | |+15| |+17| | | | 101 | /// +---+---+---+---+---+---+---+---+ 102 | /// 5 | | +6| | | |+10| | | 103 | /// +---+---+---+---+---+---+---+---+ 104 | /// 4 | | | | 0 | | | | | 105 | /// +---+---+---+---+---+---+---+---+ 106 | /// 3 | |-10| | | | -6| | | 107 | /// +---+---+---+---+---+---+---+---+ 108 | /// 2 | | |-17| |-15| | | | 109 | /// +---+---+---+---+---+---+---+---+ 110 | /// 1 | | | | | | | | | 111 | /// +---+---+---+---+---+---+---+---+ 112 | /// a b c d e f g h 113 | /// ``` 114 | Square.allCases.forEach { square in 115 | let sq = square.bb 116 | knights[sq] = [17, 15, 10, 6].reduce(Bitboard(0)) { 117 | var result = $0 118 | 119 | let up = sq << $1 120 | if distance(sq, up) <= 2 { 121 | result |= up 122 | } 123 | 124 | let down = sq >> $1 125 | if distance(sq, down) <= 2 { 126 | result |= down 127 | } 128 | 129 | return result 130 | } 131 | } 132 | } 133 | 134 | /// Computes the Chebyshev Distance between two bitboard squares. 135 | private static func distance(_ sq1: Bitboard, _ sq2: Bitboard) -> Int { 136 | guard let s1 = Square(sq1), let s2 = Square(sq2) else { 137 | return .max 138 | } 139 | 140 | return max( 141 | abs(s1.file.number - s2.file.number), 142 | abs(s1.rank.value - s2.rank.value) 143 | ) 144 | } 145 | 146 | // MARK: Sliding Attacks 147 | 148 | /// Generates array containing a `Magic` object for each 149 | /// square on the chess board. 150 | /// 151 | /// Uses a similar techique as Stockfish (see [`Stockfish/init_magics`](https://github.com/official-stockfish/Stockfish/blob/0716b845fdef8a20102b07eaec074b8da8162523/src/bitboard.cpp#L139)) except with hardcoded magics rather than 152 | /// seeded random generation. 153 | private static func createMagics(for kind: SlidingPieceKind) { 154 | guard let magicNumbers = magicNumbers[kind] else { return } 155 | 156 | // ensure magics are only initialized once 157 | switch kind { 158 | case .bishop: guard bishops.isEmpty else { return } 159 | case .rook: guard rooks.isEmpty else { return } 160 | } 161 | 162 | let magics = Square.allCases.map { sq in 163 | // determine board edges not including current square 164 | let edges: Bitboard = ((.rank1 | .rank8) & ~sq.rank.bb) | ((.aFile | .hFile) & ~sq.file.bb) 165 | 166 | // calculate magic bitboard factors 167 | var m = Magic( 168 | magic: magicNumbers[sq.rawValue], 169 | mask: slidingAttacks(for: kind, from: sq, occupancy: 0) & ~edges 170 | ) 171 | 172 | // use Carry-Rippler technique to generate 173 | // all possible subsets of current "mask" 174 | // 175 | // "mask" contains the possible moves on an empty board, 176 | // "subset" is a subset of those moves that accounts for 177 | // any possible blocking piece 178 | var subset = Bitboard(0) 179 | 180 | repeat { 181 | // calculate magic index 182 | let key = m.key(for: subset) 183 | // store subset in attacks dictionary 184 | m.attacks[key] = slidingAttacks( 185 | for: kind, 186 | from: sq, 187 | occupancy: subset 188 | ) 189 | // generate new subset 190 | subset = (subset &- m.mask) & m.mask 191 | } while subset != 0 192 | 193 | return m 194 | } 195 | 196 | switch kind { 197 | case .rook: rooks = magics 198 | case .bishop: bishops = magics 199 | } 200 | } 201 | 202 | /// Piece kinds for which sliding attack magic bitboards can be generated. 203 | private enum SlidingPieceKind { 204 | case bishop 205 | case rook 206 | } 207 | 208 | /// Returns the possible moves for a sliding piece (bishop or rook) 209 | /// accounting for blocking pieces. 210 | /// 211 | /// - note: The first blocking piece encountered in each direction 212 | /// is included in the returned bitboard. It is up to the caller to handle 213 | /// captures or non-capturable pieces (i.e. same color pieces). 214 | private static func slidingAttacks( 215 | for kind: SlidingPieceKind, 216 | from square: Square, 217 | occupancy: Bitboard 218 | ) -> Bitboard { 219 | var attacks = Bitboard(0) 220 | 221 | /// Single square directional moves for given piece. 222 | let directions: [(Bitboard) -> Bitboard] = 223 | switch kind { 224 | case .rook: 225 | [ 226 | { $0.north() }, 227 | { $0.south() }, 228 | { $0.east() }, 229 | { $0.west() } 230 | ] 231 | case .bishop: 232 | [ 233 | { $0.northEast() }, 234 | { $0.northWest() }, 235 | { $0.southEast() }, 236 | { $0.southWest() } 237 | ] 238 | } 239 | 240 | directions.forEach { d in 241 | var nextSquare = square.bb 242 | 243 | repeat { 244 | nextSquare = d(nextSquare) 245 | attacks |= nextSquare 246 | } while nextSquare != 0 && (occupancy & nextSquare) == 0 247 | } 248 | 249 | return attacks 250 | } 251 | 252 | /// Magic numbers for calculating bishop and rook magic bitboards. 253 | /// 254 | /// Derived by [Pradyumna Kannan](http://pradu.us/old/Nov27_2008/Buzz/research/magic/Bitboards.pdf). 255 | private static let magicNumbers: [SlidingPieceKind: [Bitboard]] = [ 256 | .bishop: [ 257 | 0x0002020202020200, 0x0002020202020000, 0x0004010202000000, 0x0004040080000000, 258 | 0x0001104000000000, 0x0000821040000000, 0x0000410410400000, 0x0000104104104000, 259 | 0x0000040404040400, 0x0000020202020200, 0x0000040102020000, 0x0000040400800000, 260 | 0x0000011040000000, 0x0000008210400000, 0x0000004104104000, 0x0000002082082000, 261 | 0x0004000808080800, 0x0002000404040400, 0x0001000202020200, 0x0000800802004000, 262 | 0x0000800400A00000, 0x0000200100884000, 0x0000400082082000, 0x0000200041041000, 263 | 0x0002080010101000, 0x0001040008080800, 0x0000208004010400, 0x0000404004010200, 264 | 0x0000840000802000, 0x0000404002011000, 0x0000808001041000, 0x0000404000820800, 265 | 0x0001041000202000, 0x0000820800101000, 0x0000104400080800, 0x0000020080080080, 266 | 0x0000404040040100, 0x0000808100020100, 0x0001010100020800, 0x0000808080010400, 267 | 0x0000820820004000, 0x0000410410002000, 0x0000082088001000, 0x0000002011000800, 268 | 0x0000080100400400, 0x0001010101000200, 0x0002020202000400, 0x0001010101000200, 269 | 0x0000410410400000, 0x0000208208200000, 0x0000002084100000, 0x0000000020880000, 270 | 0x0000001002020000, 0x0000040408020000, 0x0004040404040000, 0x0002020202020000, 271 | 0x0000104104104000, 0x0000002082082000, 0x0000000020841000, 0x0000000000208800, 272 | 0x0000000010020200, 0x0000000404080200, 0x0000040404040400, 0x0002020202020200 273 | ], 274 | .rook: [ 275 | 0x0080001020400080, 0x0040001000200040, 0x0080081000200080, 0x0080040800100080, 276 | 0x0080020400080080, 0x0080010200040080, 0x0080008001000200, 0x0080002040800100, 277 | 0x0000800020400080, 0x0000400020005000, 0x0000801000200080, 0x0000800800100080, 278 | 0x0000800400080080, 0x0000800200040080, 0x0000800100020080, 0x0000800040800100, 279 | 0x0000208000400080, 0x0000404000201000, 0x0000808010002000, 0x0000808008001000, 280 | 0x0000808004000800, 0x0000808002000400, 0x0000010100020004, 0x0000020000408104, 281 | 0x0000208080004000, 0x0000200040005000, 0x0000100080200080, 0x0000080080100080, 282 | 0x0000040080080080, 0x0000020080040080, 0x0000010080800200, 0x0000800080004100, 283 | 0x0000204000800080, 0x0000200040401000, 0x0000100080802000, 0x0000080080801000, 284 | 0x0000040080800800, 0x0000020080800400, 0x0000020001010004, 0x0000800040800100, 285 | 0x0000204000808000, 0x0000200040008080, 0x0000100020008080, 0x0000080010008080, 286 | 0x0000040008008080, 0x0000020004008080, 0x0000010002008080, 0x0000004081020004, 287 | 0x0000204000800080, 0x0000200040008080, 0x0000100020008080, 0x0000080010008080, 288 | 0x0000040008008080, 0x0000020004008080, 0x0000800100020080, 0x0000800041000080, 289 | 0x00FFFCDDFCED714A, 0x007FFCDDFCED714A, 0x003FFFCDFFD88096, 0x0000040810002101, 290 | 0x0001000204080011, 0x0001000204000801, 0x0001000082000401, 0x0001FFFAABFAD1A2 291 | ] 292 | ] 293 | 294 | } 295 | 296 | /// Stores the magic factors and attacks for a given piece 297 | /// type (bishop or rook) and square (a1-h8). 298 | struct Magic: Sendable { 299 | /// The magic number used to compute the hash key. 300 | fileprivate var magic: Bitboard 301 | /// The bitmask representing the possible moves on an empty board 302 | /// excluding edges. 303 | fileprivate var mask: Bitboard 304 | 305 | /// The number of zero bits in the mask, used to calculate the hash key. 306 | fileprivate var shift: Int { 307 | Bitboard.bitWidth - mask.nonzeroBitCount 308 | } 309 | 310 | /// The dictionary of attack bitboards, keyed by the hash key. 311 | fileprivate var attacks: [Bitboard: Bitboard] = [:] 312 | 313 | /// Returns the hash key for a given `subset` of possible moves. 314 | fileprivate func key(for subset: Bitboard) -> Bitboard { 315 | (subset &* magic) >> shift 316 | } 317 | 318 | /// Returns the attack bitboard for the piece represented 319 | /// by the receiver for the given `occupancy`. 320 | fileprivate func attacks(for occupancy: Bitboard) -> Bitboard { 321 | attacks[key(for: occupancy & mask)] ?? 0 322 | } 323 | } 324 | 325 | extension [Magic] { 326 | func attacks(from square: Square, for occupancy: Bitboard) -> Bitboard { 327 | guard square.rawValue < count else { return 0 } 328 | return self[square.rawValue].attacks(for: occupancy) 329 | } 330 | } 331 | -------------------------------------------------------------------------------- /Sources/ChessKit/Bitboards/Bitboard.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Bitboard.swift 3 | // ChessKit 4 | // 5 | 6 | /// Contains `UInt64`-based utilities for manipulating 7 | /// chess bitboards. 8 | typealias Bitboard = UInt64 9 | 10 | extension Bitboard { 11 | 12 | /// Bitboard representing all the squares on the A file. 13 | static let aFile: Bitboard = 0x0101010101010101 14 | /// Bitboard representing all the squares on the H file. 15 | static let hFile: Bitboard = aFile << 7 16 | 17 | /// Bitboard representing all the squares on the 1st rank. 18 | static let rank1: Bitboard = 0xFF 19 | /// Bitboard representing all the squares on the 8th rank. 20 | static let rank8: Bitboard = rank1 << (8 * 7) 21 | 22 | /// Bitboard representing all the dark squares on the board. 23 | static let dark: Bitboard = 0xAA55AA55AA55AA55 24 | /// Bitboard representing all the light squares on the board. 25 | static let light: Bitboard = ~dark 26 | 27 | /// Translates the receiver `n` column "east" on an 8x8 grid. 28 | /// 29 | /// `n` should be in the range `[1, 7]`. 30 | func east(_ n: Int = 1) -> Bitboard { 31 | (self & ~Self.hFile) << n 32 | } 33 | 34 | /// Translates the receiver `n` columns "west" on an 8x8 grid. 35 | /// 36 | /// `n` should be in the range `[1, 7]`. 37 | func west(_ n: Int = 1) -> Bitboard { 38 | (self & ~Self.aFile) >> n 39 | } 40 | 41 | /// Translates the receiver `n` rows "north" on an 8x8 grid. 42 | /// 43 | /// `n` should be in the range `[1, 7]`. 44 | func north(_ n: Int = 1) -> Bitboard { 45 | self << (8 * n) 46 | } 47 | 48 | /// Translates the receiver `n` rows "south" on an 8x8 grid. 49 | /// 50 | /// `n` should be in the range `[1, 7]`. 51 | func south(_ n: Int = 1) -> Bitboard { 52 | self >> (8 * n) 53 | } 54 | 55 | /// Translates the receiver `n` rows "north" and `n` columns "east" on an 8x8 grid. 56 | /// 57 | /// `n` should be in the range `[1, 7]`. 58 | func northEast(_ n: Int = 1) -> Bitboard { 59 | (self & ~Self.hFile) << (9 * n) 60 | } 61 | 62 | /// Translates the receiver `n` rows "north" and `n` columns "west" on an 8x8 grid. 63 | /// 64 | /// `n` should be in the range `[1, 7]`. 65 | func northWest(_ n: Int = 1) -> Bitboard { 66 | (self & ~Self.aFile) << (7 * n) 67 | } 68 | 69 | /// Translates the receiver `n` rows "south" and `n` columns "east" on an 8x8 grid. 70 | /// 71 | /// `n` should be in the range `[1, 7]`. 72 | func southEast(_ n: Int = 1) -> Bitboard { 73 | (self & ~Self.hFile) >> (7 * n) 74 | } 75 | 76 | /// Translates the receiver `n` rows "south" and `n` columns "west" on an 8x8 grid. 77 | /// 78 | /// `n` should be in the range `[1, 7]`. 79 | func southWest(_ n: Int = 1) -> Bitboard { 80 | (self & ~Self.aFile) >> (9 * n) 81 | } 82 | 83 | } 84 | 85 | extension Bitboard { 86 | 87 | /// Converts the `Bitboard` to an 8x8 board representation string. 88 | /// 89 | /// - parameter occupied: The character with which to represent occupied squares. 90 | /// - parameter empty: The character with which to represent unoccupied squares. 91 | /// - parameter labelRanks: Whether or not to label ranks (i.e. 1, 2, 3, ...). 92 | /// - parameter labelFiles: Whether or not to label ranks (i.e. a, b, c, ...). 93 | /// - returns: A string representing an 8x8 chess board. 94 | /// 95 | // periphery:ignore 96 | func chessString( 97 | _ occupied: Character = "⨯", 98 | _ empty: Character = "·", 99 | labelRanks: Bool = true, 100 | labelFiles: Bool = true 101 | ) -> String { 102 | var s = "" 103 | 104 | for rank in Square.Rank.range.reversed() { 105 | s += labelRanks ? "\(rank) " : "" 106 | var newLine = "" 107 | 108 | for file in Square.File.allCases { 109 | let sq = Square(file, .init(rank)).bb 110 | newLine += (self & sq != 0) ? "\(occupied) " : "\(empty) " 111 | } 112 | 113 | s += newLine.trimmingCharacters(in: .whitespaces) + "\n" 114 | } 115 | 116 | let fileLabels = (labelFiles ? "\n a b c d e f g h" : "") 117 | return s.trimmingCharacters(in: .whitespacesAndNewlines) + fileLabels 118 | } 119 | 120 | } 121 | 122 | extension Bitboard { 123 | var squares: [Square] { 124 | var indices: [Int] = [] 125 | var bb = self 126 | 127 | while bb != 0 { 128 | let index = bb.trailingZeroBitCount 129 | indices.append(index) 130 | bb &= bb &- 1 131 | } 132 | 133 | return indices.compactMap(Square.init) 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /Sources/ChessKit/Bitboards/PieceSet.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PieceSet.swift 3 | // ChessKit 4 | // 5 | 6 | /// Stores the bitboards for all pieces. 7 | /// 8 | /// Also contains convenient amalgamations 9 | /// of different combinations of pieces. 10 | struct PieceSet: Hashable, Sendable { 11 | /// Bitboard for black king pieces. 12 | var k: Bitboard = 0 13 | /// Bitboard for black queen pieces. 14 | var q: Bitboard = 0 15 | /// Bitboard for black rook pieces. 16 | var r: Bitboard = 0 17 | /// Bitboard for black bishop pieces. 18 | var b: Bitboard = 0 19 | /// Bitboard for black knight pieces. 20 | var n: Bitboard = 0 21 | /// Bitboard for black pawn pieces. 22 | var p: Bitboard = 0 23 | 24 | /// Bitboard for white king pieces. 25 | var K: Bitboard = 0 26 | /// Bitboard for white queen pieces. 27 | var Q: Bitboard = 0 28 | /// Bitboard for white rook pieces. 29 | var R: Bitboard = 0 30 | /// Bitboard for white bishop pieces. 31 | var B: Bitboard = 0 32 | /// Bitboard for white knight pieces. 33 | var N: Bitboard = 0 34 | /// Bitboard for white pawn pieces. 35 | var P: Bitboard = 0 36 | 37 | /// Bitboard for all the pieces. 38 | var all: Bitboard { black | white } 39 | /// Bitboard for all the black pieces. 40 | var black: Bitboard { k | q | r | b | n | p } 41 | /// Bitboard for all the white pieces. 42 | var white: Bitboard { K | Q | R | B | N | P } 43 | 44 | /// Bitboard for all the king pieces. 45 | var kings: Bitboard { k | K } 46 | /// Bitboard for all the queen pieces. 47 | var queens: Bitboard { q | Q } 48 | /// Bitboard for all the rook pieces. 49 | var rooks: Bitboard { r | R } 50 | /// Bitboard for all the bishop pieces. 51 | var bishops: Bitboard { b | B } 52 | /// Bitboard for all the knight pieces. 53 | var knights: Bitboard { n | N } 54 | /// Bitboard for all the pawn pieces. 55 | var pawns: Bitboard { p | P } 56 | 57 | /// Bitboard for all the diagonal sliding pieces. 58 | var diagonals: Bitboard { Q | q | B | b } 59 | /// Bitboard for all the vertical/horizontal sliding pieces. 60 | var lines: Bitboard { Q | q | R | r } 61 | 62 | var pieces: [Piece] { 63 | k.squares.map { Piece(.king, color: .black, square: $0) } + q.squares.map { Piece(.queen, color: .black, square: $0) } + r.squares.map { Piece(.rook, color: .black, square: $0) } 64 | + b.squares.map { Piece(.bishop, color: .black, square: $0) } + n.squares.map { Piece(.knight, color: .black, square: $0) } + p.squares.map { Piece(.pawn, color: .black, square: $0) } 65 | + K.squares.map { Piece(.king, color: .white, square: $0) } + Q.squares.map { Piece(.queen, color: .white, square: $0) } + R.squares.map { Piece(.rook, color: .white, square: $0) } 66 | + B.squares.map { Piece(.bishop, color: .white, square: $0) } + N.squares.map { Piece(.knight, color: .white, square: $0) } + P.squares.map { Piece(.pawn, color: .white, square: $0) } 67 | } 68 | 69 | init(pieces: [Piece]) { 70 | pieces.forEach { add($0) } 71 | } 72 | 73 | func get(_ color: Piece.Color) -> Bitboard { 74 | switch color { 75 | case .white: white 76 | case .black: black 77 | } 78 | } 79 | 80 | func get(_ kind: Piece.Kind) -> Bitboard { 81 | switch kind { 82 | case .pawn: pawns 83 | case .knight: knights 84 | case .bishop: bishops 85 | case .rook: rooks 86 | case .queen: queens 87 | case .king: kings 88 | } 89 | } 90 | 91 | func get(_ square: Square) -> Piece? { 92 | if k & square.bb != 0 { 93 | .init(.king, color: .black, square: square) 94 | } else if q & square.bb != 0 { 95 | .init(.queen, color: .black, square: square) 96 | } else if r & square.bb != 0 { 97 | .init(.rook, color: .black, square: square) 98 | } else if b & square.bb != 0 { 99 | .init(.bishop, color: .black, square: square) 100 | } else if n & square.bb != 0 { 101 | .init(.knight, color: .black, square: square) 102 | } else if p & square.bb != 0 { 103 | .init(.pawn, color: .black, square: square) 104 | } 105 | 106 | else if K & square.bb != 0 { 107 | .init(.king, color: .white, square: square) 108 | } else if Q & square.bb != 0 { 109 | .init(.queen, color: .white, square: square) 110 | } else if R & square.bb != 0 { 111 | .init(.rook, color: .white, square: square) 112 | } else if B & square.bb != 0 { 113 | .init(.bishop, color: .white, square: square) 114 | } else if N & square.bb != 0 { 115 | .init(.knight, color: .white, square: square) 116 | } else if P & square.bb != 0 { 117 | .init(.pawn, color: .white, square: square) 118 | } else { 119 | nil 120 | } 121 | } 122 | 123 | mutating func add(_ piece: Piece) { 124 | add(piece, to: piece.square) 125 | } 126 | 127 | mutating func add(_ piece: Piece, to square: Square) { 128 | switch (piece.color, piece.kind) { 129 | case (.black, .king): k |= square.bb 130 | case (.black, .queen): q |= square.bb 131 | case (.black, .rook): r |= square.bb 132 | case (.black, .bishop): b |= square.bb 133 | case (.black, .knight): n |= square.bb 134 | case (.black, .pawn): p |= square.bb 135 | 136 | case (.white, .king): K |= square.bb 137 | case (.white, .queen): Q |= square.bb 138 | case (.white, .rook): R |= square.bb 139 | case (.white, .bishop): B |= square.bb 140 | case (.white, .knight): N |= square.bb 141 | case (.white, .pawn): P |= square.bb 142 | } 143 | } 144 | 145 | mutating func remove(_ piece: Piece) { 146 | switch (piece.color, piece.kind) { 147 | case (.black, .king): k &= ~piece.square.bb 148 | case (.black, .queen): q &= ~piece.square.bb 149 | case (.black, .rook): r &= ~piece.square.bb 150 | case (.black, .bishop): b &= ~piece.square.bb 151 | case (.black, .knight): n &= ~piece.square.bb 152 | case (.black, .pawn): p &= ~piece.square.bb 153 | 154 | case (.white, .king): K &= ~piece.square.bb 155 | case (.white, .queen): Q &= ~piece.square.bb 156 | case (.white, .rook): R &= ~piece.square.bb 157 | case (.white, .bishop): B &= ~piece.square.bb 158 | case (.white, .knight): N &= ~piece.square.bb 159 | case (.white, .pawn): P &= ~piece.square.bb 160 | } 161 | } 162 | 163 | /// Replaces a piece's kind with another, such as when 164 | /// performing a piece promotion. 165 | mutating func replace(_ kind: Piece.Kind, for piece: Piece) { 166 | var newPiece = piece 167 | newPiece.kind = kind 168 | 169 | remove(piece) 170 | add(newPiece) 171 | } 172 | 173 | mutating func move(_ piece: Piece, to square: Square) { 174 | remove(piece) 175 | add(piece, to: square) 176 | } 177 | } 178 | 179 | // MARK: - CustomStringConvertible 180 | extension PieceSet: CustomStringConvertible { 181 | 182 | var description: String { 183 | var s = "" 184 | 185 | for rank in Square.Rank.range.reversed() { 186 | s += "\(rank)" 187 | 188 | for file in Square.File.allCases { 189 | let sq = Square(file, .init(rank)) 190 | 191 | if let piece = get(sq) { 192 | s += " \(ChessKitConfiguration.printOptions.mode == .graphic ? piece.graphic : piece.fen)" 193 | } else { 194 | s += " ·" 195 | } 196 | } 197 | 198 | s += "\n" 199 | } 200 | 201 | return s + " a b c d e f g h" 202 | } 203 | 204 | } 205 | -------------------------------------------------------------------------------- /Sources/ChessKit/Bitboards/Square+BB.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Square+BB.swift 3 | // ChessKit 4 | // 5 | 6 | extension Square { 7 | var bb: Bitboard { 1 << rawValue } 8 | 9 | init?(_ bb: Bitboard) { 10 | self.init(rawValue: bb.trailingZeroBitCount) 11 | } 12 | } 13 | 14 | extension [Square] { 15 | var bb: Bitboard { 16 | var bb = Bitboard(0) 17 | 18 | self.forEach { 19 | bb |= $0.bb 20 | } 21 | 22 | return bb 23 | } 24 | } 25 | 26 | extension Square.File { 27 | var bb: Bitboard { .aFile.east(number - 1) } 28 | } 29 | 30 | extension Square.Rank { 31 | var bb: Bitboard { .rank1.north(value - 1) } 32 | } 33 | -------------------------------------------------------------------------------- /Sources/ChessKit/Clock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Clock.swift 3 | // ChessKit 4 | // 5 | 6 | /// Tracks the number of moves in a game for 7 | /// the purposes of regulating the 50 move rule. 8 | public struct Clock: Hashable, Sendable { 9 | 10 | /// The maximum number of half moves before 11 | /// a draw by the fifty move rule should be called. 12 | static let halfMoveMaximum = 100 13 | 14 | /// The number of halfmoves, incremented after each move. 15 | /// It is reset to zero after each capture or pawn move. 16 | public var halfmoves = 0 17 | 18 | /// The number of fullmoves, incremented after each Black move. 19 | public var fullmoves = 1 20 | 21 | } 22 | -------------------------------------------------------------------------------- /Sources/ChessKit/Configuration.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ChessKitConfiguration.swift 3 | // ChessKit 4 | // 5 | 6 | /// Stores configuration options for the `ChessKit` package. 7 | public struct ChessKitConfiguration: Sendable { 8 | 9 | /// Configuration options for printing ``Board`` and ``Position`` objects, useful 10 | /// for debugging. 11 | public nonisolated(unsafe) static var printOptions = PrintOptions() 12 | 13 | public struct PrintOptions: Sendable { 14 | /// Whether to print pieces as letters (`letter`) or unicode graphics (`graphic`) 15 | /// when printing ``Board`` and ``Position`` objects. 16 | /// 17 | /// The default value is `graphic`. 18 | public var mode = PrintMode.graphic 19 | 20 | /// Whether to print rank and file labels 21 | /// when printing ``Board`` and ``Position`` objects. 22 | /// 23 | /// The default value is `graphic`. 24 | public var showCoordinates = true 25 | 26 | /// ChessKit `printMode` options. 27 | public enum PrintMode: Sendable { 28 | /// Print pieces as unicode graphic characters, e.g. ♟, ♞, ♝, ♜, ♛, ♚. 29 | case graphic 30 | /// Print pieces as FEN letters, e.g. P, N, B, R, Q, K. 31 | /// 32 | /// Uppercase letters are white pieces and lowercase letters are black pieces. 33 | case letter 34 | } 35 | } 36 | 37 | /// Whether to print pieces as letters (`letter`) or unicode graphics (`graphic`) 38 | /// when printing ``Board`` and ``Position`` objects. 39 | /// 40 | /// The default value is `graphic`. 41 | @available(*, deprecated, renamed: "printOptions.mode") 42 | public static var printMode: PrintOptions.PrintMode { 43 | get { printOptions.mode } 44 | set { printOptions.mode = newValue } 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /Sources/ChessKit/Move.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ChessMove.swift 3 | // ChessKit 4 | // 5 | 6 | /// Represents a move on a chess board. 7 | public struct Move: Hashable, Sendable { 8 | 9 | /// The result of the move. 10 | public enum Result: Hashable, Sendable { 11 | case move 12 | case capture(Piece) 13 | case castle(Castling) 14 | } 15 | 16 | /// The check state resulting from the move. 17 | public enum CheckState: String, Sendable { 18 | case none 19 | case check 20 | case checkmate 21 | case stalemate 22 | 23 | var notation: String { 24 | switch self { 25 | case .none, .stalemate: "" 26 | case .check: "+" 27 | case .checkmate: "#" 28 | } 29 | } 30 | } 31 | 32 | /// Rank, file, or square disambiguation of moves. 33 | public enum Disambiguation: Hashable, Sendable { 34 | case byFile(Square.File) 35 | case byRank(Square.Rank) 36 | case bySquare(Square) 37 | } 38 | 39 | /// The result of the move. 40 | public internal(set) var result: Result 41 | /// The piece that made the move. 42 | public internal(set) var piece: Piece 43 | /// The starting square of the move. 44 | public internal(set) var start: Square 45 | /// The ending square of the move. 46 | public internal(set) var end: Square 47 | /// The piece that was promoted to, if applicable. 48 | public internal(set) var promotedPiece: Piece? 49 | /// The move disambiguation, if applicable. 50 | public internal(set) var disambiguation: Disambiguation? 51 | /// The check state resulting from the move. 52 | public internal(set) var checkState: CheckState 53 | /// The move assessment annotation. 54 | public var assessment: Assessment 55 | /// The comment associated with a move. 56 | public var comment: String 57 | 58 | /// Initialize a move with the given characteristics. 59 | public init( 60 | result: Result, 61 | piece: Piece, 62 | start: Square, 63 | end: Square, 64 | checkState: CheckState = .none, 65 | assessment: Assessment = .null, 66 | comment: String = "" 67 | ) { 68 | self.result = result 69 | self.piece = piece 70 | self.start = start 71 | self.end = end 72 | self.checkState = checkState 73 | self.assessment = assessment 74 | self.comment = comment 75 | } 76 | 77 | /// Initialize a move with a given SAN string. 78 | /// 79 | /// This initializer fails if the provided SAN string is invalid. 80 | public init?(san: String, position: Position) { 81 | guard let move = SANParser.parse(move: san, in: position) else { 82 | return nil 83 | } 84 | 85 | self = move 86 | } 87 | 88 | /// The SAN represenation of the move. 89 | public var san: String { 90 | SANParser.convert(move: self) 91 | } 92 | 93 | /// The engine LAN represenation of the move. 94 | /// 95 | /// - note: This is intended for engine communication 96 | /// so piece names, capture/check indicators, etc. are not included. 97 | public var lan: String { 98 | EngineLANParser.convert(move: self) 99 | } 100 | 101 | } 102 | 103 | // MARK: - Assessment 104 | extension Move { 105 | 106 | /// Single move assessments. 107 | /// 108 | /// The raw String value corresponds to what is displayed 109 | /// in a PGN string. 110 | public enum Assessment: String, Sendable { 111 | case null = "$0" 112 | case good = "$1" 113 | case mistake = "$2" 114 | case brilliant = "$3" 115 | case blunder = "$4" 116 | case interesting = "$5" 117 | case dubious = "$6" 118 | case forced = "$7" 119 | case singular = "$8" 120 | case worst = "$9" 121 | 122 | /// The human-readable move assessment notation. 123 | public var notation: String { 124 | switch self { 125 | case .null: "" 126 | case .good: "!" 127 | case .mistake: "?" 128 | case .brilliant: "!!" 129 | case .blunder: "??" 130 | case .interesting: "!?" 131 | case .dubious: "?!" 132 | case .forced: "□" 133 | case .singular: "" 134 | case .worst: "" 135 | } 136 | } 137 | 138 | public init?(notation: String) { 139 | switch notation { 140 | case "": self = .null 141 | case "!": self = .good 142 | case "?": self = .mistake 143 | case "!!": self = .brilliant 144 | case "??": self = .blunder 145 | case "!?": self = .interesting 146 | case "?!": self = .dubious 147 | case "□": self = .forced 148 | default: return nil 149 | } 150 | } 151 | } 152 | 153 | } 154 | 155 | // MARK: - CustomStringConvertible 156 | extension Move: CustomStringConvertible { 157 | public var description: String { 158 | san 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /Sources/ChessKit/MoveTree/MoveTree+Collection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MoveTree+Collection.swift 3 | // ChessKit 4 | // 5 | 6 | // MARK: - Collection 7 | extension MoveTree: Collection { 8 | 9 | public var startIndex: Index { minimumIndex } 10 | 11 | public var endIndex: Index { lastMainVariationIndex } 12 | 13 | public subscript(_ index: Index) -> Move? { 14 | get { 15 | dictionary[index]?.move 16 | } 17 | set { 18 | if let newValue { 19 | add(move: newValue, toParentIndex: index.previous) 20 | } 21 | } 22 | } 23 | 24 | } 25 | 26 | // MARK: - BidirectionalCollection 27 | extension MoveTree: BidirectionalCollection { 28 | 29 | /// Returns the previous index in the move tree based on `i`. 30 | /// 31 | /// If there is no previous index, `i` is returned. 32 | /// Use `hasIndex(before:)` to check whether a valid index 33 | /// before `i` exists. 34 | public func index(before i: Index) -> Index { 35 | _previousIndex(for: i) ?? i 36 | } 37 | 38 | /// Returns `true` if a valid index before `i` exists. 39 | public func hasIndex(before i: Index) -> Bool { 40 | _previousIndex(for: i) != nil 41 | } 42 | 43 | private func _previousIndex(for index: Index) -> Index? { 44 | if index == minimumIndex.next { 45 | minimumIndex 46 | } else { 47 | dictionary[index]?.previous?.index 48 | } 49 | } 50 | 51 | /// Returns the next index in the move tree based on `i`. 52 | /// 53 | /// If there is no next index, `i` is returned. 54 | /// Use `hasIndex(after:)` to check whether a valid index 55 | /// after `i` exists. 56 | public func index(after i: Index) -> Index { 57 | _nextIndex(for: i) ?? i 58 | } 59 | 60 | /// Returns `true` if a valid index after `i` exists. 61 | public func hasIndex(after i: Index) -> Bool { 62 | _nextIndex(for: i) != nil 63 | } 64 | 65 | private func _nextIndex(for index: Index) -> Index? { 66 | if index == minimumIndex { 67 | dictionary[minimumIndex.next]?.index 68 | } else { 69 | dictionary[index]?.next?.index 70 | } 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /Sources/ChessKit/MoveTree/MoveTree+Deprecated.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MoveTree+Deprecated.swift 3 | // ChessKit 4 | // 5 | 6 | extension MoveTree { 7 | /// Returns the move at the specified index. 8 | /// 9 | /// - parameter index: The move index to query. 10 | /// - returns: The move at the given index, or `nil` if no 11 | /// move exists at that index. 12 | /// 13 | /// This value can also be accessed using a subscript on 14 | /// the ``MoveTree`` directly, 15 | /// e.g. `tree[.init(number: 2, color: .white)]` 16 | @available(*, deprecated, renamed: "subscript(_:)") 17 | public func move(at index: Index) -> Move? { 18 | dictionary[index]?.move 19 | } 20 | 21 | /// Returns the index of the previous move given an `index`. 22 | @available( 23 | *, deprecated, 24 | renamed: "index(before:)", 25 | message: "Use index(before:) to obtain the previous index or hasIndex(before:) to check if a valid previous index exists." 26 | ) 27 | public func previousIndex(for index: Index) -> Index? { 28 | if index == minimumIndex.next { 29 | minimumIndex 30 | } else { 31 | dictionary[index]?.previous?.index 32 | } 33 | } 34 | 35 | /// Returns the index of the next move given an `index`. 36 | @available( 37 | *, deprecated, 38 | renamed: "index(after:)", 39 | message: "Use index(after:) to obtain the next index or hasIndex(after:) to check if a valid next index exists." 40 | ) 41 | public func nextIndex(for index: Index) -> Index? { 42 | if index == minimumIndex { 43 | dictionary[minimumIndex.next]?.index 44 | } else { 45 | dictionary[index]?.next?.index 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Sources/ChessKit/MoveTree/MoveTree+Index.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MoveTree+Index.swift 3 | // ChessKit 4 | // 5 | 6 | extension MoveTree { 7 | /// Object that represents the index of a node in the move tree. 8 | public struct Index: Hashable, Sendable { 9 | 10 | /// Variation number corresponding to the main variation of the tree. 11 | public static let mainVariation = 0 12 | 13 | /// The move number. 14 | public let number: Int 15 | /// The color of the piece moved on this move. 16 | public let color: Piece.Color 17 | /// The variation number of the move. 18 | /// 19 | /// If multiple moves occur for the same move number and piece color, 20 | /// the `variation` is incremented. 21 | /// 22 | /// A `variation` equal to `MoveTree.Index.mainVariation` is assumed to be the 23 | /// main variation in a move tree. 24 | public var variation: Int = mainVariation 25 | 26 | /// Creates a `MoveTree.Index` with a given `number`, `color`, 27 | /// and `variation` (default is `0`). 28 | public init(number: Int, color: Piece.Color, variation: Int = mainVariation) { 29 | self.number = number 30 | self.color = color 31 | self.variation = variation 32 | } 33 | 34 | /// The minimum value of `MoveTree.Index(number: 0, color: .black)` 35 | /// 36 | /// This represents the starting position of the game. 37 | /// 38 | /// i.e. `MoveTree.Index(number: 1, color: .white)` is returned by `MoveTree.Index.minimum.next` 39 | /// which is the first move of the game (played by white). 40 | public static let minimum = Index(number: 0, color: .black) 41 | 42 | /// The previous index. 43 | /// 44 | /// This assumes `variation` is constant. 45 | /// For the previous index taking into account variations 46 | /// use `MoveTree.index(before:)`. 47 | public var previous: Index { 48 | switch color { 49 | case .white: 50 | Index( 51 | number: number - 1, 52 | color: .black, 53 | variation: variation 54 | ) 55 | case .black: 56 | Index( 57 | number: number, 58 | color: .white, 59 | variation: variation 60 | ) 61 | } 62 | } 63 | 64 | /// The next index. 65 | /// 66 | /// This assumes `variation` is constant. 67 | /// For the next index taking into account variations 68 | /// use `MoveTree.index(after:)`. 69 | public var next: Index { 70 | switch color { 71 | case .white: 72 | Index( 73 | number: number, 74 | color: .black, 75 | variation: variation 76 | ) 77 | case .black: 78 | Index( 79 | number: number + 1, 80 | color: .white, 81 | variation: variation 82 | ) 83 | } 84 | } 85 | 86 | } 87 | 88 | } 89 | 90 | // MARK: - Comparable 91 | extension MoveTree.Index: Comparable { 92 | public static func < (lhs: Self, rhs: Self) -> Bool { 93 | if lhs.variation == rhs.variation { 94 | if lhs.number == rhs.number { 95 | lhs.color == .white && rhs.color == .black 96 | } else { 97 | lhs.number < rhs.number 98 | } 99 | } else { 100 | // prioritize lower variation numbers (since 0 is the main variation) 101 | lhs.variation > rhs.variation 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /Sources/ChessKit/MoveTree/MoveTree.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MoveTree.swift 3 | // ChessKit 4 | // 5 | 6 | import Foundation 7 | 8 | /// A tree-like data structure that represents the moves of a chess game. 9 | /// 10 | /// The tree maintains the move order including variations and 11 | /// provides index-based access for any element in the tree. 12 | public struct MoveTree: Hashable, Sendable { 13 | 14 | /// The index of the root of the move tree. 15 | /// 16 | /// Defaults to `MoveTree.Index.minimum`. 17 | var minimumIndex: Index = .minimum 18 | 19 | /// The last index of the main variation of the move tree. 20 | private(set) var lastMainVariationIndex: Index = .minimum 21 | 22 | /// Dictionary representation of the tree for faster access. 23 | private(set) var dictionary: [Index: Node] = [:] 24 | /// The root node of the tree. 25 | private var root: Node? 26 | 27 | /// A set containing the indices of all the moves stored in the tree. 28 | public var indices: [Index] { 29 | Array(dictionary.keys) 30 | } 31 | 32 | /// Lock to restrict modification of tree nodes 33 | /// to ensure `Sendable` conformance for ``Node``. 34 | private static let nodeLock = NSLock() 35 | 36 | /// Adds a move to the move tree. 37 | /// 38 | /// - parameter move: The move to add to the tree. 39 | /// - parameter moveIndex: The `MoveIndex` of the parent move, if applicable. 40 | /// If `moveIndex` is `nil`, the move tree is cleared and the provided 41 | /// move is set to the `head` of the move tree. 42 | /// 43 | /// - returns: The move index resulting from the addition of the move. 44 | /// 45 | @discardableResult 46 | public mutating func add( 47 | move: Move, 48 | toParentIndex moveIndex: Index? = nil 49 | ) -> Index { 50 | let newNode = Node(move: move) 51 | 52 | guard let root, let moveIndex else { 53 | let index = minimumIndex.next 54 | 55 | newNode.index = index 56 | self.root = newNode 57 | 58 | dictionary = [index: newNode] 59 | 60 | if index.variation == Index.mainVariation { 61 | lastMainVariationIndex = index 62 | } 63 | return index 64 | } 65 | 66 | let parent = dictionary[moveIndex] ?? root 67 | newNode.previous = parent 68 | 69 | var newIndex = moveIndex.next 70 | 71 | if parent.next == nil { 72 | parent.next = newNode 73 | } else { 74 | parent.children.append(newNode) 75 | while indices.contains(newIndex) { 76 | newIndex.variation += 1 77 | } 78 | } 79 | 80 | Self.nodeLock.withLock { 81 | dictionary[newIndex] = newNode 82 | } 83 | newNode.index = newIndex 84 | 85 | if newIndex.variation == Index.mainVariation { 86 | lastMainVariationIndex = newIndex 87 | } 88 | 89 | return newIndex 90 | } 91 | 92 | /// Returns the index matching `move` in the next or child moves of the 93 | /// move contained at `index`. 94 | public func nextIndex(containing move: Move, for index: Index) -> Index? { 95 | guard let node = dictionary[index] else { 96 | if index == minimumIndex, let root, root.move == move { 97 | return root.index 98 | } else { 99 | return nil 100 | } 101 | } 102 | 103 | if let next = node.next, next.move == move { 104 | return next.index 105 | } else { 106 | return node.children.filter { $0.move == move }.first?.index 107 | } 108 | } 109 | 110 | /// Provides a single history for a given index. 111 | /// 112 | /// - parameter index: The index from which to generate the history. 113 | /// - returns: An array of move indices sorted from beginning to end with 114 | /// the end being the provided `index`. 115 | /// 116 | /// For chess this would represent an array of all the move indices 117 | /// from the starting move until the move defined by `index`, accounting 118 | /// for any branching variations in between. 119 | public func history(for index: Index) -> [Index] { 120 | let index = index == .minimum ? .minimum.next : index 121 | var currentNode = dictionary[index] 122 | var history: [Index] = [] 123 | 124 | while currentNode != nil { 125 | if let node = currentNode { 126 | history.append(node.index) 127 | } 128 | 129 | currentNode = currentNode?.previous 130 | } 131 | 132 | return history.reversed() 133 | } 134 | 135 | /// Provides a single future for a given index. 136 | /// 137 | /// - parameter index: The index from which to generate the future. 138 | /// - returns: An array of move indices sorted from beginning to end. 139 | /// 140 | /// For chess this would represent an array of all the move indices 141 | /// from the move after the move defined by `index` to the last move 142 | /// of the variation. 143 | public func future(for index: Index) -> [Index] { 144 | let index = index == .minimum ? .minimum.next : index 145 | var currentNode = dictionary[index] 146 | var future: [Index] = [] 147 | 148 | while currentNode != nil { 149 | currentNode = currentNode?.next 150 | 151 | if let node = currentNode { 152 | future.append(node.index) 153 | } 154 | } 155 | 156 | return future 157 | } 158 | 159 | /// Returns the full variation for a move at the provided `index`. 160 | /// 161 | /// This returns the sum of `history(for:)` and `future(for:)`. 162 | public func fullVariation(for index: Index) -> [Index] { 163 | history(for: index) + future(for: index) 164 | } 165 | 166 | private func indices(between start: Index, and end: Index) -> [Index] { 167 | var result = [Index]() 168 | 169 | let endNode = dictionary[end] 170 | var currentNode = dictionary[start] 171 | 172 | while currentNode != endNode { 173 | if let currentNode { 174 | result.append(currentNode.index) 175 | } 176 | 177 | currentNode = currentNode?.previous 178 | } 179 | 180 | return result 181 | } 182 | 183 | /// Provides the shortest path through the move tree 184 | /// from the given start and end indices. 185 | /// 186 | /// - parameter startIndex: The starting index of the path. 187 | /// - parameter endIndex: The ending index of the path. 188 | /// - returns: An array of indices starting with the index after `startIndex` 189 | /// and ending with `endIndex`. If `startIndex` and `endIndex` 190 | /// are the same, an empty array is returned. 191 | /// 192 | /// The purpose of this path is return the indices of the moves required to 193 | /// go from the current position at `startIndex` and end up with the 194 | /// final position at `endIndex`, so `startIndex` is included in the returned 195 | /// array, but `endIndex` is not. The path direction included with the index 196 | /// indicates the direction to move to get to the next index. 197 | public func path( 198 | from startIndex: Index, 199 | to endIndex: Index 200 | ) -> [(direction: PathDirection, index: Index)] { 201 | var results = [(PathDirection, Index)]() 202 | let startHistory = history(for: startIndex) 203 | let endHistory = history(for: endIndex) 204 | 205 | if startIndex == endIndex { 206 | // keep results array empty 207 | } else if startHistory.contains(endIndex) { 208 | results = indices(between: startIndex, and: endIndex) 209 | .map { (.reverse, $0) } 210 | } else if endHistory.contains(startIndex) { 211 | results = indices(between: endIndex, and: startIndex) 212 | .map { (.forward, $0) } 213 | .reversed() 214 | } else { 215 | // lowest common ancestor 216 | guard 217 | let lca = zip(startHistory, endHistory).filter({ $0 == $1 }).last?.0, 218 | let startLCAIndex = startHistory.firstIndex(where: { $0 == lca }), 219 | let endLCAIndex = endHistory.firstIndex(where: { $0 == lca }) 220 | else { 221 | return [] 222 | } 223 | 224 | let startToLCAPath = startHistory[startLCAIndex...] 225 | .reversed() // reverse since history is in ascending order 226 | .dropLast() // drop LCA; to be included in the next array 227 | .map { (PathDirection.reverse, $0) } 228 | 229 | let LCAtoEndPath = endHistory[endLCAIndex...] 230 | .map { (PathDirection.forward, $0) } 231 | 232 | results = startToLCAPath + LCAtoEndPath 233 | } 234 | 235 | return results 236 | } 237 | 238 | /// The direction of the ``MoveTree`` path. 239 | public enum PathDirection: Sendable { 240 | /// Move forward (i.e. perform a move). 241 | case forward 242 | /// Move backward (i.e. undo a move). 243 | case reverse 244 | } 245 | 246 | /// Whether the tree is empty or not. 247 | public var isEmpty: Bool { 248 | root == nil 249 | } 250 | 251 | /// Annotates the move at the provided index. 252 | /// 253 | /// - parameter index: The index of the move to annotate. 254 | /// - parameter assessment: The assessment to annotate the move with. 255 | /// - parameter comment: The comment to annotate the move with. 256 | /// 257 | /// - returns: The move updated with the given annotations. 258 | /// 259 | @discardableResult 260 | public mutating func annotate( 261 | moveAt index: Index, 262 | assessment: Move.Assessment = .null, 263 | comment: String = "" 264 | ) -> Move? { 265 | Self.nodeLock.withLock { 266 | dictionary[index]?.move.assessment = assessment 267 | dictionary[index]?.move.comment = comment 268 | } 269 | return dictionary[index]?.move 270 | } 271 | 272 | /// Annotates the position at the provided index. 273 | /// 274 | /// - parameter index: The index of the position to annotate. 275 | /// - parameter assessment: The assessment to annotate the position with. 276 | /// 277 | /// This value is stored in the move tree to generate an accurate 278 | /// PGN representation with `MoveTree.pgnRepresentation`. 279 | /// 280 | public mutating func annotate( 281 | positionAt index: Index, 282 | assessment: Position.Assessment 283 | ) { 284 | Self.nodeLock.withLock { 285 | dictionary[index]?.positionAssessment = assessment 286 | } 287 | } 288 | 289 | // MARK: - PGN 290 | 291 | /// An element for representing the ``MoveTree`` in 292 | /// PGN (Portable Game Notation) format. 293 | public enum PGNElement: Hashable, Sendable { 294 | /// e.g. `1.` 295 | case whiteNumber(Int) 296 | /// e.g. `1...` 297 | case blackNumber(Int) 298 | /// e.g. `e4` 299 | case move(Move, Index) 300 | /// e.g. `$10` 301 | case positionAssessment(Position.Assessment) 302 | /// e.g. `(` 303 | case variationStart 304 | /// e.g. `)` 305 | case variationEnd 306 | } 307 | 308 | private func pgn(for node: Node?) -> [PGNElement] { 309 | guard let node else { return [] } 310 | var result: [PGNElement] = [] 311 | 312 | switch node.index.color { 313 | case .white: 314 | result.append(.whiteNumber(node.index.number)) 315 | case .black: 316 | result.append(.blackNumber(node.index.number)) 317 | } 318 | 319 | result.append(.move(node.move, node.index)) 320 | if node.positionAssessment != .null { 321 | result.append(.positionAssessment(node.positionAssessment)) 322 | } 323 | 324 | var iterator = node.next?.makeIterator() 325 | var previousIndex = node.index 326 | 327 | while let currentNode = iterator?.next() { 328 | let currentIndex = currentNode.index 329 | 330 | switch (previousIndex.number, currentIndex.number) { 331 | case let (x, y) where x < y: 332 | result.append(.whiteNumber(currentIndex.number)) 333 | default: 334 | break 335 | } 336 | 337 | result.append(.move(currentNode.move, currentIndex)) 338 | 339 | if currentNode.positionAssessment != .null { 340 | result.append(.positionAssessment(currentNode.positionAssessment)) 341 | } 342 | 343 | // recursively generate PGN for all child nodes 344 | currentNode.previous?.children.forEach { child in 345 | result.append(.variationStart) 346 | result.append(contentsOf: pgn(for: child)) 347 | result.append(.variationEnd) 348 | } 349 | 350 | previousIndex = currentIndex 351 | } 352 | 353 | return result 354 | } 355 | 356 | /// Returns the ``MoveTree`` as an array of PGN 357 | /// (Portable Game Format) elements. 358 | public var pgnRepresentation: [PGNElement] { 359 | pgn(for: root) 360 | } 361 | 362 | } 363 | 364 | // MARK: - Equatable 365 | extension MoveTree: Equatable { 366 | 367 | public static func == (lhs: MoveTree, rhs: MoveTree) -> Bool { 368 | lhs.dictionary == rhs.dictionary 369 | } 370 | 371 | } 372 | 373 | // MARK: - Node 374 | extension MoveTree { 375 | 376 | /// Object that represents a node in the move tree. 377 | class Node: Hashable, @unchecked Sendable, Sequence { 378 | 379 | /// The move for this node. 380 | var move: Move 381 | /// The position assessment for this node. 382 | var positionAssessment = Position.Assessment.null 383 | /// The index for this node. 384 | fileprivate(set) var index = Index.minimum 385 | /// The previous node. 386 | fileprivate(set) var previous: Node? 387 | /// The next node. 388 | fileprivate(set) weak var next: Node? 389 | /// Children nodes (i.e. variation moves). 390 | fileprivate var children: [Node] = [] 391 | 392 | fileprivate init(move: Move) { 393 | self.move = move 394 | } 395 | 396 | // MARK: Equatable 397 | static func == (lhs: Node, rhs: Node) -> Bool { 398 | lhs.index == rhs.index && lhs.move == rhs.move 399 | } 400 | 401 | // MARK: Hashable 402 | func hash(into hasher: inout Hasher) { 403 | hasher.combine(move) 404 | hasher.combine(index) 405 | hasher.combine(previous) 406 | hasher.combine(next) 407 | hasher.combine(children) 408 | } 409 | 410 | // MARK: Sequence 411 | func makeIterator() -> NodeIterator { 412 | .init(start: self) 413 | } 414 | 415 | } 416 | 417 | struct NodeIterator: IteratorProtocol { 418 | private var current: Node? 419 | 420 | init(start: Node?) { 421 | current = start 422 | } 423 | 424 | mutating func next() -> Node? { 425 | defer { current = current?.next } 426 | return current 427 | } 428 | } 429 | 430 | } 431 | -------------------------------------------------------------------------------- /Sources/ChessKit/Parsers/EngineLANParser+Regex.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EngineLANParser+Regex.swift 3 | // ChessKit 4 | // 5 | 6 | extension EngineLANParser { 7 | 8 | /// Contains useful regex strings for engine LAN parsing. 9 | struct Pattern { 10 | static let move = #"^([a-h][1-8]){2}[qrbn]?$"# 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /Sources/ChessKit/Parsers/EngineLANParser.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LANParser.swift 3 | // ChessKit 4 | // 5 | 6 | /// Parses and converts the Long Algebraic Notation (LAN) 7 | /// of a chess move used by chess engines. 8 | /// 9 | /// This notation omits the piece type and any indication 10 | /// for special move types such as captures, castling, checks, etc. 11 | /// 12 | /// Examples: 13 | /// - e2e4 14 | /// - e7e5 15 | /// - e1g1 (white short castling) 16 | /// - e7e8q (for promotion) 17 | /// 18 | /// See [UCI protocol documentation](https://backscattering.de/chess/uci/2006-04.txt) 19 | /// for more information. 20 | public enum EngineLANParser { 21 | 22 | // MARK: Public 23 | 24 | /// Parses a LAN string and returns a move. 25 | /// 26 | /// - parameter lan: The (engine) LAN string of a move. 27 | /// - parameter color: The color of the piece being moved. 28 | /// - parameter position: The current chess position to make the move from. 29 | /// - returns: A Swift representation of a move, or `nil` if the 30 | /// LAN is invalid. 31 | /// 32 | /// This parser does not look for checks or checkmates, 33 | /// i.e. the move's `checkState` will always be `.none`. 34 | public static func parse( 35 | move lan: String, 36 | for color: Piece.Color, 37 | in position: Position 38 | ) -> Move? { 39 | guard isValid(lan: lan) else { return nil } 40 | 41 | let startSquareIndex = lan.index(lan.startIndex, offsetBy: 2) 42 | let startSquareString = String(lan[.. String { 95 | move.start.notation + move.end.notation + (move.promotedPiece?.fen.lowercased() ?? "") 96 | } 97 | 98 | // MARK: Private 99 | 100 | /// Returns whether the provided engine LAN is valid. 101 | /// 102 | /// - parameter lan: The LAN string to check. 103 | /// - returns: Whether the LAN is valid. 104 | /// 105 | private static func isValid(lan: String) -> Bool { 106 | lan.range(of: EngineLANParser.Pattern.move, options: .regularExpression) != nil 107 | } 108 | 109 | } 110 | 111 | private extension Castling { 112 | 113 | init?(engineLAN: String) { 114 | switch engineLAN { 115 | case "e1g1": self = .wK 116 | case "e1c1": self = .wQ 117 | case "e8g8": self = .bK 118 | case "e8c8": self = .bQ 119 | default: return nil 120 | } 121 | } 122 | 123 | } 124 | -------------------------------------------------------------------------------- /Sources/ChessKit/Parsers/FENParser.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FENParser.swift 3 | // ChessKit 4 | // 5 | 6 | /// Parses and converts the Forsyth–Edwards Notation (FEN) 7 | /// of a chess position. 8 | /// 9 | /// For example, the standard starting position is represented as: 10 | /// ``` 11 | /// "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1" 12 | /// ``` 13 | public enum FENParser { 14 | 15 | /// Number of components in FEN 16 | /// 1. Piece placement 17 | /// 2. Side to move 18 | /// 3. Castling ability 19 | /// 4. En Passant 20 | /// 5. Halfmoves 21 | /// 6. Fullmoves 22 | private static let componentCount = 6 23 | 24 | /// Parses a FEN string and returns a position. 25 | /// 26 | /// - parameter fen: The FEN string of a chess position. 27 | /// - returns: A Swift representation of the chess position, 28 | /// or `nil` if the FEN is invalid. 29 | /// 30 | public static func parse(fen: String) -> Position? { 31 | let sections = fen.components(separatedBy: .whitespaces) 32 | 33 | guard sections.count == FENParser.componentCount else { 34 | return nil 35 | } 36 | 37 | // piece placement 38 | 39 | let piecePlacementByRank = sections[0] 40 | .components(separatedBy: "/") 41 | .enumerated() 42 | 43 | let pieces = piecePlacementByRank.compactMap { (index, rankString) -> [Piece]? in 44 | let piecesInRank = rankString.map(String.init) 45 | let rank = Square.Rank(Square.Rank.range.upperBound - index) 46 | 47 | let digits = Square.Rank.range.map(String.init) 48 | var fileNumber = 0 49 | 50 | return piecesInRank.compactMap { (s: String) -> Piece? in 51 | if digits.contains(s), let numberOfEmptySpaces = Int(s) { 52 | fileNumber += numberOfEmptySpaces 53 | return nil 54 | } else { 55 | fileNumber += 1 56 | let file = Square.File(fileNumber) 57 | let square = Square(file, rank) 58 | return Piece(fen: s, square: square) 59 | } 60 | } 61 | }.flatMap { $0 } 62 | 63 | // side to move 64 | 65 | let sideToMove = Piece.Color(rawValue: sections[1]) ?? .white 66 | 67 | // castling ability 68 | 69 | var legalCastlings: [Castling] = [] 70 | let castlingAbility = sections[2].map(String.init) 71 | 72 | if castlingAbility.contains("k") { legalCastlings.append(.bK) } 73 | if castlingAbility.contains("K") { legalCastlings.append(.wK) } 74 | if castlingAbility.contains("q") { legalCastlings.append(.bQ) } 75 | if castlingAbility.contains("Q") { legalCastlings.append(.wQ) } 76 | 77 | // en passant target square 78 | 79 | var enPassant: EnPassant? 80 | 81 | let ep = sections[3] 82 | 83 | if ep != "-" && ep.count == 2 { 84 | let epFile = Square.File(rawValue: ep.map(String.init)[0]) 85 | let epRank = Int(ep.map(String.init)[1]) 86 | 87 | if let epRank, epRank == 3, let epFile = epFile { 88 | enPassant = EnPassant(pawn: Piece(.pawn, color: .white, square: Square(epFile, Square.Rank(epRank + 1)))) 89 | } else if let epRank, epRank == 6, let epFile { 90 | enPassant = EnPassant(pawn: Piece(.pawn, color: .black, square: Square(epFile, Square.Rank(epRank - 1)))) 91 | } 92 | } 93 | 94 | // clock 95 | 96 | var clock = Clock() 97 | 98 | if let halfmove = Int(sections[4]), let fullmove = Int(sections[5]) { 99 | clock = Clock(halfmoves: halfmove, fullmoves: fullmove) 100 | } 101 | 102 | // final position 103 | 104 | return Position( 105 | pieces: pieces, 106 | sideToMove: sideToMove, 107 | legalCastlings: LegalCastlings(legal: legalCastlings), 108 | enPassant: enPassant, 109 | clock: clock 110 | ) 111 | } 112 | 113 | /// Converts a ``Position`` object into a FEN string. 114 | /// 115 | /// - parameter position: The chess position to convert. 116 | /// - returns: A string containing the FEN of `position`. 117 | /// 118 | public static func convert(position: Position) -> String { 119 | var fen = "" 120 | 121 | // piece position 122 | 123 | for r in Square.Rank.range.reversed() { 124 | let rank = Square.Rank(r) 125 | var emptySpaceCounter = 0 126 | 127 | for file in Square.File.allCases { 128 | if let piece = position.piece(at: Square(file, rank)) { 129 | if emptySpaceCounter > 0 { 130 | fen += "\(emptySpaceCounter)" 131 | } 132 | 133 | fen += piece.fen 134 | emptySpaceCounter = 0 135 | } else { 136 | emptySpaceCounter += 1 137 | } 138 | } 139 | 140 | if emptySpaceCounter > 0 { 141 | fen += "\(emptySpaceCounter)" 142 | emptySpaceCounter = 0 143 | } 144 | 145 | fen += "/" 146 | } 147 | 148 | // remove extra `/` 149 | fen.removeLast() 150 | 151 | fen += " " 152 | 153 | // side to move 154 | 155 | fen += position.sideToMove.rawValue + " " 156 | 157 | // castling ability 158 | 159 | fen += position.legalCastlings.fen + " " 160 | 161 | // en passant 162 | 163 | if let enPassant = position.enPassant { 164 | fen += "\(enPassant.captureSquare) " 165 | } else { 166 | fen += "- " 167 | } 168 | 169 | // clock 170 | 171 | fen += "\(position.clock.halfmoves) \(position.clock.fullmoves)" 172 | 173 | return fen 174 | } 175 | 176 | } 177 | -------------------------------------------------------------------------------- /Sources/ChessKit/Parsers/PGNParser/PGNParser+Deprecated.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PGNParser+Deprecated.swift 3 | // ChessKit 4 | // 5 | 6 | import Foundation 7 | 8 | /// Parses and converts the Portable Game Notation (PGN) 9 | /// of a chess game. 10 | extension PGNParser { 11 | 12 | /// Contains the contents of a single parsed move pair. 13 | private struct ParsedMove { 14 | /// The number of the move within the game. 15 | let number: Int 16 | /// The white move SAN string, annotation, and comment. 17 | let whiteMove: (san: String, annotation: Move.Assessment, comment: String) 18 | /// The black move SAN string, annotation, and comment (can be `nil`). 19 | let blackMove: (san: String, annotation: Move.Assessment, comment: String)? 20 | /// The result of the game, if applicable. 21 | let result: String? 22 | } 23 | 24 | // MARK: Public 25 | 26 | /// Parses a PGN string and returns a game. 27 | /// 28 | /// - parameter pgn: The PGN string of a chess game. 29 | /// - parameter position: The starting position of the chess game. 30 | /// Defaults to the standard position. 31 | /// - returns: A Swift representation of the chess game. 32 | /// 33 | @available(*, deprecated, renamed: "parse(game:)") 34 | public static func parse( 35 | game pgn: String, 36 | startingWith position: Position = .standard 37 | ) -> Game { 38 | let processedPGN = 39 | pgn 40 | .replacingOccurrences(of: "\n", with: " ") 41 | .replacingOccurrences(of: "\r", with: " ") 42 | 43 | let range = NSRange(0..= 3 else { return nil } 60 | let key = match.range(at: 1) 61 | let value = match.range(at: 2) 62 | 63 | return ( 64 | NSString(string: tag).substring(with: key) 65 | .trimmingCharacters(in: .whitespacesAndNewlines), 66 | NSString(string: tag).substring(with: value) 67 | .trimmingCharacters(in: .whitespacesAndNewlines) 68 | ) 69 | } 70 | 71 | let parsedTags = parsed(tags: Dictionary(tags ?? []) { a, _ in a }) 72 | 73 | // movetext 74 | 75 | let moveText: [String] 76 | 77 | moveText = try! NSRegularExpression(pattern: Pattern.moveText) 78 | .matches(in: processedPGN, range: range) 79 | .map { 80 | NSString(string: pgn).substring(with: $0.range) 81 | .trimmingCharacters(in: .whitespacesAndNewlines) 82 | } 83 | 84 | let parsedMoves = moveText.compactMap { move -> ParsedMove? in 85 | let range = NSRange(0..= 1 && m.count <= 2 96 | else { return nil } 97 | 98 | let whiteMove = 99 | try? NSRegularExpression(pattern: PGNParser.Pattern.fullMove) 100 | .matches(in: m[0], range: NSRange(0.. Game.Tags { 226 | var gameTags = Game.Tags() 227 | 228 | tags.forEach { key, value in 229 | switch key.lowercased() { 230 | case "event": gameTags.event = value 231 | case "site": gameTags.site = value 232 | case "date": gameTags.date = value 233 | case "round": gameTags.round = value 234 | case "white": gameTags.white = value 235 | case "black": gameTags.black = value 236 | case "result": gameTags.result = value 237 | case "annotator": gameTags.annotator = value 238 | case "plycount": gameTags.plyCount = value 239 | case "timecontrol": gameTags.timeControl = value 240 | case "time": gameTags.time = value 241 | case "termination": gameTags.termination = value 242 | case "mode": gameTags.mode = value 243 | case "fen": gameTags.fen = value 244 | case "setup": gameTags.setUp = value 245 | default: gameTags.other[key] = value 246 | } 247 | } 248 | 249 | return gameTags 250 | } 251 | 252 | /// Contains useful regex strings for PGN parsing. 253 | private struct Pattern { 254 | // tag pair components 255 | static let tags = #"\[[^\]]+\]"# 256 | static let tagPair = #"\[([^"]+?)\s"([^"]+)"\]"# 257 | 258 | // move text 259 | static let moveText = "\\d{1,}\\.{1,3}\\s?(\(fullMove)([\\?!]{1,2})?(\\s?\\$\\d)?(\\s?\\{.+?\\})?(\\s(1-0|0-1|1\\/2-1\\/2|\\*)\\s*$)?\\s?){1,2}" 260 | static let moveNumber = #"^\d{1,}"# 261 | static let fullMove = #"([Oo0]-[Oo0](-[Oo0])?|[KQRBN]?[a-h]?[1-8]?x?[a-h][1-8](\=[QRBN])?[+#]?)"# 262 | static let annotatedMove = "\(PGNParser.Pattern.fullMove)(\\s?\(annotation))?(\\s?\(comment))?" 263 | static let result = #"(\s(1-0|0-1|1\/2-1\/2|\*)\s?){1}$"# 264 | 265 | // move pair components 266 | static let annotation = #"\$\d"# 267 | static let comment = #"\{.+?\}"# 268 | } 269 | 270 | } 271 | -------------------------------------------------------------------------------- /Sources/ChessKit/Parsers/PGNParser/PGNParser+MoveText.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PGNParser+MoveText.swift 3 | // ChessKit 4 | // 5 | 6 | import Foundation 7 | 8 | extension PGNParser { 9 | /// Parses PGN movetext. 10 | enum MoveTextParser { 11 | 12 | // MARK: Internal 13 | 14 | static func game( 15 | from moveText: String, 16 | startingPosition: Position 17 | ) throws(PGNParser.Error) -> Game { 18 | let moveTextTokens = try MoveTextParser.tokenize( 19 | moveText: moveText 20 | ) 21 | 22 | return try MoveTextParser.parse(tokens: moveTextTokens, startingWith: startingPosition) 23 | } 24 | 25 | // MARK: Private 26 | 27 | private static func tokenize(moveText: String) throws(PGNParser.Error) -> [Token] { 28 | let inlineMoveText = moveText.components(separatedBy: .newlines).joined(separator: "") 29 | var iterator = inlineMoveText.makeIterator() 30 | 31 | var tokens = [Token]() 32 | var currentTokenType = TokenType.none 33 | var currentToken = "" 34 | 35 | while let c = iterator.next() { 36 | if c == "{" { 37 | currentTokenType = .comment 38 | } else if c == "}" { 39 | if currentTokenType != .comment { 40 | throw .unpairedCommentDelimiter 41 | } else { 42 | if !currentToken.isEmpty, let token = currentTokenType.convert(currentToken) { 43 | tokens.append(token) 44 | } 45 | 46 | currentTokenType = .none 47 | } 48 | } else if currentTokenType == .comment || currentTokenType.isValid(character: c) { 49 | currentToken += String(c) 50 | } else { 51 | if !currentToken.isEmpty, let token = currentTokenType.convert(currentToken) { 52 | tokens.append(token) 53 | } 54 | 55 | currentTokenType = .match(character: c) 56 | currentToken = String(c) 57 | } 58 | } 59 | 60 | if !currentToken.isEmpty, let token = currentTokenType.convert(currentToken) { 61 | tokens.append(token) 62 | } 63 | 64 | return tokens 65 | } 66 | 67 | private static func parse( 68 | tokens: [Token], 69 | startingWith position: Position 70 | ) throws(PGNParser.Error) -> Game { 71 | var game = Game(startingWith: position) 72 | var iterator = tokens.makeIterator() 73 | 74 | var currentToken = iterator.next() 75 | var currentMoveIndex: MoveTree.Index 76 | 77 | // determine if first move is white or black 78 | 79 | if case let .number(number) = currentToken, let n = Int(number.prefix { $0 != "." }) { 80 | if number.filter({ $0 == "." }).count >= 3 { 81 | currentMoveIndex = .init(number: n, color: .black).previous 82 | } else { 83 | currentMoveIndex = .init(number: n, color: .white).previous 84 | } 85 | } else if case let .san(san) = currentToken { 86 | currentMoveIndex = position.sideToMove == .white ? .minimum : .minimum.next 87 | if let position = game.positions[currentMoveIndex] { 88 | if let move = SANParser.parse(move: san, in: position) { 89 | currentMoveIndex = game.make(move: move, from: currentMoveIndex) 90 | } else { 91 | throw .invalidMove(san) 92 | } 93 | } 94 | } else { 95 | throw .unexpectedMoveTextToken 96 | } 97 | 98 | // iterate through remaining tokens 99 | 100 | var variationStack = Stack() 101 | 102 | while let token = iterator.next() { 103 | currentToken = token 104 | 105 | switch currentToken { 106 | case .none, .number, .result: 107 | break 108 | case let .san(san): 109 | if let position = game.positions[currentMoveIndex], 110 | let move = SANParser.parse(move: san, in: position) 111 | { 112 | currentMoveIndex = game.make(move: move, from: currentMoveIndex) 113 | } else { 114 | throw .invalidMove(san) 115 | } 116 | case let .annotation(annotation): 117 | if let rawValue = firstMatch( 118 | in: annotation, for: .numericPosition 119 | ), let positionAssessment = Position.Assessment(rawValue: rawValue) { 120 | game.annotate( 121 | positionAt: currentMoveIndex, 122 | assessment: positionAssessment 123 | ) 124 | continue 125 | } 126 | 127 | var moveAssessment: Move.Assessment? 128 | 129 | if let notation = firstMatch(in: annotation, for: .traditional) { 130 | moveAssessment = .init(notation: notation) 131 | } else if let rawValue = firstMatch(in: annotation, for: .numericMove) { 132 | moveAssessment = .init(rawValue: rawValue) 133 | } else { 134 | throw .invalidAnnotation(annotation) 135 | } 136 | 137 | if let moveAssessment { 138 | game.annotate(moveAt: currentMoveIndex, assessment: moveAssessment) 139 | } else { 140 | throw .invalidAnnotation(annotation) 141 | } 142 | case let .comment(comment): 143 | game.annotate(moveAt: currentMoveIndex, comment: comment) 144 | case .variationStart: 145 | variationStack.push(currentMoveIndex) 146 | currentMoveIndex = currentMoveIndex.previous 147 | case .variationEnd: 148 | if let index = variationStack.pop() { 149 | currentMoveIndex = index 150 | } else { 151 | throw .unpairedVariationDelimiter 152 | } 153 | } 154 | } 155 | 156 | return game 157 | } 158 | 159 | private static func firstMatch(in string: String, for pattern: Pattern) -> String? { 160 | let matches = try? NSRegularExpression(pattern: pattern.rawValue) 161 | .matches(in: string, range: NSRange(0.. Bool { 205 | character.isWholeNumber || character == "." 206 | } 207 | 208 | static func isSAN(_ character: Character) -> Bool { 209 | character.isLetter || character.isWholeNumber || ["x", "+", "#", "=", "O", "o", "0", "-"].contains(character) 210 | } 211 | 212 | static func isAnnotation(_ character: Character) -> Bool { 213 | character.isWholeNumber || ["$", "?", "!", "□"].contains(character) 214 | } 215 | 216 | static func isVariationStart(_ character: Character) -> Bool { 217 | character == "(" 218 | } 219 | 220 | static func isVariationEnd(_ character: Character) -> Bool { 221 | character == ")" 222 | } 223 | 224 | static func isResult(_ character: Character) -> Bool { 225 | ["1", "2", "/", "-", "0", "*"].contains(character) 226 | } 227 | 228 | func isValid(character: Character) -> Bool { 229 | switch self { 230 | // .comment is omitted from these checks because 231 | // it is handled separately by checking for { } delimiters 232 | case .none, .comment: false 233 | case .number: Self.isNumber(character) 234 | case .san: Self.isSAN(character) 235 | case .annotation: Self.isAnnotation(character) 236 | case .variationStart: Self.isVariationStart(character) 237 | case .variationEnd: Self.isVariationEnd(character) 238 | case .result: Self.isResult(character) 239 | } 240 | } 241 | 242 | static func match(character: Character) -> Self { 243 | if isNumber(character) { 244 | .number 245 | } else if isSAN(character) { 246 | .san 247 | } else if isAnnotation(character) { 248 | .annotation 249 | } else if isVariationStart(character) { 250 | .variationStart 251 | } else if isVariationEnd(character) { 252 | .variationEnd 253 | } else if isResult(character) { 254 | .result 255 | } else { 256 | // .comment is omitted from these checks because 257 | // it is handled separately by checking for { } delimiters 258 | .none 259 | } 260 | } 261 | 262 | func convert(_ text: String) -> Token? { 263 | switch self { 264 | case .none: nil 265 | case .number: .number(text.trimmingCharacters(in: .whitespaces)) 266 | case .san: .san(text.trimmingCharacters(in: .whitespaces)) 267 | case .annotation: .annotation(text.trimmingCharacters(in: .whitespaces)) 268 | case .comment: .comment(text.trimmingCharacters(in: .whitespaces)) 269 | case .variationStart: .variationStart 270 | case .variationEnd: .variationEnd 271 | case .result: .result(text.trimmingCharacters(in: .whitespaces)) 272 | } 273 | } 274 | } 275 | } 276 | -------------------------------------------------------------------------------- /Sources/ChessKit/Parsers/PGNParser/PGNParser+Tags.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PGNParser+Tags.swift 3 | // ChessKit 4 | // 5 | 6 | extension PGNParser { 7 | /// Parses PGN tag pairs. 8 | enum PGNTagParser { 9 | 10 | // MARK: Interal 11 | 12 | static func gameTags(from tagString: String) throws(PGNParser.Error) -> Game.Tags { 13 | var gameTags = Game.Tags() 14 | 15 | try parse(tags: tagString).forEach { key, value in 16 | switch key.lowercased() { 17 | case "event": gameTags.event = value 18 | case "site": gameTags.site = value 19 | case "date": gameTags.date = value 20 | case "round": gameTags.round = value 21 | case "white": gameTags.white = value 22 | case "black": gameTags.black = value 23 | case "result": gameTags.result = value 24 | case "annotator": gameTags.annotator = value 25 | case "plycount": gameTags.plyCount = value 26 | case "timecontrol": gameTags.timeControl = value 27 | case "time": gameTags.time = value 28 | case "termination": gameTags.termination = value 29 | case "mode": gameTags.mode = value 30 | case "fen": gameTags.fen = value 31 | case "setup": gameTags.setUp = value 32 | default: gameTags.other[key] = value 33 | } 34 | } 35 | 36 | return gameTags 37 | } 38 | 39 | // MARK: Private 40 | 41 | private static func tokenize(tags: String) throws(PGNParser.Error) -> [Token] { 42 | let inlineTags = tags.components(separatedBy: .newlines).joined(separator: "") 43 | var iterator = inlineTags.makeIterator() 44 | 45 | var tokens = [Token]() 46 | var state = TokenizationState() 47 | var symbol = "" 48 | var string = "" 49 | 50 | while let c = iterator.next() { 51 | if c == "[" { 52 | tokens.append(.openBracket) 53 | } else if c == "]" { 54 | tokens.append(.closeBracket) 55 | } else if c.isOpenQuote && !state.quoteOpened { 56 | if !symbol.isEmpty { 57 | tokens.append(.symbol(symbol)) 58 | symbol = "" 59 | } 60 | state.quoteOpened = true 61 | } else if c.isCloseQuote && state.quoteOpened { 62 | if !string.isEmpty { 63 | tokens.append(.string(string)) 64 | string = "" 65 | } 66 | state.quoteOpened = false 67 | } else { 68 | if c.isWhitespace && !state.quoteOpened { 69 | if !symbol.isEmpty { 70 | tokens.append(.symbol(symbol)) 71 | symbol = "" 72 | } 73 | } else if state.quoteOpened { 74 | string += String(c) 75 | } else { 76 | if c.isLetter || c.isNumber || c == "_" { 77 | symbol += String(c) 78 | } else { 79 | throw .unexpectedTagCharacter(String(c)) 80 | } 81 | } 82 | } 83 | } 84 | 85 | return tokens 86 | } 87 | 88 | private static func parse(tags: String) throws(PGNParser.Error) -> [String: String] { 89 | let tokens = try tokenize(tags: tags) 90 | 91 | guard tokens.count % 4 == 0 else { 92 | throw .invalidTagFormat 93 | } 94 | 95 | typealias TokenGroup = (Token, Token, Token, Token) 96 | let groupedTokens: [TokenGroup] = stride(from: 0, to: tokens.count, by: 4).map { 97 | (tokens[$0], tokens[$0 + 1], tokens[$0 + 2], tokens[$0 + 3]) 98 | } 99 | 100 | var parsedTags = [(String, String)]() 101 | 102 | for token in groupedTokens { 103 | guard token.0 == .openBracket && token.3 == .closeBracket else { 104 | throw .mismatchedTagBrackets 105 | } 106 | 107 | guard case let .symbol(symbol) = token.1 else { 108 | throw .tagSymbolNotFound 109 | } 110 | 111 | guard case let .string(string) = token.2 else { 112 | throw .tagStringNotFound 113 | } 114 | 115 | parsedTags.append((symbol, string)) 116 | } 117 | 118 | return Dictionary(parsedTags) { first, _ in first } 119 | } 120 | 121 | } 122 | } 123 | 124 | // MARK: - Tokens 125 | private extension PGNParser.PGNTagParser { 126 | /// Represents a tag pair token. 127 | /// 128 | /// The format of a tag pair is: 129 | /// ` "" ` 130 | /// with 0 or more whitespaces between tokens. 131 | enum Token: Equatable { 132 | case openBracket 133 | case symbol(String) 134 | case string(String) 135 | case closeBracket 136 | } 137 | 138 | struct TokenizationState { 139 | var bracketOpened = false 140 | var quoteOpened = false 141 | } 142 | } 143 | 144 | private extension Character { 145 | 146 | var isOpenQuote: Bool { 147 | ["\"", "“"].contains(self) 148 | } 149 | 150 | var isCloseQuote: Bool { 151 | ["\"", "”"].contains(self) 152 | } 153 | 154 | } 155 | -------------------------------------------------------------------------------- /Sources/ChessKit/Parsers/PGNParser/PGNParser.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PGNParser.swift 3 | // ChessKit 4 | // 5 | 6 | import Foundation 7 | 8 | /// Parses and converts the Portable Game Notation (PGN) 9 | /// of a chess game. 10 | public enum PGNParser { 11 | 12 | // MARK: Public 13 | 14 | /// Parses a PGN string and returns a game. 15 | /// 16 | /// - parameter pgn: The PGN string of a chess game. 17 | /// - returns: A Swift representation of the chess game. 18 | /// - throws: ``Error`` indicating the first error 19 | /// encountered while parsing `pgn`. 20 | /// 21 | /// The parsing implementation is based on the [PGN Standard](https://www.saremba.de/chessgml/standards/pgn/pgn-complete.htm)'s 22 | /// import format. 23 | /// 24 | /// The starting position is read from the `FEN` tag if 25 | /// the `SetUp` tag is set to `1`. Otherwise the standard 26 | /// starting position is assumed. 27 | /// 28 | public static func parse(game pgn: String) throws(Error) -> Game { 29 | // initial processing 30 | 31 | let lines = pgn.components(separatedBy: .newlines) 32 | .map { $0.trimmingCharacters(in: .whitespaces) } 33 | // lines beginning with % are ignored 34 | .filter { $0.prefix(1) != "%" } 35 | 36 | let sections = lines.split(separator: "").map(Array.init) 37 | 38 | guard sections.count <= 2 else { throw .tooManyLineBreaks } 39 | guard let firstSection = sections.first else { return Game() } 40 | 41 | let tagPairLines = sections.count == 2 ? firstSection : [] 42 | let moveTextLines = sections.count == 2 ? sections[1] : firstSection 43 | 44 | // parse tags 45 | 46 | let tags = try PGNTagParser.gameTags(from: tagPairLines.joined()) 47 | 48 | // parse movetext 49 | 50 | var game = try MoveTextParser.game( 51 | from: moveTextLines.joined(separator: " "), 52 | startingPosition: try startingPosition(from: tags) 53 | ) 54 | 55 | // return game with tags + movetext 56 | 57 | game.tags = tags 58 | return game 59 | } 60 | 61 | /// Converts a ``Game`` object into a PGN string. 62 | /// 63 | /// - parameter game: The chess game to convert. 64 | /// - returns: A string containing the PGN of `game`. 65 | /// 66 | /// The conversion implementation is based on the [PGN Standard](https://www.saremba.de/chessgml/standards/pgn/pgn-complete.htm)'s 67 | /// export format. 68 | /// 69 | public static func convert(game: Game) -> String { 70 | var pgn = "" 71 | 72 | // tags 73 | 74 | game.tags.all 75 | .map(\.pgn) 76 | .filter { !$0.isEmpty } 77 | .forEach { pgn += $0 + "\n" } 78 | 79 | game.tags.other.sorted(by: <).forEach { key, value in 80 | pgn += "[\(key) \"\(value)\"]\n" 81 | } 82 | 83 | if !pgn.isEmpty { 84 | pgn += "\n" // extra line between tags and movetext 85 | } 86 | 87 | // movetext 88 | 89 | for element in game.moves.pgnRepresentation { 90 | switch element { 91 | case let .whiteNumber(number): 92 | pgn += "\(number). " 93 | case let .blackNumber(number): 94 | pgn += "\(number)... " 95 | case let .move(move, _): 96 | pgn += movePGN(for: move) 97 | case let .positionAssessment(assessment): 98 | pgn += "\(assessment.rawValue) " 99 | case .variationStart: 100 | pgn += "(" 101 | case .variationEnd: 102 | pgn = pgn.trimmingCharacters(in: .whitespaces) 103 | pgn += ") " 104 | } 105 | } 106 | 107 | pgn += game.tags.result 108 | 109 | return pgn.trimmingCharacters(in: .whitespaces) 110 | } 111 | 112 | // MARK: Private 113 | 114 | /// Generates starting position from `"SetUp"` and `"FEN"` tags. 115 | private static func startingPosition( 116 | from tags: Game.Tags 117 | ) throws(PGNParser.Error) -> Position { 118 | if tags.setUp == "1", let position = FENParser.parse(fen: tags.fen) { 119 | position 120 | } else if tags.setUp == "0" || (tags.setUp.isEmpty && tags.fen.isEmpty) { 121 | .standard 122 | } else { 123 | throw .invalidSetUpOrFEN 124 | } 125 | } 126 | 127 | /// Generates PGN string for the given `move` including assessments 128 | /// and comments. 129 | private static func movePGN(for move: Move) -> String { 130 | var result = "" 131 | 132 | result += "\(move.san) " 133 | 134 | if move.assessment != .null { 135 | result += "\(move.assessment.rawValue) " 136 | } 137 | 138 | if !move.comment.isEmpty { 139 | result += "{\(move.comment)} " 140 | } 141 | 142 | return result 143 | } 144 | 145 | } 146 | 147 | // MARK: - Error 148 | extension PGNParser { 149 | /// Possible errors returned by `PGNParser`. 150 | /// 151 | /// These errors are thrown when issues are encountered 152 | /// while scanning and parsing the provided PGN text. 153 | public enum Error: Swift.Error, Equatable { 154 | /// There are too many line breaks in the provided PGN. 155 | /// PGN should contain a single blank line between the 156 | /// tags and move text. 157 | case tooManyLineBreaks 158 | /// If included in the PGN's tag pairs, the `SetUp` tag must 159 | /// be set to either `"0"` or `"1"`. 160 | /// 161 | /// If `"0"`, the `FEN` tag must be blank. If `1`, the 162 | /// `FEN` tag must contain a valid FEN string representing 163 | /// the starting position of the game. 164 | /// 165 | /// - seealso: ``FENParser`` 166 | case invalidSetUpOrFEN 167 | 168 | // MARK: Tags 169 | /// Tags must be surrounded by brackets with an unquoted 170 | /// string (key) followed by a quoted string (value) inside. 171 | /// 172 | /// For example: `[Round "29"]` 173 | case invalidTagFormat 174 | /// Tags must have an open bracket (`[`) and a close bracket (`]`). 175 | /// If there is a close bracket without an open, this error 176 | /// will be thrown. 177 | case mismatchedTagBrackets 178 | /// Tag string (value) could not be parsed. 179 | case tagStringNotFound 180 | /// Tag symbol (key) could not be parsed. 181 | case tagSymbolNotFound 182 | /// Tag symbols must be either letters, numbers, or underscores (`_`). 183 | case unexpectedTagCharacter(String) 184 | 185 | // MARK: Move Text 186 | /// The move or position assessment annotation is invalid. 187 | case invalidAnnotation(String) 188 | /// The move SAN is invalid for the implied position given 189 | /// by its location within the PGN string. 190 | case invalidMove(String) 191 | /// The first item in a move text string must be either a 192 | /// number (e.g. `1.`) or a move SAN (e.g. `e4`). 193 | case unexpectedMoveTextToken 194 | /// Comments must be enclosed on both sides by braces (`{`, `}`). 195 | case unpairedCommentDelimiter 196 | /// Variations must be enclosed on both sides by parentheses (`(`, `)`). 197 | case unpairedVariationDelimiter 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /Sources/ChessKit/Parsers/SANParser+Regex.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SANParser+Regex.swift 3 | // ChessKit 4 | // 5 | 6 | extension SANParser { 7 | 8 | /// Contains useful regex strings for SAN parsing. 9 | struct Pattern { 10 | static let full = #"^([Oo0]-[Oo0](-[Oo0])?|[KQRBN]?[a-h]?[1-8]?x?[a-h][1-8](\=[QRBN])?[+#]?)$"# 11 | 12 | // piece kinds 13 | static let pawnFile = #"^[a-h]"# 14 | static let pieceKind = #"^[KQRBN]"# 15 | 16 | // castling 17 | static let shortCastle = #"^[Oo0]-[Oo0]\+?#?$"# 18 | static let longCastle = #"^[Oo0]-[Oo0]-[Oo0]\+?#?$"# 19 | 20 | // disambiguation 21 | static let disambiguation = #"[a-h]?[1-8]?(?=([a-h][1-8])$)"# 22 | static let rank = #"^[1-8]$"# 23 | static let file = #"^[a-h]$"# 24 | static let square = #"^[a-h][1-8]$"# 25 | 26 | // other 27 | static let promotion = #"\=[QRBN]"# 28 | static let targetSquare = #"([a-h][1-8])(?!([a-h][1-8]))"# 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /Sources/ChessKit/Parsers/SANParser.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SANParser.swift 3 | // ChessKit 4 | // 5 | 6 | /// Parses and converts the Standard Algebraic Notation (SAN) 7 | /// of a chess move. 8 | public enum SANParser { 9 | 10 | // MARK: Public 11 | 12 | /// Parses a SAN string and returns a move. 13 | /// 14 | /// - parameter san: The SAN string of a move. 15 | /// - parameter position: The current chess position to make the move from. 16 | /// - returns: A Swift representation of a move, or `nil` if the 17 | /// SAN is invalid. 18 | /// 19 | /// - note: Make sure the provided `position` has the correct `sideToMove` 20 | /// set or the parsing may fail due to invalid moves. 21 | /// 22 | public static func parse( 23 | move san: String, 24 | in position: Position 25 | ) -> Move? { 26 | guard isValid(san: san) else { return nil } 27 | 28 | let color = position.sideToMove 29 | var checkState = Move.CheckState.none 30 | 31 | if san.contains("#") { 32 | checkState = .checkmate 33 | } else if san.contains("+") { 34 | checkState = .check 35 | } 36 | 37 | // castling 38 | var castling: Castling? 39 | 40 | if san.range(of: Pattern.shortCastle, options: .regularExpression) != nil { 41 | castling = Castling(side: .king, color: color) 42 | } else if san.range(of: Pattern.longCastle, options: .regularExpression) != nil { 43 | castling = Castling(side: .queen, color: color) 44 | } 45 | 46 | if let castling { 47 | return Move( 48 | result: .castle(castling), 49 | piece: Piece(.king, color: color, square: castling.kingStart), 50 | start: castling.kingStart, 51 | end: castling.kingEnd, 52 | checkState: checkState 53 | ) 54 | } 55 | 56 | // pawns 57 | if let range = san.range(of: Pattern.pawnFile, options: .regularExpression), let end = targetSquare(for: san) { 58 | let startingFile = String(san[range]) 59 | 60 | let board = Board(position: position) 61 | let possiblePiece = position.pieces 62 | .filter { 63 | $0.kind == .pawn && $0.color == color && $0.square.file == Square.File(rawValue: startingFile) 64 | } 65 | .filter { 66 | board.canMove(pieceAt: $0.square, to: end) 67 | } 68 | .first 69 | 70 | guard var pawn = possiblePiece else { 71 | return nil 72 | } 73 | 74 | let start = pawn.square 75 | pawn.square = end 76 | 77 | var move: Move? 78 | 79 | if isCapture(san: san) { 80 | if let capturedPiece = position.piece(at: end) { 81 | move = Move(result: .capture(capturedPiece), piece: pawn, start: start, end: capturedPiece.square, checkState: checkState) 82 | } else if let ep = position.enPassant, ep.captureSquare == end { 83 | move = Move(result: .capture(ep.pawn), piece: pawn, start: start, end: end, checkState: checkState) 84 | } 85 | } else { 86 | move = Move(result: .move, piece: pawn, start: start, end: end, checkState: checkState) 87 | } 88 | 89 | if let promotionPieceKind = promotionPiece(for: san) { 90 | move?.promotedPiece = Piece(promotionPieceKind, color: color, square: end) 91 | } 92 | 93 | return move 94 | } 95 | 96 | // pieces 97 | guard let range = san.range(of: Pattern.pieceKind, options: .regularExpression), 98 | let pieceKind = Piece.Kind(rawValue: String(san[range])), 99 | let end = targetSquare(for: san) 100 | else { return nil } 101 | 102 | var move: Move? 103 | let disambiguation = self.disambiguation(for: san) 104 | 105 | let board = Board(position: position) 106 | let possiblePiece = position.pieces 107 | .filter { $0.kind == pieceKind && $0.color == color } 108 | .filter { 109 | board.canMove(pieceAt: $0.square, to: end) 110 | } 111 | .filter { 112 | switch disambiguation { 113 | case let .byFile(file): 114 | $0.square.file == file 115 | case let .byRank(rank): 116 | $0.square.rank == rank 117 | case let .bySquare(square): 118 | $0.square == square 119 | case .none: 120 | true 121 | } 122 | } 123 | .first 124 | 125 | guard var piece = possiblePiece else { 126 | return nil 127 | } 128 | 129 | let start = piece.square 130 | piece.square = end 131 | 132 | if isCapture(san: san), let capturedPiece = position.piece(at: end) { 133 | move = Move(result: .capture(capturedPiece), piece: piece, start: start, end: end, checkState: checkState) 134 | } else { 135 | move = Move(result: .move, piece: piece, start: start, end: end, checkState: checkState) 136 | } 137 | 138 | move?.disambiguation = disambiguation 139 | 140 | return move 141 | } 142 | 143 | /// Converts a ``Move`` object into a SAN string. 144 | /// 145 | /// - parameter move: The chess move to convert. 146 | /// - returns: A string containing the SAN of `move`. 147 | /// 148 | public static func convert(move: Move) -> String { 149 | switch move.result { 150 | case let .castle(castling): 151 | return "\(castling.side.notation)\(move.checkState.notation)" 152 | default: 153 | var pieceNotation = move.piece.kind.notation 154 | 155 | if move.piece.kind == .pawn, case .capture = move.result { 156 | pieceNotation = move.start.file.rawValue 157 | } 158 | 159 | var disambiguationNotation = "" 160 | 161 | if let disambiguation = move.disambiguation { 162 | switch disambiguation { 163 | case let .byFile(file): disambiguationNotation = file.rawValue 164 | case let .byRank(rank): disambiguationNotation = "\(rank.value)" 165 | case let .bySquare(square): disambiguationNotation = square.notation 166 | } 167 | } 168 | 169 | var captureNotation = "" 170 | 171 | if case .capture = move.result { 172 | captureNotation = "x" 173 | } 174 | 175 | var promotionNotation = "" 176 | 177 | if let promotedPiece = move.promotedPiece { 178 | promotionNotation = "=\(promotedPiece.kind.notation)" 179 | } 180 | 181 | return "\(pieceNotation)\(disambiguationNotation)\(captureNotation)\(move.end.notation)\(promotionNotation)\(move.checkState.notation)" 182 | } 183 | } 184 | 185 | // MARK: Private 186 | 187 | /// Returns whether the provided SAN is valid. 188 | /// 189 | /// - parameter san: The SAN string to check. 190 | /// - returns: Whether the SAN is valid. 191 | /// 192 | private static func isValid(san: String) -> Bool { 193 | san.range(of: SANParser.Pattern.full, options: .regularExpression) != nil 194 | } 195 | 196 | /// Returns the target square for a SAN move. 197 | /// 198 | /// - parameter san: The SAN represenation of a move. 199 | /// - returns: The square the move is targeting, or `nil` 200 | /// if the SAN is invalid. 201 | /// 202 | private static func targetSquare(for san: String) -> Square? { 203 | guard 204 | let range = san.range( 205 | of: Pattern.targetSquare, 206 | options: .regularExpression 207 | ) 208 | else { return nil } 209 | 210 | return Square(String(san[range])) 211 | } 212 | 213 | /// Checks if a SAN string contains a capture. 214 | /// 215 | /// - parameter san: The SAN represenation of a move. 216 | /// - returns: Whether or not the move represents a capture. 217 | /// 218 | private static func isCapture(san: String) -> Bool { 219 | san.contains("x") 220 | } 221 | 222 | /// Checks if a SAN string contains a promotion. 223 | /// 224 | /// - parameter san: The SAN represenation of a move. 225 | /// - returns: The kind of piece that is being promoted to, 226 | /// or `nil` if the SAN does not contain a promotion. 227 | /// 228 | private static func promotionPiece(for san: String) -> Piece.Kind? { 229 | guard let range = san.range(of: Pattern.promotion, options: .regularExpression) else { 230 | return nil 231 | } 232 | 233 | return Piece.Kind( 234 | rawValue: san[range].replacingOccurrences(of: "=", with: "") 235 | ) 236 | } 237 | 238 | /// Checks if a SAN string contains a disambiguation. 239 | /// 240 | /// - parameter san: The SAN represenation of a move. 241 | /// - returns: The disambiguation contained within the SAN, 242 | /// or `nil` if there is none. 243 | /// 244 | /// If multiple pieces of the same type can move to the target 245 | /// square, the SAN contains a disambiguating file, rank, or square 246 | /// so the piece that is moving can be determined. 247 | private static func disambiguation(for san: String) -> Move.Disambiguation? { 248 | guard let range = san.range(of: Pattern.disambiguation, options: .regularExpression) else { 249 | return nil 250 | } 251 | 252 | let value = String(san[range]) 253 | 254 | if let rankRange = value.range(of: Pattern.rank, options: .regularExpression), let rank = Int(String(value[rankRange])) { 255 | return .byRank(Square.Rank(rank)) 256 | } else if let fileRange = value.range(of: Pattern.file, options: .regularExpression), let file = Square.File(rawValue: String(value[fileRange])) { 257 | return .byFile(file) 258 | } else if let squareRange = value.range(of: Pattern.square, options: .regularExpression) { 259 | return .bySquare(Square(String(value[squareRange]))) 260 | } else { 261 | return nil 262 | } 263 | } 264 | 265 | } 266 | -------------------------------------------------------------------------------- /Sources/ChessKit/Piece.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Piece.swift 3 | // ChessKit 4 | // 5 | 6 | /// Represents a piece on the chess board. 7 | public struct Piece: Hashable, Sendable { 8 | 9 | /// Represents the color of a piece. 10 | public enum Color: String, CaseIterable, Sendable { 11 | case black = "b", white = "w" 12 | 13 | /// The opposite color of the given color. 14 | public var opposite: Color { 15 | self == .black ? .white : .black 16 | } 17 | 18 | /// Toggles to the opposite color value. 19 | public mutating func toggle() { 20 | self = self.opposite 21 | } 22 | } 23 | 24 | /// Represents the type of piece. 25 | public enum Kind: String, CaseIterable, Sendable { 26 | case pawn = "" 27 | case knight = "N", bishop = "B", rook = "R", queen = "Q", king = "K" 28 | 29 | /// The notation of the given piece kind. 30 | public var notation: String { 31 | switch self { 32 | case .pawn: "" 33 | case .bishop: "B" 34 | case .knight: "N" 35 | case .rook: "R" 36 | case .queen: "Q" 37 | case .king: "K" 38 | } 39 | } 40 | 41 | } 42 | 43 | /// The color of the piece. 44 | public var color: Color 45 | /// The kind of piece, e.g. `.pawn`. 46 | public var kind: Kind 47 | /// The square where this piece is located on the board. 48 | public var square: Square 49 | 50 | /// Initializes a chess piece with the given kind, color, and square. 51 | /// 52 | /// - parameter kind: The kind of piece, e.g. `.pawn`. 53 | /// - parameter color: The color of the piece, e.g. `.white`. 54 | /// - parameter square: The square the piece is located on, e.g. `.a1`. 55 | /// 56 | public init(_ kind: Kind, color: Color, square: Square) { 57 | self.kind = kind 58 | self.color = color 59 | self.square = square 60 | } 61 | 62 | /// Initializes a chess piece from its FEN notation. 63 | /// 64 | /// - parameter fen: The Forsyth–Edwards Notation of a piece kind 65 | /// and color, e.g. `"p"`. 66 | /// - parameter square: The square the piece is located on, e.g. `.a1`. 67 | /// 68 | init?(fen: String, square: Square) { 69 | switch fen { 70 | case "p": self = Piece(.pawn, color: .black, square: square) 71 | case "b": self = Piece(.bishop, color: .black, square: square) 72 | case "n": self = Piece(.knight, color: .black, square: square) 73 | case "r": self = Piece(.rook, color: .black, square: square) 74 | case "q": self = Piece(.queen, color: .black, square: square) 75 | case "k": self = Piece(.king, color: .black, square: square) 76 | case "P": self = Piece(.pawn, color: .white, square: square) 77 | case "B": self = Piece(.bishop, color: .white, square: square) 78 | case "N": self = Piece(.knight, color: .white, square: square) 79 | case "R": self = Piece(.rook, color: .white, square: square) 80 | case "Q": self = Piece(.queen, color: .white, square: square) 81 | case "K": self = Piece(.king, color: .white, square: square) 82 | default: return nil 83 | } 84 | } 85 | 86 | /// The FEN representation of the piece. 87 | /// 88 | /// - note: This value does not convey any information regarding 89 | /// the piece's location on the board (only kind and color). 90 | var fen: String { 91 | switch (color, kind) { 92 | case (.black, .pawn): "p" 93 | case (.black, .bishop): "b" 94 | case (.black, .knight): "n" 95 | case (.black, .rook): "r" 96 | case (.black, .queen): "q" 97 | case (.black, .king): "k" 98 | case (.white, .pawn): "P" 99 | case (.white, .bishop): "B" 100 | case (.white, .knight): "N" 101 | case (.white, .rook): "R" 102 | case (.white, .queen): "Q" 103 | case (.white, .king): "K" 104 | } 105 | } 106 | 107 | var graphic: String { 108 | switch (color, kind) { 109 | case (.black, .pawn): "♟\u{FE0E}" 110 | case (.black, .bishop): "♝" 111 | case (.black, .knight): "♞" 112 | case (.black, .rook): "♜" 113 | case (.black, .queen): "♛" 114 | case (.black, .king): "♚" 115 | case (.white, .pawn): "♙" 116 | case (.white, .bishop): "♗" 117 | case (.white, .knight): "♘" 118 | case (.white, .rook): "♖" 119 | case (.white, .queen): "♕" 120 | case (.white, .king): "♔" 121 | } 122 | } 123 | 124 | } 125 | 126 | // MARK: - CustomStringConvertible 127 | extension Piece: CustomStringConvertible { 128 | public var description: String { 129 | "\(String(describing: color)) \(String(describing: kind)) on \(square.notation)" 130 | } 131 | } 132 | 133 | extension Piece.Color: CustomStringConvertible { 134 | public var description: String { 135 | switch self { 136 | case .white: "White" 137 | case .black: "Black" 138 | } 139 | } 140 | } 141 | 142 | extension Piece.Kind: CustomStringConvertible { 143 | public var description: String { 144 | switch self { 145 | case .pawn: "Pawn" 146 | case .bishop: "Bishop" 147 | case .knight: "Knight" 148 | case .rook: "Rook" 149 | case .queen: "Queen" 150 | case .king: "King" 151 | } 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /Sources/ChessKit/Special Moves/Castling.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Castling.swift 3 | // ChessKit 4 | // 5 | 6 | /// Structure that captures legal castling moves. 7 | struct LegalCastlings: Hashable, Sendable { 8 | 9 | private var legal: [Castling] 10 | 11 | /// Initialize a `LegalCastlings` struct with an 12 | /// array of legal castling moves. 13 | /// 14 | /// - parameter legal: An array of legal ``Castling`` moves. 15 | /// 16 | init(legal: [Castling] = [.bK, .wK, .bQ, .wQ]) { 17 | self.legal = legal 18 | } 19 | 20 | /// Removes any castling moves associated with `piece` from the 21 | /// list of legal castlings. 22 | /// 23 | /// - parameter piece: The piece for which to invalidate castlings. 24 | /// Must be either a `.king` or `.rook` piece. 25 | /// 26 | /// For example, if a king has moved, pass the king piece to this 27 | /// method to remove any castlings associated with that king. 28 | mutating func invalidateCastling(for piece: Piece) { 29 | if piece.kind == .king { 30 | legal.removeAll { $0.color == piece.color } 31 | } else if piece.kind == .rook { 32 | legal.removeAll { $0.color == piece.color && $0.rookStart == piece.square } 33 | } 34 | } 35 | 36 | /// Checks if a given castling is currently legal. 37 | /// 38 | /// - parameter castling: The castling move to check for legality. 39 | /// - returns: Whether or not the provided `castling` is legal. 40 | /// 41 | func contains(_ castling: Castling) -> Bool { 42 | legal.contains(castling) 43 | } 44 | 45 | /// The FEN representation of the legal castlings. 46 | /// 47 | /// Examples: `KQkq`, `QK`, `Qkq`, `k`, `-` 48 | var fen: String { 49 | legal.isEmpty ? "-" : legal.map(\.fen).sorted().joined() 50 | } 51 | 52 | } 53 | 54 | /// Represents a castling move in a standard chess game. 55 | /// 56 | /// Contains various characteristics of the castling move 57 | /// such as king and rook start and end squares and notation. 58 | public struct Castling: Hashable, Sendable { 59 | 60 | /// Kingside castle for black. 61 | static let bK = Castling(side: .king, color: .black) 62 | /// Kingside castle for white. 63 | static let wK = Castling(side: .king, color: .white) 64 | /// Queenside castle for black. 65 | static let bQ = Castling(side: .queen, color: .black) 66 | /// Queenside castle for white. 67 | static let wQ = Castling(side: .queen, color: .white) 68 | 69 | /// Represents the side of the board from which castling an occur. 70 | /// Either `king` or `queen`. 71 | enum Side: CaseIterable, Sendable { 72 | case king, queen 73 | 74 | var notation: String { 75 | switch self { 76 | case .king: "O-O" 77 | case .queen: "O-O-O" 78 | } 79 | } 80 | } 81 | 82 | /// The side of the board for which this castling object represents. 83 | var side: Side 84 | /// The color of the king and rook castling. 85 | var color: Piece.Color 86 | 87 | /// The squares that the king will pass through when castling. 88 | var squares: [Square] { 89 | switch color { 90 | case .white: (side == .queen) ? [.c1, .d1] : [.f1, .g1] 91 | case .black: (side == .queen) ? [.c8, .d8] : [.f8, .g8] 92 | } 93 | } 94 | 95 | /// The squares between the king and rook that must be clear for castling. 96 | var path: [Square] { 97 | switch color { 98 | case .white: (side == .queen) ? [.b1, .c1, .d1] : [.f1, .g1] 99 | case .black: (side == .queen) ? [.b8, .c8, .d8] : [.f8, .g8] 100 | } 101 | } 102 | 103 | /// The starting square of the king, depending on the color. 104 | public var kingStart: Square { 105 | switch color { 106 | case .white: .e1 107 | case .black: .e8 108 | } 109 | } 110 | 111 | /// The ending square of the king, depending on the castle side and color. 112 | public var kingEnd: Square { 113 | switch color { 114 | case .white: (side == .queen) ? .c1 : .g1 115 | case .black: (side == .queen) ? .c8 : .g8 116 | } 117 | } 118 | 119 | /// The starting square of the rook, depending on the castle side and color. 120 | public var rookStart: Square { 121 | switch color { 122 | case .white: (side == .queen) ? .a1 : .h1 123 | case .black: (side == .queen) ? .a8 : .h8 124 | } 125 | } 126 | 127 | /// The ending square of the rook, depending on the castle side and color. 128 | public var rookEnd: Square { 129 | switch color { 130 | case .white: (side == .queen) ? .d1 : .f1 131 | case .black: (side == .queen) ? .d8 : .f8 132 | } 133 | } 134 | 135 | /// The FEN representation of the castling. 136 | /// 137 | /// Possible values: `K`, `Q`, `k`, or `q` 138 | var fen: String { 139 | switch color { 140 | case .white: (side == .queen) ? "Q" : "K" 141 | case .black: (side == .queen) ? "q" : "k" 142 | } 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /Sources/ChessKit/Special Moves/EnPassant.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EnPassant.swift 3 | // ChessKit 4 | // 5 | 6 | /// Structure that captures en passant moves. 7 | struct EnPassant: Hashable, Sendable { 8 | 9 | /// Pawn that is capable of being captured by en passant. 10 | var pawn: Piece 11 | 12 | /// The square that the capturing pawn will move to after the en passant. 13 | var captureSquare: Square { 14 | Square(pawn.square.file, pawn.color == .white ? 3 : 6) 15 | } 16 | 17 | /// Determines whether or not the pawn could be captured by en passant. 18 | /// 19 | /// - parameter capturingPiece: The piece that is capturing the contained pawn. 20 | /// - returns: Whether `capturingPiece` could capture `pawn`. 21 | /// 22 | /// `capturingPiece` must be an opposite color pawn that is on the 23 | /// same rank as the target pawn and exactly 1 file away from the 24 | /// target pawn for this method to return `true`, otherwise `false` 25 | /// is returned. 26 | /// 27 | /// - note: This function only considers the properties of the capturing piece 28 | /// and `pawn`, other validations may be required such as whether or not 29 | /// the side with `capturingPiece` has passed their opportunity to capture 30 | /// by en passant. 31 | func couldBeCaptured(by capturingPiece: Piece) -> Bool { 32 | capturingPiece.kind == .pawn && capturingPiece.color == pawn.color.opposite && capturingPiece.square.rank == pawn.square.rank 33 | && abs(capturingPiece.square.file.number - pawn.square.file.number) == 1 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /Sources/ChessKit/Square.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BoardSquare.swift 3 | // ChessKit 4 | // 5 | 6 | /// Represents a square on the chess board. 7 | public enum Square: Int, CaseIterable, Sendable { 8 | /// The file on the chess board, from a to h. 9 | public enum File: String, CaseIterable, Sendable { 10 | case a, b, c, d, e, f, g, h 11 | 12 | /// The number corresponding to the file. 13 | /// 14 | /// For example: 15 | /// ``` 16 | /// Square.File.a.number // 1 17 | /// Square.File.h.number // 8 18 | /// ``` 19 | public var number: Int { 20 | File.allCases.firstIndex(of: self)! + 1 21 | } 22 | 23 | /// Initialize a file from a number from 1 through 8. 24 | /// 25 | /// - parameter number: The number of the file to set. 26 | /// 27 | /// If an invalid number is passed, i.e. less than 1 or 28 | /// greater than 8, the file is set to `.a`. 29 | /// 30 | /// See also `Square.File.number`. 31 | public init(_ number: Int) { 32 | switch number { 33 | case 1: self = .a 34 | case 2: self = .b 35 | case 3: self = .c 36 | case 4: self = .d 37 | case 5: self = .e 38 | case 6: self = .f 39 | case 7: self = .g 40 | case 8: self = .h 41 | case let n where n < 1: self = .a 42 | case let n where n > 8: self = .h 43 | default: self = .a 44 | } 45 | } 46 | } 47 | 48 | /// The rank on the chess board, from 1 to 8. 49 | public struct Rank: ExpressibleByIntegerLiteral, Hashable, Sendable { 50 | /// The possible range of Rank numbers. 51 | public static let range = 1...8 52 | 53 | /// The integer value of the Rank. 54 | public var value: Int 55 | 56 | /// Initialize a Rank with a provided integer value. 57 | public init(_ value: Int) { 58 | self.value = value.bounded(by: Rank.range) 59 | } 60 | 61 | /// Initialize a Rank with a provided integer literal. 62 | public init(integerLiteral value: IntegerLiteralType) { 63 | self.init(value) 64 | } 65 | } 66 | 67 | // MARK: Squares 68 | 69 | case a1, b1, c1, d1, e1, f1, g1, h1 70 | case a2, b2, c2, d2, e2, f2, g2, h2 71 | case a3, b3, c3, d3, e3, f3, g3, h3 72 | case a4, b4, c4, d4, e4, f4, g4, h4 73 | case a5, b5, c5, d5, e5, f5, g5, h5 74 | case a6, b6, c6, d6, e6, f6, g6, h6 75 | case a7, b7, c7, d7, e7, f7, g7, h7 76 | case a8, b8, c8, d8, e8, f8, g8, h8 77 | 78 | // MARK: Initializer 79 | 80 | /// Initializes a board square from the given notation string. 81 | /// 82 | /// - parameter notation: The notation of the square, e.g. `"a1"`. 83 | /// 84 | public init(_ notation: String) { 85 | let file = File.allCases.filter { $0.rawValue == notation.prefix(1) }.first ?? .a 86 | let rank = Rank(Int(notation.suffix(1)) ?? 1) 87 | self.init(file, rank) 88 | } 89 | 90 | /// Initializes a board square from the provided file and rank. 91 | /// 92 | /// - parameter file: The file (column) of the square, from `a` to `h`. 93 | /// - parameter rank: The rank (row) of the square, from `1` to `8`. 94 | /// 95 | init(_ file: File, _ rank: Rank) { 96 | switch (file, rank) { 97 | case (.a, 1): self = .a1 98 | case (.a, 2): self = .a2 99 | case (.a, 3): self = .a3 100 | case (.a, 4): self = .a4 101 | case (.a, 5): self = .a5 102 | case (.a, 6): self = .a6 103 | case (.a, 7): self = .a7 104 | case (.a, 8): self = .a8 105 | case (.b, 1): self = .b1 106 | case (.b, 2): self = .b2 107 | case (.b, 3): self = .b3 108 | case (.b, 4): self = .b4 109 | case (.b, 5): self = .b5 110 | case (.b, 6): self = .b6 111 | case (.b, 7): self = .b7 112 | case (.b, 8): self = .b8 113 | case (.c, 1): self = .c1 114 | case (.c, 2): self = .c2 115 | case (.c, 3): self = .c3 116 | case (.c, 4): self = .c4 117 | case (.c, 5): self = .c5 118 | case (.c, 6): self = .c6 119 | case (.c, 7): self = .c7 120 | case (.c, 8): self = .c8 121 | case (.d, 1): self = .d1 122 | case (.d, 2): self = .d2 123 | case (.d, 3): self = .d3 124 | case (.d, 4): self = .d4 125 | case (.d, 5): self = .d5 126 | case (.d, 6): self = .d6 127 | case (.d, 7): self = .d7 128 | case (.d, 8): self = .d8 129 | case (.e, 1): self = .e1 130 | case (.e, 2): self = .e2 131 | case (.e, 3): self = .e3 132 | case (.e, 4): self = .e4 133 | case (.e, 5): self = .e5 134 | case (.e, 6): self = .e6 135 | case (.e, 7): self = .e7 136 | case (.e, 8): self = .e8 137 | case (.f, 1): self = .f1 138 | case (.f, 2): self = .f2 139 | case (.f, 3): self = .f3 140 | case (.f, 4): self = .f4 141 | case (.f, 5): self = .f5 142 | case (.f, 6): self = .f6 143 | case (.f, 7): self = .f7 144 | case (.f, 8): self = .f8 145 | case (.g, 1): self = .g1 146 | case (.g, 2): self = .g2 147 | case (.g, 3): self = .g3 148 | case (.g, 4): self = .g4 149 | case (.g, 5): self = .g5 150 | case (.g, 6): self = .g6 151 | case (.g, 7): self = .g7 152 | case (.g, 8): self = .g8 153 | case (.h, 1): self = .h1 154 | case (.h, 2): self = .h2 155 | case (.h, 3): self = .h3 156 | case (.h, 4): self = .h4 157 | case (.h, 5): self = .h5 158 | case (.h, 6): self = .h6 159 | case (.h, 7): self = .h7 160 | case (.h, 8): self = .h8 161 | default: self = .a1 162 | } 163 | } 164 | 165 | // MARK: Components 166 | 167 | /// The file (column) of the given square, from `a` through `h`. 168 | public var file: File { 169 | switch self { 170 | case .a1, .a2, .a3, .a4, .a5, .a6, .a7, .a8: .a 171 | case .b1, .b2, .b3, .b4, .b5, .b6, .b7, .b8: .b 172 | case .c1, .c2, .c3, .c4, .c5, .c6, .c7, .c8: .c 173 | case .d1, .d2, .d3, .d4, .d5, .d6, .d7, .d8: .d 174 | case .e1, .e2, .e3, .e4, .e5, .e6, .e7, .e8: .e 175 | case .f1, .f2, .f3, .f4, .f5, .f6, .f7, .f8: .f 176 | case .g1, .g2, .g3, .g4, .g5, .g6, .g7, .g8: .g 177 | case .h1, .h2, .h3, .h4, .h5, .h6, .h7, .h8: .h 178 | } 179 | } 180 | 181 | /// The rank (row) of the given square, from `1` to `8`. 182 | public var rank: Rank { 183 | switch self { 184 | case .a1, .b1, .c1, .d1, .e1, .f1, .g1, .h1: 1 185 | case .a2, .b2, .c2, .d2, .e2, .f2, .g2, .h2: 2 186 | case .a3, .b3, .c3, .d3, .e3, .f3, .g3, .h3: 3 187 | case .a4, .b4, .c4, .d4, .e4, .f4, .g4, .h4: 4 188 | case .a5, .b5, .c5, .d5, .e5, .f5, .g5, .h5: 5 189 | case .a6, .b6, .c6, .d6, .e6, .f6, .g6, .h6: 6 190 | case .a7, .b7, .c7, .d7, .e7, .f7, .g7, .h7: 7 191 | case .a8, .b8, .c8, .d8, .e8, .f8, .g8, .h8: 8 192 | } 193 | } 194 | 195 | /// The notation for the given square. 196 | public var notation: String { 197 | file.rawValue + "\(rank.value)" 198 | } 199 | 200 | // MARK: Color 201 | 202 | /// Represents the possible colors of each board square. 203 | public enum Color: CaseIterable { 204 | case light, dark 205 | } 206 | 207 | /// The color of the square on the board, either light or dark. 208 | public var color: Color { 209 | if (file.number % 2 == 0 && rank.value % 2 == 0) || (file.number % 2 != 0 && rank.value % 2 != 0) { 210 | .dark 211 | } else { 212 | .light 213 | } 214 | } 215 | 216 | // MARK: Directional 217 | 218 | /// The `Square` to the left of the current one. 219 | /// 220 | /// Returns the same square if called from a square on the A file. 221 | public var left: Square { 222 | Square(File(file.number - 1), rank) 223 | } 224 | 225 | /// The `Square` to the right of the current one. 226 | /// 227 | /// Returns the same square if called from a square on the H file. 228 | public var right: Square { 229 | Square(File(file.number + 1), rank) 230 | } 231 | 232 | /// The `Square` above the current one. 233 | /// 234 | /// Returns the same square if called from a square on the 8th rank. 235 | public var up: Square { 236 | Square(file, Rank(rank.value + 1)) 237 | } 238 | 239 | /// The `Square` below the current one. 240 | /// 241 | /// Returns the same square if called from a square on the 1st rank. 242 | public var down: Square { 243 | Square(file, Rank(rank.value - 1)) 244 | } 245 | 246 | } 247 | -------------------------------------------------------------------------------- /Sources/ChessKit/Utilities/Comparable+Bounded.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Comparable+Bounded.swift 3 | // ChessKit 4 | // 5 | 6 | extension Comparable { 7 | func bounded(by limits: ClosedRange) -> Self { 8 | min(max(self, limits.lowerBound), limits.upperBound) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Sources/ChessKit/Utilities/Stack.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Stack.swift 3 | // ChessKit 4 | // 5 | 6 | struct Stack { 7 | private var top: Node? 8 | 9 | mutating func push(_ value: T) { 10 | let temp = top 11 | top = Node(value) 12 | top?.next = temp 13 | } 14 | 15 | @discardableResult 16 | mutating func pop() -> T? { 17 | defer { top = top?.next } 18 | return top?.value 19 | } 20 | } 21 | 22 | extension Stack { 23 | private class Node { 24 | let value: V 25 | var next: Node? 26 | 27 | init(_ value: V) { 28 | self.value = value 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Tests/ChessKitTests/CastlingTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CastlingTests.swift 3 | // ChessKit 4 | // 5 | 6 | @testable import ChessKit 7 | import Testing 8 | 9 | struct CastlingTests { 10 | 11 | @Test func castling() { 12 | var board = Board(position: .castling) 13 | #expect(board.position.legalCastlings.contains(.bK)) 14 | #expect(!board.position.legalCastlings.contains(.wK)) 15 | #expect(!board.position.legalCastlings.contains(.bQ)) 16 | #expect(board.position.legalCastlings.contains(.wQ)) 17 | 18 | // white queenside castle 19 | let wQmove = board.move(pieceAt: .e1, to: .c1)! 20 | #expect(wQmove.result == .castle(.wQ)) 21 | 22 | // black kingside castle 23 | let bkMove = board.move(pieceAt: .e8, to: .g8)! 24 | #expect(bkMove.result == .castle(.bK)) 25 | } 26 | 27 | @Test func invalidCastling() { 28 | let position = Position( 29 | pieces: [ 30 | .init(.queen, color: .black, square: .e8), 31 | .init(.king, color: .white, square: .e1), 32 | .init(.rook, color: .white, square: .h1) 33 | ] 34 | ) 35 | var board = Board(position: position) 36 | 37 | // attempt to castle while in check 38 | #expect(!board.canMove(pieceAt: .e1, to: .g1)) 39 | 40 | // attempt to castle through check 41 | board.move(pieceAt: .e8, to: .f8) 42 | #expect(!board.canMove(pieceAt: .e1, to: .g1)) 43 | 44 | // valid castling move 45 | board.move(pieceAt: .f8, to: .h8) 46 | #expect(board.canMove(pieceAt: .e1, to: .g1)) 47 | } 48 | 49 | @Test func invalidCastlingThroughPiece() { 50 | let position = Position( 51 | pieces: [ 52 | .init(.bishop, color: .white, square: .f1), 53 | .init(.king, color: .white, square: .e1), 54 | .init(.rook, color: .white, square: .h1) 55 | ] 56 | ) 57 | var board = Board(position: position) 58 | 59 | // attempt to castle through another piece 60 | #expect(!board.canMove(pieceAt: .e1, to: .g1)) 61 | 62 | // valid castling move 63 | board.move(pieceAt: .f1, to: .c4) 64 | #expect(board.canMove(pieceAt: .e1, to: .g1)) 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /Tests/ChessKitTests/GameTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GameTests.swift 3 | // ChessKitTests 4 | // 5 | 6 | @testable import ChessKit 7 | import Testing 8 | 9 | final class GameTests { 10 | 11 | private var game = Game() 12 | 13 | // MARK: Test Indices 14 | 15 | private let nf3Index = MoveTree.Index(number: 2, color: .white, variation: 0) 16 | private let bc4Index = MoveTree.Index(number: 3, color: .white, variation: 0) 17 | private let nc3Index = MoveTree.Index(number: 2, color: .white, variation: 1) 18 | private let nf6Index = MoveTree.Index(number: 2, color: .black, variation: 1) 19 | private let nc6Index = MoveTree.Index(number: 2, color: .black, variation: 2) 20 | private let nc6Index2 = MoveTree.Index(number: 2, color: .black, variation: 0) 21 | private let f5Index = MoveTree.Index(number: 2, color: .black, variation: 3) 22 | 23 | // MARK: Setup 24 | 25 | init() { 26 | game.tags = Self.mockTags 27 | 28 | game.make(moves: ["e4", "e5", "Nf3", "Nc6", "Bc4"], from: .minimum) 29 | 30 | // add 2. Nc3 ... variation to 2. Nf3 31 | game.make(moves: ["Nc3", "Nf6", "Bc4"], from: nf3Index.previous) 32 | 33 | // add 2... Nc6 ... variation to 2... Nf6 34 | game.make(moves: ["Nc6", "f4"], from: nf6Index.previous) 35 | 36 | // add another variation to 2... Nf6 37 | game.make(moves: ["f5", "exf5"], from: nc6Index2.previous) 38 | 39 | // make repeat moves to test proper handling 40 | game.make(move: "e4", from: .minimum) 41 | game.make(move: "e5", from: .minimum.next) 42 | game.make(moves: ["Nc3", "Nf6"], from: nf3Index.previous) 43 | } 44 | 45 | deinit { 46 | // reset game 47 | game = Game() 48 | } 49 | 50 | // MARK: Test Cases 51 | 52 | @Test func startingPosition() { 53 | let game1 = Game(startingWith: .standard) 54 | #expect(game1.startingIndex == .init(number: 0, color: .black, variation: 0)) 55 | #expect(game1.startingPosition == .standard) 56 | 57 | let fen = "r1bqkb1r/pp1ppppp/2n2n2/8/2B1P3/2N2N2/PP3PPP/R1BQK2R b KQkq - 4 6" 58 | var game2 = Game(startingWith: .init(fen: fen)!) 59 | #expect(game2.startingIndex == .init(number: 1, color: .white, variation: 0)) 60 | #expect(game2.startingPosition == .init(fen: fen)!) 61 | 62 | game2.make(move: "O-O", from: game2.startingIndex) 63 | #expect(game2.moves.index(after: game2.moves.minimumIndex) == .init(number: 1, color: .black, variation: 0)) 64 | #expect(game2.moves[.init(number: 1, color: .black, variation: 0)] == .init(san: "O-O", position: .init(fen: fen)!)) 65 | } 66 | 67 | @Test func validMoves() { 68 | #expect(!game.moves.isEmpty) 69 | #expect(game.moves[.init(number: 1, color: .white)]?.san == "e4") 70 | #expect(game.moves[.init(number: 1, color: .black)]?.san == "e5") 71 | #expect(game.moves[.init(number: 2, color: .white)]?.san == "Nf3") 72 | #expect(game.moves[.init(number: 2, color: .black)]?.san == "Nc6") 73 | #expect(game.moves[.init(number: 3, color: .white)]?.san == "Bc4") 74 | } 75 | 76 | @Test func invalidMoves() { 77 | let movesBefore = game.moves 78 | 79 | #expect(game.make(move: "e1", from: .init(number: 10, color: .black)) == .init(number: 10, color: .black)) 80 | // test that `MoveTree` has not changed 81 | #expect(movesBefore == game.moves) 82 | 83 | #expect( 84 | game.make( 85 | move: .init(san: "e4", position: .standard)!, 86 | from: .init(number: 100, color: .white) 87 | ) == .init(number: 100, color: .white) 88 | ) 89 | } 90 | 91 | @Test func moveTree() { 92 | #expect(game.moves[nf3Index]?.san == "Nf3") 93 | #expect(game.moves[nc3Index]?.san == "Nc3") 94 | 95 | #expect(game.moves[nf6Index]?.san == "Nf6") 96 | #expect(game.moves[nc6Index]?.san == "Nc6") 97 | 98 | #expect(game.moves.index(before: nc6Index) == nc3Index) 99 | 100 | #expect(game.moves[nc6Index2]?.san == "Nc6") 101 | #expect(game.moves[f5Index]?.san == "f5") 102 | 103 | #expect(game.moves.index(before: f5Index) == nf3Index) 104 | 105 | #expect(game.moves.index(before: .minimum.next) == .minimum) 106 | #expect(game.moves.index(after: nc3Index) == nf6Index) 107 | } 108 | 109 | @Test func moveAnnotation() { 110 | game.annotate(moveAt: nc3Index, assessment: .brilliant) 111 | game.annotate(moveAt: f5Index, comment: "Comment test") 112 | 113 | let moveText = String(PGNParser.convert(game: game).split(separator: "\n").last!) 114 | let expectedMoveText = "1. e4 e5 2. Nf3 (2. Nc3 $3 Nf6 (2... Nc6 3. f4) 3. Bc4) Nc6 (2... f5 {Comment test} 3. exf5) 3. Bc4 1-0" 115 | 116 | #expect(moveText == expectedMoveText) 117 | } 118 | 119 | @Test func positionAnnotation() { 120 | game.annotate(positionAt: nc3Index, assessment: .whiteHasCrushingAdvantage) 121 | game.annotate(positionAt: bc4Index, assessment: .whiteHasModerateTimeAdvantage) 122 | 123 | let moveText = String(PGNParser.convert(game: game).split(separator: "\n").last!) 124 | let expectedMoveText = "1. e4 e5 2. Nf3 (2. Nc3 $20 Nf6 (2... Nc6 3. f4) 3. Bc4) Nc6 (2... f5 3. exf5) 3. Bc4 $32 1-0" 125 | 126 | #expect(moveText == expectedMoveText) 127 | } 128 | 129 | @Test func moveHistory() { 130 | let f5History = game.moves.history(for: f5Index) 131 | let expectedF5History = [ 132 | .init(number: 1, color: .white, variation: 0), 133 | .init(number: 1, color: .black, variation: 0), 134 | .init(number: 2, color: .white, variation: 0), 135 | f5Index 136 | ] 137 | #expect(f5History == expectedF5History) 138 | 139 | let emptyHistory = game.moves.history(for: .minimum) 140 | #expect(emptyHistory == [.init(number: 1, color: .white)]) 141 | } 142 | 143 | @Test func moveFuture() { 144 | let f5Future = game.moves.future(for: f5Index) 145 | let expectedF5Future = [MoveTree.Index(number: 3, color: .white, variation: 3)] 146 | #expect(f5Future == expectedF5Future) 147 | 148 | let fullFuture = game.moves.future(for: .minimum) 149 | let expectedFullFuture = [ 150 | MoveTree.Index(number: 1, color: .black, variation: 0), 151 | MoveTree.Index(number: 2, color: .white, variation: 0), 152 | MoveTree.Index(number: 2, color: .black, variation: 0), 153 | MoveTree.Index(number: 3, color: .white, variation: 0) 154 | ] 155 | #expect(fullFuture == expectedFullFuture) 156 | } 157 | 158 | @Test func moveFullVariation() { 159 | let f5History = game.moves.history(for: f5Index) 160 | let f5Future = game.moves.future(for: f5Index) 161 | let f5Full = game.moves.fullVariation(for: f5Index) 162 | #expect(f5History + f5Future == f5Full) 163 | } 164 | 165 | @Test func moveTreeEmptyPath() { 166 | #expect(game.moves.path(from: nc3Index, to: nc3Index).isEmpty) 167 | } 168 | 169 | @Test func moveTreeInvalidPath() { 170 | #expect( 171 | game.moves 172 | .path(from: nc3Index, to: .init(number: 100, color: .white)) 173 | .isEmpty 174 | ) 175 | } 176 | 177 | @Test func moveTreeSimplePath() { 178 | // "1. e4 e5 2. Nf3 (2. Nc3 Nf6 (2... Nc6 3. f4) 3. Bc4) Nc6 (2... f5 3. exf5) 3. Bc4" 179 | let f4 = MoveTree.Index(number: 3, color: .white, variation: 2) 180 | let e5 = MoveTree.Index(number: 1, color: .black, variation: 0) 181 | 182 | // 3. f4 to 1. e5 183 | let path1 = game.moves.path(from: f4, to: e5) 184 | 185 | #expect(path1.map(\.direction) == [.reverse, .reverse, .reverse]) 186 | 187 | #expect( 188 | path1.map(\.index) == [ 189 | f4, 190 | .init(number: 2, color: .black, variation: 2), 191 | .init(number: 2, color: .white, variation: 1) 192 | ] 193 | ) 194 | 195 | // 1. e5 to 3. f4 196 | let path2 = game.moves.path(from: e5, to: f4) 197 | 198 | #expect(path2.map(\.direction) == [.forward, .forward, .forward]) 199 | 200 | #expect( 201 | path2.map(\.index) == [ 202 | .init(number: 2, color: .white, variation: 1), 203 | .init(number: 2, color: .black, variation: 2), 204 | f4 205 | ] 206 | ) 207 | } 208 | 209 | @Test func moveTreeComplexPath() { 210 | // "1. e4 e5 2. Nf3 (2. Nc3 Nf6 (2... Nc6 3. f4) 3. Bc4) Nc6 (2... f5 3. exf5) 3. Bc4" 211 | // 3. f4 to 3. Bc4 212 | let f4 = MoveTree.Index(number: 3, color: .white, variation: 2) 213 | let Bc4 = MoveTree.Index(number: 3, color: .white, variation: 0) 214 | let path = game.moves.path(from: f4, to: Bc4) 215 | 216 | #expect( 217 | path.map(\.direction) == [ 218 | .reverse, 219 | .reverse, 220 | .reverse, 221 | .forward, 222 | .forward, 223 | .forward, 224 | .forward 225 | ] 226 | ) 227 | 228 | #expect( 229 | path.map(\.index) == [ 230 | f4, 231 | .init(number: 2, color: .black, variation: 2), 232 | .init(number: 2, color: .white, variation: 1), 233 | .init(number: 1, color: .black, variation: 0), 234 | .init(number: 2, color: .white, variation: 0), 235 | .init(number: 2, color: .black, variation: 0), 236 | Bc4 237 | ] 238 | ) 239 | } 240 | 241 | @Test func pgn() { 242 | let pgn = 243 | """ 244 | [Event "Test Event"] 245 | [Site "Barrow, Alaska USA"] 246 | [Date "2000.01.01"] 247 | [Round "5"] 248 | [White "Player One"] 249 | [Black "Player Two"] 250 | [Result "1-0"] 251 | [Annotator "Annotator"] 252 | [PlyCount "15"] 253 | [TimeControl "40/7200:3600"] 254 | [Time "12:00"] 255 | [Termination "abandoned"] 256 | [Mode "OTB"] 257 | [FEN "\(Position.standard.fen)"] 258 | [SetUp "1"] 259 | [TestKey1 "Test Value 1"] 260 | [TestKey2 "Test Value 2"] 261 | 262 | 1. e4 e5 2. Nf3 (2. Nc3 Nf6 (2... Nc6 3. f4) 3. Bc4) Nc6 (2... f5 3. exf5) 3. Bc4 1-0 263 | """ 264 | 265 | #expect(game.pgn == pgn) 266 | } 267 | 268 | @Test func validTagPairs() throws { 269 | let pgn = 270 | """ 271 | [Event "Test Event"] 272 | [Site "Barrow, Alaska USA"] 273 | [Date "2000.01.01"] 274 | [Round "5"] 275 | [White "Player One"] 276 | [Black "Player Two"] 277 | [Result "1-0"] 278 | 279 | 1. e4 e5 2. Nf3 (2. Nc3 Nf6 (2... Nc6 3. f4) 3. Bc4) Nc6 (2... f5 3. exf5) 3. Bc4 280 | """ 281 | 282 | let game = try Game(pgn: pgn) 283 | #expect(game.tags.isValid) 284 | } 285 | 286 | @Test func invalidTagPairs() throws { 287 | let pgn = 288 | """ 289 | [Event "Test Event"] 290 | 291 | 1. e4 e5 2. Nf3 (2. Nc3 Nf6 (2... Nc6 3. f4) 3. Bc4) Nc6 (2... f5 3. exf5) 3. Bc4 292 | """ 293 | 294 | let game = try Game(pgn: pgn) 295 | #expect(!game.tags.isValid) 296 | #expect(game.tags.$site.pgn.isEmpty) 297 | } 298 | 299 | @Test func gameWithPromotion() { 300 | game.make(moves: ["f5", "d4", "fxe4", "d5", "exf3", "d6", "fxg2", "dxc7", "gxh1=Q+", "Ke2", "Nb4", "cxd8=Q+"], from: bc4Index) 301 | #expect(game.moves.map(\.?.san).contains("gxh1=Q+")) 302 | } 303 | 304 | } 305 | 306 | extension GameTests { 307 | 308 | private static let mockTags = Game.Tags( 309 | event: "Test Event", 310 | site: "Barrow, Alaska USA", 311 | date: "2000.01.01", 312 | round: "5", 313 | white: "Player One", 314 | black: "Player Two", 315 | result: "1-0", 316 | annotator: "Annotator", 317 | plyCount: "15", 318 | timeControl: "40/7200:3600", 319 | time: "12:00", 320 | termination: "abandoned", 321 | mode: "OTB", 322 | fen: Position.standard.fen, 323 | setUp: "1", 324 | other: [ 325 | "TestKey1": "Test Value 1", 326 | "TestKey2": "Test Value 2" 327 | ] 328 | ) 329 | 330 | } 331 | -------------------------------------------------------------------------------- /Tests/ChessKitTests/MoveTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MoveTests.swift 3 | // ChessKitTests 4 | // 5 | 6 | @testable import ChessKit 7 | import Testing 8 | 9 | struct MoveTests { 10 | 11 | @Test func moveSANInit() { 12 | let move = Move(result: .move, piece: .init(.pawn, color: .white, square: .e4), start: .e2, end: .e4) 13 | let moveFromSAN = Move(san: "e4", position: .standard) 14 | 15 | #expect(move == moveFromSAN) 16 | } 17 | 18 | @Test func moveInvalidSANInit() { 19 | #expect(Move(san: "e5", position: .standard) == nil) 20 | } 21 | 22 | @Test func moveNotation() { 23 | let pawnD3 = Move(result: .move, piece: Piece(.pawn, color: .white, square: .d3), start: .d2, end: .d3) 24 | #expect(String(describing: pawnD3) == "d3") 25 | #expect(pawnD3.san == "d3") 26 | #expect(pawnD3.lan == "d2d3") 27 | 28 | let bishopF4 = Move(result: .move, piece: Piece(.bishop, color: .white, square: .f4), start: .c1, end: .f4) 29 | #expect(String(describing: bishopF4) == "Bf4") 30 | #expect(bishopF4.san == "Bf4") 31 | #expect(bishopF4.lan == "c1f4") 32 | } 33 | 34 | @Test func captureNotation() { 35 | let capturedPiece = Piece(.bishop, color: .black, square: .d5) 36 | let capturingPiece = Piece(.pawn, color: .white, square: .e4) 37 | let capture = Move(result: .capture(capturedPiece), piece: capturingPiece, start: .e4, end: .d5) 38 | #expect(capture.san == "exd5") 39 | #expect(capture.lan == "e4d5") 40 | } 41 | 42 | @Test func enPassantNotation() { 43 | let ep = EnPassant(pawn: Piece(.pawn, color: .black, square: .d5)) 44 | let move = Move(result: .capture(ep.pawn), piece: Piece(.pawn, color: .white, square: .e5), start: .e5, end: .d6) 45 | #expect(move.san == "exd6") 46 | #expect(move.lan == "e5d6") 47 | } 48 | 49 | @Test func castlingNotation() { 50 | let shortCastle = Move(result: .castle(.bK), piece: Piece(.king, color: .black, square: .e8), start: .e8, end: .g8) 51 | #expect(shortCastle.san == "O-O") 52 | #expect(shortCastle.lan == "e8g8") 53 | 54 | let longCastle = Move(result: .castle(.bQ), piece: Piece(.king, color: .black, square: .e8), start: .e8, end: .c8, checkState: .checkmate) 55 | #expect(longCastle.san == "O-O-O#") 56 | #expect(longCastle.lan == "e8c8") 57 | } 58 | 59 | @Test func promotionsNotation() { 60 | let pawn = Piece(.pawn, color: .white, square: .e8) 61 | let queen = Piece(.queen, color: .white, square: .e8) 62 | let rook = Piece(.rook, color: .white, square: .e8) 63 | 64 | var queenPromo = Move(result: .move, piece: pawn, start: .e7, end: .e8) 65 | queenPromo.promotedPiece = queen 66 | #expect(queenPromo.san == "e8=Q") 67 | #expect(queenPromo.lan == "e7e8q") 68 | 69 | let capturedPiece = Piece(.bishop, color: .black, square: .f8) 70 | var rookCapturePromo = Move(result: .capture(capturedPiece), piece: pawn, start: .e7, end: .f8, checkState: .check) 71 | rookCapturePromo.promotedPiece = rook 72 | #expect(rookCapturePromo.san == "exf8=R+") 73 | #expect(rookCapturePromo.lan == "e7f8r") 74 | } 75 | 76 | @Test func checksNotation() { 77 | let check = Move(result: .move, piece: Piece(.queen, color: .white, square: .d4), start: .e3, end: .d4, checkState: .check) 78 | #expect(check.san == "Qd4+") 79 | #expect(check.lan == "e3d4") 80 | } 81 | 82 | @Test func checkmateNotation() { 83 | let checkmate = Move(result: .move, piece: Piece(.rook, color: .white, square: .g7), start: .g4, end: .g7, checkState: .checkmate) 84 | #expect(checkmate.san == "Rg7#") 85 | #expect(checkmate.lan == "g4g7") 86 | } 87 | 88 | @Test func moveAssessments() { 89 | #expect(Move.Assessment.null.notation == "") 90 | #expect(Move.Assessment.good.notation == "!") 91 | #expect(Move.Assessment.mistake.notation == "?") 92 | #expect(Move.Assessment.brilliant.notation == "!!") 93 | #expect(Move.Assessment.blunder.notation == "??") 94 | #expect(Move.Assessment.interesting.notation == "!?") 95 | #expect(Move.Assessment.dubious.notation == "?!") 96 | #expect(Move.Assessment.forced.notation == "□") 97 | #expect(Move.Assessment.singular.notation == "") 98 | #expect(Move.Assessment.worst.notation == "") 99 | 100 | #expect(Move.Assessment(notation: "") == .null) 101 | #expect(Move.Assessment(notation: "!") == .good) 102 | #expect(Move.Assessment(notation: "?") == .mistake) 103 | #expect(Move.Assessment(notation: "!!") == .brilliant) 104 | #expect(Move.Assessment(notation: "??") == .blunder) 105 | #expect(Move.Assessment(notation: "!?") == .interesting) 106 | #expect(Move.Assessment(notation: "?!") == .dubious) 107 | #expect(Move.Assessment(notation: "□") == .forced) 108 | } 109 | 110 | } 111 | -------------------------------------------------------------------------------- /Tests/ChessKitTests/MoveTreeTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MoveTreeTests.swift 3 | // ChessKitTests 4 | // 5 | 6 | @testable import ChessKit 7 | import Testing 8 | 9 | struct MoveTreeTests { 10 | 11 | @Test func emptyCollection() { 12 | let moveTree = MoveTree() 13 | #expect(moveTree.isEmpty) 14 | #expect(moveTree.startIndex == .minimum) 15 | #expect(moveTree.endIndex == .minimum) 16 | 17 | #expect(!moveTree.hasIndex(before: .minimum)) 18 | #expect(!moveTree.hasIndex(after: .minimum)) 19 | } 20 | 21 | @Test func subscriptAccess() { 22 | var moveTree = MoveTree() 23 | #expect(moveTree[.minimum] == nil) 24 | 25 | let e4 = Move(san: "e4", position: .standard) 26 | moveTree[.minimum.next] = e4 27 | #expect(moveTree[.minimum.next] == e4) 28 | } 29 | 30 | @Test func nodeHashValue() { 31 | var moveTree = MoveTree() 32 | let e4 = Move(san: "e4", position: .standard) 33 | moveTree[.minimum.next] = e4 34 | #expect(moveTree.dictionary[.minimum.next]?.hashValue != nil) 35 | } 36 | 37 | @Test func sameVariationComparability() { 38 | let wIndex = MoveTree.Index(number: 4, color: .white, variation: 2) 39 | #expect(wIndex < wIndex.next) 40 | #expect(wIndex > wIndex.previous) 41 | 42 | let bIndex = MoveTree.Index(number: 4, color: .black, variation: 2) 43 | #expect(bIndex < bIndex.next) 44 | #expect(bIndex > bIndex.previous) 45 | } 46 | 47 | @Test func differentVariationComparability() { 48 | let wIndex1 = MoveTree.Index(number: 4, color: .white, variation: 2) 49 | let wIndex2 = MoveTree.Index(number: 4, color: .white, variation: 3) 50 | #expect(wIndex1 > wIndex2) 51 | #expect(wIndex1.next > wIndex2.next) 52 | #expect(wIndex1.previous > wIndex2.next) 53 | #expect(wIndex1.next > wIndex2.previous) 54 | #expect(wIndex1.previous > wIndex2.previous) 55 | 56 | let bIndex1 = MoveTree.Index(number: 4, color: .black, variation: 2) 57 | let bIndex2 = MoveTree.Index(number: 4, color: .black, variation: 3) 58 | #expect(bIndex1 > bIndex2) 59 | #expect(bIndex1.next > bIndex2.next) 60 | #expect(bIndex1.previous > bIndex2.next) 61 | #expect(bIndex1.next > bIndex2.previous) 62 | #expect(bIndex1.previous > bIndex2.previous) 63 | } 64 | 65 | @Test func nonexistentIndexBeforeAndAfter() { 66 | let tree = MoveTree() 67 | #expect(tree.index(after: .minimum) == .minimum) 68 | #expect(tree.index(before: .minimum) == .minimum) 69 | } 70 | 71 | } 72 | 73 | // MARK: - Deprecated Tests 74 | 75 | extension MoveTreeTests { 76 | 77 | @available(*, deprecated) 78 | @Test func deprecated() { 79 | var moveTree = MoveTree() 80 | 81 | let move1 = Move(san: "e4", position: .standard)! 82 | let move2 = Move(san: "e5", position: .init(fen: "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 2")!)! 83 | 84 | let i1 = MoveTree.Index(number: 1, color: .white) 85 | let i2 = MoveTree.Index(number: 1, color: .black) 86 | 87 | moveTree.add(move: move1) 88 | moveTree.add(move: move2, toParentIndex: i1) 89 | 90 | #expect(moveTree.previousIndex(for: i1) == moveTree.index(before: i1)) 91 | #expect(moveTree.nextIndex(for: i1) == moveTree.index(after: i1)) 92 | 93 | #expect(moveTree.move(at: i1) == moveTree[i1]) 94 | #expect(moveTree.move(at: i1) == move1) 95 | 96 | #expect(moveTree.move(at: i2) == moveTree[i2]) 97 | #expect(moveTree.move(at: i2) == move2) 98 | 99 | #expect(moveTree.previousIndex(for: .minimum) == nil) 100 | #expect(moveTree.nextIndex(for: i2) == nil) 101 | 102 | #expect(moveTree.nextIndex(for: .minimum) == i1) 103 | } 104 | 105 | } 106 | -------------------------------------------------------------------------------- /Tests/ChessKitTests/Parsers/EngineLANParserTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EngineLANParserTests.swift 3 | // ChessKitTests 4 | // 5 | 6 | @testable import ChessKit 7 | import Testing 8 | 9 | struct EngineLANParserTests { 10 | 11 | @Test func capture() { 12 | let position = Position(fen: "8/8/8/4p3/3P4/8/8/8 w - - 0 1")! 13 | let move = EngineLANParser.parse(move: "d4e5", for: .white, in: position) 14 | 15 | let capturedPiece = Piece(.pawn, color: .black, square: .e5) 16 | #expect(move?.result == .capture(capturedPiece)) 17 | } 18 | 19 | @Test func castling() { 20 | let p1 = Position(fen: "8/8/8/8/8/8/8/4K2R w KQ - 0 1")! 21 | let wShortCastle = EngineLANParser.parse(move: "e1g1", for: .white, in: p1) 22 | #expect(wShortCastle?.result == .castle(.wK)) 23 | 24 | let p2 = Position(fen: "8/8/8/8/8/8/8/R3K3 w KQ - 0 1")! 25 | let wLongCastle = EngineLANParser.parse(move: "e1c1", for: .white, in: p2) 26 | #expect(wLongCastle?.result == .castle(.wQ)) 27 | 28 | let p3 = Position(fen: "4k2r/8/8/8/8/8/8/8 b kq - 0 1")! 29 | let bShortCastle = EngineLANParser.parse(move: "e8g8", for: .black, in: p3) 30 | #expect(bShortCastle?.result == .castle(.bK)) 31 | 32 | let p4 = Position(fen: "r3k3/8/8/8/8/8/8/8 b kq - 0 1")! 33 | let bLongCastle = EngineLANParser.parse(move: "e8c8", for: .black, in: p4) 34 | #expect(bLongCastle?.result == .castle(.bQ)) 35 | } 36 | 37 | @Test func promotion() { 38 | let p = Position(fen: "8/P7/8/8/8/8/8/8 w - - 0 1")! 39 | 40 | let qPromotion = EngineLANParser.parse(move: "a7a8q", for: .white, in: p) 41 | let promotedQueen = Piece(.queen, color: .white, square: .a8) 42 | #expect(qPromotion?.promotedPiece == promotedQueen) 43 | 44 | let rPromotion = EngineLANParser.parse(move: "a7a8r", for: .white, in: p) 45 | let promotedRook = Piece(.rook, color: .white, square: .a8) 46 | #expect(rPromotion?.promotedPiece == promotedRook) 47 | 48 | let bPromotion = EngineLANParser.parse(move: "a7a8b", for: .white, in: p) 49 | let promotedBishop = Piece(.bishop, color: .white, square: .a8) 50 | #expect(bPromotion?.promotedPiece == promotedBishop) 51 | 52 | let nPromotion = EngineLANParser.parse(move: "a7a8n", for: .white, in: p) 53 | let promotedKnight = Piece(.knight, color: .white, square: .a8) 54 | #expect(nPromotion?.promotedPiece == promotedKnight) 55 | } 56 | 57 | @Test func validLANButInvalidMove() { 58 | #expect(EngineLANParser.parse(move: "a4b5", for: .white, in: .standard) == nil) 59 | #expect(EngineLANParser.parse(move: "f8b5", for: .black, in: .standard) == nil) 60 | } 61 | 62 | @Test func invalidLAN() { 63 | #expect(EngineLANParser.parse(move: "bad move", for: .white, in: .standard) == nil) 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /Tests/ChessKitTests/Parsers/FENParserTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FENParserTests.swift 3 | // ChessKitTests 4 | // 5 | 6 | @testable import ChessKit 7 | import Testing 8 | 9 | struct FENParserTests { 10 | 11 | @Test func standardStartingPosition() { 12 | let p = Position.standard 13 | 14 | #expect(p.pieces.count == 32) 15 | #expect(p.sideToMove == .white) 16 | #expect(p.legalCastlings == LegalCastlings(legal: [.bK, .wK, .bQ, .wQ])) 17 | #expect(p.enPassant == nil) 18 | #expect(p.clock.halfmoves == 0) 19 | #expect(p.clock.fullmoves == 1) 20 | } 21 | 22 | @Test func complexPiecePlacement() { 23 | let p = Position.complex 24 | 25 | #expect(p.pieces.count == 24) 26 | #expect(p.sideToMove == .black) 27 | #expect(p.legalCastlings == LegalCastlings(legal: [.bK, .bQ])) 28 | #expect(p.enPassant == nil) 29 | #expect(p.clock.halfmoves == 0) 30 | #expect(p.clock.fullmoves == 20) 31 | 32 | // pieces 33 | #expect(p.pieces.contains(Piece(.rook, color: .black, square: .a8))) 34 | #expect(p.pieces.contains(Piece(.bishop, color: .black, square: .c8))) 35 | #expect(p.pieces.contains(Piece(.king, color: .black, square: .e8))) 36 | #expect(p.pieces.contains(Piece(.knight, color: .black, square: .g8))) 37 | #expect(p.pieces.contains(Piece(.rook, color: .black, square: .h8))) 38 | #expect(p.pieces.contains(Piece(.pawn, color: .black, square: .a7))) 39 | #expect(p.pieces.contains(Piece(.pawn, color: .black, square: .d7))) 40 | #expect(p.pieces.contains(Piece(.pawn, color: .black, square: .f7))) 41 | #expect(p.pieces.contains(Piece(.knight, color: .white, square: .g7))) 42 | #expect(p.pieces.contains(Piece(.pawn, color: .black, square: .h7))) 43 | #expect(p.pieces.contains(Piece(.knight, color: .black, square: .a6))) 44 | #expect(p.pieces.contains(Piece(.bishop, color: .white, square: .d6))) 45 | #expect(p.pieces.contains(Piece(.pawn, color: .black, square: .b5))) 46 | #expect(p.pieces.contains(Piece(.knight, color: .white, square: .d5))) 47 | #expect(p.pieces.contains(Piece(.pawn, color: .white, square: .e5))) 48 | #expect(p.pieces.contains(Piece(.pawn, color: .white, square: .h5))) 49 | #expect(p.pieces.contains(Piece(.pawn, color: .white, square: .g4))) 50 | #expect(p.pieces.contains(Piece(.pawn, color: .white, square: .d3))) 51 | #expect(p.pieces.contains(Piece(.queen, color: .white, square: .f3))) 52 | #expect(p.pieces.contains(Piece(.pawn, color: .white, square: .a2))) 53 | #expect(p.pieces.contains(Piece(.pawn, color: .white, square: .c2))) 54 | #expect(p.pieces.contains(Piece(.king, color: .white, square: .e2))) 55 | #expect(p.pieces.contains(Piece(.queen, color: .black, square: .a1))) 56 | #expect(p.pieces.contains(Piece(.bishop, color: .black, square: .g1))) 57 | } 58 | 59 | @Test func enPassantPosition() { 60 | let whiteEP = Position(fen: "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1")! 61 | 62 | #expect(whiteEP.sideToMove == .black) 63 | #expect(whiteEP.enPassant == EnPassant(pawn: Piece(.pawn, color: .white, square: .e4))) 64 | 65 | let blackEP = Position(fen: "rnbqkbnr/pppppppp/8/4P3/4P3/8/PPPP1PPP/RNBQKBNR w KQkq e6 0 2")! 66 | 67 | #expect(blackEP.sideToMove == .white) 68 | #expect(blackEP.enPassant == EnPassant(pawn: Piece(.pawn, color: .black, square: .e5))) 69 | } 70 | 71 | @Test func invalidFen() { 72 | let p = Position(fen: "invalid") 73 | #expect(p == nil) 74 | 75 | let invalidSideToMove = Position(fen: "8/8/8/4p1K1/2k1P3/8/8/8 B - - 0 1")! 76 | #expect(invalidSideToMove.sideToMove == .white) 77 | } 78 | 79 | @Test func convertPosition() { 80 | let standardFen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1" 81 | #expect(Position.standard.fen == standardFen) 82 | 83 | let complexFen = "r1b1k1nr/p2p1pNp/n2B4/1p1NP2P/6P1/3P1Q2/P1P1K3/q5b1 b kq - 0 20" 84 | #expect(Position.complex.fen == complexFen) 85 | 86 | let epFen = "rnbqkbnr/ppppp1pp/8/8/4Pp2/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1" 87 | #expect(Position.ep.fen == epFen) 88 | 89 | let castlingFen = "4k2r/6r1/8/8/8/8/3R4/R3K3 w Qk - 0 1" 90 | #expect(Position.castling.fen == castlingFen) 91 | 92 | let fiftyMoveFen = "8/5k2/3p4/1p1Pp2p/pP2Pp1P/P4P1K/8/8 b - - 99 50" 93 | #expect(Position.fiftyMove.fen == fiftyMoveFen) 94 | } 95 | 96 | } 97 | -------------------------------------------------------------------------------- /Tests/ChessKitTests/Parsers/PGNParserTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PGNParserTests.swift 3 | // ChessKitTests 4 | // 5 | 6 | @testable import ChessKit 7 | import Testing 8 | 9 | struct PGNParserTests { 10 | 11 | @Test func gameFromEmptyPGN() throws { 12 | #expect(try PGNParser.parse(game: "") == .init(startingWith: .standard)) 13 | } 14 | 15 | @Test func gameFromPGN() throws { 16 | let game = try PGNParser.parse(game: Game.fischerSpassky) 17 | let gameFromPGN = try Game(pgn: Game.fischerSpassky) 18 | 19 | #expect(game == gameFromPGN) 20 | } 21 | 22 | @Test func pgnFromGame() { 23 | var game = Game() 24 | game.make(moves: ["e4", "e5", "Nf3", "Nc6", "Bc4"], from: .minimum) 25 | #expect(PGNParser.convert(game: game) == "1. e4 e5 2. Nf3 Nc6 3. Bc4") 26 | } 27 | 28 | @Test func pgnFromEmptyGame() { 29 | #expect(PGNParser.convert(game: Game()) == "") 30 | } 31 | 32 | // MARK: Tags 33 | 34 | @Test func tagParsing() throws { 35 | let game = try PGNParser.parse(game: Game.fischerSpassky) 36 | #expect(game.tags.event == "F/S Return Match") 37 | #expect(game.tags.site == "Belgrade, Serbia JUG") 38 | #expect(game.tags.date == "1992.11.04") 39 | #expect(game.tags.round == "29") 40 | #expect(game.tags.white == "Fischer, Robert J.") 41 | #expect(game.tags.black == "Spassky, Boris V.") 42 | #expect(game.tags.result == "1/2-1/2") 43 | #expect(game.tags.annotator == "Mr. Annotator") 44 | #expect(game.tags.plyCount == "85") 45 | #expect(game.tags.timeControl == "?") 46 | #expect(game.tags.time == "??:??:??") 47 | #expect(game.tags.termination == "normal") 48 | #expect(game.tags.mode == "OTB") 49 | } 50 | 51 | @Test func tagParsingIrregularWhitespace() throws { 52 | let game = try PGNParser.parse( 53 | game: """ 54 | [Tag1 "A" ] 55 | [ Tag2 "B"] 56 | [ Tag3"C" ] 57 | 58 | 1. e4 e5 59 | """) 60 | 61 | #expect(game.tags.other["Tag1"] == "A") 62 | #expect(game.tags.other["Tag2"] == "B") 63 | #expect(game.tags.other["Tag3"] == "C") 64 | } 65 | 66 | @Test func customTagParsing() throws { 67 | // invalid pair 68 | #expect(throws: PGNParser.Error.invalidTagFormat) { 69 | try PGNParser.parse(game: "[a]\n\n1. e4 e5") 70 | } 71 | 72 | // custom tag 73 | let g2 = try PGNParser.parse(game: "[Custom_Tag \"Value\"]\n\n1. e4 e5") 74 | #expect(g2.tags.other["Custom_Tag"] == "Value") 75 | 76 | // duplicate tags 77 | let g3 = try PGNParser.parse(game: "[CustomTag \"Value\"] [CustomTag \"Value2\"]\n\n1. e4 e5") 78 | #expect(g3.tags.other["CustomTag"] == "Value") 79 | } 80 | 81 | // MARK: MoveText 82 | 83 | @Test func moveTextParsing() throws { 84 | let game = try PGNParser.parse(game: Game.fischerSpassky) 85 | 86 | // starting position + 85 ply 87 | #expect(game.positions.keys.count == 86) 88 | 89 | #expect(game.moves[.init(number: 1, color: .white)]?.assessment == .blunder) 90 | #expect(game.moves[.init(number: 1, color: .black)]?.assessment == .brilliant) 91 | #expect(game.moves[.init(number: 3, color: .black)]?.comment == "This opening is called the Ruy Lopez.") 92 | #expect(game.moves[.init(number: 4, color: .white)]?.comment == "test comment") 93 | #expect(game.positions[.init(number: 7, color: .black)]?.assessment == .blackHasDecisiveCounterplay) 94 | #expect(game.moves[.init(number: 10, color: .white)]?.end == .d4) 95 | #expect(game.moves[.init(number: 18, color: .black)]?.piece.kind == .queen) 96 | #expect(game.moves[.init(number: 18, color: .black)]?.end == .e7) 97 | #expect(game.moves[.init(number: 36, color: .white)]?.checkState == .check) 98 | } 99 | 100 | @Test func numberlessMoveTextParsing() throws { 101 | let game = try PGNParser.parse(game: "e4 e5 Nf3") 102 | #expect(game.moves[.init(number: 1, color: .white)]?.san == "e4") 103 | #expect(game.moves[.init(number: 1, color: .black)]?.san == "e5") 104 | #expect(game.moves[.init(number: 2, color: .white)]?.san == "Nf3") 105 | } 106 | 107 | @Test func startWithBlack() throws { 108 | let g1 = try PGNParser.parse(game: "[FEN \"rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1\"][SetUp \"1\"]\n\n1... e5 2. Nf3 Nc6") 109 | #expect(g1.moves[.init(number: 1, color: .white)] == nil) 110 | #expect(g1.moves[.init(number: 1, color: .black)]?.san == "e5") 111 | #expect(g1.moves[.init(number: 2, color: .white)]?.san == "Nf3") 112 | #expect(g1.moves[.init(number: 2, color: .black)]?.san == "Nc6") 113 | 114 | let g2 = try PGNParser.parse(game: "[FEN \"rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1\"][SetUp \"1\"]\n\ne5 Nf3 Nc6") 115 | #expect(g2.moves[.init(number: 1, color: .white)] == nil) 116 | #expect(g2.moves[.init(number: 1, color: .black)]?.san == "e5") 117 | #expect(g2.moves[.init(number: 2, color: .white)]?.san == "Nf3") 118 | #expect(g2.moves[.init(number: 2, color: .black)]?.san == "Nc6") 119 | } 120 | 121 | @Test func variationParsing() throws { 122 | let game = try PGNParser.parse(game: "1. e4 e5 (1... c6)") 123 | 124 | // starting position + 3 ply 125 | #expect(game.positions.keys.count == 4) 126 | 127 | #expect(game.moves[.init(number: 1, color: .white)]?.san == "e4") 128 | #expect(game.moves[.init(number: 1, color: .black)]?.san == "e5") 129 | #expect(game.moves[.init(number: 1, color: .black, variation: 1)]?.san == "c6") 130 | } 131 | 132 | // MARK: Errors 133 | 134 | @Test func tooManyLineBreaksError() throws { 135 | #expect(throws: PGNParser.Error.tooManyLineBreaks) { 136 | try PGNParser.parse(game: "[Round \"1\"]\n\n1.e4 e5\n\n2. Nf3 Nc6") 137 | } 138 | } 139 | 140 | @Test func invalidSetUpOrFENError() throws { 141 | #expect(throws: PGNParser.Error.invalidSetUpOrFEN) { 142 | try PGNParser.parse(game: "[SetUp \"2\"]\n\n1. e4 e5") 143 | } 144 | 145 | #expect(throws: PGNParser.Error.invalidSetUpOrFEN) { 146 | try PGNParser.parse(game: "[FEN \"invalid\"] [SetUp \"1\"]\n\n1. e4 e5") 147 | } 148 | } 149 | 150 | @Test func unexpectedCharacterError() throws { 151 | #expect(throws: PGNParser.Error.unexpectedTagCharacter("%")) { 152 | try PGNParser.parse(game: "[Tag% \"Value\"]\n\n1. e4 e5") 153 | } 154 | } 155 | 156 | @Test func tagTokenErrors() throws { 157 | #expect(throws: PGNParser.Error.mismatchedTagBrackets) { 158 | try PGNParser.parse(game: "][Tag \"Value\"\n\n1.e4 e5") 159 | } 160 | 161 | #expect(throws: PGNParser.Error.tagSymbolNotFound) { 162 | try PGNParser.parse(game: "[\"Tag\" \"Value\"]\n\n1.e4 e5") 163 | } 164 | 165 | #expect(throws: PGNParser.Error.tagStringNotFound) { 166 | try PGNParser.parse(game: "[Tag Value ]\n\n1.e4 e5") 167 | } 168 | } 169 | 170 | @Test func unexpectedMoveTextTokenError() throws { 171 | #expect(throws: PGNParser.Error.unexpectedMoveTextToken) { 172 | try PGNParser.parse(game: "$0 1. e4 abc123 2. Nc6") 173 | } 174 | } 175 | 176 | @Test func invalidMoveError() throws { 177 | #expect(throws: PGNParser.Error.invalidMove("abc123")) { 178 | try PGNParser.parse(game: "1. e4 abc123 2. Nc6") 179 | } 180 | 181 | #expect(throws: PGNParser.Error.invalidMove("abc123")) { 182 | try PGNParser.parse(game: "abc123 e5 Nc6") 183 | } 184 | } 185 | 186 | @Test func invalidAnnotationError() throws { 187 | #expect(throws: PGNParser.Error.invalidAnnotation("$$0")) { 188 | try PGNParser.parse(game: "1. e4 e5 $$0 2. Nc6") 189 | } 190 | 191 | #expect(throws: PGNParser.Error.invalidAnnotation("$999")) { 192 | try PGNParser.parse(game: "1. e4 e5 $999 2. Nc6") 193 | } 194 | 195 | #expect(throws: PGNParser.Error.invalidAnnotation("!!!")) { 196 | try PGNParser.parse(game: "1. e4 e5!!! 2. Nc6") 197 | } 198 | 199 | #expect(throws: PGNParser.Error.invalidAnnotation("□□")) { 200 | try PGNParser.parse(game: "1. e4 e5□□ 2. Nc6") 201 | } 202 | } 203 | 204 | @Test func unpairedCommentDelimiterError() throws { 205 | #expect(throws: PGNParser.Error.unpairedCommentDelimiter) { 206 | try PGNParser.parse(game: "1. e4 e5 2. c6 } { this is a comment }") 207 | } 208 | } 209 | 210 | @Test func moveVariationError() throws { 211 | #expect(throws: PGNParser.Error.unpairedVariationDelimiter) { 212 | try PGNParser.parse(game: "1. e4 e5 )1... c6)") 213 | } 214 | } 215 | 216 | } 217 | 218 | // MARK: - Deprecated Tests 219 | extension PGNParserTests { 220 | 221 | @available(*, deprecated) 222 | @Test func legacyParsing() throws { 223 | // remove position assessment as it was not supported 224 | // in legacy parser 225 | let pgn = Game.fischerSpassky.replacingOccurrences(of: "$135 ", with: "") 226 | 227 | #expect( 228 | try PGNParser.parse(game: pgn) == PGNParser.parse(game: pgn, startingWith: .standard) 229 | ) 230 | } 231 | 232 | } 233 | -------------------------------------------------------------------------------- /Tests/ChessKitTests/Parsers/SANParserTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SANParserTests.swift 3 | // ChessKitTests 4 | // 5 | 6 | @testable import ChessKit 7 | import Testing 8 | 9 | struct SANParserTests { 10 | 11 | @Test func castling() { 12 | let p1 = Position(fen: "r3k3/8/8/8/8/8/8/4K2R w Kq - 0 1")! 13 | let shortCastle = SANParser.parse(move: "O-O", in: p1) 14 | #expect(shortCastle?.result == .castle(.wK)) 15 | 16 | let p2 = Position(fen: "r3k3/8/8/8/8/8/8/5RK1 b q - 0 1")! 17 | let longCastle = SANParser.parse(move: "O-O-O", in: p2) 18 | #expect(longCastle?.result == .castle(.bQ)) 19 | } 20 | 21 | @Test func enPassant() { 22 | let p = Position(fen: "rnbqkbnr/pp2pppp/8/2pP4/8/8/PPPP1PPP/RNBQKBNR w KQkq c6 0 1")! 23 | let enPassant = SANParser.parse(move: "dxc6", in: p) 24 | #expect(enPassant?.result == .capture(.init(.pawn, color: .black, square: .c5))) 25 | } 26 | 27 | @Test func promotion() { 28 | let p = Position(fen: "8/P7/8/8/8/8/8/8 w - - 0 1")! 29 | let promotion = SANParser.parse(move: "a8=Q", in: p) 30 | 31 | let promotedPiece = Piece(.queen, color: .white, square: .a8) 32 | #expect(promotion?.promotedPiece == promotedPiece) 33 | } 34 | 35 | @Test func checksAndMates() { 36 | let p1 = Position(fen: "8/k7/7Q/6R1/8/8/8/8 w - - 0 1")! 37 | 38 | let check = SANParser.parse(move: "Rg7+", in: p1) 39 | #expect(check?.checkState == .check) 40 | 41 | let p2 = Position(fen: "8/k5R1/7Q/8/8/8/8/8 b - - 0 1")! 42 | 43 | let kingMove = SANParser.parse(move: "Ka8", in: p2) 44 | #expect(kingMove?.checkState == Move.CheckState.none) 45 | 46 | let p3 = Position(fen: "k7/6R1/7Q/8/8/8/8/8 w - - 0 1")! 47 | 48 | let checkmate = SANParser.parse(move: "Qh8#", in: p3) 49 | #expect(checkmate?.checkState == .checkmate) 50 | } 51 | 52 | @Test func disambiguation() { 53 | let pw = Position(fen: "3r3r/8/8/R7/4Q2Q/8/8/R6Q w - - 0 1")! 54 | let pb = Position(fen: "3r3r/8/8/R7/4Q2Q/8/8/R6Q b - - 0 1")! 55 | 56 | let rookFileMove = SANParser.parse(move: "R1a3", in: pw) 57 | #expect(rookFileMove?.result == .move) 58 | #expect(rookFileMove?.piece.kind == .rook) 59 | #expect(rookFileMove?.disambiguation == .byRank(1)) 60 | #expect(rookFileMove?.start == .a1) 61 | #expect(rookFileMove?.end == .a3) 62 | #expect(rookFileMove?.promotedPiece == nil) 63 | #expect(rookFileMove?.checkState == Move.CheckState.none) 64 | 65 | let rookRankMove = SANParser.parse(move: "Rdf8", in: pb) 66 | #expect(rookRankMove?.result == .move) 67 | #expect(rookRankMove?.piece.kind == .rook) 68 | #expect(rookRankMove?.disambiguation == .byFile(.d)) 69 | #expect(rookRankMove?.start == .d8) 70 | #expect(rookRankMove?.end == .f8) 71 | #expect(rookRankMove?.promotedPiece == nil) 72 | #expect(rookRankMove?.checkState == Move.CheckState.none) 73 | 74 | let queenMove = SANParser.parse(move: "Qh4e1", in: pw) 75 | #expect(queenMove?.result == .move) 76 | #expect(queenMove?.piece.kind == .queen) 77 | #expect(queenMove?.disambiguation == .bySquare(.h4)) 78 | #expect(queenMove?.start == .h4) 79 | #expect(queenMove?.end == .e1) 80 | #expect(queenMove?.promotedPiece == nil) 81 | #expect(queenMove?.checkState == Move.CheckState.none) 82 | } 83 | 84 | @Test func testValidSANButInvalidMove() { 85 | #expect(SANParser.parse(move: "axb5", in: .standard) == nil) 86 | #expect(SANParser.parse(move: "Bb5", in: .standard) == nil) 87 | } 88 | 89 | @Test func invalidSAN() { 90 | #expect(SANParser.parse(move: "bad move", in: .standard) == nil) 91 | #expect(SANParser.parse(move: "exf3", in: .standard) == nil) 92 | #expect(SANParser.parse(move: "aNf3", in: .standard) == nil) 93 | #expect(SANParser.parse(move: "e44", in: .standard) == nil) 94 | } 95 | 96 | } 97 | -------------------------------------------------------------------------------- /Tests/ChessKitTests/Performance/BoardPerformanceTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BoardPerformanceTests.swift 3 | // ChessKitTests 4 | // 5 | 6 | @testable import ChessKit 7 | import XCTest 8 | 9 | final class BoardPerformanceTests: XCTestCase { 10 | 11 | func testBoardPerformance() { 12 | measure( 13 | metrics: [ 14 | XCTClockMetric(), 15 | XCTCPUMetric(), 16 | XCTMemoryMetric() 17 | ], 18 | block: simulateGame 19 | ) 20 | } 21 | 22 | private func simulateGame() { 23 | var board = Board() 24 | 25 | board.move(pieceAt: .e2, to: .e4) 26 | XCTAssertEqual(board.position.fen, "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1") 27 | 28 | board.move(pieceAt: .e7, to: .e5) 29 | XCTAssertEqual(board.position.fen, "rnbqkbnr/pppp1ppp/8/4p3/4P3/8/PPPP1PPP/RNBQKBNR w KQkq e6 0 2") 30 | 31 | board.move(pieceAt: .g1, to: .f3) 32 | XCTAssertEqual(board.position.fen, "rnbqkbnr/pppp1ppp/8/4p3/4P3/5N2/PPPP1PPP/RNBQKB1R b KQkq - 1 2") 33 | 34 | board.move(pieceAt: .b8, to: .c6) 35 | XCTAssertEqual(board.position.fen, "r1bqkbnr/pppp1ppp/2n5/4p3/4P3/5N2/PPPP1PPP/RNBQKB1R w KQkq - 2 3") 36 | 37 | board.move(pieceAt: .f1, to: .b5) 38 | XCTAssertEqual(board.position.fen, "r1bqkbnr/pppp1ppp/2n5/1B2p3/4P3/5N2/PPPP1PPP/RNBQK2R b KQkq - 3 3") 39 | 40 | board.move(pieceAt: .a7, to: .a6) 41 | XCTAssertEqual(board.position.fen, "r1bqkbnr/1ppp1ppp/p1n5/1B2p3/4P3/5N2/PPPP1PPP/RNBQK2R w KQkq - 0 4") 42 | 43 | board.move(pieceAt: .b5, to: .a4) 44 | XCTAssertEqual(board.position.fen, "r1bqkbnr/1ppp1ppp/p1n5/4p3/B3P3/5N2/PPPP1PPP/RNBQK2R b KQkq - 1 4") 45 | 46 | board.move(pieceAt: .g8, to: .f6) 47 | XCTAssertEqual(board.position.fen, "r1bqkb1r/1ppp1ppp/p1n2n2/4p3/B3P3/5N2/PPPP1PPP/RNBQK2R w KQkq - 2 5") 48 | 49 | board.move(pieceAt: .e1, to: .g1) 50 | XCTAssertEqual(board.position.fen, "r1bqkb1r/1ppp1ppp/p1n2n2/4p3/B3P3/5N2/PPPP1PPP/RNBQ1RK1 b kq - 3 5") 51 | 52 | board.move(pieceAt: .f8, to: .e7) 53 | XCTAssertEqual(board.position.fen, "r1bqk2r/1pppbppp/p1n2n2/4p3/B3P3/5N2/PPPP1PPP/RNBQ1RK1 w kq - 4 6") 54 | 55 | board.move(pieceAt: .f1, to: .e1) 56 | XCTAssertEqual(board.position.fen, "r1bqk2r/1pppbppp/p1n2n2/4p3/B3P3/5N2/PPPP1PPP/RNBQR1K1 b kq - 5 6") 57 | 58 | board.move(pieceAt: .b7, to: .b5) 59 | XCTAssertEqual(board.position.fen, "r1bqk2r/2ppbppp/p1n2n2/1p2p3/B3P3/5N2/PPPP1PPP/RNBQR1K1 w kq b6 0 7") 60 | 61 | board.move(pieceAt: .a4, to: .b3) 62 | XCTAssertEqual(board.position.fen, "r1bqk2r/2ppbppp/p1n2n2/1p2p3/4P3/1B3N2/PPPP1PPP/RNBQR1K1 b kq - 1 7") 63 | 64 | board.move(pieceAt: .d7, to: .d6) 65 | XCTAssertEqual(board.position.fen, "r1bqk2r/2p1bppp/p1np1n2/1p2p3/4P3/1B3N2/PPPP1PPP/RNBQR1K1 w kq - 0 8") 66 | 67 | board.move(pieceAt: .c2, to: .c3) 68 | XCTAssertEqual(board.position.fen, "r1bqk2r/2p1bppp/p1np1n2/1p2p3/4P3/1BP2N2/PP1P1PPP/RNBQR1K1 b kq - 0 8") 69 | 70 | board.move(pieceAt: .e8, to: .g8) 71 | XCTAssertEqual(board.position.fen, "r1bq1rk1/2p1bppp/p1np1n2/1p2p3/4P3/1BP2N2/PP1P1PPP/RNBQR1K1 w - - 1 9") 72 | 73 | board.move(pieceAt: .h2, to: .h3) 74 | XCTAssertEqual(board.position.fen, "r1bq1rk1/2p1bppp/p1np1n2/1p2p3/4P3/1BP2N1P/PP1P1PP1/RNBQR1K1 b - - 0 9") 75 | 76 | board.move(pieceAt: .c6, to: .b8) 77 | XCTAssertEqual(board.position.fen, "rnbq1rk1/2p1bppp/p2p1n2/1p2p3/4P3/1BP2N1P/PP1P1PP1/RNBQR1K1 w - - 1 10") 78 | 79 | board.move(pieceAt: .d2, to: .d4) 80 | XCTAssertEqual(board.position.fen, "rnbq1rk1/2p1bppp/p2p1n2/1p2p3/3PP3/1BP2N1P/PP3PP1/RNBQR1K1 b - d3 0 10") 81 | 82 | board.move(pieceAt: .b8, to: .d7) 83 | XCTAssertEqual(board.position.fen, "r1bq1rk1/2pnbppp/p2p1n2/1p2p3/3PP3/1BP2N1P/PP3PP1/RNBQR1K1 w - - 1 11") 84 | 85 | board.move(pieceAt: .c3, to: .c4) 86 | XCTAssertEqual(board.position.fen, "r1bq1rk1/2pnbppp/p2p1n2/1p2p3/2PPP3/1B3N1P/PP3PP1/RNBQR1K1 b - - 0 11") 87 | 88 | board.move(pieceAt: .c7, to: .c6) 89 | XCTAssertEqual(board.position.fen, "r1bq1rk1/3nbppp/p1pp1n2/1p2p3/2PPP3/1B3N1P/PP3PP1/RNBQR1K1 w - - 0 12") 90 | 91 | board.move(pieceAt: .c4, to: .b5) 92 | XCTAssertEqual(board.position.fen, "r1bq1rk1/3nbppp/p1pp1n2/1P2p3/3PP3/1B3N1P/PP3PP1/RNBQR1K1 b - - 0 12") 93 | 94 | board.move(pieceAt: .a6, to: .b5) 95 | XCTAssertEqual(board.position.fen, "r1bq1rk1/3nbppp/2pp1n2/1p2p3/3PP3/1B3N1P/PP3PP1/RNBQR1K1 w - - 0 13") 96 | 97 | board.move(pieceAt: .b1, to: .c3) 98 | XCTAssertEqual(board.position.fen, "r1bq1rk1/3nbppp/2pp1n2/1p2p3/3PP3/1BN2N1P/PP3PP1/R1BQR1K1 b - - 1 13") 99 | 100 | board.move(pieceAt: .c8, to: .b7) 101 | XCTAssertEqual(board.position.fen, "r2q1rk1/1b1nbppp/2pp1n2/1p2p3/3PP3/1BN2N1P/PP3PP1/R1BQR1K1 w - - 2 14") 102 | 103 | board.move(pieceAt: .c1, to: .g5) 104 | XCTAssertEqual(board.position.fen, "r2q1rk1/1b1nbppp/2pp1n2/1p2p1B1/3PP3/1BN2N1P/PP3PP1/R2QR1K1 b - - 3 14") 105 | 106 | board.move(pieceAt: .b5, to: .b4) 107 | XCTAssertEqual(board.position.fen, "r2q1rk1/1b1nbppp/2pp1n2/4p1B1/1p1PP3/1BN2N1P/PP3PP1/R2QR1K1 w - - 0 15") 108 | 109 | board.move(pieceAt: .c3, to: .b1) 110 | XCTAssertEqual(board.position.fen, "r2q1rk1/1b1nbppp/2pp1n2/4p1B1/1p1PP3/1B3N1P/PP3PP1/RN1QR1K1 b - - 1 15") 111 | 112 | board.move(pieceAt: .h7, to: .h6) 113 | XCTAssertEqual(board.position.fen, "r2q1rk1/1b1nbpp1/2pp1n1p/4p1B1/1p1PP3/1B3N1P/PP3PP1/RN1QR1K1 w - - 0 16") 114 | 115 | board.move(pieceAt: .g5, to: .h4) 116 | XCTAssertEqual(board.position.fen, "r2q1rk1/1b1nbpp1/2pp1n1p/4p3/1p1PP2B/1B3N1P/PP3PP1/RN1QR1K1 b - - 1 16") 117 | 118 | board.move(pieceAt: .c6, to: .c5) 119 | XCTAssertEqual(board.position.fen, "r2q1rk1/1b1nbpp1/3p1n1p/2p1p3/1p1PP2B/1B3N1P/PP3PP1/RN1QR1K1 w - - 0 17") 120 | 121 | board.move(pieceAt: .d4, to: .e5) 122 | XCTAssertEqual(board.position.fen, "r2q1rk1/1b1nbpp1/3p1n1p/2p1P3/1p2P2B/1B3N1P/PP3PP1/RN1QR1K1 b - - 0 17") 123 | 124 | board.move(pieceAt: .f6, to: .e4) 125 | XCTAssertEqual(board.position.fen, "r2q1rk1/1b1nbpp1/3p3p/2p1P3/1p2n2B/1B3N1P/PP3PP1/RN1QR1K1 w - - 0 18") 126 | 127 | board.move(pieceAt: .h4, to: .e7) 128 | XCTAssertEqual(board.position.fen, "r2q1rk1/1b1nBpp1/3p3p/2p1P3/1p2n3/1B3N1P/PP3PP1/RN1QR1K1 b - - 0 18") 129 | 130 | board.move(pieceAt: .d8, to: .e7) 131 | XCTAssertEqual(board.position.fen, "r4rk1/1b1nqpp1/3p3p/2p1P3/1p2n3/1B3N1P/PP3PP1/RN1QR1K1 w - - 0 19") 132 | 133 | board.move(pieceAt: .e5, to: .d6) 134 | XCTAssertEqual(board.position.fen, "r4rk1/1b1nqpp1/3P3p/2p5/1p2n3/1B3N1P/PP3PP1/RN1QR1K1 b - - 0 19") 135 | 136 | board.move(pieceAt: .e7, to: .f6) 137 | XCTAssertEqual(board.position.fen, "r4rk1/1b1n1pp1/3P1q1p/2p5/1p2n3/1B3N1P/PP3PP1/RN1QR1K1 w - - 1 20") 138 | 139 | board.move(pieceAt: .b1, to: .d2) 140 | XCTAssertEqual(board.position.fen, "r4rk1/1b1n1pp1/3P1q1p/2p5/1p2n3/1B3N1P/PP1N1PP1/R2QR1K1 b - - 2 20") 141 | 142 | board.move(pieceAt: .e4, to: .d6) 143 | XCTAssertEqual(board.position.fen, "r4rk1/1b1n1pp1/3n1q1p/2p5/1p6/1B3N1P/PP1N1PP1/R2QR1K1 w - - 0 21") 144 | 145 | board.move(pieceAt: .d2, to: .c4) 146 | XCTAssertEqual(board.position.fen, "r4rk1/1b1n1pp1/3n1q1p/2p5/1pN5/1B3N1P/PP3PP1/R2QR1K1 b - - 1 21") 147 | 148 | board.move(pieceAt: .d6, to: .c4) 149 | XCTAssertEqual(board.position.fen, "r4rk1/1b1n1pp1/5q1p/2p5/1pn5/1B3N1P/PP3PP1/R2QR1K1 w - - 0 22") 150 | 151 | board.move(pieceAt: .b3, to: .c4) 152 | XCTAssertEqual(board.position.fen, "r4rk1/1b1n1pp1/5q1p/2p5/1pB5/5N1P/PP3PP1/R2QR1K1 b - - 0 22") 153 | 154 | board.move(pieceAt: .d7, to: .b6) 155 | XCTAssertEqual(board.position.fen, "r4rk1/1b3pp1/1n3q1p/2p5/1pB5/5N1P/PP3PP1/R2QR1K1 w - - 1 23") 156 | 157 | board.move(pieceAt: .f3, to: .e5) 158 | XCTAssertEqual(board.position.fen, "r4rk1/1b3pp1/1n3q1p/2p1N3/1pB5/7P/PP3PP1/R2QR1K1 b - - 2 23") 159 | 160 | board.move(pieceAt: .a8, to: .e8) 161 | XCTAssertEqual(board.position.fen, "4rrk1/1b3pp1/1n3q1p/2p1N3/1pB5/7P/PP3PP1/R2QR1K1 w - - 3 24") 162 | 163 | board.move(pieceAt: .c4, to: .f7) 164 | XCTAssertEqual(board.position.fen, "4rrk1/1b3Bp1/1n3q1p/2p1N3/1p6/7P/PP3PP1/R2QR1K1 b - - 0 24") 165 | 166 | board.move(pieceAt: .f8, to: .f7) 167 | XCTAssertEqual(board.position.fen, "4r1k1/1b3rp1/1n3q1p/2p1N3/1p6/7P/PP3PP1/R2QR1K1 w - - 0 25") 168 | 169 | board.move(pieceAt: .e5, to: .f7) 170 | XCTAssertEqual(board.position.fen, "4r1k1/1b3Np1/1n3q1p/2p5/1p6/7P/PP3PP1/R2QR1K1 b - - 0 25") 171 | 172 | board.move(pieceAt: .e8, to: .e1) 173 | XCTAssertEqual(board.position.fen, "6k1/1b3Np1/1n3q1p/2p5/1p6/7P/PP3PP1/R2Qr1K1 w - - 0 26") 174 | 175 | board.move(pieceAt: .d1, to: .e1) 176 | XCTAssertEqual(board.position.fen, "6k1/1b3Np1/1n3q1p/2p5/1p6/7P/PP3PP1/R3Q1K1 b - - 0 26") 177 | 178 | board.move(pieceAt: .g8, to: .f7) 179 | XCTAssertEqual(board.position.fen, "8/1b3kp1/1n3q1p/2p5/1p6/7P/PP3PP1/R3Q1K1 w - - 0 27") 180 | 181 | board.move(pieceAt: .e1, to: .e3) 182 | XCTAssertEqual(board.position.fen, "8/1b3kp1/1n3q1p/2p5/1p6/4Q2P/PP3PP1/R5K1 b - - 1 27") 183 | 184 | board.move(pieceAt: .f6, to: .g5) 185 | XCTAssertEqual(board.position.fen, "8/1b3kp1/1n5p/2p3q1/1p6/4Q2P/PP3PP1/R5K1 w - - 2 28") 186 | 187 | board.move(pieceAt: .e3, to: .g5) 188 | XCTAssertEqual(board.position.fen, "8/1b3kp1/1n5p/2p3Q1/1p6/7P/PP3PP1/R5K1 b - - 0 28") 189 | 190 | board.move(pieceAt: .h6, to: .g5) 191 | XCTAssertEqual(board.position.fen, "8/1b3kp1/1n6/2p3p1/1p6/7P/PP3PP1/R5K1 w - - 0 29") 192 | 193 | board.move(pieceAt: .b2, to: .b3) 194 | XCTAssertEqual(board.position.fen, "8/1b3kp1/1n6/2p3p1/1p6/1P5P/P4PP1/R5K1 b - - 0 29") 195 | 196 | board.move(pieceAt: .f7, to: .e6) 197 | XCTAssertEqual(board.position.fen, "8/1b4p1/1n2k3/2p3p1/1p6/1P5P/P4PP1/R5K1 w - - 1 30") 198 | 199 | board.move(pieceAt: .a2, to: .a3) 200 | XCTAssertEqual(board.position.fen, "8/1b4p1/1n2k3/2p3p1/1p6/PP5P/5PP1/R5K1 b - - 0 30") 201 | 202 | board.move(pieceAt: .e6, to: .d6) 203 | XCTAssertEqual(board.position.fen, "8/1b4p1/1n1k4/2p3p1/1p6/PP5P/5PP1/R5K1 w - - 1 31") 204 | 205 | board.move(pieceAt: .a3, to: .b4) 206 | XCTAssertEqual(board.position.fen, "8/1b4p1/1n1k4/2p3p1/1P6/1P5P/5PP1/R5K1 b - - 0 31") 207 | 208 | board.move(pieceAt: .c5, to: .b4) 209 | XCTAssertEqual(board.position.fen, "8/1b4p1/1n1k4/6p1/1p6/1P5P/5PP1/R5K1 w - - 0 32") 210 | 211 | board.move(pieceAt: .a1, to: .a5) 212 | XCTAssertEqual(board.position.fen, "8/1b4p1/1n1k4/R5p1/1p6/1P5P/5PP1/6K1 b - - 1 32") 213 | 214 | board.move(pieceAt: .b6, to: .d5) 215 | XCTAssertEqual(board.position.fen, "8/1b4p1/3k4/R2n2p1/1p6/1P5P/5PP1/6K1 w - - 2 33") 216 | 217 | board.move(pieceAt: .f2, to: .f3) 218 | XCTAssertEqual(board.position.fen, "8/1b4p1/3k4/R2n2p1/1p6/1P3P1P/6P1/6K1 b - - 0 33") 219 | 220 | board.move(pieceAt: .b7, to: .c8) 221 | XCTAssertEqual(board.position.fen, "2b5/6p1/3k4/R2n2p1/1p6/1P3P1P/6P1/6K1 w - - 1 34") 222 | 223 | board.move(pieceAt: .g1, to: .f2) 224 | XCTAssertEqual(board.position.fen, "2b5/6p1/3k4/R2n2p1/1p6/1P3P1P/5KP1/8 b - - 2 34") 225 | 226 | board.move(pieceAt: .c8, to: .f5) 227 | XCTAssertEqual(board.position.fen, "8/6p1/3k4/R2n1bp1/1p6/1P3P1P/5KP1/8 w - - 3 35") 228 | 229 | board.move(pieceAt: .a5, to: .a7) 230 | XCTAssertEqual(board.position.fen, "8/R5p1/3k4/3n1bp1/1p6/1P3P1P/5KP1/8 b - - 4 35") 231 | 232 | board.move(pieceAt: .g7, to: .g6) 233 | XCTAssertEqual(board.position.fen, "8/R7/3k2p1/3n1bp1/1p6/1P3P1P/5KP1/8 w - - 0 36") 234 | 235 | board.move(pieceAt: .a7, to: .a6) 236 | XCTAssertEqual(board.position.fen, "8/8/R2k2p1/3n1bp1/1p6/1P3P1P/5KP1/8 b - - 1 36") 237 | 238 | board.move(pieceAt: .d6, to: .c5) 239 | XCTAssertEqual(board.position.fen, "8/8/R5p1/2kn1bp1/1p6/1P3P1P/5KP1/8 w - - 2 37") 240 | 241 | board.move(pieceAt: .f2, to: .e1) 242 | XCTAssertEqual(board.position.fen, "8/8/R5p1/2kn1bp1/1p6/1P3P1P/6P1/4K3 b - - 3 37") 243 | 244 | board.move(pieceAt: .d5, to: .f4) 245 | XCTAssertEqual(board.position.fen, "8/8/R5p1/2k2bp1/1p3n2/1P3P1P/6P1/4K3 w - - 4 38") 246 | 247 | board.move(pieceAt: .g2, to: .g3) 248 | XCTAssertEqual(board.position.fen, "8/8/R5p1/2k2bp1/1p3n2/1P3PPP/8/4K3 b - - 0 38") 249 | 250 | board.move(pieceAt: .f4, to: .h3) 251 | XCTAssertEqual(board.position.fen, "8/8/R5p1/2k2bp1/1p6/1P3PPn/8/4K3 w - - 0 39") 252 | 253 | board.move(pieceAt: .e1, to: .d2) 254 | XCTAssertEqual(board.position.fen, "8/8/R5p1/2k2bp1/1p6/1P3PPn/3K4/8 b - - 1 39") 255 | 256 | board.move(pieceAt: .c5, to: .b5) 257 | XCTAssertEqual(board.position.fen, "8/8/R5p1/1k3bp1/1p6/1P3PPn/3K4/8 w - - 2 40") 258 | 259 | board.move(pieceAt: .a6, to: .d6) 260 | XCTAssertEqual(board.position.fen, "8/8/3R2p1/1k3bp1/1p6/1P3PPn/3K4/8 b - - 3 40") 261 | 262 | board.move(pieceAt: .b5, to: .c5) 263 | XCTAssertEqual(board.position.fen, "8/8/3R2p1/2k2bp1/1p6/1P3PPn/3K4/8 w - - 4 41") 264 | 265 | board.move(pieceAt: .d6, to: .a6) 266 | XCTAssertEqual(board.position.fen, "8/8/R5p1/2k2bp1/1p6/1P3PPn/3K4/8 b - - 5 41") 267 | 268 | board.move(pieceAt: .h3, to: .f2) 269 | XCTAssertEqual(board.position.fen, "8/8/R5p1/2k2bp1/1p6/1P3PP1/3K1n2/8 w - - 6 42") 270 | 271 | board.move(pieceAt: .g3, to: .g4) 272 | XCTAssertEqual(board.position.fen, "8/8/R5p1/2k2bp1/1p4P1/1P3P2/3K1n2/8 b - - 0 42") 273 | 274 | board.move(pieceAt: .f5, to: .d3) 275 | XCTAssertEqual(board.position.fen, "8/8/R5p1/2k3p1/1p4P1/1P1b1P2/3K1n2/8 w - - 1 43") 276 | 277 | board.move(pieceAt: .a6, to: .e6) 278 | XCTAssertEqual(board.position.fen, "8/8/4R1p1/2k3p1/1p4P1/1P1b1P2/3K1n2/8 b - - 2 43") 279 | 280 | XCTAssertEqual(board.position.piece(at: .d2)?.kind, .king) 281 | } 282 | 283 | } 284 | -------------------------------------------------------------------------------- /Tests/ChessKitTests/Performance/PGNParserPerformanceTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PGNParserPerformanceTests.swift 3 | // ChessKit 4 | // 5 | 6 | @testable import ChessKit 7 | import XCTest 8 | 9 | final class PGNParserPerformanceTests: XCTestCase { 10 | 11 | func testPGNParserPerformance() throws { 12 | // Clock Monotonic Time: 0.011 s 13 | // CPU Cycles: 32097.649 kC 14 | // CPU Instructions Retired: 107155.368 kI 15 | // CPU Time: 0.010 s 16 | // Memory Peak Physical: 34026.138 kB 17 | // Memory Physical: 160.563 kB 18 | measure( 19 | metrics: [ 20 | XCTClockMetric(), 21 | XCTCPUMetric(), 22 | XCTMemoryMetric() 23 | ], 24 | block: parsePGN 25 | ) 26 | } 27 | 28 | private func parsePGN() { 29 | let parsedGame = try? PGNParser.parse(game: Game.fischerSpassky) 30 | XCTAssertNotNil(parsedGame) 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /Tests/ChessKitTests/PieceTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PieceTests.swift 3 | // ChessKitTests 4 | // 5 | 6 | @testable import ChessKit 7 | import Testing 8 | 9 | struct PieceTests { 10 | 11 | @Test func notation() { 12 | let pawn = Piece.Kind.pawn 13 | #expect(pawn.notation == "") 14 | #expect(String(describing: pawn) == "Pawn") 15 | 16 | let bishop = Piece.Kind.bishop 17 | #expect(bishop.notation == "B") 18 | #expect(String(describing: bishop) == "Bishop") 19 | 20 | let knight = Piece.Kind.knight 21 | #expect(knight.notation == "N") 22 | #expect(String(describing: knight) == "Knight") 23 | 24 | let rook = Piece.Kind.rook 25 | #expect(rook.notation == "R") 26 | #expect(String(describing: rook) == "Rook") 27 | 28 | let queen = Piece.Kind.queen 29 | #expect(queen.notation == "Q") 30 | #expect(String(describing: queen) == "Queen") 31 | 32 | let king = Piece.Kind.king 33 | #expect(king.notation == "K") 34 | #expect(String(describing: king) == "King") 35 | } 36 | 37 | @Test func pieceColor() { 38 | let white = Piece.Color.white 39 | #expect(white.rawValue == "w") 40 | #expect(white.opposite == .black) 41 | #expect(String(describing: white) == "White") 42 | 43 | let black = Piece.Color.black 44 | #expect(black.rawValue == "b") 45 | #expect(black.opposite == .white) 46 | #expect(String(describing: black) == "Black") 47 | } 48 | 49 | @Test func fenRepresentation() { 50 | let sq = Square.a1 51 | 52 | let wP = Piece(fen: "P", square: sq) 53 | #expect(wP?.color == .white) 54 | #expect(wP?.kind == .pawn) 55 | #expect(wP?.square == sq) 56 | 57 | let wB = Piece(fen: "B", square: sq) 58 | #expect(wB?.color == .white) 59 | #expect(wB?.kind == .bishop) 60 | #expect(wB?.square == sq) 61 | 62 | let wN = Piece(fen: "N", square: sq) 63 | #expect(wN?.color == .white) 64 | #expect(wN?.kind == .knight) 65 | #expect(wN?.square == sq) 66 | 67 | let wR = Piece(fen: "R", square: sq) 68 | #expect(wR?.color == .white) 69 | #expect(wR?.kind == .rook) 70 | #expect(wR?.square == sq) 71 | 72 | let wQ = Piece(fen: "Q", square: sq) 73 | #expect(wQ?.color == .white) 74 | #expect(wQ?.kind == .queen) 75 | #expect(wQ?.square == sq) 76 | 77 | let wK = Piece(fen: "K", square: sq) 78 | #expect(wK?.color == .white) 79 | #expect(wK?.kind == .king) 80 | #expect(wK?.square == sq) 81 | 82 | let bP = Piece(fen: "p", square: sq) 83 | #expect(bP?.color == .black) 84 | #expect(bP?.kind == .pawn) 85 | #expect(bP?.square == sq) 86 | 87 | let bB = Piece(fen: "b", square: sq) 88 | #expect(bB?.color == .black) 89 | #expect(bB?.kind == .bishop) 90 | #expect(bB?.square == sq) 91 | 92 | let bN = Piece(fen: "n", square: sq) 93 | #expect(bN?.color == .black) 94 | #expect(bN?.kind == .knight) 95 | #expect(bN?.square == sq) 96 | 97 | let bR = Piece(fen: "r", square: sq) 98 | #expect(bR?.color == .black) 99 | #expect(bR?.kind == .rook) 100 | #expect(bR?.square == sq) 101 | 102 | let bQ = Piece(fen: "q", square: sq) 103 | #expect(bQ?.color == .black) 104 | #expect(bQ?.kind == .queen) 105 | #expect(bQ?.square == sq) 106 | 107 | let bK = Piece(fen: "k", square: sq) 108 | #expect(bK?.color == .black) 109 | #expect(bK?.kind == .king) 110 | #expect(bK?.square == sq) 111 | } 112 | 113 | @Test func invalidFenRepresentation() { 114 | let invalidFen = Piece(fen: "invalid", square: .a1) 115 | #expect(invalidFen == nil) 116 | } 117 | 118 | } 119 | -------------------------------------------------------------------------------- /Tests/ChessKitTests/PositionTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PositionTests.swift 3 | // ChessKit 4 | // 5 | 6 | @testable import ChessKit 7 | import Testing 8 | 9 | struct PositionTests { 10 | 11 | @Test func initializer() { 12 | let whitePawn = Piece(.pawn, color: .white, square: .e5) 13 | let blackPawn = Piece(.pawn, color: .black, square: .d5) 14 | 15 | let position1 = Position( 16 | pieces: [whitePawn, blackPawn], 17 | sideToMove: .white, 18 | legalCastlings: .init(), 19 | enPassant: .init(pawn: blackPawn), 20 | clock: .init() 21 | ) 22 | 23 | #expect(position1.enPassantIsPossible) 24 | 25 | let position2 = Position( 26 | pieces: [whitePawn, blackPawn], 27 | sideToMove: .white, 28 | legalCastlings: .init(), 29 | clock: .init() 30 | ) 31 | 32 | #expect(!position2.enPassantIsPossible) 33 | } 34 | 35 | @Test func sideToMove() { 36 | var position = Position.standard 37 | #expect(position.sideToMove == .white) 38 | 39 | position.move(pieceAt: .e2, to: .e4) 40 | #expect(position.sideToMove == .black) 41 | 42 | position.move(pieceAt: .e7, to: .e5) 43 | #expect(position.sideToMove == .white) 44 | } 45 | 46 | @Test func moveNonexistentPieces() { 47 | var position = Position.standard 48 | 49 | #expect(position.move(pieceAt: .a3, to: .a4) == nil) 50 | #expect(position.move(.init(.pawn, color: .white, square: .a3), to: .a4) == nil) 51 | } 52 | 53 | } 54 | 55 | // MARK: - Deprecated Tests 56 | extension PositionTests { 57 | 58 | @available(*, deprecated) 59 | @Test func positionToggleSideToMove() { 60 | var position = Position.standard 61 | let initialSideToMove = position.sideToMove 62 | position.toggleSideToMove() 63 | #expect(initialSideToMove == position.sideToMove) 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /Tests/ChessKitTests/SpecialMoveTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SpecialMoveTests.swift 3 | // ChessKitTests 4 | // 5 | 6 | @testable import ChessKit 7 | import Testing 8 | 9 | struct SpecialMoveTests { 10 | 11 | @Test func legalCastlingInvalidationForKings() { 12 | let blackKing = Piece(.king, color: .black, square: .e8) 13 | let whiteKing = Piece(.king, color: .white, square: .e1) 14 | 15 | var legalCastlings = LegalCastlings() 16 | legalCastlings.invalidateCastling(for: blackKing) 17 | #expect(!legalCastlings.contains(.bK)) 18 | #expect(!legalCastlings.contains(.bQ)) 19 | #expect(legalCastlings.contains(.wK)) 20 | #expect(legalCastlings.contains(.wQ)) 21 | 22 | legalCastlings.invalidateCastling(for: whiteKing) 23 | #expect(!legalCastlings.contains(.bK)) 24 | #expect(!legalCastlings.contains(.bQ)) 25 | #expect(!legalCastlings.contains(.wK)) 26 | #expect(!legalCastlings.contains(.wQ)) 27 | } 28 | 29 | @Test func legalCastlingInvalidationForRooks() { 30 | let blackKingsideRook = Piece(.rook, color: .black, square: .h8) 31 | let blackQueensideRook = Piece(.rook, color: .black, square: .a8) 32 | let whiteKingsideRook = Piece(.rook, color: .white, square: .h1) 33 | let whiteQueensideRook = Piece(.rook, color: .white, square: .a1) 34 | 35 | var legalCastlings = LegalCastlings() 36 | legalCastlings.invalidateCastling(for: blackKingsideRook) 37 | #expect(!legalCastlings.contains(.bK)) 38 | #expect(legalCastlings.contains(.bQ)) 39 | #expect(legalCastlings.contains(.wK)) 40 | #expect(legalCastlings.contains(.wQ)) 41 | 42 | legalCastlings.invalidateCastling(for: blackQueensideRook) 43 | #expect(!legalCastlings.contains(.bK)) 44 | #expect(!legalCastlings.contains(.bQ)) 45 | #expect(legalCastlings.contains(.wK)) 46 | #expect(legalCastlings.contains(.wQ)) 47 | 48 | legalCastlings.invalidateCastling(for: whiteKingsideRook) 49 | #expect(!legalCastlings.contains(.bK)) 50 | #expect(!legalCastlings.contains(.bQ)) 51 | #expect(!legalCastlings.contains(.wK)) 52 | #expect(legalCastlings.contains(.wQ)) 53 | 54 | legalCastlings.invalidateCastling(for: whiteQueensideRook) 55 | #expect(!legalCastlings.contains(.bK)) 56 | #expect(!legalCastlings.contains(.bQ)) 57 | #expect(!legalCastlings.contains(.wK)) 58 | #expect(!legalCastlings.contains(.wQ)) 59 | } 60 | 61 | @Test func enPassantCaptureSquare() { 62 | let blackPawn = Piece(.pawn, color: .black, square: .d5) 63 | let blackEnPassant = EnPassant(pawn: blackPawn) 64 | #expect(blackEnPassant.captureSquare == Square.d6) 65 | #expect(blackEnPassant.couldBeCaptured(by: Piece(.pawn, color: .white, square: .e5))) 66 | #expect(blackEnPassant.couldBeCaptured(by: Piece(.pawn, color: .white, square: .c5))) 67 | #expect(!blackEnPassant.couldBeCaptured(by: Piece(.pawn, color: .black, square: .e5))) 68 | #expect(!blackEnPassant.couldBeCaptured(by: Piece(.pawn, color: .white, square: .f5))) 69 | #expect(!blackEnPassant.couldBeCaptured(by: Piece(.pawn, color: .white, square: .b5))) 70 | #expect(!blackEnPassant.couldBeCaptured(by: Piece(.bishop, color: .white, square: .c5))) 71 | 72 | let whitePawn = Piece(.pawn, color: .white, square: .d4) 73 | let whiteEnPassant = EnPassant(pawn: whitePawn) 74 | #expect(whiteEnPassant.captureSquare == Square.d3) 75 | #expect(whiteEnPassant.couldBeCaptured(by: Piece(.pawn, color: .black, square: .e4))) 76 | #expect(whiteEnPassant.couldBeCaptured(by: Piece(.pawn, color: .black, square: .c4))) 77 | #expect(!whiteEnPassant.couldBeCaptured(by: Piece(.pawn, color: .white, square: .e4))) 78 | #expect(!whiteEnPassant.couldBeCaptured(by: Piece(.pawn, color: .black, square: .f4))) 79 | #expect(!whiteEnPassant.couldBeCaptured(by: Piece(.pawn, color: .black, square: .b4))) 80 | #expect(!whiteEnPassant.couldBeCaptured(by: Piece(.bishop, color: .black, square: .c4))) 81 | } 82 | 83 | } 84 | -------------------------------------------------------------------------------- /Tests/ChessKitTests/SquareTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SquareTests.swift 3 | // ChessKitTests 4 | // 5 | 6 | @testable import ChessKit 7 | import Testing 8 | 9 | struct SquareTests { 10 | 11 | @Test func notation() { 12 | #expect(Square.a1.notation == "a1") 13 | #expect(Square.h1.notation == "h1") 14 | #expect(Square.a8.notation == "a8") 15 | #expect(Square.h8.notation == "h8") 16 | 17 | #expect(Square("a1") == .a1) 18 | #expect(Square("h1") == .h1) 19 | #expect(Square("a8") == .a8) 20 | #expect(Square("h8") == .h8) 21 | } 22 | 23 | @Test func invalidNotation() { 24 | #expect(Square("invalid") == .a1) 25 | } 26 | 27 | @Test func squareColor() { 28 | #expect(Square.a1.color == .dark) 29 | #expect(Square.h1.color == .light) 30 | #expect(Square.a8.color == .light) 31 | #expect(Square.h8.color == .dark) 32 | } 33 | 34 | @Test func fileNumber() { 35 | #expect(Square.File.a.number == 1) 36 | #expect(Square.File.h.number == 8) 37 | 38 | #expect(Square.File(1) == .a) 39 | #expect(Square.File(2) == .b) 40 | #expect(Square.File(3) == .c) 41 | #expect(Square.File(4) == .d) 42 | #expect(Square.File(5) == .e) 43 | #expect(Square.File(6) == .f) 44 | #expect(Square.File(7) == .g) 45 | #expect(Square.File(8) == .h) 46 | } 47 | 48 | @Test func invalidFileNumber() { 49 | #expect(Square.File(-10) == .a) 50 | #expect(Square.File(100) == .h) 51 | } 52 | 53 | @Test func directionalSquares() { 54 | #expect(Square.a1.left == .a1) 55 | #expect(Square.b1.left == .a1) 56 | #expect(Square.h1.left == .g1) 57 | 58 | #expect(Square.a1.right == .b1) 59 | #expect(Square.g1.right == .h1) 60 | #expect(Square.h1.right == .h1) 61 | 62 | #expect(Square.a8.up == .a8) 63 | #expect(Square.a7.up == .a8) 64 | #expect(Square.a1.up == .a2) 65 | 66 | #expect(Square.a1.down == .a1) 67 | #expect(Square.a2.down == .a1) 68 | #expect(Square.a8.down == .a7) 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /Tests/ChessKitTests/Utilities/MockBoardDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockBoardDelegate.swift 3 | // ChessKit 4 | // 5 | 6 | import ChessKit 7 | 8 | final class MockBoardDelegate: BoardDelegate { 9 | private let willPromote: (@Sendable (Move) -> Void)? 10 | private let didPromote: (@Sendable (Move) -> Void)? 11 | private let didCheckKing: (@Sendable (Piece.Color) -> Void)? 12 | private let didEnd: (@Sendable (Board.EndResult) -> Void)? 13 | 14 | init( 15 | willPromote: (@Sendable (Move) -> Void)? = nil, 16 | didPromote: (@Sendable (Move) -> Void)? = nil, 17 | didCheckKing: (@Sendable (Piece.Color) -> Void)? = nil, 18 | didEnd: (@Sendable (Board.EndResult) -> Void)? = nil 19 | ) { 20 | self.willPromote = willPromote 21 | self.didPromote = didPromote 22 | self.didCheckKing = didCheckKing 23 | self.didEnd = didEnd 24 | } 25 | 26 | func willPromote(with move: Move) { 27 | willPromote?(move) 28 | } 29 | 30 | func didPromote(with move: Move) { 31 | didPromote?(move) 32 | } 33 | 34 | func didCheckKing(ofColor color: Piece.Color) { 35 | didCheckKing?(color) 36 | } 37 | 38 | func didEnd(with result: Board.EndResult) { 39 | didEnd?(result) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Tests/ChessKitTests/Utilities/SampleGames.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SampleGames.swift 3 | // ChessKit 4 | // 5 | 6 | @testable import struct ChessKit.Game 7 | 8 | extension Game { 9 | static let fischerSpassky = 10 | """ 11 | [Event "F/S Return Match"] 12 | [Site "Belgrade, Serbia JUG"] 13 | [Date "1992.11.04"] 14 | [Round "29"] 15 | [White "Fischer, Robert J."] 16 | [Black "Spassky, Boris V."] 17 | [Result "1/2-1/2"] 18 | [Annotator "Mr. Annotator"] 19 | [PlyCount "85"] 20 | [TimeControl "?"] 21 | [Time "??:??:??"] 22 | [Termination "normal"] 23 | [Mode "OTB"] 24 | [FEN "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"] 25 | [SetUp "1"] 26 | [CustomTag "test"] 27 | 28 | 1. e4 $4 e5 $3 2. Nf3 Nc6 3. Bb5 a6 {This opening is called the Ruy Lopez.} 29 | 4. Ba4 {test comment} Nf6 5. O-O Be7 6. Re1 b5 7. Bb3 d6 $135 8. c3 O-O 9. h3 Nb8 10. d4 Nbd7 30 | 11. c4 c6 12. cxb5 axb5 13. Nc3 Bb7 14. Bg5 b4 15. Nb1 h6 16. Bh4 c5 17. dxe5 31 | Nxe4 18. Bxe7 Qxe7 19. exd6 Qf6 20. Nbd2 Nxd6 21. Nc4 Nxc4 22. Bxc4 Nb6 32 | 23. Ne5 Rae8 24. Bxf7+ Rxf7 25. Nxf7 Rxe1+ 26. Qxe1 Kxf7 27. Qe3 Qg5 28. Qxg5 33 | hxg5 29. b3 Ke6 30. a3 Kd6 31. axb4 cxb4 32. Ra5 Nd5 33. f3 Bc8 34. Kf2 Bf5 34 | 35. Ra7 g6 36. Ra6+ Kc5 37. Ke1 Nf4 38. g3 Nxh3 39. Kd2 Kb5 40. Rd6 Kc5 41. Ra6 35 | Nf2 42. g4 Bd3 43. Re6 1/2-1/2 36 | """ 37 | } 38 | -------------------------------------------------------------------------------- /Tests/ChessKitTests/Utilities/SamplePositions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SamplePositions.swift 3 | // ChessKit 4 | // 5 | 6 | @testable import struct ChessKit.Position 7 | 8 | extension Position { 9 | static let complex = Position(fen: "r1b1k1nr/p2p1pNp/n2B4/1p1NP2P/6P1/3P1Q2/P1P1K3/q5b1 b kq - 0 20")! 10 | 11 | static let ep = Position(fen: "rnbqkbnr/ppppp1pp/8/8/4Pp2/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1")! 12 | 13 | static let castling = Position(fen: "4k2r/6r1/8/8/8/8/3R4/R3K3 w Qk - 0 1")! 14 | 15 | static let fiftyMove = Position(fen: "8/5k2/3p4/1p1Pp2p/pP2Pp1P/P4P1K/8/8 b - - 99 50")! 16 | } 17 | --------------------------------------------------------------------------------