Skip to main content

pinch_points/sim/
crab.rs

1use crate::sim::direction::Direction;
2use crate::sim::pose::Pose;
3
4/// Which claw is oversized, i.e. which side this crab tries first when its
5/// forward path is blocked. The one-bit divergence from ChuChu Rocket that
6/// turns herding puzzles into sorting puzzles (spec §2).
7#[derive(Clone, Copy, PartialEq, Eq, Debug)]
8pub enum Handedness {
9    Left,
10    Right,
11}
12
13#[derive(Clone, Copy, PartialEq, Eq, Debug)]
14pub enum CrabKind {
15    Common,
16    Juvenile,
17    Giant,
18    /// Banking one starts the 10-second lure that draws every loose crab to
19    /// the banking castle.
20    Molting,
21    /// The jackpot (ChuChu Rocket's gold mouse, re-shelled): vanishingly
22    /// rare, quick, and worth a small castle tier by itself.
23    Golden,
24    /// The "?" mouse, re-shelled: banking it spins the tide-event roulette
25    /// on boards where events are enabled.
26    Sparkling,
27}
28
29impl CrabKind {
30    pub const ALL: [CrabKind; 6] = [
31        CrabKind::Common,
32        CrabKind::Juvenile,
33        CrabKind::Giant,
34        CrabKind::Molting,
35        CrabKind::Golden,
36        CrabKind::Sparkling,
37    ];
38
39    /// The level-format token; `from_token` is its inverse.
40    pub fn token(self) -> &'static str {
41        match self {
42            CrabKind::Common => "common",
43            CrabKind::Juvenile => "juvenile",
44            CrabKind::Giant => "giant",
45            CrabKind::Molting => "molting",
46            CrabKind::Golden => "golden",
47            CrabKind::Sparkling => "sparkling",
48        }
49    }
50
51    pub fn from_token(token: &str) -> Option<CrabKind> {
52        Self::ALL.iter().copied().find(|k| k.token() == token)
53    }
54
55    /// Walking speed in subunits per tick (one tile = 256 subunits, spec §4.2).
56    pub fn speed(self) -> u16 {
57        match self {
58            CrabKind::Common | CrabKind::Molting | CrabKind::Sparkling => 12,
59            CrabKind::Juvenile => 18, // 1.5×
60            CrabKind::Giant => 7,     // 0.6× of 12 is 7.2; integer sim rounds down
61            CrabKind::Golden => 15,   // 1.25×: catchable, but it makes you work
62        }
63    }
64
65    /// Score awarded when banked. Every kind banks differently: commons are
66    /// the bread and butter, juveniles pay a little extra for being hard to
67    /// route, molting crabs are prized (and start the lure), giants are the
68    /// jackpot worth protecting.
69    pub fn value(self) -> u32 {
70        match self {
71            CrabKind::Common => 1,
72            CrabKind::Juvenile => 2,
73            CrabKind::Molting => 5,
74            CrabKind::Giant => 10,
75            CrabKind::Golden => 50,
76            CrabKind::Sparkling => 1, // the event is the prize
77        }
78    }
79
80    pub(crate) fn id(self) -> u8 {
81        match self {
82            CrabKind::Common => 0,
83            CrabKind::Juvenile => 1,
84            CrabKind::Giant => 2,
85            CrabKind::Molting => 3,
86            CrabKind::Golden => 4,
87            CrabKind::Sparkling => 5,
88        }
89    }
90}
91
92/// One crab. Position is integer-only: the crab is `progress` subunits past the
93/// centre of `tile`, moving toward the centre of the next tile in `dir`.
94/// `prev` holds last tick's position for render-side interpolation (spec §7.4)
95/// and is never read by the simulation itself.
96#[derive(Clone, Copy, Debug)]
97pub struct Crab {
98    /// Stable identity for the render layer's sprite mapping; unique within a
99    /// match, assigned by the `Board` at spawn.
100    pub id: u32,
101    pub tile: u16,
102    pub dir: Direction,
103    pub progress: u16,
104    pub prev: Pose,
105    pub handed: Handedness,
106    pub kind: CrabKind,
107}
108
109impl Crab {
110    /// The current position as one value, so a tick can save it into
111    /// `prev` in a single assignment.
112    pub fn pose(&self) -> Pose {
113        Pose {
114            tile: self.tile,
115            dir: self.dir,
116            progress: self.progress,
117        }
118    }
119}
120
121impl Handedness {
122    pub(crate) fn id(self) -> u8 {
123        match self {
124            Handedness::Left => 0,
125            Handedness::Right => 1,
126        }
127    }
128
129    /// The level-format token (`L`/`R`); `from_token` is its inverse.
130    pub fn token(self) -> &'static str {
131        match self {
132            Handedness::Left => "L",
133            Handedness::Right => "R",
134        }
135    }
136
137    pub fn from_token(token: &str) -> Option<Handedness> {
138        match token {
139            "L" => Some(Handedness::Left),
140            "R" => Some(Handedness::Right),
141            _ => None,
142        }
143    }
144}
145
146#[cfg(test)]
147mod tests {
148    use super::{CrabKind, Handedness};
149
150    #[test]
151    fn tokens_round_trip_for_every_kind() {
152        for kind in CrabKind::ALL {
153            assert_eq!(CrabKind::from_token(kind.token()), Some(kind));
154        }
155        assert_eq!(CrabKind::from_token("hermit"), None);
156        for handed in [Handedness::Left, Handedness::Right] {
157            assert_eq!(Handedness::from_token(handed.token()), Some(handed));
158        }
159    }
160
161    #[test]
162    fn golden_crab_is_the_jackpot() {
163        assert_eq!(CrabKind::Golden.value(), 50);
164        assert!(CrabKind::Golden.speed() > CrabKind::Common.speed());
165        assert!(CrabKind::Golden.speed() < CrabKind::Juvenile.speed());
166    }
167}