From e7e30f219c0129db7cb72f04e200098417ce25d0 Mon Sep 17 00:00:00 2001 From: Navan Chauhan Date: Wed, 17 Apr 2024 12:02:38 -0600 Subject: initial commit --- Sources/SwiftChessNeo/Bitboard.swift | 646 ++++++++++++++++++++++ Sources/SwiftChessNeo/Board.swift | 696 ++++++++++++++++++++++++ Sources/SwiftChessNeo/CastlingRights.swift | 371 +++++++++++++ Sources/SwiftChessNeo/Color.swift | 83 +++ Sources/SwiftChessNeo/File.swift | 190 +++++++ Sources/SwiftChessNeo/Game.swift | 825 +++++++++++++++++++++++++++++ Sources/SwiftChessNeo/InternalTypes.swift | 63 +++ Sources/SwiftChessNeo/Move.swift | 203 +++++++ Sources/SwiftChessNeo/PGN.swift | 454 ++++++++++++++++ Sources/SwiftChessNeo/PGNMove.swift | 219 ++++++++ Sources/SwiftChessNeo/Piece.swift | 297 +++++++++++ Sources/SwiftChessNeo/Player.swift | 82 +++ Sources/SwiftChessNeo/Rank.swift | 147 +++++ Sources/SwiftChessNeo/Sequence+Sage.swift | 33 ++ Sources/SwiftChessNeo/Square.swift | 428 +++++++++++++++ Sources/SwiftChessNeo/Tables.swift | 114 ++++ Sources/SwiftChessNeo/Variant.swift | 46 ++ 17 files changed, 4897 insertions(+) create mode 100644 Sources/SwiftChessNeo/Bitboard.swift create mode 100644 Sources/SwiftChessNeo/Board.swift create mode 100644 Sources/SwiftChessNeo/CastlingRights.swift create mode 100644 Sources/SwiftChessNeo/Color.swift create mode 100644 Sources/SwiftChessNeo/File.swift create mode 100644 Sources/SwiftChessNeo/Game.swift create mode 100644 Sources/SwiftChessNeo/InternalTypes.swift create mode 100644 Sources/SwiftChessNeo/Move.swift create mode 100644 Sources/SwiftChessNeo/PGN.swift create mode 100644 Sources/SwiftChessNeo/PGNMove.swift create mode 100644 Sources/SwiftChessNeo/Piece.swift create mode 100644 Sources/SwiftChessNeo/Player.swift create mode 100644 Sources/SwiftChessNeo/Rank.swift create mode 100644 Sources/SwiftChessNeo/Sequence+Sage.swift create mode 100644 Sources/SwiftChessNeo/Square.swift create mode 100644 Sources/SwiftChessNeo/Tables.swift create mode 100644 Sources/SwiftChessNeo/Variant.swift diff --git a/Sources/SwiftChessNeo/Bitboard.swift b/Sources/SwiftChessNeo/Bitboard.swift new file mode 100644 index 0000000..f8c3dba --- /dev/null +++ b/Sources/SwiftChessNeo/Bitboard.swift @@ -0,0 +1,646 @@ +// +// Bitboard.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 lookup table of least significant bit indices. +private let _lsbTable: [Int] = [00, 01, 48, 02, 57, 49, 28, 03, + 61, 58, 50, 42, 38, 29, 17, 04, + 62, 55, 59, 36, 53, 51, 43, 22, + 45, 39, 33, 30, 24, 18, 12, 05, + 63, 47, 56, 27, 60, 41, 37, 16, + 54, 35, 52, 21, 44, 32, 23, 11, + 46, 26, 40, 15, 34, 20, 31, 10, + 25, 14, 19, 09, 13, 08, 07, 06] + +/// A lookup table of most significant bit indices. +private let _msbTable: [Int] = [00, 47, 01, 56, 48, 27, 02, 60, + 57, 49, 41, 37, 28, 16, 03, 61, + 54, 58, 35, 52, 50, 42, 21, 44, + 38, 32, 29, 23, 17, 11, 04, 62, + 46, 55, 26, 59, 40, 36, 15, 53, + 34, 51, 20, 43, 31, 22, 10, 45, + 25, 39, 14, 33, 19, 30, 09, 24, + 13, 18, 08, 12, 07, 06, 05, 63] + +/// A lookup table of bitboards for all squares. +private let _bitboardTable: [Bitboard] = (0 ..< 64).map { Bitboard(rawValue: 1 << $0) } + +/// The De Bruijn multiplier. +private let _debruijn64: UInt64 = 0x03f79d71b4cb0a89 + +/// Returns the index of the lsb value. +private func _index(lsb value: Bitboard) -> Int? { + guard value != 0 else { + return nil + } + return _lsbTable[Int((value.rawValue &* _debruijn64) >> 58)] +} + +/// Mask for bits not in File A. +private let _notFileA: Bitboard = 0xfefefefefefefefe + +/// Mask for bits not in Files A and B. +private let _notFileAB: Bitboard = 0xfcfcfcfcfcfcfcfc + +/// Mask for bits not in File H. +private let _notFileH: Bitboard = 0x7f7f7f7f7f7f7f7f + +/// Mask for bits not in Files G and H. +private let _notFileGH: Bitboard = 0x3f3f3f3f3f3f3f3f + +/// A bitmap of sixty-four bits suitable for storing squares for various pieces. +/// +/// The first bit refers to `Square.A1` the last (64th) bit refers to `Square.H8`. +/// +/// Due to their compact nature, bitboards can store information such as positions in memory very efficiently. Bitboards +/// can also be used to answer questions about the state of a `Board` quickly with very few operations. +/// +/// Bitboards are used internally within `Board` to store positions for all twelve cases of `Piece`. +/// +/// - see also: [Bitboard (Wikipedia)](https://en.wikipedia.org/wiki/Bitboard ), +/// [Bitboards (Chess Programming Wiki)](https://chessprogramming.org/Bitboards) +public struct Bitboard: RawRepresentable, Hashable, CustomStringConvertible { + + /// A bitboard shift direction. + public enum ShiftDirection { + + /// North direction. + case north + + /// South direction. + case south + + /// East direction. + case east + + /// West direction. + case west + + /// Northeast direction. + case northeast + + /// Southeast direction. + case southeast + + /// Northwest direction. + case northwest + + /// Southwest direction. + case southwest + + /// North regardless of Swift version. + internal static let _north = ShiftDirection.north + + /// South regardless of Swift version. + internal static let _south = ShiftDirection.south + + /// East regardless of Swift version. + internal static let _east = ShiftDirection.east + + /// West regardless of Swift version. + internal static let _west = ShiftDirection.west + + /// Northeast regardless of Swift version. + internal static let _northeast = ShiftDirection.northeast + + /// Southeast regardless of Swift version. + internal static let _southeast = ShiftDirection.southeast + + /// Northwest regardless of Swift version. + internal static let _northwest = ShiftDirection.northwest + + /// Southwest regardless of Swift version. + internal static let _southwest = ShiftDirection.southwest + + } + + /// An iterator for the squares of a `Bitboard`. + public struct Iterator: IteratorProtocol { + + fileprivate var _bitboard: Bitboard + + /// Advances and returns the next element of the underlying sequence, or + /// `nil` if no next element exists. + public mutating func next() -> Square? { + return _bitboard.popLSBSquare() + } + + } + + /// The empty bitset. + public static var allZeros: Bitboard { + return Bitboard(rawValue: 0) + } + + /// The edges of a board. + public static let edges: Bitboard = 0xff818181818181ff + + /// The corresponding value of the "raw" type. + /// + /// `Self(rawValue: self.rawValue)!` is equivalent to `self`. + public var rawValue: UInt64 + + /// A textual representation of `self`. + public var description: String { + let num = String(rawValue, radix: 16) + let str = repeatElement("0", count: 16 - num.count).joined(separator: "") + return "Bitboard(0x\(str + num))" + } + + /// The hash value. + public var hashValue: Int { + return rawValue.hashValue + } + + /// An ASCII art representation of `self`. + /// + /// The ASCII representation for the starting board's bitboard: + /// + /// ``` + /// +-----------------+ + /// 8 | 1 1 1 1 1 1 1 1 | + /// 7 | 1 1 1 1 1 1 1 1 | + /// 6 | . . . . . . . . | + /// 5 | . . . . . . . . | + /// 4 | . . . . . . . . | + /// 3 | . . . . . . . . | + /// 2 | 1 1 1 1 1 1 1 1 | + /// 1 | 1 1 1 1 1 1 1 1 | + /// +-----------------+ + /// a b c d e f g h + /// ``` + public var ascii: String { + let edge = " +-----------------+\n" + var result = edge + let ranks = Rank.all.reversed() + for rank in ranks { + let strings = File.all.map({ file in self[(file, rank)] ? "1" : "." }) + let str = strings.joined(separator: " ") + result += "\(rank) | \(str) |\n" + } + result += "\(edge) a b c d e f g h " + return result + } + + /// The number of bits set in `self`. + public var count: Int { + var n = rawValue + n = n - ((n >> 1) & 0x5555555555555555) + n = (n & 0x3333333333333333) + ((n >> 2) & 0x3333333333333333) + return Int((((n + (n >> 4)) & 0xF0F0F0F0F0F0F0F) &* 0x101010101010101) >> 56) + } + + /// `true` if `self` is empty. + public var isEmpty: Bool { + return self == 0 + } + + /// `self` has more than one bit set. + public var hasMoreThanOne: Bool { + return rawValue & (rawValue &- 1) != 0 + } + + /// The least significant bit. + public var lsb: Bitboard { + return Bitboard(rawValue: rawValue & (0 &- rawValue)) + } + + /// The index for the least significant bit of `self`. + public var lsbIndex: Int? { + return _index(lsb: lsb) + } + + /// The square for the least significant bit of `self`. + public var lsbSquare: Square? { + return lsbIndex.flatMap({ Square(rawValue: $0) }) + } + + private var _msbShifted: UInt64 { + var x = rawValue + x |= x >> 1 + x |= x >> 2 + x |= x >> 4 + x |= x >> 8 + x |= x >> 16 + x |= x >> 32 + return x + } + + /// The most significant bit. + public var msb: Bitboard { + return Bitboard(rawValue: (_msbShifted >> 1) + 1) + } + + /// The index for the most significant bit of `self`. + public var msbIndex: Int? { + guard rawValue != 0 else { + return nil + } + return _msbTable[Int((_msbShifted &* _debruijn64) >> 58)] + } + + /// The square for the most significant bit of `self`. + public var msbSquare: Square? { + return msbIndex.flatMap({ Square(rawValue: $0) }) + } + + /// Convert from a raw value of `UInt64`. + public init(rawValue: UInt64) { + self.rawValue = rawValue + } + + /// Create an empty bitboard. + public init() { + rawValue = 0 + } + + /// Create a starting bitboard for `piece`. + public init(startFor piece: Piece) { + let value: Bitboard + switch piece.kind { + case .pawn: value = 0xFF00 + case .knight: value = 0x0042 + case .bishop: value = 0x0024 + case .rook: value = 0x0081 + case .queen: value = 0x0008 + case .king: value = 0x0010 + } + self = piece.color.isWhite ? value : value << (piece.kind.isPawn ? 40 : 56) + } + + /// Create a bitboard from `squares`. + public init(squares: S) where S.Iterator.Element == Square { + rawValue = squares.reduce(0) { $0 | (1 << UInt64($1.rawValue)) } + } + + /// Create a bitboard from `locations`. + public init(locations: S) where S.Iterator.Element == Location { + self.init(squares: locations.map(Square.init(location:))) + } + + /// Create a bitboard from the start and end of `move`. + public init(move: Move) { + self.init(squares: [move.start, move.end]) + } + + /// Create a bitboard mask for `file`. + public init(file: File) { + switch file { + case .a: rawValue = 0x0101010101010101 + case .b: rawValue = 0x0202020202020202 + case .c: rawValue = 0x0404040404040404 + case .d: rawValue = 0x0808080808080808 + case .e: rawValue = 0x1010101010101010 + case .f: rawValue = 0x2020202020202020 + case .g: rawValue = 0x4040404040404040 + case .h: rawValue = 0x8080808080808080 + } + } + + /// Create a bitboard mask for `rank`. + public init(rank: Rank) { + rawValue = 0xFF << (UInt64(rank.index) * 8) + } + + /// Create a bitboard mask for `square`. + /// + /// - complexity: O(1). + public init(square: Square) { + self = _bitboardTable[square.rawValue] + } + + /// The `Bool` value for the bit at `square`. + /// + /// - complexity: O(1). + public subscript(square: Square) -> Bool { + get { + return intersects(_bitboardTable[square.rawValue]) + } + set { + let bit = Bitboard(square: square) + if newValue { + rawValue |= bit.rawValue + } else { + rawValue &= ~bit.rawValue + } + } + } + + /// The `Bool` value for the bit at `location`. + /// + /// - complexity: O(1). + public subscript(location: Location) -> Bool { + get { + return self[Square(location: location)] + } + set { + self[Square(location: location)] = newValue + } + } + + /// Returns the pawn pushes available for `color` in `self`. + internal func _pawnPushes(for color: Color, empty: Bitboard) -> Bitboard { + return (color.isWhite ? shifted(toward: ._north) : shifted(toward: ._south)) & empty + } + + /// Returns the attacks available to the pawns for `color` in `self`. + internal func _pawnAttacks(for color: Color) -> Bitboard { + if color.isWhite { + return shifted(toward: ._northeast) | shifted(toward: ._northwest) + } else { + return shifted(toward: ._southeast) | shifted(toward: ._southwest) + } + } + + /// Returns the attacks available to the knight in `self`. + internal func _knightAttacks() -> Bitboard { + let x = self + let a = ((x << 17) | (x >> 15)) & _notFileA + let b = ((x << 10) | (x >> 06)) & _notFileAB + let c = ((x << 15) | (x >> 17)) & _notFileH + let d = ((x << 06) | (x >> 10)) & _notFileGH + return a | b | c | d + } + + /// Returns the attacks available to the bishop in `self`. + internal func _bishopAttacks(stoppers bitboard: Bitboard = 0) -> Bitboard { + return filled(toward: ._northeast, stoppers: bitboard).shifted(toward: ._northeast) + | filled(toward: ._northwest, stoppers: bitboard).shifted(toward: ._northwest) + | filled(toward: ._southeast, stoppers: bitboard).shifted(toward: ._southeast) + | filled(toward: ._southwest, stoppers: bitboard).shifted(toward: ._southwest) + } + + /// Returns the attacks available to the rook in `self`. + internal func _rookAttacks(stoppers bitboard: Bitboard = 0) -> Bitboard { + return filled(toward: ._north, stoppers: bitboard).shifted(toward: ._north) + | filled(toward: ._south, stoppers: bitboard).shifted(toward: ._south) + | filled(toward: ._east, stoppers: bitboard).shifted(toward: ._east) + | filled(toward: ._west, stoppers: bitboard).shifted(toward: ._west) + } + + /// Returns the x-ray attacks available to the bishop in `self`. + internal func _xrayBishopAttacks(occupied occ: Bitboard, stoppers: Bitboard) -> Bitboard { + let attacks = _bishopAttacks(stoppers: occ) + return attacks ^ _bishopAttacks(stoppers: (stoppers & attacks) ^ stoppers) + } + + /// Returns the x-ray attacks available to the rook in `self`. + internal func _xrayRookAttacks(occupied occ: Bitboard, stoppers: Bitboard) -> Bitboard { + let attacks = _rookAttacks(stoppers: occ) + return attacks ^ _rookAttacks(stoppers: (stoppers & attacks) ^ stoppers) + } + + /// Returns the attacks available to the queen in `self`. + internal func _queenAttacks(stoppers bitboard: Bitboard = 0) -> Bitboard { + return _rookAttacks(stoppers: bitboard) | _bishopAttacks(stoppers: bitboard) + } + + /// Returns the attacks available to the king in `self`. + internal func _kingAttacks() -> Bitboard { + let attacks = shifted(toward: ._east) | shifted(toward: ._west) + let bitboard = self | attacks + return attacks + | bitboard.shifted(toward: ._north) + | bitboard.shifted(toward: ._south) + } + + /// Returns the attacks available to `piece` in `self`. + internal func _attacks(for piece: Piece, stoppers: Bitboard = 0) -> Bitboard { + switch piece.kind { + case .pawn: + return _pawnAttacks(for: piece.color) + case .knight: + return _knightAttacks() + case .bishop: + return _bishopAttacks(stoppers: stoppers) + case .rook: + return _rookAttacks(stoppers: stoppers) + case .queen: + return _queenAttacks(stoppers: stoppers) + case .king: + return _kingAttacks() + } + } + + /// Returns `true` if `self` intersects `other`. + public func intersects(_ other: Bitboard) -> Bool { + return rawValue & other.rawValue != 0 + } + + /// Returns `self` flipped horizontally. + public func flippedHorizontally() -> Bitboard { + let x = 0x5555555555555555 as Bitboard + let y = 0x3333333333333333 as Bitboard + let z = 0x0F0F0F0F0F0F0F0F as Bitboard + var n = self + n = ((n >> 1) & x) | ((n & x) << 1) + n = ((n >> 2) & y) | ((n & y) << 2) + n = ((n >> 4) & z) | ((n & z) << 4) + return n + } + + /// Returns `self` flipped vertically. + public func flippedVertically() -> Bitboard { + let x = 0x00FF00FF00FF00FF as Bitboard + let y = 0x0000FFFF0000FFFF as Bitboard + var n = self + n = ((n >> 8) & x) | ((n & x) << 8) + n = ((n >> 16) & y) | ((n & y) << 16) + n = (n >> 32) | (n << 32) + return n + } + + /// Returns the bits of `self` filled toward `direction` stopped by `stoppers`. + public func filled(toward direction: ShiftDirection, stoppers: Bitboard) -> Bitboard { + let empty = ~stoppers + var bitboard = self + for _ in 0 ..< 7 { + bitboard |= empty & bitboard.shifted(toward: direction) + } + return bitboard + } + + /// Returns the bits of `self` shifted once toward `direction`. + public func shifted(toward direction: ShiftDirection) -> Bitboard { + switch direction { + case .north: return self << 8 + case .south: return self >> 8 + case .east: return (self << 1) & _notFileA + case .northeast: return (self << 9) & _notFileA + case .southeast: return (self >> 7) & _notFileA + case .west: return (self >> 1) & _notFileH + case .southwest: return (self >> 9) & _notFileH + case .northwest: return (self << 7) & _notFileH + } + } + + /// Flips `self` horizontally. + public mutating func flipHorizontally() { + self = flippedHorizontally() + } + + /// Flips `self` vertically. + public mutating func flipVertically() { + self = flippedVertically() + } + + /// Shifts the bits of `self` once toward `direction`. + public mutating func shift(toward direction: ShiftDirection) { + self = shifted(toward: direction) + } + + /// Fills the bits of `self` toward `direction` stopped by `stoppers`. + public mutating func fill(toward direction: ShiftDirection, stoppers: Bitboard = 0) { + self = filled(toward: direction, stoppers: stoppers) + } + + /// Swaps the bits between the two squares. + public mutating func swap(_ first: Square, _ second: Square) { + (self[first], self[second]) = (self[second], self[first]) + } + + /// Removes the least significant bit and returns it. + public mutating func popLSB() -> Bitboard { + let lsb = self.lsb + rawValue -= lsb.rawValue + return lsb + } + + /// Removes the least significant bit and returns its index, if any. + public mutating func popLSBIndex() -> Int? { + return _index(lsb: popLSB()) + } + + /// Removes the least significant bit and returns its square, if any. + public mutating func popLSBSquare() -> Square? { + return popLSBIndex().flatMap({ Square(rawValue: $0) }) + } + + /// Removes the most significant bit and returns it. + public mutating func popMSB() -> Bitboard { + let msb = self.msb + rawValue -= msb.rawValue + return msb + } + + /// Removes the most significant bit and returns its index, if any. + public mutating func popMSBIndex() -> Int? { + guard rawValue != 0 else { return nil } + let shifted = _msbShifted + rawValue -= (shifted >> 1) + 1 + return _msbTable[Int((shifted &* _debruijn64) >> 58)] + } + + /// Removes the most significant bit and returns its square, if any. + public mutating func popMSBSquare() -> Square? { + return popMSBIndex().flatMap({ Square(rawValue: $0) }) + } + + /// Returns the ranks of `self` as eight 8-bit integers. + public func ranks() -> [UInt8] { + return (0 ..< 8).map { UInt8((rawValue >> ($0 * 8)) & 255) } + } + +} + +extension Bitboard: Sequence { + + /// A value less than or equal to the number of elements in + /// the sequence, calculated nondestructively. + /// + /// - complexity: O(1). + public var underestimatedCount: Int { + return count + } + + /// Returns a Boolean value indicating whether the sequence contains the + /// given element. + /// + /// - complexity: O(1). + public func contains(_ element: Square) -> Bool { + return self[element] + } + + /// Returns an iterator over the squares of the board. + public func makeIterator() -> Iterator { + return Iterator(_bitboard: self) + } + +} + +extension Bitboard: ExpressibleByIntegerLiteral { + + /// Create an instance initialized to `value`. + public init(integerLiteral value: UInt64) { + rawValue = value + } +} + +/// Returns the intersection of bits set in `lhs` and `rhs`. +/// +/// - complexity: O(1). +public func & (lhs: Bitboard, rhs: Bitboard) -> Bitboard { + return Bitboard(rawValue: lhs.rawValue & rhs.rawValue) +} + +/// Returns the union of bits set in `lhs` and `rhs`. +/// +/// - complexity: O(1). +public func | (lhs: Bitboard, rhs: Bitboard) -> Bitboard { + return Bitboard(rawValue: lhs.rawValue | rhs.rawValue) +} + +/// Stores the result of performing a bitwise OR operation on the two given values in the left-hand-side variable. +public func |= (lhs: inout Bitboard, rhs: Bitboard) { + lhs.rawValue |= rhs.rawValue +} + +/// Returns the bits that are set in exactly one of `lhs` and `rhs`. +/// +/// - complexity: O(1). +public func ^ (lhs: Bitboard, rhs: Bitboard) -> Bitboard { + return Bitboard(rawValue: lhs.rawValue ^ rhs.rawValue) +} + +/// Returns `x ^ ~Self.allZeros`. +/// +/// - complexity: O(1). +public prefix func ~ (x: Bitboard) -> Bitboard { + return Bitboard(rawValue: ~x.rawValue) +} + +/// Returns the bits of `lhs` shifted right by `rhs`. +public func >> (lhs: Bitboard, rhs: Bitboard) -> Bitboard { + return Bitboard(rawValue: lhs.rawValue >> rhs.rawValue) +} + +/// Returns the bits of `lhs` shifted left by `rhs`. +public func << (lhs: Bitboard, rhs: Bitboard) -> Bitboard { + return Bitboard(rawValue: lhs.rawValue << rhs.rawValue) +} + +/// Shifts the bits of `lhs` right by `rhs`. +public func >>= (lhs: inout Bitboard, rhs: Bitboard) { + lhs.rawValue >>= rhs.rawValue +} + +/// Shifts the bits of `lhs` left by `rhs`. +public func <<= (lhs: inout Bitboard, rhs: Bitboard) { + lhs.rawValue <<= rhs.rawValue +} diff --git a/Sources/SwiftChessNeo/Board.swift b/Sources/SwiftChessNeo/Board.swift new file mode 100644 index 0000000..d9d9b8f --- /dev/null +++ b/Sources/SwiftChessNeo/Board.swift @@ -0,0 +1,696 @@ +// +// Board.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. +// + +#if os(OSX) + import Cocoa +#elseif os(iOS) || os(tvOS) + import UIKit +#endif + +/// A chess board used to map `Square`s to `Piece`s. +/// +/// Pieces map to separate instances of `Bitboard` which can be retrieved with `bitboard(for:)`. +public struct Board: Hashable, CustomStringConvertible { + + /// A chess board space. + public struct Space: Hashable, CustomStringConvertible { + + /// The occupying chess piece. + public var piece: Piece? + + /// The space's file. + public var file: File + + /// The space's rank. + public var rank: Rank + + /// The space's location on a chess board. + public var location: Location { + get { + return (file, rank) + } + set { + (file, rank) = newValue + } + } + + /// The space's square on a chess board. + public var square: Square { + get { + return Square(file: file, rank: rank) + } + set { + location = newValue.location + } + } + + /// The space's color. + public var color: Color { + return (file.index & 1 != rank.index & 1) ? .white : .black + } + + /// The space's name. + public var name: String { + return "\(file.character)\(rank.rawValue)" + } + + /// A textual representation of `self`. + public var description: String { + return "Space(\(name), \(piece._altDescription))" + } + + /// The hash value. +// public var hashValue: Int { +// let pieceHash = piece?.hashValue ?? (6 << 1) +// let fileHash = file.hashValue << 4 +// let rankHash = rank.hashValue << 7 +// return pieceHash + fileHash + rankHash +// } + + public func hash(into hasher: inout Hasher) { + hasher.combine(piece) + hasher.combine(file) + hasher.combine(rank) + } + + /// Create a chess board space with a piece, file, and rank. + public init(piece: Piece? = nil, file: File, rank: Rank) { + self.init(piece: piece, location: (file, rank)) + } + + /// Create a chess board space with a piece and location. + public init(piece: Piece? = nil, location: Location) { + self.piece = piece + (file, rank) = location + } + + /// Create a chess board space with a piece and square. + public init(piece: Piece? = nil, square: Square) { + self.piece = piece + (file, rank) = square.location + } + + /// Clears the piece from the space and returns it. + public mutating func clear() -> Piece? { + let piece = self.piece + self.piece = nil + return piece + } + + #if os(OSX) || os(iOS) || os(tvOS) + + internal func _view(size: CGFloat) -> _View { + #if os(OSX) + let rectY = CGFloat(rank.index) * size + #else + let rectY = CGFloat(7 - rank.index) * size + #endif + let frame = CGRect(x: CGFloat(file.index) * size, + y: rectY, + width: size, + height: size) + var textFrame = CGRect(x: 0, y: 0, width: size, height: size) + let fontSize = size * 0.625 + let view = _View(frame: frame) + let str = piece.map({ String($0.specialCharacter(background: color)) }) ?? "" + let white = _Color.white + let black = _Color.black + let bg: _Color = color.isWhite ? white : black + let tc: _Color = color.isWhite ? black : white + #if os(OSX) + view.wantsLayer = true + let text = NSText(frame: textFrame) + view.layer?.backgroundColor = bg.cgColor + text.alignment = .center + text.font = .systemFont(ofSize: fontSize) + text.isEditable = false + text.isSelectable = false + text.string = str + text.drawsBackground = false + text.textColor = tc + view.addSubview(text) + #else + view.backgroundColor = bg + let label = UILabel(frame: textFrame) + label.textAlignment = .center + label.font = .systemFont(ofSize: fontSize) + label.text = str + label.textColor = tc + view.addSubview(label) + #endif + return view + } + + #endif + + } + + /// An iterator for the spaces of a chess board. + public struct Iterator: IteratorProtocol { + + let _board: Board + + var _index: Int + + fileprivate init(_ board: Board) { + self._board = board + self._index = 0 + } + + /// Advances to the next space on the board and returns it. + public mutating func next() -> Board.Space? { + guard let square = Square(rawValue: _index) else { + return nil + } + defer { _index += 1 } + return _board.space(at: square) + } + + } + + /// A board side. + public enum Side { + + /// Right side of the board. + case kingside + + /// Right side of the board. + case queenside + + /// `self` is kingside. + public var isKingside: Bool { + return self == .kingside + } + + /// `self` is queenside. + public var isQueenside: Bool { + return self == .queenside + } + + } + + /// The bitboards of `self`. + internal var _bitboards: [Bitboard] + + /// The board's pieces. + public var pieces: [Piece] { + return self.compactMap({ $0.piece }) + } + + /// The board's white pieces. + public var whitePieces: [Piece] { + return pieces.filter({ $0.color.isWhite }) + } + + /// The board's black pieces. + public var blackPieces: [Piece] { + return pieces.filter({ $0.color.isBlack }) + } + + /// A bitboard for the occupied spaces of `self`. + public var occupiedSpaces: Bitboard { + return _bitboards.reduce(0, |) + } + + /// A bitboard for the empty spaces of `self`. + public var emptySpaces: Bitboard { + return ~occupiedSpaces + } + + /// A textual representation of `self`. + public var description: String { + return "Board(\(fen()))" + } + + /// The hash value. + public func hash(into hasher: inout Hasher) { + _bitboards.forEach { hasher.combine($0) } + } + + /// An ASCII art representation of `self`. + /// + /// The ASCII representation for the starting board: + /// + /// ``` + /// +-----------------+ + /// 8 | r n b q k b n r | + /// 7 | p p p p p p p p | + /// 6 | . . . . . . . . | + /// 5 | . . . . . . . . | + /// 4 | . . . . . . . . | + /// 3 | . . . . . . . . | + /// 2 | P P P P P P P P | + /// 1 | R N B Q K B N R | + /// +-----------------+ + /// a b c d e f g h + /// ``` + public var ascii: String { + let edge = " +-----------------+\n" + var result = edge + let reversed = Rank.all.reversed() + for rank in reversed { + let strings = File.all.map({ file in "\(self[(file, rank)]?.character ?? ".")" }) + let str = strings.joined(separator: " ") + result += "\(rank) | \(str) |\n" + } + result += "\(edge) a b c d e f g h " + return result + } + + /// Create a chess board. + /// + /// - parameter variant: The variant to populate the board for. Won't populate if `nil`. Default is `Standard`. + public init(variant: Variant? = .standard) { + _bitboards = Array(repeating: 0, count: 12) + if let variant = variant { + for piece in Piece.all { + _bitboards[piece.rawValue] = Bitboard(startFor: piece) + } + if variant.isUpsideDown { + for index in _bitboards.indices { + _bitboards[index].flipVertically() + } + } + } + } + + /// Create a chess board from a valid FEN string. + /// + /// - Warning: Only to be used with the board part of a full 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) { + func pieces(for string: String) -> [Piece?]? { + var pieces: [Piece?] = [] + for char in string { + guard pieces.count < 8 else { + return nil + } + if let piece = Piece(character: char) { + pieces.append(piece) + } else if let num = Int(String(char)) { + guard 1...8 ~= num else { return nil } + pieces += Array(repeating: nil, count: num) + } else { + return nil + } + } + return pieces + } + guard !fen.contains(" ") else { + return nil + } + let parts = fen.split(separator: "/").map(String.init) + let ranks = Rank.all.reversed() + guard parts.count == 8 else { + return nil + } + var board = Board(variant: nil) + for (rank, part) in zip(ranks, parts) { + guard let pieces = pieces(for: part) else { + return nil + } + for (file, piece) in zip(File.all, pieces) { + board[(file, rank)] = piece + } + } + self = board + } + + /// Create a chess board from arrays of piece characters. + /// + /// Returns `nil` if a piece can't be initialized from a character. Characters beyond the 8x8 area are ignored. + /// Empty spaces are denoted with a whitespace or period. + /// + /// ```swift + /// Board(pieces: [["r", "n", "b", "q", "k", "b", "n", "r"], + /// ["p", "p", "p", "p", "p", "p", "p", "p"], + /// [" ", " ", " ", " ", " ", " ", " ", " "], + /// [" ", " ", " ", " ", " ", " ", " ", " "], + /// [" ", " ", " ", " ", " ", " ", " ", " "], + /// [" ", " ", " ", " ", " ", " ", " ", " "], + /// ["P", "P", "P", "P", "P", "P", "P", "P"], + /// ["R", "N", "B", "Q", "K", "B", "N", "R"]]) + /// ``` + public init?(pieces: [[Character]]) { + self.init(variant: nil) + for rankIndex in pieces.indices { + guard let rank = Rank(index: rankIndex)?.opposite() else { break } + for fileIndex in pieces[rankIndex].indices { + guard let file = File(index: fileIndex) else { break } + let pieceChar = pieces[rankIndex][fileIndex] + if pieceChar != " " && pieceChar != "." { + guard let piece = Piece(character: pieceChar) else { return nil } + self[(file, rank)] = piece + } + } + } + } + + /// Gets and sets a piece at `location`. + public subscript(location: Location) -> Piece? { + get { + return self[Square(location: location)] + } + set { + self[Square(location: location)] = newValue + } + } + + /// Gets and sets a piece at `square`. + public subscript(square: Square) -> Piece? { + get { + for index in _bitboards.indices { + if _bitboards[index][square] { + return Piece(value: index) + } + } + return nil + } + set { + for index in _bitboards.indices { + _bitboards[index][square] = false + } + if let piece = newValue { + self[piece][square] = true + } + } + } + + /// Gets and sets the bitboard for `piece`. + internal subscript(piece: Piece) -> Bitboard { + get { + return _bitboards[piece.rawValue] + } + set { + _bitboards[piece.rawValue] = newValue + } + } + + /// Returns `self` flipped horizontally. + public func flippedHorizontally() -> Board { + var board = self + for index in _bitboards.indices { + board._bitboards[index].flipHorizontally() + } + return board + } + + /// Returns `self` flipped vertically. + public func flippedVertically() -> Board { + var board = self + for index in _bitboards.indices { + board._bitboards[index].flipVertically() + } + return board + } + + /// Clears all the pieces from `self`. + public mutating func clear() { + self = Board(variant: nil) + } + + /// Populates `self` with all of the pieces at their proper locations for the given chess variant. + public mutating func populate(for variant: Variant = .standard) { + self = Board(variant: variant) + } + + /// Flips `self` horizontally. + public mutating func flipHorizontally() { + self = flippedHorizontally() + } + + /// Flips `self` vertically. + public mutating func flipVertically() { + self = flippedVertically() + } + + /// Returns the number of pieces for `color`, or all if `nil`. + public func pieceCount(for color: Color? = nil) -> Int { + if let color = color { + return bitboard(for: color).count + } else { + return _bitboards.reduce(0) { $0 + $1.count } + } + } + + /// Returns the number of `piece` in `self`. + public func count(of piece: Piece) -> Int { + return bitboard(for: piece).count + } + + /// Returns the bitboard for `piece`. + public func bitboard(for piece: Piece) -> Bitboard { + return self[piece] + } + + /// Returns the bitboard for `color`. + public func bitboard(for color: Color) -> Bitboard { + return Piece._hashes(for: color).reduce(0) { $0 | _bitboards[$1] } + } + + /// The squares with pinned pieces for `color`. + public func pinned(for color: Color) -> Bitboard { + guard let kingSquare = squareForKing(for: color) else { + return 0 + } + let occupied = occupiedSpaces + var pinned = Bitboard() + let pieces = bitboard(for: color) + let king = bitboard(for: Piece(king: color)) + let opRQ = bitboard(for: Piece(rook: color.inverse())) | bitboard(for: Piece(queen: color.inverse())) + let opBQ = bitboard(for: Piece(bishop: color.inverse())) | bitboard(for: Piece(queen: color.inverse())) + for square in king._xrayRookAttacks(occupied: occupied, stoppers: pieces) & opRQ { + pinned |= square.between(kingSquare) & pieces + } + for square in king._xrayBishopAttacks(occupied: occupied, stoppers: pieces) & opBQ { + pinned |= square.between(kingSquare) & pieces + } + return pinned + } + + /// Returns the attackers to `square` corresponding to `color`. + /// + /// - parameter square: The `Square` being attacked. + /// - parameter color: The `Color` of the attackers. + public func attackers(to square: Square, color: Color) -> Bitboard { + let all = occupiedSpaces + let attackPieces = Piece._nonQueens(for: color) + let playerPieces = Piece._nonQueens(for: color.inverse()) + let attacks = playerPieces.map({ piece in + square.attacks(for: piece, stoppers: all) + }) + let queens = (attacks[2] | attacks[3]) & self[Piece(queen: color)] + return zip(attackPieces, attacks).reduce(queens) { $0 | (self[$1.0] & $1.1) } + } + + /// Returns the attackers to the king for `color`. + /// + /// - parameter color: The `Color` of the potentially attacked king. + /// + /// - returns: A bitboard of all attackers, or 0 if the king does not exist or if there are no pieces attacking the + /// king. + public func attackersToKing(for color: Color) -> Bitboard { + guard let square = squareForKing(for: color) else { + return 0 + } + return attackers(to: square, color: color.inverse()) + } + + /// Returns `true` if the king for `color` is in check. + public func kingIsChecked(for color: Color) -> Bool { + return attackersToKing(for: color) != 0 + } + + /// Returns the spaces at `file`. + public func spaces(at file: File) -> [Space] { + return Rank.all.map { space(at: (file, $0)) } + } + + /// Returns the spaces at `rank`. + public func spaces(at rank: Rank) -> [Space] { + return File.all.map { space(at: ($0, rank)) } + } + + /// Returns the space at `location`. + public func space(at location: Location) -> Space { + return Space(piece: self[location], location: location) + } + + /// Returns the square at `location`. + public func space(at square: Square) -> Space { + return Space(piece: self[square], square: square) + } + + /// Removes a piece at `square`, and returns it. + @discardableResult + public mutating func removePiece(at square: Square) -> Piece? { + if let piece = self[square] { + self[piece][square] = false + return piece + } else { + return nil + } + } + + /// Removes a piece at `location`, and returns it. + @discardableResult + public mutating func removePiece(at location: Location) -> Piece? { + return removePiece(at: Square(location: location)) + } + + /// Swaps the pieces between the two locations. + public mutating func swap(_ first: Location, _ second: Location) { + swap(Square(location: first), Square(location: second)) + } + + /// Swaps the pieces between the two squares. + public mutating func swap(_ first: Square, _ second: Square) { + switch (self[first], self[second]) { + case let (firstPiece?, secondPiece?): + self[firstPiece].swap(first, second) + self[secondPiece].swap(first, second) + case let (firstPiece?, nil): + self[firstPiece].swap(first, second) + case let (nil, secondPiece?): + self[secondPiece].swap(first, second) + default: + break + } + } + + /// Returns the locations where `piece` exists. + public func locations(for piece: Piece) -> [Location] { + return bitboard(for: piece).map({ $0.location }) + } + + /// Returns the squares where `piece` exists. + public func squares(for piece: Piece) -> [Square] { + return Array(bitboard(for: piece)) + } + + /// Returns the squares where pieces for `color` exist. + public func squares(for color: Color) -> [Square] { + return Array(bitboard(for: color)) + } + + /// Returns the square of the king for `color`, if any. + public func squareForKing(for color: Color) -> Square? { + return bitboard(for: Piece(king: color)).lsbSquare + } + + /// Returns `true` if `self` contains `piece`. + public func contains(_ piece: Piece) -> Bool { + return !self[piece].isEmpty + } + + /// Returns the FEN string for the board. + /// + /// - see also: [FEN (Wikipedia)](https://en.wikipedia.org/wiki/Forsyth%E2%80%93Edwards_Notation), + /// [FEN (Chess Programming Wiki)](https://www.chessprogramming.org/Forsyth-Edwards_Notation) + public func fen() -> String { + func fen(forRank rank: Rank) -> String { + var fen = "" + var accumulator = 0 + for space in spaces(at: rank) { + if let piece = space.piece { + if accumulator > 0 { + fen += String(accumulator) + accumulator = 0 + } + fen += String(piece.character) + } else { + accumulator += 1 + if space.file == ._h { + fen += String(accumulator) + } + } + } + return fen + } + return Rank.all.reversed().map(fen).joined(separator: "/") + } + + public func toArray() -> [Board.Space] { + var array = [Board.Space]() + for space in self { + array.append(space) + } + + return array.reversed() + } + +} + +extension Board: Sequence { + + /// A value less than or equal to the number of elements in + /// the sequence, calculated nondestructively. + /// + /// - Complexity: O(1). + public var underestimatedCount: Int { + return 64 + } + + /// Returns an iterator over the spaces of the board. + public func makeIterator() -> Iterator { + return Iterator(self) + } + +} + +#if os(OSX) || os(iOS) || os(tvOS) + +extension Board: CustomPlaygroundDisplayConvertible { + + /// Returns the `playgroundDescription` for `self`. + private var _playgroundDescription: _View { + let spaceSize: CGFloat = 80 + let boardSize = spaceSize * 8 + let frame = CGRect(x: 0, y: 0, width: boardSize, height: boardSize) + let view = _View(frame: frame) + + for space in self { + view.addSubview(space._view(size: spaceSize)) + } + return view + } + + /// A custom playground description for this instance. + public var playgroundDescription: Any { + return _playgroundDescription + } + +} + +#endif + +/// Returns `true` if both boards are the same. +public func == (lhs: Board, rhs: Board) -> Bool { + return lhs._bitboards == rhs._bitboards +} + +/// Returns `true` if both spaces are the same. +public func == (lhs: Board.Space, rhs: Board.Space) -> Bool { + return lhs.piece == rhs.piece + && lhs.file == rhs.file + && lhs.rank == rhs.rank +} diff --git a/Sources/SwiftChessNeo/CastlingRights.swift b/Sources/SwiftChessNeo/CastlingRights.swift new file mode 100644 index 0000000..fc22c86 --- /dev/null +++ b/Sources/SwiftChessNeo/CastlingRights.swift @@ -0,0 +1,371 @@ +// +// CastlingRights.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. +// + +/// Castling rights of a `Game`. +/// +/// Defines whether a `Color` has the right to castle for a `Board.Side`. +public struct CastlingRights: CustomStringConvertible { + + /// A castling right. + public enum Right: String, CustomStringConvertible { + + /// White can castle kingside. + case whiteKingside + + /// White can castle queenside. + case whiteQueenside + + /// Black can castle kingside. + case blackKingside + + /// Black can castle queenside. + case blackQueenside + + /// All rights. + public static let all: [Right] = [.whiteKingside, .whiteQueenside, .blackKingside, .blackQueenside] + + /// White rights. + public static let white: [Right] = all.filter({ $0.color.isWhite }) + + /// Black rights. + public static let black: [Right] = all.filter({ $0.color.isBlack }) + + /// Kingside rights. + public static let kingside: [Right] = all.filter({ $0.side.isKingside }) + + /// Queenside rights. + public static let queenside: [Right] = all.filter({ $0.side.isQueenside }) + + /// The color for `self`. + public var color: Color { + get { + switch self { + case .whiteKingside, .whiteQueenside: + return .white + default: + return .black + } + } + set { + self = Right(color: newValue, side: side) + } + } + + /// The board side for `self`. + public var side: Board.Side { + get { + switch self { + case .whiteKingside, .blackKingside: + return .kingside + default: + return .queenside + } + } + set { + self = Right(color: color, side: newValue) + } + } + + /// The squares expected to be empty for a castle. + public var emptySquares: Bitboard { + switch self { + case .whiteKingside: + return 0b01100000 + case .whiteQueenside: + return 0b00001110 + case .blackKingside: + return 0b01100000 << 56 + case .blackQueenside: + return 0b00001110 << 56 + } + } + + /// The castle destination square of a king. + public var castleSquare: Square { + switch self { + case .whiteKingside: + return .g1 + case .whiteQueenside: + return .c1 + case .blackKingside: + return .g8 + case .blackQueenside: + return .c8 + } + } + + /// The character for `self`. + public var character: Character { + switch self { + case .whiteKingside: return "K" + case .whiteQueenside: return "Q" + case .blackKingside: return "k" + case .blackQueenside: return "q" + } + } + + /// A textual representation of `self`. + public var description: String { + return rawValue + } + + fileprivate var _bit: Int { + switch self { + case .whiteKingside: return 0b0001 + case .whiteQueenside: return 0b0010 + case .blackKingside: return 0b0100 + case .blackQueenside: return 0b1000 + } + } + + /// Create a `Right` from `color` and `side`. + public init(color: Color, side: Board.Side) { + switch (color, side) { + case (.white, .kingside): self = .whiteKingside + case (.white, .queenside): self = .whiteQueenside + case (.black, .kingside): self = .blackKingside + case (.black, .queenside): self = .blackQueenside + } + } + + /// Create a `Right` from a `Character`. + public init?(character: Character) { + switch character { + case "K": self = .whiteKingside + case "Q": self = .whiteQueenside + case "k": self = .blackKingside + case "q": self = .blackQueenside + default: return nil + } + } + + } + + /// An iterator over the members of `CastlingRights`. + public struct Iterator: IteratorProtocol { + + fileprivate var _base: SetIterator + + /// Advance to the next element and return it, or `nil` if no next element exists. + public mutating func next() -> Right? { + return _base.next() + } + + } + + /// All castling rights. + public static let all = CastlingRights(Right.all) + + /// White castling rights. + public static let white = CastlingRights(Right.white) + + /// Black castling rights. + public static let black = CastlingRights(Right.black) + + /// Kingside castling rights. + public static let kingside = CastlingRights(Right.kingside) + + /// Queenside castling rights. + public static let queenside = CastlingRights(Right.queenside) + + /// The rights. + fileprivate var _rights: Set + + /// A textual representation of `self`. + public var description: String { + if !_rights.isEmpty { + return String(_rights.map({ $0.character }).sorted()) + } else { + return "-" + } + } + + /// Creates empty rights. + public init() { + _rights = Set() + } + + /// Creates a `CastlingRights` from a `String`. + /// + /// - returns: `nil` if `string` is empty or invalid. + public init?(string: String) { + guard !string.isEmpty else { + return nil + } + if string == "-" { + _rights = Set() + } else { + var rights = Set() + for char in string { + guard let right = Right(character: char) else { + return nil + } + rights.insert(right) + } + _rights = rights + } + } + + /// Creates castling rights for `color`. + public init(color: Color) { + self = color.isWhite ? .white : .black + } + + /// Creates castling rights for `side`. + public init(side: Board.Side) { + self = side.isKingside ? .kingside : .queenside + } + + /// Creates a set of rights from a sequence. + public init(_ sequence: S) where S.Iterator.Element == Right { + if let set = sequence as? Set { + _rights = set + } else { + _rights = Set(sequence) + } + } + + /// Returns `true` if `self` can castle for `color`. + public func canCastle(for color: Color) -> Bool { + return !self.intersection(CastlingRights(color: color)).isEmpty + } + + /// Returns `true` if `self` can castle for `side`. + public func canCastle(for side: Board.Side) -> Bool { + return !self.intersection(CastlingRights(side: side)).isEmpty + } + +} + +extension CastlingRights: Sequence { + + /// Returns an iterator over the members. + public func makeIterator() -> Iterator { + return Iterator(_base: _rights.makeIterator()) + } + +} + +extension CastlingRights: SetAlgebra { + + /// A Boolean value that indicates whether the set has no elements. + public var isEmpty: Bool { + return _rights.isEmpty + } + + /// Returns a Boolean value that indicates whether the given element exists + /// in the set. + public func contains(_ member: Right) -> Bool { + return _rights.contains(member) + } + + /// Returns a new set with the elements of both this and the given set. + public func union(_ other: CastlingRights) -> CastlingRights { + return CastlingRights(_rights.union(other._rights)) + } + + /// Returns a new set with the elements that are common to both this set and + /// the given set. + public func intersection(_ other: CastlingRights) -> CastlingRights { + return CastlingRights(_rights.intersection(other._rights)) + } + + /// Returns a new set with the elements that are either in this set or in the + /// given set, but not in both. + public func symmetricDifference(_ other: CastlingRights) -> CastlingRights { + return CastlingRights(_rights.symmetricDifference(other._rights)) + } + + /// Inserts the given element in the set if it is not already present. + @discardableResult + public mutating func insert(_ newMember: Right) -> (inserted: Bool, memberAfterInsert: Right) { + return _rights.insert(newMember) + } + + /// Removes the given element and any elements subsumed by the given element. + @discardableResult + public mutating func remove(_ member: Right) -> Right? { + return _rights.remove(member) + } + + /// Inserts the given element into the set unconditionally. + @discardableResult + public mutating func update(with newMember: Right) -> Right? { + return _rights.update(with: newMember) + } + + /// Adds the elements of the given set to the set. + public mutating func formUnion(_ other: CastlingRights) { + _rights.formUnion(other._rights) + } + + /// Removes the elements of this set that aren't also in the given set. + public mutating func formIntersection(_ other: CastlingRights) { + _rights.formIntersection(other._rights) + } + + /// Removes the elements of the set that are also in the given set and + /// adds the members of the given set that are not already in the set. + public mutating func formSymmetricDifference(_ other: CastlingRights) { + _rights.formSymmetricDifference(other._rights) + } + + /// Returns a new set containing the elements of this set that do not occur + /// in the given set. + public func subtracting(_ other: CastlingRights) -> CastlingRights { + return CastlingRights(_rights.subtracting(other._rights)) + } + + /// Returns a Boolean value that indicates whether the set is a subset of + /// another set. + public func isSubset(of other: CastlingRights) -> Bool { + return _rights.isSubset(of: other._rights) + } + + /// Returns a Boolean value that indicates whether the set has no members in + /// common with the given set. + public func isDisjoint(with other: CastlingRights) -> Bool { + return _rights.isDisjoint(with: other._rights) + } + + /// Returns a Boolean value that indicates whether the set is a superset of + /// the given set. + public func isSuperset(of other: CastlingRights) -> Bool { + return _rights.isSuperset(of: other._rights) + } + + /// Removes the elements of the given set from this set. + public mutating func subtract(_ other: CastlingRights) { + _rights.subtract(other) + } + +} + +extension CastlingRights: Hashable { + /// The hash value. + public func hash(into hasher: inout Hasher) { + hasher.combine(_rights) + } +} + +/// Returns `true` if both have the same rights. +public func == (lhs: CastlingRights, rhs: CastlingRights) -> Bool { + return lhs._rights == rhs._rights +} diff --git a/Sources/SwiftChessNeo/Color.swift b/Sources/SwiftChessNeo/Color.swift new file mode 100644 index 0000000..39d63a4 --- /dev/null +++ b/Sources/SwiftChessNeo/Color.swift @@ -0,0 +1,83 @@ +// +// Color.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 color. +public enum Color: String, CustomStringConvertible { + + /// White chess color. + case white + + /// Black chess color. + case black + + /// White color regardless of Swift version. + internal static let _white = Color.white + + /// Black color regardless of Swift version. + internal static let _black = Color.black + + /// An array of all colors. + public static let all: [Color] = [.white, .black] + + /// Whether the color is white or not. + public var isWhite: Bool { + return self == ._white + } + + /// Whether the color is black or not. + public var isBlack: Bool { + return self == ._black + } + + /// A textual representation of `self`. + public var description: String { + return rawValue + } + + /// The numeric representation of `self`. `White` is "0", `Black` is "1". + public var numericValue: Int { + return self.isWhite ? 0 : 1 + } + + /// The lowercase character for the color. `White` is "w", `Black` is "b". + public var character: Character { + return self.isWhite ? "w" : "b" + } + + /// Create a color from a character of any case. + public init?(character: Character) { + switch character { + case "W", "w": self = ._white + case "B", "b": self = ._black + default: return nil + } + } + + /// Returns the inverse of `self`. + public func inverse() -> Color { + return self.isWhite ? ._black : ._white + } + + /// Inverts the color of `self`. + public mutating func invert() { + self = inverse() + } + +} diff --git a/Sources/SwiftChessNeo/File.swift b/Sources/SwiftChessNeo/File.swift new file mode 100644 index 0000000..158eee7 --- /dev/null +++ b/Sources/SwiftChessNeo/File.swift @@ -0,0 +1,190 @@ +// +// File.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 board file. +/// +/// Files refer to the eight columns of a chess board, beginning with A and ending with H from left to right. +public enum File: Int, Comparable, CustomStringConvertible { + + /// A direction in file. + public enum Direction { + + /// Left direction. + case left + + /// Right direction. + case right + + } + + /// File "A". + case a = 1 + + /// File "B". + case b = 2 + + /// File "C". + case c = 3 + + /// File "D". + case d = 4 + + /// File "E". + case e = 5 + + /// File "F". + case f = 6 + + /// File "G". + case g = 7 + + /// File "H". + case h = 8 + + /// A regardless of Swift version. + internal static let _a = File.a + + /// B regardless of Swift version. + internal static let _b = File.b + + /// C regardless of Swift version. + internal static let _c = File.c + + /// D regardless of Swift version. + internal static let _d = File.d + + /// E regardless of Swift version. + internal static let _e = File.e + + /// F regardless of Swift version. + internal static let _f = File.f + + /// G regardless of Swift version. + internal static let _g = File.g + + /// H regardless of Swift version. + internal static let _h = File.h + +} + +extension File { + + /// An array of all files. + public static let all: [File] = [.a, .b, .c, .d, .e, .f, .g, .h] + + /// The column index of `self`. + public var index: Int { + return rawValue - 1 + } + + /// A textual representation of `self`. + public var description: String { + return String(character) + } + + /// The character value of `self`. + public var character: Character { + switch self { + case .a: return "a" + case .b: return "b" + case .c: return "c" + case .d: return "d" + case .e: return "e" + case .f: return "f" + case .g: return "g" + case .h: return "h" + } + } + + /// Create an instance from a character value. + public init?(_ character: Character) { + switch character { + case "A", "a": self = .a + case "B", "b": self = .b + case "C", "c": self = .c + case "D", "d": self = .d + case "E", "e": self = .e + case "F", "f": self = .f + case "G", "g": self = .g + case "H", "h": self = .h + default: return nil + } + } + + /// Create a `File` from a zero-based column index. + public init?(index: Int) { + self.init(rawValue: index + 1) + } + + /// Returns a rank from advancing `self` by `value`. + public func advanced(by value: Int) -> File? { + return File(rawValue: rawValue + value) + } + + /// The next file after `self`. + public func next() -> File? { + return File(rawValue: (rawValue + 1)) + } + + /// The previous file to `self`. + public func previous() -> File? { + return File(rawValue: (rawValue - 1)) + } + + /// The opposite file of `self`. + public func opposite() -> File { + return File(rawValue: 9 - rawValue)! + } + + /// The files from `self` to `other`. + public func to(_ other: File) -> [File] { + return _to(other) + } + + /// The files between `self` and `other`. + public func between(_ other: File) -> [File] { + return _between(other) + } + +} + +extension File: ExpressibleByExtendedGraphemeClusterLiteral { } + +extension File { + + /// Create an instance initialized to `value`. + public init(unicodeScalarLiteral value: Character) { + guard let file = File(value) else { + fatalError("File value not within \"A\" and \"H\" or \"a\" and \"h\", inclusive") + } + self = file + } + + /// Create an instance initialized to `value`. + public init(extendedGraphemeClusterLiteral value: Character) { + self.init(unicodeScalarLiteral: value) + } + +} + +/// Returns `true` if one file is further left than the other. +public func < (lhs: File, rhs: File) -> Bool { + return lhs.rawValue < rhs.rawValue +} 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 +} diff --git a/Sources/SwiftChessNeo/InternalTypes.swift b/Sources/SwiftChessNeo/InternalTypes.swift new file mode 100644 index 0000000..c418c64 --- /dev/null +++ b/Sources/SwiftChessNeo/InternalTypes.swift @@ -0,0 +1,63 @@ +// +// InternalTypes.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. +// + +#if os(OSX) + import Cocoa + internal typealias _View = NSView + internal typealias _Color = NSColor +#elseif os(iOS) || os(tvOS) + import UIKit + internal typealias _View = UIView + internal typealias _Color = UIColor +#endif + +internal extension Optional { + + var _altDescription: String { + return self.map({ String(describing: $0) }) ?? "nil" + } + +} + +extension RawRepresentable where RawValue == Int, Self: Comparable { + + internal func _to(_ other: Self) -> [Self] { + if other > self { + return (rawValue...other.rawValue).compactMap(Self.init(rawValue:)) + } else if other < self { + let values = (other.rawValue...rawValue).reversed() + return values.compactMap(Self.init(rawValue:)) + } else { + return [self] + } + } + + internal func _between(_ other: Self) -> [Self] { + if other > self { + return (rawValue + 1 ..< other.rawValue).compactMap(Self.init(rawValue:)) + } else if other < self { + let values = (other.rawValue + 1 ..< rawValue).reversed() + return values.compactMap(Self.init(rawValue:)) + } else { + return [] + } + } + +} diff --git a/Sources/SwiftChessNeo/Move.swift b/Sources/SwiftChessNeo/Move.swift new file mode 100644 index 0000000..efc88d7 --- /dev/null +++ b/Sources/SwiftChessNeo/Move.swift @@ -0,0 +1,203 @@ +// +// Move.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 move from a start `Square` to an end `Square`. +public struct Move: Hashable, CustomStringConvertible { + + /// The move's start square. + public var start: Square + + /// The move's end square. + public var end: Square + + /// The move's change in file. + public var fileChange: Int { + return end.file.rawValue - start.file.rawValue + } + + /// The move's change in rank. + public var rankChange: Int { + return end.rank.rawValue - start.rank.rawValue + } + + /// The move is a real change in location. + public var isChange: Bool { + return start != end + } + + /// The move is diagonal. + public var isDiagonal: Bool { + let fileChange = self.fileChange + return fileChange != 0 && abs(fileChange) == abs(rankChange) + } + + /// The move is horizontal. + public var isHorizontal: Bool { + return start.file != end.file && start.rank == end.rank + } + + /// The move is vertical. + public var isVertical: Bool { + return start.file == end.file && start.rank != end.rank + } + + /// The move is horizontal or vertical. + public var isAxial: Bool { + return isHorizontal || isVertical + } + + /// The move is leftward. + public var isLeftward: Bool { + return end.file < start.file + } + + /// The move is rightward. + public var isRightward: Bool { + return end.file > start.file + } + + /// The move is downward. + public var isDownward: Bool { + return end.rank < start.rank + } + + /// The move is upward. + public var isUpward: Bool { + return end.rank > start.rank + } + + /// The move is a knight jump two spaces horizontally and one space vertically, or two spaces vertically and one + /// space horizontally. + public var isKnightJump: Bool { + let fileChange = abs(self.fileChange) + let rankChange = abs(self.rankChange) + return (fileChange == 2 && rankChange == 1) + || (rankChange == 2 && fileChange == 1) + } + + /// The move's direction in file, if any. + public var fileDirection: File.Direction? { + if self.isLeftward { + return .left + } else if self.isRightward { + return .right + } else { + return .none + } + } + + /// The move's direction in rank, if any. + public var rankDirection: Rank.Direction? { + if self.isUpward { + return .up + } else if self.isDownward { + return .down + } else { + return .none + } + } + + /// A textual representation of `self`. + public var description: String { + return "\(start) >>> \(end)" + } + + /// The hash value. + public func hash(into hasher: inout Hasher) { + hasher.combine(start) + hasher.combine(end) + } + + /// Create a move with start and end squares. + public init(start: Square, end: Square) { + self.start = start + self.end = end + } + + /// Create a move with start and end locations. + public init(start: Location, end: Location) { + self.start = Square(location: start) + self.end = Square(location: end) + } + + /// A castle move for `color` in `direction`. + public init(castle color: Color, direction: File.Direction) { + let rank: Rank = color.isWhite ? 1 : 8 + self = Move(start: Square(file: .e, rank: rank), end: Square(file: direction == .left ? .c : .g, rank: rank)) + } + + /// Returns the castle squares for a rook. + internal func _castleSquares() -> (old: Square, new: Square) { + let rank = start.rank + let movedLeft = self.isLeftward + let old = Square(file: movedLeft ? .a : .h, rank: rank) + let new = Square(file: movedLeft ? .d : .f, rank: rank) + return (old, new) + } + + /// Returns a move with the end and start of `self` reversed. + public func reversed() -> Move { + return Move(start: end, end: start) + } + + /// Returns the result of rotating `self` 180 degrees. + public func rotated() -> Move { + let start = Square(file: self.start.file.opposite(), + rank: self.start.rank.opposite()) + let end = Square(file: self.end.file.opposite(), + rank: self.end.rank.opposite()) + return start >>> end + } + + /// Returns `true` if `self` is castle move for `color`. + /// + /// - parameter color: The color to check the rank against. If `nil`, the rank can be either 1 or 8. The default + /// value is `nil`. + public func isCastle(for color: Color? = nil) -> Bool { + let startRank = start.rank + if let color = color { + guard startRank == Rank(startFor: color) else { return false } + } else { + guard startRank == 1 || startRank == 8 else { return false } + } + let endFile = end.file + return startRank == end.rank + && start.file == ._e + && (endFile == ._c || endFile == ._g) + } + +} + +infix operator >>> + +/// Returns `true` if both moves are the same. +public func == (lhs: Move, rhs: Move) -> Bool { + return lhs.start == rhs.start && lhs.end == rhs.end +} + +/// Returns a `Move` from the two squares. +public func >>> (start: Square, end: Square) -> Move { + return Move(start: start, end: end) +} + +/// Returns a `Move` from the two locations. +public func >>> (start: Location, rhs: Location) -> Move { + return Square(location: start) >>> Square(location: rhs) +} diff --git a/Sources/SwiftChessNeo/PGN.swift b/Sources/SwiftChessNeo/PGN.swift new file mode 100644 index 0000000..4150347 --- /dev/null +++ b/Sources/SwiftChessNeo/PGN.swift @@ -0,0 +1,454 @@ +// +// PGN.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. +// + +/// Portable game notation data. +/// +/// - seealso: [Portable Game Notation (Wikipedia)](https://en.wikipedia.org/wiki/Portable_Game_Notation), +/// [PGN Specification](https://www.chessclub.com/user/help/PGN-spec) + +import Foundation + +public struct PGN: Equatable { + + /// PGN tag. + public enum Tag: String, CustomStringConvertible { + + /// Event tag. + case event = "Event" + + /// Site tag. + case site = "Site" + + /// Date tag. + case date = "Date" + + /// Round tag. + case round = "Round" + + /// White tag. + case white = "White" + + /// Black tag. + case black = "Black" + + /// Result tag. + case result = "Result" + + /// Annotator tag. + case annotator = "Annotator" + + /// Ply (moves) count tag. + case plyCount = "PlyCount" + + /// TimeControl tag. + case timeControl = "TimeControl" + + /// Time tag. + case time = "Time" + + /// Termination tag. + case termination = "Termination" + + /// Playing mode tag. + case mode = "Mode" + + /// FEN tag. + case fen = "FEN" + + /// White player's title tag. + case whiteTitle = "WhiteTitle" + + /// Black player's title tag. + case blackTitle = "BlackTitle" + + /// White player's elo rating tag. + case whiteElo = "WhiteElo" + + /// Black player's elo rating tag. + case blackElo = "BlackElo" + + /// White player's United States Chess Federation rating tag. + case whiteUSCF = "WhiteUSCF" + + /// Black player's United States Chess Federation rating tag. + case blackUSCF = "BlackUSCF" + + /// White player's network or email address tag. + case whiteNA = "WhiteNA" + + /// Black player's network or email address tag. + case blackNA = "BlackNA" + + /// White player's type tag; either human or program. + case whiteType = "WhiteType" + + /// Black player's type tag; either human or program. + case blackType = "BlackType" + + /// The starting date tag of the event. + case eventDate = "EventDate" + + /// Tag for the name of the sponsor of the event. + case eventSponsor = "EventSponsor" + + /// The playing section tag of a tournament. + case section = "Section" + + /// Tag for the stage of a multistage event. + case stage = "Stage" + + /// The board number tag in a team event or in a simultaneous exhibition. + case board = "Board" + + /// The traditional opening name tag. + case opening = "Opening" + + /// Tag used to further refine the opening tag. + case variation = "Variation" + + /// Used to further refine the variation tag. + case subVariation = "SubVariation" + + /// Tag used for an opening designation from the five volume *Encyclopedia of Chess Openings*. + case eco = "ECO" + + /// Tag used for an opening designation from the *New in Chess* database. + case nic = "NIC" + + /// Tag similar to the Time tag but given according to the Universal Coordinated Time standard. + case utcTime = "UTCTime" + + /// Tag similar to the Date tag but given according to the Universal Coordinated Time standard. + case utcDate = "UTCDate" + + /// Tag for the "set-up" status of the game. + case setUp = "SetUp" + + /// A textual representation of `self`. + public var description: String { + return rawValue + } + + } + + /// An error thrown by `PGN.init(parse:)`. + public enum ParseError: Error { + + /// Unexpected quote found in move text. + case unexpectedQuote(String) + + /// Unexpected closing brace found outside of comment. + case unexpectedClosingBrace(String) + + /// No closing brace for comment. + case noClosingBrace(String) + + /// No closing quote for tag value. + case noClosingQuote(String) + + /// No closing bracket for tag pair. + case noClosingBracket(String) + + /// Wrong number of tokens for tag pair. + case tagPairTokenCount([String]) + + /// Incorrect count of parenthesis for recursive annotation variation. + case parenthesisCountForRAV(String) + + } + + /// The tag pairs for `self`. + public var tagPairs: [String: String] + + /// The moves in standard algebraic notation. + public var moves: [String] + + /// The game outcome. + public var outcome: Game.Outcome? { + get { + let resultTag = Tag.result + return self[resultTag].flatMap(Game.Outcome.init) + } + set { + let resultTag = Tag.result + self[resultTag] = newValue?.description + } + } + + /// Create PGN with `tagPairs` and `moves`. + public init(tagPairs: [String: String] = [:], moves: [String] = []) { + self.tagPairs = tagPairs + self.moves = moves + } + + /// Create PGN with `tagPairs` and `moves`. + public init(tagPairs: [Tag: String], moves: [String] = []) { + self.init(moves: moves) + for (tag, value) in tagPairs { + self[tag] = value + } + } + + /// Create PGN by parsing `string`. + /// + /// - throws: `ParseError` if an error occured while parsing. + public init(parse string: String) throws { + self.init() + if string.isEmpty { return } + for line in string._splitByNewlines() { + if line.first == "[" { + let commentsStripped = try line._commentsStripped(strings: true) + let (tag, value) = try commentsStripped._tagPair() + tagPairs[tag] = value + } else if line.first != "%" { + let commentsStripped = try line._commentsStripped(strings: false) + // Lichess PGNs have an extra comment after a few days + if (commentsStripped.count == 0 ) { + continue + } + let (moves, outcome) = try commentsStripped._moves() + self.moves += moves + if let outcome = outcome { + self.outcome = outcome + } + } + } + } + + /// Get or set the value for `tag`. + public subscript(tag: Tag) -> String? { + get { + return tagPairs[tag.rawValue] + } + set { + tagPairs[tag.rawValue] = newValue + } + } + + /// Returns `self` in export string format. + /// // PGN(tagPairs: ["Black": "Spassky, Boris V.", "Event": "F/S Return Match", "Round": "29", "Result": "½-½", "Site": "Belgrade, Serbia Yugoslavia|JUG", "Date": "1992.11.04", "White": "Fischer, Robert J." + public func exported() -> String { + var result = "" + var tagPairs = self.tagPairs + let sevenTags = [("Event", "?"), + ("Site", "?"), + ("Date", "????.??.??"), + ("Round", "?"), + ("White", "?"), + ("Black", "?"), + ("Result", "*")] + let orderedTags = ["Event", "Site", "Date", "Round", "White", "Black", "Result"] + for tag in orderedTags { + if let value = tagPairs.removeValue(forKey: tag) { + result += "[\(tag) \"\(value)\"]\n" + } else { + if let defaultValue = sevenTags.first(where: { $0.0 == tag })?.1 { + result += "[\(tag) \"\(defaultValue)\"]\n" + } else { + // Handle cases where the tag is not in 'sevenTags' + } + } + } + for (tag, value) in tagPairs { + result += "[\(tag) \"\(value)\"]\n" + } + let strideTo = stride(from: 0, to: moves.endIndex, by: 2) + var moveLine = "" + for num in strideTo { + let moveNumber = (num + 2) / 2 + var moveString = "\(moveNumber). \(moves[num])" + if num + 1 < moves.endIndex { + moveString += " \(moves[num + 1])" + } + if moveString.count + moveLine.count < 80 { + if !moveLine.isEmpty { + moveString = " \(moveString)" + } + moveLine += moveString + } else { + result += "\n\(moveLine)" + moveLine = moveString + } + } + if !moveLine.isEmpty { + result += "\n\(moveLine)" + } + if let outcomeString = outcome?.description { + if moveLine.isEmpty { + result += "\n\(outcomeString)" + } else if outcomeString.count + moveLine.count < 80 { + result += " \(outcomeString)" + } else { + result += "\n\(outcomeString)" + } + } + return result + } + +} + +private extension Character { + + static let newlines: Set = ["\u{000A}", "\u{000B}", "\u{000C}", "\u{000D}", + "\u{0085}", "\u{2028}", "\u{2029}"] + + static let whitespaces: Set = ["\u{0020}", "\u{00A0}", "\u{1680}", "\u{180E}", "\u{2000}", + "\u{2001}", "\u{2002}", "\u{2003}", "\u{2004}", "\u{2005}", + "\u{2006}", "\u{2007}", "\u{2008}", "\u{2009}", "\u{200A}", + "\u{200B}", "\u{202F}", "\u{205F}", "\u{3000}", "\u{FEFF}"] + + static let digits: Set = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"] + + var isDigit: Bool { + return Character.digits.contains(self) + } + +} + +private extension String { + + var _lastIndex: Index { + return index(before: endIndex) + } + + @inline(__always) + func _split(by set: Set) -> [String] { + return split(whereSeparator: set.contains).map(String.init) + } + + @inline(__always) + func _splitByNewlines() -> [String] { + return _split(by: Character.newlines) + } + + @inline(__always) + func _splitByWhitespaces() -> [String] { + return _split(by: Character.whitespaces) + } + + @inline(__always) + func _tagPair() throws -> (String, String) { + guard last == "]" else { + throw PGN.ParseError.noClosingBracket(self) + } + let startIndex = index(after: self.startIndex) + let endIndex = index(before: self.endIndex) + let tokens = String(self[startIndex ..< endIndex])._split(by: ["\""]) + guard tokens.count == 2 else { + throw PGN.ParseError.tagPairTokenCount(tokens) + } + let tagParts = tokens[0]._splitByWhitespaces() + guard tagParts.count == 1 else { + throw PGN.ParseError.tagPairTokenCount(tagParts) + } + return (tagParts[0], tokens[1]) + } + + @inline(__always) + func _moves() throws -> (moves: [String], outcome: Game.Outcome?) { + var stripped = "" + var ravDepth = 0 + var startIndex = self.startIndex + let lastIndex = _lastIndex + for (index, character) in zip(indices, self) { + if character == "(" { + if ravDepth == 0 { + stripped += self[startIndex ..< index] + } + ravDepth += 1 + } else if character == ")" { + ravDepth -= 1 + if ravDepth == 0 { + startIndex = self.index(after: index) + } + } else if index == lastIndex && ravDepth == 0 { + stripped += self[startIndex ... index] + } + } + guard ravDepth == 0 else { + throw PGN.ParseError.parenthesisCountForRAV(self) + } + let tokens = stripped._split(by: [" ", "."]) + let moves = tokens.filter({ $0.first?.isDigit == false }).map { $0.replacingOccurrences(of: "?", with: "").replacingOccurrences(of: "!", with: "")} + let outcome = tokens.last.flatMap(Game.Outcome.init) + return (moves, outcome) + } + + @inline(__always) + func _commentsStripped(strings consideringStrings: Bool) throws -> String { + var stripped = "" + var startIndex = self.startIndex + let lastIndex = _lastIndex + var afterEscape = false + var inString = false + var inComment = false + for (index, character) in zip(indices, self) { + if character == "\\" { + afterEscape = true + continue + } + if character == "\"" { + if !inComment { + guard consideringStrings else { + throw PGN.ParseError.unexpectedQuote(self) + } + if !inString { + inString = true + } else if !afterEscape { + inString = false + } + } + } else if !inString { + if character == ";" && !inComment { + stripped += self[startIndex ..< index] + break + } else if character == "{" && !inComment { + inComment = true + stripped += self[startIndex ..< index] + } else if character == "}" { + guard inComment else { + throw PGN.ParseError.unexpectedClosingBrace(self) + } + inComment = false + startIndex = self.index(after: index) + } + } + if index >= startIndex && index == lastIndex && !inComment { + stripped += self[startIndex ... index] + } + afterEscape = false + } + guard !inString else { + throw PGN.ParseError.noClosingQuote(self) + } + guard !inComment else { + throw PGN.ParseError.noClosingBrace(self) + } + + return stripped + } + +} + +/// Returns a Boolean value indicating whether two values are equal. +public func == (lhs: PGN, rhs: PGN) -> Bool { + return lhs.tagPairs == rhs.tagPairs + && lhs.moves == rhs.moves +} diff --git a/Sources/SwiftChessNeo/PGNMove.swift b/Sources/SwiftChessNeo/PGNMove.swift new file mode 100644 index 0000000..3be103b --- /dev/null +++ b/Sources/SwiftChessNeo/PGNMove.swift @@ -0,0 +1,219 @@ +// +// PGNMove.swift +// Sage +// +// Created by Kajetan Dąbrowski on 19/10/2016. +// Copyright © 2016 Nikolai Vazquez. All rights reserved. +// + +import Foundation + +/// A PGN move representation in a string. +public struct PGNMove: RawRepresentable, ExpressibleByStringLiteral { + + + /// PGN Move parsing error + /// + /// - invalidMove: The move is invalid + public enum ParseError: Error { + case invalidMove(String) + } + + public typealias RawValue = String + public typealias StringLiteralType = String + public typealias ExtendedGraphemeClusterLiteralType = String + public typealias UnicodeScalarLiteralType = String + public let rawValue: String + + static private let pattern = "^(?:([NBRQK]?)([a-h]?)([1-8]?)(x?)([a-h])([1-8])((?:=[NBRQ])?)|(O-O)|(O-O-O))([+#]?)$" + static private let regex: NSRegularExpression! = try? NSRegularExpression(pattern: pattern, options: []) + + private var match: NSTextCheckingResult? + private var unwrappedMatch: NSTextCheckingResult { + guard let unwrappedMatch = match else { + fatalError("PGNMove not possible. Check move.isPossible before checking other properties") + } + return unwrappedMatch + } + + public init?(rawValue: String) { + self.rawValue = rawValue + parse() + if !isPossible { return nil } + } + + public init(stringLiteral value: StringLiteralType) { + rawValue = value + parse() + } + + public init(unicodeScalarLiteral value: UnicodeScalarLiteralType) { + rawValue = value + parse() + } + + public init(extendedGraphemeClusterLiteral value: ExtendedGraphemeClusterLiteralType) { + rawValue = value + parse() + } + + mutating private func parse() { + let matches = PGNMove.regex.matches(in: rawValue, options: [], range: fullRange) + match = matches.filter { $0.range.length == self.fullRange.length }.first + } + + private var fullRange: NSRange { + return NSRange(location: 0, length: rawValue.count) + } + + /// Indicates whether the move is possible. + public var isPossible: Bool { + return match != nil + } + + /// Indicated whether the pgn represents a capture + public var isCapture: Bool { + return unwrappedMatch.range(at: 4).length > 0 + } + + /// Indicates whether the move represents a promotion + public var isPromotion: Bool { + return unwrappedMatch.range(at: 7).length > 0 + } + + /// Indicates whether the move is castle + public var isCastle: Bool { + return isCastleKingside || isCastleQueenside + } + + /// Indicates whether the move is castle kingside + public var isCastleKingside: Bool { + return unwrappedMatch.range(at: 8).length > 0 + } + + /// Indicates whether the move is castle queenside + public var isCastleQueenside: Bool { + return unwrappedMatch.range(at: 9).length > 0 + } + + /// Indicates whether the move represents a check + public var isCheck: Bool { + return unwrappedMatch.range(at: 10).length > 0 + } + + /// Indicates whether the move represents a checkmate + public var isCheckmate: Bool { + return stringAt(rangeIndex: 10) == "#" + } + + /// A piece kind that is moved by the move + public var piece: Piece.Kind { + let pieceLetter = stringAt(rangeIndex: 1) + guard let piece = PGNMove.pieceFor(letter: pieceLetter) else { + fatalError("Invalid piece") + } + return piece + } + + /// The rank to move to + public var rank: Rank { + let rankSymbol = stringAt(rangeIndex: 6) + guard let raw = Int(rankSymbol), let rank = Rank(rawValue: raw) else { fatalError("Could not get rank") } + return rank + } + + /// The file to move to + public var file: File { + guard let fileSymbol = stringAt(rangeIndex: 5).first, + let file = File(fileSymbol) else { fatalError("Could not get file") } + return file + } + + /// The rank to move from. + /// For example in the move 'Nf3' there is no source rank, since PGNMove is out of board context. + /// However, if you specify the move like 'N4d2' the move will represent the knight from the fourth rank. + public var sourceRank: Rank? { + let sourceRankSymbol = stringAt(rangeIndex: 3) + if sourceRankSymbol == "" { return nil } + guard let sourceRankRaw = Int(sourceRankSymbol), + let sourceRank = Rank(rawValue: sourceRankRaw) else { fatalError("Could not get source rank") } + return sourceRank + } + + /// The file to move from. + /// For example in the move 'Nf3' there is no source file, since PGNMove is out of board context. + /// However, if you specify the move like 'Nfd2' the move will represent the knight from the d file. + public var sourceFile: File? { + let sourceFileSymbol = stringAt(rangeIndex: 2) + if sourceFileSymbol == "" { return nil } + guard let sourceFileRaw = sourceFileSymbol.first, + let sourceFile = File(sourceFileRaw) else { fatalError("Could not get source file") } + return sourceFile + } + + /// Represents a piece that the move wants to promote to + public var promotionPiece: Piece.Kind? { + if !isPromotion { return nil } + guard let pieceLetter = stringAt(rangeIndex: 7).last, + let piece = PGNMove.pieceFor(letter: String(pieceLetter)) else { fatalError("Could not get promotion piece") } + return piece + } + + private static func pieceFor(letter: String) -> Piece.Kind? { + switch letter { + case "N": + return ._knight + case "B": + return ._bishop + case "K": + return ._king + case "Q": + return ._queen + case "R": + return ._rook + case "": + return ._pawn + default: + return nil + } + } + + private func stringAt(rangeIndex: Int) -> String { + guard let match = match else { fatalError() } + let range = match.range(at: rangeIndex) + let substring = (rawValue as NSString).substring(with: range) + return substring + } +} + +public struct PGNParser { + + + /// Parses the move in context of the game position + /// + /// - parameter move: Move that needs to be parsed + /// - parameter position: position to parse in + /// + /// - throws: Errors if move is invalid, or if it cannot be executed in this position, or if it's ambiguous. + /// + /// - returns: Parsed move that can be applied to a game (containing source and destination squares) + public static func parse(move: PGNMove, in position: Game.Position) throws -> Move { + if !move.isPossible { throw PGNMove.ParseError.invalidMove(move.rawValue) } + let colorToMove = position.playerTurn + if move.isCastleKingside { return Move(castle: colorToMove, direction: .right) } + if move.isCastleQueenside { return Move(castle: colorToMove, direction: .left) } + + let piece = Piece(kind: move.piece, color: colorToMove) + let destinationSquare: Square = Square(file: move.file, rank: move.rank) + let game = try Game(position: position) + var possibleMoves = game.availableMoves().filter { return $0.end == destinationSquare }.filter { move -> Bool in + game.board.locations(for: piece).contains(where: { move.start.location == $0 }) + } + + if let sourceFile = move.sourceFile { possibleMoves = possibleMoves.filter { $0.start.file == sourceFile } } + if let sourceRank = move.sourceRank { possibleMoves = possibleMoves.filter { $0.start.rank == sourceRank } } + + if possibleMoves.count != 1 { throw PGNMove.ParseError.invalidMove(move.rawValue) } + return possibleMoves.first! + } +} diff --git a/Sources/SwiftChessNeo/Piece.swift b/Sources/SwiftChessNeo/Piece.swift new file mode 100644 index 0000000..492ad87 --- /dev/null +++ b/Sources/SwiftChessNeo/Piece.swift @@ -0,0 +1,297 @@ +// +// Piece.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 piece. +public struct Piece: Hashable, CustomStringConvertible { + + /// A piece kind. + public enum Kind: Int { + + /// Pawn piece kind. + case pawn + + /// Knight piece kind. + case knight + + /// Bishop piece kind. + case bishop + + /// Rook piece kind. + case rook + + /// Queen piece kind. + case queen + + /// King piece kind. + case king + + /// Pawn + internal static let _pawn = Kind.pawn + + /// Knight + internal static let _knight = Kind.knight + + /// Bishop + internal static let _bishop = Kind.bishop + + /// Rook + internal static let _rook = Kind.rook + + /// Queen + internal static let _queen = Kind.queen + + /// King + internal static let _king = Kind.king + + /// An array of all piece kinds. + public static let all: [Kind] = [.pawn, .knight, .bishop, .rook, .queen, .king] + + /// The piece kind's name. + public var name: String { + switch self { + case .pawn: return "Pawn" + case .knight: return "Knight" + case .bishop: return "Bishop" + case .rook: return "Rook" + case .queen: return "Queen" + case .king: return "King" + } + } + + /// The piece kind's relative value. Can be used to determine how valuable a piece or combination of pieces is. + public var relativeValue: Double { + switch self { + case .pawn: return 1 + case .knight: return 3 + case .bishop: return 3.25 + case .rook: return 5 + case .queen: return 9 + case .king: return .infinity + } + } + + /// The piece is `Pawn`. + public var isPawn: Bool { + return self == ._pawn + } + + /// The piece `Knight`. + public var isKnight: Bool { + return self == ._knight + } + + /// The piece is `Bishop`. + public var isBishop: Bool { + return self == ._bishop + } + + /// The piece is `Rook`. + public var isRook: Bool { + return self == ._rook + } + + /// The piece is `Queen`. + public var isQueen: Bool { + return self == ._queen + } + + /// The piece is `King`. + public var isKing: Bool { + return self == ._king + } + + /// Returns `true` if `self` can be a promotion for a pawn. + public func canPromote() -> Bool { + return !(isPawn || isKing) + } + + /// Returns `true` if `self` can be a promotion for `other`. + public func canPromote(_ other: Kind) -> Bool { + return canPromote() ? other.isPawn : false + } + + } + + internal static let _whiteHashes: [Int] = whitePieces.map({ $0.rawValue }) + + internal static let _blackHashes: [Int] = blackPieces.map({ $0.rawValue }) + + internal static func _hashes(for color: Color) -> [Int] { + return color.isWhite ? _whiteHashes : _blackHashes + } + + internal static let _whiteNonQueens: [Piece] = whitePieces.filter({ !$0.kind.isQueen }) + + internal static let _blackNonQueens: [Piece] = blackPieces.filter({ !$0.kind.isQueen }) + + internal static func _nonQueens(for color: Color) -> [Piece] { + return color.isWhite ? _whiteNonQueens : _blackNonQueens + } + + /// An array of all pieces. + public static let all: [Piece] = { + return [.white, .black].reduce([]) { pieces, color in + return pieces + Kind.all.map({ Piece(kind: $0, color: color) }) + } + }() + + /// An array of all white pieces. + public static let whitePieces: [Piece] = all.filter({ $0.color.isWhite }) + + /// An array of all black pieces. + public static let blackPieces: [Piece] = all.filter({ $0.color.isBlack }) + + /// Returns an array of all pieces for `color`. + public static func pieces(for color: Color) -> [Piece] { + return color.isWhite ? whitePieces : blackPieces + } + + /// The piece's kind. + public var kind: Kind + + /// The piece's color. + public var color: Color + + /// The character for the piece. Uppercase if white or lowercase if black. + public var character: Character { + switch kind { + case .pawn: return color.isWhite ? "P" : "p" + case .knight: return color.isWhite ? "N" : "n" + case .bishop: return color.isWhite ? "B" : "b" + case .rook: return color.isWhite ? "R" : "r" + case .queen: return color.isWhite ? "Q" : "q" + case .king: return color.isWhite ? "K" : "k" + } + } + + /// A textual representation of `self`. + public var description: String { + return "\(kind.name)(\(color))" + } + + /// The piece's raw value. + public var rawValue: Int { + return (kind.rawValue << 1) | color.numericValue + } + + /// Create a piece from an integer value. + internal init?(value: Int) { + guard let kind = Kind(rawValue: value >> 1) else { + return nil + } + self.init(kind: kind, color: value & 1 == 0 ? .white : .black) + } + + /// Create a piece from `kind` and `color`. + public init(kind: Kind, color: Color) { + self.kind = kind + self.color = color + } + + /// Create a pawn piece with `color`. + public init(pawn color: Color) { + self.init(kind: ._pawn, color: color) + } + + /// Create a knight piece with `color`. + public init(knight color: Color) { + self.init(kind: ._knight, color: color) + } + + /// Create a bishop piece with `color`. + public init(bishop color: Color) { + self.init(kind: ._bishop, color: color) + } + + /// Create a rook piece with `color`. + public init(rook color: Color) { + self.init(kind: ._rook, color: color) + } + + /// Create a queen piece with `color`. + public init(queen color: Color) { + self.init(kind: ._queen, color: color) + } + + /// Create a king piece with `color`. + public init(king color: Color) { + self.init(kind: ._king, color: color) + } + + /// Create a piece from a character. + public init?(character: Character) { + switch character { + case "P": self.init(pawn: .white) + case "p": self.init(pawn: .black) + case "N": self.init(knight: .white) + case "n": self.init(knight: .black) + case "B": self.init(bishop: .white) + case "b": self.init(bishop: .black) + case "R": self.init(rook: .white) + case "r": self.init(rook: .black) + case "Q": self.init(queen: .white) + case "q": self.init(queen: .black) + case "K": self.init(king: .white) + case "k": self.init(king: .black) + default: + return nil + } + } + + /// Returns `true` if `self` can be a promotion for `other`. + public func canPromote(_ other: Piece) -> Bool { + return kind.canPromote(other.kind) && color == other.color + } + + /// Returns `true` if `self` can be a promotion for `color`. + public func canPromote(_ color: Color) -> Bool { + return kind.canPromote() ? self.color == color : false + } + + /// The special character for the piece. + public func specialCharacter(background color: Color = .white) -> Character { + switch kind { + case .pawn: return color == self.color ? "♙" : "♟" + case .knight: return color == self.color ? "♘" : "♞" + case .bishop: return color == self.color ? "♗" : "♝" + case .rook: return color == self.color ? "♖" : "♜" + case .queen: return color == self.color ? "♕" : "♛" + case .king: return color == self.color ? "♔" : "♚" + } + } + + public func unicodeCharacter() -> Character { + switch kind { + case .pawn: return "♟" + case .knight: return "♞" + case .bishop: return "♝" + case .rook: return "♜" + case .queen: return "♛" + case .king: return "♚" + } + } + +} + +/// Returns `true` if both pieces are the same. +public func == (lhs: Piece, rhs: Piece) -> Bool { + return lhs.kind == rhs.kind + && lhs.color == rhs.color +} diff --git a/Sources/SwiftChessNeo/Player.swift b/Sources/SwiftChessNeo/Player.swift new file mode 100644 index 0000000..45f3145 --- /dev/null +++ b/Sources/SwiftChessNeo/Player.swift @@ -0,0 +1,82 @@ +// +// Player.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 player. +public struct Player: Equatable, CustomStringConvertible { + + /// A player kind. + public enum Kind: String, CustomStringConvertible { + + /// Human player kind. + case human = "Human" + + /// Computer player kind. + case computer = "Computer" + + /// Boolean indicating `self` is a human. + public var isHuman: Bool { + return self == .human + } + + /// Boolean indicating `self` is a computer. + public var isComputer: Bool { + return self == .computer + } + + /// A textual representation of this instance. + public var description: String { + return rawValue + } + + } + + /// The player's kind. + public var kind: Kind + + /// The player's name. + public var name: String? + + /// The player's elo rating. + public var elo: UInt? + + /// A textual representation of this instance. + public var description: String { + return "Player(kind: \(kind), name: \(name._altDescription), elo: \(elo._altDescription))" + } + + /// Create a player with `kind` and `name`. + /// + /// - parameter kind: The player's kind. Default is human. + /// - parameter name: The player's name. Default is `nil`. + /// - parameter elo: The player's elo rating. Default is `nil`. + public init(kind: Kind = .human, name: String? = nil, elo: UInt? = nil) { + self.kind = kind + self.name = name + self.elo = elo + } + +} + +/// Returns `true` if the players are the same. +public func == (lhs: Player, rhs: Player) -> Bool { + return lhs.kind == rhs.kind + && lhs.name == rhs.name + && lhs.elo == rhs.elo +} diff --git a/Sources/SwiftChessNeo/Rank.swift b/Sources/SwiftChessNeo/Rank.swift new file mode 100644 index 0000000..9998e41 --- /dev/null +++ b/Sources/SwiftChessNeo/Rank.swift @@ -0,0 +1,147 @@ +// +// Rank.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 board rank. +/// +/// `Rank`s refer to the eight rows of a chess board, beginning with 1 at the bottom and ending with 8 at the top. +public enum Rank: Int, Comparable, CustomStringConvertible { + + /// A direction in rank. + public enum Direction { + + /// Up direction. + case up + + /// Down direction. + case down + + } + + /// Rank 1. + case one = 1 + + /// Rank 2. + case two = 2 + + /// Rank 3. + case three = 3 + + /// Rank 4. + case four = 4 + + /// Rank 5. + case five = 5 + + /// Rank 6. + case six = 6 + + /// Rank 7. + case seven = 7 + + /// Rank 8. + case eight = 8 + +} + +extension Rank { + + /// An array of all ranks. + public static let all: [Rank] = [1, 2, 3, 4, 5, 6, 7, 8] + + /// The row index of `self`. + public var index: Int { + return rawValue - 1 + } + + /// A textual representation of `self`. + public var description: String { + return String(rawValue) + } + + /// Create an instance from an integer value. + public init?(_ value: Int) { + self.init(rawValue: value) + } + + /// Create a `Rank` from a zero-based row index. + public init?(index: Int) { + self.init(rawValue: index + 1) + } + + /// Creates the starting `Rank` for the color. + public init(startFor color: Color) { + self = color.isWhite ? 1 : 8 + } + + /// Creates the ending `Rank` for the color. + public init(endFor color: Color) { + self = color.isWhite ? 8 : 1 + } + + /// Returns a rank from advancing `self` by `value` with respect to `color`. + public func advanced(by value: Int, for color: Color = .white) -> Rank? { + return Rank(rawValue: rawValue + (color.isWhite ? value : -value)) + } + + /// The next rank after `self`. + public func next() -> Rank? { + return Rank(rawValue: (rawValue + 1)) + } + + /// The previous rank to `self`. + public func previous() -> Rank? { + return Rank(rawValue: (rawValue - 1)) + } + + /// The opposite rank of `self`. + public func opposite() -> Rank { + return Rank(rawValue: 9 - rawValue)! + } + + /// The files from `self` to `other`. + public func to(_ other: Rank) -> [Rank] { + return _to(other) + } + + /// The files between `self` and `other`. + public func between(_ other: Rank) -> [Rank] { + return _between(other) + } + +} + +extension Rank: ExpressibleByIntegerLiteral { } + +extension Rank { + + /// Create an instance initialized to `value`. + public init(integerLiteral value: Int) { + guard let rank = Rank(rawValue: value) else { + fatalError("Rank value not within 1 and 8, inclusive") + } + self = rank + } + +} + +/// Returns `true` if one rank is higher than the other. +public func < (lhs: Rank, rhs: Rank) -> Bool { + return lhs.rawValue < rhs.rawValue +} diff --git a/Sources/SwiftChessNeo/Sequence+Sage.swift b/Sources/SwiftChessNeo/Sequence+Sage.swift new file mode 100644 index 0000000..19aaa67 --- /dev/null +++ b/Sources/SwiftChessNeo/Sequence+Sage.swift @@ -0,0 +1,33 @@ +// +// Sequence+Sage.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. +// + +extension Sequence where Iterator.Element == Square { + + /// Returns moves from `square` to the squares in `self`. + public func moves(from square: Square) -> [Move] { + return self.map({ square >>> $0 }) + } + + /// Returns moves from the squares in `self` to `square`. + public func moves(to square: Square) -> [Move] { + return self.map({ $0 >>> square }) + } + +} diff --git a/Sources/SwiftChessNeo/Square.swift b/Sources/SwiftChessNeo/Square.swift new file mode 100644 index 0000000..8dbc17c --- /dev/null +++ b/Sources/SwiftChessNeo/Square.swift @@ -0,0 +1,428 @@ +// +// Square.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 pair of a chess board `File` and `Rank`. +public typealias Location = (file: File, rank: Rank) + +/// A chess board square. +/// +/// A `Square` can be one of sixty-four possible values, ranging from `A1` to `H8`. +public enum Square: Int, CustomStringConvertible { + + /// A1 square. + case a1 + + /// B1 square. + case b1 + + /// C1 square. + case c1 + + /// D1 square. + case d1 + + /// E1 square. + case e1 + + /// F1 square. + case f1 + + /// G1 square. + case g1 + + /// H1 square. + case h1 + + /// A2 square. + case a2 + + /// B2 square. + case b2 + + /// C2 square. + case c2 + + /// D2 square. + case d2 + + /// E2 square. + case e2 + + /// F2 square. + case f2 + + /// G2 square. + case g2 + + /// H2 square. + case h2 + + /// A3 square. + case a3 + + /// B3 square. + case b3 + + /// C3 square. + case c3 + + /// D3 square. + case d3 + + /// E3 square. + case e3 + + /// F3 square. + case f3 + + /// G3 square. + case g3 + + /// H3 square. + case h3 + + /// A4 square. + case a4 + + /// B4 square. + case b4 + + /// C4 square. + case c4 + + /// D4 square. + case d4 + + /// E4 square. + case e4 + + /// F4 square. + case f4 + + /// G4 square. + case g4 + + /// H4 square. + case h4 + + /// A5 square. + case a5 + + /// B5 square. + case b5 + + /// C5 square. + case c5 + + /// D5 square. + case d5 + + /// E5 square. + case e5 + + /// F5 square. + case f5 + + /// G5 square. + case g5 + + /// H5 square. + case h5 + + /// A6 square. + case a6 + + /// B6 square. + case b6 + + /// C6 square. + case c6 + + /// D6 square. + case d6 + + /// E6 square. + case e6 + + /// F6 square. + case f6 + + /// G6 square. + case g6 + + /// H6 square. + case h6 + + /// A7 square. + case a7 + + /// B7 square. + case b7 + + /// C7 square. + case c7 + + /// D7 square. + case d7 + + /// E7 square. + case e7 + + /// F7 square. + case f7 + + /// G7 square. + case g7 + + /// H7 square. + case h7 + + /// A8 square. + case a8 + + /// B8 square. + case b8 + + /// C8 square. + case c8 + + /// D8 square. + case d8 + + /// E8 square. + case e8 + + /// F8 square. + case f8 + + /// G8 square. + case g8 + + /// H8 square. + case h8 + +} + +extension Square { + + /// An array of all squares. + public static let all: [Square] = (0 ..< 64).compactMap(Square.init(rawValue:)) + + /// The file of `self`. + public var file: File { + get { + return File(index: rawValue & 7)! + } + set(newFile) { + self = Square(file: newFile, rank: rank) + } + } + + /// The rank of `self`. + public var rank: Rank { + get { + return Rank(index: rawValue >> 3)! + } + set(newRank) { + self = Square(file: file, rank: newRank) + } + } + + /// The location of `self`. + public var location: Location { + get { + return (file, rank) + } + set(newLocation) { + self = Square(location: newLocation) + } + } + + /// The square's color. + public var color: Color { + return (file.index & 1 != rank.index & 1) ? ._white : ._black + } + + /// A textual representation of `self`. + public var description: String { + return "\(file)\(rank)" + } + + /// Create a square from `file` and `rank`. + public init(file: File, rank: Rank) { + self.init(rawValue: file.index + (rank.index << 3))! + } + + /// Create a square from `location`. + public init(location: Location) { + self.init(file: location.file, rank: location.rank) + } + + /// Create a square from `file` and `rank`. Returns `nil` if either is `nil`. + public init?(file: File?, rank: Rank?) { + guard let file = file, let rank = rank else { + return nil + } + self.init(file: file, rank: rank) + } + + /// Create a square from `string`. + public init?(_ string: String) { + guard string.count == 2 else { + return nil + } + guard let file = File(string.first!) else { + return nil + } + guard let rank = Int(String(string.last!)).flatMap(Rank.init(_:)) else { + return nil + } + self.init(file: file, rank: rank) + } + + /// Returns the squares between `self` and `other`. + public func between(_ other: Square) -> Bitboard { + return _betweenTable[_triangleIndex(self, other)] + } + + /// Returns the squares along the line with `other`. + public func line(with other: Square) -> Bitboard { + return _lineTable[_triangleIndex(self, other)] + } + + /// Returns `true` if `self` is between `start` and `end`. + public func isBetween(start: Square, end: Square) -> Bool { + return start.between(end)[self] + } + + /// Returns `true` if `self` is aligned with `first` and `second`. + public func isAligned(with first: Square, and second: Square) -> Bool { + return line(with: first)[second] + || line(with: second)[first] + || (self == first && self == second) + } + + /// Returns `true` if `self` is aligned with `first` and `rest`. + public func isAligned(with first: Square, _ rest: Square...) -> Bool { + var line = self == first ? Bitboard(square: self) : self.line(with: first) + for square in rest where square != self { + if line == Bitboard(square: self) { + line = self.line(with: square) + } + guard line[square] else { + return false + } + } + return !line.isEmpty + } + + /// Returns `true` if `self` is aligned with `squares`. + public func isAligned(with squares: S) -> Bool where S.Iterator.Element == Square { + var line: Bitboard? = nil + let bitboard = Bitboard(square: self) + for square in squares { + if let lineBitboard = line { + if lineBitboard == bitboard { + line = self.line(with: square) + } else { + guard lineBitboard[square] else { + return false + } + } + } else if square == self { + line = bitboard + } else { + line = self.line(with: square) + } + } + return line?.isEmpty == false + } + + /// Returns a bitboard mask of attacks for a king at `self`. + public func kingAttacks() -> Bitboard { + return _kingAttackTable[rawValue] + } + + /// Returns a bitboard mask of attacks for a knight at `self`. + public func knightAttacks() -> Bitboard { + return _knightAttackTable[rawValue] + } + + /// Returns a bitboard mask of attacks for a piece at `self`. + /// + /// - parameter piece: The piece for the attacks. + /// - parameter stoppers: The pieces stopping a sliding move. The returned bitboard includes the stopped space. + /// + /// - seealso: `attackMoves(for:stoppers:)` + public func attacks(for piece: Piece, stoppers: Bitboard = 0) -> Bitboard { + switch piece.kind { + case .king: + return kingAttacks() + case .knight: + return knightAttacks() + case .pawn: + return _pawnAttackTable(for: piece.color)[rawValue] + default: + return Bitboard(square: self)._attacks(for: piece, stoppers: stoppers) + } + } + + /// Returns an array of attack moves for a piece at `self`. + /// + /// - seealso: `attacks(for:stoppers:)` + public func attackMoves(for piece: Piece, stoppers: Bitboard = 0) -> [Move] { + return attacks(for: piece, stoppers: stoppers).moves(from: self) + } + + /// Returns moves from the squares in `squares` to `self`. + public func moves(from squares: S) -> [Move] where S.Iterator.Element == Square { + return squares.moves(to: self) + } + + /// Returns moves from `self` to the squares in `squares`. + public func moves(to squares: S) -> [Move] where S.Iterator.Element == Square { + return squares.moves(from: self) + } + +} + +extension Square: ExpressibleByStringLiteral { } + +extension Square { + + /// Create an instance initialized to `value`. + public init(stringLiteral value: String) { + guard let square = Square(value) else { + fatalError("Invalid string for square: \"\(value)\"") + } + self = square + } + + /// Create an instance initialized to `value`. + public init(unicodeScalarLiteral value: String) { + self.init(stringLiteral: value) + } + + /// Create an instance initialized to `value`. + public init(extendedGraphemeClusterLiteral value: String) { + self.init(stringLiteral: value) + } + +} diff --git a/Sources/SwiftChessNeo/Tables.swift b/Sources/SwiftChessNeo/Tables.swift new file mode 100644 index 0000000..afad90d --- /dev/null +++ b/Sources/SwiftChessNeo/Tables.swift @@ -0,0 +1,114 @@ +// +// Tables.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. +// + +/// Returns the pawn attack table for `color`. +internal func _pawnAttackTable(for color: Color) -> [Bitboard] { + if color.isWhite { + return _whitePawnAttackTable + } else { + return _blackPawnAttackTable + } +} + +/// A lookup table of all white pawn attack bitboards. +internal let _whitePawnAttackTable = Square.all.map { square in + return Bitboard(square: square)._pawnAttacks(for: ._white) +} + +/// A lookup table of all black pawn attack bitboards. +internal let _blackPawnAttackTable = Square.all.map { square in + return Bitboard(square: square)._pawnAttacks(for: ._black) +} + +/// A lookup table of all king attack bitboards. +internal let _kingAttackTable = Square.all.map { square in + return Bitboard(square: square)._kingAttacks() +} + +/// A lookup table of all knight attack bitboards. +internal let _knightAttackTable = Square.all.map { square in + return Bitboard(square: square)._knightAttacks() +} + +/// Returns the squares between `start` and `end`. +private func _between(_ start: Square, _ end: Square) -> Bitboard { + let start = UInt64(start.hashValue) + let end = UInt64(end.hashValue) + let max = UInt64.max + let a2a7: UInt64 = 0x0001010101010100 + let b2g7: UInt64 = 0x0040201008040200 + let h1b7: UInt64 = 0x0002040810204080 + + let between = (max << start) ^ (max << end) + let file = (end & 7) &- (start & 7) + let rank = ((end | 7) &- start) >> 3 + + var line = ((file & 7) &- 1) & a2a7 + line += 2 &* (((rank & 7) &- 1) >> 58) + line += (((rank &- file) & 15) &- 1) & b2g7 + line += (((rank &+ file) & 15) &- 1) & h1b7 + line = line &* (between & (0 &- between)) + + return Bitboard(rawValue: line & between) +} + +/// Returns the triangle index for `start` and `end`. +internal func _triangleIndex(_ start: Square, _ end: Square) -> Int { + var a = start.hashValue + var b = end.hashValue + var d = a &- b + d &= d >> 31 + b = b &+ d + a = a &- d + b = b &* (b ^ 127) + return (b >> 1) + a +} + +/// A lookup table of squares between two squares. +internal let _betweenTable: [Bitboard] = { + var table = [Bitboard](repeating: 0, count: 2080) + for start in Square.all { + for end in Square.all { + let index = _triangleIndex(start, end) + table[index] = _between(start, end) + } + } + return table +}() + +/// A lookup table of lines for two squares. +internal let _lineTable: [Bitboard] = { + var table = [Bitboard](repeating: 0, count: 2080) + for start in Square.all { + for end in Square.all { + let startBB = Bitboard(square: start) + let endBB = Bitboard(square: end) + let index = _triangleIndex(start, end) + let rookAttacks = startBB._rookAttacks() + let bishopAttacks = startBB._bishopAttacks() + if rookAttacks[end] { + table[index] = startBB | endBB | (rookAttacks & endBB._rookAttacks()) + } else if bishopAttacks[end] { + table[index] = startBB | endBB | (bishopAttacks & endBB._bishopAttacks()) + } + } + } + return table +}() diff --git a/Sources/SwiftChessNeo/Variant.swift b/Sources/SwiftChessNeo/Variant.swift new file mode 100644 index 0000000..70f7b71 --- /dev/null +++ b/Sources/SwiftChessNeo/Variant.swift @@ -0,0 +1,46 @@ +// +// Variant.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 variant that defines how a `Board` is populated or how a `Game` is played. +public enum Variant { + + /// Standard chess. + case standard + + /// Upside down chess where the piece colors swap starting squares. + case upsideDown + + /// Standard regardless of Swift version. + internal static let _standard = Variant.standard + + /// UpsideDown regardless of Swift version. + internal static let _upsideDown = Variant.upsideDown + + /// `self` is standard variant. + public var isStandard: Bool { + return self == ._standard + } + + /// `self` is upside down variant. + public var isUpsideDown: Bool { + return self == ._upsideDown + } + +} -- cgit v1.2.3