aboutsummaryrefslogtreecommitdiff
path: root/Sources/SwiftChessNeo/Game.swift
blob: b70c0bbf6c7dfa49967fd1fa1e6025a6c25f19e6 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
//
//  Game.swift
//  Sage
//
//  Copyright 2016-2017 Nikolai Vazquez
//  Modified by SuperGeroy
//
//  Licensed under the Apache License, Version 2.0 (the "License");
//  you may not use this file except in compliance with the License.
//  You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
//  Unless required by applicable law or agreed to in writing, software
//  distributed under the License is distributed on an "AS IS" BASIS,
//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//  See the License for the specific language governing permissions and
//  limitations under the License.
//
/// A chess game.
public final class Game {
    
    public var PGN: PGN?

    /// A chess game outcome.
    public enum Outcome: Hashable, CustomStringConvertible {

        /// A win for a `Color`.
        case win(Color)

        /// A draw.
        case draw

        /// Draw.
        internal static let _draw = Outcome.draw

        /// Win.
        internal static func _win(_ color: Color) -> Outcome {
            return .win(color)
        }

        /// The hash value.
        public func hash(into hasher: inout Hasher) {
            switch self {
            case .win(let color):
                hasher.combine(0)
                hasher.combine(color)
            case .draw:
                hasher.combine(1)
            }
        }

        /// A textual representation of `self`.
        public var description: String {
            if let color = winColor {
                return color.isWhite ? "1-0" : "0-1"
            } else {
                return "1/2-1/2"
            }
        }

        /// The color for the winning player.
        public var winColor: Color? {
            guard case let .win(color) = self else { return nil }
            return color
        }

        /// `self` is a win.
        public var isWin: Bool {
            if case .win = self { return true } else { return false }
        }

        /// `self` is a draw.
        public var isDraw: Bool {
            return !isWin
        }

        /// Create an outcome from `string`. Ignores whitespace.
        public init?(_ string: String) {
            let stripped = string.split(separator: " ").map(String.init).joined(separator: "")
            switch stripped {
            case "1-0":
                self = ._win(.white)
            case "0-1":
                self = ._win(.black)
            case "1/2-1/2":
                self = ._draw
            case "½-½":
                self = .draw
            default:
                return nil
            }
        }

        /// The point value for a player. Can be 1 for win, 0.5 for draw, or 0 for loss.
        public func value(for playerColor: Color) -> Double {
            return winColor.map({ $0 == playerColor ? 1 : 0 }) ?? 0.5
        }

    }

    /// A game position.
    public struct Position: Equatable, CustomStringConvertible {

        /// The board for the position.
        public var board: Board

        /// The active player turn.
        public var playerTurn: PlayerTurn

        /// The castling rights.
        public var castlingRights: CastlingRights

        /// The en passant target location.
        public var enPassantTarget: Square?

        /// The halfmove number.
        public var halfmoves: UInt

        /// The fullmove clock.
        public var fullmoves: UInt

        /// A textual representation of `self`.
        public var description: String {
            return "Position(\(fen()))"
        }

        /// Create a position.
        public init(board: Board = Board(),
                    playerTurn: PlayerTurn = .white,
                    castlingRights: CastlingRights = .all,
                    enPassantTarget: Square? = nil,
                    halfmoves: UInt = 0,
                    fullmoves: UInt = 1) {
            self.board = board
            self.playerTurn = playerTurn
            self.castlingRights = castlingRights
            self.enPassantTarget = enPassantTarget
            self.halfmoves = halfmoves
            self.fullmoves = fullmoves
        }

