Skip to content

Commit

Permalink
Implement PGN parsing of tag pairs (#9)
Browse files Browse the repository at this point in the history
* Added support for tag pairs in `PGNParser` and therefore `Game(pgn:)`.
* Utilities new public `Game.Tags` struct with support for mandatory and
other common tags, as well as any custom tags.

closes #8
  • Loading branch information
pdil authored Apr 14, 2024
2 parents 58ed80d + 78686dc commit 11063eb
Show file tree
Hide file tree
Showing 8 changed files with 433 additions and 22 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
# [unreleased]

#### Improvements
* PGN parsing now supports tag pairs (for example `[Event "Name"]`) located at the top of the PGN format, see [Issue #8](https://github.com/chesskit-app/chesskit-swift/issues/8).

# ChessKit 0.4.0
Released Saturday, April 13, 2024.

Expand Down
3 changes: 2 additions & 1 deletion Sources/ChessKit/Bitboards/Attacks.swift
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,7 @@ struct Magic {

extension [Magic] {
func attacks(from square: Square, for occupancy: Bitboard) -> Bitboard {
self[square.rawValue].attacks(for: occupancy)
guard square.rawValue < count else { return 0 }
return self[square.rawValue].attacks(for: occupancy)
}
}
209 changes: 206 additions & 3 deletions Sources/ChessKit/Game.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,29 @@ import Foundation
/// chess game within `ChessKit`. It provides methods for
/// making moves and publishes the played moves in an observable way.
///
public class Game: Equatable, ObservableObject {
public class Game: ObservableObject {

// MARK: - Properties

/// The move tree representing all moves made in the game.
@Published public private(set) var moves: MoveTree
/// A dictionary of every position in the game, keyed by move index.
public private(set) var positions: [MoveTree.Index: Position]

/// Contains the tag pairs for this game.
public var tags: Tags

// MARK: - Initializer

/// Initialize a game with a starting position.
///
/// - parameter position: The starting position of the game.
/// Defaults to the starting position.
///
public init(startingWith position: Position = .standard) {
public init(startingWith position: Position = .standard, tags: Tags? = nil) {
moves = MoveTree()
positions = [.minimum: position]
self.tags = tags ?? .init()
}

/// Initialize a game with a PGN string.
Expand All @@ -41,8 +49,11 @@ public class Game: Equatable, ObservableObject {

moves = parsed.moves
positions = parsed.positions
tags = parsed.tags
}

// MARK: - Moves

/// Perform the provided move in the game.
///
/// - parameter move: The move to perform.
Expand Down Expand Up @@ -165,9 +176,201 @@ public class Game: Equatable, ObservableObject {
PGNParser.convert(game: self)
}

// MARK: Equatable
}

// MARK: - Equatable

extension Game: Equatable {

public static func == (lhs: Game, rhs: Game) -> Bool {
lhs.moves == rhs.moves && lhs.positions == rhs.positions
}

}

// MARK: - Tags

extension Game {

/// Denotes a PGN tag pair.
@propertyWrapper
public struct Tag {

/// The name of the tag pair.
///
/// Used as the key in a PGN tag pair.
var name: String

/// The value of the tag pair.
///
/// Appears at the top of the PGN after the
/// corresponding `name`, within brackets.
public var wrappedValue: String = ""

/// The projected value of this `Tag` object.
public var projectedValue: Tag { self }

/// The PGN representation of this tag.
///
/// Formatted as `[Name "Value"]`.
public var pgn: String {
if wrappedValue.isEmpty {
return ""
} else {
return "[\(name) \"\(wrappedValue)\"]"
}
}

}

/// Contains the PGN tag pairs for a game.
public struct Tags {

/// Whether or not all the standard mandatory tags for
/// PGN archival are set.
///
/// These include `event`, `site`, `date`, `round`,
/// `white`, `black`, and `result` (known as the "Seven Tag Roster").
public var isValid: Bool {
[event, site, date, round, white, black, result]
.allSatisfy { !$0.isEmpty }
}

/// Name of the tournament or match event.
///
/// Example: `"F/S Return Match"`
@Tag(name: "Event")
public var event: String

/// Location of the event.
///
/// Example: `"Belgrade, Serbia JUG"`
///
/// The format for this value is "City, Region COUNTRY",
/// where "COUNTRY" is the three-letter International Olympic Committee
/// code for the country.
///
/// Although not part of the specification, some online chess platforms
/// will include a URL or website as the site value.
@Tag(name: "Site")
public var site: String

/// Starting date of the game, in YYYY.MM.DD format.
///
/// Example: `"1992.11.04"`
///
/// `"??"` is used for unknown values.
@Tag(name: "Date")
public var date: String

/// Playing round ordinal of the game within the event.
///
/// Example `"29"`
@Tag(name: "Round")
public var round: String

/// Player of the white pieces, in "Lastname, Firstname" format.
///
/// Example: `"Fischer, Robert J."`
@Tag(name: "White")
public var white: String

/// Player of the black pieces, in "Lastname, Firstname" format.
///
/// Example: `"Spassky, Boris V."`
@Tag(name: "Black")
public var black: String

/// Result of the game.
///
/// Example: `"1/2-1/2"`
///
/// It is recorded as White score, dash, then Black score, or `*` (other, e.g., the game is ongoing).
@Tag(name: "Result")
public var result: String

/// The person providing notes to the game. (optional)
@Tag(name: "Annotator")
public var annotator: String

/// String value denoting the total number of half-moves played. (optional)
@Tag(name: "PlyCount")
public var plyCount: String

/// e.g. 40/7200:3600 (moves per seconds: sudden death seconds) (optional)
@Tag(name: "TimeControl")
public var timeControl: String

/// Time the game started, in HH:MM:SS format, in local clock time. (optional)
@Tag(name: "Time")
public var time: String

/// Gives more details about the termination of the game. It may be abandoned, adjudication (result determined by third-party adjudication), death, emergency, normal, rules infraction, time forfeit, or unterminated. (optional)
@Tag(name: "Termination")
public var termination: String

/// The mode of play used for the game. (optional)
///
/// `"OTB"` (over-the-board) or `"ICS"` (Internet Chess Server)
@Tag(name: "Mode")
public var mode: String

/// The initial position of the chessboard, in Forsyth–Edwards Notation. (optional)
///
/// This is used to record partial games (starting at some initial position). It is also necessary for chess variants such as Chess960, where the initial position is not always the same as traditional chess.
///
/// If a FEN tag is used, a separate tag pair SetUp must also appear and have its value set to 1.
@Tag(name: "FEN")
public var fen: String

@Tag(name: "SetUp")
public var setUp: String

/// Extra custom tags.
///
/// The key will be used as the tag name in the PGN.
public var other: [String: String] = [:]

/// Initializes a `Game.Tags` object with the provided
/// values.
///
/// For initialization purposes, all values are optional,
/// and any omitted values will be set to empty strings.
public init(
event: String = "",
site: String = "",
date: String = "",
round: String = "",
white: String = "",
black: String = "",
result: String = "",
annotator: String = "",
plyCount: String = "",
timeControl: String = "",
time: String = "",
termination: String = "",
mode: String = "",
fen: String = "",
setUp: String = "",
other: [String : String] = [:]
) {
self.event = event
self.site = site
self.date = date
self.round = round
self.white = white
self.black = black
self.result = result
self.annotator = annotator
self.plyCount = plyCount
self.timeControl = timeControl
self.time = time
self.termination = termination
self.mode = mode
self.fen = fen
self.setUp = setUp
self.other = other
}
}

}
4 changes: 4 additions & 0 deletions Sources/ChessKit/Parsers/PGNParser+Regex.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ extension PGNParser {

/// Contains useful regex strings for PGN parsing.
struct Regex {
// tag pair components
static let tags = #"\[[^\]]+\]"#
static let tagPair = #"\[([^"]+?)\s"([^"]+)"\]"#

// move pair components
static let number = #"\d{1,}\.{1,3}"#
static let castle = #"[Oo0]-[Oo0](-[Oo0])"#
Expand Down
Loading

0 comments on commit 11063eb

Please sign in to comment.