Skip to main content

pinch_points/sim/board/
events.rs

1//! Tide events: the sparkling crab's roulette, and the event and mania
2//! types it draws from.
3
4use super::*;
5
6/// Tempo shifts (tide events): the beach's clock run faster or slower.
7#[derive(Clone, Copy, PartialEq, Eq, Debug)]
8pub enum Tempo {
9    /// Everything on the beach moves at double speed.
10    Fast,
11    /// And at half.
12    Slow,
13}
14
15#[derive(Clone, Copy, PartialEq, Eq, Debug)]
16pub enum Mania {
17    Crab,
18    Gull,
19}
20
21/// The sparkling crab's roulette (the original's "?"-mouse events, re-themed
22/// for the beach).
23#[derive(Clone, Copy, PartialEq, Eq, Debug)]
24pub enum TideEvent {
25    /// Gulls washed away; spawners flood crabs for a while.
26    CrabMania,
27    /// Spawners emit gulls for a while.
28    GullMania,
29    /// Half the loose crabs scuttle straight into the banker's castle.
30    Monopoly,
31    /// A gull lands beside every rival castle.
32    GullAttack,
33    SpeedUp,
34    SlowDown,
35    /// Every signpost on the beach washes away.
36    FreshSand,
37    /// Castles trade owners (rockets swap places!).
38    CastleSwap,
39}
40
41impl TideEvent {
42    /// Every event, in the order the roulette and the string tables use.
43    pub const ALL: [TideEvent; 8] = [
44        TideEvent::CrabMania,
45        TideEvent::GullMania,
46        TideEvent::Monopoly,
47        TideEvent::GullAttack,
48        TideEvent::SpeedUp,
49        TideEvent::SlowDown,
50        TideEvent::FreshSand,
51        TideEvent::CastleSwap,
52    ];
53
54    /// Position in [`TideEvent::ALL`]: the index of this event's name in the
55    /// string tables, and the bit that records having seen it.
56    pub fn index(self) -> usize {
57        TideEvent::ALL
58            .iter()
59            .position(|&event| event == self)
60            .expect("every event is in ALL")
61    }
62}
63
64impl Board {
65    /// The sparkling crab's roulette (spec: the original's "?"-mouse random
66    /// events, re-themed). Deterministic: one PRNG draw picks the event, and
67    /// every effect operates in fixed order.
68    pub(super) fn spin_tide_event(&mut self, banker: PlayerId) {
69        if !self.events_enabled {
70            return;
71        }
72        // Not while the last event is still running. The wheel is spun by
73        // banking a Sparkling crab, and several of the faces it lands on
74        // put more crabs on the beach, so without this the events feed
75        // themselves: three Crab Manias inside fourteen seconds was
76        // measured on a kept round. The crab still banks and still scores;
77        // what it does not do is stack a second event on the one the
78        // player is already watching.
79        if self.event_cooldown > 0 {
80            return;
81        }
82        // One draw indexes ALL, so the roulette's order *is* ALL's order:
83        // the same one the string tables and the snapshot use.
84        let event = TideEvent::ALL[(self.rng.next_u32() % TideEvent::ALL.len() as u32) as usize];
85        self.apply_tide_event(self.surge_safe(event), banker);
86    }
87
88    /// The surge already doubles the flock, so the roulette keeps off the
89    /// gull events for the last 30 seconds. Observed once live: Gull Mania
90    /// on top of the surge left fifteen gulls and almost no crabs with half
91    /// a minute to play, which is the tensest stretch of a round with
92    /// nothing left to route. Swapped rather than re-rolled, so the draw
93    /// count stays fixed.
94    pub(super) fn surge_safe(&self, event: TideEvent) -> TideEvent {
95        if !self.in_surge() {
96            return event;
97        }
98        match event {
99            TideEvent::GullMania => TideEvent::CrabMania,
100            TideEvent::GullAttack => TideEvent::SpeedUp,
101            kept @ (TideEvent::CrabMania
102            | TideEvent::Monopoly
103            | TideEvent::SpeedUp
104            | TideEvent::SlowDown
105            | TideEvent::FreshSand
106            | TideEvent::CastleSwap) => kept,
107        }
108    }
109
110    /// Start a lure for `owner`, as banking a molting crab does.
111    ///
112    /// Same reason as [`Self::force_tide_event`]: the dev hook and the
113    /// tests need one on demand, and waiting for a molt to turn up and be
114    /// banked is not a thing a screenshot can do.
115    pub fn force_lure(&mut self, owner: PlayerId) {
116        self.lure = Some((owner, crate::sim::LURE_TICKS));
117    }
118
119    /// Fire a named tide event outright. The roulette is the only caller
120    /// in play; this exists for the dev hook that has to show one on
121    /// demand, and for the tests, which cannot wait for a sparkling crab.
122    pub fn force_tide_event(&mut self, event: TideEvent, banker: PlayerId) {
123        self.apply_tide_event(event, banker);
124    }
125
126    /// Apply one tide event's effects (split from the roulette so each event
127    /// is unit-testable in isolation).
128    pub(super) fn apply_tide_event(&mut self, event: TideEvent, banker: PlayerId) {
129        self.last_event = Some((event, self.tick));
130        // Set here rather than in the roulette so a forced event starts
131        // the clock too: the point is "an event is running", not "the
132        // wheel was spun".
133        self.event_cooldown = crate::sim::EVENT_COOLDOWN;
134        match event {
135            TideEvent::CrabMania => {
136                self.gulls.clear();
137                self.mania = Some((Mania::Crab, EVENT_TICKS));
138            }
139            TideEvent::GullMania => {
140                self.mania = Some((Mania::Gull, EVENT_TICKS));
141            }
142            TideEvent::Monopoly => {
143                // Half the loose crabs (front of the line) scuttle straight
144                // into the banker's castle.
145                let take = self.crabs.len() / 2;
146                let at = self.tick;
147                for crab in self.crabs.drain(..take) {
148                    self.scores[banker as usize] += crab.kind.value();
149                    self.crabs_banked += 1;
150                    if crab.kind == CrabKind::Golden {
151                        self.golden_banked += 1;
152                    }
153                    // Written down because it cannot be worked out later:
154                    // the crab is gone from a tile that is not a castle,
155                    // which from the outside is exactly what being eaten
156                    // looks like. See `Board::swept_home`.
157                    self.swept_home.push(Swept {
158                        crab: crab.id,
159                        owner: banker,
160                        at,
161                    });
162                }
163            }
164            TideEvent::GullAttack => {
165                // A gull lands beside every rival castle, facing it.
166                let targets: Vec<(u16, PlayerId)> = self
167                    .grid
168                    .tiles
169                    .iter()
170                    .enumerate()
171                    .filter_map(|(t, tile)| match tile {
172                        TileKind::Castle(owner) if *owner != banker => Some((t as u16, *owner)),
173                        TileKind::Castle(_)
174                        | TileKind::Empty
175                        | TileKind::Rock
176                        | TileKind::Spawner(_)
177                        | TileKind::Turnstile { .. }
178                        | TileKind::Kelp
179                        | TileKind::Pool => None,
180                    })
181                    .collect();
182                for (castle, _) in targets {
183                    let (cx, cy) = self.coords(castle);
184                    // The first open edge-adjacent spot; the gull faces
185                    // back toward the castle it besieges.
186                    let ring = self.ring_openings(cx, cy, &CASTLE_RING[..4]);
187                    if let Some(&(nx, ny, ox, oy)) = ring.first() {
188                        let dir = Direction::toward(ox, oy).reverse();
189                        self.spawn_gull(nx as u8, ny as u8, dir);
190                    }
191                }
192            }
193            TideEvent::SpeedUp => self.tempo = Some((Tempo::Fast, EVENT_TICKS)),
194            TideEvent::SlowDown => self.tempo = Some((Tempo::Slow, EVENT_TICKS)),
195            TideEvent::FreshSand => self.signposts.fill(None),
196            TideEvent::CastleSwap => {
197                // Rockets swap places: every castle passes to the next
198                // participating owner, in a fixed rotation.
199                let mut owners: Vec<PlayerId> = Vec::new();
200                for tile in &self.grid.tiles {
201                    if let TileKind::Castle(owner) = tile
202                        && !owners.contains(owner)
203                    {
204                        owners.push(*owner);
205                    }
206                }
207                if owners.len() > 1 {
208                    for tile in &mut self.grid.tiles {
209                        if let TileKind::Castle(owner) = tile {
210                            let at = owners.iter().position(|o| o == owner).unwrap_or(0);
211                            *owner = owners[(at + 1) % owners.len()];
212                        }
213                    }
214                }
215            }
216        }
217    }
218}