Skip to content

Commit 371f718

Browse files
authored
fix: some issues (#14) (#15)
1 parent dd9e162 commit 371f718

27 files changed

+650
-295
lines changed

Sources/FischerCore/Board.swift

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -41,17 +41,10 @@ public struct Board: Equatable {
4141

4242
internal var bitboards: [Bitboard] = Array(repeating: 0, count: 12)
4343

44-
public init(variant: Variant? = .standard) {
44+
public init() {
4545
bitboards = Array(repeating: 0, count: 12)
46-
if let variant = variant {
47-
for piece in Piece.all {
48-
bitboards[piece.bitValue] = Bitboard(startFor: piece)
49-
}
50-
if variant.isUpsideDown {
51-
for index in bitboards.indices {
52-
bitboards[index].flipVertically()
53-
}
54-
}
46+
for piece in Piece.all {
47+
bitboards[piece.bitValue] = Bitboard(startFor: piece)
5548
}
5649
}
5750

Sources/FischerCore/File.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import Foundation
66
/// and initialization from characters or strings. It is used in board representation and SAN parsing.
77
///
88
/// Files are 1-based internally (`.a = 1`) but expose a zero-based `index` for array-like access.
9-
public enum File: Int {
9+
public enum File: Int, Equatable {
1010

1111
public enum Direction {
1212
case left

Sources/FischerCore/Game.swift

Lines changed: 84 additions & 173 deletions
Original file line numberDiff line numberDiff line change
@@ -30,175 +30,6 @@ public struct Game {
3030
}
3131
}
3232

33-
public enum Outcome: Hashable, CustomStringConvertible {
34-
case win(PlayerColor)
35-
case draw
36-
37-
public var description: String {
38-
if let color = winColor {
39-
return color.isWhite() ? "1-0" : "0-1"
40-
} else {
41-
return "1/2-1/2"
42-
}
43-
}
44-
45-
public var winColor: PlayerColor? {
46-
guard case let .win(color) = self else { return nil }
47-
return color
48-
}
49-
50-
public var isWin: Bool {
51-
if case .win = self { return true } else { return false }
52-
}
53-
54-
public var isDraw: Bool {
55-
return !isWin
56-
}
57-
58-
public init?(_ string: String) {
59-
let stripped = string.split(separator: " ").map(String.init).joined(separator: "")
60-
switch stripped {
61-
case "1-0":
62-
self = .win(.white)
63-
case "0-1":
64-
self = .win(.black)
65-
case "1/2-1/2":
66-
self = .draw
67-
default:
68-
return nil
69-
}
70-
}
71-
72-
public func value(for playerColor: PlayerColor) -> Double {
73-
return winColor.map({ $0 == playerColor ? 1 : 0 }) ?? 0.5
74-
}
75-
76-
}
77-
public struct Position: Equatable, CustomStringConvertible {
78-
public var board: Board
79-
public var playerTurn: PlayerColor
80-
public var castlingRights: CastlingRights
81-
public var enPassantTarget: Square?
82-
public var halfmoves: UInt
83-
public var fullmoves: UInt
84-
85-
public var description: String {
86-
return "Position(\(fen()))"
87-
}
88-
89-
public init(board: Board = Board(),
90-
playerTurn: PlayerColor = .white,
91-
castlingRights: CastlingRights = .all,
92-
enPassantTarget: Square? = nil,
93-
halfmoves: UInt = 0,
94-
fullmoves: UInt = 1) {
95-
self.board = board
96-
self.playerTurn = playerTurn
97-
self.castlingRights = castlingRights
98-
self.enPassantTarget = enPassantTarget
99-
self.halfmoves = halfmoves
100-
self.fullmoves = fullmoves
101-
}
102-
103-
public init?(fen: String) {
104-
let parts = fen.split(separator: " ").map(String.init)
105-
guard
106-
parts.count == 6,
107-
let board = Board(fen: parts[0]),
108-
parts[1].count == 1,
109-
let playerTurn = PlayerColor.init(string: parts[1]),
110-
let rights = CastlingRights(string: parts[2]),
111-
let halfmoves = UInt(parts[4]),
112-
let fullmoves = UInt(parts[5]),
113-
fullmoves > 0 else {
114-
return nil
115-
}
116-
var target: Square?
117-
let targetStr = parts[3]
118-
if targetStr.count == 2 {
119-
guard let square = Square(targetStr) else {
120-
return nil
121-
}
122-
target = square
123-
} else {
124-
guard targetStr == "-" else {
125-
return nil
126-
}
127-
}
128-
self.init(board: board,
129-
playerTurn: playerTurn,
130-
castlingRights: rights,
131-
enPassantTarget: target,
132-
halfmoves: halfmoves,
133-
fullmoves: fullmoves)
134-
}
135-
136-
func fen() -> String {
137-
return board.fen()
138-
+ " \(playerTurn.isWhite() ? "w" : "b") \(castlingRights.description) "
139-
+ (enPassantTarget.map { "\($0 as Square)".lowercased() } ?? "-")
140-
+ " \(halfmoves) \(fullmoves)"
141-
}
142-
143-
public static func == (lhs: Game.Position, rhs: Game.Position) -> Bool {
144-
return lhs.playerTurn == rhs.playerTurn
145-
&& lhs.castlingRights == rhs.castlingRights
146-
&& lhs.halfmoves == rhs.halfmoves
147-
&& lhs.fullmoves == rhs.fullmoves
148-
&& lhs.enPassantTarget == rhs.enPassantTarget
149-
&& lhs.board == rhs.board
150-
}
151-
152-
internal func validationError() -> PositionError? {
153-
for color in PlayerColor.allCases {
154-
guard board.count(of: Piece(king: color)) == 1 else {
155-
return .wrongKingCount(color)
156-
}
157-
}
158-
for right in castlingRights {
159-
let color = right.color
160-
let king = Piece(king: color)
161-
guard board.bitboard(for: king) == Bitboard(startFor: king) else {
162-
return .missingKing(right) }
163-
let rook = Piece(rook: color)
164-
let square = Square(file: right.side.isKingside ? .h : .a,
165-
rank: Rank(startFor: color))
166-
guard board.bitboard(for: rook)[square] else {
167-
return .missingRook(right)
168-
}
169-
}
170-
if let target = enPassantTarget {
171-
guard target.rank == (playerTurn.isWhite() ? 6 : 3) else {
172-
return .wrongEnPassantTargetRank(target.rank)
173-
}
174-
if let piece = board[target] {
175-
return .nonEmptyEnPassantTarget(target, piece)
176-
}
177-
let pawnSquare = Square(file: target.file, rank: playerTurn.isWhite() ? 5 : 4)
178-
guard board[pawnSquare] == Piece(pawn: playerTurn.inverse()) else {
179-
return .missingEnPassantPawn(pawnSquare)
180-
}
181-
let startSquare = Square(file: target.file, rank: playerTurn.isWhite() ? 7 : 2)
182-
if let piece = board[startSquare] {
183-
return .nonEmptyEnPassantSquare(startSquare, piece)
184-
185-
}
186-
}
187-
return nil
188-
}
189-
}
190-
191-
public enum PositionError: Error {
192-
case wrongKingCount(PlayerColor)
193-
case missingKing(CastlingRights.Right)
194-
case missingRook(CastlingRights.Right)
195-
case wrongEnPassantTargetRank(Rank)
196-
case nonEmptyEnPassantTarget(Square, Piece)
197-
case missingEnPassantPawn(Square)
198-
case nonEmptyEnPassantSquare(Square, Piece)
199-
case fenMalformed
200-
}
201-
20233
public enum ExecutionError: Error {
20334

20435
case missingPiece(Square)
@@ -288,16 +119,15 @@ public struct Game {
288119
}
289120

290121
public init(whitePlayer: String = "",
291-
blackPlayer: String = "",
292-
variant: Variant = .standard) {
122+
blackPlayer: String = "") {
293123
self.moveHistory = []
294124
self.undoHistory = []
295-
self.board = Board(variant: variant)
125+
self.board = Board()
296126
self.playerTurn = .white
297127
self.castlingRights = .all
298128
self.whitePlayer = whitePlayer
299129
self.blackPlayer = blackPlayer
300-
self.variant = variant
130+
self.variant = .standard
301131
self.attackersToKing = 0
302132
self.halfmoves = 0
303133
self.fullmoves = 1
@@ -605,3 +435,84 @@ extension Game {
605435
variant: variant)
606436
}
607437
}
438+
439+
public extension Game {
440+
func sanMove(from uci: String) throws -> SANMove {
441+
guard uci.count >= 4 else { throw FischerCoreError.illegalMove }
442+
443+
let fromString = String(uci.prefix(2))
444+
let toString = String(uci.dropFirst(2).prefix(2))
445+
let promotionChar = uci.count == 5 ? uci.last : nil
446+
447+
guard
448+
let from = Square(fromString),
449+
let to = Square(toString),
450+
let piece = board[from]
451+
else {
452+
throw FischerCoreError.illegalMove
453+
}
454+
455+
guard isLegal(move: from >>> to) else { throw FischerCoreError.illegalMove }
456+
457+
if piece.kind == .king && from.file == .e {
458+
if to.file == .g {
459+
return .kingsideCastling
460+
} else if to.file == .c {
461+
return .queensideCastling
462+
}
463+
}
464+
465+
let isCapture = self.board[to] != nil || (piece.kind == .pawn && to == enPassantTarget)
466+
467+
let promotionTo: SANMove.PromotionPiece? = {
468+
guard let char = promotionChar else { return nil }
469+
return SANMove.PromotionPiece(rawValue: String(char).uppercased())
470+
}()
471+
472+
let possibleDisambiguations = self.board.bitboard(for: piece)
473+
.filter { $0 != from }
474+
.filter {
475+
self.isLegal(move: $0 >>> to)
476+
}
477+
478+
let disambiguation: SANMove.FromPosition? = {
479+
if isCapture && piece.kind == .pawn { return .file(from.file) }
480+
guard !possibleDisambiguations.isEmpty else { return nil }
481+
let fileUnique = !possibleDisambiguations.contains(where: { $0.file == from.file && $0 != from })
482+
let rankUnique = !possibleDisambiguations.contains(where: { $0.rank == from.rank && $0 != from })
483+
484+
if fileUnique {
485+
return .file(from.file)
486+
} else if rankUnique {
487+
return .rank(from.rank)
488+
} else {
489+
return .square(from)
490+
}
491+
}()
492+
493+
var gameAfterMove = self
494+
try gameAfterMove.execute(move: Move(start: from, end: to))
495+
496+
let sanDefault = SANMove.SANDefaultMove(
497+
piece: piece.kind,
498+
from: disambiguation,
499+
isCapture: isCapture,
500+
toSquare: to,
501+
promotionTo: promotionTo,
502+
isCheck: gameAfterMove.kingIsChecked,
503+
isCheckmate: gameAfterMove.isFinished
504+
)
505+
return .san(sanDefault)
506+
}
507+
508+
func sanMoveList(from uciArray: [String]) throws -> [SANMove] {
509+
var sanMoves: [SANMove] = []
510+
var currentGame = self
511+
for uci in uciArray {
512+
let sanMove = try currentGame.sanMove(from: uci)
513+
try currentGame.execute(move: Move.init(game: currentGame, sanMove: sanMove))
514+
sanMoves.append(sanMove)
515+
}
516+
return sanMoves
517+
}
518+
}

Sources/FischerCore/Move.swift

Lines changed: 15 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -183,19 +183,19 @@ extension Sequence where Iterator.Element == Square {
183183

184184

185185
extension Move {
186-
public init(board: Board, sanMove: SANMove, turn: PlayerColor) throws {
186+
public init(game: Game, sanMove: SANMove) throws {
187187
switch sanMove {
188188
case .san(let sanDefaultMove):
189-
try self.init(board: board, sanDefaultMove: sanDefaultMove, turn: turn)
189+
try self.init(game: game, sanDefaultMove: sanDefaultMove)
190190
case .kingsideCastling:
191-
switch turn {
191+
switch game.playerTurn {
192192
case .white:
193193
self.init(start: .e1, end: .g1)
194194
case .black:
195195
self.init(start: .e8, end: .g8)
196196
}
197197
case .queensideCastling:
198-
switch turn {
198+
switch game.playerTurn {
199199
case .white:
200200
self.init(start: .e1, end: .c1)
201201
case .black:
@@ -204,46 +204,33 @@ extension Move {
204204
}
205205
}
206206

207-
init(board: Board, sanDefaultMove: SANMove.SANDefaultMove, turn: PlayerColor) throws {
208-
let piece = Piece(kind: sanDefaultMove.piece, color: turn)
209-
let bitboard = board[piece]
207+
init(game: Game, sanDefaultMove: SANMove.SANDefaultMove) throws {
208+
let piece = Piece(kind: sanDefaultMove.piece, color: game.playerTurn)
209+
let bitboard = game.board[piece]
210210
if let from = sanDefaultMove.from {
211-
guard let start = bitboard.first(where: { currentSquare in
212-
switch from {
213-
case .file(let file):
214-
currentSquare.file == file
211+
guard let start = bitboard.first(
212+
where: { currentSquare in
213+
switch from {
214+
case .file(let file):
215+
return (currentSquare.file == file) && game.isLegal(move: currentSquare >>> sanDefaultMove.toSquare)
215216
case .rank(let rank):
216-
currentSquare.rank == rank
217+
return (currentSquare.rank == rank) && game.isLegal(move: currentSquare >>> sanDefaultMove.toSquare)
217218
case .square(let sqr):
218-
sqr == currentSquare
219+
return sqr == currentSquare
219220
}
220221
}) else {
221222
throw FischerCoreError.illegalMove
222223
}
223224
self.init(start: start, end: sanDefaultMove.toSquare)
224225
} else {
225226
guard let start = bitboard.first(where: { currentSquare in
226-
Move.isLegal(start: currentSquare, end: sanDefaultMove.toSquare, piece: piece)
227+
game.isLegal(move: currentSquare >>> sanDefaultMove.toSquare)
227228
}) else {
228229
throw FischerCoreError.illegalMove
229230
}
230231
self.init(start: start, end: sanDefaultMove.toSquare)
231232
}
232233
}
233-
234-
internal static func isLegal(start: Square, end: Square, piece: Piece) -> Bool {
235-
var allEndSquareMoves = start.attacks(for: piece, stoppers: 0)
236-
if piece.kind == .pawn {
237-
let squareBitboard = Bitboard(square: start)
238-
let pushes = squareBitboard.pawnPushes(for: piece.color,
239-
empty: ~0)
240-
let doublePushes = (squareBitboard & Bitboard(startFor: piece))
241-
.pawnPushes(for: piece.color, empty: ~0)
242-
.pawnPushes(for: piece.color, empty: ~0)
243-
allEndSquareMoves = allEndSquareMoves | pushes | doublePushes
244-
}
245-
return allEndSquareMoves.contains(end)
246-
}
247234
}
248235

249236
enum FischerCoreError: Error {

0 commit comments

Comments
 (0)