Skip to main content

pinch_points/sim/
crab.rs

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