        /// Create a position from a valid FEN string.
        ///
        /// - see also: [FEN (Wikipedia)](https://en.wikipedia.org/wiki/Forsyth%E2%80%93Edwards_Notation ),
        ///            [FEN (Chess Programming Wiki)](https://chessprogramming.org/Forsyth-Edwards_Notation)
        public init?(fen: String) {
            let parts = fen.split(separator: " ").map(String.init)
            guard
                parts.count == 6,
                let board = Board(fen: parts[0]),
                parts[1].count == 1,
                let playerTurn = parts[1].first.flatMap(Color.init),
                let rights = CastlingRights(string: parts[2]),
                let halfmoves = UInt(parts[4]),
                let fullmoves = UInt(parts[5]),
                fullmoves > 0 else {
                    return nil
            }
            var target: Square? = nil
            let targetStr = parts[3]
            if targetStr.count == 2 {
                guard let square = Square(targetStr) else {
                    return nil
                }
                target = square
            } else {
                guard targetStr == "-" else {
                    return nil
                }
            }
            self.init(board: board,
                      playerTurn: playerTurn,
                      castlingRights: rights,
                      enPassantTarget: target,
                      halfmoves: halfmoves,
                      fullmoves: fullmoves)
        }

        internal func _validationError() -> PositionError? {
            for color in Color.all {
                guard board.count(of: Piece(king: color)) == 1 else {
                    return .wrongKingCount(color)
                }
            }
            for right in castlingRights {
                let color = right.color
                let king = Piece(king: color)
                guard board.bitboard(for: king) == Bitboard(startFor: king) else {
                    return .missingKing(right)
                }
                let rook = Piece(rook: color)
                let square = Square(file: right.side.isKingside ? ._h : ._a,
                                    rank: Rank(startFor: color))
                guard board.bitboard(for: rook)[square] else {
                    return .missingRook(right)
                }
            }
            if let target = enPassantTarget {
                guard target.rank == (playerTurn.isWhite ? 6 : 3) else {
                    return .wrongEnPassantTargetRank(target.rank)
                }
                if let piece = board[target] {
                    return .nonEmptyEnPassantTarget(target, piece)
                }
                let pawnSquare = Square(file: target.file, rank: playerTurn.isWhite ? 5 : 4)
                guard board[pawnSquare] == Piece(pawn: playerTurn.inverse()) else {
                    return .missingEnPassantPawn(pawnSquare)
                }
                let startSquare = Square(file: target.file, rank: playerTurn.isWhite ? 7 : 2)
                if let piece = board[startSquare] {
                    return .nonEmptyEnPassantSquare(startSquare, piece)
                }
            }
            return nil
        }

        /// Returns the FEN string for the position.
        ///
        /// - see also: [FEN (Wikipedia)](https://en.wikipedia.org/wiki/Forsyth%E2%80%93Edwards_Notation ),
        ///            [FEN (Chess Programming Wiki)](https://chessprogramming.org/Forsyth-Edwards_Notation)
        public func fen() -> String {
            return board.fen()
                + " \(playerTurn.isWhite ? "w" : "b") \(castlingRights) "
                + (enPassantTarget.map({ "\($0 as Square)".lowercased() }) ?? "-")
                + " \(halfmoves) \(fullmoves)"
        }

    }

    /// An error in position validation.
    public enum PositionError: Error {

        /// Found number other than 1 for king count.
        case wrongKingCount(Color)

        /// King missing for castling right.
        case missingKing(CastlingRights.Right)

        /// Rook missing for castling right.
        case missingRook(CastlingRights.Right)

        /// Wrong rank for en passant target.
        case wrongEnPassantTargetRank(Rank)

        /// Non empty en passant target square.
        case nonEmptyEnPassantTarget(Square, Piece)

        /// Pawn missing for previous en passant.
        case missingEnPassantPawn(Square)

        /// Piece found at start of en passant move.
        case nonEmptyEnPassantSquare(Square, Piece)

    }

    /// An error in move execution.
    ///
    /// Thrown by the `execute(move:promotion:)` or `execute(uncheckedMove:promotion:)` method for a `Game` instance.
    public enum ExecutionError: Error {

        /// Missing piece at a square.
        case missingPiece(Square)

