├── .gitignore ├── Cartfile ├── Cartfile.resolved ├── LICENSE.md ├── README.md ├── SudoChess.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── xcshareddata │ ├── xcbaselines │ │ └── 27E3216F231DD2AB00B4B520.xcbaseline │ │ │ ├── 4A6F6598-5D91-4DD4-8053-39DEBCC6673C.plist │ │ │ ├── 58855021-6656-43BF-82C9-E44D1E241E1C.plist │ │ │ └── Info.plist │ └── xcschemes │ │ └── SudoChess.xcscheme └── xcuserdata │ └── cruxcode.xcuserdatad │ └── xcschemes │ └── xcschememanagement.plist ├── SudoChess ├── AI Opponents │ ├── ModestMike.swift │ └── StupidSteve.swift ├── Documentation.playground │ ├── Pages │ │ ├── Custom Scenarios.xcplaygroundpage │ │ │ └── Contents.swift │ │ ├── Getting Started.xcplaygroundpage │ │ │ └── Contents.swift │ │ └── Making Chess Computers.xcplaygroundpage │ │ │ └── Contents.swift │ └── contents.xcplayground ├── Extensions │ ├── ArrayExtension.swift │ ├── ColorExtension.swift │ └── IndexPathExtension.swift ├── Grids │ ├── Board.swift │ ├── BooleanChessGrid.swift │ ├── ChessGrid.swift │ └── Grid.swift ├── Handlers │ └── CheckHandler.swift ├── Icons.xcassets │ ├── Contents.json │ ├── bishop_black.imageset │ │ ├── Contents.json │ │ ├── bishop_black.png │ │ ├── bishop_black@2x.png │ │ └── bishop_black@3x.png │ ├── bishop_white.imageset │ │ ├── Contents.json │ │ ├── bishop_white.png │ │ ├── bishop_white@2x.png │ │ └── bishop_white@3x.png │ ├── king_black.imageset │ │ ├── Contents.json │ │ ├── king_black.png │ │ ├── king_black@2x.png │ │ └── king_black@3x.png │ ├── king_white.imageset │ │ ├── Contents.json │ │ ├── king_white.png │ │ ├── king_white@2x.png │ │ └── king_white@3x.png │ ├── knight_black.imageset │ │ ├── Contents.json │ │ ├── knight_black.png │ │ ├── knight_black@2x.png │ │ └── knight_black@3x.png │ ├── knight_white.imageset │ │ ├── Contents.json │ │ ├── knight_white.png │ │ ├── knight_white@2x.png │ │ └── knight_white@3x.png │ ├── pawn_black.imageset │ │ ├── Contents.json │ │ ├── pawn_black.png │ │ ├── pawn_black@2x.png │ │ └── pawn_black@3x.png │ ├── pawn_white.imageset │ │ ├── Contents.json │ │ ├── pawn_white.png │ │ ├── pawn_white@2x.png │ │ └── pawn_white@3x.png │ ├── queen_black.imageset │ │ ├── Contents.json │ │ ├── queen_black.png │ │ ├── queen_black@2x.png │ │ └── queen_black@3x.png │ ├── queen_white.imageset │ │ ├── Contents.json │ │ ├── queen_white.png │ │ ├── queen_white@2x.png │ │ └── queen_white@3x.png │ ├── rook_black.imageset │ │ ├── Contents.json │ │ ├── rook_black.png │ │ ├── rook_black@2x.png │ │ └── rook_black@3x.png │ └── rook_white.imageset │ │ ├── Contents.json │ │ ├── rook_white.png │ │ ├── rook_white@2x.png │ │ └── rook_white@3x.png ├── Info.plist ├── Models │ ├── ArtificialOpponent.swift │ ├── DirectedPosition.swift │ ├── Game.swift │ ├── GameViewModel.swift │ ├── Move.swift │ ├── Player.swift │ ├── Position.swift │ ├── Roster.swift │ └── Team.swift ├── Pieces │ ├── Bishop.swift │ ├── King.swift │ ├── Knight.swift │ ├── Pawn.swift │ ├── Piece.swift │ ├── Queen.swift │ └── Rook.swift ├── SudoChess.h └── Views │ ├── BoardView.swift │ ├── ConsoleView.swift │ ├── GameView.swift │ ├── HistoryView.swift │ ├── PieceView.swift │ └── TileView.swift └── SudoChessTests ├── BishopSpec.swift ├── BooleanChessGridSpec.swift ├── CheckHandlerPerformanceSpec.swift ├── CheckHandlerSpec.swift ├── DirectedPositionSpec.swift ├── GameSpec.swift ├── GridSpec.swift ├── Info.plist ├── KingSpec.swift ├── KnightSpec.swift ├── PawnSpec.swift ├── PositionSpec.swift ├── QueenSpec.swift ├── RookSpec.swift └── SudoChessTests.swift /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## User settings 6 | xcuserdata/ 7 | 8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 9 | *.xcscmblueprint 10 | *.xccheckout 11 | 12 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 13 | build/ 14 | DerivedData/ 15 | *.moved-aside 16 | *.pbxuser 17 | !default.pbxuser 18 | *.mode1v3 19 | !default.mode1v3 20 | *.mode2v3 21 | !default.mode2v3 22 | *.perspectivev3 23 | !default.perspectivev3 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | 28 | ## App packaging 29 | *.ipa 30 | *.dSYM.zip 31 | *.dSYM 32 | 33 | ## Playgrounds 34 | timeline.xctimeline 35 | playground.xcworkspace 36 | 37 | # Swift Package Manager 38 | # 39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 40 | # Packages/ 41 | # Package.pins 42 | # Package.resolved 43 | # *.xcodeproj 44 | # 45 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 46 | # hence it is not needed unless you have added a package configuration file to your project 47 | # .swiftpm 48 | 49 | .build/ 50 | 51 | # CocoaPods 52 | # 53 | # We recommend against adding the Pods directory to your .gitignore. However 54 | # you should judge for yourself, the pros and cons are mentioned at: 55 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 56 | # 57 | # Pods/ 58 | # 59 | # Add this line if you want to avoid checking in source code from the Xcode workspace 60 | # *.xcworkspace 61 | 62 | # Carthage 63 | # 64 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 65 | Carthage/Checkouts 66 | 67 | Carthage/Build/ 68 | 69 | # Accio dependency management 70 | Dependencies/ 71 | .accio/ 72 | 73 | # fastlane 74 | # 75 | # It is recommended to not store the screenshots in the git repo. 76 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 77 | # For more information about the recommended setup visit: 78 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 79 | 80 | fastlane/report.xml 81 | fastlane/Preview.html 82 | fastlane/screenshots/**/*.png 83 | fastlane/test_output 84 | 85 | # Code Injection 86 | # 87 | # After new code Injection tools there's a generated folder /iOSInjectionProject 88 | # https://github.com/johnno1962/injectionforxcode 89 | 90 | iOSInjectionProject/ 91 | -------------------------------------------------------------------------------- /Cartfile: -------------------------------------------------------------------------------- 1 | github "Quick/Quick" 2 | github "Quick/Nimble" 3 | -------------------------------------------------------------------------------- /Cartfile.resolved: -------------------------------------------------------------------------------- 1 | github "Quick/Nimble" "v8.0.4" 2 | github "Quick/Quick" "v2.2.0" 3 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Daniel Panzer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | --- 24 | 25 | Chess Piece images in the included interface were acquired [here][1] from and are in use under the **CC BY-SA 3.0** license 26 | 27 | [1]: https://commons.wikimedia.org/wiki/Category:SVG_chess_pieces 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **SudoChess** is an open-ended chess library leveraging **SwiftUI** and **Combine** that makes it easy to speak the language of chess. Design your own computer opponent, or use it as a foundation for your own chess app. Includes a simple board and game interface all written in **SwiftUI**. 2 | 3 | ![preview](http://s3.amazonaws.com/PermanentHosting/sudochess_preview.gif) 4 | 5 | ## Getting Started 6 | 7 | Clone the repo and check out `Documentation.playground` for interactive framework documentation. 8 | 9 | SwiftUI: 10 | 11 | ```swift 12 | //Add a game view model to your SwiftUI view as an @ObservableObject 13 | //Pass the view model into the included game interface 14 | 15 | struct ContentView: View { 16 | 17 | @ObservedObject var viewModel: GameViewModel = GameViewModel(roster: Roster(whitePlayer: .human, 18 | blackPlayer: .ai(ModestMike()))) 19 | var body: some View { 20 | GameView(viewModel: self.viewModel) 21 | } 22 | } 23 | 24 | ``` 25 | 26 | UIKit: 27 | 28 | ```swift 29 | //Construct a game view model with the desired players 30 | 31 | let viewModel = GameViewModel(roster: Roster(whitePlayer: .human), 32 | blackPlayer: .ai(ModestMike()))) 33 | 34 | //Provide the view model to the included game interface 35 | 36 | let gameView = GameView(viewModel: viewModel) 37 | let viewController = UIHostingController(rootView: gameView) 38 | 39 | ``` 40 | 41 | ## Creating a computer opponent 42 | 43 | Write a new class that conforms to `ArtificialOpponent`: 44 | 45 | ```swift 46 | /// Extremely simple AI full of bad move decisions 47 | class StupidSteve : ArtificialOpponent { 48 | 49 | var name: String { 50 | "Stupid Steve" 51 | } 52 | 53 | func nextMove(in game: Game) -> Move { 54 | 55 | let validMoves = game.currentMoves() 56 | let piecesWeCanCapture: [(piece: Piece, move: Move)] = validMoves 57 | .compactMap { move in 58 | guard let potentiallyCapturedPiece = game.board[move.destination] else { 59 | return nil 60 | } 61 | 62 | return (potentiallyCapturedPiece, move) 63 | } 64 | 65 | if piecesWeCanCapture.count > 0 { 66 | 67 | let mostValuableCapture = piecesWeCanCapture 68 | .sorted(by: {return $0.piece.value > $1.piece.value}) 69 | .first! 70 | 71 | return mostValuableCapture.move 72 | 73 | } else { 74 | return validMoves.randomElement()! 75 | } 76 | } 77 | } 78 | ``` 79 | 80 | Pass your class into the game roster to play against it: 81 | 82 | ```swift 83 | let roster = Roster(whitePlayer: .human, blackPlayer: .ai(StupidSteve())) 84 | let viewModel = GameViewModel(roster: roster) 85 | ``` 86 | -------------------------------------------------------------------------------- /SudoChess.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /SudoChess.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /SudoChess.xcodeproj/xcshareddata/xcbaselines/27E3216F231DD2AB00B4B520.xcbaseline/4A6F6598-5D91-4DD4-8053-39DEBCC6673C.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | classNames 6 | 7 | CheckHandlerPerformanceSpec 8 | 9 | test_CheckHandler_checkSummaryForStandardGame_performance() 10 | 11 | com.apple.XCTPerformanceMetric_WallClockTime 12 | 13 | baselineAverage 14 | 0.216 15 | baselineIntegrationDisplayName 16 | Local Baseline 17 | 18 | 19 | test_CheckHandler_validMovesForStandardGame_performance() 20 | 21 | com.apple.XCTPerformanceMetric_WallClockTime 22 | 23 | baselineAverage 24 | 0.479 25 | baselineIntegrationDisplayName 26 | Local Baseline 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /SudoChess.xcodeproj/xcshareddata/xcbaselines/27E3216F231DD2AB00B4B520.xcbaseline/58855021-6656-43BF-82C9-E44D1E241E1C.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | classNames 6 | 7 | CheckHandlerPerformanceSpec 8 | 9 | test_CheckHandler_checkSummaryForStandardGame_performance() 10 | 11 | com.apple.XCTPerformanceMetric_WallClockTime 12 | 13 | baselineAverage 14 | 0.22748 15 | baselineIntegrationDisplayName 16 | Local Baseline 17 | 18 | 19 | test_CheckHandler_validMovesForStandardGame_performance() 20 | 21 | com.apple.XCTPerformanceMetric_WallClockTime 22 | 23 | baselineAverage 24 | 0.52475 25 | baselineIntegrationDisplayName 26 | Local Baseline 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /SudoChess.xcodeproj/xcshareddata/xcbaselines/27E3216F231DD2AB00B4B520.xcbaseline/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | runDestinationsByUUID 6 | 7 | 4A6F6598-5D91-4DD4-8053-39DEBCC6673C 8 | 9 | localComputer 10 | 11 | busSpeedInMHz 12 | 100 13 | cpuCount 14 | 1 15 | cpuKind 16 | Quad-Core Intel Core i7 17 | cpuSpeedInMHz 18 | 2200 19 | logicalCPUCoresPerPackage 20 | 8 21 | modelCode 22 | MacBookPro11,4 23 | physicalCPUCoresPerPackage 24 | 4 25 | platformIdentifier 26 | com.apple.platform.macosx 27 | 28 | targetArchitecture 29 | x86_64 30 | targetDevice 31 | 32 | modelCode 33 | iPad8,1 34 | platformIdentifier 35 | com.apple.platform.iphonesimulator 36 | 37 | 38 | 58855021-6656-43BF-82C9-E44D1E241E1C 39 | 40 | localComputer 41 | 42 | busSpeedInMHz 43 | 100 44 | cpuCount 45 | 1 46 | cpuKind 47 | Quad-Core Intel Core i7 48 | cpuSpeedInMHz 49 | 2200 50 | logicalCPUCoresPerPackage 51 | 8 52 | modelCode 53 | MacBookPro11,4 54 | physicalCPUCoresPerPackage 55 | 4 56 | platformIdentifier 57 | com.apple.platform.macosx 58 | 59 | targetArchitecture 60 | x86_64 61 | targetDevice 62 | 63 | modelCode 64 | iPhone12,3 65 | platformIdentifier 66 | com.apple.platform.iphonesimulator 67 | 68 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /SudoChess.xcodeproj/xcshareddata/xcschemes/SudoChess.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 32 | 33 | 39 | 40 | 41 | 42 | 44 | 50 | 51 | 52 | 53 | 54 | 64 | 65 | 71 | 72 | 78 | 79 | 80 | 81 | 83 | 84 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /SudoChess.xcodeproj/xcuserdata/cruxcode.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | SudoChess.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 1 11 | 12 | 13 | SuppressBuildableAutocreation 14 | 15 | 27E32166231DD2AB00B4B520 16 | 17 | primary 18 | 19 | 20 | 27E3216F231DD2AB00B4B520 21 | 22 | primary 23 | 24 | 25 | 27F8B45123270E3A004D7996 26 | 27 | primary 28 | 29 | 30 | 27F8B45923270E3A004D7996 31 | 32 | primary 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /SudoChess/AI Opponents/ModestMike.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ModestMike.swift 3 | // SudoChess 4 | // 5 | // Created by Daniel Panzer on 10/23/19. 6 | // Copyright © 2019 Daniel Panzer. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// AI written around conserving piece values. Will make generally intelligent moves and go after your most valuable pieces 12 | public class ModestMike { 13 | 14 | public init() {} 15 | 16 | private struct ScenarioAnalysis { 17 | 18 | let mostValuablePieceThreatenedByOpponent: Piece? 19 | let mostValuablePieceThreatenedByUs: Piece? 20 | 21 | var opponentThreatValue: Int { 22 | return mostValuablePieceThreatenedByOpponent?.value ?? 0 23 | } 24 | 25 | var ourThreatValue: Int { 26 | return mostValuablePieceThreatenedByUs?.value ?? 0 27 | } 28 | 29 | var threatScore: Int { 30 | return ourThreatValue - opponentThreatValue 31 | } 32 | } 33 | 34 | private struct MoveAnalysis { 35 | let capturedPiece: Piece? 36 | let oldScenario: ScenarioAnalysis 37 | let newScenario: ScenarioAnalysis 38 | let move: Move 39 | 40 | var exchangeRatio: Int { 41 | (capturedPiece?.value ?? 0) - newScenario.opponentThreatValue 42 | } 43 | 44 | var threatScore: Int { 45 | return newScenario.threatScore - oldScenario.threatScore 46 | } 47 | 48 | func isObjectivelyBetter(than otherAnalysis: MoveAnalysis) -> Bool { 49 | 50 | guard self.exchangeRatio == otherAnalysis.exchangeRatio else { 51 | return self.exchangeRatio > otherAnalysis.exchangeRatio 52 | } 53 | 54 | return self.threatScore > otherAnalysis.threatScore 55 | } 56 | } 57 | 58 | private func analysis(for move: Move, by team: Team, in game: Game) -> MoveAnalysis { 59 | 60 | let currentMostValuablePieceThreatenedByOpponent = mostValuablePieceThreatened(by: team.opponent, in: game) 61 | let currentMostValuablePieceThreatenedByUs = mostValuablePieceThreatened(by: team, in: game) 62 | let oldScenarioAnalysis = ScenarioAnalysis(mostValuablePieceThreatenedByOpponent: currentMostValuablePieceThreatenedByOpponent, 63 | mostValuablePieceThreatenedByUs: currentMostValuablePieceThreatenedByUs) 64 | 65 | let newScenario = game.performing(move) 66 | 67 | let newMostValuablePieceThreatenedByOpponent = mostValuablePieceThreatened(by: team.opponent, in: newScenario) 68 | let newMostValuablePieceThreatenedByUs = mostValuablePieceThreatened(by: team, in: newScenario) 69 | let newScenarioAnalysis = ScenarioAnalysis(mostValuablePieceThreatenedByOpponent: newMostValuablePieceThreatenedByOpponent, 70 | mostValuablePieceThreatenedByUs: newMostValuablePieceThreatenedByUs) 71 | 72 | return MoveAnalysis(capturedPiece: move.capturedPiece, 73 | oldScenario: oldScenarioAnalysis, 74 | newScenario: newScenarioAnalysis, 75 | move: move) 76 | } 77 | 78 | private func mostValuablePieceThreatened(by team: Team, in game: Game) -> Piece? { 79 | 80 | let threatenedPositions = game.positionsThreatened(by: team) 81 | 82 | return zip(threatenedPositions.indices, threatenedPositions) 83 | .filter { position, isThreatened in isThreatened } 84 | .compactMap { position, isThreatened in 85 | guard let piece = game.board[position] else {return nil} 86 | return piece 87 | } 88 | .sorted(by: {$0.value > $1.value}) 89 | .first 90 | } 91 | 92 | } 93 | 94 | extension ModestMike : ArtificialOpponent { 95 | 96 | public var name: String { 97 | "Modest Mike" 98 | } 99 | 100 | public func nextMove(in game: Game) -> Move { 101 | 102 | let moves = game.currentMoves() 103 | let analysies = moves 104 | .map { move in 105 | analysis(for: move, by: game.turn, in: game) 106 | } 107 | .sorted(by: {$0.isObjectivelyBetter(than: $1)}) 108 | 109 | return analysies.first!.move 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /SudoChess/AI Opponents/StupidSteve.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StupidSteve.swift 3 | // SudoChess 4 | // 5 | // Created by Daniel Panzer on 10/23/19. 6 | // Copyright © 2019 Daniel Panzer. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// Extremely simple AI full of bad move decisions 12 | public class StupidSteve { 13 | public init() {} 14 | } 15 | 16 | extension StupidSteve : ArtificialOpponent { 17 | 18 | public var name: String { 19 | "Stupid Steve" 20 | } 21 | 22 | public func nextMove(in game: Game) -> Move { 23 | 24 | let validMoves = game.currentMoves() 25 | let piecesWeCanCapture: [(piece: Piece, move: Move)] = validMoves 26 | .compactMap { move in 27 | guard let potentiallyCapturedPiece = game.board[move.destination] else { 28 | return nil 29 | } 30 | 31 | return (potentiallyCapturedPiece, move) 32 | } 33 | 34 | if piecesWeCanCapture.count > 0 { 35 | 36 | let mostValuableCapture = piecesWeCanCapture 37 | .sorted(by: {return $0.piece.value > $1.piece.value}) 38 | .first! 39 | 40 | return mostValuableCapture.move 41 | 42 | } else { 43 | return validMoves.randomElement()! 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /SudoChess/Documentation.playground/Pages/Custom Scenarios.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | 3 | import Foundation 4 | import SudoChess 5 | import SwiftUI 6 | import PlaygroundSupport 7 | 8 | //: Create your own scenario 9 | 10 | let customBoard: Board = { 11 | 12 | let pieces: [Piece?] = [ 13 | 14 | Knight(owner: .black), 15 | Knight(owner: .black), 16 | Knight(owner: .black), 17 | Queen(owner: .black), 18 | King(owner: .black), 19 | Knight(owner: .black), 20 | Knight(owner: .black), 21 | Knight(owner: .black), 22 | 23 | Knight(owner: .black), 24 | Knight(owner: .black), 25 | Knight(owner: .black), 26 | Knight(owner: .black), 27 | Knight(owner: .black), 28 | Knight(owner: .black), 29 | Knight(owner: .black), 30 | Knight(owner: .black), 31 | 32 | nil, nil, nil, nil, nil, nil, nil, nil, 33 | nil, nil, nil, nil, nil, nil, nil, nil, 34 | nil, nil, nil, nil, nil, nil, nil, nil, 35 | nil, nil, nil, nil, nil, nil, nil, nil, 36 | 37 | Knight(owner: .white), 38 | Knight(owner: .white), 39 | Knight(owner: .white), 40 | Knight(owner: .white), 41 | Knight(owner: .white), 42 | Knight(owner: .white), 43 | Knight(owner: .white), 44 | Knight(owner: .white), 45 | 46 | Knight(owner: .white), 47 | Knight(owner: .white), 48 | Knight(owner: .white), 49 | Queen(owner: .white), 50 | King(owner: .white), 51 | Knight(owner: .white), 52 | Knight(owner: .white), 53 | Knight(owner: .white) 54 | ] 55 | 56 | return Board(array: pieces) 57 | }() 58 | 59 | let roster = Roster(whitePlayer: .ai(ModestMike()), 60 | blackPlayer: .ai(ModestMike())) 61 | 62 | let customGame = Game(board: customBoard) 63 | 64 | let viewModel = GameViewModel(roster: roster, game: customGame) 65 | let view = BoardView(viewModel: viewModel) 66 | PlaygroundPage.current.liveView = UIHostingController(rootView: view) 67 | 68 | -------------------------------------------------------------------------------- /SudoChess/Documentation.playground/Pages/Getting Started.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: Getting started 2 | 3 | import UIKit 4 | import SwiftUI 5 | import SudoChess 6 | import PlaygroundSupport 7 | 8 | let roster = Roster(whitePlayer: .human, 9 | blackPlayer: .ai(ModestMike())) 10 | 11 | let viewModel = GameViewModel(roster: roster) 12 | let view = GameView(viewModel: viewModel) 13 | PlaygroundPage.current.liveView = UIHostingController(rootView: view) 14 | 15 | //: Leveraging Combine 16 | 17 | let stateSubscription = viewModel 18 | .$state 19 | .sink { state in print(state) } 20 | 21 | let moveSubscription = viewModel 22 | .$game 23 | .compactMap { game in game.history.last } 24 | .sink { lastMove in print(lastMove) } 25 | 26 | //: [Next](@next) 27 | -------------------------------------------------------------------------------- /SudoChess/Documentation.playground/Pages/Making Chess Computers.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | 3 | import UIKit 4 | import SwiftUI 5 | import SudoChess 6 | import PlaygroundSupport 7 | 8 | //: Making a chess computer 9 | 10 | ///A gambler at heart 11 | class RandomRob : ArtificialOpponent { 12 | 13 | var name: String { 14 | return "Random Rob" 15 | } 16 | 17 | func nextMove(in game: Game) -> Move { 18 | let availableMoves = game.currentMoves() 19 | return availableMoves.randomElement()! 20 | } 21 | } 22 | 23 | //Add your chess computer to the roster 24 | let roster = Roster(whitePlayer: .ai(RandomRob()), 25 | blackPlayer: .ai(ModestMike())) 26 | 27 | let viewModel = GameViewModel(roster: roster) 28 | let view = GameView(viewModel: viewModel) 29 | PlaygroundPage.current.liveView = UIHostingController(rootView: view) 30 | 31 | //More fun subscriptions as Modest Mike destroys Random Rob 32 | let subscription = 33 | viewModel 34 | .$game 35 | .sink { currentGame in 36 | 37 | guard let capturedPiece = currentGame.history.last?.capturedPiece else {return} 38 | 39 | let whiteTotalPieceValue = 40 | currentGame 41 | .board 42 | .compactMap { $0 } 43 | .filter { piece in piece.owner == .white && !(piece is King) } 44 | .reduce(0) { currentValue, piece in currentValue + piece.value } 45 | 46 | let blackTotalPieceValue = 47 | currentGame 48 | .board 49 | .compactMap { $0 } 50 | .filter { piece in piece.owner == .black && !(piece is King) } 51 | .reduce(0) { currentValue, piece in currentValue + piece.value } 52 | 53 | let message = """ 54 | Captured Piece: \(capturedPiece) (\(capturedPiece.owner)) 55 | New Total Piece Values 56 | White: \(whiteTotalPieceValue), Black: \(blackTotalPieceValue) 57 | 58 | """ 59 | 60 | print(message) 61 | } 62 | 63 | //: [Next](@next) 64 | -------------------------------------------------------------------------------- /SudoChess/Documentation.playground/contents.xcplayground: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /SudoChess/Extensions/ArrayExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ArrayExtension.swift 3 | // SudoChess 4 | // 5 | // Created by Daniel Panzer on 11/3/19. 6 | // Copyright © 2019 Daniel Panzer. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension Array where Element : Hashable { 12 | 13 | func toSet() -> Set { 14 | return Set(self) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /SudoChess/Extensions/ColorExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ColorExtension.swift 3 | // SudoChess 4 | // 5 | // Created by Daniel Panzer on 10/10/19. 6 | // Copyright © 2019 Daniel Panzer. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import UIKit 11 | 12 | extension Color { 13 | 14 | static let darkGray: Color = { 15 | return Color(.darkGray) 16 | }() 17 | } 18 | -------------------------------------------------------------------------------- /SudoChess/Extensions/IndexPathExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IndexPathExtension.swift 3 | // SudoChess 4 | // 5 | // Created by Daniel Panzer on 9/2/19. 6 | // Copyright © 2019 Daniel Panzer. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension IndexPath { 12 | 13 | init(row: Int, column: Int) { 14 | self.init(indexes: [column, row]) 15 | } 16 | 17 | var row: Int { 18 | return self[1] 19 | } 20 | 21 | var column: Int { 22 | return self[0] 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /SudoChess/Grids/Board.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Board.swift 3 | // SudoChess 4 | // 5 | // Created by Daniel Panzer on 10/14/19. 6 | // Copyright © 2019 Daniel Panzer. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SwiftUI 11 | 12 | public typealias Board = ChessGrid 13 | 14 | extension Board { 15 | 16 | /// A standard starting chessboard 17 | public static let standard: Board = { 18 | 19 | let pieces: [Piece?] = [ 20 | 21 | Rook(owner: .black), 22 | Knight(owner: .black), 23 | Bishop(owner: .black), 24 | Queen(owner: .black), 25 | King(owner: .black), 26 | Bishop(owner: .black), 27 | Knight(owner: .black), 28 | Rook(owner: . black), 29 | 30 | Pawn(owner: .black), 31 | Pawn(owner: .black), 32 | Pawn(owner: .black), 33 | Pawn(owner: .black), 34 | Pawn(owner: .black), 35 | Pawn(owner: .black), 36 | Pawn(owner: .black), 37 | Pawn(owner: .black), 38 | 39 | nil, nil, nil, nil, nil, nil, nil, nil, 40 | nil, nil, nil, nil, nil, nil, nil, nil, 41 | nil, nil, nil, nil, nil, nil, nil, nil, 42 | nil, nil, nil, nil, nil, nil, nil, nil, 43 | 44 | Pawn(owner: .white), 45 | Pawn(owner: .white), 46 | Pawn(owner: .white), 47 | Pawn(owner: .white), 48 | Pawn(owner: .white), 49 | Pawn(owner: .white), 50 | Pawn(owner: .white), 51 | Pawn(owner: .white), 52 | 53 | Rook(owner: . white), 54 | Knight(owner: .white), 55 | Bishop(owner: .white), 56 | Queen(owner: .white), 57 | King(owner: .white), 58 | Bishop(owner: .white), 59 | Knight(owner: .white), 60 | Rook(owner: . white) 61 | ] 62 | 63 | return ChessGrid(array: pieces) 64 | }() 65 | 66 | static let colors: ChessGrid = { 67 | 68 | let colors: [Color] = zip(standard.indices, standard) 69 | .map { position, piece in 70 | return (position.row.rawValue + position.rank.rawValue) % 2 == 0 ? .darkGray : .white 71 | } 72 | 73 | return ChessGrid(array: colors) 74 | }() 75 | } 76 | -------------------------------------------------------------------------------- /SudoChess/Grids/BooleanChessGrid.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BooleanGrid.swift 3 | // SudoChess 4 | // 5 | // Created by Daniel Panzer on 9/17/19. 6 | // Copyright © 2019 Daniel Panzer. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public let O = BooleanChessGrid.O 12 | public let X = BooleanChessGrid.X 13 | 14 | public typealias BooleanChessGrid = ChessGrid 15 | 16 | extension BooleanChessGrid { 17 | 18 | static let O = true 19 | static let X = false 20 | 21 | static let `false` = BooleanChessGrid(array: Array(repeating: false, count: 64)) 22 | static let `true` = BooleanChessGrid(array: Array(repeating: true, count: 64)) 23 | 24 | public init() { 25 | self = BooleanChessGrid(array: Array(repeating: false, count: 64)) 26 | } 27 | 28 | init(positions: [Position]) { 29 | self = BooleanChessGrid() 30 | for position in positions { 31 | self[position] = true 32 | } 33 | } 34 | 35 | func toMoves(from origin: Position, in board: Board) -> Set { 36 | assert(self[origin] == false, "we shouldn't be creating moves where the origin and destination are the same") 37 | return zip(self.indices, self) 38 | .compactMap { destination, canMove in 39 | guard canMove else {return nil} 40 | return Move(origin: origin, destination: destination, capturedPiece: board[destination]) 41 | } 42 | .toSet() 43 | } 44 | } 45 | 46 | extension BooleanChessGrid : SetAlgebra { 47 | 48 | public var isEmpty: Bool { 49 | return self == BooleanChessGrid.false 50 | } 51 | 52 | public func union(_ other: BooleanChessGrid) -> BooleanChessGrid { 53 | let newArray = zip(self, other).map{$0 || $1} 54 | return BooleanChessGrid(array: newArray) 55 | } 56 | 57 | public func intersection(_ other: BooleanChessGrid) -> BooleanChessGrid { 58 | let newArray = zip(self, other).map{$0 && $1} 59 | return BooleanChessGrid(array: newArray) 60 | } 61 | 62 | public func symmetricDifference(_ other: BooleanChessGrid) -> BooleanChessGrid { 63 | let newArray = zip(self, other) 64 | .map {(Int(truncating: NSNumber(value: $0)) ^ Int(truncating: NSNumber(value: $1))) == 1} 65 | return BooleanChessGrid(array: newArray) 66 | } 67 | 68 | public mutating func insert(_ newMember: Bool) -> (inserted: Bool, memberAfterInsert: Bool) { 69 | return (false, false) 70 | } 71 | 72 | public mutating func remove(_ member: Bool) -> Bool? { 73 | return nil 74 | } 75 | 76 | public mutating func update(with newMember: Bool) -> Bool? { 77 | return nil 78 | } 79 | 80 | public mutating func formUnion(_ other: BooleanChessGrid) { 81 | let newArray = zip(self, other).map{$0 || $1} 82 | self = BooleanChessGrid(array: newArray) 83 | } 84 | 85 | public mutating func formIntersection(_ other: BooleanChessGrid) { 86 | let newArray = zip(self, other).map{$0 && $1} 87 | self = BooleanChessGrid(array: newArray) 88 | } 89 | 90 | public mutating func formSymmetricDifference(_ other: BooleanChessGrid) { 91 | let newArray = zip(self, other) 92 | .map {(Int(truncating: NSNumber(value: $0)) ^ Int(truncating: NSNumber(value: $1))) == 1} 93 | self = BooleanChessGrid(array: newArray) 94 | } 95 | 96 | 97 | } 98 | 99 | -------------------------------------------------------------------------------- /SudoChess/Grids/ChessGrid.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ChessGrid.swift 3 | // SudoChess 4 | // 5 | // Created by Daniel Panzer on 9/2/19. 6 | // Copyright © 2019 Daniel Panzer. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// A generalized grid data structure that speaks in chess positions 12 | public struct ChessGrid { 13 | 14 | /// Creates a grid from an array of values, which will populate from the upper left to lower right 15 | /// - Precondition: Array count must exactly match the number of grid elements (rowSize * columnSize) 16 | /// - Parameter array: The list of values with which to populate the grid 17 | public init(array: [Element]) { 18 | self.grid = Grid(rowSize: 8, columnSize: 8, using: array) 19 | } 20 | 21 | /// Creates a grid from a single value. This single value will be repeated for every position of the grid 22 | /// - Parameter repeatingElement: The element with which to populate the grid 23 | public init(repeatingElement prototypeElement: Element) { 24 | self.grid = Grid(rowSize: 8, columnSize: 8, using: Array(repeating: prototypeElement, count: 64)) 25 | } 26 | 27 | private var grid: Grid 28 | 29 | public subscript(position: Position) -> Element { 30 | get { 31 | return grid[position.gridIndex] 32 | } 33 | set { 34 | grid[position.gridIndex] = newValue 35 | } 36 | } 37 | } 38 | 39 | extension ChessGrid : Equatable where Element : Equatable {} 40 | 41 | extension ChessGrid : Hashable where Element : Hashable {} 42 | 43 | extension ChessGrid : ExpressibleByArrayLiteral { 44 | 45 | public init(arrayLiteral elements: Element...) { 46 | self = ChessGrid(array: elements) 47 | } 48 | 49 | public typealias ArrayLiteralElement = Element 50 | 51 | } 52 | 53 | extension ChessGrid where Element : ExpressibleByNilLiteral { 54 | 55 | init() { 56 | self.grid = Grid(rowSize: 8, columnSize: 8) 57 | } 58 | } 59 | 60 | extension ChessGrid : Collection { 61 | 62 | public typealias Index = Position 63 | 64 | public var startIndex: Position { 65 | return Position(gridIndex: grid.startIndex)! 66 | } 67 | 68 | public var endIndex: Position { 69 | return Position(gridIndex: grid.endIndex)! 70 | } 71 | 72 | public func index(after i: Position) -> Position { 73 | return Position(gridIndex: grid.index(after: i.gridIndex))! 74 | } 75 | } 76 | 77 | extension ChessGrid : BidirectionalCollection { 78 | 79 | public func index(before i: Position) -> Position { 80 | return Position(gridIndex: grid.index(before: i.gridIndex))! 81 | } 82 | } 83 | 84 | extension ChessGrid : RandomAccessCollection {} 85 | 86 | extension ChessGrid : CustomStringConvertible { 87 | 88 | public var description: String { 89 | return grid.description 90 | } 91 | } 92 | 93 | extension ChessGrid : CustomDebugStringConvertible { 94 | 95 | public var debugDescription: String { 96 | return grid.debugDescription 97 | } 98 | } 99 | 100 | -------------------------------------------------------------------------------- /SudoChess/Grids/Grid.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Grid.swift 3 | // SudoChess 4 | // 5 | // Created by Daniel Panzer on 9/2/19. 6 | // Copyright © 2019 Daniel Panzer. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct Grid { 12 | 13 | private typealias Column = [Element] 14 | 15 | //MARK: Lifecycle 16 | 17 | init(rowSize: Int, columnSize: Int, using array: [Element]) { 18 | precondition(array.count == rowSize * columnSize, "passed array must match the grid size") 19 | 20 | var columns = Array(repeating: Column(), count: rowSize) 21 | var currentColumn = 0 22 | for element in array { 23 | columns[currentColumn].append(element) 24 | if currentColumn == columns.count - 1 { 25 | currentColumn = 0 26 | } else { 27 | currentColumn += 1 28 | } 29 | } 30 | 31 | self.columns = columns 32 | self.rowSize = rowSize 33 | self.columnSize = columnSize 34 | } 35 | 36 | //MARK: Public Interface 37 | 38 | let rowSize: Int 39 | let columnSize: Int 40 | 41 | subscript(row: Int, column: Int) -> Element { 42 | get { 43 | return columns[column][row] 44 | } 45 | set { 46 | columns[column][row] = newValue 47 | } 48 | } 49 | 50 | subscript(position: IndexPath) -> Element { 51 | get { 52 | return self[position.row, position.column] 53 | } 54 | set { 55 | self[position.row, position.column] = newValue 56 | } 57 | } 58 | 59 | //MARK: Private Properties 60 | 61 | private var columns: [Column] 62 | 63 | } 64 | 65 | extension Grid : Collection { 66 | 67 | func index(after index: IndexPath) -> IndexPath { 68 | if (index.column == columnSize - 1) { 69 | return IndexPath(row: index.row + 1, column: 0) 70 | } else { 71 | return IndexPath(row: index.row, column: index.column + 1) 72 | } 73 | } 74 | 75 | typealias Index = IndexPath 76 | 77 | var startIndex: IndexPath { 78 | return IndexPath(indexes: [0, 0]) 79 | } 80 | 81 | var endIndex: IndexPath { 82 | return IndexPath(indexes: [0, columnSize]) 83 | } 84 | } 85 | 86 | extension Grid : BidirectionalCollection { 87 | 88 | func index(before index: IndexPath) -> IndexPath { 89 | if (index.column == 0) { 90 | return IndexPath(row: index.row - 1, column: columnSize - 1) 91 | } else { 92 | return IndexPath(row: index.row, column: index.column - 1) 93 | } 94 | } 95 | } 96 | 97 | extension Grid : RandomAccessCollection {} 98 | 99 | extension Grid : CustomStringConvertible { 100 | 101 | var description: String { 102 | return columns.reduce("", { (currentResult, column) -> String in 103 | return currentResult + String(describing: column) + "\n" 104 | }) 105 | } 106 | } 107 | 108 | extension Grid : CustomDebugStringConvertible { 109 | 110 | var debugDescription: String { 111 | return description 112 | } 113 | } 114 | 115 | extension Grid where Element : ExpressibleByNilLiteral { 116 | 117 | init(rowSize: Int, columnSize: Int) { 118 | let emptyArray = Array(repeating: nil, count: rowSize*columnSize) 119 | self = Grid(rowSize: rowSize, columnSize: columnSize, using: emptyArray) 120 | } 121 | } 122 | 123 | extension Grid : Equatable where Element : Equatable {} 124 | 125 | extension Grid : Hashable where Element : Hashable {} 126 | -------------------------------------------------------------------------------- /SudoChess/Handlers/CheckHandler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CheckHandler.swift 3 | // SudoChess 4 | // 5 | // Created by Daniel Panzer on 10/1/19. 6 | // Copyright © 2019 Daniel Panzer. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | protocol CheckHandlerInterface { 12 | func state(for team: Team, in game: Game) -> CheckHandler.State 13 | func validMoves(from possibleMoves: Set, in game: Game) -> Set 14 | } 15 | 16 | struct CheckHandler { 17 | 18 | enum State { 19 | case none 20 | case check 21 | case checkmate 22 | } 23 | 24 | struct Summary : Equatable { 25 | let white: State 26 | let black: State 27 | 28 | subscript(team: Team) -> State { 29 | switch team { 30 | case .black: 31 | return self.black 32 | case .white: 33 | return self.white 34 | } 35 | } 36 | } 37 | 38 | private func oneOfThese(scenarios: [Game], doesNotThreatenTheKingBelongingTo team: Team) -> Bool { 39 | scenarios 40 | .filter { scenario in 41 | let playerNewKingPosition = scenario.kingPosition(for: team) 42 | let opposingPlayersNewThreatenedPositions = scenario.positionsThreatened(by: team.opponent) 43 | let playersKingIsThreatenedInThisScenario = opposingPlayersNewThreatenedPositions[playerNewKingPosition] 44 | return !playersKingIsThreatenedInThisScenario 45 | } 46 | .count > 0 47 | } 48 | } 49 | 50 | extension CheckHandler : CheckHandlerInterface { 51 | 52 | func state(for team: Team, in game: Game) -> CheckHandler.State { 53 | 54 | let whiteKingPosition = game 55 | .kingPosition(for: .white) 56 | 57 | let blackKingPosition = game 58 | .kingPosition(for: .black) 59 | 60 | let whitePlayerThreatenedPositions = game 61 | .positionsThreatened(by: .white) 62 | 63 | let blackPlayerThreatenedPositions = game 64 | .positionsThreatened(by: .black) 65 | 66 | switch team { 67 | 68 | case .white: 69 | 70 | let whitePlayerStatus: State = { 71 | 72 | let whiteKingIsThreatened = blackPlayerThreatenedPositions[whiteKingPosition] 73 | let allPossibleWhiteMoves = game.allMoves(for: .white) 74 | let allPossibleNewGameStates = allPossibleWhiteMoves 75 | .map {game.performing($0)} 76 | 77 | if whiteKingIsThreatened { 78 | guard game.turn == .white else {return .checkmate} 79 | 80 | let somePossibleWhiteMoveEliminatesKingThreat = oneOfThese(scenarios: allPossibleNewGameStates, doesNotThreatenTheKingBelongingTo: .white) 81 | return somePossibleWhiteMoveEliminatesKingThreat ? .check : .checkmate 82 | 83 | } else { 84 | guard game.turn == .white else {return .none} 85 | 86 | let somePossibleWhiteMoveDoesNotIntroduceKingThreat = oneOfThese(scenarios: allPossibleNewGameStates, doesNotThreatenTheKingBelongingTo: .white) 87 | return somePossibleWhiteMoveDoesNotIntroduceKingThreat ? .none : .checkmate 88 | } 89 | }() 90 | 91 | return whitePlayerStatus 92 | 93 | case .black: 94 | 95 | let blackPlayerStatus: State = { 96 | 97 | let blackKingIsThreatened = whitePlayerThreatenedPositions[blackKingPosition] 98 | let allPossibleBlackMoves = game.allMoves(for: .black) 99 | let allPossibleNewGameStates = allPossibleBlackMoves 100 | .map {game.performing($0)} 101 | 102 | if blackKingIsThreatened { 103 | guard game.turn == .black else {return .checkmate} 104 | 105 | let somePossibleBlackMoveEliminatesKingThreat = oneOfThese(scenarios: allPossibleNewGameStates, doesNotThreatenTheKingBelongingTo: .black) 106 | return somePossibleBlackMoveEliminatesKingThreat ? .check : .checkmate 107 | 108 | } else { 109 | guard game.turn == .black else {return .none} 110 | 111 | let somePossibleBlackMoveDoesNotIntroduceKingThreat = oneOfThese(scenarios: allPossibleNewGameStates, doesNotThreatenTheKingBelongingTo: .black) 112 | return somePossibleBlackMoveDoesNotIntroduceKingThreat ? .none : .checkmate 113 | } 114 | }() 115 | 116 | return blackPlayerStatus 117 | } 118 | } 119 | } 120 | 121 | extension CheckHandlerInterface { 122 | 123 | func validMoves(from possibleMoves: Set, in game: Game) -> Set { 124 | 125 | let potentialNewScenarios: [Game] = possibleMoves 126 | .map { possibleMove in game.performing(possibleMove) } 127 | 128 | let validMoves: [Move] = zip(possibleMoves, potentialNewScenarios) 129 | .compactMap { possibleMove, scenario in 130 | guard state(for: game.turn, in: scenario) == .none else {return nil} 131 | return possibleMove 132 | } 133 | 134 | return Set(validMoves) 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /SudoChess/Icons.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /SudoChess/Icons.xcassets/bishop_black.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "bishop_black.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "bishop_black@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "bishop_black@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | }, 23 | "properties" : { 24 | "template-rendering-intent" : "original" 25 | } 26 | } -------------------------------------------------------------------------------- /SudoChess/Icons.xcassets/bishop_black.imageset/bishop_black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielpanzer/SudoChess/03954d4eb86eec42e577627a73fc9d095fc00b46/SudoChess/Icons.xcassets/bishop_black.imageset/bishop_black.png -------------------------------------------------------------------------------- /SudoChess/Icons.xcassets/bishop_black.imageset/bishop_black@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielpanzer/SudoChess/03954d4eb86eec42e577627a73fc9d095fc00b46/SudoChess/Icons.xcassets/bishop_black.imageset/bishop_black@2x.png -------------------------------------------------------------------------------- /SudoChess/Icons.xcassets/bishop_black.imageset/bishop_black@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielpanzer/SudoChess/03954d4eb86eec42e577627a73fc9d095fc00b46/SudoChess/Icons.xcassets/bishop_black.imageset/bishop_black@3x.png -------------------------------------------------------------------------------- /SudoChess/Icons.xcassets/bishop_white.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "bishop_white.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "bishop_white@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "bishop_white@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | }, 23 | "properties" : { 24 | "template-rendering-intent" : "original" 25 | } 26 | } -------------------------------------------------------------------------------- /SudoChess/Icons.xcassets/bishop_white.imageset/bishop_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielpanzer/SudoChess/03954d4eb86eec42e577627a73fc9d095fc00b46/SudoChess/Icons.xcassets/bishop_white.imageset/bishop_white.png -------------------------------------------------------------------------------- /SudoChess/Icons.xcassets/bishop_white.imageset/bishop_white@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielpanzer/SudoChess/03954d4eb86eec42e577627a73fc9d095fc00b46/SudoChess/Icons.xcassets/bishop_white.imageset/bishop_white@2x.png -------------------------------------------------------------------------------- /SudoChess/Icons.xcassets/bishop_white.imageset/bishop_white@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielpanzer/SudoChess/03954d4eb86eec42e577627a73fc9d095fc00b46/SudoChess/Icons.xcassets/bishop_white.imageset/bishop_white@3x.png -------------------------------------------------------------------------------- /SudoChess/Icons.xcassets/king_black.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "king_black.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "king_black@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "king_black@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | }, 23 | "properties" : { 24 | "template-rendering-intent" : "original" 25 | } 26 | } -------------------------------------------------------------------------------- /SudoChess/Icons.xcassets/king_black.imageset/king_black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielpanzer/SudoChess/03954d4eb86eec42e577627a73fc9d095fc00b46/SudoChess/Icons.xcassets/king_black.imageset/king_black.png -------------------------------------------------------------------------------- /SudoChess/Icons.xcassets/king_black.imageset/king_black@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielpanzer/SudoChess/03954d4eb86eec42e577627a73fc9d095fc00b46/SudoChess/Icons.xcassets/king_black.imageset/king_black@2x.png -------------------------------------------------------------------------------- /SudoChess/Icons.xcassets/king_black.imageset/king_black@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielpanzer/SudoChess/03954d4eb86eec42e577627a73fc9d095fc00b46/SudoChess/Icons.xcassets/king_black.imageset/king_black@3x.png -------------------------------------------------------------------------------- /SudoChess/Icons.xcassets/king_white.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "king_white.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "king_white@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "king_white@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | }, 23 | "properties" : { 24 | "template-rendering-intent" : "original" 25 | } 26 | } -------------------------------------------------------------------------------- /SudoChess/Icons.xcassets/king_white.imageset/king_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielpanzer/SudoChess/03954d4eb86eec42e577627a73fc9d095fc00b46/SudoChess/Icons.xcassets/king_white.imageset/king_white.png -------------------------------------------------------------------------------- /SudoChess/Icons.xcassets/king_white.imageset/king_white@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielpanzer/SudoChess/03954d4eb86eec42e577627a73fc9d095fc00b46/SudoChess/Icons.xcassets/king_white.imageset/king_white@2x.png -------------------------------------------------------------------------------- /SudoChess/Icons.xcassets/king_white.imageset/king_white@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielpanzer/SudoChess/03954d4eb86eec42e577627a73fc9d095fc00b46/SudoChess/Icons.xcassets/king_white.imageset/king_white@3x.png -------------------------------------------------------------------------------- /SudoChess/Icons.xcassets/knight_black.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "knight_black.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "knight_black@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "knight_black@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | }, 23 | "properties" : { 24 | "template-rendering-intent" : "original" 25 | } 26 | } -------------------------------------------------------------------------------- /SudoChess/Icons.xcassets/knight_black.imageset/knight_black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielpanzer/SudoChess/03954d4eb86eec42e577627a73fc9d095fc00b46/SudoChess/Icons.xcassets/knight_black.imageset/knight_black.png -------------------------------------------------------------------------------- /SudoChess/Icons.xcassets/knight_black.imageset/knight_black@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielpanzer/SudoChess/03954d4eb86eec42e577627a73fc9d095fc00b46/SudoChess/Icons.xcassets/knight_black.imageset/knight_black@2x.png -------------------------------------------------------------------------------- /SudoChess/Icons.xcassets/knight_black.imageset/knight_black@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielpanzer/SudoChess/03954d4eb86eec42e577627a73fc9d095fc00b46/SudoChess/Icons.xcassets/knight_black.imageset/knight_black@3x.png -------------------------------------------------------------------------------- /SudoChess/Icons.xcassets/knight_white.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "knight_white.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "knight_white@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "knight_white@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | }, 23 | "properties" : { 24 | "template-rendering-intent" : "original" 25 | } 26 | } -------------------------------------------------------------------------------- /SudoChess/Icons.xcassets/knight_white.imageset/knight_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielpanzer/SudoChess/03954d4eb86eec42e577627a73fc9d095fc00b46/SudoChess/Icons.xcassets/knight_white.imageset/knight_white.png -------------------------------------------------------------------------------- /SudoChess/Icons.xcassets/knight_white.imageset/knight_white@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielpanzer/SudoChess/03954d4eb86eec42e577627a73fc9d095fc00b46/SudoChess/Icons.xcassets/knight_white.imageset/knight_white@2x.png -------------------------------------------------------------------------------- /SudoChess/Icons.xcassets/knight_white.imageset/knight_white@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielpanzer/SudoChess/03954d4eb86eec42e577627a73fc9d095fc00b46/SudoChess/Icons.xcassets/knight_white.imageset/knight_white@3x.png -------------------------------------------------------------------------------- /SudoChess/Icons.xcassets/pawn_black.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "pawn_black.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "pawn_black@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "pawn_black@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | }, 23 | "properties" : { 24 | "template-rendering-intent" : "original" 25 | } 26 | } -------------------------------------------------------------------------------- /SudoChess/Icons.xcassets/pawn_black.imageset/pawn_black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielpanzer/SudoChess/03954d4eb86eec42e577627a73fc9d095fc00b46/SudoChess/Icons.xcassets/pawn_black.imageset/pawn_black.png -------------------------------------------------------------------------------- /SudoChess/Icons.xcassets/pawn_black.imageset/pawn_black@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielpanzer/SudoChess/03954d4eb86eec42e577627a73fc9d095fc00b46/SudoChess/Icons.xcassets/pawn_black.imageset/pawn_black@2x.png -------------------------------------------------------------------------------- /SudoChess/Icons.xcassets/pawn_black.imageset/pawn_black@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielpanzer/SudoChess/03954d4eb86eec42e577627a73fc9d095fc00b46/SudoChess/Icons.xcassets/pawn_black.imageset/pawn_black@3x.png -------------------------------------------------------------------------------- /SudoChess/Icons.xcassets/pawn_white.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "pawn_white.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "pawn_white@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "pawn_white@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | }, 23 | "properties" : { 24 | "template-rendering-intent" : "original" 25 | } 26 | } -------------------------------------------------------------------------------- /SudoChess/Icons.xcassets/pawn_white.imageset/pawn_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielpanzer/SudoChess/03954d4eb86eec42e577627a73fc9d095fc00b46/SudoChess/Icons.xcassets/pawn_white.imageset/pawn_white.png -------------------------------------------------------------------------------- /SudoChess/Icons.xcassets/pawn_white.imageset/pawn_white@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielpanzer/SudoChess/03954d4eb86eec42e577627a73fc9d095fc00b46/SudoChess/Icons.xcassets/pawn_white.imageset/pawn_white@2x.png -------------------------------------------------------------------------------- /SudoChess/Icons.xcassets/pawn_white.imageset/pawn_white@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielpanzer/SudoChess/03954d4eb86eec42e577627a73fc9d095fc00b46/SudoChess/Icons.xcassets/pawn_white.imageset/pawn_white@3x.png -------------------------------------------------------------------------------- /SudoChess/Icons.xcassets/queen_black.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "queen_black.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "queen_black@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "queen_black@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | }, 23 | "properties" : { 24 | "template-rendering-intent" : "original" 25 | } 26 | } -------------------------------------------------------------------------------- /SudoChess/Icons.xcassets/queen_black.imageset/queen_black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielpanzer/SudoChess/03954d4eb86eec42e577627a73fc9d095fc00b46/SudoChess/Icons.xcassets/queen_black.imageset/queen_black.png -------------------------------------------------------------------------------- /SudoChess/Icons.xcassets/queen_black.imageset/queen_black@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielpanzer/SudoChess/03954d4eb86eec42e577627a73fc9d095fc00b46/SudoChess/Icons.xcassets/queen_black.imageset/queen_black@2x.png -------------------------------------------------------------------------------- /SudoChess/Icons.xcassets/queen_black.imageset/queen_black@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielpanzer/SudoChess/03954d4eb86eec42e577627a73fc9d095fc00b46/SudoChess/Icons.xcassets/queen_black.imageset/queen_black@3x.png -------------------------------------------------------------------------------- /SudoChess/Icons.xcassets/queen_white.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "queen_white.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "queen_white@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "queen_white@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | }, 23 | "properties" : { 24 | "template-rendering-intent" : "original" 25 | } 26 | } -------------------------------------------------------------------------------- /SudoChess/Icons.xcassets/queen_white.imageset/queen_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielpanzer/SudoChess/03954d4eb86eec42e577627a73fc9d095fc00b46/SudoChess/Icons.xcassets/queen_white.imageset/queen_white.png -------------------------------------------------------------------------------- /SudoChess/Icons.xcassets/queen_white.imageset/queen_white@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielpanzer/SudoChess/03954d4eb86eec42e577627a73fc9d095fc00b46/SudoChess/Icons.xcassets/queen_white.imageset/queen_white@2x.png -------------------------------------------------------------------------------- /SudoChess/Icons.xcassets/queen_white.imageset/queen_white@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielpanzer/SudoChess/03954d4eb86eec42e577627a73fc9d095fc00b46/SudoChess/Icons.xcassets/queen_white.imageset/queen_white@3x.png -------------------------------------------------------------------------------- /SudoChess/Icons.xcassets/rook_black.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "rook_black.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "rook_black@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "rook_black@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | }, 23 | "properties" : { 24 | "template-rendering-intent" : "original" 25 | } 26 | } -------------------------------------------------------------------------------- /SudoChess/Icons.xcassets/rook_black.imageset/rook_black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielpanzer/SudoChess/03954d4eb86eec42e577627a73fc9d095fc00b46/SudoChess/Icons.xcassets/rook_black.imageset/rook_black.png -------------------------------------------------------------------------------- /SudoChess/Icons.xcassets/rook_black.imageset/rook_black@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielpanzer/SudoChess/03954d4eb86eec42e577627a73fc9d095fc00b46/SudoChess/Icons.xcassets/rook_black.imageset/rook_black@2x.png -------------------------------------------------------------------------------- /SudoChess/Icons.xcassets/rook_black.imageset/rook_black@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielpanzer/SudoChess/03954d4eb86eec42e577627a73fc9d095fc00b46/SudoChess/Icons.xcassets/rook_black.imageset/rook_black@3x.png -------------------------------------------------------------------------------- /SudoChess/Icons.xcassets/rook_white.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "rook_white.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "rook_white@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "rook_white@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | }, 23 | "properties" : { 24 | "template-rendering-intent" : "original" 25 | } 26 | } -------------------------------------------------------------------------------- /SudoChess/Icons.xcassets/rook_white.imageset/rook_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielpanzer/SudoChess/03954d4eb86eec42e577627a73fc9d095fc00b46/SudoChess/Icons.xcassets/rook_white.imageset/rook_white.png -------------------------------------------------------------------------------- /SudoChess/Icons.xcassets/rook_white.imageset/rook_white@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielpanzer/SudoChess/03954d4eb86eec42e577627a73fc9d095fc00b46/SudoChess/Icons.xcassets/rook_white.imageset/rook_white@2x.png -------------------------------------------------------------------------------- /SudoChess/Icons.xcassets/rook_white.imageset/rook_white@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielpanzer/SudoChess/03954d4eb86eec42e577627a73fc9d095fc00b46/SudoChess/Icons.xcassets/rook_white.imageset/rook_white@3x.png -------------------------------------------------------------------------------- /SudoChess/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | $(MARKETING_VERSION) 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | 22 | 23 | -------------------------------------------------------------------------------- /SudoChess/Models/ArtificialOpponent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ArtificialOpponent.swift 3 | // SudoChess 4 | // 5 | // Created by Daniel Panzer on 10/23/19. 6 | // Copyright © 2019 Daniel Panzer. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public protocol ArtificialOpponent : class { 12 | 13 | /// Displayed name of the AI 14 | var name: String {get} 15 | 16 | /// This method is called by the game when it needs the AI to decide the next move. Implement your decision making code here 17 | /// - Parameter game: The current state of the game 18 | /// - Returns: The AI's move decision 19 | func nextMove(in game: Game) -> Move 20 | 21 | } 22 | -------------------------------------------------------------------------------- /SudoChess/Models/DirectedPosition.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DirectedPosition.swift 3 | // SudoChess 4 | // 5 | // Created by Daniel Panzer on 9/9/19. 6 | // Copyright © 2019 Daniel Panzer. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// A chess position from the perspective of a particular team 12 | public struct DirectedPosition { 13 | 14 | public let position: Position 15 | public let perspective: Team 16 | 17 | /// - Returns: A new directed position that represents the position in front of this position, from the same perspective, or nil if no such position exists on the board 18 | public var front: DirectedPosition? { 19 | 20 | guard let newRow = Position.Row( 21 | rawValue: perspective == .white ? position.row.rawValue + 1 : position.row.rawValue - 1 22 | ) else { 23 | return nil 24 | } 25 | 26 | let newPosition = Position(position.rank, newRow) 27 | return DirectedPosition(position: newPosition, perspective: perspective) 28 | } 29 | 30 | /// - Returns: A new directed position that represents the position in back of this position, from the same perspective, or nil if no such position exists on the board 31 | public var back: DirectedPosition? { 32 | 33 | guard let newRow = Position.Row( 34 | rawValue: perspective == .white ? position.row.rawValue - 1 : position.row.rawValue + 1 35 | ) else { 36 | return nil 37 | } 38 | 39 | let newPosition = Position(position.rank, newRow) 40 | return DirectedPosition(position: newPosition, perspective: perspective) 41 | } 42 | 43 | /// - Returns: A new directed position that represents the position to the left of this position, from the same perspective, or nil if no such position exists on the board 44 | public var left: DirectedPosition? { 45 | 46 | guard let newRank = Position.Rank( 47 | rawValue: perspective == .white ? position.rank.rawValue - 1 : position.rank.rawValue + 1 48 | ) else { 49 | return nil 50 | } 51 | 52 | let newPosition = Position(newRank, position.row) 53 | return DirectedPosition(position: newPosition, perspective: perspective) 54 | } 55 | 56 | /// - Returns: A new directed position that represents the position to the right of this position, from the same perspective, or nil if no such position exists on the board 57 | public var right: DirectedPosition? { 58 | 59 | guard let newRank = Position.Rank( 60 | rawValue: perspective == .white ? position.rank.rawValue + 1 : position.rank.rawValue - 1 61 | ) else { 62 | return nil 63 | } 64 | 65 | let newPosition = Position(newRank, position.row) 66 | return DirectedPosition(position: newPosition, perspective: perspective) 67 | } 68 | 69 | /// - Returns: A new directed position that represents the position to the front left corner of this position, from the same perspective, or nil if no such position exists on the board 70 | public var diagonalLeftFront: DirectedPosition? { 71 | return self 72 | .front? 73 | .left 74 | } 75 | 76 | /// - Returns: A new directed position that represents the position to the front right corner of this position, from the same perspective, or nil if no such position exists on the board 77 | public var diagonalRightFront: DirectedPosition? { 78 | return self 79 | .front? 80 | .right 81 | } 82 | 83 | /// - Returns: A new directed position that represents the position to the rear left corner of this position, from the same perspective, or nil if no such position exists on the board 84 | public var diagonalLeftBack: DirectedPosition? { 85 | return self 86 | .back? 87 | .left 88 | } 89 | 90 | /// - Returns: A new directed position that represents the position to the rear right corner of this position, from the same perspective, or nil if no such position exists on the board 91 | public var diagonalRightBack: DirectedPosition? { 92 | return self 93 | .back? 94 | .right 95 | } 96 | 97 | /// - Returns: All available positions in a line in front of this position, all from the same perspective 98 | public var frontSpaces: [DirectedPosition] { 99 | return allSpaces(using: { position in position.front }) 100 | } 101 | 102 | /// - Returns: All available positions in a line in back of this position, all from the same perspective 103 | public var backSpaces: [DirectedPosition] { 104 | return allSpaces(using: { position in position.back }) 105 | } 106 | 107 | /// - Returns: All available positions in a line to the left of this position, all from the same perspective 108 | public var leftSpaces: [DirectedPosition] { 109 | return allSpaces(using: { position in position.left }) 110 | } 111 | 112 | /// - Returns: All available positions in a line to the right of this position, all from the same perspective 113 | public var rightSpaces: [DirectedPosition] { 114 | return allSpaces(using: { position in position.right }) 115 | } 116 | 117 | /// - Returns: All available positions in a line on a left front diagonal from this position, all from the same perspective 118 | public var diagonalLeftFrontSpaces: [DirectedPosition] { 119 | return allSpaces(using: { position in position.diagonalLeftFront }) 120 | } 121 | 122 | /// - Returns: All available positions in a line on a right front diagonal from this position, all from the same perspective 123 | public var diagonalRightFrontSpaces: [DirectedPosition] { 124 | return allSpaces(using: { position in position.diagonalRightFront }) 125 | } 126 | 127 | /// - Returns: All available positions in a line on a left rear diagonal from this position, all from the same perspective 128 | public var diagonalLeftBackSpaces: [DirectedPosition] { 129 | return allSpaces(using: { position in position.diagonalLeftBack }) 130 | } 131 | 132 | /// - Returns: All available positions in a line on a right rear diagonal from this position, all from the same perspective 133 | public var diagonalRightBackSpaces: [DirectedPosition] { 134 | return allSpaces(using: { position in position.diagonalRightBack }) 135 | } 136 | 137 | private func allSpaces(using calculateNextPosition: (DirectedPosition) -> DirectedPosition?) -> [DirectedPosition] { 138 | 139 | var result = [DirectedPosition]() 140 | var currentPosition = self 141 | 142 | while let nextPosition = calculateNextPosition(currentPosition) { 143 | result.append(nextPosition) 144 | currentPosition = nextPosition 145 | } 146 | 147 | return result 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /SudoChess/Models/Game.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Game.swift 3 | // SudoChess 4 | // 5 | // Created by Daniel Panzer on 9/18/19. 6 | // Copyright © 2019 Daniel Panzer. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// A game of chess 12 | public struct Game : Equatable { 13 | 14 | /// Game of chess with a standard starting board 15 | public static let standard = Game(board: .standard) 16 | private static let validator = CheckHandler() 17 | 18 | //MARK: Lifecycle 19 | 20 | public init(board: Board) { 21 | self = Game(board: board, history: [], turn: .white) 22 | } 23 | 24 | init(board: Board, 25 | history: [Move] = [], 26 | turn: Team = .white) { 27 | 28 | self.board = board 29 | self.history = history 30 | self.turn = turn 31 | } 32 | 33 | //MARK: Public Properties 34 | 35 | ///The chessboard 36 | public let board: Board 37 | 38 | //The history of all moves performed 39 | public let history: [Move] 40 | 41 | //The current turn 42 | public let turn: Team 43 | 44 | //MARK: Public Modifiers 45 | 46 | /// Performs a move 47 | /// - Returns: A new version of the game with the move performed 48 | /// - Parameter move: The move to perform 49 | public func performing(_ move: Move) -> Game { 50 | 51 | var newBoard = board 52 | var newHistory = history 53 | let newTurn = turn.opponent 54 | 55 | let pieceToMove = board[move.origin]! 56 | newBoard[move.origin] = nil 57 | newBoard[move.destination] = pieceToMove 58 | 59 | switch move.kind { 60 | 61 | case .standard, .needsPromotion: 62 | 63 | break 64 | 65 | case .enPassant: 66 | 67 | let capturedPosition = 68 | DirectedPosition(position: move.destination, perspective: turn) 69 | .back! 70 | .position 71 | 72 | newBoard[capturedPosition] = nil 73 | 74 | case .castle: 75 | 76 | let isKingsideCastle = move.origin.rank.rawValue < move.destination.rank.rawValue 77 | let newRookRank = Position.Rank(rawValue: 78 | isKingsideCastle ? move.destination.rank.rawValue - 1 : move.destination.rank.rawValue + 1 79 | )! 80 | let rookDestination = Position(newRookRank, move.destination.row) 81 | let rookOrigin = Position(isKingsideCastle ? .h : .a, pieceToMove.owner == .white ? .one : .eight) 82 | 83 | assert(newBoard[rookOrigin] is Rook) 84 | 85 | let rook = board[rookOrigin]! 86 | newBoard[rookOrigin] = nil 87 | newBoard[rookDestination] = rook 88 | 89 | case .promotion(let promotionPiece): 90 | 91 | newBoard[move.destination] = promotionPiece 92 | 93 | } 94 | 95 | newHistory.append(move) 96 | 97 | return Game(board: newBoard, history: newHistory, turn: newTurn) 98 | } 99 | 100 | /// Reverses the last move made. If no moves have been made, this method has no effect on the state of the game 101 | /// - Returns: A new version of the game with the move reversed 102 | public func reversingLastMove() -> Game { 103 | 104 | var newBoard = board 105 | var newHistory = history 106 | let newTurn = turn.opponent 107 | 108 | guard let move = newHistory.popLast() else {return self} 109 | 110 | switch move.kind { 111 | 112 | case .standard, .needsPromotion: 113 | 114 | let pieceToReverse = newBoard[move.destination]! 115 | newBoard[move.origin] = pieceToReverse 116 | newBoard[move.destination] = move.capturedPiece 117 | 118 | case .enPassant: 119 | 120 | let pieceToReverse = board[move.destination]! 121 | newBoard[move.origin] = pieceToReverse 122 | newBoard[move.destination] = nil 123 | 124 | let capturedPosition = 125 | DirectedPosition(position: move.destination, perspective: turn.opponent) 126 | .back! 127 | .position 128 | 129 | newBoard[capturedPosition] = move.capturedPiece 130 | 131 | case .castle: 132 | 133 | let pieceToReverse = board[move.destination]! 134 | newBoard[move.origin] = pieceToReverse 135 | newBoard[move.destination] = nil 136 | 137 | let isKingsideCastle = move.origin.rank.rawValue < move.destination.rank.rawValue 138 | let rookToReverseRank = Position.Rank(rawValue: 139 | isKingsideCastle ? move.destination.rank.rawValue - 1 : move.destination.rank.rawValue + 1 140 | )! 141 | let rookCurrentPosition = Position(rookToReverseRank, move.destination.row) 142 | let rookDestination = Position(isKingsideCastle ? .h : .a, pieceToReverse.owner == .white ? .one : .eight) 143 | 144 | let rook = board[rookCurrentPosition]! 145 | newBoard[rookCurrentPosition] = nil 146 | newBoard[rookDestination] = rook 147 | 148 | case .promotion(_): 149 | 150 | let pieceToReverse = Pawn(owner: turn.opponent) 151 | newBoard[move.origin] = pieceToReverse 152 | newBoard[move.destination] = move.capturedPiece 153 | 154 | } 155 | 156 | return Game(board: newBoard, history: newHistory, turn: newTurn) 157 | } 158 | 159 | //MARK: Public Methods 160 | 161 | /// - Returns: The position of the king for a particular team 162 | /// - Parameter team: The team with which to perform a lookup 163 | public func kingPosition(for team: Team) -> Position { 164 | return zip(board.indices, board) 165 | .first(where: { position, piece in 166 | piece is King && piece?.owner == team 167 | })!.0 168 | } 169 | 170 | /// - Returns: A grid of threatened positions by a particular team. `true` on the grid represents a position that is threatened 171 | /// - Parameter team: The team with which to perform a lookup 172 | public func positionsThreatened(by team: Team) -> BooleanChessGrid { 173 | 174 | return zip(board.indices, board) 175 | .compactMap { position, piece in 176 | guard piece?.owner == team else {return nil} 177 | return piece?.threatenedPositions(from: position, in: self) 178 | } 179 | .reduce(BooleanChessGrid.false) { (nextPartialResult, moves) -> BooleanChessGrid in 180 | nextPartialResult.union(moves) 181 | } 182 | } 183 | 184 | /// - Returns: All moves that are available to the current turn 185 | public func currentMoves() -> Set { 186 | return Game 187 | .validator 188 | .validMoves(from: allMoves(for: turn), 189 | in: self) 190 | } 191 | 192 | //MARK: Internal Methods 193 | 194 | func moves(forPieceAt position: Position) -> Set? { 195 | guard let piece = board[position] else {return nil} 196 | return piece.possibleMoves(from: position, in: self) 197 | } 198 | 199 | func allMoves(for team: Team) -> Set { 200 | 201 | return board.indices 202 | .reduce(Set()) { (nextPartialResult, position) -> Set in 203 | 204 | guard let piece = board[position], piece.owner == team else {return nextPartialResult} 205 | return nextPartialResult.union(piece.possibleMoves(from: position, in: self)) 206 | } 207 | } 208 | } 209 | 210 | -------------------------------------------------------------------------------- /SudoChess/Models/GameViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GameViewModel.swift 3 | // SudoChess 4 | // 5 | // Created by Daniel Panzer on 10/23/19. 6 | // Copyright © 2019 Daniel Panzer. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Combine 11 | import SwiftUI 12 | 13 | protocol GameViewModelInterface { 14 | func select(_ position: Position) 15 | func reverseLastMove() 16 | func handlePromotion(with promotionType: Piece.Type) 17 | } 18 | 19 | /// A view model that handles the flow of game logic, alternating turns, and AI opponents 20 | public class GameViewModel : ObservableObject, Identifiable { 21 | 22 | private typealias ValidMoveCollection = Set 23 | 24 | /// The possible view states 25 | public enum State : Equatable, CustomStringConvertible { 26 | case awaitingHumanInput 27 | case working 28 | case computerThinking(name: String) 29 | case stalemate 30 | case gameOver(Team) 31 | 32 | public var description: String { 33 | switch self { 34 | case .awaitingHumanInput: 35 | return "Make your move..." 36 | case .working: 37 | return "Processing..." 38 | case .computerThinking(let name): 39 | return "\(name) is thinking..." 40 | case .stalemate: 41 | return "Stalemate!" 42 | case .gameOver(let winner): 43 | return "\(winner) has won!" 44 | } 45 | } 46 | } 47 | 48 | /// A board selection 49 | public struct Selection { 50 | public let moves: Set 51 | public let origin: Position 52 | 53 | var grid: BooleanChessGrid { 54 | return BooleanChessGrid(positions: moves.map {$0.destination}) 55 | } 56 | 57 | public func move(for position: Position) -> Move? { 58 | return moves.first(where: {$0.destination == position}) 59 | } 60 | } 61 | 62 | //MARK: Public Properties 63 | 64 | /// The assignment of players for this game 65 | public let roster: Roster 66 | 67 | /// The current game state 68 | @Published public private(set) var game: Game 69 | 70 | /// The current view state 71 | @Published public private(set) var state: State 72 | 73 | /// The current view selection 74 | @Published public private(set) var selection: Selection? 75 | 76 | /// Trigger for a piece promotion prompt 77 | public var shouldPromptForPromotion = PassthroughSubject() 78 | 79 | //MARK: Private Properties 80 | 81 | private let checkHandler: CheckHandlerInterface 82 | private var validMoveGrid = ChessGrid(repeatingElement: ValidMoveCollection()) 83 | 84 | //MARK: Lifecycle 85 | 86 | public init(roster: Roster, game: Game = Game.standard) { 87 | self.roster = roster 88 | self.game = game 89 | self.checkHandler = CheckHandler() 90 | self.state = .working 91 | 92 | self.beginNextTurn() 93 | } 94 | 95 | init(game: Game, 96 | roster: Roster = Roster(whitePlayer: .human, blackPlayer: .human), 97 | selection: Selection? = nil, 98 | checkHandler: CheckHandlerInterface = CheckHandler()) { 99 | 100 | self.game = game 101 | self.roster = roster 102 | self.selection = selection 103 | self.checkHandler = checkHandler 104 | self.state = .working 105 | 106 | self.beginNextTurn() 107 | } 108 | 109 | //MARK: Private Methods 110 | 111 | private func regenerateValidMoveGrid(completion: @escaping () -> ()) { 112 | self.state = .working 113 | 114 | DispatchQueue.global(qos: .userInitiated).async { 115 | self.validMoveGrid = ChessGrid(repeatingElement: ValidMoveCollection()) 116 | let allMoves = self.game.allMoves(for: self.game.turn) 117 | let validMoves = self.checkHandler.validMoves(from: allMoves, in: self.game) 118 | 119 | for validMove in validMoves { 120 | self.validMoveGrid[validMove.origin].insert(validMove) 121 | } 122 | 123 | DispatchQueue.main.async { 124 | completion() 125 | } 126 | } 127 | } 128 | 129 | private func beginNextTurn() { 130 | self.selection = nil 131 | 132 | guard checkHandler.state(for: self.game.turn, in: self.game) != .checkmate else { 133 | self.state = .gameOver(self.game.turn.opponent) 134 | return 135 | } 136 | 137 | switch roster[game.turn] { 138 | case .human: 139 | regenerateValidMoveGrid { 140 | self.state = .awaitingHumanInput 141 | } 142 | case .ai(let artificialOpponent): 143 | regenerateValidMoveGrid { 144 | self.performAIMove(for: artificialOpponent) { 145 | self.beginNextTurn() 146 | } 147 | } 148 | } 149 | } 150 | 151 | private func performAIMove(for artificialOpponent: ArtificialOpponent, callback: () -> ()) { 152 | self.state = .computerThinking(name: artificialOpponent.name) 153 | 154 | let minimumThinkingTime = DispatchTime.now() + DispatchTimeInterval.seconds(1) 155 | 156 | DispatchQueue.global(qos: .userInitiated).async { 157 | let nextMove = artificialOpponent.nextMove(in: self.game) 158 | 159 | DispatchQueue.main.asyncAfter(deadline: minimumThinkingTime) { 160 | 161 | self.select(nextMove.origin) 162 | 163 | let selectionDelay = DispatchTime.now() + DispatchTimeInterval.milliseconds(600) 164 | DispatchQueue.main.asyncAfter(deadline: selectionDelay) { 165 | self.perform(nextMove) 166 | } 167 | } 168 | } 169 | } 170 | 171 | private func perform(_ move: Move) { 172 | game = game.performing(move) 173 | 174 | if case .needsPromotion = move.kind { 175 | 176 | guard case .human = roster[game.turn.opponent] else { 177 | handlePromotion(with: Queen.self) 178 | return 179 | } 180 | 181 | shouldPromptForPromotion.send(move) 182 | } else { 183 | beginNextTurn() 184 | } 185 | } 186 | 187 | private func moves(for position: Position) -> Set? { 188 | let moves = validMoveGrid[position] 189 | return moves.isEmpty ? nil : moves 190 | } 191 | } 192 | 193 | extension GameViewModel : GameViewModelInterface { 194 | 195 | //MARK: Public Methods 196 | 197 | /// Select a current position on the board 198 | /// - Parameter position: The position to select 199 | public func select(_ position: Position) { 200 | switch selection { 201 | case .none: 202 | 203 | guard let moves = moves(for: position) else {return} 204 | self.selection = Selection(moves: moves, origin: position) 205 | 206 | case .some(let selection): 207 | 208 | if let moves = moves(for: position) { 209 | self.selection = Selection(moves: moves, origin: position) 210 | } else if let selectedMove = selection.move(for: position) { 211 | perform(selectedMove) 212 | return 213 | } else { 214 | self.selection = nil 215 | } 216 | } 217 | } 218 | 219 | /// Reverse the last move 220 | public func reverseLastMove() { 221 | self.game = game.reversingLastMove() 222 | 223 | if case .ai(_) = roster[self.game.turn] { 224 | self.game = game.reversingLastMove() 225 | } 226 | 227 | beginNextTurn() 228 | } 229 | 230 | /// Handle the promotion of a pawn that has reached the end of the board 231 | /// - Parameter promotionType: the type of piece to promote to 232 | public func handlePromotion(with promotionType: Piece.Type) { 233 | 234 | let moveToPromote = game.history.last! 235 | assert(moveToPromote.kind == .needsPromotion) 236 | 237 | game = game.reversingLastMove() 238 | 239 | let promotionMove = Move(origin: moveToPromote.origin, 240 | destination: moveToPromote.destination, 241 | capturedPiece: moveToPromote.capturedPiece, 242 | kind: .promotion(promotionType.init(owner: game.turn))) 243 | 244 | game = game.performing(promotionMove) 245 | beginNextTurn() 246 | } 247 | } 248 | 249 | -------------------------------------------------------------------------------- /SudoChess/Models/Move.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Move.swift 3 | // SudoChess 4 | // 5 | // Created by Daniel Panzer on 9/18/19. 6 | // Copyright © 2019 Daniel Panzer. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | ///A chess move 12 | public struct Move : Hashable { 13 | 14 | init(origin: Position, 15 | destination: Position, 16 | capturedPiece: Piece?, 17 | kind: Kind = .standard) { 18 | self.origin = origin 19 | self.destination = destination 20 | self.capturedPiece = capturedPiece 21 | self.kind = kind 22 | } 23 | 24 | /// The kind of chess move, to differentiate between a normal move and other moves that have special rules 25 | public enum Kind : Hashable { 26 | case standard 27 | case castle 28 | case enPassant 29 | case needsPromotion 30 | case promotion(Piece) 31 | } 32 | 33 | /// The starting position of the moved piece 34 | public let origin: Position 35 | 36 | /// The destination of the moved piece 37 | public let destination: Position 38 | 39 | /// The piece that was potentially captured by this move 40 | public let capturedPiece: Piece? 41 | 42 | /// The kind of move 43 | public let kind: Kind 44 | } 45 | 46 | extension Move : CustomStringConvertible { 47 | 48 | public var description: String { 49 | switch kind { 50 | case .standard: 51 | return "\(origin) -> \(destination)" 52 | case .castle: 53 | return "\(origin) -> \(destination) : Castle" 54 | case .enPassant: 55 | return "\(origin) -> \(destination) : En Passant" 56 | case .needsPromotion: 57 | return "\(origin) -> \(destination) : Needs Promotion" 58 | case .promotion(_): 59 | return "\(origin) -> \(destination) : Promotion" 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /SudoChess/Models/Player.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Player.swift 3 | // SudoChess 4 | // 5 | // Created by Daniel Panzer on 10/23/19. 6 | // Copyright © 2019 Daniel Panzer. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// Describes the possible kinds of chess players 12 | public enum Player { 13 | case human 14 | case ai(ArtificialOpponent) 15 | } 16 | 17 | extension Player : CustomStringConvertible { 18 | 19 | public var description: String { 20 | switch self { 21 | case .human: 22 | return "Human" 23 | case .ai(let artificialOpponent): 24 | return artificialOpponent.name 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /SudoChess/Models/Position.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ChessPosition.swift 3 | // SudoChess 4 | // 5 | // Created by Daniel Panzer on 9/2/19. 6 | // Copyright © 2019 Daniel Panzer. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// Describes a single position on a chessboard 12 | public struct Position : Equatable { 13 | 14 | public init(_ rank: Rank, _ row: Row) { 15 | self.rank = rank 16 | self.row = row 17 | } 18 | 19 | init?(gridIndex: IndexPath) { 20 | 21 | guard gridIndex != IndexPath(row: 8, column: 0) else { 22 | self = Position(.a, .end) 23 | return 24 | } 25 | 26 | guard 27 | let row = Row(rawValue: 8 - gridIndex.row), 28 | let rank = Rank(rawValue: gridIndex.column + 1) 29 | else {return nil} 30 | 31 | self.rank = rank 32 | self.row = row 33 | } 34 | 35 | /// A chess rank, moving from left to right 36 | public enum Rank : Int { 37 | case a = 1 38 | case b 39 | case c 40 | case d 41 | case e 42 | case f 43 | case g 44 | case h 45 | } 46 | 47 | /// A chess row, moving from bottom to top 48 | public enum Row : Int { 49 | 50 | case end = -1 51 | 52 | case one = 1 53 | case two = 2 54 | case three = 3 55 | case four = 4 56 | case five = 5 57 | case six = 6 58 | case seven = 7 59 | case eight = 8 60 | } 61 | 62 | /// The rank of the position 63 | public let rank: Rank 64 | 65 | 66 | /// The row of the position 67 | public let row: Row 68 | 69 | var gridIndex: IndexPath { 70 | let convertedRow = abs(row.rawValue - 8) 71 | let convertedRank = rank.rawValue - 1 72 | return IndexPath(row: convertedRow, column: convertedRank) 73 | } 74 | 75 | /// Generates a directed position, exposing further position logic 76 | /// - Parameter team: The team perspective from which to create the directed position 77 | public func fromPerspective(of team: Team) -> DirectedPosition { 78 | return DirectedPosition(position: self, perspective: team) 79 | } 80 | 81 | /// - Returns: True if the position is directly adjacent (on sides or corners) to the passed position; false if not 82 | /// - Parameter otherPosition: The position to compare 83 | public func isAdjacent(to otherPosition: Position) -> Bool { 84 | let directedPosition = DirectedPosition(position: self, perspective: .white) 85 | let adjacentPositions = [ 86 | directedPosition.front, 87 | directedPosition.back, 88 | directedPosition.left, 89 | directedPosition.right, 90 | directedPosition.diagonalLeftFront, 91 | directedPosition.diagonalRightFront, 92 | directedPosition.diagonalRightBack, 93 | directedPosition.diagonalLeftBack 94 | ].compactMap({$0?.position}) 95 | 96 | return adjacentPositions.contains(otherPosition) 97 | } 98 | 99 | static func pathConsideringCollisions( 100 | for team: Team, 101 | traversing path: [Position], 102 | in board: Board 103 | ) -> [Position] { 104 | 105 | assert(Position.isValidPath(path)) 106 | 107 | var result = [Position]() 108 | 109 | for position in path { 110 | 111 | switch board[position] { 112 | case .none: 113 | result.append(position) 114 | continue 115 | case .some(let collidingPiece): 116 | 117 | if collidingPiece.owner == team.opponent { 118 | result.append(position) 119 | } 120 | 121 | return result 122 | } 123 | } 124 | 125 | return result 126 | } 127 | 128 | static func isValidPath(_ array: [Position]) -> Bool { 129 | 130 | guard array.count > 0 else {return true} 131 | 132 | var iterator = array.makeIterator() 133 | var currentElement: Position = iterator.next()! 134 | 135 | while let nextElement = iterator.next() { 136 | guard currentElement.isAdjacent(to: nextElement) else {return false} 137 | currentElement = nextElement 138 | } 139 | 140 | return true 141 | } 142 | } 143 | 144 | extension Position : Comparable { 145 | 146 | public static func < (lhs: Position, rhs: Position) -> Bool { 147 | return lhs.gridIndex < rhs.gridIndex 148 | } 149 | } 150 | 151 | extension Position : Hashable {} 152 | 153 | extension Position : CustomStringConvertible { 154 | 155 | public var description: String { 156 | "\(rank)-\(row.rawValue)" 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /SudoChess/Models/Roster.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Roster.swift 3 | // SudoChess 4 | // 5 | // Created by Daniel Panzer on 10/23/19. 6 | // Copyright © 2019 Daniel Panzer. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// An assignment of two players to teams 12 | public struct Roster { 13 | 14 | public init(whitePlayer: Player, blackPlayer: Player) { 15 | self.whitePlayer = whitePlayer 16 | self.blackPlayer = blackPlayer 17 | } 18 | 19 | public let whitePlayer: Player 20 | public let blackPlayer: Player 21 | 22 | public subscript(team: Team) -> Player { 23 | switch team { 24 | case .white: 25 | return whitePlayer 26 | case .black: 27 | return blackPlayer 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /SudoChess/Models/Team.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ChessColor.swift 3 | // SudoChess 4 | // 5 | // Created by Daniel Panzer on 9/2/19. 6 | // Copyright © 2019 Daniel Panzer. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SwiftUI 11 | 12 | /// Describes the two possible teams, white and black 13 | public enum Team { 14 | case white 15 | case black 16 | 17 | public var opponent: Team { 18 | switch self { 19 | case .white: 20 | return .black 21 | case .black: 22 | return .white 23 | } 24 | } 25 | 26 | var color: Color { 27 | switch self { 28 | case .white: 29 | return .white 30 | case .black: 31 | return .black 32 | } 33 | } 34 | } 35 | 36 | extension Team : CustomStringConvertible { 37 | 38 | public var description: String { 39 | switch self { 40 | case .white: 41 | return "white" 42 | case .black: 43 | return "black" 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /SudoChess/Pieces/Bishop.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Bishop.swift 3 | // SudoChess 4 | // 5 | // Created by Daniel Panzer on 9/17/19. 6 | // Copyright © 2019 Daniel Panzer. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public class Bishop : Piece { 12 | 13 | public required init(owner: Team) { 14 | super.init(owner: owner) 15 | } 16 | 17 | public override var value: Int { 18 | return 4 19 | } 20 | 21 | override func possibleMoves(from position: Position, in game: Game) -> Set { 22 | return threatenedPositions(from: position, in: game) 23 | .toMoves(from: position, in: game.board) 24 | } 25 | 26 | override func threatenedPositions(from position: Position, in game: Game) -> BooleanChessGrid { 27 | 28 | let directedPosition = DirectedPosition(position: position, perspective: owner) 29 | 30 | let frontLeftDiagonal = Position 31 | .pathConsideringCollisions(for: owner, 32 | traversing: directedPosition.diagonalLeftFrontSpaces.map({$0.position}), 33 | in: game.board) 34 | 35 | let frontRightDiagonal = Position 36 | .pathConsideringCollisions(for: owner, 37 | traversing: directedPosition.diagonalRightFrontSpaces.map({$0.position}), 38 | in: game.board) 39 | 40 | let backLeftDiagonal = Position 41 | .pathConsideringCollisions(for: owner, 42 | traversing: directedPosition.diagonalLeftBackSpaces.map({$0.position}), 43 | in: game.board) 44 | 45 | let backRightDiagonal = Position 46 | .pathConsideringCollisions(for: owner, 47 | traversing: directedPosition.diagonalRightBackSpaces.map({$0.position}), 48 | in: game.board) 49 | 50 | let allMoves = frontLeftDiagonal + frontRightDiagonal + backLeftDiagonal + backRightDiagonal 51 | return BooleanChessGrid(positions: allMoves) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /SudoChess/Pieces/King.swift: -------------------------------------------------------------------------------- 1 | // 2 | // King.swift 3 | // SudoChess 4 | // 5 | // Created by Daniel Panzer on 9/17/19. 6 | // Copyright © 2019 Daniel Panzer. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public class King : Piece { 12 | 13 | public required init(owner: Team) { 14 | super.init(owner: owner) 15 | } 16 | 17 | public override var value: Int { 18 | return 99999 19 | } 20 | 21 | override func threatenedPositions(from position: Position, in game: Game) -> BooleanChessGrid { 22 | return standardMoveGrid(from: position, in: game) 23 | } 24 | 25 | override func possibleMoves(from position: Position, in game: Game) -> Set { 26 | let standardMoves = standardMoveGrid(from: position, in: game) 27 | .toMoves(from: position, in: game.board) 28 | 29 | let castleMoves: Set = { 30 | 31 | let startingPosition = owner == .white ? Position(.e, .one) : Position(.e, .eight) 32 | let movesFromStartingPosition = game.history 33 | .filter {$0.origin == startingPosition} 34 | 35 | guard 36 | position == startingPosition, 37 | movesFromStartingPosition.count == 0 38 | else {return Set()} 39 | 40 | let directedPosition = DirectedPosition(position: position, perspective: owner) 41 | 42 | let queensideCastle: Move? = { 43 | 44 | let rookPosition = owner == .white ? Position(.a, .one) : Position(.a, .eight) 45 | let movesFromRookPosition = game.history 46 | .filter {$0.origin == rookPosition} 47 | 48 | guard 49 | let _ = game.board[rookPosition] as? Rook, 50 | movesFromRookPosition.count == 0 51 | else {return nil} 52 | 53 | let spacesThatAreRequiredToBeEmpty = 54 | (owner == .white ? directedPosition.leftSpaces : directedPosition.rightSpaces) 55 | .dropLast() 56 | .map {$0.position} 57 | 58 | for space in spacesThatAreRequiredToBeEmpty { 59 | guard game.board[space] == nil else {return nil} 60 | } 61 | 62 | let spacesThatAreRequiredToBeUnthreatened = spacesThatAreRequiredToBeEmpty 63 | .dropLast() 64 | 65 | let positionsThreatenedByOpponent = game 66 | .positionsThreatened(by: owner.opponent) 67 | 68 | for space in spacesThatAreRequiredToBeUnthreatened { 69 | guard !positionsThreatenedByOpponent[space] else {return nil} 70 | } 71 | 72 | let destination: Position! = 73 | (owner == .white ? 74 | directedPosition 75 | .left? 76 | .left : 77 | directedPosition 78 | .right? 79 | .right 80 | )? 81 | .position 82 | 83 | guard 84 | !positionsThreatenedByOpponent[position], 85 | !positionsThreatenedByOpponent[destination] 86 | else {return nil} 87 | 88 | return Move(origin: position, destination: destination, capturedPiece: nil, kind: .castle) 89 | }() 90 | 91 | let kingsideCastle: Move? = { 92 | 93 | let rookPosition = owner == .white ? Position(.h, .one) : Position(.h, .eight) 94 | let movesFromRookPosition = game.history 95 | .filter {$0.origin == rookPosition} 96 | 97 | guard 98 | let _ = game.board[rookPosition] as? Rook, 99 | movesFromRookPosition.count == 0 100 | else {return nil} 101 | 102 | let spacesThatAreRequiredToBeEmptyAndUnthreatened = 103 | (owner == .white ? directedPosition.rightSpaces : directedPosition.leftSpaces) 104 | .dropLast() 105 | .map {$0.position} 106 | 107 | for space in spacesThatAreRequiredToBeEmptyAndUnthreatened { 108 | guard game.board[space] == nil else {return nil} 109 | } 110 | 111 | let positionsThreatenedByOpponent = game 112 | .positionsThreatened(by: owner.opponent) 113 | 114 | for space in spacesThatAreRequiredToBeEmptyAndUnthreatened { 115 | guard !positionsThreatenedByOpponent[space] else {return nil} 116 | } 117 | 118 | let destination: Position! = 119 | (owner == .white ? 120 | directedPosition 121 | .right? 122 | .right : 123 | directedPosition 124 | .left? 125 | .left 126 | )? 127 | .position 128 | 129 | guard 130 | !positionsThreatenedByOpponent[position], 131 | !positionsThreatenedByOpponent[destination] 132 | else {return nil} 133 | 134 | return Move(origin: position, destination: destination, capturedPiece: nil, kind: .castle) 135 | }() 136 | 137 | return [queensideCastle, kingsideCastle] 138 | .compactMap {$0} 139 | .toSet() 140 | }() 141 | 142 | return standardMoves.union(castleMoves) 143 | } 144 | 145 | private func standardMoveGrid(from position: Position, in game: Game) -> BooleanChessGrid { 146 | 147 | let directedPosition = DirectedPosition(position: position, perspective: owner) 148 | 149 | let frontMove = Position 150 | .pathConsideringCollisions(for: owner, 151 | traversing: [directedPosition.front?.position].compactMap({$0}), 152 | in: game.board) 153 | 154 | let backMove = Position 155 | .pathConsideringCollisions(for: owner, 156 | traversing: [directedPosition.back?.position].compactMap({$0}), 157 | in: game.board) 158 | 159 | let leftMove = Position 160 | .pathConsideringCollisions(for: owner, 161 | traversing: [directedPosition.left?.position].compactMap({$0}), 162 | in: game.board) 163 | 164 | let rightMove = Position 165 | .pathConsideringCollisions(for: owner, 166 | traversing: [directedPosition.right?.position].compactMap({$0}), 167 | in: game.board) 168 | 169 | let frontLeftDiagonal = Position 170 | .pathConsideringCollisions(for: owner, 171 | traversing: [directedPosition.diagonalLeftFront?.position].compactMap({$0}), 172 | in: game.board) 173 | 174 | let frontRightDiagonal = Position 175 | .pathConsideringCollisions(for: owner, 176 | traversing: [directedPosition.diagonalRightFront?.position].compactMap({$0}), 177 | in: game.board) 178 | 179 | let backLeftDiagonal = Position 180 | .pathConsideringCollisions(for: owner, 181 | traversing: [directedPosition.diagonalLeftBack?.position].compactMap({$0}), 182 | in: game.board) 183 | 184 | let backRightDiagonal = Position 185 | .pathConsideringCollisions(for: owner, 186 | traversing: [directedPosition.diagonalRightBack?.position].compactMap({$0}), 187 | in: game.board) 188 | 189 | let allMoves = frontMove + backMove + leftMove + rightMove + 190 | frontLeftDiagonal + frontRightDiagonal + backLeftDiagonal + backRightDiagonal 191 | 192 | return BooleanChessGrid(positions: allMoves) 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /SudoChess/Pieces/Knight.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Knight.swift 3 | // SudoChess 4 | // 5 | // Created by Daniel Panzer on 9/17/19. 6 | // Copyright © 2019 Daniel Panzer. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public class Knight : Piece { 12 | 13 | public required init(owner: Team) { 14 | super.init(owner: owner) 15 | } 16 | 17 | public override var value: Int { 18 | return 3 19 | } 20 | 21 | override func possibleMoves(from position: Position, in game: Game) -> Set { 22 | return threatenedPositions(from: position, in: game) 23 | .toMoves(from: position, in: game.board) 24 | } 25 | 26 | override func threatenedPositions(from position: Position, in game: Game) -> BooleanChessGrid { 27 | 28 | let directedPosition = DirectedPosition(position: position, perspective: owner) 29 | 30 | let upperLeftA = 31 | directedPosition 32 | .left? 33 | .front? 34 | .front 35 | 36 | let upperLeftB = 37 | directedPosition 38 | .left? 39 | .left? 40 | .front 41 | 42 | let upperRightA = 43 | directedPosition 44 | .right? 45 | .front? 46 | .front 47 | 48 | let upperRightB = 49 | directedPosition 50 | .right? 51 | .right? 52 | .front 53 | 54 | let lowerLeftA = 55 | directedPosition 56 | .left? 57 | .back? 58 | .back 59 | 60 | let lowerLeftB = 61 | directedPosition 62 | .left? 63 | .left? 64 | .back 65 | 66 | let lowerRightA = 67 | directedPosition 68 | .right? 69 | .back? 70 | .back 71 | 72 | let lowerRightB = 73 | directedPosition 74 | .right? 75 | .right? 76 | .back 77 | 78 | let allMoves = [upperLeftA, upperLeftB, upperRightA, upperRightB, 79 | lowerLeftA, lowerLeftB, lowerRightA, lowerRightB] 80 | .compactMap { $0?.position } 81 | .filter { switch game.board[$0] { 82 | case .some(let collidingPiece): 83 | return collidingPiece.owner != self.owner 84 | case .none: 85 | return true 86 | } 87 | } 88 | 89 | return BooleanChessGrid(positions: allMoves) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /SudoChess/Pieces/Pawn.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Pawn.swift 3 | // SudoChess 4 | // 5 | // Created by Daniel Panzer on 9/17/19. 6 | // Copyright © 2019 Daniel Panzer. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public class Pawn : Piece { 12 | 13 | private static let doubleMoveAllowedPositionsForWhitePawns = DirectedPosition(position: Position(.a, .two), perspective: .white) 14 | .rightSpaces 15 | .map({$0.position}) 16 | + [Position(.a, .two)] 17 | 18 | private static let doubleMoveAllowedPositionsForBlackPawns = DirectedPosition(position: Position(.a, .seven), perspective: .black) 19 | .leftSpaces 20 | .map({$0.position}) 21 | + [Position(.a, .seven)] 22 | 23 | private static let promotionRequiredPositionsForWhitePawns = DirectedPosition(position: Position(.a, .eight), perspective: .white) 24 | .rightSpaces 25 | .map({$0.position}) 26 | + [Position(.a, .eight)] 27 | 28 | private static let promotionRequiredPositionsForBlackPawns = DirectedPosition(position: Position(.a, .one), perspective: .black) 29 | .leftSpaces 30 | .map({$0.position}) 31 | + [Position(.a, .one)] 32 | 33 | public required init(owner: Team) { 34 | super.init(owner: owner) 35 | } 36 | 37 | public override var value: Int { 38 | return 1 39 | } 40 | 41 | override func possibleMoves(from position: Position, in game: Game) -> Set { 42 | 43 | let directedPosition = position.fromPerspective(of: owner) 44 | 45 | let frontMove: Position? = { 46 | guard let space = directedPosition.front?.position else {return nil} 47 | let collidingPiece = game.board[space] 48 | 49 | switch collidingPiece { 50 | case .none: 51 | return space 52 | case .some(_): 53 | return nil 54 | } 55 | }() 56 | 57 | let frontLeftMove: Position? = { 58 | guard let space = directedPosition.diagonalLeftFront?.position else {return nil} 59 | 60 | switch game.board[space] { 61 | case .none: 62 | return nil 63 | case .some(let collidingPiece): 64 | if collidingPiece.owner == self.owner { 65 | return nil 66 | } else { 67 | return space 68 | } 69 | } 70 | }() 71 | 72 | let frontRightMove: Position? = { 73 | guard let space = directedPosition.diagonalRightFront?.position else {return nil} 74 | 75 | switch game.board[space] { 76 | case .none: 77 | return nil 78 | case .some(let collidingPiece): 79 | if collidingPiece.owner == self.owner { 80 | return nil 81 | } else { 82 | return space 83 | } 84 | } 85 | }() 86 | 87 | let allMovePositions = [frontMove, frontLeftMove, frontRightMove].compactMap({$0}) 88 | let normalMoves: [Move] = allMovePositions.map { destination in 89 | if (owner == .white ? Pawn.promotionRequiredPositionsForWhitePawns : Pawn.promotionRequiredPositionsForBlackPawns).contains(destination) { 90 | return Move(origin: position, destination: destination, capturedPiece: game.board[destination], kind: .needsPromotion) 91 | } else { 92 | return Move(origin: position, destination: destination, capturedPiece: game.board[destination]) 93 | } 94 | } 95 | 96 | let doubleMove = { () -> Move? in 97 | 98 | switch owner { 99 | case .white: 100 | guard Pawn.doubleMoveAllowedPositionsForWhitePawns.contains(position) else {return nil} 101 | case .black: 102 | guard Pawn.doubleMoveAllowedPositionsForBlackPawns.contains(position) else {return nil} 103 | } 104 | 105 | guard let front = directedPosition.front?.position, game.board[front] == nil else {return nil} 106 | 107 | guard 108 | let doubleMoveDestination = directedPosition 109 | .front? 110 | .front? 111 | .position, 112 | game.board[doubleMoveDestination] == nil else { 113 | return nil 114 | } 115 | 116 | return Move(origin: position, 117 | destination: doubleMoveDestination, 118 | capturedPiece: game.board[doubleMoveDestination]) 119 | 120 | }() 121 | 122 | let enPassantMove = { () -> Move? in 123 | 124 | guard 125 | let lastMove = game.history.last, 126 | let piece = game.board[lastMove.destination], 127 | let pawn = piece as? Pawn 128 | else {return nil} 129 | 130 | assert(pawn.owner == game.turn.opponent) 131 | 132 | switch game.turn { 133 | case .white: 134 | 135 | guard 136 | Pawn.doubleMoveAllowedPositionsForBlackPawns.contains(lastMove.origin), 137 | abs(lastMove.origin.row.rawValue - lastMove.destination.row.rawValue) == 2 138 | else {return nil} 139 | 140 | case .black: 141 | 142 | guard 143 | Pawn.doubleMoveAllowedPositionsForWhitePawns.contains(lastMove.origin), 144 | abs(lastMove.origin.row.rawValue - lastMove.destination.row.rawValue) == 2 145 | else {return nil} 146 | } 147 | 148 | guard lastMove.destination != directedPosition.left?.position else { 149 | return Move(origin: position, 150 | destination: directedPosition.diagonalLeftFront!.position, 151 | capturedPiece: game.board[lastMove.destination], 152 | kind: .enPassant) 153 | } 154 | 155 | guard lastMove.destination != directedPosition.right?.position else { 156 | return Move(origin: position, 157 | destination: directedPosition.diagonalRightFront!.position, 158 | capturedPiece: game.board[lastMove.destination], 159 | kind: .enPassant) 160 | } 161 | 162 | return nil 163 | }() 164 | 165 | return Set(normalMoves + [doubleMove, enPassantMove].compactMap{$0}) 166 | } 167 | 168 | override func threatenedPositions(from position: Position, in game: Game) -> BooleanChessGrid { 169 | 170 | let directedPosition = position.fromPerspective(of: owner) 171 | 172 | let frontLeftThreat: Position? = { 173 | guard let space = directedPosition.diagonalLeftFront?.position else {return nil} 174 | 175 | switch game.board[space] { 176 | case .none: 177 | return space 178 | case .some(let collidingPiece): 179 | if collidingPiece.owner == self.owner { 180 | return nil 181 | } else { 182 | return space 183 | } 184 | } 185 | }() 186 | 187 | let frontRightThreat: Position? = { 188 | guard let space = directedPosition.diagonalRightFront?.position else {return nil} 189 | 190 | switch game.board[space] { 191 | case .none: 192 | return space 193 | case .some(let collidingPiece): 194 | if collidingPiece.owner == self.owner { 195 | return nil 196 | } else { 197 | return space 198 | } 199 | } 200 | }() 201 | 202 | return BooleanChessGrid(positions: [frontLeftThreat, frontRightThreat].compactMap{$0}) 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /SudoChess/Pieces/Piece.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Piece.swift 3 | // SudoChess 4 | // 5 | // Created by Daniel Panzer on 9/2/19. 6 | // Copyright © 2019 Daniel Panzer. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | public class Piece { 12 | 13 | required init(owner: Team) { 14 | self.owner = owner 15 | } 16 | 17 | public let owner: Team 18 | 19 | public var value: Int { 20 | return 0 21 | } 22 | 23 | var image: Image { 24 | 25 | let color = owner.description 26 | let piece = String(describing: type(of: self)).lowercased() 27 | return Image("\(piece)_\(color)", bundle: Bundle(for: type(of: self))) 28 | 29 | } 30 | 31 | func possibleMoves(from position: Position, in game: Game) -> Set { 32 | fatalError("must be overridden by subclass") 33 | } 34 | 35 | func threatenedPositions(from position: Position, in game: Game) -> BooleanChessGrid { 36 | fatalError("must be overridden by subclass") 37 | } 38 | } 39 | 40 | extension Piece : Equatable { 41 | 42 | public static func == (lhs: Piece, rhs: Piece) -> Bool { 43 | guard 44 | lhs.owner == rhs.owner && 45 | type(of: lhs) == type(of: rhs) 46 | else {return false} 47 | 48 | return true 49 | } 50 | } 51 | 52 | extension Piece : Hashable { 53 | 54 | public func hash(into hasher: inout Hasher) { 55 | hasher.combine(owner) 56 | hasher.combine(String(describing: type(of: self))) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /SudoChess/Pieces/Queen.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Queen.swift 3 | // SudoChess 4 | // 5 | // Created by Daniel Panzer on 9/17/19. 6 | // Copyright © 2019 Daniel Panzer. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public class Queen : Piece { 12 | 13 | public required init(owner: Team) { 14 | super.init(owner: owner) 15 | } 16 | 17 | public override var value: Int { 18 | return 9 19 | } 20 | 21 | override func possibleMoves(from position: Position, in game: Game) -> Set { 22 | return threatenedPositions(from: position, in: game) 23 | .toMoves(from: position, in: game.board) 24 | } 25 | 26 | override func threatenedPositions(from position: Position, in game: Game) -> BooleanChessGrid { 27 | 28 | let directedPosition = DirectedPosition(position: position, perspective: owner) 29 | 30 | let frontMoves = Position 31 | .pathConsideringCollisions(for: owner, 32 | traversing: directedPosition.frontSpaces.map({$0.position}), 33 | in: game.board) 34 | 35 | let backMoves = Position 36 | .pathConsideringCollisions(for: owner, 37 | traversing: directedPosition.backSpaces.map({$0.position}), 38 | in: game.board) 39 | 40 | let leftMoves = Position 41 | .pathConsideringCollisions(for: owner, 42 | traversing: directedPosition.leftSpaces.map({$0.position}), 43 | in: game.board) 44 | 45 | let rightMoves = Position 46 | .pathConsideringCollisions(for: owner, 47 | traversing: directedPosition.rightSpaces.map({$0.position}), 48 | in: game.board) 49 | 50 | let frontLeftDiagonal = Position 51 | .pathConsideringCollisions(for: owner, 52 | traversing: directedPosition.diagonalLeftFrontSpaces.map({$0.position}), 53 | in: game.board) 54 | 55 | let frontRightDiagonal = Position 56 | .pathConsideringCollisions(for: owner, 57 | traversing: directedPosition.diagonalRightFrontSpaces.map({$0.position}), 58 | in: game.board) 59 | 60 | let backLeftDiagonal = Position 61 | .pathConsideringCollisions(for: owner, 62 | traversing: directedPosition.diagonalLeftBackSpaces.map({$0.position}), 63 | in: game.board) 64 | 65 | let backRightDiagonal = Position 66 | .pathConsideringCollisions(for: owner, 67 | traversing: directedPosition.diagonalRightBackSpaces.map({$0.position}), 68 | in: game.board) 69 | 70 | let allMoves = frontMoves + backMoves + leftMoves + rightMoves + 71 | frontLeftDiagonal + frontRightDiagonal + backLeftDiagonal + backRightDiagonal 72 | 73 | return BooleanChessGrid(positions: allMoves) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /SudoChess/Pieces/Rook.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Rook.swift 3 | // SudoChess 4 | // 5 | // Created by Daniel Panzer on 9/11/19. 6 | // Copyright © 2019 Daniel Panzer. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public class Rook : Piece { 12 | 13 | public required init(owner: Team) { 14 | super.init(owner: owner) 15 | } 16 | 17 | public override var value: Int { 18 | return 5 19 | } 20 | 21 | override func possibleMoves(from position: Position, in game: Game) -> Set { 22 | return threatenedPositions(from: position, in: game) 23 | .toMoves(from: position, in: game.board) 24 | } 25 | 26 | override func threatenedPositions(from position: Position, in game: Game) -> BooleanChessGrid { 27 | 28 | let directedPosition = DirectedPosition(position: position, perspective: owner) 29 | 30 | let frontMoves = Position 31 | .pathConsideringCollisions(for: owner, 32 | traversing: directedPosition.frontSpaces.map({$0.position}), 33 | in: game.board) 34 | 35 | let backMoves = Position 36 | .pathConsideringCollisions(for: owner, 37 | traversing: directedPosition.backSpaces.map({$0.position}), 38 | in: game.board) 39 | 40 | let leftMoves = Position 41 | .pathConsideringCollisions(for: owner, 42 | traversing: directedPosition.leftSpaces.map({$0.position}), 43 | in: game.board) 44 | 45 | let rightMoves = Position 46 | .pathConsideringCollisions(for: owner, 47 | traversing: directedPosition.rightSpaces.map({$0.position}), 48 | in: game.board) 49 | 50 | let allMoves = frontMoves + backMoves + leftMoves + rightMoves 51 | return BooleanChessGrid(positions: allMoves) 52 | } 53 | } 54 | 55 | -------------------------------------------------------------------------------- /SudoChess/SudoChess.h: -------------------------------------------------------------------------------- 1 | // 2 | // SudoChess.h 3 | // SudoChess 4 | // 5 | // Created by Daniel Panzer on 9/2/19. 6 | // Copyright © 2019 Daniel Panzer. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for SudoChess. 12 | FOUNDATION_EXPORT double SudoChessVersionNumber; 13 | 14 | //! Project version string for SudoChess. 15 | FOUNDATION_EXPORT const unsigned char SudoChessVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | 19 | 20 | -------------------------------------------------------------------------------- /SudoChess/Views/BoardView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BoardView.swift 3 | // SudoChess 4 | // 5 | // Created by Daniel Panzer on 10/2/19. 6 | // Copyright © 2019 Daniel Panzer. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | /// View encapsulating a fully functional chessboard 12 | public struct BoardView: View { 13 | 14 | public init(viewModel: GameViewModel) { 15 | self.viewModel = viewModel 16 | } 17 | 18 | @ObservedObject private var viewModel: GameViewModel 19 | @State private var isShowingPromotionSheet: Bool = false 20 | 21 | private func position(_ row: Int, _ column: Int) -> Position { 22 | return Position(gridIndex: IndexPath(row: row, column: column))! 23 | } 24 | 25 | private static let spacing: CGFloat = { 26 | return UIDevice.current.userInterfaceIdiom == .phone ? 4 : 8 27 | }() 28 | 29 | public var body: some View { 30 | ZStack { 31 | 32 | VStack(spacing: BoardView.spacing) { 33 | ForEach(0..<8) { row in 34 | HStack(spacing: BoardView.spacing) { 35 | ForEach(0..<8) { column in 36 | TileView(viewModel: self.viewModel, 37 | position: self.position(row, column)) 38 | } 39 | } 40 | } 41 | } 42 | } 43 | .padding(BoardView.spacing) 44 | .border(Color.black, width: 1) 45 | .aspectRatio(CGSize(width: 1, height: 1), contentMode: .fit) 46 | 47 | .onReceive(viewModel.shouldPromptForPromotion) { moveNeedingPromotion in 48 | self.isShowingPromotionSheet = true 49 | } 50 | 51 | .actionSheet(isPresented: $isShowingPromotionSheet, content: { 52 | ActionSheet(title: Text("Promotion"), 53 | message: Text("Please select a piece for promotion"), 54 | buttons: 55 | 56 | [Queen.self, Rook.self, Bishop.self, Knight.self].map { promotionType in 57 | let title = String(describing: promotionType) 58 | return ActionSheet.Button.default(Text(title)) { 59 | self.viewModel.handlePromotion(with: promotionType) 60 | self.isShowingPromotionSheet = false 61 | } 62 | } 63 | ) 64 | }) 65 | } 66 | } 67 | 68 | private struct BoardViewPreview: View { 69 | 70 | @ObservedObject var viewModel: GameViewModel = GameViewModel(game: .standard) 71 | 72 | var body: some View { 73 | BoardView(viewModel: viewModel) 74 | } 75 | } 76 | 77 | struct BoardView_Previews: PreviewProvider { 78 | static var previews: some View { 79 | BoardViewPreview() 80 | .previewLayout(.fixed(width: 600, height: 600)) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /SudoChess/Views/ConsoleView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConsoleView.swift 3 | // SudoChess 4 | // 5 | // Created by Daniel Panzer on 10/10/19. 6 | // Copyright © 2019 Daniel Panzer. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct ConsoleView: View { 12 | 13 | init(viewModel: GameViewModel) { 14 | self.viewModel = viewModel 15 | } 16 | 17 | @ObservedObject private var viewModel: GameViewModel 18 | 19 | var body: some View { 20 | VStack { 21 | HStack { 22 | // Button(action: {self.viewModel.reverseLastMove()}, 23 | // label: { 24 | // Text("REVERSE MOVE") 25 | // .fontWeight(.bold) 26 | // .padding(12) 27 | // .background(Color.blue) 28 | // .cornerRadius(20) 29 | // .foregroundColor(.white) 30 | // .padding(5) 31 | // }) 32 | // .disabled(self.viewModel.state != .awaitingHumanInput) 33 | // .opacity(self.viewModel.state == .awaitingHumanInput ? 1 : 0.5) 34 | // Spacer(minLength: 20) 35 | Text("\(self.viewModel.state.description)") 36 | .lineLimit(1) 37 | Spacer(minLength: 8) 38 | Text("Turn: \(self.viewModel.game.turn.description.capitalized)") 39 | } 40 | Spacer() 41 | } 42 | .padding(.vertical) 43 | .frame(minHeight: 100, maxHeight: 200) 44 | } 45 | } 46 | 47 | private struct ConsoleViewPreview: View { 48 | 49 | @ObservedObject var viewModel: GameViewModel = GameViewModel(game: .standard) 50 | 51 | var body: some View { 52 | ConsoleView(viewModel: viewModel) 53 | } 54 | } 55 | 56 | struct ConsoleView_Previews: PreviewProvider { 57 | static var previews: some View { 58 | ConsoleViewPreview() 59 | .previewLayout(.fixed(width: 600, height: 250)) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /SudoChess/Views/GameView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GameView.swift 3 | // SudoChess 4 | // 5 | // Created by Daniel Panzer on 10/10/19. 6 | // Copyright © 2019 Daniel Panzer. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | /// View that includes a board and basic game layout 12 | public struct GameView: View { 13 | 14 | public init(viewModel: GameViewModel) { 15 | self.viewModel = viewModel 16 | } 17 | 18 | @ObservedObject private var viewModel: GameViewModel 19 | 20 | public var body: some View { 21 | VStack { 22 | Text("\(viewModel.roster.blackPlayer.description) (black)") 23 | BoardView(viewModel: viewModel) 24 | .layoutPriority(11) 25 | Text("\(viewModel.roster.whitePlayer.description) (white)") 26 | //.rotationEffect(.degrees(180)) 27 | HStack(alignment: .center) { 28 | ConsoleView(viewModel: self.viewModel) 29 | } 30 | } 31 | .padding() 32 | } 33 | } 34 | 35 | private struct GameViewPreview: View { 36 | 37 | @ObservedObject var viewModel = GameViewModel(game: Game.standard, 38 | checkHandler: CheckHandler()) 39 | 40 | var body: some View { 41 | GameView(viewModel: viewModel) 42 | } 43 | } 44 | 45 | struct GameView_Previews: PreviewProvider { 46 | 47 | static var previews: some View { 48 | Group { 49 | 50 | // GameViewPreview() 51 | // .previewDevice(PreviewDevice(rawValue: "iPhone SE")) 52 | // .previewDisplayName("iPhone SE") 53 | 54 | GameViewPreview() 55 | .previewDevice(PreviewDevice(rawValue: "iPhone 11")) 56 | .previewDisplayName("iPhone 11") 57 | 58 | // GameViewPreview() 59 | // .previewDevice(PreviewDevice(rawValue: "iPad Pro (11-inch)")) 60 | // .previewDisplayName("iPad Pro (11-inch)") 61 | } 62 | 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /SudoChess/Views/HistoryView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HistoryView.swift 3 | // SudoChess 4 | // 5 | // Created by Daniel Panzer on 10/10/19. 6 | // Copyright © 2019 Daniel Panzer. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct HistoryView: View { 12 | 13 | @EnvironmentObject private var viewModel: GameViewModel 14 | 15 | var body: some View { 16 | List { 17 | ForEach(self.viewModel.game.history, id: \.self) { move in 18 | Text(move.description) 19 | } 20 | } 21 | .frame(minWidth: 50, idealWidth: 200, maxWidth: 200, 22 | minHeight: 200, idealHeight: 300, maxHeight: 300) 23 | } 24 | } 25 | 26 | private struct HistoryViewPreview: View { 27 | 28 | @State var viewModel: GameViewModel = GameViewModel(game: .standard) 29 | 30 | var body: some View { 31 | HistoryView() 32 | .environmentObject(viewModel) 33 | } 34 | } 35 | 36 | struct HistoryView_Previews: PreviewProvider { 37 | static var previews: some View { 38 | HistoryViewPreview() 39 | .previewLayout(.fixed(width: 200, height: 600)) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /SudoChess/Views/PieceView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PieceView.swift 3 | // SudoChess 4 | // 5 | // Created by Daniel Panzer on 10/14/19. 6 | // Copyright © 2019 Daniel Panzer. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct PieceView: View { 12 | 13 | let piece: Piece? 14 | 15 | var body: some View { 16 | ZStack { 17 | self.piece?.image 18 | .resizable() 19 | .scaledToFit() 20 | } 21 | //.rotationEffect(piece?.owner == .black ? .degrees(180) : .degrees(0)) 22 | } 23 | } 24 | 25 | struct PieceView_Previews: PreviewProvider { 26 | static var previews: some View { 27 | Group { 28 | 29 | PieceView(piece: Pawn(owner: .black)) 30 | .previewLayout(.fixed(width: 200, height: 200)) 31 | 32 | PieceView(piece: Pawn(owner: .white)) 33 | .previewLayout(.fixed(width: 200, height: 200)) 34 | } 35 | 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /SudoChess/Views/TileView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PieceCell.swift 3 | // SudoChess 4 | // 5 | // Created by Daniel Panzer on 10/9/19. 6 | // Copyright © 2019 Daniel Panzer. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct TileView: View { 12 | 13 | private let shouldShowPosition = false 14 | 15 | init(viewModel: GameViewModel, position: Position) { 16 | self.viewModel = viewModel 17 | self.position = position 18 | } 19 | 20 | @ObservedObject private var viewModel: GameViewModel 21 | let position: Position 22 | 23 | private var isCurrentlyHighlighted: Bool { 24 | return viewModel.selection?.grid[position] ?? false 25 | } 26 | 27 | private var highlightColor: Color { 28 | 29 | guard viewModel.selection?.origin != self.position else { 30 | return Color.green 31 | } 32 | 33 | guard !isCurrentlyHighlighted else { 34 | if let piece = viewModel.game.board[self.position], piece.owner != viewModel.game.turn { 35 | return Color.red 36 | } else { 37 | return Color.blue 38 | } 39 | } 40 | 41 | return Color.clear 42 | } 43 | 44 | var body: some View { 45 | 46 | Button(action: { 47 | guard self.viewModel.state == .awaitingHumanInput else {return} 48 | self.viewModel.select(self.position) 49 | }, label: { 50 | ZStack { 51 | Board.colors[position] 52 | self.highlightColor.opacity(0.5) 53 | PieceView(piece: self.viewModel.game.board[position]) 54 | 55 | VStack { 56 | Spacer() 57 | Text(shouldShowPosition ? self.position.description : "") 58 | .font(Font.caption) 59 | .bold() 60 | .foregroundColor(.black) 61 | } 62 | } 63 | .border(Color.black, width: 1) 64 | }) 65 | } 66 | } 67 | 68 | private struct PieceCellPreview : View { 69 | 70 | @ObservedObject private var viewModelNoHighlighting = GameViewModel(game: .standard) 71 | 72 | @ObservedObject private var viewModelYesHighlighting = GameViewModel( 73 | game: .standard, 74 | selection: GameViewModel.Selection( 75 | moves: Set(arrayLiteral: Move( 76 | origin: Position(.b, .one), 77 | destination: Position(.a, .one), 78 | capturedPiece: nil) 79 | ), origin: Position(.a, .one) 80 | ) 81 | ) 82 | 83 | let isHighlighted: Bool 84 | let position: Position 85 | 86 | var body: some View { 87 | TileView(viewModel: isHighlighted ? viewModelYesHighlighting : viewModelNoHighlighting, 88 | position: position) 89 | } 90 | } 91 | 92 | struct PieceCell_Previews: PreviewProvider { 93 | 94 | static var previews: some View { 95 | return Group { 96 | 97 | PieceCellPreview(isHighlighted: false, 98 | position: Position(.b, .two)) 99 | .previewLayout(.fixed(width: 200, height: 200)) 100 | // 101 | // PieceCellPreview(isHighlighted: false, 102 | // position: Position(.b, .two)) 103 | // .previewLayout(.fixed(width: 200, height: 200)) 104 | // 105 | // PieceCellPreview(isHighlighted: false, 106 | // position: Position(.g, .six)) 107 | // .previewLayout(.fixed(width: 200, height: 200)) 108 | // 109 | // PieceCellPreview(isHighlighted: false, 110 | // position: Position(.g, .six)) 111 | // .previewLayout(.fixed(width: 200, height: 200)) 112 | // 113 | // PieceCellPreview(isHighlighted: true, 114 | // position: Position(.b, .two)) 115 | // .previewLayout(.fixed(width: 200, height: 200)) 116 | // 117 | // PieceCellPreview(isHighlighted: true, 118 | // position: Position(.b, .two)) 119 | // .previewLayout(.fixed(width: 200, height: 200)) 120 | // 121 | // PieceCellPreview(isHighlighted: true, 122 | // position: Position(.g, .six)) 123 | // .previewLayout(.fixed(width: 200, height: 200)) 124 | // 125 | // PieceCellPreview(isHighlighted: true, 126 | // position: Position(.g, .six)) 127 | // .previewLayout(.fixed(width: 200, height: 200)) 128 | 129 | } 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /SudoChessTests/BishopSpec.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BishopSpec.swift 3 | // SudoChessFoundationTests 4 | // 5 | // Created by Daniel Panzer on 9/11/19. 6 | // Copyright © 2019 Daniel Panzer. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import Quick 11 | import Nimble 12 | 13 | @testable import SudoChess 14 | 15 | class BishopSpec: QuickSpec { 16 | 17 | private var subject: Bishop! 18 | private var grid: BooleanChessGrid! 19 | 20 | override func spec() { 21 | 22 | describe("A Bishop owned by white") { 23 | 24 | beforeEach { 25 | self.subject = Bishop(owner: .white) 26 | } 27 | 28 | afterEach { 29 | self.subject = nil 30 | } 31 | 32 | let expectedA1Positions = ChessGrid(array: [ 33 | X, X, X, X, X, X, X, O, 34 | X, X, X, X, X, X, O, X, 35 | X, X, X, X, X, O, X, X, 36 | X, X, X, X, O, X, X, X, 37 | X, X, X, O, X, X, X, X, 38 | X, X, O, X, X, X, X, X, 39 | X, O, X, X, X, X, X, X, 40 | X, X, X, X, X, X, X, X, 41 | ]) 42 | 43 | let expectedD5Positions = ChessGrid(array: [ 44 | O, X, X, X, X, X, O, X, 45 | X, O, X, X, X, O, X, X, 46 | X, X, O, X, O, X, X, X, 47 | X, X, X, X, X, X, X, X, 48 | X, X, O, X, O, X, X, X, 49 | X, O, X, X, X, O, X, X, 50 | O, X, X, X, X, X, O, X, 51 | X, X, X, X, X, X, X, O, 52 | ]) 53 | 54 | let expectedF8Positions = ChessGrid(array: [ 55 | X, X, X, X, X, X, X, X, 56 | X, X, X, X, O, X, O, X, 57 | X, X, X, O, X, X, X, O, 58 | X, X, O, X, X, X, X, X, 59 | X, O, X, X, X, X, X, X, 60 | O, X, X, X, X, X, X, X, 61 | X, X, X, X, X, X, X, X, 62 | X, X, X, X, X, X, X, X, 63 | ]) 64 | 65 | context("when considering moves from a1 on an empty board") { 66 | 67 | beforeEach { 68 | let emptyBoard = Board() 69 | let moves = self.subject.possibleMoves(from: Position(.a, .one), in: Game(board: emptyBoard)) 70 | self.grid = BooleanChessGrid(positions: moves.map{$0.destination}) 71 | } 72 | 73 | afterEach { 74 | self.grid = nil 75 | } 76 | 77 | it("generates the expected grid of moves") { 78 | expect(self.grid) == expectedA1Positions 79 | } 80 | } 81 | 82 | context("when considering threatened positions from a1 on an empty board") { 83 | 84 | beforeEach { 85 | let emptyBoard = Board() 86 | self.grid = self.subject.threatenedPositions(from: Position(.a, .one), in: Game(board: emptyBoard)) 87 | } 88 | 89 | afterEach { 90 | self.grid = nil 91 | } 92 | 93 | it("generates the expected threat grid") { 94 | expect(self.grid) == expectedA1Positions 95 | } 96 | } 97 | 98 | context("when considering moves from d5 on an empty board") { 99 | 100 | beforeEach { 101 | let emptyBoard = Board() 102 | let moves = self.subject.possibleMoves(from: Position(.d, .five), in: Game(board: emptyBoard)) 103 | self.grid = BooleanChessGrid(positions: moves.map{$0.destination}) 104 | } 105 | 106 | afterEach { 107 | self.grid = nil 108 | } 109 | 110 | it("generates the expected grid of moves") { 111 | expect(self.grid) == expectedD5Positions 112 | } 113 | } 114 | 115 | context("when considering threatened positions from d5 on an empty board") { 116 | 117 | beforeEach { 118 | let emptyBoard = Board() 119 | self.grid = self.subject.threatenedPositions(from: Position(.d, .five), in: Game(board: emptyBoard)) 120 | } 121 | 122 | afterEach { 123 | self.grid = nil 124 | } 125 | 126 | it("generates the expected threat grid") { 127 | expect(self.grid) == expectedD5Positions 128 | } 129 | } 130 | 131 | context("when considering moves from f8 on an empty board") { 132 | 133 | beforeEach { 134 | let emptyBoard = Board() 135 | let moves = self.subject.possibleMoves(from: Position(.f, .eight), in: Game(board: emptyBoard)) 136 | self.grid = BooleanChessGrid(positions: moves.map{$0.destination}) 137 | } 138 | 139 | afterEach { 140 | self.grid = nil 141 | } 142 | 143 | it("generates the expected grid of moves") { 144 | expect(self.grid) == expectedF8Positions 145 | } 146 | } 147 | 148 | context("when considering threatened positions from f8 on an empty board") { 149 | 150 | beforeEach { 151 | let emptyBoard = Board() 152 | self.grid = self.subject.threatenedPositions(from: Position(.f, .eight), in: Game(board: emptyBoard)) 153 | } 154 | 155 | afterEach { 156 | self.grid = nil 157 | } 158 | 159 | it("generates the expected threat grid") { 160 | expect(self.grid) == expectedF8Positions 161 | } 162 | } 163 | } 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /SudoChessTests/BooleanChessGridSpec.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BooleanChessGridSpec.swift 3 | // SudoChessFoundationTests 4 | // 5 | // Created by Daniel Panzer on 10/14/19. 6 | // Copyright © 2019 Daniel Panzer. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import Quick 11 | import Nimble 12 | 13 | @testable import SudoChess 14 | 15 | class BooleanChessGridSpec: QuickSpec { 16 | 17 | private var subject: BooleanChessGrid! 18 | 19 | override func spec() { 20 | 21 | describe("A boolean chess grid created from the union of two grids") { 22 | 23 | beforeEach { 24 | let grid1 = BooleanChessGrid(array: [ 25 | O, O, O, O, O, O, O, O, 26 | X, X, O, O, X, X, X, O, 27 | X, X, X, X, X, X, X, X, 28 | X, X, X, X, X, X, X, X, 29 | X, X, X, X, X, X, X, X, 30 | X, X, X, X, X, X, X, X, 31 | X, X, X, X, X, X, X, X, 32 | O, X, X, X, X, X, X, X, 33 | ]) 34 | 35 | let grid2 = BooleanChessGrid(array: [ 36 | O, O, O, X, X, X, X, X, 37 | X, X, X, X, X, X, X, O, 38 | X, X, X, X, X, X, X, X, 39 | X, X, X, X, X, X, X, X, 40 | X, X, X, X, X, X, X, X, 41 | X, X, X, X, O, O, O, X, 42 | X, X, X, X, X, X, X, X, 43 | O, X, X, X, X, X, X, X, 44 | ]) 45 | 46 | self.subject = grid1.union(grid2) 47 | } 48 | 49 | afterEach { 50 | self.subject = nil 51 | } 52 | 53 | it("contains the expected content") { 54 | let expectedGrid = BooleanChessGrid(array: [ 55 | O, O, O, O, O, O, O, O, 56 | X, X, O, O, X, X, X, O, 57 | X, X, X, X, X, X, X, X, 58 | X, X, X, X, X, X, X, X, 59 | X, X, X, X, X, X, X, X, 60 | X, X, X, X, O, O, O, X, 61 | X, X, X, X, X, X, X, X, 62 | O, X, X, X, X, X, X, X, 63 | ]) 64 | 65 | expect(self.subject) == expectedGrid 66 | } 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /SudoChessTests/CheckHandlerPerformanceSpec.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CheckHandlerPerformanceSpec.swift 3 | // SudoChessTests 4 | // 5 | // Created by Daniel Panzer on 11/15/19. 6 | // Copyright © 2019 Daniel Panzer. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | 11 | @testable import SudoChess 12 | 13 | class CheckHandlerPerformanceSpec: XCTestCase { 14 | 15 | private var subject: CheckHandler! 16 | private var summary: CheckHandler.State! 17 | private var moves: Set! 18 | 19 | override func setUp() { 20 | self.subject = CheckHandler() 21 | } 22 | 23 | override func tearDown() { 24 | self.subject = nil 25 | self.summary = nil 26 | self.moves = nil 27 | } 28 | 29 | func test_CheckHandler_validMovesForStandardGame_performance() { 30 | 31 | let game = Game 32 | .standard 33 | 34 | let allMoves = game 35 | .allMoves(for: .white) 36 | 37 | self.measure { 38 | self.moves = self.subject.validMoves(from: allMoves, in: game) 39 | } 40 | } 41 | 42 | func test_CheckHandler_checkSummaryForStandardGame_performance() { 43 | 44 | let game = Game 45 | .standard 46 | 47 | self.measure { 48 | self.summary = self.subject.state(for: .white, in: game) 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /SudoChessTests/CheckHandlerSpec.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CheckHandlerSpec.swift 3 | // SudoChessFoundationTests 4 | // 5 | // Created by Daniel Panzer on 10/14/19. 6 | // Copyright © 2019 Daniel Panzer. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import Quick 11 | import Nimble 12 | 13 | @testable import SudoChess 14 | 15 | class CheckHandlerSpec: QuickSpec { 16 | 17 | private static let blackNoValidMoveScenario: Board = { 18 | 19 | let rook1 = Rook(owner: .white) 20 | let rook2 = Rook(owner: .white) 21 | 22 | let pieces: [Piece?] = [ 23 | King(owner: .black), nil, nil, nil, nil, nil, nil, nil, 24 | nil, nil, nil, nil, nil, nil, nil, rook1, 25 | nil, rook2, nil, nil, nil, nil, nil, nil, 26 | nil, nil, nil, nil, nil, nil, nil, nil, 27 | nil, nil, nil, nil, nil, nil, nil, nil, 28 | nil, nil, nil, nil, nil, nil, nil, nil, 29 | nil, nil, nil, nil, nil, nil, nil, nil, 30 | nil, nil, nil, King(owner: .white), nil, nil, nil, nil 31 | ] 32 | 33 | return ChessGrid(array: pieces) 34 | }() 35 | 36 | private static let blackInCheckAndNoValidMoveScenario: Board = { 37 | 38 | let rook1 = Rook(owner: .white) 39 | let rook2 = Rook(owner: .white) 40 | let bishop = Bishop(owner: .white) 41 | 42 | let pieces: [Piece?] = [ 43 | King(owner: .black), nil, nil, nil, nil, nil, nil, nil, 44 | nil, nil, nil, nil, nil, nil, nil, rook1, 45 | nil, rook2, nil, nil, nil, nil, nil, nil, 46 | nil, nil, nil, bishop, nil, nil, nil, nil, 47 | nil, nil, nil, nil, nil, nil, nil, nil, 48 | nil, nil, nil, nil, nil, nil, nil, nil, 49 | nil, nil, nil, nil, nil, nil, nil, nil, 50 | nil, nil, nil, King(owner: .white), nil, nil, nil, nil 51 | ] 52 | 53 | return ChessGrid(array: pieces) 54 | }() 55 | 56 | private static let blackInCheckScenario: Board = { 57 | 58 | let king1 = King(owner: .black) 59 | let rook1 = Rook(owner: .white) 60 | let rook2 = Rook(owner: .white) 61 | let king2 = King(owner: .white) 62 | 63 | let pieces: [Piece?] = [ 64 | nil, nil, nil, nil, nil, nil, nil, nil, 65 | nil, king1, nil, nil, nil, nil, nil, rook1, 66 | nil, rook2, nil, nil, nil, nil, nil, nil, 67 | nil, nil, nil, nil, nil, nil, nil, nil, 68 | nil, nil, nil, nil, nil, nil, nil, nil, 69 | nil, nil, nil, nil, nil, nil, nil, nil, 70 | nil, nil, nil, nil, nil, nil, nil, nil, 71 | nil, nil, nil, king2, nil, nil, nil, nil 72 | ] 73 | 74 | return ChessGrid(array: pieces) 75 | }() 76 | 77 | private var subject: CheckHandler! 78 | private var scenario: Game! 79 | 80 | override func spec() { 81 | 82 | describe("A CheckHandler") { 83 | 84 | beforeEach { 85 | self.subject = CheckHandler() 86 | } 87 | 88 | afterEach { 89 | self.subject = nil 90 | } 91 | 92 | context("when presented with the standard starting scenario") { 93 | 94 | beforeEach { 95 | self.scenario = Game.standard 96 | } 97 | 98 | afterEach { 99 | self.scenario = nil 100 | } 101 | 102 | it("does not report white is in check or checkmate") { 103 | expect(self.subject.state(for: .white, in: self.scenario)) == CheckHandler.State.none 104 | } 105 | 106 | it("does not report black is in check or checkmate") { 107 | expect(self.subject.state(for: .black, in: self.scenario)) == CheckHandler.State.none 108 | } 109 | } 110 | 111 | context("when presented with a scenario where black is in check and it is black's move") { 112 | 113 | beforeEach { 114 | self.scenario = Game(board: CheckHandlerSpec.blackInCheckScenario, turn: .black) 115 | } 116 | 117 | afterEach { 118 | self.scenario = nil 119 | } 120 | 121 | it("does not report white is in check or checkmate") { 122 | expect(self.subject.state(for: .white, in: self.scenario)) == CheckHandler.State.none 123 | } 124 | 125 | it("reports black is in check") { 126 | expect(self.subject.state(for: .black, in: self.scenario)) == CheckHandler.State.check 127 | } 128 | } 129 | 130 | context("when presented with a scenario where black has no valid moves and it is black's move") { 131 | 132 | beforeEach { 133 | self.scenario = Game(board: CheckHandlerSpec.blackNoValidMoveScenario, turn: .black) 134 | } 135 | 136 | afterEach { 137 | self.scenario = nil 138 | } 139 | 140 | it("does not report white is in check or checkmate") { 141 | expect(self.subject.state(for: .white, in: self.scenario)) == CheckHandler.State.none 142 | } 143 | 144 | it("reports black is in checkmate") { 145 | expect(self.subject.state(for: .black, in: self.scenario)) == CheckHandler.State.checkmate 146 | } 147 | } 148 | 149 | context("when presented with a scenario where black has no valid moves and it is white's move") { 150 | 151 | beforeEach { 152 | self.scenario = Game(board: CheckHandlerSpec.blackNoValidMoveScenario, turn: .white) 153 | } 154 | 155 | afterEach { 156 | self.scenario = nil 157 | } 158 | 159 | it("does not report white is in check or checkmate") { 160 | expect(self.subject.state(for: .white, in: self.scenario)) == CheckHandler.State.none 161 | } 162 | 163 | it("does not report black is in check or checkmate") { 164 | expect(self.subject.state(for: .black, in: self.scenario)) == CheckHandler.State.none 165 | } 166 | } 167 | } 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /SudoChessTests/DirectedPositionSpec.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DirectedPositionSpec.swift 3 | // SudoChessFoundationTests 4 | // 5 | // Created by Daniel Panzer on 9/9/19. 6 | // Copyright © 2019 Daniel Panzer. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import Quick 11 | import Nimble 12 | 13 | @testable import SudoChess 14 | 15 | class DirectedPositionSpec: QuickSpec { 16 | 17 | private var subject: DirectedPosition! 18 | 19 | override func spec() { 20 | 21 | describe("Position a1 from white's perspective") { 22 | 23 | beforeEach { 24 | self.subject = DirectedPosition(position: Position(.a, .one), perspective: .white) 25 | } 26 | 27 | afterEach { 28 | self.subject = nil 29 | } 30 | 31 | it("reports a2 as its front") { 32 | expect(self.subject.front?.position) == Position(.a, .two) 33 | } 34 | 35 | it("reports no back") { 36 | expect(self.subject.back?.position).to(beNil()) 37 | } 38 | 39 | it("reports no left") { 40 | expect(self.subject.left?.position).to(beNil()) 41 | } 42 | 43 | it("reports b1 as its right") { 44 | expect(self.subject.right?.position) == Position(.b, .one) 45 | } 46 | 47 | it("reports no left front diagonal") { 48 | expect(self.subject.diagonalLeftFront?.position).to(beNil()) 49 | } 50 | 51 | it("reports b2 as its right front diagonal") { 52 | expect(self.subject.diagonalRightFront?.position) == Position(.b, .two) 53 | } 54 | 55 | it("reports no left back diagonal") { 56 | expect(self.subject.diagonalLeftBack?.position).to(beNil()) 57 | } 58 | 59 | it("reports no right back diagonal") { 60 | expect(self.subject.diagonalRightBack?.position).to(beNil()) 61 | } 62 | 63 | it("reports a2 - a8 as its front spaces") { 64 | let expectedResult = [ 65 | Position(.a, .two), 66 | Position(.a, .three), 67 | Position(.a, .four), 68 | Position(.a, .five), 69 | Position(.a, .six), 70 | Position(.a, .seven), 71 | Position(.a, .eight), 72 | ] 73 | expect(self.subject.frontSpaces.map({$0.position})) == expectedResult 74 | } 75 | 76 | it("reports no back spaces") { 77 | expect(self.subject.backSpaces).to(beEmpty()) 78 | } 79 | 80 | it("reports no left spaces") { 81 | expect(self.subject.leftSpaces).to(beEmpty()) 82 | } 83 | 84 | it("reports b1 - h1 as its right spaces") { 85 | let expectedResult = [ 86 | Position(.b, .one), 87 | Position(.c, .one), 88 | Position(.d, .one), 89 | Position(.e, .one), 90 | Position(.f, .one), 91 | Position(.g, .one), 92 | Position(.h, .one), 93 | ] 94 | expect(self.subject.rightSpaces.map({$0.position})) == expectedResult 95 | } 96 | } 97 | 98 | describe("Position a1 from black's perspective") { 99 | 100 | beforeEach { 101 | self.subject = DirectedPosition(position: Position(.a, .one), perspective: .black) 102 | } 103 | 104 | afterEach { 105 | self.subject = nil 106 | } 107 | 108 | it("reports no front") { 109 | expect(self.subject.front?.position).to(beNil()) 110 | } 111 | 112 | it("reports a2 as its back") { 113 | expect(self.subject.back?.position) == Position(.a, .two) 114 | } 115 | 116 | it("reports b1 as its left") { 117 | expect(self.subject.left?.position) == Position(.b, .one) 118 | } 119 | 120 | it("reports no right") { 121 | expect(self.subject.right?.position).to(beNil()) 122 | } 123 | 124 | it("reports no left front diagonal") { 125 | expect(self.subject.diagonalLeftFront?.position).to(beNil()) 126 | } 127 | 128 | it("reports no right front diagonal") { 129 | expect(self.subject.diagonalRightFront?.position).to(beNil()) 130 | } 131 | 132 | it("reports b2 as its left back diagonal") { 133 | expect(self.subject.diagonalLeftBack?.position) == Position(.b, .two) 134 | } 135 | 136 | it("reports no right back diagonal") { 137 | expect(self.subject.diagonalRightBack?.position).to(beNil()) 138 | } 139 | } 140 | 141 | describe("Position d5 from white's perspective") { 142 | 143 | beforeEach { 144 | self.subject = DirectedPosition(position: Position(.d, .five), perspective: .white) 145 | } 146 | 147 | afterEach { 148 | self.subject = nil 149 | } 150 | 151 | it("reports d6 as its front") { 152 | expect(self.subject.front?.position) == Position(.d, .six) 153 | } 154 | 155 | it("reports d4 as its back") { 156 | expect(self.subject.back?.position) == Position(.d, .four) 157 | } 158 | 159 | it("reports c5 as its left") { 160 | expect(self.subject.left?.position) == Position(.c, .five) 161 | } 162 | 163 | it("reports e5 as its right") { 164 | expect(self.subject.right?.position) == Position(.e, .five) 165 | } 166 | 167 | it("reports c6 left front diagonal") { 168 | expect(self.subject.diagonalLeftFront?.position) == Position(.c, .six) 169 | } 170 | 171 | it("reports e6 as its right front diagonal") { 172 | expect(self.subject.diagonalRightFront?.position) == Position(.e, .six) 173 | } 174 | 175 | it("reports c4 as its left back diagonal") { 176 | expect(self.subject.diagonalLeftBack?.position) == Position(.c, .four) 177 | } 178 | 179 | it("reports e4 as its right back diagonal") { 180 | expect(self.subject.diagonalRightBack?.position) == Position(.e, .four) 181 | } 182 | } 183 | 184 | describe("Position d5 from black's perspective") { 185 | 186 | beforeEach { 187 | self.subject = DirectedPosition(position: Position(.d, .five), perspective: .black) 188 | } 189 | 190 | afterEach { 191 | self.subject = nil 192 | } 193 | 194 | it("reports d4 as its front") { 195 | expect(self.subject.front?.position) == Position(.d, .four) 196 | } 197 | 198 | it("reports d6 as its back") { 199 | expect(self.subject.back?.position) == Position(.d, .six) 200 | } 201 | 202 | it("reports e5 as its left") { 203 | expect(self.subject.left?.position) == Position(.e, .five) 204 | } 205 | 206 | it("reports c5 as its right") { 207 | expect(self.subject.right?.position) == Position(.c, .five) 208 | } 209 | 210 | it("reports e4 left front diagonal") { 211 | expect(self.subject.diagonalLeftFront?.position) == Position(.e, .four) 212 | } 213 | 214 | it("reports c4 as its right front diagonal") { 215 | expect(self.subject.diagonalRightFront?.position) == Position(.c, .four) 216 | } 217 | 218 | it("reports e6 as its left back diagonal") { 219 | expect(self.subject.diagonalLeftBack?.position) == Position(.e, .six) 220 | } 221 | 222 | it("reports c6 as its right back diagonal") { 223 | expect(self.subject.diagonalRightBack?.position) == Position(.c, .six) 224 | } 225 | } 226 | 227 | describe("Position f8 from white's perspective") { 228 | 229 | beforeEach { 230 | self.subject = DirectedPosition(position: Position(.f, .eight), perspective: .white) 231 | } 232 | 233 | afterEach { 234 | self.subject = nil 235 | } 236 | 237 | it("reports no front") { 238 | expect(self.subject.front?.position).to(beNil()) 239 | } 240 | 241 | it("reports f7 as its back") { 242 | expect(self.subject.back?.position) == Position(.f, .seven) 243 | } 244 | 245 | it("reports e8 as its left") { 246 | expect(self.subject.left?.position) == Position(.e, .eight) 247 | } 248 | 249 | it("reports g8 as its right") { 250 | expect(self.subject.right?.position) == Position(.g, .eight) 251 | } 252 | 253 | it("reports no left front diagonal") { 254 | expect(self.subject.diagonalLeftFront?.position).to(beNil()) 255 | } 256 | 257 | it("reports no right front diagonal") { 258 | expect(self.subject.diagonalRightFront?.position).to(beNil()) 259 | } 260 | 261 | it("reports e7 as its left back diagonal") { 262 | expect(self.subject.diagonalLeftBack?.position) == Position(.e, .seven) 263 | } 264 | 265 | it("reports g7 as its right back diagonal") { 266 | expect(self.subject.diagonalRightBack?.position) == Position(.g, .seven) 267 | } 268 | } 269 | } 270 | } 271 | -------------------------------------------------------------------------------- /SudoChessTests/GridSpec.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GridSpec.swift 3 | // SudoChessFoundationTests 4 | // 5 | // Created by Daniel Panzer on 9/9/19. 6 | // Copyright © 2019 Daniel Panzer. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import Quick 11 | import Nimble 12 | 13 | @testable import SudoChess 14 | 15 | class GridSpec: QuickSpec { 16 | 17 | private var nullableSubject: Grid! 18 | private var nonnullSubject: Grid! 19 | 20 | override func spec() { 21 | 22 | describe("an empty 3x3 nullable integer grid") { 23 | 24 | beforeEach { 25 | self.nullableSubject = Grid(rowSize: 3, columnSize: 3) 26 | } 27 | 28 | afterEach { 29 | self.nullableSubject = nil 30 | } 31 | 32 | it("reports nil for index 0,0") { 33 | expect(self.nullableSubject[0,0]).to(beNil()) 34 | } 35 | 36 | it("reports nil for index 1,1") { 37 | expect(self.nullableSubject[1,1]).to(beNil()) 38 | } 39 | 40 | it("reports an empty array when compactMapped") { 41 | expect(self.nullableSubject.compactMap({$0}).isEmpty) == true 42 | } 43 | 44 | context("when setting index 1,1 to 5") { 45 | 46 | beforeEach { 47 | self.nullableSubject[1,1] = 5 48 | } 49 | 50 | it("continues to report nil for index 0,0") { 51 | expect(self.nullableSubject[0,0]).to(beNil()) 52 | } 53 | 54 | it("reports 5 for index 1,1") { 55 | expect(self.nullableSubject[1,1]) == 5 56 | } 57 | 58 | it("reports an array with one element 5 when compactMapped") { 59 | expect(self.nullableSubject.compactMap({$0})) == [5] 60 | } 61 | } 62 | 63 | context("when setting index 0,0 to 2 and index 0,1 to 4") { 64 | 65 | beforeEach { 66 | self.nullableSubject[0,0] = 2 67 | self.nullableSubject[0,1] = 4 68 | } 69 | 70 | it("continues to report nil for index 1,1") { 71 | expect(self.nullableSubject[1,1]).to(beNil()) 72 | } 73 | 74 | it("reports 2 for index 0,0") { 75 | expect(self.nullableSubject[0,0]) == 2 76 | } 77 | 78 | it("reports 4 for index 0,1") { 79 | expect(self.nullableSubject[0,1]) == 4 80 | } 81 | 82 | it("reports an array of [2,4] when compactMapped") { 83 | expect(self.nullableSubject.compactMap({$0})) == [2, 4] 84 | } 85 | 86 | it("reports the expected array when mapped") { 87 | let expectedArray: [Int?] = [2, 4, nil, nil, nil, nil, nil, nil, nil] 88 | expect(self.nullableSubject.map({$0})) == expectedArray 89 | } 90 | } 91 | 92 | context("when setting index 0,0 to 3 and index 2,2 to 6") { 93 | 94 | beforeEach { 95 | self.nullableSubject[0,0] = 3 96 | self.nullableSubject[2,2] = 6 97 | } 98 | 99 | it("continues to report nil for index 1,1") { 100 | expect(self.nullableSubject[1,1]).to(beNil()) 101 | } 102 | 103 | it("reports 3 for index 0,0") { 104 | expect(self.nullableSubject[0,0]) == 3 105 | } 106 | 107 | it("reports 6 for index 2,2") { 108 | expect(self.nullableSubject[2,2]) == 6 109 | } 110 | 111 | it("reports an array of [3,6] when compactMapped") { 112 | expect(self.nullableSubject.compactMap({$0})) == [3, 6] 113 | } 114 | 115 | it("reports the expected array when mapped") { 116 | let expectedArray: [Int?] = [3, nil, nil, nil, nil, nil, nil, nil, 6] 117 | expect(self.nullableSubject.map({$0})) == expectedArray 118 | } 119 | } 120 | } 121 | 122 | describe("A 3x3 nullable integer grid initialized with an array") { 123 | 124 | let array = [nil, 2, 3, 4, 5, 6, 7, 8, 9] 125 | 126 | beforeEach { 127 | self.nullableSubject = Grid(rowSize: 3, columnSize: 3, using: array) 128 | } 129 | 130 | afterEach { 131 | self.nullableSubject = nil 132 | } 133 | 134 | it("reports the expected value for 0,0") { 135 | expect(self.nullableSubject[0,0]).to(beNil()) 136 | } 137 | 138 | it("reports the expected value for 0,1") { 139 | expect(self.nullableSubject[0,1]) == 2 140 | } 141 | 142 | it("reports the expected value for 1,0") { 143 | expect(self.nullableSubject[1,0]) == 4 144 | } 145 | 146 | it("reports the expected value for 2,2") { 147 | expect(self.nullableSubject[2,2]) == 9 148 | } 149 | 150 | it("reports the original array when mapped") { 151 | expect(self.nullableSubject.map({$0})) == array 152 | } 153 | 154 | it("reports the expected last element") { 155 | expect(self.nullableSubject.last) == 9 156 | } 157 | 158 | it("reports the correct reverse map") { 159 | expect(self.nullableSubject.reversed()) == array.reversed() 160 | } 161 | } 162 | 163 | describe("A 3x3 nonnull integer grid initialized with an array") { 164 | 165 | beforeEach { 166 | self.nonnullSubject = Grid(rowSize: 3, columnSize: 3, using: [1, 2, 3, 4, 5, 6, 7, 8, 9]) 167 | } 168 | 169 | afterEach { 170 | self.nonnullSubject = nil 171 | } 172 | 173 | it("reports the expected value for 0,0") { 174 | expect(self.nonnullSubject[0,0]) == 1 175 | } 176 | 177 | it("reports the expected value for 0,1") { 178 | expect(self.nonnullSubject[0,1]) == 2 179 | } 180 | 181 | it("reports the expected value for 1,0") { 182 | expect(self.nonnullSubject[1,0]) == 4 183 | } 184 | 185 | it("reports the expected value for 2,2") { 186 | expect(self.nonnullSubject[2,2]) == 9 187 | } 188 | 189 | it("reports the original array when mapped") { 190 | let expectedArray: [Int] = [1, 2, 3, 4, 5, 6, 7, 8, 9] 191 | expect(self.nonnullSubject.map({$0})) == expectedArray 192 | } 193 | } 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /SudoChessTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /SudoChessTests/KnightSpec.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KnightSpec.swift 3 | // SudoChessFoundationTests 4 | // 5 | // Created by Daniel Panzer on 9/11/19. 6 | // Copyright © 2019 Daniel Panzer. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import Quick 11 | import Nimble 12 | 13 | @testable import SudoChess 14 | 15 | class KnightSpec: QuickSpec { 16 | 17 | private var subject: Knight! 18 | private var moveGrid: BooleanChessGrid! 19 | 20 | override func spec() { 21 | 22 | describe("A Knight owned by white") { 23 | 24 | beforeEach { 25 | self.subject = Knight(owner: .white) 26 | } 27 | 28 | afterEach { 29 | self.subject = nil 30 | } 31 | 32 | let expectedA1Positions = ChessGrid(array: [ 33 | X, X, X, X, X, X, X, X, 34 | X, X, X, X, X, X, X, X, 35 | X, X, X, X, X, X, X, X, 36 | X, X, X, X, X, X, X, X, 37 | X, X, X, X, X, X, X, X, 38 | X, O, X, X, X, X, X, X, 39 | X, X, O, X, X, X, X, X, 40 | X, X, X, X, X, X, X, X, 41 | ]) 42 | 43 | let expectedD5Positions = ChessGrid(array: [ 44 | X, X, X, X, X, X, X, X, 45 | X, X, O, X, O, X, X, X, 46 | X, O, X, X, X, O, X, X, 47 | X, X, X, X, X, X, X, X, 48 | X, O, X, X, X, O, X, X, 49 | X, X, O, X, O, X, X, X, 50 | X, X, X, X, X, X, X, X, 51 | X, X, X, X, X, X, X, X, 52 | ]) 53 | 54 | let expectedF8Positions = ChessGrid(array: [ 55 | X, X, X, X, X, X, X, X, 56 | X, X, X, O, X, X, X, O, 57 | X, X, X, X, O, X, O, X, 58 | X, X, X, X, X, X, X, X, 59 | X, X, X, X, X, X, X, X, 60 | X, X, X, X, X, X, X, X, 61 | X, X, X, X, X, X, X, X, 62 | X, X, X, X, X, X, X, X, 63 | ]) 64 | 65 | let expectedF8PositionsWithFriendlyCollisions = ChessGrid(array: [ 66 | X, X, X, X, X, X, X, X, 67 | X, X, X, X, X, X, X, X, 68 | X, X, X, X, O, X, O, X, 69 | X, X, X, X, X, X, X, X, 70 | X, X, X, X, X, X, X, X, 71 | X, X, X, X, X, X, X, X, 72 | X, X, X, X, X, X, X, X, 73 | X, X, X, X, X, X, X, X, 74 | ]) 75 | 76 | let expectedF8PositionsWithEnemyCollisions = ChessGrid(array: [ 77 | X, X, X, X, X, X, X, X, 78 | X, X, X, O, X, X, X, O, 79 | X, X, X, X, O, X, O, X, 80 | X, X, X, X, X, X, X, X, 81 | X, X, X, X, X, X, X, X, 82 | X, X, X, X, X, X, X, X, 83 | X, X, X, X, X, X, X, X, 84 | X, X, X, X, X, X, X, X, 85 | ]) 86 | 87 | context("when considering moves from a1 on an empty board") { 88 | 89 | beforeEach { 90 | let emptyBoard = Board() 91 | let moves = self.subject.possibleMoves(from: Position(.a, .one), in: Game(board: emptyBoard)) 92 | self.moveGrid = BooleanChessGrid(positions: moves.map{$0.destination}) 93 | } 94 | 95 | afterEach { 96 | self.moveGrid = nil 97 | } 98 | 99 | it("generates the expected grid of moves") { 100 | expect(self.moveGrid) == expectedA1Positions 101 | } 102 | } 103 | 104 | context("when considering threatened positions from a1 on an empty board") { 105 | 106 | beforeEach { 107 | let emptyBoard = Board() 108 | self.moveGrid = self.subject.threatenedPositions(from: Position(.a, .one), in: Game(board: emptyBoard)) 109 | } 110 | 111 | afterEach { 112 | self.moveGrid = nil 113 | } 114 | 115 | it("generates the expected threat grid") { 116 | expect(self.moveGrid) == expectedA1Positions 117 | } 118 | } 119 | 120 | context("when considering moves from d5 on an empty board") { 121 | 122 | beforeEach { 123 | let emptyBoard = Board() 124 | let moves = self.subject.possibleMoves(from: Position(.d, .five), in: Game(board: emptyBoard)) 125 | self.moveGrid = BooleanChessGrid(positions: moves.map{$0.destination}) 126 | } 127 | 128 | afterEach { 129 | self.moveGrid = nil 130 | } 131 | 132 | it("generates the expected grid of moves") { 133 | expect(self.moveGrid) == expectedD5Positions 134 | } 135 | } 136 | 137 | context("when considering threatened positions from d5 on an empty board") { 138 | 139 | beforeEach { 140 | let emptyBoard = Board() 141 | self.moveGrid = self.subject.threatenedPositions(from: Position(.d, .five), in: Game(board: emptyBoard)) 142 | } 143 | 144 | afterEach { 145 | self.moveGrid = nil 146 | } 147 | 148 | it("generates the expected threat grid") { 149 | expect(self.moveGrid) == expectedD5Positions 150 | } 151 | } 152 | 153 | context("when considering moves from f8 on an empty board") { 154 | 155 | beforeEach { 156 | let emptyBoard = Board() 157 | let moves = self.subject.possibleMoves(from: Position(.f, .eight), in: Game(board: emptyBoard)) 158 | self.moveGrid = BooleanChessGrid(positions: moves.map{$0.destination}) 159 | } 160 | 161 | afterEach { 162 | self.moveGrid = nil 163 | } 164 | 165 | it("generates the expected grid of moves") { 166 | expect(self.moveGrid) == expectedF8Positions 167 | } 168 | } 169 | 170 | context("when considering threatened positions from f8 on an empty board") { 171 | 172 | beforeEach { 173 | let emptyBoard = Board() 174 | self.moveGrid = self.subject.threatenedPositions(from: Position(.f, .eight), in: Game(board: emptyBoard)) 175 | } 176 | 177 | afterEach { 178 | self.moveGrid = nil 179 | } 180 | 181 | it("generates the expected threat grid") { 182 | expect(self.moveGrid) == expectedF8Positions 183 | } 184 | } 185 | 186 | context("when considering moves from f8 on a board with friendly collisions") { 187 | 188 | beforeEach { 189 | var boardWithFriendlyCollisions = Board() 190 | boardWithFriendlyCollisions[Position(.d, .seven)] = Rook(owner: .white) 191 | boardWithFriendlyCollisions[Position(.h, .seven)] = Rook(owner: .white) 192 | let moves = self.subject.possibleMoves(from: Position(.f, .eight), in: Game(board: boardWithFriendlyCollisions)) 193 | self.moveGrid = BooleanChessGrid(positions: moves.map{$0.destination}) 194 | } 195 | 196 | afterEach { 197 | self.moveGrid = nil 198 | } 199 | 200 | it("generates the expected grid of moves") { 201 | expect(self.moveGrid) == expectedF8PositionsWithFriendlyCollisions 202 | } 203 | } 204 | 205 | context("when considering threatened positions from f8 on a board with friendly collisions") { 206 | 207 | beforeEach { 208 | var boardWithFriendlyCollisions = Board() 209 | boardWithFriendlyCollisions[Position(.d, .seven)] = Rook(owner: .white) 210 | boardWithFriendlyCollisions[Position(.h, .seven)] = Rook(owner: .white) 211 | self.moveGrid = self.subject.threatenedPositions(from: Position(.f, .eight), in: Game(board: boardWithFriendlyCollisions)) 212 | } 213 | 214 | afterEach { 215 | self.moveGrid = nil 216 | } 217 | 218 | it("generates the expected threat grid") { 219 | expect(self.moveGrid) == expectedF8PositionsWithFriendlyCollisions 220 | } 221 | } 222 | 223 | context("when considering moves from f8 on a board with enemy collisions") { 224 | 225 | beforeEach { 226 | var boardWithEnemyCollisions = Board() 227 | boardWithEnemyCollisions[Position(.d, .seven)] = Rook(owner: .black) 228 | boardWithEnemyCollisions[Position(.h, .seven)] = Rook(owner: .black) 229 | let moves = self.subject.possibleMoves(from: Position(.f, .eight), in: Game(board: boardWithEnemyCollisions)) 230 | self.moveGrid = BooleanChessGrid(positions: moves.map{$0.destination}) 231 | } 232 | 233 | afterEach { 234 | self.moveGrid = nil 235 | } 236 | 237 | it("generates the expected grid of moves") { 238 | expect(self.moveGrid) == expectedF8PositionsWithEnemyCollisions 239 | } 240 | } 241 | 242 | context("when considering threatened positions from f8 on a board with enemy collisions") { 243 | 244 | beforeEach { 245 | var boardWithEnemyCollisions = Board() 246 | boardWithEnemyCollisions[Position(.d, .seven)] = Rook(owner: .black) 247 | boardWithEnemyCollisions[Position(.h, .seven)] = Rook(owner: .black) 248 | self.moveGrid = self.subject.threatenedPositions(from: Position(.f, .eight), in: Game(board: boardWithEnemyCollisions)) 249 | } 250 | 251 | afterEach { 252 | self.moveGrid = nil 253 | } 254 | 255 | it("generates the expected grid of moves") { 256 | expect(self.moveGrid) == expectedF8PositionsWithEnemyCollisions 257 | } 258 | } 259 | } 260 | } 261 | } 262 | -------------------------------------------------------------------------------- /SudoChessTests/PositionSpec.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SudoChessFoundationTests.swift 3 | // SudoChessFoundationTests 4 | // 5 | // Created by Daniel Panzer on 9/9/19. 6 | // Copyright © 2019 Daniel Panzer. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import Quick 11 | import Nimble 12 | 13 | @testable import SudoChess 14 | 15 | class PositionSpec: QuickSpec { 16 | 17 | private var subject: Position! 18 | private var indexPath: IndexPath! 19 | private var positionPath: [Position]! 20 | 21 | override func spec() { 22 | 23 | describe("The upper left chess position") { 24 | 25 | beforeEach { 26 | self.subject = Position(.a, .eight) 27 | } 28 | 29 | afterEach { 30 | self.subject = nil 31 | } 32 | 33 | it("translates to the correct grid index") { 34 | let expectedResult = IndexPath(row: 0, column: 0) 35 | expect(self.subject.gridIndex).to(equal(expectedResult)) 36 | } 37 | } 38 | 39 | describe("The lower left chess position") { 40 | 41 | beforeEach { 42 | self.subject = Position(.a, .one) 43 | } 44 | 45 | afterEach { 46 | self.subject = nil 47 | } 48 | 49 | it("translates to the correct grid index") { 50 | let expectedResult = IndexPath(row: 7, column: 0) 51 | expect(self.subject.gridIndex).to(equal(expectedResult)) 52 | } 53 | } 54 | 55 | describe("The upper right chess position") { 56 | 57 | beforeEach { 58 | self.subject = Position(.h, .eight) 59 | } 60 | 61 | afterEach { 62 | self.subject = nil 63 | } 64 | 65 | it("translates to the correct grid index") { 66 | let expectedResult = IndexPath(row: 0, column: 7) 67 | expect(self.subject.gridIndex).to(equal(expectedResult)) 68 | } 69 | } 70 | 71 | describe("The lower right chess position") { 72 | 73 | beforeEach { 74 | self.subject = Position(.h, .one) 75 | } 76 | 77 | afterEach { 78 | self.subject = nil 79 | } 80 | 81 | it("translates to the correct grid index") { 82 | let expectedResult = IndexPath(row: 7, column: 7) 83 | expect(self.subject.gridIndex).to(equal(expectedResult)) 84 | } 85 | } 86 | 87 | describe("A mid board chess position") { 88 | 89 | beforeEach { 90 | self.subject = Position(.c, .three) 91 | } 92 | 93 | afterEach { 94 | self.subject = nil 95 | } 96 | 97 | it("translates to the correct grid index") { 98 | let expectedResult = IndexPath(row: 5, column: 2) 99 | expect(self.subject.gridIndex).to(equal(expectedResult)) 100 | } 101 | } 102 | 103 | describe("The upper left grid IndexPath") { 104 | 105 | beforeEach { 106 | self.indexPath = IndexPath(row: 0, column: 0) 107 | } 108 | 109 | afterEach { 110 | self.indexPath = nil 111 | } 112 | 113 | it("converts to the correct chess position") { 114 | let expectedResult = Position(.a, .eight) 115 | let conversion = Position(gridIndex: self.indexPath) 116 | expect(conversion) == expectedResult 117 | } 118 | } 119 | 120 | describe("The lower right grid IndexPath") { 121 | 122 | beforeEach { 123 | self.indexPath = IndexPath(row: 7, column: 7) 124 | } 125 | 126 | afterEach { 127 | self.indexPath = nil 128 | } 129 | 130 | it("converts to the correct chess position") { 131 | let expectedResult = Position(.h, .one) 132 | let conversion = Position(gridIndex: self.indexPath) 133 | expect(conversion) == expectedResult 134 | } 135 | } 136 | 137 | describe("A mid board grid IndexPath") { 138 | 139 | beforeEach { 140 | self.indexPath = IndexPath(row: 3, column: 5) 141 | } 142 | 143 | afterEach { 144 | self.indexPath = nil 145 | } 146 | 147 | it("converts to the correct chess position") { 148 | let expectedResult = Position(.f, .five) 149 | let conversion = Position(gridIndex: self.indexPath) 150 | expect(conversion) == expectedResult 151 | } 152 | } 153 | 154 | describe("An IndexPath that is out of bounds for a chess grid") { 155 | 156 | beforeEach { 157 | self.indexPath = IndexPath(row: 10, column: 0) 158 | } 159 | 160 | afterEach { 161 | self.indexPath = nil 162 | } 163 | 164 | it("converts to a null chess position") { 165 | let conversion = Position(gridIndex: self.indexPath) 166 | expect(conversion).to(beNil()) 167 | } 168 | } 169 | 170 | describe("A straight line of positions") { 171 | 172 | beforeEach { 173 | self.positionPath = [ 174 | Position(.a, .one), 175 | Position(.a, .two), 176 | Position(.a, .three) 177 | ] 178 | } 179 | 180 | afterEach { 181 | self.positionPath = nil 182 | } 183 | 184 | it("is a valid path") { 185 | expect(Position.isValidPath(self.positionPath)) == true 186 | } 187 | } 188 | 189 | describe("A diagonal line of positions") { 190 | 191 | beforeEach { 192 | self.positionPath = [ 193 | Position(.a, .one), 194 | Position(.b, .two), 195 | Position(.c, .three) 196 | ] 197 | } 198 | 199 | afterEach { 200 | self.positionPath = nil 201 | } 202 | 203 | it("is a valid path") { 204 | expect(Position.isValidPath(self.positionPath)) == true 205 | } 206 | } 207 | 208 | describe("A disjoined line of positions") { 209 | 210 | beforeEach { 211 | self.positionPath = [ 212 | Position(.a, .one), 213 | Position(.a, .three), 214 | Position(.a, .four) 215 | ] 216 | } 217 | 218 | afterEach { 219 | self.positionPath = nil 220 | } 221 | 222 | it("is not a valid path") { 223 | expect(Position.isValidPath(self.positionPath)) == false 224 | } 225 | } 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /SudoChessTests/QueenSpec.swift: -------------------------------------------------------------------------------- 1 | // 2 | // QueenSpec.swift 3 | // SudoChessFoundationTests 4 | // 5 | // Created by Daniel Panzer on 9/11/19. 6 | // Copyright © 2019 Daniel Panzer. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import Quick 11 | import Nimble 12 | 13 | @testable import SudoChess 14 | 15 | class QueenSpec: QuickSpec { 16 | 17 | private var subject: Queen! 18 | private var moveGrid: BooleanChessGrid! 19 | 20 | override func spec() { 21 | 22 | describe("A Queen owned by white") { 23 | 24 | beforeEach { 25 | self.subject = Queen(owner: .white) 26 | } 27 | 28 | afterEach { 29 | self.subject = nil 30 | } 31 | 32 | let expectedA1Positions = ChessGrid(array: [ 33 | O, X, X, X, X, X, X, O, 34 | O, X, X, X, X, X, O, X, 35 | O, X, X, X, X, O, X, X, 36 | O, X, X, X, O, X, X, X, 37 | O, X, X, O, X, X, X, X, 38 | O, X, O, X, X, X, X, X, 39 | O, O, X, X, X, X, X, X, 40 | X, O, O, O, O, O, O, O 41 | ]) 42 | 43 | let expectedD5Positions = ChessGrid(array: [ 44 | O, X, X, O, X, X, O, X, 45 | X, O, X, O, X, O, X, X, 46 | X, X, O, O, O, X, X, X, 47 | O, O, O, X, O, O, O, O, 48 | X, X, O, O, O, X, X, X, 49 | X, O, X, O, X, O, X, X, 50 | O, X, X, O, X, X, O, X, 51 | X, X, X, O, X, X, X, O, 52 | ]) 53 | 54 | let expectedF8Positions = ChessGrid(array: [ 55 | O, O, O, O, O, X, O, O, 56 | X, X, X, X, O, O, O, X, 57 | X, X, X, O, X, O, X, O, 58 | X, X, O, X, X, O, X, X, 59 | X, O, X, X, X, O, X, X, 60 | O, X, X, X, X, O, X, X, 61 | X, X, X, X, X, O, X, X, 62 | X, X, X, X, X, O, X, X, 63 | ]) 64 | 65 | context("when considering moves from a1 on an empty board") { 66 | 67 | beforeEach { 68 | let emptyGame = Game(board: Board()) 69 | let moves = self.subject.possibleMoves(from: Position(.a, .one), in: emptyGame) 70 | self.moveGrid = BooleanChessGrid(positions: moves.map{$0.destination}) 71 | } 72 | 73 | afterEach { 74 | self.moveGrid = nil 75 | } 76 | 77 | it("generates the expected grid of moves") { 78 | expect(self.moveGrid) == expectedA1Positions 79 | } 80 | } 81 | 82 | context("when considering threatened positions from a1 on an empty board") { 83 | 84 | beforeEach { 85 | let emptyGame = Game(board: Board()) 86 | self.moveGrid = self.subject.threatenedPositions(from: Position(.a, .one), in: emptyGame) 87 | } 88 | 89 | afterEach { 90 | self.moveGrid = nil 91 | } 92 | 93 | it("generates the expected threat grid") { 94 | expect(self.moveGrid) == expectedA1Positions 95 | } 96 | } 97 | 98 | context("when considering moves from d5 on an empty board") { 99 | 100 | beforeEach { 101 | let emptyGame = Game(board: Board()) 102 | let moves = self.subject.possibleMoves(from: Position(.d, .five), in: emptyGame) 103 | self.moveGrid = BooleanChessGrid(positions: moves.map{$0.destination}) 104 | } 105 | 106 | afterEach { 107 | self.moveGrid = nil 108 | } 109 | 110 | it("generates the expected grid of moves") { 111 | expect(self.moveGrid) == expectedD5Positions 112 | } 113 | } 114 | 115 | context("when considering threatened positions from d5 on an empty board") { 116 | 117 | beforeEach { 118 | let emptyGame = Game(board: Board()) 119 | self.moveGrid = self.subject.threatenedPositions(from: Position(.d, .five), in: emptyGame) 120 | } 121 | 122 | afterEach { 123 | self.moveGrid = nil 124 | } 125 | 126 | it("generates the expected threat grid") { 127 | expect(self.moveGrid) == expectedD5Positions 128 | } 129 | } 130 | 131 | context("when considering moves from f8 on an empty board") { 132 | 133 | beforeEach { 134 | let emptyGame = Game(board: Board()) 135 | let moves = self.subject.possibleMoves(from: Position(.f, .eight), in: emptyGame) 136 | self.moveGrid = BooleanChessGrid(positions: moves.map{$0.destination}) 137 | } 138 | 139 | afterEach { 140 | self.moveGrid = nil 141 | } 142 | 143 | it("generates the expected grid of moves") { 144 | expect(self.moveGrid) == expectedF8Positions 145 | } 146 | } 147 | 148 | context("when considering threatened positions from f8 on an empty board") { 149 | 150 | beforeEach { 151 | let emptyGame = Game(board: Board()) 152 | self.moveGrid = self.subject.threatenedPositions(from: Position(.f, .eight), in: emptyGame) 153 | } 154 | 155 | afterEach { 156 | self.moveGrid = nil 157 | } 158 | 159 | it("generates the expected threat grid") { 160 | expect(self.moveGrid) == expectedF8Positions 161 | } 162 | } 163 | } 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /SudoChessTests/RookSpec.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RookSpec.swift 3 | // SudoChessFoundationTests 4 | // 5 | // Created by Daniel Panzer on 9/11/19. 6 | // Copyright © 2019 Daniel Panzer. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import Quick 11 | import Nimble 12 | 13 | @testable import SudoChess 14 | 15 | class RookSpec: QuickSpec { 16 | 17 | private var subject: Rook! 18 | private var moveGrid: BooleanChessGrid! 19 | 20 | override func spec() { 21 | 22 | describe("A rook owned by white") { 23 | 24 | beforeEach { 25 | self.subject = Rook(owner: .white) 26 | } 27 | 28 | afterEach { 29 | self.subject = nil 30 | } 31 | 32 | let expectedA1Positions = BooleanChessGrid(array: [ 33 | O, X, X, X, X, X, X, X, 34 | O, X, X, X, X, X, X, X, 35 | O, X, X, X, X, X, X, X, 36 | O, X, X, X, X, X, X, X, 37 | O, X, X, X, X, X, X, X, 38 | O, X, X, X, X, X, X, X, 39 | O, X, X, X, X, X, X, X, 40 | X, O, O, O, O, O, O, O 41 | ]) 42 | 43 | let expectedD5Positions = ChessGrid(array: [ 44 | X, X, X, O, X, X, X, X, 45 | X, X, X, O, X, X, X, X, 46 | X, X, X, O, X, X, X, X, 47 | O, O, O, X, O, O, O, O, 48 | X, X, X, O, X, X, X, X, 49 | X, X, X, O, X, X, X, X, 50 | X, X, X, O, X, X, X, X, 51 | X, X, X, O, X, X, X, X, 52 | ]) 53 | 54 | let expectedF8Positions = ChessGrid(array: [ 55 | O, O, O, O, O, X, O, O, 56 | X, X, X, X, X, O, X, X, 57 | X, X, X, X, X, O, X, X, 58 | X, X, X, X, X, O, X, X, 59 | X, X, X, X, X, O, X, X, 60 | X, X, X, X, X, O, X, X, 61 | X, X, X, X, X, O, X, X, 62 | X, X, X, X, X, O, X, X, 63 | ]) 64 | 65 | context("when considering moves from a1 on an empty board") { 66 | 67 | beforeEach { 68 | let emptyBoard = Board() 69 | let moves = self.subject.possibleMoves(from: Position(.a, .one), in: Game(board: emptyBoard)) 70 | self.moveGrid = BooleanChessGrid(positions: moves.map{$0.destination}) 71 | } 72 | 73 | afterEach { 74 | self.moveGrid = nil 75 | } 76 | 77 | it("generates the expected grid of moves") { 78 | expect(self.moveGrid) == expectedA1Positions 79 | } 80 | } 81 | 82 | context("when considering threatened positions from a1 on an empty board") { 83 | 84 | beforeEach { 85 | let emptyBoard = Board() 86 | self.moveGrid = self.subject.threatenedPositions(from: Position(.a, .one), in: Game(board: emptyBoard)) 87 | } 88 | 89 | afterEach { 90 | self.moveGrid = nil 91 | } 92 | 93 | it("generates the expected threat grid") { 94 | expect(self.moveGrid) == expectedA1Positions 95 | } 96 | } 97 | 98 | context("when considering moves from d5 on an empty board") { 99 | 100 | beforeEach { 101 | let emptyBoard = Board() 102 | let moves = self.subject.possibleMoves(from: Position(.d, .five), in: Game(board: emptyBoard)) 103 | self.moveGrid = BooleanChessGrid(positions: moves.map{$0.destination}) 104 | } 105 | 106 | afterEach { 107 | self.moveGrid = nil 108 | } 109 | 110 | it("generates the expected grid of moves") { 111 | expect(self.moveGrid) == expectedD5Positions 112 | } 113 | } 114 | 115 | context("when considering threatened positions from d5 on an empty board") { 116 | 117 | beforeEach { 118 | let emptyBoard = Board() 119 | self.moveGrid = self.subject.threatenedPositions(from: Position(.d, .five), in: Game(board: emptyBoard)) 120 | } 121 | 122 | afterEach { 123 | self.moveGrid = nil 124 | } 125 | 126 | it("generates the expected grid of moves") { 127 | expect(self.moveGrid) == expectedD5Positions 128 | } 129 | } 130 | 131 | context("when considering moves from f8 on an empty board") { 132 | 133 | beforeEach { 134 | let emptyBoard = Board() 135 | let moves = self.subject.possibleMoves(from: Position(.f, .eight), in: Game(board: emptyBoard)) 136 | self.moveGrid = BooleanChessGrid(positions: moves.map{$0.destination}) 137 | } 138 | 139 | afterEach { 140 | self.moveGrid = nil 141 | } 142 | 143 | it("generates the expected grid of moves") { 144 | expect(self.moveGrid) == expectedF8Positions 145 | } 146 | } 147 | 148 | context("when considering threatened positions from f8 on an empty board") { 149 | 150 | beforeEach { 151 | let emptyBoard = Board() 152 | self.moveGrid = self.subject.threatenedPositions(from: Position(.f, .eight), in: Game(board: emptyBoard)) 153 | } 154 | 155 | afterEach { 156 | self.moveGrid = nil 157 | } 158 | 159 | it("generates the expected grid of moves") { 160 | expect(self.moveGrid) == expectedF8Positions 161 | } 162 | } 163 | } 164 | 165 | describe("A rook owned by black") { 166 | 167 | beforeEach { 168 | self.subject = Rook(owner: .black) 169 | } 170 | 171 | afterEach { 172 | self.subject = nil 173 | } 174 | 175 | let expectedA8Positions = ChessGrid(array: [ 176 | X, O, O, O, X, X, X, X, 177 | O, X, X, X, X, X, X, X, 178 | O, X, X, X, X, X, X, X, 179 | O, X, X, X, X, X, X, X, 180 | X, X, X, X, X, X, X, X, 181 | X, X, X, X, X, X, X, X, 182 | X, X, X, X, X, X, X, X, 183 | X, X, X, X, X, X, X, X, 184 | ]) 185 | 186 | let expectedH8Positions = ChessGrid(array: [ 187 | X, X, X, X, O, O, O, X, 188 | X, X, X, X, X, X, X, O, 189 | X, X, X, X, X, X, X, O, 190 | X, X, X, X, X, X, X, O, 191 | X, X, X, X, X, X, X, O, 192 | X, X, X, X, X, X, X, O, 193 | X, X, X, X, X, X, X, O, 194 | X, X, X, X, X, X, X, X, 195 | ]) 196 | 197 | context("when considering moves from a8 on a board with colliding friendly pieces") { 198 | 199 | beforeEach { 200 | var board = Board() 201 | board[Position(.a, .four)] = Pawn(owner: .black) 202 | board[Position(.e, .eight)] = Rook(owner: .black) 203 | let moves = self.subject.possibleMoves(from: Position(.a, .eight), in: Game(board: board)) 204 | self.moveGrid = BooleanChessGrid(positions: moves.map{$0.destination}) 205 | } 206 | 207 | afterEach { 208 | self.moveGrid = nil 209 | } 210 | 211 | it("generates the expected grid of moves") { 212 | expect(self.moveGrid) == expectedA8Positions 213 | } 214 | } 215 | 216 | context("when considering threatened positions from a8 on a board with colliding friendly pieces") { 217 | 218 | beforeEach { 219 | var board = Board() 220 | board[Position(.a, .four)] = Pawn(owner: .black) 221 | board[Position(.e, .eight)] = Rook(owner: .black) 222 | self.moveGrid = self.subject.threatenedPositions(from: Position(.a, .eight), in: Game(board: board)) 223 | } 224 | 225 | afterEach { 226 | self.moveGrid = nil 227 | } 228 | 229 | it("generates the expected threat grid") { 230 | expect(self.moveGrid) == expectedA8Positions 231 | } 232 | } 233 | 234 | context("when considering moves from h8 on a board with colliding enemy pieces") { 235 | 236 | beforeEach { 237 | var board = Board() 238 | board[Position(.h, .two)] = Pawn(owner: .white) 239 | board[Position(.e, .eight)] = Queen(owner: .white) 240 | let moves = self.subject.possibleMoves(from: Position(.h, .eight), in: Game(board: board)) 241 | self.moveGrid = BooleanChessGrid(positions: moves.map{$0.destination}) 242 | } 243 | 244 | afterEach { 245 | self.moveGrid = nil 246 | } 247 | 248 | it("generates the expected grid of moves") { 249 | expect(self.moveGrid) == expectedH8Positions 250 | } 251 | } 252 | 253 | context("when considering threatened positions from h8 on a board with colliding enemy pieces") { 254 | 255 | beforeEach { 256 | var board = Board() 257 | board[Position(.h, .two)] = Pawn(owner: .white) 258 | board[Position(.e, .eight)] = Queen(owner: .white) 259 | self.moveGrid = self.subject.threatenedPositions(from: Position(.h, .eight), in: Game(board: board)) 260 | } 261 | 262 | afterEach { 263 | self.moveGrid = nil 264 | } 265 | 266 | it("generates the expected threat grid") { 267 | expect(self.moveGrid) == expectedH8Positions 268 | } 269 | } 270 | } 271 | } 272 | } 273 | -------------------------------------------------------------------------------- /SudoChessTests/SudoChessTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SudoChessTests.swift 3 | // SudoChessTests 4 | // 5 | // Created by Daniel Panzer on 9/2/19. 6 | // Copyright © 2019 Daniel Panzer. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import SudoChess 11 | 12 | class SudoChessTests: XCTestCase { 13 | 14 | override func setUp() { 15 | // Put setup code here. This method is called before the invocation of each test method in the class. 16 | } 17 | 18 | override func tearDown() { 19 | // Put teardown code here. This method is called after the invocation of each test method in the class. 20 | } 21 | 22 | func testExample() { 23 | // This is an example of a functional test case. 24 | // Use XCTAssert and related functions to verify your tests produce the correct results. 25 | } 26 | 27 | func testPerformanceExample() { 28 | // This is an example of a performance test case. 29 | self.measure { 30 | // Put the code you want to measure the time of here. 31 | } 32 | } 33 | 34 | } 35 | --------------------------------------------------------------------------------