diff options
Diffstat (limited to 'Sources/SwiftChessNeo/Game.swift')
-rw-r--r-- | Sources/SwiftChessNeo/Game.swift | 825 |
1 files changed, 825 insertions, 0 deletions
diff --git a/Sources/SwiftChessNeo/Game.swift b/Sources/SwiftChessNeo/Game.swift new file mode 100644 index 0000000..b70c0bb --- /dev/null +++ b/Sources/SwiftChessNeo/Game.swift @@ -0,0 +1,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 +} |