1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
|
//
// 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!
}
}
|