        /// Attempted illegal move.
        case illegalMove(Move, Color, Board)

        /// Could not promote with a piece kind.
        case invalidPromotion(Piece.Kind)

        /// The error message.
        public var message: String {
            switch self {
            case let .missingPiece(square):
                return "Missing piece: \(square)"
            case let .illegalMove(move, color, board):
                return "Illegal move: \(move) for \(color) on \(board)"
            case let .invalidPromotion(pieceKind):
                return "Invalid promoton: \(pieceKind)"
            }
        }

    }

    /// A player turn.
    public typealias PlayerTurn = Color

    /// All of the conducted moves in the game.
    private var _moveHistory: [(move: Move,
                                piece: Piece,
                                capture: Piece?,
                                enPassantTarget: Square?,
                                kingAttackers: Bitboard,
                                halfmoves: UInt,
                                rights: CastlingRights)]

    /// All of the undone moves in the game.
    private var _undoHistory: [(move: Move, promotion: Piece.Kind?, kingAttackers: Bitboard)]

    /// The game's board.
    public private(set) var board: Board

    /// The current player's turn.
    public private(set) var playerTurn: PlayerTurn

    /// The castling rights.
    public private(set) var castlingRights: CastlingRights

    /// The white player.
    public var whitePlayer: Player

    /// The black player.
    public var blackPlayer: Player

    /// The game's variant.
    public let variant: Variant

    /// Attackers to the current player's king.
    private var attackersToKing: Bitboard

    /// The current player's king is in check.
    public var kingIsChecked: Bool {
        return attackersToKing != 0
    }

    /// The current player's king is checked by two or more pieces.
    public var kingIsDoubleChecked: Bool {
        return attackersToKing.count > 1
    }

    /// All of the moves played in the game.
    public var playedMoves: [Move] {
        return _moveHistory.map({ $0.move })
    }

    /// The amount of plies (half moves) executed.
    public var moveCount: Int {
        return _moveHistory.count
    }

    /// The current fullmove number.
    public private(set) var fullmoves: UInt

    /// The current halfmove clock (used for 50-moves draw rule).
    ///
    /// Counts no-pawn moves with no capture.
    public private(set) var halfmoves: UInt

    /// The target move location for an en passant.
    public private(set) var enPassantTarget: Square?

    /// The captured piece for the last move.
    public var captureForLastMove: Piece? {
        return _moveHistory.last?.capture
    }

    /// The current position for `self`.
    public var position: Position {
        return Position(board: board,
                        playerTurn: playerTurn,
                        castlingRights: castlingRights,
                        enPassantTarget: enPassantTarget,
                        halfmoves: halfmoves,
                        fullmoves: fullmoves)
    }

    /// The outcome for `self` if no moves are available.
    public var outcome: Outcome? {
        let moves = _availableMoves(considerHalfmoves: false)
        if moves.isEmpty {
            return kingIsChecked ? ._win(playerTurn.inverse()) : ._draw
        } else if halfmoves >= 100 {
            return ._draw
        } else {
            return nil
        }
    }

    /// The game has no more available moves.
    public var isFinished: Bool {
        return availableMoves().isEmpty
    }

    /// Create a game from another.
    private init(game: Game) {
        self._moveHistory    = game._moveHistory
        self._undoHistory    = game._undoHistory
        self.board           = game.board
        self.playerTurn      = game.playerTurn
        self.castlingRights  = game.castlingRights
        self.whitePlayer     = game.whitePlayer
        self.blackPlayer     = game.blackPlayer
        self.variant         = game.variant
        self.attackersToKing = game.attackersToKing
        self.halfmoves       = game.halfmoves
        self.fullmoves       = game.fullmoves
        self.enPassantTarget = game.enPassantTarget
        self.PGN             = game.PGN
    }

