1use super::*;
4
5impl Board {
6 pub fn state_hash(&self) -> u64 {
10 let Self {
18 grid:
20 Grid {
21 width: _,
22 height: _,
23 h_walls: _,
24 v_walls: _,
25 tiles: _,
26 },
27 signposts: _,
28 crabs: _,
30 scores: _,
31 rng: _,
32 gulls: _,
33 next_gull_id: _,
34 tick: _,
36 signpost_seq: _,
37 next_crab_id: _,
38 rules:
39 Rules {
40 signpost_cap: _,
41 cap_policy: _,
42 gull_period: _,
43 round_length: _,
44 castle_raids: _,
45 },
46 lure: _,
47 lure_cooldown: _,
48 event_cooldown: _,
49 crabs_banked: _,
50 golden_banked: _,
51 events_enabled: _,
52 mania: _,
53 tempo: _,
54 last_event: _,
55 wrap: _,
56 seed: _,
66 event_queue: _,
67 swept_home: _,
68 } = self;
69 let mut h = Fnv::new();
70 self.hash_terrain(&mut h);
71 self.hash_creatures(&mut h);
72 self.hash_round(&mut h);
73 h.finish()
74 }
75
76 fn hash_terrain(&self, h: &mut Fnv) {
79 h.u8(self.grid.width);
80 h.u8(self.grid.height);
81 for &wall in &self.grid.h_walls {
82 h.bool(wall);
83 }
84 for &wall in &self.grid.v_walls {
85 h.bool(wall);
86 }
87 for tile in &self.grid.tiles {
88 match *tile {
89 TileKind::Empty => h.u8(0),
90 TileKind::Rock => h.u8(1),
91 TileKind::Castle(owner) => {
92 h.u8(2);
93 h.u8(owner);
94 }
95 TileKind::Spawner(s) => {
96 h.u8(3);
97 h.u8(s.dir.id());
98 h.u32(s.period);
99 }
100 TileKind::Turnstile { next_right } => {
101 h.u8(4);
102 h.bool(next_right);
103 }
104 TileKind::Kelp => h.u8(5),
105 TileKind::Pool => h.u8(6),
106 }
107 }
108 for slot in &self.signposts {
109 match slot {
110 None => h.u8(0),
111 Some(sp) => {
112 h.u8(1);
113 h.u8(sp.dir.id());
114 h.u8(sp.owner);
115 h.u8(match sp.health {
116 SignpostHealth::Full => 0,
117 SignpostHealth::Worn => 1,
118 });
119 h.u64(sp.seq);
120 h.u64(sp.placed);
121 }
122 }
123 }
124 }
125
126 fn hash_creatures(&self, h: &mut Fnv) {
129 for crab in &self.crabs {
130 h.u32(crab.id);
131 h.u16(crab.tile);
132 h.u8(crab.dir.id());
133 h.u16(crab.progress);
134 h.u16(crab.prev.tile);
135 h.u16(crab.prev.progress);
136 h.u8(crab.prev.dir.id());
137 h.u8(crab.handed.id());
138 h.u8(crab.kind.id());
139 }
140 for &score in &self.scores {
141 h.u32(score);
142 }
143 let (state, inc) = self.rng.hash_state();
144 h.u64(state);
145 h.u64(inc);
146 for gull in &self.gulls {
147 h.u32(gull.id);
148 h.u16(gull.tile);
149 h.u8(gull.dir.id());
150 h.u16(gull.progress);
151 h.u16(gull.prev.tile);
152 h.u16(gull.prev.progress);
153 h.u8(gull.prev.dir.id());
154 h.u8(gull.handed.id());
155 match gull.state {
156 GullState::Walking => h.u8(0),
157 GullState::Flying { remaining } => {
158 h.u8(1);
159 h.u8(remaining);
160 }
161 }
162 h.u32(gull.takeoff_in);
163 }
164 h.u32(self.next_gull_id);
165 }
166
167 fn hash_round(&self, h: &mut Fnv) {
170 h.u32(self.rules.gull_period);
171 match self.rules.round_length {
172 None => h.u8(0),
173 Some(len) => {
174 h.u8(1);
175 h.u32(len);
176 }
177 }
178 match self.lure {
179 None => h.u8(0),
180 Some((p, t)) => {
181 h.u8(1);
182 h.u8(p);
183 h.u32(t);
184 }
185 }
186 h.u32(self.golden_banked);
187 h.bool(self.rules.castle_raids);
188 h.bool(self.events_enabled);
189 h.bool(self.wrap);
190 match self.mania {
191 None => h.u8(0),
192 Some((Mania::Crab, t)) => {
193 h.u8(1);
194 h.u32(t);
195 }
196 Some((Mania::Gull, t)) => {
197 h.u8(2);
198 h.u32(t);
199 }
200 }
201 match self.tempo {
202 None => h.u8(0),
203 Some((tempo, t)) => {
204 h.u8(match tempo {
205 Tempo::Fast => 1,
206 Tempo::Slow => 2,
207 });
208 h.u32(t);
209 }
210 }
211 match self.last_event {
212 None => h.u8(0),
213 Some((event, tick)) => {
214 h.u8(1 + event.index() as u8);
218 h.u64(tick);
219 }
220 }
221 h.u64(self.tick);
222 h.u64(self.signpost_seq);
223 h.u32(self.next_crab_id);
224 h.u32(self.crabs_banked);
225 h.u8(self.rules.signpost_cap);
226 h.u8(match self.rules.cap_policy {
227 CapPolicy::Evict => 0,
228 CapPolicy::Reject => 1,
229 });
230 h.u32(self.lure_cooldown);
234 h.u32(self.event_cooldown);
235 }
236}
237
238#[cfg(test)]
239mod tests {
240 use crate::sim::{Board, CapPolicy, CrabKind, Direction, Handedness, Spawner, TileKind};
241
242 #[test]
245 fn every_mutator_changes_the_hash() {
246 let base = || Board::new(8, 6, 42);
247 type Mutation = (&'static str, Box<dyn Fn(&mut Board)>);
248 let mutations: Vec<Mutation> = vec![
249 ("set_tile", Box::new(|b| b.set_tile(2, 2, TileKind::Rock))),
250 (
251 "set_spawner",
252 Box::new(|b| {
253 b.set_tile(
254 3,
255 3,
256 TileKind::Spawner(Spawner {
257 dir: Direction::Right,
258 period: 40,
259 }),
260 );
261 }),
262 ),
263 (
264 "set_wall",
265 Box::new(|b| b.set_wall(1, 1, Direction::Up, true)),
266 ),
267 ("set_wrap", Box::new(|b| b.set_wrap(true))),
268 ("set_score", Box::new(|b| b.set_score(0, 7))),
269 ("set_gull_period", Box::new(|b| b.set_gull_period(99))),
270 (
271 "set_round_length",
272 Box::new(|b| b.set_round_length(Some(500))),
273 ),
274 (
275 "set_events_enabled",
276 Box::new(|b| b.set_events_enabled(true)),
277 ),
278 ("set_castle_raids", Box::new(|b| b.set_castle_raids(false))),
279 (
280 "set_signpost_rule",
281 Box::new(|b| b.set_signpost_rule(5, CapPolicy::Reject)),
282 ),
283 (
284 "place_signpost",
285 Box::new(|b| {
286 b.place_signpost(0, 4, 4, Direction::Left);
287 }),
288 ),
289 (
290 "spawn_crab",
291 Box::new(|b| {
292 b.spawn_crab(2, 3, Direction::Up, Handedness::Left, CrabKind::Common);
293 }),
294 ),
295 (
296 "spawn_gull",
297 Box::new(|b| b.spawn_gull(5, 2, Direction::Down)),
298 ),
299 ("tick_idle", Box::new(Board::tick_idle)),
300 ];
301 let clean = base().state_hash();
302 for (name, mutate) in mutations {
303 let mut board = base();
304 mutate(&mut board);
305 assert_ne!(board.state_hash(), clean, "{name} did not move state_hash");
306 }
307 }
308
309 #[test]
315 fn the_lure_cooldown_is_part_of_the_fingerprint() {
316 use crate::sim::{MAX_PLAYERS, PlayerAction};
317 let armed = |cooldown: u32| {
318 let mut board = Board::new(6, 5, 1);
319 board.set_tile(3, 2, TileKind::Castle(0));
320 board.lure_cooldown = cooldown;
321 board.spawn_crab(2, 2, Direction::Right, Handedness::Right, CrabKind::Molting);
322 board
323 };
324 let (mut quiet, mut ready) = (armed(200), armed(0));
325 assert_ne!(
326 quiet.state_hash(),
327 ready.state_hash(),
328 "a cooldown that changes the round has to change the hash"
329 );
330 for board in [&mut quiet, &mut ready] {
331 for _ in 0..40 {
332 board.tick(&[PlayerAction::None; MAX_PLAYERS]);
333 }
334 }
335 assert!(quiet.lure().is_none(), "the quiet spell swallows the lure");
336 assert!(ready.lure().is_some(), "a clear board starts one");
337 }
338
339 #[test]
346 fn the_event_cooldown_is_part_of_the_fingerprint() {
347 use crate::sim::{MAX_PLAYERS, PlayerAction};
348 let armed = |cooldown: u32| {
349 let mut board = Board::new(6, 5, 1);
350 board.set_events_enabled(true);
351 board.set_tile(3, 2, TileKind::Castle(0));
352 board.event_cooldown = cooldown;
353 board.spawn_crab(
354 2,
355 2,
356 Direction::Right,
357 Handedness::Right,
358 CrabKind::Sparkling,
359 );
360 board
361 };
362 let (mut quiet, mut ready) = (armed(200), armed(0));
363 assert_ne!(
364 quiet.state_hash(),
365 ready.state_hash(),
366 "a cooldown that changes the round has to change the hash"
367 );
368 for board in [&mut quiet, &mut ready] {
369 for _ in 0..40 {
370 board.tick(&[PlayerAction::None; MAX_PLAYERS]);
371 }
372 }
373 assert!(
374 quiet.last_event().is_none(),
375 "the cooldown swallows the spin"
376 );
377 assert!(
378 ready.last_event().is_some(),
379 "a clear board spins the wheel"
380 );
381 }
382}