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