PGN¶
ChessCore's PGN support consists of several cooperating types: a game model, a parser, a token-level exporter, and an escape-safe tag codec.
| Type | Role |
|---|---|
PGNGame |
one parsed game: ordered tags, mainline SANs, token stream, result |
PGNParser |
tokenize, parse multi-game text, replay to a Sendable snapshot |
PGNExporter |
serialize a game's tokens back to movetext |
PGNToken |
the token alphabet (move / variation / comment / NAG) |
GameTagCodec |
escape-safe key=value;… codec for the tag set |
Parse a PGN string¶
PGNParser.parse(_:) parses a full multi-game PGN — tags plus movetext — and
returns one PGNGame per game:
let pgn = """
[Event "World Championship"]
[White "Carlsen, Magnus"]
[Black "Nepomniachtchi, Ian"]
[Result "1-0"]
1. e4 e5 2. Nf3 Nc6 3. Bb5 a6 4. Ba4 Nf6 5. O-O 1-0
"""
let games = PGNParser.parse(pgn)
let game = games[0]
print(game.white) // "Carlsen, Magnus"
print(game.black) // "Nepomniachtchi, Ian"
print(game.resultText) // "1-0"
print(game.opening) // Opening tag, falling back to ECO
print(game.moveCount) // full-move count
print(game.moves) // ["e4", "e5", "Nf3", "Nc6", "Bb5", ...]
PGNGame exposes convenience accessors (white, black, date, event,
resultText, opening, moveCount) that return sensible defaults when a tag is
missing.
Ordered tags¶
for key in game.tags.orderedKeys { // seven-tag roster first, then the rest
print(key, "=", game.tags[key] ?? "")
}
PGNGame.OrderedTags preserves insertion order; assigning nil removes a key.
Replay a mainline into board snapshots¶
To follow a game move by move with full positions, replay it into a
ParsedMainLine — a Sendable value containing a starting Position and an
array of MainLineMoveSnapshot:
let line: ParsedMainLine = PGNParser.parseMainLineSnapshot(from: game)
for snap in line.moves {
print(snap.notation,
snap.positionBefore.fen, "->", snap.positionAfter.fen)
if let annotation = snap.annotation { print(" ", annotation.rawValue) }
if let eval = snap.engineEval { print(" eval", eval) }
if let best = snap.engineBestMoveUCI { print(" best", best) }
if let clk = snap.clockSeconds { print(" clock", clk) }
}
Each snapshot carries the parsed Move, its SAN, the position before and after,
the MoveAnnotation (from !?-style suffixes or NAGs), any inline comment, and
engine evaluation, best-move, and clock data extracted from the comment.
What a comment yields¶
PGNParser.parseEngineComment(_:) splits a { … } comment into the engine data
it carries and the prose left over. Two vocabularies are understood:
- The
[%key value]command syntax the PGN specification reserves. ChessCore reads[%clk H:MM:SS]for a clock reading and[%eval …]for an evaluation — either a signed decimal in pawns ([%eval -1.42]) or a mate distance ([%eval #-3], reported as"-M3"). - The dialect
PGNExporterwrites — an evaluation, thenbest <SAN>, then free prose, separated by semicolons:{+0.34; best Nf3; solid}. That is this library's own format, not a standard, and it round-trips what this library exports.
Everything else comes back untouched as prose. An evaluation token must contain
a digit, so the Informant symbols (+-, -+, +/-) survive in a reader's
comment rather than being consumed as evaluations. A [%eval …] tag wins over a
bare token in the same comment.
Off the main actor¶
Both ParsedMainLine and MainLineMoveSnapshot are Sendable, so a large parse
can run on a detached task:
let snapshot = await Task.detached(priority: .userInitiated) {
PGNParser.mainLineSnapshot(fromMoveText: rawMoveText)
}.value
Parse a single move¶
var position = Position.initial()
if let move = PGNParser.parseMove("e4", in: position) {
MoveGenerator.applyMoveUnchecked(&position, move)
}
parseMove handles castling (O-O / 0-0), promotion (=Q and the lenient
Q suffix), disambiguation, and captures. A promotion suffix is required;
omitting it does not silently choose a queen.
Write PGN¶
tokenText(from:) serializes the game's moveTokens (falling back to its flat
moves list), including {} comments and $n NAGs.
PGN text → live Game → PGN text¶
The snapshot path above produces an immutable ParsedMainLine. For a live,
mutable game tree use PGNParser.loadGame and PGNExporter.export — the
round-trip that bridges PGN text to a Game instance and back.
// Parse PGN text directly into a live Game (first game in the string):
if let game = PGNParser.loadGame(from: pgnString) {
// game is a fully-hydrated Game with a MoveNode tree,
// ready for navigation, annotation, and analysis.
game.undoMove()
game.setAnnotation(.brilliant, on: game.currentNode!)
}
// If you already have a PGNGame from PGNParser.parse(_:):
if let game = PGNParser.loadGame(from: pgnGame) {
// SetUp/FEN tags are honored: non-standard start positions work.
}
loadGame(from:) returns nil only when the PGN is syntactically invalid or a
SetUp/FEN tag contains a malformed FEN string. A valid-but-empty game (no
moves) returns an empty Game.
Export a live Game to PGN¶
// Full PGN: seven-tag roster + move text (with variations and annotations):
let pgn = game.exportPGN()
// Export with custom tags:
var tags = PGNGame.OrderedTags()
tags["Event"] = "Club Championship"
tags["White"] = "Alice"
tags["Black"] = "Bob"
let pgn2 = PGNExporter.export(game: game, tags: tags)
// Move text only (no headers) — useful for embedding in a larger document:
let text = PGNExporter.moveText(for: game.rootChildren)
PGNExporter.export writes variations with ( … ) brackets, annotation
suffixes, and inline comments that include engine eval, best-move, clock time,
and user comments — the same comment format that loadGame reads back.
The tag codec¶
GameTagCodec encodes a full PGN tag set as a single escape-safe
key=value;key=value string, suitable for compact per-game storage:
let encoded = GameTagCodec.encode(game.tags) // "Event=...;White=...;..."
let tags = GameTagCodec.decodeOrdered(encoded) // back to OrderedTags
let dict = GameTagCodec.decode(encoded) // unordered [String: String]
let white = GameTagCodec.firstValue(forKey: "White", in: encoded)
decodeOrdered preserves the original key order for faithful re-export, while
decode returns an unordered dictionary for cases where order is not significant.