aboutsummaryrefslogtreecommitdiff
path: root/Sources/SwiftChessNeo/PGNMove.swift
diff options
context:
space:
mode:
authorNavan Chauhan <navanchauhan@gmail.com>2024-04-17 12:02:38 -0600
committerNavan Chauhan <navanchauhan@gmail.com>2024-04-17 12:02:38 -0600
commite7e30f219c0129db7cb72f04e200098417ce25d0 (patch)
tree3d34da76f3a45e93d4d2fc31aad13a3712208e53 /Sources/SwiftChessNeo/PGNMove.swift
parentebfdb87512e7e32787b71e7a162ff109584f6fcf (diff)
initial commit
Diffstat (limited to 'Sources/SwiftChessNeo/PGNMove.swift')
-rw-r--r--Sources/SwiftChessNeo/PGNMove.swift219
1 files changed, 219 insertions, 0 deletions
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!
+ }
+}