Live location tracking and playback for the game "manhunt"
at main 738 lines 22 kB view raw
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}