├── .github └── workflows │ └── swift.yml ├── .gitignore ├── Icon.sketch ├── Jumbotron.png ├── LICENSE ├── Package.swift ├── README.md ├── Sources ├── Evaluation.swift ├── Extensions.swift ├── GameTree.swift ├── MiniMaxTreeSearch.swift ├── MonteCarloTreeSearch.swift ├── MonteCarloTreeSearchPolicy.swift ├── NegaMaxStrategy.swift ├── ParallelMonteCarloTreeSearch.swift ├── RandomStrategy.swift ├── Score.swift ├── Strategist.swift ├── Strategy.swift └── TreeSearchPolicy.swift ├── Strategist.podspec ├── Strategist.xcodeproj ├── Configs │ └── Project.xcconfig ├── StrategistTestSuite_Info.plist ├── Strategist_Info.plist ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcshareddata │ └── xcschemes │ ├── Strategist.xcscheme │ └── xcschememanagement.plist └── Tests └── Strategist ├── FourtyTwo ├── FourtyTwo.swift └── FourtyTwoTests.swift ├── LinuxMain.swift ├── StrategistTests.swift └── TicTacToe ├── TicTacToe.swift └── TicTacToeTests.swift /.github/workflows/swift.yml: -------------------------------------------------------------------------------- 1 | name: Swift 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: macos-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Build 17 | run: swift build -v 18 | - name: Run tests 19 | run: swift test -v 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/swift 3 | 4 | ### Swift ### 5 | # Xcode 6 | # 7 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 8 | 9 | ## Build generated 10 | build/ 11 | DerivedData/ 12 | 13 | ## Various settings 14 | *.pbxuser 15 | !default.pbxuser 16 | *.mode1v3 17 | !default.mode1v3 18 | *.mode2v3 19 | !default.mode2v3 20 | *.perspectivev3 21 | !default.perspectivev3 22 | xcuserdata/ 23 | 24 | ## Other 25 | *.moved-aside 26 | *.xccheckout 27 | *.xcscmblueprint 28 | 29 | ## Obj-C/Swift specific 30 | *.hmap 31 | *.ipa 32 | *.dSYM.zip 33 | *.dSYM 34 | 35 | ## Playgrounds 36 | timeline.xctimeline 37 | playground.xcworkspace 38 | 39 | # Swift Package Manager 40 | # 41 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 42 | # Packages/ 43 | # Package.pins 44 | .build/ 45 | 46 | # CocoaPods - Refactored to standalone file 47 | 48 | # Carthage - Refactored to standalone file 49 | 50 | # fastlane 51 | # 52 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 53 | # screenshots whenever they are needed. 54 | # For more information about the recommended setup visit: 55 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 56 | 57 | fastlane/report.xml 58 | fastlane/Preview.html 59 | fastlane/screenshots 60 | fastlane/test_output 61 | 62 | # End of https://www.gitignore.io/api/swift 63 | 64 | # Created by https://www.gitignore.io/api/spm 65 | 66 | #!! ERROR: spm is undefined. Use list command to see defined gitignore types !!# 67 | 68 | # End of https://www.gitignore.io/api/spm 69 | 70 | # Created by https://www.gitignore.io/api/osx 71 | 72 | ### OSX ### 73 | *.DS_Store 74 | .AppleDouble 75 | .LSOverride 76 | 77 | # Icon must end with two \r 78 | Icon 79 | 80 | # Thumbnails 81 | ._* 82 | 83 | # Files that might appear in the root of a volume 84 | .DocumentRevisions-V100 85 | .fseventsd 86 | .Spotlight-V100 87 | .TemporaryItems 88 | .Trashes 89 | .VolumeIcon.icns 90 | .com.apple.timemachine.donotpresent 91 | 92 | # Directories potentially created on remote AFP share 93 | .AppleDB 94 | .AppleDesktop 95 | Network Trash Folder 96 | Temporary Items 97 | .apdisk 98 | 99 | # End of https://www.gitignore.io/api/osx 100 | -------------------------------------------------------------------------------- /Icon.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/regexident/Strategist/8fdb4ed79cb374d97005d1e5d3ed6e025090caec/Icon.sketch -------------------------------------------------------------------------------- /Jumbotron.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/regexident/Strategist/8fdb4ed79cb374d97005d1e5d3ed6e025090caec/Jumbotron.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | import PackageDescription 2 | 3 | let package = Package( 4 | name: "Strategist" 5 | ) 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Logo](Jumbotron.png) 2 | 3 | # Strategist 4 | 5 | **Strategist** provides algorithms for building strong immutable AIs for round-based games. 6 | 7 | ## Provided Algorithms: 8 | 9 | - **Minimax** Tree Search (with alpha-beta pruning) 10 | - **Negamax** Tree Search (with alpha-beta pruning) 11 | - **Monte Carlo** Tree Search 12 | 13 | ## Example usage 14 | 15 | #### Tic Tac Toe (Minimax Tree Search) 16 | 17 | ```swift 18 | typealias Game = TicTacToeGame 19 | typealias Player = TicTacToePlayer 20 | typealias Policy = SimpleTreeSearchPolicy 21 | typealias Strategy = MiniMaxTreeSearch 22 | 23 | let players: [Player] = [.X, .O] // => [Human, Ai] 24 | var game = Game(players: players) 25 | let policy = Policy(maxMoves: 10, maxExplorationDepth: 10) 26 | let strategy = Strategy(policy: policy) 27 | while !game.evaluate().isFinal { 28 | let move: TicTacToeMove 29 | if game.currentPlayer == .White { 30 | move = askAndWaitForHumanPlayersMove(game.currentPlayer) 31 | } else { 32 | move = strategy.randomMaximizingMove(game)! 33 | } 34 | game = game.update(move) // moves turn and game state forward 35 | } 36 | print("Game ended with \(game.currentPlayer)'s \(game.evaluate()).") 37 | ``` 38 | 39 | #### Chess (Monte Carlo Tree Search) 40 | 41 | ```swift 42 | typealias Game = ChessGame 43 | typealias Heuristic = UpperConfidenceBoundHeuristic 44 | typealias Policy = SimpleMonteCarloTreeSearchPolicy 45 | typealias Strategy = MonteCarloTreeSearch 46 | 47 | let players: [Player] = [.White, .Black] // => [Human, Ai] 48 | var game = Game(players: players) 49 | let heuristic = Heuristic(c: sqrt(2.0)) 50 | let policy = Policy( 51 | maxMoves: 100, 52 | maxExplorationDepth: 10, 53 | maxSimulationDepth: 10, 54 | simulations: 100, 55 | pruningThreshold: 1000, 56 | scoringHeuristic: heuristic 57 | ) 58 | var strategy = Strategy( 59 | game: game, 60 | player: players[0], 61 | policy: policy 62 | ) 63 | while !game.evaluate().isFinal { 64 | let move: ChessMove 65 | if game.currentPlayer == .White { 66 | move = askAndWaitForHumanPlayersMove(game.currentPlayer) 67 | } else { 68 | while stillPlentyOfTime() { 69 | strategy = strategy.refine() 70 | } 71 | move = strategy.randomMaximizingMove(game)! 72 | } 73 | strategy = strategy.update(move) 74 | game = game.update(move) 75 | } 76 | print("Game ended with \(game.currentPlayer)'s \(game.evaluate()).") 77 | ``` 78 | 79 | ## Documentation 80 | 81 | Online API documentation can be found here [here](https://regexident.github.io/Strategist/). 82 | 83 | ## Installation 84 | 85 | ### Swift Package Manager 86 | 87 | .Package(url: "https://github.com/regexident/Strategist.git") 88 | 89 | ### Carthage ([site](https://github.com/Carthage/Carthage)) 90 | 91 | github 'regexident/Strategist' 92 | 93 | ### CocoaPods ([site](http://cocoapods.org/)) 94 | 95 | pod 'Strategist' 96 | 97 | ## License 98 | 99 | **Strategist** is available under the [**MPL-2.0**](https://www.mozilla.org/en-US/MPL/2.0/) ([tl;dr](https://tldrlegal.com/license/mozilla-public-license-2.0-(mpl-2))) license (see `LICENSE` file). 100 | -------------------------------------------------------------------------------- /Sources/Evaluation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Evaluation.swift 3 | // Strategist 4 | // 5 | // Created by Vincent Esche on 06/06/16. 6 | // Copyright © 2016 Vincent Esche. All rights reserved. 7 | // 8 | 9 | /// Game state evaluation 10 | public enum Evaluation { 11 | /// Evaluation of a victory with additional score value 12 | case victory(T) 13 | /// Evaluation of a defeat with additional score value 14 | case defeat(T) 15 | /// Evaluation of a draw additional score value 16 | case draw(T) 17 | /// Evaluation of an ongoing game with additional score value 18 | case ongoing(T) 19 | 20 | /// Checks whether `self` is a not ongoing. 21 | /// 22 | /// - returns: `false` iff `self` is `.Ongoing(_)`, otherwise `true` 23 | public var isFinal: Bool { 24 | switch self { 25 | case .ongoing(_): return false 26 | default: return true 27 | } 28 | } 29 | 30 | /// Checks whether `self` is a victory. 31 | /// 32 | /// - returns: `true` iff `self` is `.Victory(_)`, otherwise `false` 33 | public var isVictory: Bool { 34 | switch self { 35 | case .victory(_): return true 36 | default: return false 37 | } 38 | } 39 | 40 | /// Checks whether `self` is a defeat. 41 | /// 42 | /// - returns: `true` iff `self` is `.Defeat(_)`, otherwise `false` 43 | public var isDefeat: Bool { 44 | switch self { 45 | case .defeat(_): return true 46 | default: return false 47 | } 48 | } 49 | 50 | /// Checks whether `self` is a draw. 51 | /// 52 | /// - returns: `true` iff `self` is `.Draw(_)`, otherwise `false` 53 | public var isDraw: Bool { 54 | switch self { 55 | case .draw(_): return true 56 | default: return false 57 | } 58 | } 59 | 60 | /// Inverses `self` by swapping `.Victory()` with `.Defeat()` and the value (in all cases). 61 | /// 62 | /// - returns: `.Victory(-v)` iff `self` is `.Defeat(v)` and vice versa, otherwise `.Draw|Ongoing(-v)` 63 | public func inverse() -> Evaluation { 64 | switch self { 65 | case let .victory(value): return .defeat(value.inverse()) 66 | case let .defeat(value): return .victory(value.inverse()) 67 | case let .draw(value): return .draw(value.inverse()) 68 | case let .ongoing(value): return .ongoing(value.inverse()) 69 | } 70 | } 71 | 72 | /// The worst possible evaluation 73 | /// 74 | /// - returns: `.Defeat(-Double.infinity)` 75 | public static var min: Evaluation { 76 | return .defeat(T.min) 77 | } 78 | /// The best possible evaluation 79 | /// 80 | /// - returns: `.Victory(Double.infinity)` 81 | public static var max: Evaluation { 82 | return .victory(T.max) 83 | } 84 | } 85 | 86 | extension Evaluation : Equatable {} 87 | 88 | public func ==(lhs: Evaluation, rhs: Evaluation) -> Bool { 89 | switch (lhs, rhs) { 90 | case (.victory, .victory): return true 91 | case (.defeat, .defeat): return true 92 | case (.draw, .draw): return true 93 | case let (.ongoing(l), .ongoing(r)): return l == r 94 | default: return false 95 | } 96 | } 97 | 98 | extension Evaluation : Comparable {} 99 | 100 | public func <(lhs: Evaluation, rhs: Evaluation) -> Bool { 101 | switch (lhs, rhs) { 102 | case let (.defeat(l), .defeat(r)): return l < r 103 | case let (.ongoing(l), .ongoing(r)): return l < r 104 | case let (.victory(l), .victory(r)): return l < r 105 | case (.defeat(_), _): return true 106 | case (_, .victory(_)): return true 107 | default: return false 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /Sources/Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Extensions.swift 3 | // Strategist 4 | // 5 | // Created by Vincent Esche on 08/06/16. 6 | // Copyright © 2016 Vincent Esche. All rights reserved. 7 | // 8 | 9 | extension IteratorProtocol { 10 | /// Take first `0.. Take { 12 | return Take(base: self, upperBound: upperBound) 13 | } 14 | } 15 | 16 | /// Take first `0..: IteratorProtocol { 18 | typealias Base = G 19 | typealias Element = Base.Element 20 | 21 | var base: Base 22 | var upperBound: Int 23 | 24 | init(base: Base, upperBound: Int) { 25 | self.base = base 26 | self.upperBound = upperBound 27 | } 28 | 29 | mutating func next() -> Element? { 30 | guard self.upperBound > 0 else { 31 | return nil 32 | } 33 | self.upperBound -= 1 34 | return self.base.next() 35 | } 36 | } 37 | 38 | extension Collection where Index == Int { 39 | /// Select random element from `self`. 40 | /// 41 | /// - complexity: O(1). 42 | /// - returns: Randomly selected element from `self`. 43 | func sample(_ randomSource: RandomSource = Strategist.defaultRandomSource) -> Iterator.Element? { 44 | let count = self.count 45 | guard count > 0 else { 46 | return nil 47 | } 48 | let index = randomSource(UInt32(count)) 49 | return self[Int(index)] 50 | } 51 | } 52 | 53 | extension IteratorProtocol { 54 | /// Select random element from `self`. 55 | /// 56 | /// - complexity: O(`Array(self).count`). 57 | /// - returns: Randomly selected element from `self`. 58 | mutating func sample(_ randomSource: RandomSource = Strategist.defaultRandomSource) -> Element? { 59 | var result = self.next() 60 | var count = 2 61 | while let element = self.next() { 62 | if randomSource(UInt32(count)) == 0 { 63 | result = element 64 | } 65 | count += 1 66 | } 67 | return result 68 | } 69 | } 70 | 71 | extension IteratorProtocol where Element : Comparable { 72 | //Returns a random sample from maximum elements in self or nil if the sequence is empty. 73 | // 74 | /// -complexity: O(elements.count). 75 | mutating func sampleMaxElement(randomSource: RandomSource = Strategist.defaultRandomSource) -> Element? { 76 | return self.sampleMaxElement(randomSource: randomSource) { $0 < $1 } 77 | } 78 | } 79 | 80 | extension IteratorProtocol { 81 | /// Returns a random sample from maximum elements in self or nil if the sequence is empty. 82 | /// 83 | /// - complexity: O(elements.count). 84 | /// - requires: `isOrderedBefore` is a strict weak ordering over `self`. 85 | mutating func sampleMaxElement(randomSource: RandomSource = Strategist.defaultRandomSource, isOrderedBefore: (Element, Element) throws -> Bool) rethrows -> Element? { 86 | guard var maxElement = self.next() else { 87 | return nil 88 | } 89 | var maxElementCount = 0 90 | while let element = self.next() { 91 | if try isOrderedBefore(maxElement, element) { 92 | maxElement = element 93 | maxElementCount = 1 94 | } else if try !isOrderedBefore(element, maxElement) { 95 | if randomSource(UInt32(maxElementCount)) == 0 { 96 | maxElement = element 97 | } 98 | maxElementCount += 1 99 | } 100 | } 101 | return maxElement 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /Sources/GameTree.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GameTree.swift 3 | // Strategist 4 | // 5 | // Created by Vincent Esche on 09/06/16. 6 | // Copyright © 2016 Vincent Esche. All rights reserved. 7 | // 8 | 9 | /// Generic tree representation with non-uniform branching factor. 10 | public indirect enum GameTree { 11 | /// Leaf tree node 12 | case leaf(Node) 13 | /// Branch tree node 14 | case branch(Node, [Edge: GameTree]) 15 | 16 | public var node: Node { 17 | switch self { 18 | case .leaf(let node): 19 | return node 20 | case .branch(let node, _): 21 | return node 22 | } 23 | } 24 | 25 | /// Execute passed closures based on node type. 26 | /// 27 | /// Implemented as: 28 | /// ``` 29 | /// switch self { 30 | /// case .Leaf(let node): 31 | /// return leaf(node) 32 | /// case .Branch(let node, let edges): 33 | /// return branch(node, edges) 34 | /// } 35 | /// ``` 36 | public func analysis(leaf: (Node) -> T, branch: (Node, [Edge: GameTree]) -> T) -> T { 37 | switch self { 38 | case .leaf(let node): 39 | return leaf(node) 40 | case .branch(let node, let edges): 41 | return branch(node, edges) 42 | } 43 | } 44 | 45 | /// Generate dot-formatted ([Graphviz](http://graphviz.org/)) tree representation. 46 | public func customDebugDescription(_ tree: GameTree, parentEdge: Edge? = nil, prefix: String = "root", closure: (Node, Edge?) -> (String, String?)) -> String { 47 | var string = "" 48 | if parentEdge == nil { 49 | string += "digraph GameTree {" 50 | } 51 | let (node, edges) = self.analysis(leaf: { ($0, [:]) }, branch: { ($0, $1) }) 52 | let (nodeLabel, edgeLabel) = closure(node, parentEdge) 53 | let nodeID = prefix 54 | string += "\t\(nodeID) [label=\"\(nodeLabel)\"];" 55 | for (index, (key: edge, value: subtree)) in edges.enumerated() { 56 | let subnodeID = nodeID + "_\(index)" 57 | string += self.customDebugDescription(subtree, parentEdge: edge, prefix: subnodeID, closure: closure) 58 | string += "\t\(nodeID) -> \(subnodeID) [label=\"\(edgeLabel ?? "")\"];" 59 | } 60 | if parentEdge == nil { 61 | string += "}" 62 | } 63 | return string 64 | } 65 | } 66 | 67 | extension GameTree: CustomStringConvertible { 68 | public var description: String { 69 | switch self { 70 | case .leaf(let node): 71 | return ".Leaf(\(node))" 72 | case .branch(let node, let edges): 73 | let edges = edges.map { edge, _ in edge } 74 | return ".Branch(\(node), \(edges))" 75 | } 76 | } 77 | } 78 | 79 | extension GameTree: CustomDebugStringConvertible { 80 | public var debugDescription: String { 81 | return self.customDebugDescription(self) { node, edge in 82 | let nodeLabel = "\(node)" 83 | let edgeLabel = edge.map { "\($0)" } 84 | return (nodeLabel, edgeLabel) 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /Sources/MiniMaxTreeSearch.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MiniMaxTreeSearch.swift 3 | // Strategist 4 | // 5 | // Created by Vincent Esche on 06/06/16. 6 | // Copyright © 2016 Vincent Esche. All rights reserved. 7 | // 8 | 9 | /// Implementation of [Minimax Tree Search](https://en.wikipedia.org/wiki/Minimax#Combinatorial_game_theory) algorithm with [alpha beta pruning](https://en.wikipedia.org/wiki/Alpha%E2%80%93beta_pruning). 10 | /// 11 | /// - note: Due to the lack of internal state a single instance of `MiniMaxTreeSearch` can be shared for several players in a game. 12 | public struct MiniMaxTreeSearch where P.Game == G { 13 | let policy: P 14 | 15 | public init(policy: P) { 16 | self.policy = policy 17 | } 18 | 19 | func minimax(_ game: Game, rootPlayer: G.Player, payload: MiniMaxPayload) -> Evaluation { 20 | let evaluation = game.evaluate(forPlayer: rootPlayer) 21 | guard !self.policy.hasReachedMaxExplorationDepth(payload.depth) && !evaluation.isFinal else { 22 | return evaluation 23 | } 24 | let maximize = game.playersAreAllied((rootPlayer, game.currentPlayer)) 25 | var bestEvaluation = (maximize) ? Evaluation.min : Evaluation.max 26 | var (alpha, beta) = (payload.alpha, payload.beta) 27 | let nextDepth = payload.depth + 1 28 | let moves = game.availableMoves() 29 | let filteredMoves = self.policy.filterMoves(game, depth: payload.depth, moves: moves) 30 | for move in filteredMoves { 31 | let nextState = game.update(move) 32 | let nextPayload = MiniMaxPayload(alpha: alpha, beta: beta, depth: nextDepth) 33 | let evaluation = self.minimax(nextState, rootPlayer: rootPlayer, payload: nextPayload) 34 | if maximize { 35 | alpha = max(alpha, evaluation) 36 | bestEvaluation = max(bestEvaluation, alpha) 37 | } else { 38 | beta = min(beta, evaluation) 39 | bestEvaluation = min(bestEvaluation, beta) 40 | } 41 | if alpha >= beta { 42 | break 43 | } 44 | } 45 | return bestEvaluation 46 | } 47 | } 48 | 49 | extension MiniMaxTreeSearch: Strategy { 50 | public typealias Game = G 51 | 52 | public func evaluatedMoves(_ game: Game) -> AnySequence<(Game.Move, Evaluation)> { 53 | let rootPlayer = game.currentPlayer 54 | let moves = game.availableMoves() 55 | let filteredMoves = self.policy.filterMoves(game, depth: 0, moves: moves) 56 | return AnySequence(filteredMoves.lazy.map { move in 57 | let nextState = game.update(move) 58 | let payload = MiniMaxPayload() 59 | let evaluation = self.minimax(nextState, rootPlayer: rootPlayer, payload: payload) 60 | return (move, evaluation) 61 | }) 62 | } 63 | 64 | public func update(_ move: Game.Move) -> MiniMaxTreeSearch { 65 | return self 66 | } 67 | } 68 | 69 | struct MiniMaxPayload { 70 | let alpha: Evaluation 71 | let beta: Evaluation 72 | let depth: Int 73 | 74 | init() { 75 | self.init(alpha: Evaluation.min, beta: Evaluation.max, depth: 0) 76 | } 77 | 78 | init(alpha: Evaluation, beta: Evaluation, depth: Int) { 79 | self.alpha = alpha 80 | self.beta = beta 81 | self.depth = depth 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /Sources/MonteCarloTreeSearch.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MonteCarloTreeSearch.swift 3 | // Strategist 4 | // 5 | // Created by Vincent Esche on 06/06/16. 6 | // Copyright © 2016 Vincent Esche. All rights reserved. 7 | // 8 | 9 | /// Implementation of [Monte Carlo Tree Search](https://en.wikipedia.org/wiki/Monte_Carlo_tree_search) algorithm. 10 | /// 11 | /// - note: Due to internal state a separate instance of `MonteCarloTreeSearch` has to be used for for each player in a game. 12 | public struct MonteCarloTreeSearch where P: MonteCarloTreeSearchPolicy, P.Game == G, P.Score == G.Score { 13 | 14 | typealias Tree = Strategist.GameTree 15 | 16 | public let game: G 17 | public let player: G.Player 18 | public let policy: P 19 | 20 | let tree: Tree 21 | 22 | public init(game: G, player: G.Player, policy: P) { 23 | let tree = MonteCarloTreeSearch.initialTreeForGame(game, policy: policy) 24 | self.init(game: game, player: player, policy: policy, tree: tree) 25 | } 26 | 27 | init(game: G, player: G.Player, policy: P, tree: Tree) { 28 | self.game = game 29 | self.player = player 30 | self.policy = policy 31 | self.tree = tree 32 | } 33 | 34 | public func update(_ move: G.Move) -> MonteCarloTreeSearch { 35 | let game = self.game.update(move) 36 | let player = self.player 37 | let policy = self.policy 38 | let tree = self.tree.analysis(leaf: { node in 39 | return MonteCarloTreeSearch.initialTreeForGame(self.game, policy: self.policy) 40 | }, branch: { node, edges in 41 | if let index = edges.index(where: { $0.0 == move }) { 42 | return edges[index].1 43 | } else { 44 | return MonteCarloTreeSearch.initialTreeForGame(self.game, policy: self.policy) 45 | } 46 | }) 47 | return MonteCarloTreeSearch(game: game, player: player, policy: policy, tree: tree) 48 | } 49 | 50 | public func refine(_ randomSource: @escaping RandomSource = Strategist.defaultRandomSource) -> MonteCarloTreeSearch { 51 | guard self.player == self.game.currentPlayer else { 52 | return self 53 | } 54 | let game = self.game 55 | let player = self.player 56 | let policy = self.policy 57 | let payload = MonteCarloPayload(game: self.game, randomSource: randomSource) 58 | let tree = self.refineSubtree(self.tree, payload: payload) 59 | return MonteCarloTreeSearch(game: game, player: player, policy: policy, tree: tree) 60 | } 61 | 62 | public func mergeWith(_ other: MonteCarloTreeSearch) -> MonteCarloTreeSearch { 63 | assert(self.game == other.game) 64 | assert(self.player == other.player) 65 | let game = self.game 66 | let player = self.player 67 | let policy = self.policy 68 | let tree = MonteCarloTreeSearch.mergeTrees(lhs: self.tree, rhs: other.tree) 69 | return MonteCarloTreeSearch(game: game, player: player, policy: policy, tree: tree) 70 | } 71 | 72 | func refineSubtree(_ subtree: Tree, payload: MonteCarloPayload) -> Tree { 73 | let refinedSubtree: Tree = subtree.analysis(leaf: { node in 74 | if node.explorable { 75 | return self.refineExplorableSubtree(node, edges: [:], payload: payload) 76 | } else { 77 | return subtree 78 | } 79 | }, branch: { node, edges in 80 | if node.explorable { 81 | return self.refineExplorableSubtree(node, edges: edges, payload: payload) 82 | } else { 83 | return self.refineExploredSubtree(node, edges: edges, payload: payload) 84 | } 85 | }) 86 | guard case .branch(var node, let edges) = refinedSubtree, !node.explorable else { 87 | return refinedSubtree 88 | } 89 | guard self.policy.shouldCollapseTree(node.stats, subtrees: edges.count, depth: payload.explorationDepth) else { 90 | return refinedSubtree 91 | } 92 | node.explorable = true 93 | return .leaf(node) 94 | } 95 | 96 | func refineExplorableSubtree(_ node: TreeNode, edges: [G.Move: Tree], payload: MonteCarloPayload) -> Tree { 97 | let exploredMoves = Set(edges.map { $0.0 }) 98 | let moves = payload.game.availableMoves() 99 | let filteredMoves = policy.filterMoves(payload.game, depth: payload.explorationDepth, moves: moves) 100 | let unexploredMoves = filteredMoves.filter { !exploredMoves.contains($0) } 101 | var refinedEdges = edges 102 | var refinedNode = node 103 | refinedNode.explorable = unexploredMoves.count > 1 104 | let chosenMove = self.policy.simulationMove( 105 | unexploredMoves.makeIterator(), 106 | simulationDepth: 0, 107 | randomSource: payload.randomSource 108 | ) 109 | guard let move = chosenMove else { 110 | return .branch(refinedNode, refinedEdges) 111 | } 112 | let nextPayload = MonteCarloPayload( 113 | game: payload.game.update(move), 114 | randomSource: payload.randomSource, 115 | explorationDepth: payload.explorationDepth + 1 116 | ) 117 | let refinedSubtree = self.simulateSubtree(payload.game.currentPlayer, payload: nextPayload) 118 | let refinedSubnode = refinedSubtree.node 119 | refinedNode.stats += refinedSubnode.stats 120 | if !policy.hasReachedMaxExplorationDepth(payload.explorationDepth) { 121 | refinedEdges[move] = refinedSubtree 122 | } 123 | return .branch(refinedNode, refinedEdges) 124 | } 125 | 126 | func refineExploredSubtree(_ node: TreeNode, edges: [G.Move: Tree], payload: MonteCarloPayload) -> Tree { 127 | let plays = node.stats.plays 128 | var edgesGenerator = edges.makeIterator() 129 | let generator: AnyIterator<(G.Move, TreeStats)> = AnyIterator { 130 | return edgesGenerator.next().map { move, subtree in 131 | return (move, subtree.node.stats) 132 | } 133 | } 134 | guard let move = self.policy.explorationMove(generator, explorationDepth: payload.explorationDepth, plays: plays, randomSource: payload.randomSource) else { 135 | return .branch(node, edges) 136 | } 137 | guard let subtree = edges[move] else { 138 | return .branch(node, edges) 139 | } 140 | var refinedNode = node 141 | refinedNode.stats -= subtree.node.stats 142 | let payload = MonteCarloPayload( 143 | game: payload.game.update(move), 144 | randomSource: payload.randomSource, 145 | explorationDepth: payload.explorationDepth + 1 146 | ) 147 | let refinedSubtree = self.refineSubtree(subtree, payload: payload) 148 | refinedNode.stats += refinedSubtree.node.stats 149 | var refinedEdges = edges 150 | refinedEdges[move] = refinedSubtree 151 | return .branch(refinedNode, refinedEdges) 152 | } 153 | 154 | func simulateSubtree(_ rootPlayer: G.Player, payload: MonteCarloPayload) -> Tree { 155 | var evaluation = payload.game.evaluate(forPlayer: rootPlayer) 156 | guard !evaluation.isFinal else { 157 | let score = self.policy.reward(evaluation) 158 | let stats = TreeStats(score: score, plays: 1) 159 | let node = TreeNode(stats: stats, explorable: false) 160 | return .leaf(node) 161 | } 162 | var game = payload.game 163 | var score = 0 164 | var plays = 0 165 | while self.policy.shouldContinueSimulations(game, simulationCount: plays) { 166 | var simulationDepth = 0 167 | while !self.policy.hasReachedMaxSimulationDepth(simulationDepth) { 168 | evaluation = game.evaluate(forPlayer: rootPlayer) 169 | guard !evaluation.isFinal else { 170 | break 171 | } 172 | let availableMoves = game.availableMoves() 173 | let choice = self.policy.simulationMove(availableMoves, simulationDepth: simulationDepth, randomSource: payload.randomSource) 174 | guard let move = choice else { 175 | break 176 | } 177 | game = game.update(move) 178 | simulationDepth += 1 179 | } 180 | score += self.policy.reward(evaluation) 181 | plays += 1 182 | } 183 | let stats = TreeStats(score: score, plays: plays) 184 | let node = TreeNode(stats: stats, explorable: true) 185 | return .leaf(node) 186 | } 187 | 188 | static func initialTreeForGame(_ game: G, policy: P) -> Tree { 189 | let evaluation = game.evaluate() 190 | let node: TreeNode 191 | if evaluation.isFinal { 192 | let delta = policy.reward(evaluation) 193 | let stats = TreeStats(score: delta, plays: 1) 194 | node = TreeNode(stats: stats, explorable: false) 195 | } else { 196 | let stats = TreeStats(score: 0, plays: 0) 197 | node = TreeNode(stats: stats, explorable: true) 198 | } 199 | return .leaf(node) 200 | } 201 | 202 | typealias Node = TreeNode 203 | typealias Edges = [Game.Move: Tree] 204 | 205 | func isExplorableTree(_ tree: Tree) -> Bool { 206 | switch tree { 207 | case let .branch(node, _): return node.explorable 208 | default: return false 209 | } 210 | } 211 | 212 | static func mergeNodes(_ lhs: Node, _ rhs: Node) -> Node { 213 | let stats = lhs.stats.averageWith(rhs.stats) 214 | let explorable = lhs.explorable && rhs.explorable 215 | return Node(stats: stats, explorable: explorable) 216 | } 217 | 218 | static func mergeEdges(_ lhs: Edges, _ rhs: Edges) -> Edges { 219 | var edges = lhs 220 | for (move, rhsSubtree) in rhs { 221 | if let lhsSubtree = edges[move] { 222 | edges[move] = mergeTrees(lhs: lhsSubtree, rhs: rhsSubtree) 223 | } else { 224 | edges[move] = rhsSubtree 225 | } 226 | } 227 | return edges 228 | } 229 | 230 | static func mergeTrees(lhs: Tree, rhs: Tree) -> Tree { 231 | switch (lhs, rhs) { 232 | case let (.leaf(lhsNode), .leaf(rhsNode)): 233 | let node = mergeNodes(lhsNode, rhsNode) 234 | return .leaf(node) 235 | case let (.leaf(lhsNode), .branch(rhsNode, rhsEdges)): 236 | let node = mergeNodes(lhsNode, rhsNode) 237 | return .branch(node, rhsEdges) 238 | case let (.branch(lhsNode, lhsEdges), .leaf(rhsNode)): 239 | let node = mergeNodes(lhsNode, rhsNode) 240 | return .branch(node, lhsEdges) 241 | case let (.branch(lhsNode, lhsEdges), .branch(rhsNode, rhsEdges)): 242 | let node = mergeNodes(lhsNode, rhsNode) 243 | let edges = mergeEdges(lhsEdges, rhsEdges) 244 | return .branch(node, edges) 245 | } 246 | } 247 | } 248 | 249 | extension MonteCarloTreeSearch: CustomDebugStringConvertible { 250 | public var debugDescription: String { 251 | return self.tree.debugDescription 252 | } 253 | } 254 | 255 | extension MonteCarloTreeSearch: Strategy { 256 | public typealias Game = G 257 | 258 | public func evaluatedMoves(_ game: Game) -> AnySequence<(G.Move, Evaluation)> { 259 | assert(game == self.game) 260 | assert(game.currentPlayer == self.player) 261 | let player = game.currentPlayer 262 | let (node, edges) = self.tree.analysis(leaf: { ($0, [:]) }, branch: { ($0, $1) }) 263 | let parentPlays = node.stats.plays 264 | let moves = game.availableMoves() 265 | let filteredMoves = self.policy.filterMoves(game, depth: 0, moves: moves) 266 | return AnySequence(filteredMoves.lazy.map { move in 267 | if let subtree = edges[move] { 268 | let nextGame = game.update(move) 269 | let evaluation = nextGame.evaluate(forPlayer: player) 270 | switch evaluation { 271 | case .ongoing(_): 272 | let score = self.policy.scoreMove(subtree.node.stats, parentPlays: parentPlays) 273 | return (move, Evaluation.ongoing(score)) 274 | default: 275 | return (move, evaluation) 276 | } 277 | } else { 278 | return (move, .ongoing(Game.Score.mid)) 279 | } 280 | }) 281 | } 282 | } 283 | 284 | public struct TreeStats { 285 | public var wins: Int 286 | public var plays: Int 287 | 288 | init(score: Int, plays: Int) { 289 | self.wins = score 290 | self.plays = plays 291 | } 292 | 293 | func averageWith(_ other: TreeStats) -> TreeStats { 294 | let score = (self.wins + other.wins + 1) / 2 295 | let plays = (self.plays + other.plays + 1) / 2 296 | return TreeStats(score: score, plays: plays) 297 | } 298 | } 299 | 300 | func +(lhs: TreeStats, rhs: TreeStats) -> TreeStats { 301 | return TreeStats(score: lhs.wins + rhs.wins, plays: lhs.plays + rhs.plays) 302 | } 303 | 304 | func -(lhs: TreeStats, rhs: TreeStats) -> TreeStats { 305 | return TreeStats(score: lhs.wins - rhs.wins, plays: lhs.plays - rhs.plays) 306 | } 307 | 308 | func +=(lhs: inout TreeStats, rhs: TreeStats) { 309 | lhs.wins += rhs.wins 310 | lhs.plays += rhs.plays 311 | } 312 | 313 | func -=(lhs: inout TreeStats, rhs: TreeStats) { 314 | lhs.wins -= rhs.wins 315 | lhs.plays -= rhs.plays 316 | } 317 | 318 | extension TreeStats: CustomStringConvertible { 319 | public var description: String { 320 | return "\(self.wins) / \(self.plays)" 321 | } 322 | } 323 | 324 | struct TreeNode { 325 | var stats: TreeStats 326 | var explorable: Bool 327 | 328 | init(stats: TreeStats, explorable: Bool) { 329 | self.stats = stats 330 | self.explorable = explorable 331 | } 332 | } 333 | 334 | struct MonteCarloPayload { 335 | let game: G 336 | let randomSource: RandomSource 337 | let explorationDepth: Int 338 | 339 | init(game: G, randomSource: @escaping RandomSource, explorationDepth: Int = 0) { 340 | self.game = game 341 | self.randomSource = randomSource 342 | self.explorationDepth = explorationDepth 343 | } 344 | } 345 | -------------------------------------------------------------------------------- /Sources/MonteCarloTreeSearchPolicy.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Policy.swift 3 | // Strategist 4 | // 5 | // Created by Vincent Esche on 09/06/16. 6 | // Copyright © 2016 Vincent Esche. All rights reserved. 7 | // 8 | 9 | import Darwin 10 | 11 | /// Heuristic used for scoring moves based on the statistics 12 | /// obtained from previous Monte Carlo simulations. 13 | public protocol ScoringHeuristic { 14 | /// The score type 15 | associatedtype Score: Comparable 16 | 17 | /// Scores a given move based on statistics obtained 18 | /// from previous Monte Carlo simulations. 19 | func scoreMove(_ stats: TreeStats, parentPlays: Int) -> Score 20 | } 21 | 22 | /// Upper Confidence Bound 1 applied to trees ([UCT](https://en.wikipedia.org/wiki/Monte_Carlo_tree_search#Exploration_and_exploitation)) 23 | public struct UpperConfidenceBoundHeuristic: ScoringHeuristic where G.Score == Double { 24 | public typealias Score = Double 25 | 26 | public let c: Double 27 | 28 | public init(c: Double = sqrt(2.0)) { 29 | self.c = c 30 | } 31 | 32 | public func scoreMove(_ moveStats: TreeStats, parentPlays: Int) -> Score { 33 | let wi = Double(moveStats.wins) 34 | let ni = Double(moveStats.plays) 35 | let n = Double(parentPlays) 36 | return (wi / ni) + self.c * sqrt(log(n) / ni) 37 | } 38 | } 39 | 40 | /// Policy for more direct control over a strategy's execution 41 | public protocol MonteCarloTreeSearchPolicy: TreeSearchPolicy, ScoringHeuristic { 42 | /// Whether the strategy should abort a given simulation. 43 | func hasReachedMaxSimulationDepth(_ depth: Int) -> Bool 44 | 45 | /// Whether the strategy should execute another simulation. 46 | func shouldContinueSimulations(_ game: Game, simulationCount: Int) -> Bool 47 | /// Whether the strategy should collapse a given tree into a single leaf node. 48 | func shouldCollapseTree(_ stats: TreeStats, subtrees: Int, depth: Int) -> Bool 49 | 50 | /// Heuristic used for scoring a given move. 51 | func scoreMove(_ stats: TreeStats, parentPlays: Int) -> Score 52 | 53 | /// Calculate reward from an evaluation. 54 | /// 55 | /// #### Example: 56 | /// ``` 57 | /// func reward(evaluation: Evaluation) -> Int { 58 | /// switch evaluation { 59 | /// case .Victory: return 1 60 | /// case .Defeat: return 0 61 | /// default: return 0 62 | /// } 63 | /// } 64 | /// ``` 65 | func reward(_ evaluation: Evaluation) -> Int 66 | 67 | /// Heuristic used for choosing game state subtree to further explore. 68 | func explorationMove(_ availableMoves: M, explorationDepth: Int, plays: Int, randomSource: RandomSource) -> Game.Move? where M.Element == (Game.Move, TreeStats) 69 | /// Heuristic used for choosing game state subtree to further explore. 70 | func simulationMove(_ availableMoves: M, simulationDepth: Int, randomSource: RandomSource) -> Game.Move? where M.Element == Game.Move 71 | } 72 | 73 | /// Simple minimal implementation of `MonteCarloTreeSearchPolicy`. 74 | public struct SimpleMonteCarloTreeSearchPolicy: MonteCarloTreeSearchPolicy where G: Game, G.Score == H.Score, H: ScoringHeuristic { 75 | public typealias Game = G 76 | public typealias Score = H.Score 77 | 78 | public let maxMoves: Int 79 | public let maxExplorationDepth: Int 80 | public let maxSimulationDepth: Int 81 | public let simulations: Int 82 | public let pruningThreshold: Int 83 | public let scoringHeuristic: H 84 | 85 | public init(maxMoves: Int, maxExplorationDepth: Int, maxSimulationDepth: Int, simulations: Int, pruningThreshold: Int, scoringHeuristic: H) { 86 | self.maxMoves = maxMoves 87 | self.maxExplorationDepth = maxExplorationDepth 88 | self.maxSimulationDepth = maxSimulationDepth 89 | self.simulations = simulations 90 | self.pruningThreshold = pruningThreshold 91 | self.scoringHeuristic = scoringHeuristic 92 | } 93 | 94 | public func filterMoves(_ state: Game, depth: Int, moves: M) -> AnyIterator where M.Element == Game.Move { 95 | return AnyIterator(moves.take(self.maxMoves)) 96 | } 97 | 98 | public func hasReachedMaxExplorationDepth(_ depth: Int) -> Bool { 99 | return depth >= self.maxExplorationDepth 100 | } 101 | 102 | public func hasReachedMaxSimulationDepth(_ depth: Int) -> Bool { 103 | return depth >= self.maxSimulationDepth 104 | } 105 | 106 | public func shouldContinueSimulations(_ game: Game, simulationCount: Int) -> Bool { 107 | return simulationCount < self.simulations 108 | } 109 | 110 | public func shouldCollapseTree(_ stats: TreeStats, subtrees: Int, depth: Int) -> Bool { 111 | return (stats.wins == 0) && (stats.plays > self.pruningThreshold) 112 | } 113 | 114 | public func scoreMove(_ moveStats: TreeStats, parentPlays: Int) -> Score { 115 | return self.scoringHeuristic.scoreMove(moveStats, parentPlays: parentPlays) 116 | } 117 | 118 | public func reward(_ evaluation: Evaluation) -> Int { 119 | switch evaluation { 120 | case .victory: return 1 121 | case .defeat: return 0 122 | default: return 0 123 | } 124 | } 125 | 126 | public func explorationMove(_ availableMoves: M, explorationDepth: Int, plays: Int, randomSource: RandomSource) -> Game.Move? where M.Element == (Game.Move, TreeStats) { 127 | var availableMoves = availableMoves 128 | let maxElement = availableMoves.sampleMaxElement(randomSource: randomSource) { lhs, rhs in 129 | let lhsScore = self.scoringHeuristic.scoreMove(lhs.1, parentPlays: plays) 130 | let rhsScore = self.scoringHeuristic.scoreMove(rhs.1, parentPlays: plays) 131 | return lhsScore < rhsScore 132 | } 133 | return maxElement.map { $0.0 } 134 | } 135 | 136 | public func simulationMove(_ availableMoves: M, simulationDepth: Int, randomSource: RandomSource) -> Game.Move? where M.Element == Game.Move { 137 | var availableMoves = availableMoves 138 | return availableMoves.sample(randomSource) 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /Sources/NegaMaxStrategy.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NegaMaxTreeSearch.swift 3 | // Strategist 4 | // 5 | // Created by Vincent Esche on 06/06/16. 6 | // Copyright © 2016 Vincent Esche. All rights reserved. 7 | // 8 | 9 | /// Implementation of [Negamax Tree Search](https://en.wikipedia.org/wiki/Negamax#Negamax_with_alpha_beta_pruning) algorithm with [alpha beta pruning](https://en.wikipedia.org/wiki/Alpha%E2%80%93beta_pruning). 10 | /// 11 | /// - note: Due to the lack of internal state a single instance of `NegaMaxTreeSearch` can be shared for several players in a game. 12 | public struct NegaMaxTreeSearch where P.Game == G { 13 | let policy: P 14 | 15 | public init(policy: P) { 16 | self.policy = policy 17 | } 18 | 19 | func negamax(_ game: Game, rootPlayer: G.Player, payload: NegaMaxPayload) -> Evaluation { 20 | let evaluation = game.evaluate() 21 | guard !self.policy.hasReachedMaxExplorationDepth(payload.depth) && !evaluation.isFinal else { 22 | return evaluation 23 | } 24 | let nextDepth = payload.depth + 1 25 | let currentPlayerIsAlly = game.playersAreAllied((rootPlayer, game.currentPlayer)) 26 | var bestMove = Evaluation.min 27 | var (alpha, beta) = (payload.alpha, payload.beta) 28 | let moves = game.availableMoves() 29 | let filteredMoves = self.policy.filterMoves(game, depth: payload.depth, moves: moves) 30 | for move in filteredMoves { 31 | let nextGame = game.update(move) 32 | let nextPlayerIsAlly = game.playersAreAllied((rootPlayer, nextGame.currentPlayer)) 33 | let (nextAlpha, nextBeta): (Evaluation, Evaluation) 34 | if currentPlayerIsAlly == nextPlayerIsAlly { 35 | (nextAlpha, nextBeta) = (alpha, beta) 36 | } else { 37 | (nextAlpha, nextBeta) = (beta.inverse(), alpha.inverse()) 38 | } 39 | let nextPayload = NegaMaxPayload(alpha: nextAlpha, beta: nextBeta, depth: nextDepth) 40 | let result = self.negamax(nextGame, rootPlayer: rootPlayer, payload: nextPayload) 41 | let evaluation: Evaluation 42 | if currentPlayerIsAlly == nextPlayerIsAlly { 43 | evaluation = result 44 | } else { 45 | evaluation = result.inverse() 46 | } 47 | alpha = max(alpha, evaluation) 48 | bestMove = alpha 49 | if alpha >= beta { 50 | break 51 | } 52 | } 53 | return bestMove 54 | } 55 | } 56 | 57 | extension NegaMaxTreeSearch: Strategy { 58 | 59 | public typealias Game = G 60 | 61 | public func evaluatedMoves(_ game: Game) -> AnySequence<(Game.Move, Evaluation)> { 62 | let player = game.currentPlayer 63 | let moves = game.availableMoves() 64 | let filteredMoves = self.policy.filterMoves(game, depth: 0, moves: moves) 65 | return AnySequence(filteredMoves.lazy.map { move in 66 | let nextGame = game.update(move) 67 | let nextPlayer = nextGame.currentPlayer 68 | let payload = NegaMaxPayload() 69 | let result = self.negamax(nextGame, rootPlayer: player, payload: payload) 70 | let players = (player, nextPlayer) 71 | let nextPlayerIsAlly = game.playersAreAllied(players) 72 | let evaluation = nextPlayerIsAlly ? result : result.inverse() 73 | return (move, evaluation) 74 | }) 75 | } 76 | 77 | public func update(_ move: Game.Move) -> NegaMaxTreeSearch { 78 | return self 79 | } 80 | } 81 | 82 | struct NegaMaxPayload { 83 | let alpha: Evaluation 84 | let beta: Evaluation 85 | let depth: Int 86 | 87 | init() { 88 | self.init(alpha: Evaluation.min, beta: Evaluation.max, depth: 0) 89 | } 90 | 91 | init(alpha: Evaluation, beta: Evaluation, depth: Int) { 92 | self.alpha = alpha 93 | self.beta = beta 94 | self.depth = depth 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /Sources/ParallelMonteCarloTreeSearch.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MonteCarloTreeSearch.swift 3 | // Strategist 4 | // 5 | // Created by Vincent Esche on 06/06/16. 6 | // Copyright © 2016 Vincent Esche. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// Implementation of [Monte Carlo Tree Search](https://en.wikipedia.org/wiki/Monte_Carlo_tree_search) algorithm. 12 | /// 13 | /// - note: Due to internal state a separate instance of `MonteCarloTreeSearch` has to be used for for each player in a game. 14 | public struct ParallelMonteCarloTreeSearch where P: MonteCarloTreeSearchPolicy, P.Game == G, P.Score == G.Score { 15 | 16 | typealias Base = MonteCarloTreeSearch 17 | 18 | let base: Base 19 | let threads: Int 20 | 21 | public var game: G { 22 | return self.base.game 23 | } 24 | 25 | public var player: G.Player { 26 | return self.base.player 27 | } 28 | 29 | public var policy: P { 30 | return self.base.policy 31 | } 32 | 33 | var tree: Base.Tree { 34 | return self.base.tree 35 | } 36 | 37 | public init(game: G, player: G.Player, policy: P, threads: Int) { 38 | let base = MonteCarloTreeSearch(game: game, player: player, policy: policy) 39 | self.init(base: base, threads: threads) 40 | } 41 | 42 | init(base: Base, threads: Int) { 43 | assert(threads > 0) 44 | self.base = base 45 | self.threads = threads 46 | } 47 | 48 | public func update(_ move: G.Move) -> ParallelMonteCarloTreeSearch { 49 | let base = self.base.update(move) 50 | let threads = self.threads 51 | return ParallelMonteCarloTreeSearch(base: base, threads: threads) 52 | } 53 | 54 | public func refine(_ randomSource: @escaping RandomSource = Strategist.defaultRandomSource) -> ParallelMonteCarloTreeSearch { 55 | guard self.player == self.game.currentPlayer else { 56 | return self 57 | } 58 | guard self.threads > 1 else { 59 | let base = self.base.refine(randomSource) 60 | let threads = self.threads 61 | return ParallelMonteCarloTreeSearch(base: base, threads: threads) 62 | } 63 | 64 | let syncQueue = DispatchQueue(label: "Strategist") 65 | 66 | var bases: [Base] = [] 67 | DispatchQueue.concurrentPerform(iterations: self.threads) { i in 68 | let base = self.base.refine(randomSource) 69 | syncQueue.sync { 70 | bases.append(base) 71 | } 72 | } 73 | if self.threads % 2 != 0 { 74 | let lhs = bases.removeLast() 75 | let rhs = bases.removeLast() 76 | let merged = lhs.mergeWith(rhs) 77 | bases.append(merged) 78 | } 79 | var count = bases.count 80 | bases.withUnsafeMutableBufferPointer { buffer in 81 | while bases.count > 1 { 82 | DispatchQueue.concurrentPerform(iterations: count / 2) { i in 83 | buffer[i] = buffer[i].mergeWith(buffer[i + (count / 2)]) 84 | } 85 | } 86 | count /= 2 87 | } 88 | let base = bases[0] 89 | let threads = self.threads 90 | return ParallelMonteCarloTreeSearch(base: base, threads: threads) 91 | } 92 | 93 | public func mergeWith(_ other: ParallelMonteCarloTreeSearch) -> ParallelMonteCarloTreeSearch { 94 | assert(self.base.game == other.base.game) 95 | assert(self.base.player == other.base.player) 96 | let base = self.base.mergeWith(other.base) 97 | let threads = self.threads 98 | return ParallelMonteCarloTreeSearch(base: base, threads: threads) 99 | } 100 | } 101 | 102 | extension ParallelMonteCarloTreeSearch: CustomDebugStringConvertible { 103 | public var debugDescription: String { 104 | return self.base.debugDescription 105 | } 106 | } 107 | 108 | extension ParallelMonteCarloTreeSearch: Strategy { 109 | public typealias Game = G 110 | 111 | public func evaluatedMoves(_ game: Game) -> AnySequence<(G.Move, Evaluation)> { 112 | assert(game == self.game) 113 | assert(game.currentPlayer == self.player) 114 | return self.base.evaluatedMoves(game) 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /Sources/RandomStrategy.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RandomStrategy.swift 3 | // Strategist 4 | // 5 | // Created by Vincent Esche on 06/06/16. 6 | // Copyright © 2016 Vincent Esche. All rights reserved. 7 | // 8 | 9 | /// Implementation of a simple random-based strategy. 10 | /// 11 | /// - note: Due to the lack of internal state a single instance of `RandomStrategy` can be shared for several players in a game. 12 | public struct RandomStrategy: Strategy { 13 | public typealias Game = G 14 | 15 | public init() { 16 | 17 | } 18 | 19 | public func evaluatedMoves(_ game: Game) -> AnySequence<(Game.Move, Evaluation)> { 20 | let moves = game.availableMoves() 21 | return AnySequence(moves.lazy.map { 22 | return ($0, Evaluation.ongoing(Game.Score.mid)) 23 | }) 24 | } 25 | 26 | public func update(_ move: Game.Move) -> RandomStrategy { 27 | return self 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Sources/Score.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Score.swift 3 | // Strategist 4 | // 5 | // Created by Vincent Esche on 11/06/16. 6 | // Copyright © 2016 Vincent Esche. All rights reserved. 7 | // 8 | 9 | /// Score protocol used for scores of game state evaluations. 10 | public protocol Score: Comparable { 11 | /// Smallest possible evaluation score. 12 | static var min: Self { get } 13 | /// Neutral evaluation score. 14 | static var mid: Self { get } 15 | /// Largest possible evaluation score. 16 | static var max: Self { get } 17 | 18 | /// Invert evaluation score. 19 | /// 20 | /// - returns: Inverted evaluation score 21 | func inverse() -> Self 22 | } 23 | 24 | extension Double: Score { 25 | public static var min: Double { return -.infinity } 26 | public static var mid: Double { return 0.0 } 27 | public static var max: Double { return .infinity } 28 | public func inverse() -> Double { return -self } 29 | } 30 | 31 | extension Float: Score { 32 | public static var min: Float { return -.infinity } 33 | public static var mid: Float { return 0.0 } 34 | public static var max: Float { return .infinity } 35 | public func inverse() -> Float { return -self } 36 | } 37 | 38 | extension Int: Score { 39 | public static var mid: Int { return 0 } 40 | public func inverse() -> Int { return -self } 41 | } 42 | 43 | extension UInt: Score { 44 | public static var mid: UInt { return .max / 2 } 45 | public func inverse() -> UInt { return .max - self } 46 | } 47 | 48 | extension Int64: Score { 49 | public static var mid: Int64 { return 0 } 50 | public func inverse() -> Int64 { return -self } 51 | } 52 | 53 | extension UInt64: Score { 54 | public static var mid: UInt64 { return .max / 2 } 55 | public func inverse() -> UInt64 { return .max - self } 56 | } 57 | 58 | extension Int32: Score { 59 | public static var mid: Int32 { return 0 } 60 | public func inverse() -> Int32 { return -self } 61 | } 62 | 63 | extension UInt32: Score { 64 | public static var mid: UInt32 { return .max / 2 } 65 | public func inverse() -> UInt32 { return .max - self } 66 | } 67 | 68 | extension Int16: Score { 69 | public static var mid: Int16 { return 0 } 70 | public func inverse() -> Int16 { return -self } 71 | } 72 | 73 | extension UInt16: Score { 74 | public static var mid: UInt16 { return .max / 2 } 75 | public func inverse() -> UInt16 { return .max - self } 76 | } 77 | 78 | extension Int8: Score { 79 | public static var mid: Int8 { return 0 } 80 | public func inverse() -> Int8 { return -self } 81 | } 82 | 83 | extension UInt8: Score { 84 | public static var mid: UInt8 { return .max / 2 } 85 | public func inverse() -> UInt8 { return .max - self } 86 | } 87 | -------------------------------------------------------------------------------- /Sources/Strategist.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Strategist.swift 3 | // Strategist 4 | // 5 | // Created by Vincent Esche on 06/06/16. 6 | // Copyright © 2016 Vincent Esche. All rights reserved. 7 | // 8 | 9 | import Darwin 10 | 11 | /// Function type used for injecting random sources into Strategist. 12 | public typealias RandomSource = (UInt32) -> UInt32 13 | 14 | /// Convenience function for generating curried fake random sources. 15 | public func fakeRandomSource(_ output: UInt32) -> RandomSource { 16 | return { upperBound in 17 | assert(output < upperBound) 18 | return output 19 | } 20 | } 21 | 22 | /// The default random source to use unless provided explicitly. 23 | public func defaultRandomSource(_ upperBound: UInt32) -> UInt32 { 24 | return arc4random_uniform(upperBound) 25 | } 26 | 27 | /// A lightweight player descriptor. 28 | /// 29 | /// #### Example: 30 | /// 31 | /// ``` 32 | /// enum ChessPlayer: Strategist.Player { 33 | /// case White 34 | /// case Black 35 | /// } 36 | /// ``` 37 | public protocol Player: Equatable {} 38 | 39 | /// A lightweight move descriptor. 40 | /// 41 | /// #### Example: 42 | /// 43 | /// ``` 44 | /// struct ChessMove: Strategist.Move { 45 | /// let origin: (UInt8, UInt8) 46 | /// let destination: (UInt8, UInt8) 47 | /// } 48 | /// ``` 49 | public protocol Move: Hashable {} 50 | 51 | /// A representation of a game's state. 52 | /// 53 | /// - requires: Must be immutable & have value semantics. 54 | /// - note: Depending on the game's nature it may be appropriate to model 55 | /// the game's state as a history of moves, in addition to a simple snapshot. 56 | /// 57 | /// #### Snapshot Only: 58 | /// 59 | /// ``` 60 | /// enum ChessPlayer: Strategist.Player { 61 | /// case White 62 | /// case Black 63 | /// } 64 | /// enum ChessPiece { 65 | /// case King, Queen, Rook, Bishop, Knight, Pawn 66 | /// } 67 | /// struct ChessGame: Strategist.State { 68 | /// let board: [ChessPiece?] 69 | /// let player: ChessPlayer? 70 | /// } 71 | /// ``` 72 | /// 73 | /// #### Snapshot + History: 74 | /// 75 | /// ``` 76 | /// enum GoPlayer: Strategist.Player { 77 | /// case White 78 | /// case Black 79 | /// } 80 | /// enum GoStone { 81 | /// case White, Black 82 | /// } 83 | /// struct GoGame: Strategist.Game { 84 | /// let board: [GoStone?] 85 | /// let moves: [GoMove] 86 | /// let player: GoPlayer? 87 | /// } 88 | /// ``` 89 | public protocol Game: Equatable { 90 | /// A representation of a game's moves. 91 | associatedtype Move: Strategist.Move 92 | /// A representation of a game's state. 93 | associatedtype Player: Strategist.Player 94 | /// A representation of a game's evaluation. 95 | associatedtype Score: Strategist.Score 96 | 97 | /// The player who the turn was handed over by last player's move 98 | /// or the game's initial player if no move has been made yet. 99 | var currentPlayer: Player { get } 100 | 101 | /// Advance the game by applying a move to the game's current state and advancing the players' turn. 102 | /// 103 | /// - returns: An updated game at the newly calculated state. 104 | func update(_ move: Move) -> Self 105 | 106 | /// Checks whether two players are expected to cooperate. 107 | func playersAreAllied(_ players: (Player, Player)) -> Bool 108 | 109 | /// All available moves for the next turn's player given the game's current state. 110 | /// 111 | /// - recommended: Generate the moves lazily to reduce the memory overhead. 112 | func availableMoves() -> AnyIterator 113 | 114 | /// Evaluate the game at its current state for the current player. 115 | /// 116 | /// - returns: Evaluation of game's current state from the perspective of the game's current player. 117 | func evaluate(forPlayer player: Player) -> Evaluation 118 | } 119 | 120 | extension Game { 121 | /// Evaluate the game at its current state for the current player. 122 | ///had 123 | /// - returns: Evaluation of game's current state from the perspective of the game's current player. 124 | public func evaluate() -> Evaluation { 125 | return self.evaluate(forPlayer: self.currentPlayer) 126 | } 127 | 128 | /// Checks whether the game has reached a final state. 129 | /// 130 | /// - returns: `false` iff `self.evaluate()` would return `.Ongoing(_)`, otherwise `true`. 131 | public var isFinished: Bool { 132 | switch self.evaluate() { 133 | case .ongoing(_): return false 134 | default: return true 135 | } 136 | } 137 | } 138 | 139 | /// A representation of a game with support for rewinding. 140 | /// 141 | /// - requires: Must be immutable & have value semantics. 142 | public protocol ReversibleGame: Game { 143 | /// Rewind the game by undoing a move to the game's current state. 144 | /// 145 | /// - returns: An updated game at the newly calculated prior state. 146 | func reverse(_ move: Move) -> Self 147 | } 148 | -------------------------------------------------------------------------------- /Sources/Strategy.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Strategy.swift 3 | // Strategist 4 | // 5 | // Created by Vincent Esche on 06/06/16. 6 | // Copyright © 2016 Vincent Esche. All rights reserved. 7 | // 8 | 9 | /// Protocol to be implemented for strategy algorithms. 10 | /// 11 | /// - requires: Must be immutable & have value semantics. 12 | public protocol Strategy { 13 | /// The given game type to be reasoned about. 14 | associatedtype Game: Strategist.Game 15 | 16 | /// Evaluates the available moves for the `game`'s current state. 17 | /// 18 | /// - returns: Lazy sequence of evaluated moves. 19 | func evaluatedMoves(_ game: Game) -> AnySequence<(Game.Move, Evaluation)> 20 | 21 | /// Updates strategy's internal state for chosen `move`. 22 | /// 23 | /// - returns: Updated strategy. 24 | func update(_ move: Game.Move) -> Self 25 | } 26 | 27 | extension Strategy { 28 | /// Evaluates the available moves for the `game`'s current state. 29 | /// 30 | /// - returns: Lazy sequence of evaluated moves. 31 | public func bestMoves(_ game: Game) -> [Game.Move] { 32 | var bestEvaluation = Evaluation.min 33 | var bestMoves: [Game.Move] = [] 34 | for (move, evaluation) in self.evaluatedMoves(game) { 35 | if evaluation == bestEvaluation { 36 | bestMoves.append(move) 37 | } else if evaluation > bestEvaluation { 38 | bestMoves.removeAll() 39 | bestMoves.append(move) 40 | bestEvaluation = evaluation 41 | } 42 | } 43 | return bestMoves 44 | } 45 | 46 | /// Greedily selects the first encountered maximizing 47 | /// available move for the `game`'s current state. 48 | /// 49 | /// - note: The selection is deterministic. 50 | /// 51 | /// - returns: First maximizing available move. 52 | public func firstMaximizingMove(_ game: Game) -> Game.Move? { 53 | let evaluatedMoves = self.evaluatedMoves(game) 54 | return evaluatedMoves.max{ $0.1 < $1.1 }.map{ $0.0 } 55 | } 56 | 57 | /// Randomly selects from the encountered maximizing 58 | /// available move for the `game`'s current state. 59 | /// 60 | /// - note: The selection is deterministic. 61 | /// 62 | /// - returns: Randomly chosen maximizing available move. 63 | public func randomMaximizingMove(_ game: Game, randomSource: ((UInt32) -> UInt32)? = nil) -> Game.Move? { 64 | var bestEvaluation = Evaluation.min 65 | var bestMove: Game.Move? = nil 66 | var count = 0 67 | let evaluatedMoves = self.evaluatedMoves(game) 68 | guard let randomSource = randomSource else { 69 | return evaluatedMoves.max{ $0.1 < $1.1 }.map { $0.0 } 70 | } 71 | for (move, evaluation) in evaluatedMoves { 72 | if evaluation > bestEvaluation { 73 | bestEvaluation = evaluation 74 | bestMove = move 75 | count = 1 76 | } else if evaluation == bestEvaluation { 77 | if Int(randomSource(UInt32(count))) == 0 { 78 | bestMove = move 79 | } 80 | count += 1 81 | } 82 | } 83 | return bestMove 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /Sources/TreeSearchPolicy.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TreeSearchPolicy.swift 3 | // Strategist 4 | // 5 | // Created by Vincent Esche on 09/06/16. 6 | // Copyright © 2016 Vincent Esche. All rights reserved. 7 | // 8 | 9 | /// Policy for more direct control over a strategy's execution 10 | public protocol TreeSearchPolicy { 11 | /// The given game type to be reasoned about. 12 | associatedtype Game: Strategist.Game 13 | 14 | /// Filter out moves to be ignored at any stage of the game. 15 | func filterMoves(_ state: Game, depth: Int, moves: G) -> AnyIterator where G.Element == Game.Move 16 | 17 | /// Whether the strategy should abort a given exploration. 18 | func hasReachedMaxExplorationDepth(_ depth: Int) -> Bool 19 | } 20 | 21 | /// Simple minimal implementation of `TreeSearchPolicy`. 22 | public struct SimpleTreeSearchPolicy: TreeSearchPolicy { 23 | public typealias Game = G 24 | 25 | public let maxMoves: Int 26 | public let maxExplorationDepth: Int 27 | 28 | public init(maxMoves: Int, maxExplorationDepth: Int) { 29 | self.maxMoves = maxMoves 30 | self.maxExplorationDepth = maxExplorationDepth 31 | } 32 | 33 | public func filterMoves(_ state: Game, depth: Int, moves: G) -> AnyIterator where G.Element == Game.Move { 34 | return AnyIterator(moves.take(self.maxMoves)) 35 | } 36 | 37 | public func hasReachedMaxExplorationDepth(_ depth: Int) -> Bool { 38 | return depth >= self.maxExplorationDepth 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Strategist.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | 3 | s.name = "Strategist" 4 | s.version = "0.1.0" 5 | s.summary = "Algorithms for building strong immutable AIs for round-based games." 6 | 7 | s.description = <<-DESC 8 | Strategist provides algorithms for building strong immutable AIs for round-based games. 9 | DESC 10 | 11 | s.homepage = "https://github.com/regexident/Strategist" 12 | s.license = { :type => 'MPL-2', :file => 'LICENSE' } 13 | s.author = { "Vincent Esche" => "regexident@gmail.com" } 14 | s.source = { :git => "https://github.com/regexident/Strategist.git", :tag => '0.1.0' } 15 | s.source_files = "Sources/*.{swift,h,m}" 16 | s.requires_arc = true 17 | s.ios.deployment_target = "8.0" 18 | s.osx.deployment_target = "10.9" 19 | 20 | end -------------------------------------------------------------------------------- /Strategist.xcodeproj/Configs/Project.xcconfig: -------------------------------------------------------------------------------- 1 | PRODUCT_NAME = $(TARGET_NAME) 2 | SUPPORTED_PLATFORMS = macosx iphoneos iphonesimulator appletvos appletvsimulator watchos watchsimulator 3 | MACOSX_DEPLOYMENT_TARGET = 10.10 4 | DYLIB_INSTALL_NAME_BASE = @rpath 5 | OTHER_SWIFT_FLAGS = -DXcode 6 | COMBINE_HIDPI_IMAGES = YES 7 | USE_HEADERMAP = NO 8 | -------------------------------------------------------------------------------- /Strategist.xcodeproj/StrategistTestSuite_Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | CFBundleDevelopmentRegion 5 | en 6 | CFBundleExecutable 7 | $(EXECUTABLE_NAME) 8 | CFBundleIdentifier 9 | $(PRODUCT_BUNDLE_IDENTIFIER) 10 | CFBundleInfoDictionaryVersion 11 | 6.0 12 | CFBundleName 13 | $(PRODUCT_NAME) 14 | CFBundlePackageType 15 | BNDL 16 | CFBundleShortVersionString 17 | 1.0 18 | CFBundleSignature 19 | ???? 20 | CFBundleVersion 21 | $(CURRENT_PROJECT_VERSION) 22 | NSPrincipalClass 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Strategist.xcodeproj/Strategist_Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 0.1.1 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Strategist.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | BF03DFEB1D0C7F8F00FFBAF4 /* Evaluation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF03DFE71D0C7F8F00FFBAF4 /* Evaluation.swift */; }; 11 | BF03DFEC1D0C7F8F00FFBAF4 /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF03DFE81D0C7F8F00FFBAF4 /* Extensions.swift */; }; 12 | BF03DFEE1D0C7F8F00FFBAF4 /* Score.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF03DFEA1D0C7F8F00FFBAF4 /* Score.swift */; }; 13 | BF03DFF01D0C7FAD00FFBAF4 /* Strategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF03DFEF1D0C7FAD00FFBAF4 /* Strategy.swift */; }; 14 | BF03DFF21D0C7FD900FFBAF4 /* TreeSearchPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF03DFF11D0C7FD900FFBAF4 /* TreeSearchPolicy.swift */; }; 15 | BF03DFF41D0C858700FFBAF4 /* RandomStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF03DFF31D0C858700FFBAF4 /* RandomStrategy.swift */; }; 16 | BF03DFFA1D0C85D500FFBAF4 /* TicTacToe.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF03DFF61D0C85C500FFBAF4 /* TicTacToe.swift */; }; 17 | BF03DFFB1D0C85D500FFBAF4 /* TicTacToeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF03DFF71D0C85C500FFBAF4 /* TicTacToeTests.swift */; }; 18 | BF03E0031D0CA5FA00FFBAF4 /* MiniMaxTreeSearch.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF03E0011D0CA5F800FFBAF4 /* MiniMaxTreeSearch.swift */; }; 19 | BF03E0071D0CACB400FFBAF4 /* FourtyTwo.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF03E0051D0CACB400FFBAF4 /* FourtyTwo.swift */; }; 20 | BF03E0081D0CACB400FFBAF4 /* FourtyTwoTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF03E0061D0CACB400FFBAF4 /* FourtyTwoTests.swift */; }; 21 | BF03E00F1D0CC5C500FFBAF4 /* NegaMaxStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF03E00E1D0CC5C500FFBAF4 /* NegaMaxStrategy.swift */; }; 22 | BF03E0111D0CC67200FFBAF4 /* MonteCarloTreeSearch.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF03E0101D0CC67200FFBAF4 /* MonteCarloTreeSearch.swift */; }; 23 | BF03E0131D0CC6BC00FFBAF4 /* MonteCarloTreeSearchPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF03E0121D0CC6BC00FFBAF4 /* MonteCarloTreeSearchPolicy.swift */; }; 24 | BF03E0151D0CC6FA00FFBAF4 /* GameTree.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF03E0141D0CC6FA00FFBAF4 /* GameTree.swift */; }; 25 | _LinkFileRef_Strategist_via_StrategistTestSuite /* Strategist.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = "_____Product_Strategist" /* Strategist.framework */; }; 26 | __src_cc_ref_Sources/Strategist.swift /* Strategist.swift in Sources */ = {isa = PBXBuildFile; fileRef = __PBXFileRef_Sources/Strategist.swift /* Strategist.swift */; }; 27 | __src_cc_ref_Tests/Strategist/StrategistTests.swift /* StrategistTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = __PBXFileRef_Tests/Strategist/StrategistTests.swift /* StrategistTests.swift */; }; 28 | /* End PBXBuildFile section */ 29 | 30 | /* Begin PBXContainerItemProxy section */ 31 | BF9C12241D04ECE400449EA6 /* PBXContainerItemProxy */ = { 32 | isa = PBXContainerItemProxy; 33 | containerPortal = __RootObject_ /* Project object */; 34 | proxyType = 1; 35 | remoteGlobalIDString = "______Target_Strategist"; 36 | remoteInfo = Strategist; 37 | }; 38 | /* End PBXContainerItemProxy section */ 39 | 40 | /* Begin PBXFileReference section */ 41 | BF03DFE71D0C7F8F00FFBAF4 /* Evaluation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Evaluation.swift; sourceTree = ""; }; 42 | BF03DFE81D0C7F8F00FFBAF4 /* Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = Extensions.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; 43 | BF03DFEA1D0C7F8F00FFBAF4 /* Score.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Score.swift; sourceTree = ""; }; 44 | BF03DFEF1D0C7FAD00FFBAF4 /* Strategy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Strategy.swift; sourceTree = ""; }; 45 | BF03DFF11D0C7FD900FFBAF4 /* TreeSearchPolicy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TreeSearchPolicy.swift; sourceTree = ""; }; 46 | BF03DFF31D0C858700FFBAF4 /* RandomStrategy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RandomStrategy.swift; sourceTree = ""; }; 47 | BF03DFF61D0C85C500FFBAF4 /* TicTacToe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TicTacToe.swift; sourceTree = ""; }; 48 | BF03DFF71D0C85C500FFBAF4 /* TicTacToeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TicTacToeTests.swift; sourceTree = ""; }; 49 | BF03E0011D0CA5F800FFBAF4 /* MiniMaxTreeSearch.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MiniMaxTreeSearch.swift; sourceTree = ""; }; 50 | BF03E0051D0CACB400FFBAF4 /* FourtyTwo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FourtyTwo.swift; sourceTree = ""; }; 51 | BF03E0061D0CACB400FFBAF4 /* FourtyTwoTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FourtyTwoTests.swift; sourceTree = ""; }; 52 | BF03E00E1D0CC5C500FFBAF4 /* NegaMaxStrategy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NegaMaxStrategy.swift; sourceTree = ""; }; 53 | BF03E0101D0CC67200FFBAF4 /* MonteCarloTreeSearch.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = MonteCarloTreeSearch.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; 54 | BF03E0121D0CC6BC00FFBAF4 /* MonteCarloTreeSearchPolicy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = MonteCarloTreeSearchPolicy.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; 55 | BF03E0141D0CC6FA00FFBAF4 /* GameTree.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GameTree.swift; sourceTree = ""; }; 56 | BFB574ED1D12D2490086BE45 /* ParallelMonteCarloTreeSearch.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ParallelMonteCarloTreeSearch.swift; sourceTree = ""; }; 57 | __PBXFileRef_Package.swift /* Package.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Package.swift; sourceTree = ""; }; 58 | __PBXFileRef_Sources/Strategist.swift /* Strategist.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Strategist.swift; sourceTree = ""; }; 59 | __PBXFileRef_Strategist.xcodeproj/Configs/Project.xcconfig /* Project.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Project.xcconfig; path = Strategist.xcodeproj/Configs/Project.xcconfig; sourceTree = ""; }; 60 | __PBXFileRef_StrategistTestSuite_Info.plist /* StrategistTestSuite_Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = StrategistTestSuite_Info.plist; path = Strategist.xcodeproj/StrategistTestSuite_Info.plist; sourceTree = SOURCE_ROOT; }; 61 | __PBXFileRef_Strategist_Info.plist /* Strategist_Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Strategist_Info.plist; path = Strategist.xcodeproj/Strategist_Info.plist; sourceTree = SOURCE_ROOT; }; 62 | __PBXFileRef_Tests/Strategist/StrategistTests.swift /* StrategistTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StrategistTests.swift; sourceTree = ""; }; 63 | "_____Product_Strategist" /* Strategist.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Strategist.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 64 | "_____Product_StrategistTestSuite" /* StrategistTestSuite.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; path = StrategistTestSuite.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 65 | /* End PBXFileReference section */ 66 | 67 | /* Begin PBXFrameworksBuildPhase section */ 68 | "___LinkPhase_Strategist" /* Frameworks */ = { 69 | isa = PBXFrameworksBuildPhase; 70 | buildActionMask = 0; 71 | files = ( 72 | ); 73 | runOnlyForDeploymentPostprocessing = 0; 74 | }; 75 | "___LinkPhase_StrategistTestSuite" /* Frameworks */ = { 76 | isa = PBXFrameworksBuildPhase; 77 | buildActionMask = 0; 78 | files = ( 79 | _LinkFileRef_Strategist_via_StrategistTestSuite /* Strategist.framework in Frameworks */, 80 | ); 81 | runOnlyForDeploymentPostprocessing = 0; 82 | }; 83 | /* End PBXFrameworksBuildPhase section */ 84 | 85 | /* Begin PBXGroup section */ 86 | BF03DFF51D0C85C500FFBAF4 /* TicTacToe */ = { 87 | isa = PBXGroup; 88 | children = ( 89 | BF03DFF61D0C85C500FFBAF4 /* TicTacToe.swift */, 90 | BF03DFF71D0C85C500FFBAF4 /* TicTacToeTests.swift */, 91 | ); 92 | path = TicTacToe; 93 | sourceTree = ""; 94 | }; 95 | BF03E0041D0CACB400FFBAF4 /* FourtyTwo */ = { 96 | isa = PBXGroup; 97 | children = ( 98 | BF03E0051D0CACB400FFBAF4 /* FourtyTwo.swift */, 99 | BF03E0061D0CACB400FFBAF4 /* FourtyTwoTests.swift */, 100 | ); 101 | path = FourtyTwo; 102 | sourceTree = ""; 103 | }; 104 | TestProducts_ /* Tests */ = { 105 | isa = PBXGroup; 106 | children = ( 107 | "_____Product_StrategistTestSuite" /* StrategistTestSuite.xctest */, 108 | ); 109 | name = Tests; 110 | sourceTree = ""; 111 | }; 112 | "___RootGroup_" = { 113 | isa = PBXGroup; 114 | children = ( 115 | __PBXFileRef_Package.swift /* Package.swift */, 116 | "_____Configs_" /* Configs */, 117 | "_____Sources_" /* Sources */, 118 | "_______Tests_" /* Tests */, 119 | "____Products_" /* Products */, 120 | ); 121 | sourceTree = ""; 122 | }; 123 | "____Products_" /* Products */ = { 124 | isa = PBXGroup; 125 | children = ( 126 | TestProducts_ /* Tests */, 127 | "_____Product_Strategist" /* Strategist.framework */, 128 | ); 129 | name = Products; 130 | sourceTree = ""; 131 | }; 132 | "_____Configs_" /* Configs */ = { 133 | isa = PBXGroup; 134 | children = ( 135 | __PBXFileRef_Strategist.xcodeproj/Configs/Project.xcconfig /* Project.xcconfig */, 136 | ); 137 | name = Configs; 138 | sourceTree = ""; 139 | }; 140 | "_____Sources_" /* Sources */ = { 141 | isa = PBXGroup; 142 | children = ( 143 | "_______Group_Strategist" /* Strategist */, 144 | ); 145 | name = Sources; 146 | sourceTree = ""; 147 | }; 148 | "_______Group_Strategist" /* Strategist */ = { 149 | isa = PBXGroup; 150 | children = ( 151 | __PBXFileRef_Sources/Strategist.swift /* Strategist.swift */, 152 | BF03DFEF1D0C7FAD00FFBAF4 /* Strategy.swift */, 153 | BF03DFE71D0C7F8F00FFBAF4 /* Evaluation.swift */, 154 | BF03DFE81D0C7F8F00FFBAF4 /* Extensions.swift */, 155 | BF03DFEA1D0C7F8F00FFBAF4 /* Score.swift */, 156 | BF03E0141D0CC6FA00FFBAF4 /* GameTree.swift */, 157 | BF03DFF11D0C7FD900FFBAF4 /* TreeSearchPolicy.swift */, 158 | BF03E0121D0CC6BC00FFBAF4 /* MonteCarloTreeSearchPolicy.swift */, 159 | BF03DFF31D0C858700FFBAF4 /* RandomStrategy.swift */, 160 | BF03E0011D0CA5F800FFBAF4 /* MiniMaxTreeSearch.swift */, 161 | BF03E00E1D0CC5C500FFBAF4 /* NegaMaxStrategy.swift */, 162 | BF03E0101D0CC67200FFBAF4 /* MonteCarloTreeSearch.swift */, 163 | BFB574ED1D12D2490086BE45 /* ParallelMonteCarloTreeSearch.swift */, 164 | __PBXFileRef_Strategist_Info.plist /* Strategist_Info.plist */, 165 | ); 166 | name = Strategist; 167 | path = Sources; 168 | sourceTree = ""; 169 | }; 170 | "_______Group_StrategistTestSuite" /* StrategistTestSuite */ = { 171 | isa = PBXGroup; 172 | children = ( 173 | BF03E0041D0CACB400FFBAF4 /* FourtyTwo */, 174 | BF03DFF51D0C85C500FFBAF4 /* TicTacToe */, 175 | __PBXFileRef_Tests/Strategist/StrategistTests.swift /* StrategistTests.swift */, 176 | __PBXFileRef_StrategistTestSuite_Info.plist /* StrategistTestSuite_Info.plist */, 177 | ); 178 | name = StrategistTestSuite; 179 | path = Tests/Strategist; 180 | sourceTree = ""; 181 | }; 182 | "_______Tests_" /* Tests */ = { 183 | isa = PBXGroup; 184 | children = ( 185 | "_______Group_StrategistTestSuite" /* StrategistTestSuite */, 186 | ); 187 | name = Tests; 188 | sourceTree = ""; 189 | }; 190 | /* End PBXGroup section */ 191 | 192 | /* Begin PBXNativeTarget section */ 193 | "______Target_Strategist" /* Strategist */ = { 194 | isa = PBXNativeTarget; 195 | buildConfigurationList = "_______Confs_Strategist" /* Build configuration list for PBXNativeTarget "Strategist" */; 196 | buildPhases = ( 197 | CompilePhase_Strategist /* Sources */, 198 | "___LinkPhase_Strategist" /* Frameworks */, 199 | ); 200 | buildRules = ( 201 | ); 202 | dependencies = ( 203 | ); 204 | name = Strategist; 205 | productName = Strategist; 206 | productReference = "_____Product_Strategist" /* Strategist.framework */; 207 | productType = "com.apple.product-type.framework"; 208 | }; 209 | "______Target_StrategistTestSuite" /* StrategistTestSuite */ = { 210 | isa = PBXNativeTarget; 211 | buildConfigurationList = "_______Confs_StrategistTestSuite" /* Build configuration list for PBXNativeTarget "StrategistTestSuite" */; 212 | buildPhases = ( 213 | CompilePhase_StrategistTestSuite /* Sources */, 214 | "___LinkPhase_StrategistTestSuite" /* Frameworks */, 215 | ); 216 | buildRules = ( 217 | ); 218 | dependencies = ( 219 | __Dependency_Strategist /* PBXTargetDependency */, 220 | ); 221 | name = StrategistTestSuite; 222 | productName = StrategistTestSuite; 223 | productReference = "_____Product_StrategistTestSuite" /* StrategistTestSuite.xctest */; 224 | productType = "com.apple.product-type.bundle.unit-test"; 225 | }; 226 | /* End PBXNativeTarget section */ 227 | 228 | /* Begin PBXProject section */ 229 | __RootObject_ /* Project object */ = { 230 | isa = PBXProject; 231 | attributes = { 232 | LastUpgradeCheck = 0920; 233 | TargetAttributes = { 234 | "______Target_Strategist" = { 235 | DevelopmentTeam = RHSV5Y8MLD; 236 | LastSwiftMigration = 0920; 237 | }; 238 | "______Target_StrategistTestSuite" = { 239 | DevelopmentTeam = RHSV5Y8MLD; 240 | LastSwiftMigration = 0920; 241 | }; 242 | }; 243 | }; 244 | buildConfigurationList = "___RootConfs_" /* Build configuration list for PBXProject "Strategist" */; 245 | compatibilityVersion = "Xcode 3.2"; 246 | developmentRegion = English; 247 | hasScannedForEncodings = 0; 248 | knownRegions = ( 249 | en, 250 | ); 251 | mainGroup = "___RootGroup_"; 252 | productRefGroup = "____Products_" /* Products */; 253 | projectDirPath = ""; 254 | projectRoot = ""; 255 | targets = ( 256 | "______Target_Strategist" /* Strategist */, 257 | "______Target_StrategistTestSuite" /* StrategistTestSuite */, 258 | ); 259 | }; 260 | /* End PBXProject section */ 261 | 262 | /* Begin PBXSourcesBuildPhase section */ 263 | CompilePhase_Strategist /* Sources */ = { 264 | isa = PBXSourcesBuildPhase; 265 | buildActionMask = 0; 266 | files = ( 267 | BF03DFF21D0C7FD900FFBAF4 /* TreeSearchPolicy.swift in Sources */, 268 | __src_cc_ref_Sources/Strategist.swift /* Strategist.swift in Sources */, 269 | BF03E0031D0CA5FA00FFBAF4 /* MiniMaxTreeSearch.swift in Sources */, 270 | BF03E0111D0CC67200FFBAF4 /* MonteCarloTreeSearch.swift in Sources */, 271 | BF03DFEC1D0C7F8F00FFBAF4 /* Extensions.swift in Sources */, 272 | BF03DFF41D0C858700FFBAF4 /* RandomStrategy.swift in Sources */, 273 | BF03DFF01D0C7FAD00FFBAF4 /* Strategy.swift in Sources */, 274 | BF03DFEB1D0C7F8F00FFBAF4 /* Evaluation.swift in Sources */, 275 | BF03E0151D0CC6FA00FFBAF4 /* GameTree.swift in Sources */, 276 | BF03E00F1D0CC5C500FFBAF4 /* NegaMaxStrategy.swift in Sources */, 277 | BF03E0131D0CC6BC00FFBAF4 /* MonteCarloTreeSearchPolicy.swift in Sources */, 278 | BF03DFEE1D0C7F8F00FFBAF4 /* Score.swift in Sources */, 279 | ); 280 | runOnlyForDeploymentPostprocessing = 0; 281 | }; 282 | CompilePhase_StrategistTestSuite /* Sources */ = { 283 | isa = PBXSourcesBuildPhase; 284 | buildActionMask = 0; 285 | files = ( 286 | BF03DFFA1D0C85D500FFBAF4 /* TicTacToe.swift in Sources */, 287 | BF03E0081D0CACB400FFBAF4 /* FourtyTwoTests.swift in Sources */, 288 | BF03E0071D0CACB400FFBAF4 /* FourtyTwo.swift in Sources */, 289 | BF03DFFB1D0C85D500FFBAF4 /* TicTacToeTests.swift in Sources */, 290 | __src_cc_ref_Tests/Strategist/StrategistTests.swift /* StrategistTests.swift in Sources */, 291 | ); 292 | runOnlyForDeploymentPostprocessing = 0; 293 | }; 294 | /* End PBXSourcesBuildPhase section */ 295 | 296 | /* Begin PBXTargetDependency section */ 297 | __Dependency_Strategist /* PBXTargetDependency */ = { 298 | isa = PBXTargetDependency; 299 | target = "______Target_Strategist" /* Strategist */; 300 | targetProxy = BF9C12241D04ECE400449EA6 /* PBXContainerItemProxy */; 301 | }; 302 | /* End PBXTargetDependency section */ 303 | 304 | /* Begin XCBuildConfiguration section */ 305 | _ReleaseConf_Strategist /* Release */ = { 306 | isa = XCBuildConfiguration; 307 | buildSettings = { 308 | CLANG_ENABLE_OBJC_WEAK = YES; 309 | DEVELOPMENT_TEAM = ""; 310 | ENABLE_TESTABILITY = YES; 311 | INFOPLIST_FILE = Strategist.xcodeproj/Strategist_Info.plist; 312 | LD_RUNPATH_SEARCH_PATHS = "$(TOOLCHAIN_DIR)/usr/lib/swift/macosx"; 313 | OTHER_LDFLAGS = "$(inherited)"; 314 | OTHER_SWIFT_FLAGS = "$(inherited)"; 315 | PRODUCT_BUNDLE_IDENTIFIER = com.regexident.Strategist; 316 | PRODUCT_MODULE_NAME = "$(TARGET_NAME:c99extidentifier)"; 317 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 318 | PROVISIONING_PROFILE_SPECIFIER = ""; 319 | }; 320 | name = Release; 321 | }; 322 | _ReleaseConf_StrategistTestSuite /* Release */ = { 323 | isa = XCBuildConfiguration; 324 | buildSettings = { 325 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 326 | CLANG_ENABLE_OBJC_WEAK = YES; 327 | DEVELOPMENT_TEAM = ""; 328 | INFOPLIST_FILE = Strategist.xcodeproj/StrategistTestSuite_Info.plist; 329 | LD_RUNPATH_SEARCH_PATHS = "@loader_path/../Frameworks"; 330 | OTHER_LDFLAGS = "$(inherited)"; 331 | OTHER_SWIFT_FLAGS = "$(inherited)"; 332 | PROVISIONING_PROFILE_SPECIFIER = ""; 333 | }; 334 | name = Release; 335 | }; 336 | "___DebugConf_Strategist" /* Debug */ = { 337 | isa = XCBuildConfiguration; 338 | buildSettings = { 339 | CLANG_ENABLE_OBJC_WEAK = YES; 340 | DEVELOPMENT_TEAM = ""; 341 | ENABLE_TESTABILITY = YES; 342 | INFOPLIST_FILE = Strategist.xcodeproj/Strategist_Info.plist; 343 | LD_RUNPATH_SEARCH_PATHS = "$(TOOLCHAIN_DIR)/usr/lib/swift/macosx"; 344 | OTHER_LDFLAGS = "$(inherited)"; 345 | OTHER_SWIFT_FLAGS = "$(inherited)"; 346 | PRODUCT_BUNDLE_IDENTIFIER = com.regexident.Strategist; 347 | PRODUCT_MODULE_NAME = "$(TARGET_NAME:c99extidentifier)"; 348 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 349 | PROVISIONING_PROFILE_SPECIFIER = ""; 350 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 351 | }; 352 | name = Debug; 353 | }; 354 | "___DebugConf_StrategistTestSuite" /* Debug */ = { 355 | isa = XCBuildConfiguration; 356 | buildSettings = { 357 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 358 | CLANG_ENABLE_OBJC_WEAK = YES; 359 | DEVELOPMENT_TEAM = ""; 360 | INFOPLIST_FILE = Strategist.xcodeproj/StrategistTestSuite_Info.plist; 361 | LD_RUNPATH_SEARCH_PATHS = "@loader_path/../Frameworks"; 362 | OTHER_LDFLAGS = "$(inherited)"; 363 | OTHER_SWIFT_FLAGS = "$(inherited)"; 364 | PROVISIONING_PROFILE_SPECIFIER = ""; 365 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 366 | }; 367 | name = Debug; 368 | }; 369 | "_____Release_" /* Release */ = { 370 | isa = XCBuildConfiguration; 371 | baseConfigurationReference = __PBXFileRef_Strategist.xcodeproj/Configs/Project.xcconfig /* Project.xcconfig */; 372 | buildSettings = { 373 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 374 | CLANG_WARN_BOOL_CONVERSION = YES; 375 | CLANG_WARN_COMMA = YES; 376 | CLANG_WARN_CONSTANT_CONVERSION = YES; 377 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 378 | CLANG_WARN_EMPTY_BODY = YES; 379 | CLANG_WARN_ENUM_CONVERSION = YES; 380 | CLANG_WARN_INFINITE_RECURSION = YES; 381 | CLANG_WARN_INT_CONVERSION = YES; 382 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 383 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 384 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 385 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 386 | CLANG_WARN_STRICT_PROTOTYPES = YES; 387 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 388 | CLANG_WARN_UNREACHABLE_CODE = YES; 389 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 390 | ENABLE_STRICT_OBJC_MSGSEND = YES; 391 | GCC_NO_COMMON_BLOCKS = YES; 392 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 393 | GCC_WARN_ABOUT_RETURN_TYPE = YES; 394 | GCC_WARN_UNDECLARED_SELECTOR = YES; 395 | GCC_WARN_UNINITIALIZED_AUTOS = YES; 396 | GCC_WARN_UNUSED_FUNCTION = YES; 397 | GCC_WARN_UNUSED_VARIABLE = YES; 398 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 399 | SWIFT_VERSION = 4.0; 400 | }; 401 | name = Release; 402 | }; 403 | "_______Debug_" /* Debug */ = { 404 | isa = XCBuildConfiguration; 405 | baseConfigurationReference = __PBXFileRef_Strategist.xcodeproj/Configs/Project.xcconfig /* Project.xcconfig */; 406 | buildSettings = { 407 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 408 | CLANG_WARN_BOOL_CONVERSION = YES; 409 | CLANG_WARN_COMMA = YES; 410 | CLANG_WARN_CONSTANT_CONVERSION = YES; 411 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 412 | CLANG_WARN_EMPTY_BODY = YES; 413 | CLANG_WARN_ENUM_CONVERSION = YES; 414 | CLANG_WARN_INFINITE_RECURSION = YES; 415 | CLANG_WARN_INT_CONVERSION = YES; 416 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 417 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 418 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 419 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 420 | CLANG_WARN_STRICT_PROTOTYPES = YES; 421 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 422 | CLANG_WARN_UNREACHABLE_CODE = YES; 423 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 424 | ENABLE_STRICT_OBJC_MSGSEND = YES; 425 | ENABLE_TESTABILITY = YES; 426 | GCC_NO_COMMON_BLOCKS = YES; 427 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 428 | GCC_WARN_ABOUT_RETURN_TYPE = YES; 429 | GCC_WARN_UNDECLARED_SELECTOR = YES; 430 | GCC_WARN_UNINITIALIZED_AUTOS = YES; 431 | GCC_WARN_UNUSED_FUNCTION = YES; 432 | GCC_WARN_UNUSED_VARIABLE = YES; 433 | ONLY_ACTIVE_ARCH = YES; 434 | SWIFT_VERSION = 4.0; 435 | }; 436 | name = Debug; 437 | }; 438 | /* End XCBuildConfiguration section */ 439 | 440 | /* Begin XCConfigurationList section */ 441 | "___RootConfs_" /* Build configuration list for PBXProject "Strategist" */ = { 442 | isa = XCConfigurationList; 443 | buildConfigurations = ( 444 | "_______Debug_" /* Debug */, 445 | "_____Release_" /* Release */, 446 | ); 447 | defaultConfigurationIsVisible = 0; 448 | defaultConfigurationName = Debug; 449 | }; 450 | "_______Confs_Strategist" /* Build configuration list for PBXNativeTarget "Strategist" */ = { 451 | isa = XCConfigurationList; 452 | buildConfigurations = ( 453 | "___DebugConf_Strategist" /* Debug */, 454 | _ReleaseConf_Strategist /* Release */, 455 | ); 456 | defaultConfigurationIsVisible = 0; 457 | defaultConfigurationName = Debug; 458 | }; 459 | "_______Confs_StrategistTestSuite" /* Build configuration list for PBXNativeTarget "StrategistTestSuite" */ = { 460 | isa = XCConfigurationList; 461 | buildConfigurations = ( 462 | "___DebugConf_StrategistTestSuite" /* Debug */, 463 | _ReleaseConf_StrategistTestSuite /* Release */, 464 | ); 465 | defaultConfigurationIsVisible = 0; 466 | defaultConfigurationName = Debug; 467 | }; 468 | /* End XCConfigurationList section */ 469 | }; 470 | rootObject = __RootObject_ /* Project object */; 471 | } 472 | -------------------------------------------------------------------------------- /Strategist.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Strategist.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Strategist.xcodeproj/xcshareddata/xcschemes/Strategist.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 55 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 74 | 76 | 77 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /Strategist.xcodeproj/xcshareddata/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | SchemeUserState 5 | 6 | Strategist.xcscheme 7 | 8 | 9 | SuppressBuildableAutocreation 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /Tests/Strategist/FourtyTwo/FourtyTwo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FourtyTwo.swift 3 | // MiniMaxTests 4 | // 5 | // Created by Vincent Esche on 06/06/16. 6 | // Copyright © 2016 Vincent Esche. All rights reserved. 7 | // 8 | 9 | import Strategist 10 | 11 | struct FourtyTwoPlayer: Strategist.Player {} 12 | 13 | func ==(lhs: FourtyTwoPlayer, rhs: FourtyTwoPlayer) -> Bool { 14 | return true 15 | } 16 | 17 | extension FourtyTwoPlayer: CustomStringConvertible { 18 | var description: String { 19 | return "Player" 20 | } 21 | } 22 | 23 | enum FourtyTwoMove: Strategist.Move { 24 | case add(Int) 25 | case mul(Int) 26 | } 27 | 28 | func ==(lhs: FourtyTwoMove, rhs: FourtyTwoMove) -> Bool { 29 | switch (lhs, rhs) { 30 | case let (.add(lhsValue), .add(rhsValue)): 31 | return lhsValue == rhsValue 32 | case let (.mul(lhsValue), .mul(rhsValue)): 33 | return lhsValue == rhsValue 34 | default: 35 | return false 36 | } 37 | } 38 | 39 | extension FourtyTwoMove: Hashable { 40 | var hashValue: Int { 41 | switch self { 42 | case let .add(value): return value 43 | case let .mul(value): return value 44 | } 45 | } 46 | } 47 | 48 | extension FourtyTwoMove: CustomStringConvertible { 49 | var description: String { 50 | switch self { 51 | case let .add(value): return "+ \(value)" 52 | case let .mul(value): return "* \(value)" 53 | } 54 | } 55 | } 56 | 57 | /// The objective of this ficticious single-player dummy game 58 | /// is to reach exactly 42 with the least amount of moves by either 59 | /// adding or multiplying the current total with either 2 or 3. 60 | struct FourtyTwoGame: Strategist.Game { 61 | typealias Player = FourtyTwoPlayer 62 | typealias Move = FourtyTwoMove 63 | typealias Score = Double 64 | 65 | static let moves: [Move] = [ 66 | .add(2), .add(3), .mul(2), .mul(3) 67 | ] 68 | 69 | let player: FourtyTwoPlayer 70 | let sum: Int 71 | let moves: [Move] 72 | 73 | var currentPlayer: Player { 74 | return self.player 75 | } 76 | 77 | init(player: Player) { 78 | self.init(player: player, sum: 1, moves: []) 79 | } 80 | 81 | fileprivate init(player: Player, sum: Int, moves: [Move]) { 82 | self.player = player 83 | self.sum = sum 84 | self.moves = moves 85 | } 86 | 87 | func update(_ move: Move) -> FourtyTwoGame { 88 | let player = self.player 89 | var sum = self.sum 90 | switch move { 91 | case let .add(value): sum += value 92 | case let .mul(value): sum *= value 93 | } 94 | var moves = self.moves 95 | moves.append(move) 96 | return FourtyTwoGame(player: player, sum: sum, moves: moves) 97 | } 98 | 99 | func playerAfter(_ player: Player) -> Player { 100 | return player 101 | } 102 | 103 | func playersAreAllied(_ players: (Player, Player)) -> Bool { 104 | return players.0 == players.1 105 | } 106 | 107 | func availableMoves() -> AnyIterator { 108 | guard !self.isFinished else { 109 | return AnyIterator { return nil } 110 | } 111 | return AnyIterator(FourtyTwoGame.moves.makeIterator()) 112 | } 113 | 114 | func evaluate(forPlayer player: Player) -> Evaluation { 115 | guard self.sum < 42 else { 116 | let score = Double(-self.moves.count) 117 | return (self.sum == 42) ? .victory(score) : .defeat(score) 118 | } 119 | let score = Double(abs(42 - self.sum)) 120 | return .ongoing(score) 121 | } 122 | } 123 | 124 | func ==(lhs: FourtyTwoGame, rhs: FourtyTwoGame) -> Bool { 125 | guard lhs.player == rhs.player else { 126 | return false 127 | } 128 | guard lhs.sum == rhs.sum else { 129 | return false 130 | } 131 | guard lhs.moves == rhs.moves else { 132 | return false 133 | } 134 | return true 135 | } 136 | 137 | extension FourtyTwoGame: CustomStringConvertible { 138 | var description: String { 139 | return "\(self.sum) @ \(self.moves)" 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /Tests/Strategist/FourtyTwo/FourtyTwoTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MiniMaxTests.swift 3 | // MiniMaxTests 4 | // 5 | // Created by Vincent Esche on 06/06/16. 6 | // Copyright © 2015 Vincent Esche. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import Strategist 11 | 12 | class FourtyTwoTests: XCTestCase { 13 | typealias Game = FourtyTwoGame 14 | typealias Player = FourtyTwoPlayer 15 | 16 | func testMiniMaxTreeSearch() { 17 | typealias Policy = SimpleTreeSearchPolicy 18 | typealias Strategy = MiniMaxTreeSearch 19 | 20 | var game = Game(player: Player()) 21 | let policy = Policy(maxMoves: 10, maxExplorationDepth: 10) 22 | let strategy = Strategy(policy: policy) 23 | while true { 24 | let evaluation = game.evaluate() 25 | guard !evaluation.isFinal else { 26 | XCTAssertTrue(evaluation.isVictory) 27 | break 28 | } 29 | let move = strategy.randomMaximizingMove(game)! 30 | game = game.update(move) 31 | } 32 | } 33 | 34 | func testNegaMaxTreeSearch() { 35 | typealias Policy = SimpleTreeSearchPolicy 36 | typealias Strategy = NegaMaxTreeSearch 37 | 38 | var game = Game(player: Player()) 39 | let policy = Policy(maxMoves: 10, maxExplorationDepth: 10) 40 | let strategy = Strategy(policy: policy) 41 | while true { 42 | let evaluation = game.evaluate() 43 | guard !evaluation.isFinal else { 44 | XCTAssertTrue(evaluation.isVictory) 45 | break 46 | } 47 | let move = strategy.randomMaximizingMove(game)! 48 | game = game.update(move) 49 | } 50 | } 51 | 52 | func testMonteCarloTreeSearch() { 53 | typealias Heuristic = UpperConfidenceBoundHeuristic 54 | typealias Policy = SimpleMonteCarloTreeSearchPolicy 55 | typealias Strategy = MonteCarloTreeSearch 56 | 57 | let rate = 0.75 58 | let plays = 10 59 | let wins = (0..= Int(Double(plays) * rate)) 86 | } 87 | 88 | // func testParallelMonteCarloTreeSearch() { 89 | // typealias Heuristic = UpperConfidenceBoundHeuristic 90 | // typealias Policy = SimpleMonteCarloTreeSearchPolicy 91 | // typealias Strategy = ParallelMonteCarloTreeSearch 92 | // 93 | // let threads = 1 94 | // let rate = 0.75 95 | // let plays = 10 96 | // let wins = (0..= Int(Double(plays) * rate)) 123 | // } 124 | } 125 | -------------------------------------------------------------------------------- /Tests/Strategist/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import StrategistTestSuite 3 | 4 | XCTMain([ 5 | testCase(StrategistTests.allTests), 6 | ]) 7 | -------------------------------------------------------------------------------- /Tests/Strategist/StrategistTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import Strategist 3 | 4 | class StrategistTests: XCTestCase { 5 | func testExample() { 6 | // This is an example of a functional test case. 7 | // Use XCTAssert and related functions to verify your tests produce the correct results. 8 | // XCTAssertEqual(Strategist().text, "Hello, World!") 9 | } 10 | 11 | 12 | static var allTests : [(String, (StrategistTests) -> () -> Void)] { 13 | return [ 14 | // ("testExample", testExample), 15 | ] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Tests/Strategist/TicTacToe/TicTacToe.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TicTacToe.swift 3 | // MiniMaxTests 4 | // 5 | // Created by Vincent Esche on 06/06/16. 6 | // Copyright © 2016 Vincent Esche. All rights reserved. 7 | // 8 | 9 | import Strategist 10 | 11 | enum TicTacToePlayer: Strategist.Player { 12 | case x 13 | case o 14 | } 15 | 16 | extension TicTacToePlayer: CustomStringConvertible { 17 | var description: String { 18 | switch self { 19 | case .x: return "X" 20 | case .o: return "O" 21 | } 22 | } 23 | } 24 | 25 | extension TicTacToePlayer: CustomDebugStringConvertible { 26 | var debugDescription: String { 27 | switch self { 28 | case .x: return "Max" 29 | case .o: return "Min" 30 | } 31 | } 32 | } 33 | 34 | enum TicTacToeTile { 35 | case empty 36 | case occupied(TicTacToePlayer) 37 | 38 | var player: TicTacToePlayer? { 39 | switch self { 40 | case .empty: return nil 41 | case let .occupied(player): return player 42 | } 43 | } 44 | 45 | init(player: TicTacToePlayer?) { 46 | if let player = player { 47 | self = .occupied(player) 48 | } else { 49 | self = .empty 50 | } 51 | } 52 | } 53 | 54 | extension TicTacToeTile: Hashable { 55 | var hashValue: Int { 56 | switch self { 57 | case .empty: 58 | return 0 59 | case .occupied(let player): 60 | return player.hashValue 61 | } 62 | } 63 | } 64 | 65 | extension TicTacToeTile: Equatable {} 66 | 67 | func ==(lhs: TicTacToeTile, rhs: TicTacToeTile) -> Bool { 68 | switch (lhs, rhs) { 69 | case (.empty, .empty): return true 70 | case let (.occupied(playerLhs), .occupied(playerRhs)): return playerLhs == playerRhs 71 | default: return false 72 | } 73 | } 74 | 75 | extension TicTacToeTile: CustomStringConvertible { 76 | var description: String { 77 | switch self { 78 | case .empty: return " " 79 | case let .occupied(player): return "\(player)" 80 | } 81 | } 82 | } 83 | 84 | struct TicTacToeMove: Strategist.Move { 85 | let index: Int 86 | let player: TicTacToePlayer 87 | } 88 | 89 | extension TicTacToeMove: CustomStringConvertible { 90 | var description: String { 91 | return "\(self.index)" 92 | } 93 | } 94 | 95 | extension TicTacToeMove: Hashable { 96 | var hashValue: Int { 97 | return index 98 | } 99 | } 100 | 101 | extension TicTacToeMove: Equatable {} 102 | 103 | func ==(lhs: TicTacToeMove, rhs: TicTacToeMove) -> Bool { 104 | return (lhs.index == rhs.index) 105 | } 106 | 107 | /// The objective of this game is to be the first to make three marks 108 | /// in a horizontal, vertical, or diagonal row by placing marks on a 3x3 grid 109 | /// alternating between the two playing players turn by turn. 110 | struct TicTacToeGame: Strategist.Game { 111 | typealias Player = TicTacToePlayer 112 | typealias Move = TicTacToeMove 113 | typealias Score = Double 114 | 115 | let board: [TicTacToeTile] 116 | let players: [TicTacToePlayer] 117 | let playerIndex: UInt8 118 | 119 | var currentPlayer: Player { 120 | return self.players[Int(self.playerIndex)] 121 | } 122 | 123 | init(players: [TicTacToePlayer]) { 124 | assert(players.count == 2) 125 | assert(players[0] != players[1]) 126 | self.board = [TicTacToeTile](repeating: .empty, count: 9) 127 | self.players = players 128 | self.playerIndex = 0 129 | } 130 | 131 | fileprivate init(board: [TicTacToeTile], players: [TicTacToePlayer], playerIndex: UInt8) { 132 | assert(board.count == 9) 133 | assert(players.count == 2) 134 | assert(playerIndex < 2) 135 | self.board = board 136 | self.players = players 137 | self.playerIndex = playerIndex 138 | } 139 | 140 | func update(_ move: Move) -> TicTacToeGame { 141 | var board = self.board 142 | board[move.index] = TicTacToeTile(player: move.player) 143 | let players = self.players 144 | let playerIndex = (self.playerIndex + 1) % 2 145 | return TicTacToeGame(board: board, players: players, playerIndex: playerIndex) 146 | } 147 | 148 | func isFinished() -> Bool { 149 | return self.board.reduce(true) { $0 && $1 != .empty } 150 | } 151 | 152 | func playerAfter(_ player: Player) -> Player { 153 | guard let index = self.players.index(of: player) else { 154 | fatalError("Unknown player: \(player)") 155 | } 156 | return self.players[(index + 1) % 2] 157 | } 158 | 159 | func playersAreAllied(_ players: (Player, Player)) -> Bool { 160 | return players.0 == players.1 161 | } 162 | 163 | func availableMoves() -> AnyIterator { 164 | let lazyMap = self.board.enumerated().lazy.compactMap { index, tile in 165 | return (tile == .empty) ? TicTacToeMove(index: index, player: self.currentPlayer) : nil 166 | } 167 | return AnyIterator(lazyMap.makeIterator()) 168 | } 169 | 170 | func evaluate(forPlayer player: Player) -> Evaluation { 171 | var score = 0 172 | var occupied = 0 173 | let triples = TicTacToeGame.triples() 174 | for (a, b, c) in triples { 175 | let occupants = [self.board[a], self.board[b], self.board[c]].compactMap { $0.player } 176 | var playerOccupied = 0 177 | var opponentOccupied = 0 178 | for occupant in occupants { 179 | if occupant == player { 180 | playerOccupied += 1 181 | } else { 182 | opponentOccupied += 1 183 | } 184 | occupied += 1 185 | } 186 | if playerOccupied == 3 { 187 | return .victory(0.0) 188 | } else if opponentOccupied == 3 { 189 | return .defeat(0.0) 190 | } 191 | score += playerOccupied - opponentOccupied 192 | } 193 | if occupied == triples.count * 3 { 194 | return .draw(0.0) 195 | } 196 | return .ongoing(Double(score)) 197 | } 198 | 199 | static func triples() -> [(Int, Int, Int)] { 200 | struct Holder { 201 | static let triplesArray = [ 202 | (0, 1, 2), // top row 203 | (3, 4, 5), // center row 204 | (6, 7, 8), // bottom row 205 | (0, 3, 6), // left column 206 | (1, 4, 7), // center column 207 | (2, 5, 8), // right column 208 | (0, 4, 8), // tl-to-br diagonal 209 | (2, 4, 6), // tr-to-bl diagonal 210 | ] 211 | } 212 | return Holder.triplesArray 213 | } 214 | } 215 | 216 | func ==(lhs: TicTacToeGame, rhs: TicTacToeGame) -> Bool { 217 | guard lhs.board == rhs.board else { 218 | return false 219 | } 220 | guard lhs.players == rhs.players else { 221 | return false 222 | } 223 | guard lhs.playerIndex == rhs.playerIndex else { 224 | return false 225 | } 226 | return true 227 | } 228 | 229 | //extension TicTacToeGame: Hashable { 230 | // var hashValue: Int { 231 | // return self.board.enumerate().reduce(0) { hash, tuple in 232 | // let (index, tile) = tuple 233 | // return hash ^ index ^ tile.hashValue 234 | // } 235 | // } 236 | //} 237 | 238 | extension TicTacToeGame: CustomStringConvertible { 239 | var description: String { 240 | let board = [self.board[0...2], self.board[3...5], self.board[6...8]].map { row in 241 | row.map { "\($0)" }.joined(separator: " | ") 242 | }.joined(separator: "\n") 243 | return "\(self.currentPlayer):\n\(board)" 244 | } 245 | } 246 | -------------------------------------------------------------------------------- /Tests/Strategist/TicTacToe/TicTacToeTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MiniMaxTests.swift 3 | // MiniMaxTests 4 | // 5 | // Created by Vincent Esche on 06/06/16. 6 | // Copyright © 2015 Vincent Esche. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import Strategist 11 | 12 | class TicTacToeTests: XCTestCase { 13 | typealias Game = TicTacToeGame 14 | typealias Player = TicTacToePlayer 15 | 16 | func testMiniMaxTreeSearch() { 17 | typealias Policy = SimpleTreeSearchPolicy 18 | typealias Strategy = MiniMaxTreeSearch 19 | 20 | let players: [Player] = [.x, .o] 21 | var game = Game(players: players) 22 | let policy = Policy(maxMoves: 10, maxExplorationDepth: 10) 23 | let strategy = Strategy(policy: policy) 24 | while true { 25 | let evaluation = game.evaluate() 26 | guard !evaluation.isFinal else { 27 | // Correct deterministic AIs should always play draws against themselves: 28 | XCTAssertTrue(evaluation.isDraw) 29 | break 30 | } 31 | if let move = strategy.randomMaximizingMove(game) { 32 | game = game.update(move) 33 | } else { 34 | XCTFail() 35 | break 36 | } 37 | } 38 | } 39 | 40 | func testNegaMaxTreeSearch() { 41 | typealias Policy = SimpleTreeSearchPolicy 42 | typealias Strategy = NegaMaxTreeSearch 43 | 44 | let players: [Player] = [.x, .o] 45 | var game = Game(players: players) 46 | let policy = Policy(maxMoves: 10, maxExplorationDepth: 10) 47 | let strategy = Strategy(policy: policy) 48 | while true { 49 | let evaluation = game.evaluate() 50 | guard !evaluation.isFinal else { 51 | // Correct deterministic AIs should always play draws against themselves: 52 | XCTAssertTrue(evaluation.isDraw) 53 | break 54 | } 55 | if let move = strategy.randomMaximizingMove(game) { 56 | game = game.update(move) 57 | } else { 58 | XCTFail() 59 | break 60 | } 61 | } 62 | } 63 | 64 | func testMonteCarloTreeSearch() { 65 | typealias Heuristic = UpperConfidenceBoundHeuristic 66 | typealias Policy = SimpleMonteCarloTreeSearchPolicy 67 | typealias Strategy = MonteCarloTreeSearch 68 | 69 | let rate = 0.75 70 | let plays = 10 71 | let wins = (0..() 85 | var i = 0 86 | while true { 87 | let evaluation = game.evaluate() 88 | guard !evaluation.isFinal else { 89 | if (i % 2 == 0) && (evaluation.isDraw || evaluation.isVictory) { 90 | return wins + 1 91 | } else if (evaluation.isDraw || evaluation.isDefeat) { 92 | return wins + 1 93 | } 94 | return wins 95 | } 96 | if i % 2 == 0 { 97 | let epochs = 10 98 | for _ in 0..= Int(Double(plays) * rate)) 114 | } 115 | 116 | // func testParallelMonteCarloTreeSearch() { 117 | // typealias Heuristic = UpperConfidenceBoundHeuristic 118 | // typealias Policy = SimpleMonteCarloTreeSearchPolicy 119 | // typealias Strategy = ParallelMonteCarloTreeSearch 120 | // 121 | // let threads = 8 122 | // let rate = 0.75 123 | // let plays = 10 124 | // let wins = (0..() 138 | // var i = 0 139 | // while true { 140 | // let evaluation = game.evaluate() 141 | // guard !evaluation.isFinal else { 142 | // if (i % 2 == 0) && (evaluation.isDraw || evaluation.isVictory) { 143 | // return wins + 1 144 | // } else if (evaluation.isDraw || evaluation.isDefeat) { 145 | // return wins + 1 146 | // } 147 | // return wins 148 | // } 149 | // if i % 2 == 0 { 150 | // let epochs = 10 151 | // for _ in 0..= Int(Double(plays) * rate)) 167 | // } 168 | } 169 | --------------------------------------------------------------------------------