    /// Creates a new chess game.
    ///
    /// - parameter whitePlayer: The game's white player. Default is a nameless human.
    /// - parameter blackPlayer: The game's black player. Default is a nameless human.
    /// - parameter variant: The game's chess variant. Default is standard.
    public init(whitePlayer: Player = Player(),
                blackPlayer: Player = Player(),
                variant: Variant = .standard) {
        self._moveHistory = []
        self._undoHistory = []
        self.board = Board(variant: variant)
        self.playerTurn = .white
        self.castlingRights = .all
        self.whitePlayer = whitePlayer
        self.blackPlayer = blackPlayer
        self.variant = variant
        self.attackersToKing = 0
        self.halfmoves = 0
        self.fullmoves = 1
    }

    /// Creates a chess game from a `Position`.
    ///
    /// - parameter position: The position to start off from.
    /// - parameter whitePlayer: The game's white player. Default is a nameless human.
    /// - parameter blackPlayer: The game's black player. Default is a nameless human.
    /// - parameter variant: The game's chess variant. Default is standard.
    ///
    /// - throws: `PositionError` if the position is invalid.
    public init(position: Position,
                whitePlayer: Player = Player(),
                blackPlayer: Player = Player(),
                variant: Variant = .standard) throws {
        if let error = position._validationError() {
            throw error
        }
        self._moveHistory = []
        self._undoHistory = []
        self.board = position.board
        self.playerTurn = position.playerTurn
        self.castlingRights = position.castlingRights
        self.whitePlayer = whitePlayer
        self.blackPlayer = blackPlayer
        self.variant = variant
        self.enPassantTarget = position.enPassantTarget
        self.attackersToKing = position.board.attackersToKing(for: position.playerTurn)
        self.halfmoves = position.halfmoves
        self.fullmoves = position.fullmoves
    }

    /// Creates a chess game with `moves`.
    ///
    /// - parameter moves: The moves to execute.
    /// - parameter whitePlayer: The game's white player. Default is a nameless human.
    /// - parameter blackPlayer: The game's black player. Default is a nameless human.
    /// - parameter variant: The game's chess variant. Default is standard.
    ///
    /// - throws: `ExecutionError` if any move from `moves` is illegal.
    public convenience init(moves: [Move],
                            whitePlayer: Player = Player(),
                            blackPlayer: Player = Player(),
                            variant: Variant = .standard) throws {
        self.init(whitePlayer: whitePlayer, blackPlayer: blackPlayer, variant: variant)
        for move in moves {
            try execute(move: move)
        }
    }

    /// Returns a copy of `self`.
    ///
    /// - complexity: O(1).
    public func copy() -> Game {
        return Game(game: self)
    }

    /// Returns the captured pieces for a color, or for all if color is `nil`.
    public func capturedPieces(for color: Color? = nil) -> [Piece] {
        let pieces = _moveHistory.compactMap({ $0.capture })
        if let color = color {
            return pieces.filter({ $0.color == color })
        } else {
            return pieces
        }
    }

