sgf_parse/
go.rs

1//! Types specific to the game of Go.
2//!
3//! This module contains a go-specific [`SgfProp`] implementation which
4//! includes go specific properties (HA, KM, TB, TW). Point and Stone values
5//! map to [`Point`], and Move values map to [`Move`]. Properties with
6//! invalid moves or points map to [`Prop::Invalid`] (as do any invalid
7//! [general properties](https://www.red-bean.com/sgf/properties.html)).
8//!
9//! This module also includes a convenience [`parse`] function which fails
10//! on non-go games and returns the [`SgfNode`] values directly instead of
11//! returning [`GameTree`](crate::GameTree) values.
12use std::collections::HashSet;
13
14use crate::props::parse::{parse_elist, parse_single_value, FromCompressedList};
15use crate::props::{PropertyType, SgfPropError, ToSgf};
16use crate::{InvalidNodeError, SgfNode, SgfParseError, SgfProp};
17
18/// Returns the [`SgfNode`] values for Go games parsed from the provided text.
19///
20/// This is a convenience wrapper around [`crate::parse`] for dealing with Go only collections.
21///
22/// # Errors
23/// If the text can't be parsed as an SGF FF\[4\] collection, then an error is returned.
24///
25/// # Examples
26/// ```
27/// use sgf_parse::go::parse;
28///
29/// // Prints the all the properties for the two root nodes in the SGF
30/// let sgf = "(;SZ[9]C[Some comment];B[de];W[fe])(;B[de];W[ff])";
31/// for node in parse(&sgf).unwrap().iter() {
32///     for prop in node.properties() {
33///         println!("{:?}", prop);
34///     }
35/// }
36/// ```
37pub fn parse(text: &str) -> Result<Vec<SgfNode<Prop>>, SgfParseError> {
38    let gametrees = crate::parse(text)?;
39    gametrees
40        .into_iter()
41        .map(|gametree| gametree.into_go_node())
42        .collect::<Result<Vec<_>, _>>()
43}
44
45/// An SGF [Point](https://www.red-bean.com/sgf/go.html#types) value for the Game of Go.
46///
47/// # Examples
48/// ```
49/// use sgf_parse::go::{Prop, Move, Point};
50///
51/// let point = Point {x: 10, y: 10};
52/// let prop = Prop::B(Move::Move(point));
53/// ```
54#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
55pub struct Point {
56    pub x: u8,
57    pub y: u8,
58}
59
60/// An SGF [Stone](https://www.red-bean.com/sgf/go.html#types) value for the Game of Go.
61pub type Stone = Point;
62
63/// An SGF [Move](https://www.red-bean.com/sgf/go.html#types) value for the Game of Go.
64///
65/// # Examples
66/// ```
67/// use sgf_parse::go::{parse, Move, Prop};
68///
69/// let node = parse("(;B[de])").unwrap().into_iter().next().unwrap();
70/// for prop in node.properties() {
71///     match prop {
72///         Prop::B(Move::Move(point)) => println!("B move at {:?}", point),
73///         _ => {}
74///     }
75/// }
76/// ```
77#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
78pub enum Move {
79    Pass,
80    Move(Point),
81}
82
83sgf_prop! {
84    Prop, Move, Point, Point,
85    {
86        HA(i64),
87        KM(f64),
88        TB(HashSet<Point>),
89        TW(HashSet<Point>),
90    }
91}
92
93impl SgfProp for Prop {
94    type Point = Point;
95    type Stone = Stone;
96    type Move = Move;
97
98    fn new(identifier: String, values: Vec<String>) -> Self {
99        match Prop::parse_general_prop(identifier, values) {
100            Self::Unknown(identifier, values) => match &identifier[..] {
101                "KM" => parse_single_value(&values)
102                    .map_or_else(|_| Self::Invalid(identifier, values), Self::KM),
103
104                "HA" => match parse_single_value(&values) {
105                    Ok(value) => {
106                        if value < 2 {
107                            Self::Invalid(identifier, values)
108                        } else {
109                            Self::HA(value)
110                        }
111                    }
112                    _ => Self::Invalid(identifier, values),
113                },
114                "TB" => parse_elist(&values)
115                    .map_or_else(|_| Self::Invalid(identifier, values), Self::TB),
116                "TW" => parse_elist(&values)
117                    .map_or_else(|_| Self::Invalid(identifier, values), Self::TW),
118                _ => Self::Unknown(identifier, values),
119            },
120            prop => prop,
121        }
122    }
123
124    fn identifier(&self) -> String {
125        match self.general_identifier() {
126            Some(identifier) => identifier,
127            None => match self {
128                Self::KM(_) => "KM".to_string(),
129                Self::HA(_) => "HA".to_string(),
130                Self::TB(_) => "TB".to_string(),
131                Self::TW(_) => "TW".to_string(),
132                _ => panic!("Unimplemented identifier for {:?}", self),
133            },
134        }
135    }
136
137    fn property_type(&self) -> Option<PropertyType> {
138        match self.general_property_type() {
139            Some(property_type) => Some(property_type),
140            None => match self {
141                Self::HA(_) => Some(PropertyType::GameInfo),
142                Self::KM(_) => Some(PropertyType::GameInfo),
143                _ => None,
144            },
145        }
146    }
147
148    fn validate_properties(properties: &[Self], is_root: bool) -> Result<(), InvalidNodeError> {
149        Self::general_validate_properties(properties, is_root)
150    }
151}
152
153impl std::fmt::Display for Prop {
154    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
155        let prop_string = match self.serialize_prop_value() {
156            Some(s) => s,
157            None => match self {
158                Self::HA(x) => x.to_sgf(),
159                Self::KM(x) => x.to_sgf(),
160                Self::TB(x) => x.to_sgf(),
161                Self::TW(x) => x.to_sgf(),
162                _ => panic!("Unimplemented identifier for {:?}", self),
163            },
164        };
165        write!(f, "{}[{}]", self.identifier(), prop_string)
166    }
167}
168
169impl FromCompressedList for Point {
170    fn from_compressed_list(ul: &Self, lr: &Self) -> Result<HashSet<Self>, SgfPropError> {
171        let mut points = HashSet::new();
172        if ul.x > lr.x || ul.y > lr.y {
173            return Err(SgfPropError {});
174        }
175        for x in ul.x..=lr.x {
176            for y in ul.y..=lr.y {
177                let point = Self { x, y };
178                if points.contains(&point) {
179                    return Err(SgfPropError {});
180                }
181                points.insert(point);
182            }
183        }
184        Ok(points)
185    }
186}
187
188impl ToSgf for Move {
189    fn to_sgf(&self) -> String {
190        match self {
191            Self::Pass => "".to_string(),
192            Self::Move(point) => point.to_sgf(),
193        }
194    }
195}
196
197impl ToSgf for Point {
198    fn to_sgf(&self) -> String {
199        format!("{}{}", (self.x + b'a') as char, (self.y + b'a') as char)
200    }
201}
202
203impl std::str::FromStr for Move {
204    type Err = SgfPropError;
205
206    fn from_str(s: &str) -> Result<Self, Self::Err> {
207        match s {
208            "" => Ok(Self::Pass),
209            _ => Ok(Self::Move(s.parse()?)),
210        }
211    }
212}
213
214impl std::str::FromStr for Point {
215    type Err = SgfPropError;
216
217    fn from_str(s: &str) -> Result<Self, Self::Err> {
218        fn map_char(c: char) -> Result<u8, SgfPropError> {
219            if c.is_ascii_lowercase() {
220                Ok(c as u8 - b'a')
221            } else if c.is_ascii_uppercase() {
222                Ok(c as u8 - b'A' + 26)
223            } else {
224                Err(SgfPropError {})
225            }
226        }
227
228        let chars: Vec<char> = s.chars().collect();
229        if chars.len() != 2 {
230            return Err(SgfPropError {});
231        }
232
233        Ok(Self {
234            x: map_char(chars[0])?,
235            y: map_char(chars[1])?,
236        })
237    }
238}
239
240#[cfg(test)]
241mod tests {
242    use super::Point;
243
244    #[test]
245    fn large_move_numbers() {
246        let point: Point = "aC".parse().unwrap();
247        let expected = Point { x: 0, y: 28 };
248        assert_eq!(point, expected);
249    }
250}