cygrind_utils/
parser.rs

1use std::{error::Error, fmt::Display, hint::unreachable_unchecked};
2use rand::{
3    distributions::{Distribution, Standard},
4    Rng,
5};
6use logos::Logos;
7
8/// Representation of the prefabs available on the official editor
9#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
10pub enum Prefabs {
11    None,
12    Melee,
13    Projectile,
14    JumpPad,
15    Stairs,
16    HiM,
17}
18
19impl Distribution<Prefabs> for Standard {
20    fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> Prefabs {
21        match rng.gen_range(0..=5) {
22            0 => Prefabs::None,
23            1 => Prefabs::Melee,
24            2 => Prefabs::Projectile,
25            3 => Prefabs::JumpPad,
26            4 => Prefabs::Stairs,
27            5 => Prefabs::HiM,
28            _ => unsafe { unreachable_unchecked() }
29        }
30    }
31}
32
33impl Default for Prefabs {
34    fn default() -> Self {
35        Self::None
36    }
37}
38
39impl Prefabs {
40    /// Returns the text representation of a prefab as seen on the official editor
41    pub fn short_name<'a>(&self) -> &'a str {
42        match self {
43            Prefabs::None => "0",
44            Prefabs::Melee => "n",
45            Prefabs::Projectile => "p",
46            Prefabs::JumpPad => "J",
47            Prefabs::Stairs => "s",
48            Prefabs::HiM => "H",
49        }
50    }
51}
52
53/// Representation of an individual square on a cyber grind pattern
54#[derive(Debug, Default, Clone, Copy)]
55pub struct Cell {
56    height: i32,
57    prefab: Prefabs,
58}
59
60impl Cell {
61    pub fn new() -> Self {
62        Self::default()
63    }
64
65    /// Gets the height of this cell
66    pub fn height(&self) -> i32 {
67        self.height
68    }
69
70    /// Gets the prefab of this cell
71    pub fn prefab(&self) -> Prefabs {
72        self.prefab
73    }
74
75    /// Checks if the cell's prefab is a `Prefabs::None` (`0`)
76    pub fn is_none(&self) -> bool {
77        self.prefab == Prefabs::None
78    }
79
80    pub fn set_height(&mut self, height: i32) {
81        self.height = height
82    }
83
84    pub fn set_prefab(&mut self, prefab: Prefabs) {
85        self.prefab = prefab
86    }
87}
88
89/// Representation of a cgp
90#[derive(Debug, Default)]
91pub struct Pattern(pub [[Cell; 16]; 16]);
92
93impl Pattern {
94    pub fn new() -> Self {
95        Self::default()
96    }
97
98    /// Turns this struct back into a cgp
99    pub fn to_pattern_string(&self) -> String {
100        let pattern = &self.0;
101
102        let mut height_buf = Vec::new();
103        let mut prefab_buf = Vec::new();
104
105        for i in 0..16 {
106            for j in 0..16 {
107                let cell = &pattern[i][j];
108
109                let height_str = if cell.height >= 10 || cell.height.is_negative() {
110                    format!("({})", cell.height)
111                } else {
112                    cell.height.to_string()
113                };
114
115                height_buf.push(height_str);
116                prefab_buf.push(cell.prefab.short_name().to_owned());
117            }
118
119            height_buf.push("\n".to_owned());
120            prefab_buf.push("\n".to_owned());
121        }
122
123        height_buf.push("\n".to_owned());
124
125        [height_buf, prefab_buf]
126            .concat()
127            .into_iter()
128            .collect::<String>()
129            .trim()
130            .to_owned()
131    }
132}
133
134/// Tries to parse a string to a Pattern
135pub fn parse(source: impl AsRef<str>) -> Result<Pattern, ParseError> {
136    let source = source.as_ref();
137    let lines = source.lines();
138    let mut token_grid = Vec::new();
139    let mut pattern = Pattern::default();
140
141    for line in lines {
142        let mut linebuf = Vec::new();
143        let lexer = Tokens::lexer(line);
144
145        lexer.for_each(|token| linebuf.push(token));
146        token_grid.push(linebuf);
147    }
148
149    let filtered_tokens = token_grid
150        .into_iter()
151        .filter(|p| !p.is_empty())
152        .collect::<Vec<_>>();
153
154    if filtered_tokens.len() < 32 {
155        return Err(ParseError("Input is not long enough to be a valid pattern.".into()));
156    }
157
158    let (l, r) = filtered_tokens.split_at(16);
159
160    for i in 0..16 {
161        for j in 0..16 {
162            let height = match l[i][j] {
163                Tokens::Number(n) => n,
164                Tokens::Prefab(n) if n == Prefabs::None => 0,
165                _ => {
166                    return Err(ParseError("Invalid token when parsing numbers".to_string()))
167                }
168            };
169
170            let prefab = match r[i][j] {
171                Tokens::Prefab(n) => n,
172                Tokens::Number(_) => Prefabs::None,
173                Tokens::Error => {
174                    return Err(ParseError("Invalid token when parsing prefabs".to_string()))
175                }
176            };
177
178            pattern.0[i][j] = Cell { height, prefab };
179        }
180    }
181
182    Ok(pattern)
183}
184
185/// Generic error type for parsing failures
186#[derive(Debug, Clone)]
187pub struct ParseError(pub String);
188
189impl Display for ParseError {
190    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
191        write!(f, "{:?}", self)
192    }
193}
194
195impl Error for ParseError {}
196
197#[derive(Logos, Clone, Copy, Debug, PartialEq, PartialOrd)]
198#[doc(hidden)]
199pub enum Tokens {
200    #[regex(r"\d|\(-?\d+\)", |lexer| {
201        let slice = lexer.slice();
202
203        if slice.starts_with('(') {
204            let mut chars = slice.chars();
205            chars.next();
206            chars.next_back();
207            chars.as_str().parse()
208
209        } else {
210            slice.parse()
211        }
212    })]
213    Number(i32),
214
215    #[regex("[a-zA-Z0]", |lexer| {
216        match lexer.slice().chars().next().unwrap() {
217            '0' => Some(Prefabs::None),
218            'n' => Some(Prefabs::Melee),
219            'p' => Some(Prefabs::Projectile),
220            'J' => Some(Prefabs::JumpPad),
221            's' => Some(Prefabs::Stairs),
222            'H' => Some(Prefabs::HiM),
223            _ => None
224        }
225    },
226    priority = 2)]
227    Prefab(Prefabs),
228
229    #[error]
230    Error,
231}
232
233#[cfg(test)]
234mod test {
235    use std::{fs::File, io::Write};
236
237    use super::*;
238
239    #[test]
240    fn serde() {
241        let src = include_str!("../example.cgp");
242        let out = parse(src).unwrap().to_pattern_string();
243
244        assert_eq!(src, &*out);
245    }
246
247    #[cfg(feature = "draw2d")]
248    #[test]
249    fn parser_draw2d() {
250        use crate::draw2d::draw::Draw2d;
251
252        let src = include_str!("../example.cgp");
253        let data = Draw2d::draw(parse(src).unwrap());
254
255        let bytes = &*data;
256        let mut file = File::create("example.png").unwrap();
257        file.write_all(bytes).unwrap();
258    }
259}
260// cargo test --package cygrind-utils --lib -- parser::test::parser_draw2d --exact --nocapture