camel_up/camel/
mod.rs

1//! The camel module models Camel Up.
2//!
3//! It identifies all the relevant parts of the Camel Up game. Starting from a `Race` it allows you to perform a `Roll`, which will return the resulting race.
4//!
5//! ```
6//! # use camel_up::camel::{Race, Marker, Camel, Roll, Face};
7//! let race = Race::from(vec![Marker::Camel(Camel::Red), Marker::Divider, Marker::Camel(Camel::Yellow)]);
8//! let roll = Roll::from((Camel::Red, Face::One));
9//!
10//! let actual = race.perform(roll);
11//!
12//! let expected = Race::from(vec![Marker::Camel(Camel::Yellow), Marker::Camel(Camel::Red)]);
13//! assert_eq!(actual, expected);
14//! ```
15//!
16//! One can cut down on the verbosity by using the various `parse` functions and other convenience functions. The above code example can be widdled down to
17//!
18//! ```
19//! # use camel_up::camel::{Race, Marker, Camel, Roll, Face};
20//! let race = "r,y".parse::<Race>().expect("to parse");
21//!
22//! let actual = race.perform((Camel::Red, Face::One));
23//!
24//! let expected = "yr".parse::<Race>().expect("to parse");
25//! assert_eq!(actual, expected);
26//! ```
27
28use std::collections::HashSet;
29use std::iter::repeat;
30use std::str::FromStr;
31
32/// The various camels that race in the game.
33#[derive(PartialEq, Eq, Hash, Copy, Clone, Debug)]
34pub enum Camel {
35    /// The red camel, Rachel for friends.
36    Red,
37    /// The orange camel, also known as Olleta.
38    Orange,
39    /// The yellow camel. They are a little shy. They go by the name of Yenn.
40    Yellow,
41    /// The green camel. The mysterious one, calls itself G.
42    Green,
43    /// The white camel. Responds to Whitney. Suspected to be a foreign spy.
44    White,
45}
46
47/// A marker is used to describe a race.
48#[derive(PartialEq, Eq, Copy, Clone, Debug)]
49pub enum Marker {
50    /// Signals that a camel is present at this position. Its argument tells you which camel.
51    Camel(Camel),
52    /// Divider between positions.
53    Divider,
54    /// When camels land on an oasis they advance one position.
55    Oasis,
56    /// When camels land on a fata morgana, they fallback one position.
57    FataMorgana,
58}
59
60impl Marker {
61    fn is_a_camel(self) -> bool {
62        match self {
63            Marker::Camel(_) => true,
64            _ => false,
65        }
66    }
67
68    fn is_a_divider(self) -> bool {
69        match self {
70            Marker::Divider => true,
71            _ => false,
72        }
73    }
74
75    fn is_an_oasis(self) -> bool {
76        match self {
77            Marker::Oasis => true,
78            _ => false,
79        }
80    }
81
82    fn is_a_fata_morgana(self) -> bool {
83        match self {
84            Marker::FataMorgana => true,
85            _ => false,
86        }
87    }
88
89    fn is_an_adjustment(self) -> bool {
90        self.is_an_oasis() || self.is_a_fata_morgana()
91    }
92
93    fn to_camel(self) -> Option<Camel> {
94        match self {
95            Marker::Camel(camel) => Some(camel),
96            _ => None,
97        }
98    }
99}
100
101impl FromStr for Marker {
102    type Err = NotAMarker;
103
104    fn from_str(input: &str) -> Result<Self, Self::Err> {
105        match input {
106            "r" => Ok(Marker::Camel(Camel::Red)),
107            "o" => Ok(Marker::Camel(Camel::Orange)),
108            "y" => Ok(Marker::Camel(Camel::Yellow)),
109            "g" => Ok(Marker::Camel(Camel::Green)),
110            "w" => Ok(Marker::Camel(Camel::White)),
111            "," => Ok(Marker::Divider),
112            "+" => Ok(Marker::Oasis),
113            "-" => Ok(Marker::FataMorgana),
114            _ => Err(NotAMarker::But(input.to_owned())),
115        }
116    }
117}
118
119/// When parsing of Marker goes wrong, this enumeration tells you precisely what went down.
120#[derive(PartialEq, Debug)]
121pub enum NotAMarker {
122    /// It was not a marker, but something else. The argument tells you what it was.
123    But(String),
124}
125
126/// Models a race as a sequence of markers.
127///
128/// Note that a race is normalized, i.e. leading and trailing dividers are stripped.
129///
130/// ```
131/// # use camel_up::camel::{Race, Marker, Camel};
132/// let race_with_superfluous_dividers = ",,,,,,r,y,,,,,,,".parse::<Race>().expect("to parse");
133/// let minimal_race = "r,y".parse::<Race>().expect("to parse");
134///
135/// assert_eq!(race_with_superfluous_dividers, minimal_race);
136/// ```
137#[derive(PartialEq, Eq, Debug)]
138pub struct Race {
139    positions: Vec<Marker>,
140}
141
142impl Clone for Race {
143    fn clone(&self) -> Self {
144        Self {
145            positions: self.positions.to_vec(),
146        }
147    }
148}
149
150impl From<Vec<Marker>> for Race {
151    fn from(positions: Vec<Marker>) -> Self {
152        let (min, max) = positions
153            .iter()
154            .zip(0..)
155            .filter(|(marker, _)| marker.is_a_camel())
156            .map(|(_, index)| index)
157            .fold(
158                (core::usize::MAX, core::usize::MIN),
159                |(minimum, maximum), index| (minimum.min(index), maximum.max(index)),
160            );
161        let positions = positions[min..=max]
162            .iter()
163            .skip_while(|marker| **marker == Marker::Divider)
164            .cloned()
165            .collect();
166        Self { positions }
167    }
168}
169
170impl FromStr for Race {
171    type Err = RaceParseError;
172
173    fn from_str(input: &str) -> Result<Self, Self::Err> {
174        let mut result = vec![];
175        let mut cursor = 0;
176        while cursor < input.len() {
177            result.push(input[cursor..=cursor].parse::<Marker>()?);
178            cursor += 1;
179        }
180        if result
181            .iter()
182            .zip(result.iter().skip(1))
183            .filter(|(l, r)| l.is_a_camel() && r.is_an_oasis() || l.is_an_oasis() && r.is_a_camel())
184            .count()
185            > 0
186        {
187            return Err(RaceParseError::CamelInOasis);
188        }
189
190        if result
191            .iter()
192            .zip(result.iter().skip(1))
193            .filter(|(l, r)| l.is_a_camel() && r.is_a_fata_morgana() || l.is_a_fata_morgana() && r.is_a_camel())
194            .count()
195            > 0
196        {
197            return Err(RaceParseError::CamelInFataMorgana);
198        }
199
200        if result
201            .iter()
202            .zip(result.iter().skip(1))
203            .filter(|(l, r)| l.is_an_adjustment() && r.is_an_adjustment())
204            .count()
205            > 0
206        {
207            return Err(RaceParseError::ToManyAdjustmentsInOnePosition);
208        }
209
210        if result
211            .iter()
212            .zip(result.iter().skip(2))
213            .filter(|(l, r)| l.is_an_adjustment() && r.is_an_adjustment())
214            .count()
215            > 0
216        {
217            return Err(RaceParseError::ConsecutiveAdjustments);
218        }
219
220        Ok(Race::from(result))
221    }
222}
223
224/// When parsing of Race goes wrong, this enumeration tells you precisely what went down.
225#[derive(PartialEq, Debug)]
226pub enum RaceParseError {
227    /// a race consists solely of markers, and this isn't a marker.
228    NotAMarker(NotAMarker),
229    /// a camel can't be in an oasis.
230    CamelInOasis,
231    /// a camel can't be in a fata morgana.
232    CamelInFataMorgana,
233    /// adjustments can't be in the same position.
234    ToManyAdjustmentsInOnePosition,
235    /// adjustments can't be consecutive.
236    ConsecutiveAdjustments,
237}
238
239impl From<NotAMarker> for RaceParseError {
240    fn from(problem: NotAMarker) -> Self {
241        Self::NotAMarker(problem)
242    }
243}
244
245/// A roll of the dice
246#[derive(PartialEq, Eq, Hash, Copy, Clone, Debug)]
247pub struct Roll {
248    /// The camel that is allowed to move.
249    camel: Camel,
250    /// The number of steps they are allowed to take.
251    face: Face,
252}
253
254/// The faces of the Camel dice.
255///
256/// It corresponds with the number of steps to take.
257///
258/// ```
259/// # use camel_up::camel::{Face};
260/// assert_eq!(usize::from(Face::One), 1 as usize);
261/// assert_eq!(usize::from(Face::Two), 2 as usize);
262/// assert_eq!(usize::from(Face::Three), 3 as usize);
263/// ```
264#[derive(PartialEq, Eq, Hash, Copy, Clone, Debug)]
265pub enum Face {
266    /// represents one step
267    One,
268    /// represents two steps
269    Two,
270    /// represents three steps
271    Three,
272}
273
274impl Face {
275    /// Convenience function that retuns all the possible face values.
276    pub fn values() -> HashSet<Self> {
277        vec![Face::One, Face::Two, Face::Three]
278            .iter()
279            .copied()
280            .collect()
281    }
282}
283
284impl From<(Camel, Face)> for Roll {
285    fn from((camel, face): (Camel, Face)) -> Self {
286        Self { camel, face }
287    }
288}
289
290impl From<Face> for usize {
291    fn from(face: Face) -> Self {
292        match face {
293            Face::One => 1,
294            Face::Two => 2,
295            Face::Three => 3,
296        }
297    }
298}
299
300impl Race {
301    /// perform a roll on a race, returns the race with all the camels in their correct positions.
302    pub fn perform<R>(&self, roll: R) -> Self
303    where
304        R: Into<Roll>,
305    {
306        let roll: Roll = roll.into();
307        if self.positions.contains(&Marker::Camel(roll.camel)) {
308            let index = self.positions.iter().position(|marker| *marker == Marker::Camel(roll.camel)).unwrap(/* camel is present because of contains check */);
309            let offset = self.positions[index..]
310                .iter()
311                .take_while(|marker| marker.is_a_camel())
312                .count();
313
314            let unit = &self.positions[index..(index + offset)];
315            let remaining: Vec<Marker> = self.positions[0..index]
316                .iter()
317                .chain(self.positions[(index + offset)..].iter())
318                .chain(repeat(&Marker::Divider).take(4))
319                .copied()
320                .collect();
321
322            let original_divider_offset = remaining[index..].iter().enumerate().filter(|(_, marker)| marker.is_a_divider()).map(|(index, _)| index).skip(roll.face as usize + 1).nth(0).unwrap(/* offset is present because of repeated divider */);
323            let delta: usize = match remaining[index + original_divider_offset - 1] {
324                Marker::Oasis => 2,
325                Marker::FataMorgana => 0,
326                _ => 1,
327            };
328            let divider_offset = remaining[index..].iter().enumerate().filter(|(_, marker)| marker.is_a_divider()).map(|(index, _)| index).skip(roll.face as usize + delta).nth(0).unwrap(/* offset is present because of repeated divider */);
329            let result: Vec<Marker> = remaining[0..(index + divider_offset)]
330                .iter()
331                .chain(unit.iter())
332                .chain(remaining[(index + divider_offset)..].iter())
333                .copied()
334                .collect();
335            Self::from(result)
336        } else {
337            let positions: Vec<Marker> = self.positions.to_vec();
338            Self::from(positions)
339        }
340    }
341
342    /// Determines which camel is the winner, i.e. is at the front.
343    pub fn winner(&self) -> Option<Camel> {
344        self.positions
345            .iter()
346            .filter(|marker| marker.is_a_camel())
347            .map(|marker| marker.to_camel().unwrap(/* camel is present because of filter on camel */))
348            .last()
349    }
350
351    /// Determines which camel is the loser, i.e. is at the back.
352    pub fn loser(&self) -> Option<Camel> {
353        self.positions
354            .iter()
355            .filter(|marker| marker.is_a_camel())
356            .map(|marker| marker.to_camel().unwrap(/* camel is present because of filter on camel */))
357            .nth(0)
358    }
359
360    /// Determines which camel is the runner up, i.e. is behind the winner.
361    pub fn runner_up(&self) -> Option<Camel> {
362        self.positions
363            .iter()
364            .filter(|marker| marker.is_a_camel())
365            .map(|marker| marker.to_camel().unwrap(/* camel is present because of filter on camel */))
366            .rev()
367            .nth(1)
368    }
369}
370
371/// Represents the dice that still can be rolled.
372#[derive(PartialEq, Eq, Clone, Debug)]
373pub struct Dice(HashSet<Camel>); // TODO model the fact that not all dice could be rolled.
374
375impl Dice {
376    /// Remove a dice from the pyramid, i.e. the options to throw are reduced.
377    pub fn remove(&self, camel: Camel) -> Self {
378        let mut dice = self.0.clone();
379        dice.remove(&camel);
380        Self::from(dice)
381    }
382}
383
384impl Default for Dice {
385    fn default() -> Self {
386        let mut dice = HashSet::new();
387        dice.insert(Camel::Red);
388        dice.insert(Camel::Orange);
389        dice.insert(Camel::Yellow);
390        dice.insert(Camel::Green);
391        dice.insert(Camel::White);
392        Self::from(dice)
393    }
394}
395
396impl From<HashSet<Camel>> for Dice {
397    fn from(dice: HashSet<Camel>) -> Self {
398        Self(dice)
399    }
400}
401
402impl FromStr for Dice {
403    type Err = NoDice;
404
405    fn from_str(input: &str) -> Result<Self, Self::Err> {
406        let mut dice = HashSet::new();
407        let mut index = 0;
408        while index < input.len() {
409            let marker = input[index..=index].parse::<Marker>()?;
410            index += 1;
411            match marker.to_camel() {
412                Some(camel) => {
413                    dice.insert(camel);
414                }
415                None => {
416                    return Err(NoDice::NotACamel);
417                }
418            }
419        }
420        Ok(Dice::from(dice))
421    }
422}
423
424impl IntoIterator for Dice {
425    type Item = Camel;
426    type IntoIter = std::collections::hash_set::IntoIter<Self::Item>;
427
428    fn into_iter(self) -> Self::IntoIter {
429        self.0.into_iter()
430    }
431}
432
433/// When parsing of Dice goes wrong, this enumeration tells you precisely what went down.
434#[derive(PartialEq, Debug)]
435pub enum NoDice {
436    /// What is encountered isn't even a marker.
437    NotAMarker(NotAMarker),
438    /// It is a marker, but not a camel.
439    NotACamel,
440}
441
442impl From<NotAMarker> for NoDice {
443    fn from(error: NotAMarker) -> Self {
444        NoDice::NotAMarker(error)
445    }
446}
447
448#[cfg(test)]
449mod test {
450    use super::*;
451
452    #[test]
453    fn races_can_be_equated() {
454        let left = Race::from(vec![
455            Marker::Camel(Camel::Red),
456            Marker::Divider,
457            Marker::Camel(Camel::Yellow),
458        ]);
459        let right = Race::from(vec![
460            Marker::Camel(Camel::Red),
461            Marker::Divider,
462            Marker::Camel(Camel::Yellow),
463        ]);
464
465        assert_eq!(left, right);
466    }
467
468    #[test]
469    fn races_can_be_parsed() {
470        let left = "r,y".parse::<Race>().expect("to parse");
471        let right = Race::from(vec![
472            Marker::Camel(Camel::Red),
473            Marker::Divider,
474            Marker::Camel(Camel::Yellow),
475        ]);
476
477        assert_eq!(left, right);
478    }
479
480    #[test]
481    fn camel_can_not_be_in_an_oasis() {
482        let left = "r+,y".parse::<Race>();
483        let right = Err(RaceParseError::CamelInOasis);
484
485        assert_eq!(left, right);
486    }
487
488    #[test]
489    fn camel_can_not_be_in_a_fata_morgana() {
490        let left = "r-,y".parse::<Race>();
491        let right = Err(RaceParseError::CamelInFataMorgana);
492
493        assert_eq!(left, right);
494    }
495
496    #[test]
497    fn adjustments_can_not_be_in_same_position() {
498        let left = "r,+-,y".parse::<Race>();
499        let right = Err(RaceParseError::ToManyAdjustmentsInOnePosition);
500
501        assert_eq!(left, right);
502    }
503
504    #[test]
505    fn adjustments_can_not_be_consecutive() {
506        let left = "r,+,-,y".parse::<Race>();
507        let right = Err(RaceParseError::ConsecutiveAdjustments);
508
509        assert_eq!(left, right);
510    }
511
512    #[test]
513    fn races_are_normalized() {
514        let left = ",,,,,,,r,y,,,,,,,,,,,".parse::<Race>().expect("to parse");
515        let right = Race::from(vec![
516            Marker::Camel(Camel::Red),
517            Marker::Divider,
518            Marker::Camel(Camel::Yellow),
519        ]);
520
521        assert_eq!(left, right);
522    }
523
524    #[test]
525    fn races_can_perform_a_roll_one() {
526        let race = "ro,y".parse::<Race>().expect("to parse");
527        let result = race.perform((Camel::Red, Face::One));
528        let expected = "yro".parse::<Race>().expect("to parse");
529
530        assert_eq!(result, expected);
531    }
532
533    #[test]
534    fn races_can_perform_a_roll_two() {
535        let race = "ro,y".parse::<Race>().expect("to parse");
536        let result = race.perform((Camel::Red, Face::Two));
537        let expected = "y,ro".parse::<Race>().expect("to parse");
538
539        assert_eq!(result, expected);
540    }
541
542    #[test]
543    fn races_can_perform_a_roll_three() {
544        let race = "ro,y".parse::<Race>().expect("to parse");
545        let result = race.perform((Camel::Red, Face::Three));
546        let expected = "y,,ro".parse::<Race>().expect("to parse");
547
548        assert_eq!(result, expected);
549    }
550
551    #[test]
552    fn oasis_advance_a_camel_when_it_lands() {
553        let race = "r,+,y".parse::<Race>().expect("to parse");
554        let result = race.perform((Camel::Red, Face::One));
555        let expected = "+,yr".parse::<Race>().expect("to parse");
556
557        assert_eq!(result, expected);
558    }
559
560    #[test]
561    fn fata_morgana_retreats_a_camel_when_it_lands() {
562        let race = "r,-,y".parse::<Race>().expect("to parse");
563        let result = race.perform((Camel::Red, Face::One));
564        let expected = "r,-,y".parse::<Race>().expect("to parse");
565
566        assert_eq!(result, expected);
567    }
568
569    #[test]
570    fn dice_can_be_parsed() {
571        let actual = "ryg".parse::<Dice>().expect("to parse");
572        let mut dice = HashSet::new();
573        dice.insert(Camel::Red);
574        dice.insert(Camel::Yellow);
575        dice.insert(Camel::Green);
576
577        assert_eq!(actual, Dice::from(dice));
578    }
579
580    #[test]
581    fn races_have_winners_runner_ups_and_losers() {
582        let race = "r,y,g".parse::<Race>().expect("to parse");
583        let winner = race.winner();
584        let runner_up = race.runner_up();
585        let loser = race.loser();
586
587        assert_eq!(winner, Some(Camel::Green));
588        assert_eq!(runner_up, Some(Camel::Yellow));
589        assert_eq!(loser, Some(Camel::Red));
590    }
591}