    /// Returns the moves bitboard currently available for the piece at `square`, if any.
    private func _movesBitboardForPiece(at square: Square, considerHalfmoves: Bool) -> Bitboard {
        if considerHalfmoves && halfmoves >= 100 {
            return 0
        }
        guard let piece = board[square] else { return 0 }
        guard piece.color == playerTurn else { return 0 }
        if kingIsDoubleChecked {
            guard piece.kind.isKing else {
                return 0
            }
        }

        let playerBitboard = board.bitboard(for: playerTurn)
        let enemyBitboard = board.bitboard(for: playerTurn.inverse())
        let allBitboard = playerBitboard | enemyBitboard
        let emptyBitboard = ~allBitboard
        let squareBitboard = Bitboard(square: square)

        var movesBitboard: Bitboard = 0
        let attacks = square.attacks(for: piece, stoppers: allBitboard)

        if piece.kind.isPawn {
            let enPassant = enPassantTarget.map({ Bitboard(square: $0) }) ?? 0
            let pushes = squareBitboard._pawnPushes(for: playerTurn,
                                                    empty: emptyBitboard)
            let doublePushes = (squareBitboard & Bitboard(startFor: piece))
                ._pawnPushes(for: playerTurn, empty: emptyBitboard)
                ._pawnPushes(for: playerTurn, empty: emptyBitboard)
            movesBitboard |= pushes | doublePushes
                | (attacks & enemyBitboard)
                | (attacks & enPassant)
        } else {
            movesBitboard |= attacks & ~playerBitboard
        }

        if piece.kind.isKing && squareBitboard == Bitboard(startFor: piece) && !kingIsChecked {
            rightLoop: for right in castlingRights {
                let emptySquares = right.emptySquares
                guard right.color == playerTurn && allBitboard & emptySquares == 0 else {
                    continue
                }
                for square in emptySquares {
                    guard board.attackers(to: square, color: piece.color.inverse()).isEmpty else {
                        continue rightLoop
                    }
                }
                movesBitboard |= Bitboard(square: right.castleSquare)
            }
        }

        let player = playerTurn
        for moveSquare in movesBitboard {
            try! _execute(uncheckedMove: square >>> moveSquare, promotion: { ._queen })
            if board.attackersToKing(for: player) != 0 {
                movesBitboard[moveSquare] = false
            }
            undoMove()
            _undoHistory.removeLast()
        }

        return movesBitboard
    }

    /// Returns the moves currently available for the piece at `square`, if any.
    private func _movesForPiece(at square: Square, considerHalfmoves flag: Bool) -> [Move] {
        return _movesBitboardForPiece(at: square, considerHalfmoves: flag).moves(from: square)
    }

    /// Returns the available moves for the current player.
    private func _availableMoves(considerHalfmoves flag: Bool) -> [Move] {
        let moves = Square.all.map({ _movesForPiece(at: $0, considerHalfmoves: flag) })
        return Array(moves.joined())
    }

    /// Returns the available moves for the current player.
    public func availableMoves() -> [Move] {
        return _availableMoves(considerHalfmoves: true)
    }

    /// Returns the moves bitboard currently available for the piece at `square`.
    public func movesBitboardForPiece(at square: Square) -> Bitboard {
        return _movesBitboardForPiece(at: square, considerHalfmoves: true)
    }

    /// Returns the moves bitboard currently available for the piece at `location`.
    public func movesBitboardForPiece(at location: Location) -> Bitboard {
        return movesBitboardForPiece(at: Square(location: location))
    }

    /// Returns the moves currently available for the piece at `square`.
    public func movesForPiece(at square: Square) -> [Move] {
        return _movesForPiece(at: square, considerHalfmoves: true)
    }

    /// Returns the moves currently available for the piece at `location`.
    public func movesForPiece(at location: Location) -> [Move] {
        return movesForPiece(at: Square(location: location))
    }

    /// Returns `true` if the move is legal.
    public func isLegal(move: Move) -> Bool {
        let moves = movesBitboardForPiece(at: move.start)
        return Bitboard(square: move.end).intersects(moves)
    }

