From 85c7ff54b67ec0c76adddf66953e97e4a2d9096f Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 12 Jul 2026 17:57:02 +0000 Subject: [PATCH] UCI: stop FEN parsing at the "moves" keyword MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `position fen moves ...` collected up to six FEN fields with a blind prefix(6). Board accepts a short (4–5 field) FEN, but the blind take then swallowed the "moves" keyword and the move tokens into the FEN string, leaving the field index past "moves" — so the trailing moves were silently dropped and only the base FEN position was set. Collect FEN fields until the "moves" keyword (still capped at six), so short FENs keep their moves. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01E9dkcpbwWWHrdGzpvWZd1T --- ChessKit/Sources/ChessProtocol/UCI.swift | 9 ++++++--- ChessKit/Tests/ChessProtocolTests/UCITests.swift | 10 ++++++++++ 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/ChessKit/Sources/ChessProtocol/UCI.swift b/ChessKit/Sources/ChessProtocol/UCI.swift index 007f0ac..8e3abab 100644 --- a/ChessKit/Sources/ChessProtocol/UCI.swift +++ b/ChessKit/Sources/ChessProtocol/UCI.swift @@ -69,7 +69,7 @@ public final class UCIEngine { // MARK: - Command handlers - /// Parses `position [startpos | fen <6 fields>] [moves ...]`. + /// Parses `position [startpos | fen ] [moves ...]`. private func applyPosition(_ args: [String]) { var index = 0 var newBoard: Board? @@ -78,8 +78,11 @@ public final class UCIEngine { newBoard = Board() index = 1 } else if args.first == "fen" { - // A FEN is up to six space-separated fields following "fen". - let fenFields = Array(args.dropFirst().prefix(6)) + // The FEN runs until the "moves" keyword, capped at six fields. + // Stopping at "moves" is what lets a short FEN (Board accepts 4–5 + // fields, defaulting the clocks) keep its trailing moves instead of + // swallowing the "moves" token and the moves into the FEN string. + let fenFields = Array(args.dropFirst().prefix { $0 != "moves" }.prefix(6)) newBoard = Board(fen: fenFields.joined(separator: " ")) index = 1 + fenFields.count } diff --git a/ChessKit/Tests/ChessProtocolTests/UCITests.swift b/ChessKit/Tests/ChessProtocolTests/UCITests.swift index 9e601bc..e374ae6 100644 --- a/ChessKit/Tests/ChessProtocolTests/UCITests.swift +++ b/ChessKit/Tests/ChessProtocolTests/UCITests.swift @@ -42,6 +42,16 @@ final class UCITests: XCTestCase { XCTAssertNil(uci.currentBoard[Sq.parse("e2")!]) } + func testPositionShortFENWithMoves() { + // A 4-field FEN (no halfmove/fullmove counters) followed by moves. + // The "moves" keyword must not be absorbed into the FEN, or the move + // is silently dropped and e2e4 is never applied. + let uci = makeAdapter() + uci.process("position fen 4k3/8/8/8/8/8/4P3/4K3 w - - moves e2e4") + XCTAssertEqual(uci.currentBoard[Sq.parse("e4")!], Piece(color: .white, kind: .pawn)) + XCTAssertNil(uci.currentBoard[Sq.parse("e2")!]) + } + func testUCINewGameResets() { let uci = makeAdapter() uci.process("position startpos moves e2e4")