sgf_parser/
token.rs

1use crate::token::Action::{Move, Pass};
2use crate::token::Color::{Black, White};
3use crate::token::Outcome::{Draw, WinnerByForfeit, WinnerByPoints, WinnerByResign, WinnerByTime};
4use crate::{SgfError, SgfErrorKind};
5use std::ops::Not;
6
7/// Indicates what color the token is related to
8#[derive(Debug, PartialEq, Eq, Copy, Clone)]
9pub enum Color {
10    Black,
11    White,
12}
13
14impl Not for Color {
15    type Output = Color;
16    fn not(self) -> Color {
17        match self {
18            Color::Black => Color::White,
19            Color::White => Color::Black,
20        }
21    }
22}
23
24#[derive(Debug, PartialEq, Copy, Clone)]
25pub enum Outcome {
26    WinnerByResign(Color),
27    WinnerByForfeit(Color),
28    WinnerByPoints(Color, f32),
29    WinnerByTime(Color),
30    Draw,
31}
32
33impl Outcome {
34    pub fn get_winner(self) -> Option<Color> {
35        match self {
36            WinnerByTime(color)
37            | WinnerByForfeit(color)
38            | WinnerByPoints(color, ..)
39            | WinnerByResign(color) => Some(color),
40            _ => None,
41        }
42    }
43}
44
45///Provides the used rules for this game.
46///Because there are many different rules, SGF requires
47///mandatory names only for a small set of well known rule sets.
48///Note: it's beyond the scope of this specification to give an
49///exact specification of these rule sets.
50///Mandatory names for Go (GM[1]):
51/// "AGA" (rules of the American Go Association)
52/// "GOE" (the Ing rules of Goe)
53/// "Japanese" (the Nihon-Kiin rule set)
54/// "NZ" (New Zealand rules)
55#[derive(Debug, Clone, Eq, PartialEq)]
56pub enum RuleSet {
57    Japanese,
58    NZ,
59    GOE,
60    AGA,
61    Chinese,
62    Unknown(String),
63}
64
65impl From<&str> for RuleSet {
66    fn from(s: &str) -> Self {
67        match s {
68            "Japanese" => RuleSet::Japanese,
69            "AGA" => RuleSet::AGA,
70            "NZ" => RuleSet::NZ,
71            "Chinese" => RuleSet::Chinese,
72            "GOE" => RuleSet::GOE,
73            value => RuleSet::Unknown(value.to_owned()),
74        }
75    }
76}
77
78impl ToString for RuleSet {
79    fn to_string(&self) -> String {
80        match self {
81            RuleSet::Japanese => "Japanese",
82            RuleSet::NZ => "NZ",
83            RuleSet::GOE => "GOE",
84            RuleSet::AGA => "AGA",
85            RuleSet::Chinese => "Chinese",
86            RuleSet::Unknown(v) => v,
87        }
88        .to_owned()
89    }
90}
91
92#[derive(Copy, Clone, Eq, PartialEq, Debug)]
93pub enum Action {
94    Move(u8, u8),
95    Pass,
96}
97
98#[derive(Copy, Clone, Eq, PartialEq, Debug)]
99pub enum Game {
100    Go,
101    Other(u8),
102}
103
104#[derive(Clone, Eq, PartialEq, Debug)]
105pub enum Encoding {
106    UTF8,
107    Other(String),
108}
109
110#[derive(Copy, Clone, Eq, PartialEq, Debug)]
111pub enum DisplayNodes {
112    Children,
113    Siblings,
114}
115
116/// Enum describing all possible SGF Properties
117#[derive(Debug, PartialEq, Clone)]
118pub enum SgfToken {
119    Add {
120        color: Color,
121        coordinate: (u8, u8),
122    },
123    Move {
124        color: Color,
125        action: Action,
126    },
127    Time {
128        color: Color,
129        time: u32,
130    },
131    PlayerName {
132        color: Color,
133        name: String,
134    },
135    PlayerRank {
136        color: Color,
137        rank: String,
138    },
139    Game(Game),
140    Rule(RuleSet),
141    Result(Outcome),
142    Komi(f32),
143    Event(String),
144    Copyright(String),
145    GameName(String),
146    VariationDisplay {
147        nodes: DisplayNodes,
148        on_board_display: bool,
149    },
150    Place(String),
151    Date(String),
152    Size(u32, u32),
153    FileFormat(u8),
154    Overtime(String),
155    TimeLimit(u32),
156    MovesRemaining {
157        color: Color,
158        moves: u32,
159    },
160    Handicap(u32),
161    Comment(String),
162    Charset(Encoding),
163    Application {
164        name: String,
165        version: String,
166    },
167    Unknown((String, String)),
168    Invalid((String, String)),
169    Square {
170        coordinate: (u8, u8),
171    },
172    Triangle {
173        coordinate: (u8, u8),
174    },
175    Label {
176        label: String,
177        coordinate: (u8, u8),
178    },
179}
180
181impl SgfToken {
182    /// Converts a `identifier` and `value` pair to a SGF token
183    ///
184    /// Returns `SgfToken::Unknown((identifier, value))` for tokens without a matching identifier
185    ///
186    /// Returns `SgfToken::Invalid((identifier, value))` for tokens with a matching identifier, but invalid value
187    ///
188    /// ```rust
189    /// use sgf_parser::*;
190    ///
191    /// let token = SgfToken::from_pair("B", "aa");
192    /// assert_eq!(token, SgfToken::Move { color: Color::Black, action: Action::Move(1, 1) });
193    ///
194    /// let token = SgfToken::from_pair("B", "");
195    /// assert_eq!(token, SgfToken::Move { color: Color::Black, action: Action::Pass });
196    ///
197    /// let token = SgfToken::from_pair("B", "not_coord");
198    /// assert_eq!(token, SgfToken::Invalid(("B".to_string(), "not_coord".to_string())));
199    ///
200    /// let token = SgfToken::from_pair("FOO", "aa");
201    /// assert_eq!(token, SgfToken::Unknown(("FOO".to_string(), "aa".to_string())));
202    /// ```
203    pub fn from_pair(base_ident: &str, value: &str) -> SgfToken {
204        let ident = base_ident
205            .chars()
206            .filter(|c| c.is_uppercase())
207            .collect::<String>();
208        let token: Option<SgfToken> = match ident.as_ref() {
209            "LB" => split_label_text(value).and_then(|(coord, label)| {
210                str_to_coordinates(coord)
211                    .ok()
212                    .map(|coordinate| SgfToken::Label {
213                        label: label[1..].to_string(),
214                        coordinate,
215                    })
216            }),
217            "HA" => match value.parse() {
218                Ok(value) => Some(SgfToken::Handicap(value)),
219                _ => None,
220            },
221            "RU" => Some(SgfToken::Rule(RuleSet::from(value))),
222            "SQ" => str_to_coordinates(value)
223                .ok()
224                .map(|coordinate| SgfToken::Square { coordinate }),
225            "TR" => str_to_coordinates(value)
226                .ok()
227                .map(|coordinate| SgfToken::Triangle { coordinate }),
228            "AB" => str_to_coordinates(value)
229                .ok()
230                .map(|coordinate| SgfToken::Add {
231                    color: Color::Black,
232                    coordinate,
233                }),
234            "B" => move_str_to_coord(value)
235                .ok()
236                .map(|coordinate| SgfToken::Move {
237                    color: Color::Black,
238                    action: coordinate,
239                }),
240            "BL" => value.parse().ok().map(|time| SgfToken::Time {
241                color: Color::Black,
242                time,
243            }),
244            "PB" => Some(SgfToken::PlayerName {
245                color: Color::Black,
246                name: value.to_string(),
247            }),
248            "BR" => Some(SgfToken::PlayerRank {
249                color: Color::Black,
250                rank: value.to_string(),
251            }),
252            "AW" => str_to_coordinates(value)
253                .ok()
254                .map(|coordinate| SgfToken::Add {
255                    color: Color::White,
256                    coordinate,
257                }),
258            "W" => move_str_to_coord(value)
259                .ok()
260                .map(|coordinate| SgfToken::Move {
261                    color: Color::White,
262                    action: coordinate,
263                }),
264            "WL" => value.parse().ok().map(|time| SgfToken::Time {
265                color: Color::White,
266                time,
267            }),
268            "PW" => Some(SgfToken::PlayerName {
269                color: Color::White,
270                name: value.to_string(),
271            }),
272            "WR" => Some(SgfToken::PlayerRank {
273                color: Color::White,
274                rank: value.to_string(),
275            }),
276            "RE" => parse_outcome_str(value).ok().map(SgfToken::Result),
277            "KM" => value.parse().ok().map(SgfToken::Komi),
278            "SZ" => {
279                if let Some((width, height)) = split_size_text(value) {
280                    Some(SgfToken::Size(width, height))
281                } else {
282                    value.parse().ok().map(|size| SgfToken::Size(size, size))
283                }
284            }
285            "FF" => value.parse().ok().map(|v| match v {
286                0..=4 => SgfToken::FileFormat(v),
287                _ => SgfToken::Invalid((ident.to_string(), value.to_string())),
288            }),
289            "TM" => value.parse().ok().map(SgfToken::TimeLimit),
290            "EV" => Some(SgfToken::Event(value.to_string())),
291            "OT" => Some(SgfToken::Overtime(value.to_string())),
292            "C" => Some(SgfToken::Comment(value.to_string())),
293            "GN" => Some(SgfToken::GameName(value.to_string())),
294            "CR" => Some(SgfToken::Copyright(value.to_string())),
295            "DT" => Some(SgfToken::Date(value.to_string())),
296            "PC" => Some(SgfToken::Place(value.to_string())),
297            "GM" => match value.parse::<u8>() {
298                Ok(1) => Some(SgfToken::Game(Game::Go)),
299                Ok(n) => Some(SgfToken::Game(Game::Other(n))),
300                Err(_) => Some(SgfToken::Invalid((
301                    base_ident.to_string(),
302                    value.to_string(),
303                ))),
304            },
305            "CA" => match value.to_string().to_lowercase().as_str() {
306                "utf-8" => Some(SgfToken::Charset(Encoding::UTF8)),
307                _ => Some(SgfToken::Charset(Encoding::Other(value.to_string()))),
308            },
309            "OB" => match value.parse::<u32>() {
310                Ok(n) => Some(SgfToken::MovesRemaining {
311                    color: Color::Black,
312                    moves: n,
313                }),
314                Err(_) => Some(SgfToken::Invalid((
315                    base_ident.to_string(),
316                    value.to_string(),
317                ))),
318            },
319            "OW" => match value.parse::<u32>() {
320                Ok(n) => Some(SgfToken::MovesRemaining {
321                    color: Color::White,
322                    moves: n,
323                }),
324                Err(_) => Some(SgfToken::Invalid((
325                    base_ident.to_string(),
326                    value.to_string(),
327                ))),
328            },
329            "AP" => parse_application_str(value)
330                .ok()
331                .map(|(name, version)| SgfToken::Application { name, version }),
332            "ST" => parse_variation_display_str(value)
333                .ok()
334                .map(|(nodes, on_board_display)| SgfToken::VariationDisplay {
335                    nodes,
336                    on_board_display,
337                }),
338            _ => Some(SgfToken::Unknown((
339                base_ident.to_string(),
340                value.to_string(),
341            ))),
342        };
343        match token {
344            Some(token) => token,
345            _ => SgfToken::Invalid((base_ident.to_string(), value.to_string())),
346        }
347    }
348
349    /// Checks if the token is a root token as defined by the SGF spec.
350    ///
351    /// Root tokens can only occur in the root of a gametree collection, and they are invalid
352    /// anywhere else
353    ///
354    /// ```
355    /// use sgf_parser::*;
356    ///
357    /// let token = SgfToken::from_pair("SZ", "19");
358    /// assert!(token.is_root_token());
359    ///
360    /// let token = SgfToken::from_pair("B", "aa");
361    /// assert!(!token.is_root_token());
362    /// ```
363    pub fn is_root_token(&self) -> bool {
364        use SgfToken::*;
365        matches!(
366            self,
367            Size(_, _)
368                | Charset(_)
369                | FileFormat(_)
370                | Game(_)
371                | VariationDisplay { .. }
372                | Application { .. }
373        )
374    }
375
376    /// Checks if the token is a setup token as defined by the SGF spec.
377    ///
378    /// Setup tokens modify the current position, and should not be on the same node as move tokens
379    ///
380    /// ```
381    /// use sgf_parser::*;
382    ///
383    /// let token = SgfToken::from_pair("AB", "aa");
384    /// assert!(token.is_setup_token());
385    ///
386    /// let token = SgfToken::from_pair("SZ", "19");
387    /// assert!(!token.is_setup_token());
388    /// ```
389    pub fn is_setup_token(&self) -> bool {
390        use SgfToken::*;
391        matches!(self, Add { .. })
392    }
393
394    /// Checks if the token is a game info token as defined by the SGF spec.
395    ///
396    /// Game info tokens provide some information about the game played, usually stored in the root
397    /// node
398    ///
399    /// ```
400    /// use sgf_parser::*;
401    ///
402    /// let token = SgfToken::from_pair("RE", "W+T");
403    /// assert!(token.is_game_info_token());
404    ///
405    /// let token = SgfToken::from_pair("SZ", "19");
406    /// assert!(!token.is_game_info_token());
407    /// ```
408    pub fn is_game_info_token(&self) -> bool {
409        use SgfToken::*;
410        matches!(
411            self,
412            Date(_)
413                | GameName(_)
414                | Handicap(_)
415                | Komi(_)
416                | Overtime(_)
417                | Event(_)
418                | Result(_)
419                | Rule(_)
420                | Place(_)
421                | TimeLimit(_)
422                | PlayerName { .. }
423                | PlayerRank { .. }
424                | Copyright(_)
425        )
426    }
427}
428
429impl Into<String> for &SgfToken {
430    fn into(self) -> String {
431        match self {
432            SgfToken::Label { label, coordinate } => {
433                let value = coordinate_to_str(*coordinate);
434                format!("LB[{}:{}]", value, label)
435            }
436            SgfToken::Handicap(nb_stones) => format!("HA[{}]", nb_stones),
437            SgfToken::Rule(rule) => format!("RU[{}]", rule.to_string()),
438            SgfToken::Result(outcome) => match outcome {
439                WinnerByPoints(color, points) => format!(
440                    "RE[{}+{}]",
441                    match color {
442                        Black => "B",
443                        White => "W",
444                    },
445                    points
446                ),
447                WinnerByResign(color) => format!(
448                    "RE[{}+R]",
449                    match color {
450                        Black => "B",
451                        White => "W",
452                    }
453                ),
454
455                WinnerByTime(color) => format!(
456                    "RE[{}+T]",
457                    match color {
458                        Black => "B",
459                        White => "W",
460                    }
461                ),
462                WinnerByForfeit(color) => format!(
463                    "RE[{}+F]",
464                    match color {
465                        Black => "B",
466                        White => "W",
467                    }
468                ),
469                Draw => "RE[Draw]".to_string(),
470            },
471            SgfToken::Square { coordinate } => {
472                let value = coordinate_to_str(*coordinate);
473                format!("SQ[{}]", value)
474            }
475            SgfToken::Triangle { coordinate } => {
476                let value = coordinate_to_str(*coordinate);
477                format!("TR[{}]", value)
478            }
479            SgfToken::Add { color, coordinate } => {
480                let token = match color {
481                    Color::Black => "AB",
482                    Color::White => "AW",
483                };
484                let value = coordinate_to_str(*coordinate);
485                format!("{}[{}]", token, value)
486            }
487            SgfToken::Move { color, action } => {
488                let token = match color {
489                    Color::Black => "B",
490                    Color::White => "W",
491                };
492                let value = match *action {
493                    Move(x, y) => coordinate_to_str((x, y)),
494                    Pass => String::new(),
495                };
496                format!("{}[{}]", token, value)
497            }
498            SgfToken::Time { color, time } => {
499                let token = match color {
500                    Color::Black => "BL",
501                    Color::White => "WL",
502                };
503                format!("{}[{}]", token, time)
504            }
505            SgfToken::PlayerName { color, name } => {
506                let token = match color {
507                    Color::Black => "PB",
508                    Color::White => "PW",
509                };
510                format!("{}[{}]", token, name)
511            }
512            SgfToken::PlayerRank { color, rank } => {
513                let token = match color {
514                    Color::Black => "BR",
515                    Color::White => "WR",
516                };
517                format!("{}[{}]", token, rank)
518            }
519            SgfToken::Komi(komi) => format!("KM[{}]", komi),
520            SgfToken::FileFormat(v) => format!("FF[{}]", v),
521            SgfToken::Size(width, height) if width == height => format!("SZ[{}]", width),
522            SgfToken::Size(width, height) => format!("SZ[{}:{}]", width, height),
523            SgfToken::TimeLimit(time) => format!("TM[{}]", time),
524            SgfToken::Event(value) => format!("EV[{}]", value),
525            SgfToken::Comment(value) => format!("C[{}]", value),
526            SgfToken::Overtime(value) => format!("OT[{}]", value),
527            SgfToken::GameName(value) => format!("GN[{}]", value),
528            SgfToken::Copyright(value) => format!("CR[{}]", value),
529            SgfToken::Date(value) => format!("DT[{}]", value),
530            SgfToken::Place(value) => format!("PC[{}]", value),
531            SgfToken::Game(game) => format!(
532                "GM[{}]",
533                match game {
534                    Game::Go => &1u8,
535                    Game::Other(n) => n,
536                }
537            ),
538            SgfToken::Charset(_) => "CA[UTF-8]".to_string(),
539            SgfToken::MovesRemaining { color, moves } => format!(
540                "O{}[{}]",
541                match color {
542                    Color::Black => 'B',
543                    Color::White => 'W',
544                },
545                moves
546            ),
547            SgfToken::VariationDisplay {
548                nodes,
549                on_board_display,
550            } => {
551                let num = match (nodes, on_board_display) {
552                    (DisplayNodes::Children, true) => 0,
553                    (DisplayNodes::Siblings, true) => 1,
554                    (DisplayNodes::Children, false) => 2,
555                    (DisplayNodes::Siblings, false) => 3,
556                };
557                format!("ST[{}]", num)
558            }
559            SgfToken::Application { name, version } => format!("AP[{}:{}]", name, version),
560            SgfToken::Unknown((ident, prop)) => format!("{}[{}]", ident, prop),
561            SgfToken::Invalid((ident, prop)) => format!("{}[{}]", ident, prop),
562        }
563    }
564}
565
566impl Into<String> for SgfToken {
567    fn into(self) -> String {
568        (&self).into()
569    }
570}
571
572/// Splits size input text (NN:MM) to corresponding width and height
573fn split_size_text(input: &str) -> Option<(u32, u32)> {
574    let index = input.find(':')?;
575    let (width_part, height_part) = input.split_at(index);
576    let width: u32 = width_part.parse().ok()?;
577    let height: u32 = height_part[1..].parse().ok()?;
578    Some((width, height))
579}
580
581/// Converts goban coordinates to string representation
582fn coordinate_to_str(coordinate: (u8, u8)) -> String {
583    fn to_char(c: u8) -> char {
584        (c + if c < 27 { 96 } else { 38 }) as char
585    }
586
587    let x = to_char(coordinate.0);
588    let y = to_char(coordinate.1);
589
590    format!("{}{}", x, y)
591}
592
593/// If possible, splits a label text into coordinate and label pair
594fn split_label_text(input: &str) -> Option<(&str, &str)> {
595    if input.len() >= 4 {
596        Some(input.split_at(2))
597    } else {
598        None
599    }
600}
601
602fn parse_variation_display_str(input: &str) -> Result<(DisplayNodes, bool), SgfError> {
603    match input.parse::<u8>() {
604        Ok(0) => Ok((DisplayNodes::Children, true)),
605        Ok(1) => Ok((DisplayNodes::Siblings, true)),
606        Ok(2) => Ok((DisplayNodes::Children, false)),
607        Ok(3) => Ok((DisplayNodes::Siblings, false)),
608        _ => Err(SgfError::from(SgfErrorKind::ParseError)),
609    }
610}
611
612fn parse_application_str(input: &str) -> Result<(String, String), SgfError> {
613    let index = input
614        .find(':')
615        .ok_or_else(|| SgfError::from(SgfErrorKind::ParseError))?;
616    let (name, version) = input.split_at(index);
617    Ok((name.to_string(), version[1..].to_string()))
618}
619
620/// Provides the result of the game. It is MANDATORY to use the
621/// following format:
622/// "0" (zero) or "Draw" for a draw (jigo),
623/// "B+" ["score"] for a black win and
624/// "W+" ["score"] for a white win
625/// Score is optional (some games don't have a score e.g. chess).
626/// If the score is given it has to be given as a real value,
627/// e.g. "B+0.5", "W+64", "B+12.5"
628/// Use "B+R" or "B+Resign" and "W+R" or "W+Resign" for a win by
629/// resignation. Applications must not write "Black resigns".
630/// Use "B+T" or "B+Time" and "W+T" or "W+Time" for a win on time,
631/// "B+F" or "B+Forfeit" and "W+F" or "W+Forfeit" for a win by
632/// forfeit,
633/// "Void" for no result or suspended play and
634fn parse_outcome_str(s: &str) -> Result<Outcome, SgfError> {
635    if s.is_empty() || s == "Void" {
636        return Err(SgfError::from(SgfErrorKind::ParseError));
637    }
638    if s == "Draw" || s == "D" {
639        return Ok(Draw);
640    }
641
642    let winner_option: Vec<&str> = s.split('+').collect();
643    if winner_option.len() != 2 {
644        return Err(SgfError::from(SgfErrorKind::ParseError));
645    }
646
647    let winner: Color = match &winner_option[0] as &str {
648        "B" => Black,
649        "W" => White,
650        _ => return Err(SgfError::from(SgfErrorKind::ParseError)),
651    };
652
653    match &winner_option[1] as &str {
654        "F" | "Forfeit" => Ok(WinnerByForfeit(winner)),
655        "R" | "Resign" => Ok(WinnerByResign(winner)),
656        "T" | "Time" => Ok(WinnerByTime(winner)),
657        points => {
658            if let Ok(outcome) = points
659                .parse::<f32>()
660                .map(|score| WinnerByPoints(winner, score))
661            {
662                Ok(outcome)
663            } else {
664                Err(SgfError::from(SgfErrorKind::ParseError))
665            }
666        }
667    }
668}
669
670fn move_str_to_coord(input: &str) -> Result<Action, SgfError> {
671    if input.is_empty() {
672        Ok(Pass)
673    } else {
674        match str_to_coordinates(input) {
675            Ok(coordinates) => Ok(Move(coordinates.0, coordinates.1)),
676            Err(e) => Err(e),
677        }
678    }
679}
680
681/// Converts a string describing goban coordinates to numeric coordinates
682fn str_to_coordinates(input: &str) -> Result<(u8, u8), SgfError> {
683    if input.len() != 2 {
684        Err(SgfErrorKind::ParseError.into())
685    } else {
686        let coords = input
687            .as_bytes()
688            .iter()
689            .map(|c| convert_u8_to_coordinate(*c))
690            .collect::<Vec<_>>();
691        Ok((coords[0], coords[1]))
692    }
693}
694
695/// Converts a u8 char to numeric coordinates
696///
697#[inline]
698fn convert_u8_to_coordinate(c: u8) -> u8 {
699    if c > 96 {
700        c - 96
701    } else {
702        c - 38
703    }
704}