    @inline(__always)
    private func _execute(uncheckedMove move: Move, promotion: () -> Piece.Kind) throws {
        guard let piece = board[move.start] else {
            throw ExecutionError.missingPiece(move.start)
        }
        var endPiece = piece
        var capture = board[move.end]
        var captureSquare = move.end
        let rights = castlingRights
        if piece.kind.isPawn {
            if move.end.rank == Rank(endFor: playerTurn) {
                let promotion = promotion()
                guard promotion.canPromote() else {
                    throw ExecutionError.invalidPromotion(promotion)
                }
                endPiece = Piece(kind: promotion, color: playerTurn)
            } else if move.end == enPassantTarget {
                capture = Piece(pawn: playerTurn.inverse())
                captureSquare = Square(file: move.end.file, rank: move.start.rank)
            }
        } else if piece.kind.isRook {
            switch move.start {
            case .a1: castlingRights.remove(.whiteQueenside)
            case .h1: castlingRights.remove(.whiteKingside)
            case .a8: castlingRights.remove(.blackQueenside)
            case .h8: castlingRights.remove(.blackKingside)
            default:
                break
            }
        } else if piece.kind.isKing {
            for option in castlingRights where option.color == playerTurn {
                castlingRights.remove(option)
            }
            if move.isCastle(for: playerTurn) {
                let (old, new) = move._castleSquares()
                let rook = Piece(rook: playerTurn)
                board[rook][old] = false
                board[rook][new] = true
            }
        }
        if let capture = capture, capture.kind.isRook {
            switch move.end {
            case .a1 where playerTurn.isBlack: castlingRights.remove(.whiteQueenside)
            case .h1 where playerTurn.isBlack: castlingRights.remove(.whiteKingside)
            case .a8 where playerTurn.isWhite: castlingRights.remove(.blackQueenside)
            case .h8 where playerTurn.isWhite: castlingRights.remove(.blackKingside)
            default:
                break
            }
        }

        _moveHistory.append((move, piece, capture, enPassantTarget, attackersToKing, halfmoves, rights))
        if let capture = capture {
            board[capture][captureSquare] = false
        }
        if capture == nil && !piece.kind.isPawn {
            halfmoves += 1
        } else {
            halfmoves = 0
        }
        board[piece][move.start] = false
        board[endPiece][move.end] = true
        playerTurn.invert()
    }

    /// Executes `move` without checking its legality, updating the state for `self`.
    ///
    /// - warning: Can cause unwanted effects. Should only be used with moves that are known to be legal.
    ///
    /// - parameter move: The move to be executed.
    /// - parameter promotion: A closure returning a promotion piece kind if a pawn promotion occurs.
    ///
    /// - throws: `ExecutionError` if no piece exists at `move.start` or if `promotion` is invalid.
    public func execute(uncheckedMove move: Move, promotion: () -> Piece.Kind) throws {
        try _execute(uncheckedMove: move, promotion: promotion)
        let piece = board[move.end]!
        if piece.kind.isPawn && abs(move.rankChange) == 2 {
            enPassantTarget = Square(file: move.start.file, rank: piece.color.isWhite ? 3 : 6)
        } else {
            enPassantTarget = nil
        }
        if kingIsChecked {
            attackersToKing = 0
        } else {
            attackersToKing = board.attackersToKing(for: playerTurn)
        }

        fullmoves = 1 + (UInt(moveCount) / 2)
        _undoHistory = []
    }

    /// Executes `move` without checking its legality, updating the state for `self`.
    ///
    /// - warning: Can cause unwanted effects. Should only be used with moves that are known to be legal.
    ///
    /// - parameter move: The move to be executed.
    /// - parameter promotion: A piece kind for a pawn promotion.
    ///
    /// - throws: `ExecutionError` if no piece exists at `move.start` or if `promotion` is invalid.
    public func execute(uncheckedMove move: Move, promotion: Piece.Kind) throws {
        try execute(uncheckedMove: move, promotion: { promotion })
    }

    /// Executes `move` without checking its legality, updating the state for `self`.
    ///
    /// - warning: Can cause unwanted effects. Should only be used with moves that are known to be legal.
    ///
    /// - parameter move: The move to be executed.
    ///
    /// - throws: `ExecutionError` if no piece exists at `move.start`.
    public func execute(uncheckedMove move: Move) throws {
        try execute(uncheckedMove: move, promotion: ._queen)
    }

    /// Executes `move`, updating the state for `self`.
    ///
    /// - parameter move: The move to be executed.
    /// - parameter promotion: A closure returning a promotion piece kind if a pawn promotion occurs.
    ///
    /// - throws: `ExecutionError` if `move` is illegal or if `promotion` is invalid.
    public func execute(move: Move, promotion: () -> Piece.Kind) throws {
        guard isLegal(move: move) else {
            throw ExecutionError.illegalMove(move, playerTurn, board)
        }
        try execute(uncheckedMove: move, promotion: promotion)
    }

