Live location tracking and playback for the game "manhunt"
1use anyhow::bail;
2use chrono::{DateTime, Utc};
3use std::{sync::Arc, time::Duration};
4use tokio_util::sync::CancellationToken;
5use uuid::Uuid;
6
7use tokio::sync::{RwLock, RwLockWriteGuard};
8
9use crate::StartGameInfo;
10use crate::{prelude::*, transport::TransportMessage};
11
12use crate::{
13 game_events::GameEvent,
14 game_state::{GameHistory, GameState, GameUiState},
15 location::LocationService,
16 powerups::PowerUpType,
17 settings::GameSettings,
18 transport::Transport,
19};
20
21pub type Id = Uuid;
22
23/// Convenience alias for UTC DT
24pub type UtcDT = DateTime<Utc>;
25
26pub trait StateUpdateSender {
27 fn send_update(&self);
28}
29
30/// Struct representing an ongoing game, handles communication with
31/// other clients via [Transport], gets location with [LocationService], and provides high-level methods for
32/// taking actions in the game.
33pub struct Game<L: LocationService, T: Transport, S: StateUpdateSender> {
34 state: RwLock<GameState>,
35 transport: Arc<T>,
36 location: L,
37 state_update_sender: S,
38 interval: Duration,
39 cancel: CancellationToken,
40}
41
42impl<L: LocationService, T: Transport, S: StateUpdateSender> Game<L, T, S> {
43 pub fn new(
44 interval: Duration,
45 start_info: StartGameInfo,
46 transport: Arc<T>,
47 location: L,
48 state_update_sender: S,
49 ) -> Self {
50 let state = GameState::new(
51 start_info.settings,
52 transport.self_id(),
53 start_info.initial_caught_state,
54 );
55
56 Self {
57 transport,
58 location,
59 interval,
60 state: RwLock::new(state),
61 state_update_sender,
62 cancel: CancellationToken::new(),
63 }
64 }
65
66 async fn send_event(&self, event: GameEvent) {
67 self.transport.send_message(event.into()).await;
68 }
69
70 pub async fn mark_caught(&self) {
71 let mut state = self.state.write().await;
72 let id = state.id;
73 state.mark_caught(id);
74 state.remove_ping(id);
75 // TODO: Maybe reroll for new powerups (specifically seeker ones) instead of just erasing it
76 state.use_powerup();
77 drop(state);
78 self.send_event(GameEvent::PlayerCaught(id)).await;
79 }
80
81 pub async fn clone_settings(&self) -> GameSettings {
82 self.state.read().await.clone_settings()
83 }
84
85 pub async fn get_ui_state(&self) -> GameUiState {
86 self.state.read().await.as_ui_state()
87 }
88
89 pub async fn get_powerup(&self) {
90 let mut state = self.state.write().await;
91 state.get_powerup();
92 self.send_event(GameEvent::PowerupDespawn(state.id)).await;
93 }
94
95 pub async fn use_powerup(&self) {
96 let mut state = self.state.write().await;
97
98 if let Some(powerup) = state.use_powerup() {
99 match powerup {
100 PowerUpType::PingSeeker => {}
101 PowerUpType::PingAllSeekers => {
102 for seeker in state.iter_seekers() {
103 self.send_event(GameEvent::ForcePing(seeker, None)).await;
104 }
105 }
106 PowerUpType::ForcePingOther => {
107 // Fallback to a seeker if there are no other hiders
108 let target = state.random_other_hider().or_else(|| state.random_seeker());
109
110 if let Some(target) = target {
111 self.send_event(GameEvent::ForcePing(target, None)).await;
112 }
113 }
114 }
115 }
116 }
117
118 async fn consume_event(&self, state: &mut GameState, event: GameEvent) {
119 if !state.game_ended() {
120 state.event_history.push((Utc::now(), event.clone()));
121 }
122
123 match event {
124 GameEvent::Ping(player_ping) => state.add_ping(player_ping),
125 GameEvent::ForcePing(target, display) => {
126 if target != state.id {
127 return;
128 }
129
130 let ping = if let Some(display) = display {
131 state.create_ping(display)
132 } else {
133 state.create_self_ping()
134 };
135
136 if let Some(ping) = ping {
137 state.add_ping(ping.clone());
138 self.send_event(GameEvent::Ping(ping)).await;
139 }
140 }
141 GameEvent::PowerupDespawn(_) => state.despawn_powerup(),
142 GameEvent::PlayerCaught(player) => {
143 state.mark_caught(player);
144 state.remove_ping(player);
145 }
146 GameEvent::PostGameSync(id, history) => {
147 state.insert_player_location_history(id, history);
148 }
149 }
150
151 self.state_update_sender.send_update();
152 }
153
154 async fn consume_message(
155 &self,
156 state: &mut GameState,
157 _id: Option<Uuid>,
158 msg: TransportMessage,
159 ) -> Result<bool> {
160 match msg {
161 TransportMessage::Game(event) => {
162 self.consume_event(state, *event).await;
163 Ok(false)
164 }
165 TransportMessage::PeerDisconnect(id) => {
166 state.remove_player(id);
167 Ok(false)
168 }
169 TransportMessage::Disconnected => {
170 // Expected disconnect, exit
171 Ok(true)
172 }
173 TransportMessage::Error(err) => bail!("Transport error: {err}"),
174 _ => Ok(false),
175 }
176 }
177
178 /// Perform a tick for a specific moment in time
179 /// Returns whether the game loop should be broken.
180 async fn tick(&self, state: &mut GameState, now: UtcDT) -> bool {
181 let mut send_update = false;
182
183 if state.check_end_game() {
184 // If we're at the point where the game is over, send out our location history
185 let msg = GameEvent::PostGameSync(state.id, state.location_history.clone());
186 self.send_event(msg).await;
187 send_update = true;
188 }
189
190 if state.game_ended() {
191 // Don't do normal ticks if the game is over,
192 // simply return if we're done doing a post-game sync
193 if send_update {
194 self.state_update_sender.send_update();
195 }
196 return state.check_post_game_sync();
197 }
198
199 // Push to location history
200 if let Some(location) = self.location.get_loc() {
201 state.push_loc(location);
202 }
203
204 // Release Seekers?
205 if !state.seekers_released() && state.should_release_seekers(now) {
206 state.release_seekers(now);
207 send_update = true;
208 }
209
210 // Start Pings?
211 if !state.pings_started() && state.should_start_pings(now) {
212 state.start_pings(now);
213 send_update = true;
214 }
215
216 // Do a Ping?
217 if state.should_ping(&now) {
218 if let Some(&PowerUpType::PingSeeker) = state.peek_powerup() {
219 // We have a powerup that lets us ping a seeker as us, use it.
220 if let Some(seeker) = state.random_seeker() {
221 state.use_powerup();
222 self.send_event(GameEvent::ForcePing(seeker, Some(state.id)))
223 .await;
224 state.start_pings(now);
225 }
226 } else {
227 // No powerup, normal ping
228 if let Some(ping) = state.create_self_ping() {
229 self.send_event(GameEvent::Ping(ping.clone())).await;
230 state.add_ping(ping);
231 state.start_pings(now);
232 }
233 }
234 }
235
236 // Start Powerup Rolls?
237 if !state.powerups_started() && state.should_start_powerups(now) {
238 state.start_powerups(now);
239 send_update = true;
240 }
241
242 // Should roll for a powerup?
243 if state.should_spawn_powerup(&now) {
244 state.try_spawn_powerup(now);
245 send_update = true;
246 }
247
248 // Send a state update to the UI?
249 if send_update {
250 self.state_update_sender.send_update();
251 }
252
253 false
254 }
255
256 pub async fn quit_game(&self) {
257 self.cancel.cancel();
258 }
259
260 #[cfg(test)]
261 fn get_now() -> UtcDT {
262 let fake = tokio::time::Instant::now();
263 let real = std::time::Instant::now();
264 Utc::now() + (fake.into_std().duration_since(real) + Duration::from_secs(1))
265 }
266
267 #[cfg(not(test))]
268 fn get_now() -> UtcDT {
269 Utc::now()
270 }
271
272 /// Main loop of the game, handles ticking and receiving messages from [Transport].
273 pub async fn main_loop(&self) -> Result<Option<GameHistory>> {
274 let mut interval = tokio::time::interval(self.interval);
275
276 // interval.set_missed_tick_behavior(MissedTickBehavior::Delay);
277
278 let res = 'game: loop {
279 tokio::select! {
280 biased;
281
282 _ = self.cancel.cancelled() => {
283 break 'game Ok(None);
284 }
285
286 messages = self.transport.receive_messages() => {
287 let mut state = self.state.write().await;
288 for (id, msg) in messages {
289 match self.consume_message(&mut state, id, msg).await {
290 Ok(should_break) => {
291 if should_break {
292 break 'game Ok(None);
293 }
294 }
295 Err(why) => { break 'game Err(why); }
296 }
297 }
298 }
299
300 _ = interval.tick() => {
301 let mut state = self.state.write().await;
302 let should_break = self.tick(&mut state, Self::get_now()).await;
303
304 if should_break {
305 let history = state.as_game_history();
306 break Ok(Some(history));
307 }
308 }
309 }
310 };
311
312 self.transport.disconnect().await;
313
314 res
315 }
316
317 pub async fn lock_state(&self) -> RwLockWriteGuard<'_, GameState> {
318 self.state.write().await
319 }
320}
321
322#[cfg(test)]
323mod tests {
324 use std::{collections::HashMap, sync::Arc};
325
326 use crate::{
327 location::Location,
328 settings::PingStartCondition,
329 tests::{DummySender, MockLocation, MockTransport},
330 };
331
332 use super::*;
333 use tokio::{sync::oneshot, task::yield_now, test};
334
335 type TestGame = Game<MockLocation, MockTransport, DummySender>;
336
337 type EndRecv = oneshot::Receiver<Result<Option<GameHistory>>>;
338
339 struct MockMatch {
340 uuids: Vec<Uuid>,
341 games: Vec<Arc<TestGame>>,
342 settings: GameSettings,
343 }
344
345 const INTERVAL: Duration = Duration::from_secs(600000);
346
347 impl MockMatch {
348 pub fn new(settings: GameSettings, players: u32, seekers: u32) -> Self {
349 tokio::time::pause();
350 let (uuids, transports) = MockTransport::create_mesh(players);
351
352 let initial_caught_state = (0..players)
353 .map(|id| (uuids[id as usize], id < seekers))
354 .collect::<HashMap<_, _>>();
355
356 let games = transports
357 .into_iter()
358 .map(|transport| {
359 let location = MockLocation;
360 let start_info = StartGameInfo {
361 initial_caught_state: initial_caught_state.clone(),
362 settings: settings.clone(),
363 };
364 let game = TestGame::new(
365 INTERVAL,
366 start_info,
367 Arc::new(transport),
368 location,
369 DummySender,
370 );
371
372 Arc::new(game)
373 })
374 .collect();
375
376 Self {
377 settings,
378 games,
379 uuids,
380 }
381 }
382
383 pub async fn start(&self) -> Vec<EndRecv> {
384 let mut recvs = Vec::with_capacity(self.games.len());
385 for game in self.games.iter() {
386 let game = game.clone();
387 let (send, recv) = oneshot::channel();
388 recvs.push(recv);
389 tokio::spawn(async move {
390 let res = game.main_loop().await;
391 send.send(res).expect("Failed to send");
392 });
393 yield_now().await;
394 }
395 recvs
396 }
397
398 pub async fn assert_all_states(&self, f: impl Fn(usize, &GameState)) {
399 for (i, game) in self.games.iter().enumerate() {
400 let state = game.state.read().await;
401 f(i, &state);
402 }
403 }
404
405 pub fn assert_all_transports_disconnected(&self) {
406 for game in self.games.iter() {
407 assert!(
408 game.transport.is_disconnected(),
409 "Game {} is still connected",
410 game.transport.self_id()
411 );
412 }
413 }
414
415 pub async fn wait_for_seekers(&mut self) {
416 let hiding_time = Duration::from_secs(self.settings.hiding_time_seconds as u64 + 1);
417
418 tokio::time::sleep(hiding_time).await;
419
420 self.tick().await;
421
422 self.assert_all_states(|i, s| {
423 assert!(s.seekers_released(), "Seekers not released on game {i}");
424 })
425 .await;
426 }
427
428 pub async fn wait_for_transports(&self) {
429 for game in self.games.iter() {
430 game.transport.wait_for_queue_empty().await;
431 }
432 }
433
434 pub async fn tick(&self) {
435 tokio::time::sleep(INTERVAL + Duration::from_secs(1)).await;
436 self.wait_for_transports().await;
437 yield_now().await;
438 }
439 }
440
441 fn mk_settings() -> GameSettings {
442 GameSettings {
443 random_seed: 0,
444 hiding_time_seconds: 1,
445 ping_start: PingStartCondition::Instant,
446 ping_minutes_interval: 1,
447 powerup_start: PingStartCondition::Instant,
448 powerup_chance: 0,
449 powerup_minutes_cooldown: 1,
450 powerup_locations: vec![Location {
451 lat: 0.0,
452 long: 0.0,
453 heading: None,
454 }],
455 }
456 }
457
458 #[test]
459 async fn test_minimal_game() {
460 let settings = mk_settings();
461
462 // 2 players, one is a seeker
463 let mut mat = MockMatch::new(settings, 2, 1);
464
465 let recvs = mat.start().await;
466
467 mat.wait_for_seekers().await;
468
469 mat.games[1].mark_caught().await;
470
471 mat.wait_for_transports().await;
472
473 mat.assert_all_states(|i, s| {
474 assert_eq!(
475 s.get_caught(mat.uuids[1]),
476 Some(true),
477 "Game {i} sees player 1 as not caught",
478 );
479 })
480 .await;
481
482 // Tick to process game end
483 mat.tick().await;
484
485 mat.assert_all_states(|i, s| {
486 assert!(s.game_ended(), "Game {i} has not ended");
487 })
488 .await;
489
490 // Tick for post-game sync
491 mat.tick().await;
492
493 mat.assert_all_transports_disconnected();
494
495 for (i, recv) in recvs.into_iter().enumerate() {
496 let res = recv.await.expect("Failed to recv");
497 match res {
498 Ok(Some(hist)) => {
499 assert!(!hist.locations.is_empty(), "Game {i} has no locations");
500 assert!(!hist.events.is_empty(), "Game {i} has no event");
501 }
502 Ok(None) => {
503 panic!("Game {i} exited without a history (did not end via post game sync)");
504 }
505 Err(why) => {
506 panic!("Game {i} encountered error: {why:?}");
507 }
508 }
509 }
510 }
511
512 #[test]
513 async fn test_basic_pinging() {
514 let mut settings = mk_settings();
515 settings.ping_minutes_interval = 0;
516
517 let mut mat = MockMatch::new(settings, 4, 1);
518
519 mat.start().await;
520
521 mat.wait_for_seekers().await;
522
523 mat.assert_all_states(|i, s| {
524 for id in 0..4 {
525 let ping = s.get_ping(mat.uuids[id]);
526 if id == 0 {
527 assert!(
528 ping.is_none(),
529 "Game {i} has a ping for 0, despite them being a seeker",
530 );
531 } else {
532 assert!(
533 ping.is_some(),
534 "Game {i} doesn't have a ping for {id}, despite them being a hider",
535 );
536 }
537 }
538 })
539 .await;
540
541 mat.games[1].mark_caught().await;
542
543 mat.tick().await;
544
545 mat.assert_all_states(|i, s| {
546 for id in 0..4 {
547 let ping = s.get_ping(mat.uuids[id]);
548 if id <= 1 {
549 assert!(
550 ping.is_none(),
551 "Game {i} has a ping for {id}, despite them being a seeker",
552 );
553 } else {
554 assert!(
555 ping.is_some(),
556 "Game {i} doesn't have a ping for {id}, despite them being a hider",
557 );
558 }
559 }
560 })
561 .await;
562 }
563
564 #[test]
565 async fn test_rng_sync() {
566 let mut settings = mk_settings();
567 settings.powerup_chance = 100;
568 settings.powerup_minutes_cooldown = 1;
569 settings.powerup_start = PingStartCondition::Instant;
570 settings.powerup_locations = (1..1000)
571 .map(|x| Location {
572 lat: x as f64,
573 long: 1.0,
574 heading: None,
575 })
576 .collect();
577
578 let mut mat = MockMatch::new(settings, 10, 2);
579
580 mat.start().await;
581 mat.tick().await;
582 mat.wait_for_seekers().await;
583 tokio::time::sleep(Duration::from_secs(60)).await;
584 mat.tick().await;
585
586 let game = mat.games[0].clone();
587 let state = game.state.read().await;
588 let location = state.powerup_location().expect("Powerup didn't spawn");
589
590 drop(state);
591
592 mat.assert_all_states(|i, s| {
593 assert_eq!(
594 s.powerup_location(),
595 Some(location),
596 "Game {i} has a different location than 0",
597 );
598 })
599 .await;
600 }
601
602 #[test]
603 async fn test_powerup_ping_seeker_as_you() {
604 let mut settings = mk_settings();
605 settings.ping_minutes_interval = 1;
606 let mut mat = MockMatch::new(settings, 2, 1);
607
608 mat.start().await;
609 mat.wait_for_seekers().await;
610
611 mat.tick().await;
612
613 tokio::time::sleep(Duration::from_secs(60)).await;
614
615 let game = mat.games[1].clone();
616 let mut state = game.state.write().await;
617 state.force_set_powerup(PowerUpType::PingSeeker);
618 drop(state);
619
620 mat.tick().await;
621
622 mat.assert_all_states(|i, s| {
623 if let Some(ping) = s.get_ping(mat.uuids[1]) {
624 assert_eq!(
625 ping.real_player, mat.uuids[0],
626 "Game {i} has a ping for 1, but it wasn't from 0"
627 );
628 } else {
629 panic!("Game {i} has no ping for 1");
630 }
631 })
632 .await;
633 }
634
635 #[test]
636 async fn test_powerup_ping_random_hider() {
637 let mut settings = mk_settings();
638 settings.ping_minutes_interval = u32::MAX;
639
640 let mut mat = MockMatch::new(settings, 3, 1);
641
642 mat.start().await;
643 mat.wait_for_seekers().await;
644
645 let game = mat.games[1].clone();
646 let mut state = game.state.write().await;
647 state.force_set_powerup(PowerUpType::ForcePingOther);
648 drop(state);
649
650 game.use_powerup().await;
651 mat.tick().await;
652
653 mat.assert_all_states(|i, s| {
654 // Player 0 is a seeker, player 1 used the powerup, so 2 is the only one that should
655 // have pinged
656 assert!(
657 s.get_ping(mat.uuids[2]).is_some(),
658 "Ping 2 is not present in game {i}"
659 );
660 assert!(
661 s.get_ping(mat.uuids[0]).is_none(),
662 "Ping 0 is present in game {i}"
663 );
664 assert!(
665 s.get_ping(mat.uuids[1]).is_none(),
666 "Ping 1 is present in game {i}"
667 );
668 })
669 .await;
670 }
671
672 #[test]
673 async fn test_powerup_ping_seekers() {
674 let settings = mk_settings();
675
676 let mat = MockMatch::new(settings, 5, 3);
677
678 mat.start().await;
679
680 mat.tick().await;
681
682 let game = mat.games[3].clone();
683 let mut state = game.state.write().await;
684 state.force_set_powerup(PowerUpType::PingAllSeekers);
685 drop(state);
686
687 game.use_powerup().await;
688 // One tick to send out the ForcePing
689 mat.tick().await;
690 // One tick to for the seekers to reply
691 mat.tick().await;
692
693 mat.assert_all_states(|i, s| {
694 for id in 0..3 {
695 assert!(
696 &s.get_ping(mat.uuids[id]).is_some(),
697 "Game {i} does not have a ping for {id}, despite the powerup being active",
698 );
699 }
700 })
701 .await;
702 }
703
704 #[test]
705 async fn test_player_dropped() {
706 let settings = mk_settings();
707 let mat = MockMatch::new(settings, 4, 1);
708
709 let mut recvs = mat.start().await;
710
711 let game = mat.games[2].clone();
712 let id = game.state.read().await.id;
713
714 game.quit_game().await;
715 let res = recvs.swap_remove(2).await.expect("Failed to recv");
716 assert!(res.is_ok_and(|o| o.is_none()), "2 did not exit cleanly");
717 assert!(
718 game.transport.is_disconnected(),
719 "2's transport is not disconnected"
720 );
721
722 mat.tick().await;
723
724 mat.assert_all_states(|i, s| {
725 if s.id != id {
726 assert!(
727 s.get_ping(id).is_none(),
728 "Game {i} has not removed 2 from pings",
729 );
730 assert!(
731 s.get_caught(id).is_none(),
732 "Game {i} has not removed 2 from caught state",
733 );
734 }
735 })
736 .await;
737 }
738}