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
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
//! Tide events: the sparkling crab's roulette, and the event and mania
//! types it draws from.
use super::*;
/// Tempo shifts (tide events): the beach's clock run faster or slower.
#[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,
/// The tide calls a claw: for a while, a right-clawed crab banks for
/// double and a left-clawed one costs what it was worth.
///
/// Always the right claw, never drawn: the rule falls on the whole
/// beach at once, so a fixed one is as fair as a coin and is learned
/// once instead of read off the banner every time.
RightClaws,
}
impl TideEvent {
/// Every event, in the order the roulette and the string tables use.
pub const ALL: [TideEvent; 9] = [
TideEvent::CrabMania,
TideEvent::GullMania,
TideEvent::Monopoly,
TideEvent::GullAttack,
TideEvent::SpeedUp,
TideEvent::SlowDown,
TideEvent::FreshSand,
TideEvent::CastleSwap,
// Appended, never inserted: the index is the string table's row and
// the bit in the seen-it mask, so an older save keeps its meaning.
TideEvent::RightClaws,
];
/// 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;
}
// Not while the last event is still running. The wheel is spun by
// banking a Sparkling crab, and several of the faces it lands on
// put more crabs on the beach, so without this the events feed
// themselves: three Crab Manias inside fourteen seconds, measured.
// The crab still banks and still scores.
if self.event_cooldown > 0 {
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: Gull Mania on top of the surge
/// left fifteen gulls and almost no crabs with half a minute to play.
/// Swapped rather than re-rolled, so the draw count stays fixed.
pub(super) 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 through the surge: it puts nothing on the beach, and a
// scramble over which claw is worth banking is exactly what the
// last thirty seconds are for.
| TideEvent::RightClaws) => kept,
}
}
/// 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 a screenshot cannot wait for a molt to
/// turn up and be banked.
pub fn force_lure(&mut self, owner: PlayerId) {
self.lure = Some((owner, 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);
}
/// Apply one tide event's effects (split from the roulette so each event
/// is unit-testable in isolation).
pub(super) fn apply_tide_event(&mut self, event: TideEvent, banker: PlayerId) {
self.last_event = Some((event, self.tick));
// Set here rather than in the roulette so a forced event starts
// the clock too: the point is "an event is running", not "the
// wheel was spun".
self.event_cooldown = EVENT_COOLDOWN;
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;
let at = self.tick;
// Drained first: `credit_bank` takes `&mut self`, and the
// sweep is a fixed-order loop over a list that is being
// emptied anyway.
let swept: Vec<Crab> = self.crabs.drain(..take).collect();
for crab in swept {
self.credit_bank(banker, &crab);
self.crabs_banked += 1;
if crab.kind == CrabKind::Golden {
self.golden_banked += 1;
}
// Written down because it cannot be worked out later:
// the crab is gone from a tile that is not a castle,
// which from the outside is exactly what being eaten
// looks like. See `Board::swept_home`.
self.swept_home.push(Swept {
crab: crab.id,
owner: banker,
at,
});
}
}
TideEvent::GullAttack => {
// A gull lands beside every rival castle, facing it.
let targets: Vec<(u16, PlayerId)> = self
.grid
.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::RightClaws => self.claw_call = EVENT_TICKS,
TideEvent::SpeedUp => self.tempo = Some((Tempo::Fast, EVENT_TICKS)),
TideEvent::SlowDown => self.tempo = Some((Tempo::Slow, EVENT_TICKS)),
TideEvent::FreshSand => self.signposts.fill(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.grid.tiles {
if let TileKind::Castle(owner) = tile
&& !owners.contains(owner)
{
owners.push(*owner);
}
}
if owners.len() > 1 {
for tile in &mut self.grid.tiles {
if let TileKind::Castle(owner) = tile {
let at = owners.iter().position(|o| o == owner).unwrap_or(0);
*owner = owners[(at + 1) % owners.len()];
}
}
}
}
}
}
}