    /// Executes `move`, updating the state for `self`.
    ///
    /// - parameter move: The move to be executed.
    /// - parameter promotion: A piece kind for a pawn promotion.
    ///
    /// - throws: `ExecutionError` if `move` is illegal or if `promotion` is invalid.
    public func execute(move: Move, promotion: Piece.Kind) throws {
        try execute(move: move, promotion: { promotion })
    }

    /// Executes `move`, updating the state for `self`.
    ///
    /// - parameter move: The move to be executed.
    ///
    /// - throws: `ExecutionError` if `move` is illegal.
    public func execute(move: Move) throws {
        try execute(move: move, promotion: ._queen)
    }
    
    public func execute(move: PGNMove) throws {
        let parsedMove = try PGNParser.parse(move: move, in: position)
        try execute(move: parsedMove, promotion: { move.promotionPiece ?? ._queen })
    }

    /// Returns the last move on the move stack, if any.
    public func moveToUndo() -> Move? {
        return _moveHistory.last?.move
    }

    /// Returns the last move on the undo stack, if any.
    public func moveToRedo() -> Move? {
        return _undoHistory.last?.move
    }

    /// Undoes the previous move and returns it, if any.
    private func _undoMove() -> Move? {
        guard let (move, piece, capture, enPassantTarget, attackers, halfmoves, rights) = _moveHistory.popLast() else {
            return nil
        }
        var captureSquare = move.end
        var promotionKind: Piece.Kind? = nil
        if piece.kind.isPawn {
            if move.end == enPassantTarget {
                captureSquare = Square(file: move.end.file, rank: move.start.rank)
            } else if move.end.rank == Rank(endFor: playerTurn.inverse()), let promotion = board[move.end] {
                promotionKind = promotion.kind
                board[promotion][move.end] = false
            }
        } else if piece.kind.isKing && abs(move.fileChange) == 2 {
            let (old, new) = move._castleSquares()
            let rook = Piece(rook: playerTurn.inverse())
            board[rook][old] = true
            board[rook][new] = false
        }
        if let capture = capture {
            board[capture][captureSquare] = true
        }
        _undoHistory.append((move, promotionKind, attackers))
        board[piece][move.end] = false
        board[piece][move.start] = true
        playerTurn.invert()
        self.enPassantTarget = enPassantTarget
        self.attackersToKing = attackers
        self.fullmoves = 1 + (UInt(moveCount) / 2)
        self.halfmoves = halfmoves
        self.castlingRights = rights
        return move
    }

    /// Redoes the previous undone move and returns it, if any.
    private func _redoMove() -> Move? {
        guard let (move, promotion, attackers) = _undoHistory.popLast() else {
            return nil
        }
        try! _execute(uncheckedMove: move, promotion: { promotion ?? ._queen })
        attackersToKing = attackers
        return move
    }

    /// Undoes the previous move and returns it, if any.
    @discardableResult
    public func undoMove() -> Move? {
        return _undoMove()
    }

    /// Redoes the previous undone move and returns it, if any.
    @discardableResult
    public func redoMove() -> Move? {
        return _redoMove()
    }

}

/// Returns `true` if the outcomes are the same.
public func == (lhs: Game.Outcome, rhs: Game.Outcome) -> Bool {
    return lhs.winColor == rhs.winColor
}

/// Returns `true` if the positions are the same.
public func == (lhs: Game.Position, rhs: Game.Position) -> Bool {
    return lhs.playerTurn == rhs.playerTurn
        && lhs.castlingRights == rhs.castlingRights
        && lhs.halfmoves == rhs.halfmoves
        && lhs.fullmoves == rhs.fullmoves
        && lhs.enPassantTarget == rhs.enPassantTarget
        && lhs.board == rhs.board
}