1use super::*;
4
5impl Board {
6 pub fn state_hash(&self) -> u64 {
10 let Self {
18 width: _,
20 height: _,
21 h_walls: _,
22 v_walls: _,
23 tiles: _,
24 signposts: _,
25 crabs: _,
27 scores: _,
28 rng: _,
29 gulls: _,
30 next_gull_id: _,
31 tick: _,
33 signpost_seq: _,
34 next_crab_id: _,
35 signpost_cap: _,
36 cap_policy: _,
37 gull_period: _,
38 round_length: _,
39 lure: _,
40 lure_cooldown: _,
41 crabs_banked: _,
42 golden_banked: _,
43 events_enabled: _,
44 mania: _,
45 tempo: _,
46 last_event: _,
47 wrap: _,
48 seed: _,
54 event_queue: _,
55 } = self;
56 let mut h = Fnv::new();
57 self.hash_terrain(&mut h);
58 self.hash_creatures(&mut h);
59 self.hash_round(&mut h);
60 h.finish()
61 }
62
63 fn hash_terrain(&self, h: &mut Fnv) {
66 h.u8(self.width);
67 h.u8(self.height);
68 for &wall in &self.h_walls {
69 h.bool(wall);
70 }
71 for &wall in &self.v_walls {
72 h.bool(wall);
73 }
74 for tile in &self.tiles {
75 match *tile {
76 TileKind::Empty => h.u8(0),
77 TileKind::Rock => h.u8(1),
78 TileKind::Castle(owner) => {
79 h.u8(2);
80 h.u8(owner);
81 }
82 TileKind::Spawner(s) => {
83 h.u8(3);
84 h.u8(s.dir.id());
85 h.u32(s.period);
86 }
87 TileKind::Turnstile { next_right } => {
88 h.u8(4);
89 h.bool(next_right);
90 }
91 TileKind::Kelp => h.u8(5),
92 TileKind::Pool => h.u8(6),
93 }
94 }
95 for slot in &self.signposts {
96 match slot {
97 None => h.u8(0),
98 Some(sp) => {
99 h.u8(1);
100 h.u8(sp.dir.id());
101 h.u8(sp.owner);
102 h.u8(match sp.health {
103 SignpostHealth::Full => 0,
104 SignpostHealth::Worn => 1,
105 });
106 h.u64(sp.seq);
107 h.u64(sp.placed);
108 }
109 }
110 }
111 }
112
113 fn hash_creatures(&self, h: &mut Fnv) {
116 for crab in &self.crabs {
117 h.u32(crab.id);
118 h.u16(crab.tile);
119 h.u8(crab.dir.id());
120 h.u16(crab.progress);
121 h.u16(crab.prev_tile);
122 h.u16(crab.prev_progress);
123 h.u8(crab.prev_dir.id());
124 h.u8(crab.handed.id());
125 h.u8(crab.kind.id());
126 }
127 for &score in &self.scores {
128 h.u32(score);
129 }
130 let (state, inc) = self.rng.hash_state();
131 h.u64(state);
132 h.u64(inc);
133 for gull in &self.gulls {
134 h.u32(gull.id);
135 h.u16(gull.tile);
136 h.u8(gull.dir.id());
137 h.u16(gull.progress);
138 h.u16(gull.prev_tile);
139 h.u16(gull.prev_progress);
140 h.u8(gull.prev_dir.id());
141 h.u8(gull.handed.id());
142 match gull.state {
143 GullState::Walking => h.u8(0),
144 GullState::Flying { remaining } => {
145 h.u8(1);
146 h.u8(remaining);
147 }
148 }
149 h.u32(gull.takeoff_in);
150 }
151 h.u32(self.next_gull_id);
152 }
153
154 fn hash_round(&self, h: &mut Fnv) {
157 h.u32(self.gull_period);
158 match self.round_length {
159 None => h.u8(0),
160 Some(len) => {
161 h.u8(1);
162 h.u32(len);
163 }
164 }
165 match self.lure {
166 None => h.u8(0),
167 Some((p, t)) => {
168 h.u8(1);
169 h.u8(p);
170 h.u32(t);
171 }
172 }
173 h.u32(self.golden_banked);
174 h.bool(self.events_enabled);
175 h.bool(self.wrap);
176 match self.mania {
177 None => h.u8(0),
178 Some((Mania::Crab, t)) => {
179 h.u8(1);
180 h.u32(t);
181 }
182 Some((Mania::Gull, t)) => {
183 h.u8(2);
184 h.u32(t);
185 }
186 }
187 match self.tempo {
188 None => h.u8(0),
189 Some((tempo, t)) => {
190 h.u8(match tempo {
191 Tempo::Fast => 1,
192 Tempo::Slow => 2,
193 });
194 h.u32(t);
195 }
196 }
197 match self.last_event {
198 None => h.u8(0),
199 Some((event, tick)) => {
200 h.u8(1 + event.index() as u8);
204 h.u64(tick);
205 }
206 }
207 h.u64(self.tick);
208 h.u64(self.signpost_seq);
209 h.u32(self.next_crab_id);
210 h.u32(self.crabs_banked);
211 h.u8(self.signpost_cap);
212 h.u8(match self.cap_policy {
213 CapPolicy::Evict => 0,
214 CapPolicy::Reject => 1,
215 });
216 h.u32(self.lure_cooldown);
220 }
221}
222
223#[cfg(test)]
224mod tests {
225 use crate::sim::{Board, CapPolicy, CrabKind, Direction, Handedness, Spawner, TileKind};
226
227 #[test]
230 fn every_mutator_changes_the_hash() {
231 let base = || Board::new(8, 6, 42);
232 type Mutation = (&'static str, Box<dyn Fn(&mut Board)>);
233 let mutations: Vec<Mutation> = vec![
234 ("set_tile", Box::new(|b| b.set_tile(2, 2, TileKind::Rock))),
235 (
236 "set_spawner",
237 Box::new(|b| {
238 b.set_tile(
239 3,
240 3,
241 TileKind::Spawner(Spawner {
242 dir: Direction::Right,
243 period: 40,
244 }),
245 );
246 }),
247 ),
248 (
249 "set_wall",
250 Box::new(|b| b.set_wall(1, 1, Direction::Up, true)),
251 ),
252 ("set_wrap", Box::new(|b| b.set_wrap(true))),
253 ("set_score", Box::new(|b| b.set_score(0, 7))),
254 ("set_gull_period", Box::new(|b| b.set_gull_period(99))),
255 (
256 "set_round_length",
257 Box::new(|b| b.set_round_length(Some(500))),
258 ),
259 (
260 "set_events_enabled",
261 Box::new(|b| b.set_events_enabled(true)),
262 ),
263 (
264 "set_signpost_rule",
265 Box::new(|b| b.set_signpost_rule(5, CapPolicy::Reject)),
266 ),
267 (
268 "place_signpost",
269 Box::new(|b| {
270 b.place_signpost(0, 4, 4, Direction::Left);
271 }),
272 ),
273 (
274 "spawn_crab",
275 Box::new(|b| {
276 b.spawn_crab(2, 3, Direction::Up, Handedness::Left, CrabKind::Common);
277 }),
278 ),
279 (
280 "spawn_gull",
281 Box::new(|b| b.spawn_gull(5, 2, Direction::Down)),
282 ),
283 ("tick_idle", Box::new(Board::tick_idle)),
284 ];
285 let clean = base().state_hash();
286 for (name, mutate) in mutations {
287 let mut board = base();
288 mutate(&mut board);
289 assert_ne!(board.state_hash(), clean, "{name} did not move state_hash");
290 }
291 }
292
293 #[test]
299 fn the_lure_cooldown_is_part_of_the_fingerprint() {
300 use crate::sim::{MAX_PLAYERS, PlayerAction};
301 let armed = |cooldown: u32| {
302 let mut board = Board::new(6, 5, 1);
303 board.set_tile(3, 2, TileKind::Castle(0));
304 board.lure_cooldown = cooldown;
305 board.spawn_crab(2, 2, Direction::Right, Handedness::Right, CrabKind::Molting);
306 board
307 };
308 let (mut quiet, mut ready) = (armed(200), armed(0));
309 assert_ne!(
310 quiet.state_hash(),
311 ready.state_hash(),
312 "a cooldown that changes the round has to change the hash"
313 );
314 for board in [&mut quiet, &mut ready] {
315 for _ in 0..40 {
316 board.tick(&[PlayerAction::None; MAX_PLAYERS]);
317 }
318 }
319 assert!(quiet.lure().is_none(), "the quiet spell swallows the lure");
320 assert!(ready.lure().is_some(), "a clear board starts one");
321 }
322}