Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 13 additions & 5 deletions Sources/FischerCore/PGN/Model/PGNGame.swift
Original file line number Diff line number Diff line change
Expand Up @@ -84,15 +84,23 @@ public extension Game {
try Move(game: self, sanMove: sanMove, considerHalfmoves: considerHalfmoves)
}

mutating func execute(move: SANMove, considerHalfmoves: Bool = true) throws {
let transformedMove = try transform(sanMove: move, considerHalfmoves: considerHalfmoves)
if case let .san(defaultMove) = move, let promotion = defaultMove.promotionTo {
try execute(move: transformedMove, considerHalfmoves: considerHalfmoves, promotion: promotion)
/// Resolves a SAN move against the current game state without executing it.
///
/// Use the returned value with `execute(resolvedMove:considerHalfmoves:)`
/// to avoid transforming the same SAN move more than once.
func resolvedMove(from sanMove: SANMove, considerHalfmoves: Bool = true) throws -> ResolvedMove {
let transformedMove = try transform(sanMove: sanMove, considerHalfmoves: considerHalfmoves)
if case let .san(defaultMove) = sanMove, let promotion = defaultMove.promotionTo {
return ResolvedMove(move: transformedMove, promotion: promotion)
} else {
try execute(move: transformedMove, considerHalfmoves: considerHalfmoves)
return ResolvedMove(move: transformedMove)
}
}

mutating func execute(move: SANMove, considerHalfmoves: Bool = true) throws {
try execute(resolvedMove: resolvedMove(from: move, considerHalfmoves: considerHalfmoves), considerHalfmoves: considerHalfmoves)
}

mutating func execute(san: String, considerHalfmoves: Bool = true) throws {
try execute(move: SANMove(san: san), considerHalfmoves: considerHalfmoves)
}
Expand Down
44 changes: 44 additions & 0 deletions Tests/FischerCoreTests/GameConvenienceTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,26 @@ final class GameConvenienceTests {
}
}

@Test("Game resolves SAN moves once for later execution")
func resolvesSANMoves() throws {
var game = Game()
let normal = try game.resolvedMove(from: SANMove(san: "e4"))
#expect(normal == ResolvedMove(move: .e2 >>> .e4))
try game.execute(resolvedMove: normal)

let capturePosition = try Game(with: "8/8/8/3pP3/8/8/8/4K2k w - d6 0 1")
let enPassant = try capturePosition.resolvedMove(from: SANMove(san: "exd6"))
#expect(enPassant == ResolvedMove(move: .e5 >>> .d6))

let promotionPosition = try Game(with: "8/k6P/8/8/8/8/K6p/8 w - - 0 1")
let promotion = try promotionPosition.resolvedMove(from: SANMove(san: "h8=N"))
#expect(promotion == ResolvedMove(move: .h7 >>> .h8, promotion: .knight))

let castlingPosition = try Game(with: "r3k2r/8/8/8/8/8/8/R3K2R w KQkq - 0 1")
let castling = try castlingPosition.resolvedMove(from: SANMove(san: "O-O"))
#expect(castling == ResolvedMove(move: .e1 >>> .g1))
}

@Test("Game exposes outcome when finished")
func outcome() throws {
var game = Game()
Expand Down Expand Up @@ -79,6 +99,30 @@ final class GameConvenienceTests {
#expect(game.moveHistory.isEmpty)
}

@Test("Game jump preserves FEN, history, and piece identity")
func jumpToMovePreservesGameState() throws {
var jumped = Game()
var replayed = Game()
let ePawnID = try #require(jumped.pieceID(at: .e2))

try jumped.execute(move: .e2 >>> .e4)
try jumped.execute(move: .d7 >>> .d5)
try jumped.execute(move: .e4 >>> .d5)
try jumped.execute(move: .g8 >>> .f6)

try replayed.execute(move: .e2 >>> .e4)
try replayed.execute(move: .d7 >>> .d5)

jumped.jumpToMove(2)
#expect(jumped.position.fen() == replayed.position.fen())
#expect(jumped.moveHistory.map(\.move) == replayed.moveHistory.map(\.move))
#expect(jumped.pieceID(at: .e4) == ePawnID)

jumped.jumpToMove(4)
#expect(jumped.moveHistory.map(\.move) == [.e2 >>> .e4, .d7 >>> .d5, .e4 >>> .d5, .g8 >>> .f6])
#expect(jumped.pieceID(at: .d5) == ePawnID)
}

@Test("Game returns structured SAN moves")
func playedSANMoves() throws {
var game = try Game(with: "rnbqkbnr/pppppppp/8/8/8/1P6/P1PPPPPP/RNBQKBNR b KQkq - 0 1")
Expand Down
Loading