breed_planner/
breed.rs

1use std::fmt;
2use std::str::FromStr;
3use std::hash::{Hash, Hasher};
4use std::io::prelude::*;
5use std::collections::HashSet;
6
7use regex::Regex;
8
9use crate::error::Error;
10
11#[derive(Debug, Clone, PartialEq, Eq, Hash)]
12enum Sex { Male, Female, Any }
13
14impl fmt::Display for Sex
15{
16    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result
17    {
18        match self
19        {
20            Self::Male => write!(f, "M"),
21            Self::Female => write!(f, "F"),
22            Self::Any => write!(f, "?"),
23        }
24    }
25}
26
27impl FromStr for Sex
28{
29    type Err = Error;
30    fn from_str(s: &str) -> Result<Self, Self::Err>
31    {
32        match s
33        {
34            "M" => Ok(Self::Male),
35            "F" => Ok(Self::Female),
36            "?" => Ok(Self::Any),
37            _ => Err(error!(FormatError, "Invalid sex: {}", s)),
38        }
39    }
40}
41
42#[derive(Debug, Clone, PartialEq, Eq)]
43enum Role { Base, Mate }
44
45/// A monster of some kind, like “a female RainHawk”. In a breed plan,
46/// a monster is uniquely identified (only) by the name, the sex, and
47/// the index.
48#[derive(Debug, Clone)]
49struct Monster
50{
51    /// Name of the kind of monster, e.g. “RainHawk”.
52    name: String,
53    sex: Sex,
54    /// This is used to distiguish two monsters of the same kind in a
55    /// breed plan. If the plan has 2 slimes, one can have index 0
56    /// (default) and the other can have index 1.
57    index: u16,
58    /// The minimal plus level required of this monster. 0 means no
59    /// requirements.
60    plus_level_min: u16,
61}
62
63impl Monster
64{
65    #[allow(dead_code)]
66    fn new(name: &str, sex: Sex) -> Self
67    {
68        Self {
69            name: name.to_owned(),
70            sex: sex,
71            index: 0,
72            plus_level_min: 0,
73         }
74    }
75}
76
77impl PartialEq for Monster
78{
79    fn eq(&self, other: &Self) -> bool
80    {
81        self.name == other.name && self.sex == other.sex &&
82            self.index == other.index
83    }
84}
85
86impl Eq for Monster {}
87
88impl Hash for Monster
89{
90    fn hash<H: Hasher>(&self, state: &mut H)
91    {
92        self.name.hash(state);
93        self.sex.hash(state);
94        self.index.hash(state);
95    }
96}
97
98impl fmt::Display for Monster
99{
100    /// How to display a monster as a string. Note that if the sex is
101    /// `Any`, it’s not included in the string. Similarly, 0 plus
102    /// level requirements and/or 0 index is not included.
103    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result
104    {
105        let sex_str = if self.sex == Sex::Any
106        {
107            String::new()
108        }
109        else
110        {
111            format!("({})", self.sex)
112        };
113
114        let index_str = if self.index > 0 { format!("/{}", self.index) }
115        else {String::new()};
116
117        write!(f, "{}{}{}", self.name, sex_str, index_str)
118    }
119}
120
121impl FromStr for Monster
122{
123    type Err = Error;
124
125    /// How to parse a Monster out of a string. This is the inverse of `fmt()`.
126    fn from_str(s: &str) -> Result<Self, Self::Err>
127    {
128        let pattern = Regex::new(
129            r"([a-zA-Z0-9]+)(\([MF?]\))?(/[0-9]+)?(\+[0-9]+)?"
130        ).unwrap();
131        if let Some(groups) = pattern.captures(s)
132        {
133            // Make sure it’s a complete match.
134            let whole = groups.get(0).unwrap();
135            if whole.start() != 0 || whole.end() != s.len()
136            {
137                return Err(error!(FormatError,
138                                  "Invalid monster specification: {}",
139                                  s));
140            }
141
142            Ok(Self {
143                name: groups.get(1).ok_or_else(
144                    || error!(FormatError, "Name not specified for monster"))?
145                    .as_str().to_owned(),
146                sex: if let Some(m) = groups.get(2)
147                {
148                    m.as_str()[1..2].parse()?
149                }
150                else
151                {
152                    Sex::Any
153                },
154                plus_level_min: if let Some(m) = groups.get(4)
155                {
156                    m.as_str()[1..].parse().map_err(
157                        |_| error!(FormatError, "Invalid +lvl"))?
158                }
159                else
160                {
161                    0
162                },
163                index : if let Some(m) = groups.get(3)
164                {
165                    m.as_str()[1..].parse().map_err(
166                        |_| error!(FormatError, "Invalid index"))?
167                }
168                else
169                {
170                    0
171                }
172            })
173        }
174        else
175        {
176            Err(error!(FormatError, "Invalid monster specification: {}", s))
177        }
178    }
179}
180
181/// Monster visualization spec. This defines how the monster is
182/// displayed in the generated breed plan.
183#[derive(Debug, Clone)]
184struct MonsterVis
185{
186    monster: Monster,
187    role: Option<Role>,
188    /// This is the in-game name of the monster, given by the player.
189    /// Different from `Monster::name`.
190    name: Option<String>,
191}
192
193impl MonsterVis
194{
195    fn fromMonster(m: Monster, role: Option<Role>, name: Option<String>) -> Self
196    {
197        Self {
198            monster: m,
199            role: role,
200            name: name,
201        }
202    }
203
204    /// Generate a label for this monster in the dot file.
205    fn label(&self) -> String
206    {
207        let plus_str = if self.monster.plus_level_min > 0
208        {
209            format!("+{}", self.monster.plus_level_min)
210        }
211        else
212        {
213            String::new()
214        };
215
216        let custom_name_str = if let Some(n) = &self.name
217        {
218            format!("<br/><font point-size=\"10\">“{}”</font>", n)
219        }
220        else
221        {
222            String::new()
223        };
224
225        self.monster.name.clone() + &plus_str + &custom_name_str
226    }
227
228    fn toDotSpec(&self) -> String
229    {
230        let color = match self.monster.sex
231        {
232            Sex::Male => "#70a1ff",
233            Sex::Female => "#ff4757",
234            Sex::Any => "#eccc68",
235        };
236
237        let border_str = if self.role == Some(Role::Base)
238        {
239            String::from(", penwidth=2")
240        }
241        else
242        {
243            String::new()
244        };
245
246        format!("\"{}\"[label=<{}>, style=\"filled\", fillcolor=\"{}\"{}, \
247                 URL=\"https://darksair.org/dwm2-breed/monster/{}\"];",
248                self.monster.to_string(), self.label(), color, border_str,
249                self.monster.name)
250    }
251
252    /// Update this visualization spec from other visualization spec
253    /// of the same monster. Set role and in-game name from `new` if
254    /// self does not have them. Set the plus level requirement from
255    /// `new` if that of `new` is higher in that of self.
256    fn update(&mut self, new: Self)
257    {
258        if self.role == None
259        {
260            self.role = new.role;
261        }
262        if new.monster.plus_level_min > self.monster.plus_level_min
263        {
264            self.monster.plus_level_min = new.monster.plus_level_min;
265        }
266        if self.name == None
267        {
268            self.name = new.name;
269        }
270    }
271}
272
273impl FromStr for MonsterVis
274{
275    type Err = Error;
276    fn from_str(s: &str) -> Result<Self, Self::Err>
277    {
278        let pattern = Regex::new(r"(.+):[ \t]+(.+)").unwrap();
279        if let Some(groups) = pattern.captures(s)
280        {
281            // Make sure it’s a complete match.
282            let whole = groups.get(0).unwrap();
283            if whole.start() != 0 || whole.end() != s.len()
284            {
285                return Err(error!(FormatError,
286                                  "Invalid monster specification: {}",
287                                  s));
288            }
289
290            let monster: Monster = groups.get(1).unwrap().as_str().parse()?;
291            let name = groups.get(2).unwrap().as_str();
292            Ok(Self {
293                monster: monster,
294                role: None,
295                name: Some(String::from(name)),
296            })
297        }
298        else
299        {
300            Err(error!(FormatError, "Invalid monster specification: {}", s))
301        }
302    }
303}
304
305impl PartialEq for MonsterVis
306{
307    fn eq(&self, other: &Self) -> bool
308    {
309        self.monster == other.monster
310    }
311}
312
313impl Eq for MonsterVis {}
314
315impl Hash for MonsterVis
316{
317    fn hash<H: Hasher>(&self, state: &mut H)
318    {
319        self.monster.hash(state);
320    }
321}
322
323#[derive(Debug, Clone, PartialEq, Eq)]
324struct Breed
325{
326    base: Monster,
327    mate: Monster,
328    outcome: Monster,
329}
330
331impl FromStr for Breed
332{
333    type Err = Error;
334
335    fn from_str(s: &str) -> Result<Self, Self::Err>
336    {
337        let s = s.trim();
338
339        let (lhs, rhs) = s.split_once('=')
340            .ok_or_else(|| error!(FormatError, "Invalid breed: {}", s))?;
341
342        let outcome_str = rhs.trim();
343
344        let (base_str, mate_str) = lhs.split_once('+')
345            .ok_or_else(|| error!(FormatError, "Invalid breed: {}", s))?;
346
347        Ok(Self {
348            base: base_str.trim().parse()?,
349            mate: mate_str.trim().parse()?,
350            outcome: outcome_str.parse()?,
351        })
352    }
353}
354
355
356#[derive(Debug, Clone, PartialEq, Eq)]
357enum BreedOrSpec
358{
359    Breed(Breed),
360    Spec(MonsterVis),
361}
362
363impl FromStr for BreedOrSpec
364{
365    type Err = Error;
366    fn from_str(s: &str) -> Result<Self, Self::Err>
367    {
368        if let Ok(breed) = s.parse::<Breed>()
369        {
370            Ok(Self::Breed(breed))
371        }
372        else
373        {
374            Ok(Self::Spec(s.parse()?))
375        }
376    }
377}
378
379pub struct BreedPlan
380{
381    steps: Vec<Breed>,
382    specs: HashSet<MonsterVis>,
383}
384
385impl BreedPlan
386{
387    pub fn new() -> Self
388    {
389        Self { steps: Vec::new(), specs: HashSet::new() }
390    }
391
392    fn addStep(&mut self, breed: Breed)
393    {
394        self.steps.push(breed);
395    }
396
397    fn addSpec(&mut self, spec: MonsterVis) -> bool
398    {
399        if self.specs.contains(&spec)
400        {
401            false
402        }
403        else
404        {
405            self.specs.insert(spec);
406            true
407        }
408    }
409
410    pub fn fromStream(stream: &mut dyn BufRead) -> Result<Self, Error>
411    {
412        let mut plan = Self::new();
413        for line in stream.lines()
414        {
415            let line = line.map_err(
416                |e| rterr!("Failed to read a line: {}", e))?;
417            if line.trim().is_empty()
418            {
419                continue;
420            }
421            if line.chars().next() == Some('#')
422            {
423                continue;
424            }
425
426            match line.parse::<BreedOrSpec>()?
427            {
428                BreedOrSpec::Breed(b) => { plan.addStep(b); },
429                BreedOrSpec::Spec(vis) => {
430                    let monster_str = vis.monster.to_string();
431                    if !plan.addSpec(vis)
432                    {
433                        println!("WARNING: duplicated monster spec for {}, \
434                                  ignoring...",
435                                 monster_str);
436                    }
437                },
438            }
439        }
440        Ok(plan)
441    }
442
443    pub fn toDot(&self) -> String
444    {
445        let mut lines: Vec<String> = Vec::new();
446        let mut monsters: HashSet<MonsterVis> = self.specs.clone();
447
448        lines.push(String::from("digraph G {"));
449        lines.push(String::from("node[shape=\"box\"];"));
450        for breed in &self.steps
451        {
452            let base_vis = MonsterVis::fromMonster(
453                breed.base.clone(), Some(Role::Base), None);
454            let mate_vis = MonsterVis::fromMonster(
455                breed.mate.clone(), Some(Role::Mate), None);
456            let result_vis = MonsterVis::fromMonster(
457                breed.outcome.clone(), None, None);
458
459            match monsters.take(&base_vis)
460            {
461                Some(mut m) => {
462                    m.update(base_vis);
463                    monsters.insert(m);
464                },
465                None => { monsters.insert(base_vis); },
466            }
467            match monsters.take(&mate_vis)
468            {
469                Some(mut m) => {
470                    m.update(mate_vis);
471                    monsters.insert(m);
472                },
473                None => { monsters.insert(mate_vis); },
474            }
475            match monsters.take(&result_vis)
476            {
477                Some(mut m) => {
478                    m.update(result_vis);
479                    monsters.insert(m);
480                },
481                None => { monsters.insert(result_vis); },
482            }
483            lines.push(format!("\"{}\" -> \"{}\";", breed.base.to_string(),
484                               breed.outcome.to_string()));
485            lines.push(format!("\"{}\" -> \"{}\";", breed.mate.to_string(),
486                               breed.outcome.to_string()));
487        }
488
489        for m in monsters
490        {
491            lines.push(m.toDotSpec());
492        }
493
494        lines.push(String::from("}"));
495        lines.join("\n")
496    }
497}
498
499#[cfg(test)]
500mod tests
501{
502    // Note this useful idiom: importing names from outer (for mod tests) scope.
503    use super::*;
504
505    #[test]
506    fn printMonster()
507    {
508        assert_eq!(Monster::new("Zapbird", Sex::Female).to_string(),
509                   "Zapbird(F)");
510        assert_eq!(Monster
511                   {
512                       name: String::from("Zapbird"),
513                       sex: Sex::Any,
514                       index: 1,
515                       plus_level_min: 0,
516                   }.to_string(),
517                   "Zapbird/1");
518        assert_eq!(Monster
519                   {
520                       name: String::from("Zapbird"),
521                       sex: Sex::Male,
522                       index: 1,
523                       plus_level_min: 2,
524                   }.to_string(),
525                   "Zapbird(M)/1");
526    }
527
528    #[test]
529    fn parseMonster() -> Result<(), Error>
530    {
531        assert_eq!("Zapbird(M)/3+2".parse::<Monster>()?,
532                   Monster
533                   {
534                       name: String::from("Zapbird"),
535                       sex: Sex::Male,
536                       index: 3,
537                       plus_level_min: 2,
538                   });
539        assert_eq!("Zapbird".parse::<Monster>()?,
540                   Monster
541                   {
542                       name: String::from("Zapbird"),
543                       sex: Sex::Any,
544                       index: 0,
545                       plus_level_min: 0,
546                   });
547        assert_eq!("Zapbird/2".parse::<Monster>()?,
548                   Monster
549                   {
550                       name: String::from("Zapbird"),
551                       sex: Sex::Any,
552                       index: 2,
553                       plus_level_min: 0,
554                   });
555        assert_eq!("Zapbird+5".parse::<Monster>()?,
556                   Monster
557                   {
558                       name: String::from("Zapbird"),
559                       sex: Sex::Any,
560                       index: 0,
561                       plus_level_min: 5,
562                   });
563        Ok(())
564    }
565
566    #[test]
567    fn invalidMonster() -> Result<(), Error>
568    {
569        assert!("Zapbird\\1".parse::<Monster>().is_err());
570        assert!("".parse::<Monster>().is_err());
571        assert!("Zapbird+".parse::<Monster>().is_err());
572        assert!("Zapbird\\4+1".parse::<Monster>().is_err());
573        assert!("Zapbird+2/1".parse::<Monster>().is_err());
574        assert!("Zapbird+2(F)".parse::<Monster>().is_err());
575        Ok(())
576    }
577
578    #[test]
579    fn parseBreed() -> Result<(), Error>
580    {
581        assert_eq!("Base + Mate = Result".parse::<Breed>()?,
582                Breed {
583                    base: Monster::new("Base", Sex::Any),
584                    mate: Monster::new("Mate", Sex::Any),
585                    outcome: Monster::new("Result", Sex::Any),
586                });
587
588        // Regression: allow extra spaces around '='
589        assert_eq!("Blizzardy + Phoenix  = RainHawk".parse::<Breed>()?,
590                Breed {
591                    base: Monster::new("Blizzardy", Sex::Any),
592                    mate: Monster::new("Phoenix", Sex::Any),
593                    outcome: Monster::new("RainHawk", Sex::Any),
594                });
595
596        // Bonus: allow no spaces too
597        assert_eq!("Base+Mate=Result".parse::<Breed>()?,
598                Breed {
599                    base: Monster::new("Base", Sex::Any),
600                    mate: Monster::new("Mate", Sex::Any),
601                    outcome: Monster::new("Result", Sex::Any),
602                });
603
604        Ok(())
605    }
606
607    #[test]
608    fn invalidBreed() -> Result<(), Error>
609    {
610        assert!("Base + = Result".parse::<Breed>().is_err());
611        assert!("Base + + = Result".parse::<Breed>().is_err());
612        assert!("Base + Result".parse::<Breed>().is_err());
613        assert!("".parse::<Breed>().is_err());
614        Ok(())
615    }
616}