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/// Spawn-mania flavours (tide events).
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 // One draw indexes ALL, so the roulette's order *is* ALL's order:
73 // the same one the string tables and the snapshot use.
74 let event = TideEvent::ALL[(self.rng.next_u32() % TideEvent::ALL.len() as u32) as usize];
75 self.apply_tide_event(self.surge_safe(event), banker);
76 }
77
78 /// The surge already doubles the flock, so the roulette keeps off the
79 /// gull events for the last 30 seconds. Observed once live: Gull Mania
80 /// on top of the surge left fifteen gulls and almost no crabs with half
81 /// a minute to play, which is the tensest stretch of a round with
82 /// nothing left to route. Swapped rather than re-rolled, so the draw
83 /// count stays fixed.
84 fn surge_safe(&self, event: TideEvent) -> TideEvent {
85 if !self.in_surge() {
86 return event;
87 }
88 match event {
89 TideEvent::GullMania => TideEvent::CrabMania,
90 TideEvent::GullAttack => TideEvent::SpeedUp,
91 kept @ (TideEvent::CrabMania
92 | TideEvent::Monopoly
93 | TideEvent::SpeedUp
94 | TideEvent::SlowDown
95 | TideEvent::FreshSand
96 | TideEvent::CastleSwap) => kept,
97 }
98 }
99
100 /// Apply one tide event's effects (split from the roulette so each event
101 /// is unit-testable in isolation).
102 /// Start a lure for `owner`, as banking a molting crab does.
103 ///
104 /// Same reason as [`Self::force_tide_event`]: the dev hook and the
105 /// tests need one on demand, and waiting for a molt to turn up and be
106 /// banked is not a thing a screenshot can do.
107 pub fn force_lure(&mut self, owner: PlayerId) {
108 self.lure = Some((owner, crate::sim::LURE_TICKS));
109 }
110
111 /// Fire a named tide event outright. The roulette is the only caller
112 /// in play; this exists for the dev hook that has to show one on
113 /// demand, and for the tests, which cannot wait for a sparkling crab.
114 pub fn force_tide_event(&mut self, event: TideEvent, banker: PlayerId) {
115 self.apply_tide_event(event, banker);
116 }
117
118 pub(super) fn apply_tide_event(&mut self, event: TideEvent, banker: PlayerId) {
119 self.last_event = Some((event, self.tick));
120 match event {
121 TideEvent::CrabMania => {
122 self.gulls.clear();
123 self.mania = Some((Mania::Crab, EVENT_TICKS));
124 }
125 TideEvent::GullMania => {
126 self.mania = Some((Mania::Gull, EVENT_TICKS));
127 }
128 TideEvent::Monopoly => {
129 // Half the loose crabs (front of the line) scuttle straight
130 // into the banker's castle.
131 let take = self.crabs.len() / 2;
132 for crab in self.crabs.drain(..take) {
133 self.scores[banker as usize] += crab.kind.value();
134 self.crabs_banked += 1;
135 if crab.kind == CrabKind::Golden {
136 self.golden_banked += 1;
137 }
138 }
139 }
140 TideEvent::GullAttack => {
141 // A gull lands beside every rival castle, facing it.
142 let targets: Vec<(u16, PlayerId)> = self
143 .tiles
144 .iter()
145 .enumerate()
146 .filter_map(|(t, tile)| match tile {
147 TileKind::Castle(owner) if *owner != banker => Some((t as u16, *owner)),
148 TileKind::Castle(_)
149 | TileKind::Empty
150 | TileKind::Rock
151 | TileKind::Spawner(_)
152 | TileKind::Turnstile { .. }
153 | TileKind::Kelp
154 | TileKind::Pool => None,
155 })
156 .collect();
157 for (castle, _) in targets {
158 let (cx, cy) = self.coords(castle);
159 // The first open edge-adjacent spot; the gull faces
160 // back toward the castle it besieges.
161 let ring = self.ring_openings(cx, cy, &CASTLE_RING[..4]);
162 if let Some(&(nx, ny, ox, oy)) = ring.first() {
163 let dir = Direction::toward(ox, oy).reverse();
164 self.spawn_gull(nx as u8, ny as u8, dir);
165 }
166 }
167 }
168 TideEvent::SpeedUp => self.tempo = Some((Tempo::Fast, EVENT_TICKS)),
169 TideEvent::SlowDown => self.tempo = Some((Tempo::Slow, EVENT_TICKS)),
170 TideEvent::FreshSand => {
171 for slot in &mut self.signposts {
172 *slot = None;
173 }
174 }
175 TideEvent::CastleSwap => {
176 // Rockets swap places: every castle passes to the next
177 // participating owner, in a fixed rotation.
178 let mut owners: Vec<PlayerId> = Vec::new();
179 for tile in &self.tiles {
180 if let TileKind::Castle(owner) = tile
181 && !owners.contains(owner)
182 {
183 owners.push(*owner);
184 }
185 }
186 if owners.len() > 1 {
187 for tile in &mut self.tiles {
188 if let TileKind::Castle(owner) = tile {
189 let at = owners.iter().position(|o| o == owner).unwrap_or(0);
190 *owner = owners[(at + 1) % owners.len()];
191 }
192 }
193 }
194 }
195 }
196 }
197}