├── .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 | 
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 |
--------------------------------------------------------------------------------