···1717tauri-plugin-opener = "2"
1818serde = { version = "1", features = ["derive"] }
1919serde_json = "1"
2020+chrono = { version = "0.4.41", features = ["serde", "now"] }
2121+tokio = { version = "1.45.1", features = ["sync", "macros", "time"] }
2222+rand = { version = "0.9.1", features = ["thread_rng"] }
2323+tauri-plugin-geolocation = "2.2.4"
+447
backend/src/game.rs
···11+use std::{collections::HashMap, time::Duration};
22+use tauri::Runtime;
33+use tokio::sync::RwLock;
44+55+use crate::{
66+ powerup::{PowerUpType, PowerUpUsage},
77+ state::{GameEvent, GameState, Location, PlayerId, DT},
88+};
99+1010+type EventMessage = (PlayerId, DT, GameEvent);
1111+1212+/// Struct representing an ongoing game, handles communication with
1313+/// other clients via [Transport] and provides high-level methods for
1414+/// taking actions in the game.
1515+struct Game<L: LocationService, T: Transport> {
1616+ id: PlayerId,
1717+ is_host: bool,
1818+ state: RwLock<GameState<L>>,
1919+ transport: T,
2020+ interval: Duration,
2121+}
2222+2323+pub trait Transport {
2424+ async fn receive_message(&self) -> Option<EventMessage>;
2525+ async fn send_event_to(&self, id: PlayerId, event: GameEvent);
2626+ async fn send_event_host(&self, event: GameEvent);
2727+ async fn send_event_multiple(&self, ids: Vec<PlayerId>, event: GameEvent);
2828+ async fn send_event_all(&self, event: GameEvent);
2929+}
3030+3131+pub trait LocationService {
3232+ fn get_loc(&self) -> Location;
3333+}
3434+3535+impl<L: LocationService, T: Transport> Game<L, T> {
3636+ pub fn new(id: PlayerId, game_state: GameState<L>, transport: T, interval: Duration) -> Self {
3737+ Self {
3838+ id,
3939+ is_host: game_state.host.is_some(),
4040+ state: RwLock::new(game_state),
4141+ transport,
4242+ interval,
4343+ }
4444+ }
4545+4646+ /// Mark yourself as caught, sends out a message to all other players
4747+ async fn mark_caught(&self) {
4848+ self.transport
4949+ .send_event_all(GameEvent::HiderCaught(self.id))
5050+ .await;
5151+ }
5252+5353+ /// Get the active powerup, to be called when the user is in range of the powerup
5454+ async fn get_powerup(&self) {
5555+ let mut state = self.state.write().await;
5656+ if let Some(powerup) = state.public.available_powerup.take() {
5757+ state.player.held_powerup = Some(powerup.typ);
5858+ drop(state);
5959+ self.transport
6060+ .send_event_all(GameEvent::PowerUpDespawn)
6161+ .await;
6262+ }
6363+ }
6464+6565+ async fn use_powerup(&self) {
6666+ let mut state = self.state.write().await;
6767+ if let Some(powerup) = state.player.held_powerup.take() {
6868+ drop(state);
6969+ match powerup {
7070+ PowerUpType::PingSeeker => {
7171+ let e = GameEvent::PowerUpActivate(PowerUpUsage::PingSeeker);
7272+ self.transport.send_event_host(e).await;
7373+ }
7474+ PowerUpType::PingAllSeekers => {
7575+ let e = GameEvent::PingReq(None);
7676+ let state = self.state.read().await;
7777+ let seekers = state.public.iter_seekers().collect::<Vec<_>>();
7878+ drop(state);
7979+ let host_log = GameEvent::PowerUpActivate(PowerUpUsage::PingAllSeekers);
8080+ self.transport.send_event_host(host_log).await;
8181+ self.transport.send_event_multiple(seekers, e).await;
8282+ }
8383+ PowerUpType::ForcePingOther => {
8484+ let e = GameEvent::PingReq(None);
8585+ let mut state = self.state.write().await;
8686+ if let Some(target) = state.random_other_hider() {
8787+ let host_log =
8888+ GameEvent::PowerUpActivate(PowerUpUsage::ForcePingOther(target));
8989+ self.transport.send_event_host(host_log).await;
9090+ self.transport.send_event_to(target, e).await;
9191+ }
9292+ }
9393+ }
9494+ }
9595+ }
9696+9797+ /// Start main loop of the game, this should ideally be put into its own thread via
9898+ /// [tokio::spawn].
9999+ async fn main_loop(
100100+ &self,
101101+ ) -> (
102102+ HashMap<PlayerId, Vec<Location>>,
103103+ Vec<(PlayerId, DT, GameEvent)>,
104104+ ) {
105105+ let interval = tokio::time::interval(self.interval);
106106+ tokio::pin!(interval);
107107+108108+ let mut ended = false;
109109+110110+ while !ended {
111111+ tokio::select! {
112112+ _ = interval.tick() => {
113113+ let mut state = self.state.write().await;
114114+ let messages = state.tick();
115115+ drop(state);
116116+ for (player, event) in messages {
117117+ if let Some(player) = player {
118118+ self.transport.send_event_to(player, event).await;
119119+ } else {
120120+ self.transport.send_event_all(event).await;
121121+ }
122122+ }
123123+ }
124124+125125+ Some((player, time_sent, event)) = self.transport.receive_message() => {
126126+ if let GameEvent::GameEnd(dt) = event {
127127+ ended = true;
128128+129129+ } else {
130130+ let mut state = self.state.write().await;
131131+ let new_event = state.consume_event(time_sent, event, player);
132132+ drop(state);
133133+ if let Some(event) = new_event {
134134+ self.transport.send_event_all(event).await;
135135+ }
136136+ }
137137+ }
138138+ }
139139+ }
140140+141141+ let state = self.state.read().await;
142142+ let locations = state.player.locations.clone();
143143+144144+ if self.is_host {
145145+ let player_count = state.public.caught_state.len();
146146+ let mut player_location_history =
147147+ HashMap::<PlayerId, Vec<Location>>::with_capacity(player_count);
148148+ player_location_history.insert(self.id, locations);
149149+ while player_location_history.len() != player_count {
150150+ // TODO: Join with a timeout, etc
151151+ if let Some((id, _, GameEvent::PostGameSync(player_locations))) =
152152+ self.transport.receive_message().await
153153+ {
154154+ player_location_history.insert(id, player_locations);
155155+ }
156156+ }
157157+ let history = (
158158+ player_location_history,
159159+ state.host.as_ref().unwrap().event_history.clone(),
160160+ );
161161+ let ev = GameEvent::HostHistorySync(history.clone());
162162+ self.transport.send_event_all(ev).await;
163163+ history
164164+ } else {
165165+ self.transport
166166+ .send_event_host(GameEvent::PostGameSync(locations))
167167+ .await;
168168+ loop {
169169+ if let Some((_, _, GameEvent::HostHistorySync(history))) =
170170+ self.transport.receive_message().await
171171+ {
172172+ break history;
173173+ }
174174+ }
175175+ }
176176+ }
177177+}
178178+179179+#[cfg(test)]
180180+mod tests {
181181+ use std::collections::HashMap;
182182+ use std::sync::Arc;
183183+184184+ use crate::state::{GameSettings, HostState, PingStartCondition};
185185+186186+ use super::*;
187187+ use tokio::sync::mpsc::{Receiver, Sender};
188188+ use tokio::sync::Mutex;
189189+ use tokio::task::yield_now;
190190+ use tokio::test;
191191+192192+ type EventRx = Receiver<EventMessage>;
193193+ type EventTx = Sender<EventMessage>;
194194+195195+ struct MockTransport {
196196+ player_id: PlayerId,
197197+ rx: Mutex<EventRx>,
198198+ txs: HashMap<PlayerId, EventTx>,
199199+ }
200200+201201+ impl MockTransport {
202202+ fn new(player_id: PlayerId) -> (Self, EventTx) {
203203+ let (tx, rx) = tokio::sync::mpsc::channel(5);
204204+ let trans = Self {
205205+ player_id,
206206+ rx: Mutex::new(rx),
207207+ txs: HashMap::new(),
208208+ };
209209+ (trans, tx)
210210+ }
211211+212212+ fn set_txs(&mut self, txs: HashMap<PlayerId, EventTx>) {
213213+ self.txs = txs;
214214+ }
215215+216216+ fn make_msg(&self, e: GameEvent) -> EventMessage {
217217+ (self.player_id, chrono::Utc::now(), e)
218218+ }
219219+ }
220220+221221+ impl Transport for MockTransport {
222222+ async fn receive_message(&self) -> Option<EventMessage> {
223223+ let mut rx = self.rx.lock().await;
224224+ rx.recv().await
225225+ }
226226+227227+ async fn send_event_to(&self, id: PlayerId, event: GameEvent) {
228228+ if let Some(tx) = self.txs.get(&id) {
229229+ if let Err(why) = tx.send(self.make_msg(event)).await {
230230+ eprintln!("Error sending msg to {id}: {why}");
231231+ }
232232+ }
233233+ }
234234+235235+ async fn send_event_host(&self, event: GameEvent) {
236236+ // While testing, host is always player 0
237237+ self.send_event_to(0, event).await;
238238+ }
239239+240240+ async fn send_event_multiple(&self, ids: Vec<PlayerId>, event: GameEvent) {
241241+ for id in ids {
242242+ self.send_event_to(id, event.clone()).await;
243243+ }
244244+ }
245245+246246+ async fn send_event_all(&self, event: GameEvent) {
247247+ for id in self.txs.keys() {
248248+ self.send_event_to(*id, event.clone()).await;
249249+ }
250250+ }
251251+ }
252252+253253+ struct MockLocation;
254254+255255+ impl LocationService for MockLocation {
256256+ fn get_loc(&self) -> Location {
257257+ Location {
258258+ lat: 0.0,
259259+ long: 0.0,
260260+ heading: None,
261261+ }
262262+ }
263263+ }
264264+265265+ type MockGame = Game<MockLocation, MockTransport>;
266266+267267+ struct TestMatch {
268268+ games: HashMap<PlayerId, Arc<MockGame>>,
269269+ }
270270+271271+ impl TestMatch {
272272+ /// New test match
273273+ /// player_count: number of players
274274+ /// num_seekers: number of seekers
275275+ /// host_seeker: whether to mark the host as a seeker
276276+ pub fn new(
277277+ player_count: u32,
278278+ num_seekers: u32,
279279+ host_seeker: bool,
280280+ settings: GameSettings,
281281+ ) -> Self {
282282+ let caught_state =
283283+ HashMap::<PlayerId, bool>::from_iter((0..player_count).into_iter().map(|id| {
284284+ let should_seeker =
285285+ (if id == 0 || host_seeker { id } else { id - 1 }) < num_seekers;
286286+ (id, should_seeker && (host_seeker || id != 0))
287287+ }));
288288+289289+ let mut txs = HashMap::<PlayerId, EventTx>::with_capacity(player_count as usize);
290290+ let mut games = HashMap::<PlayerId, MockGame>::with_capacity(player_count as usize);
291291+292292+ for id in 0..player_count {
293293+ let (transport, tx) = MockTransport::new(id);
294294+ let state = GameState::new(
295295+ id == 0,
296296+ id,
297297+ caught_state.clone(),
298298+ settings.clone(),
299299+ MockLocation,
300300+ );
301301+ txs.insert(id, tx);
302302+ let game = MockGame::new(id, state, transport, Duration::from_secs(1));
303303+ games.insert(id, game);
304304+ }
305305+306306+ for game in games.values_mut() {
307307+ game.transport.set_txs(txs.clone());
308308+ }
309309+310310+ Self {
311311+ games: games.into_iter().map(|(k, v)| (k, Arc::new(v))).collect(),
312312+ }
313313+ }
314314+315315+ pub fn start(&self) {
316316+ for game in self.games.values() {
317317+ let game = game.clone();
318318+ tokio::spawn(async move { game.main_loop().await });
319319+ }
320320+ }
321321+322322+ pub fn host(&self) -> Arc<MockGame> {
323323+ self.game(0)
324324+ }
325325+326326+ pub fn game(&self, id: PlayerId) -> Arc<MockGame> {
327327+ self.games.get(&id).unwrap().clone()
328328+ }
329329+330330+ pub async fn wait_tick(&self) {
331331+ tokio::time::sleep(Duration::from_secs(1)).await;
332332+ yield_now().await;
333333+ }
334334+335335+ pub async fn wait_assert_seekers_released(&self) {
336336+ tokio::time::sleep(Duration::from_secs(1)).await;
337337+ yield_now().await;
338338+339339+ self.assert_all_player_states(|state| {
340340+ assert!(state.public.seekers_started.is_some());
341341+ });
342342+ }
343343+344344+ /// Assert a condition on the host state
345345+ pub async fn assert_host_state<F: Fn(&HostState)>(&self, f: F) {
346346+ let host = self.host();
347347+ let state = host.state.read().await;
348348+ f(state.host.as_ref().unwrap());
349349+ }
350350+351351+ /// Assert a condition on all player states
352352+ pub async fn assert_all_player_states<F: Fn(&GameState<MockLocation>)>(&self, f: F) {
353353+ for game in self.games.values() {
354354+ let state = game.state.read().await;
355355+ f(&state);
356356+ }
357357+ }
358358+ }
359359+360360+ const TEST_LOC: Location = Location {
361361+ lat: 0.0,
362362+ long: 0.0,
363363+ heading: None,
364364+ };
365365+366366+ #[test]
367367+ async fn test_game() {
368368+ let settings = GameSettings {
369369+ hiding_time_seconds: 1,
370370+ ping_start: PingStartCondition::Players(3),
371371+ ping_minutes_interval: 0,
372372+ powerup_start: PingStartCondition::Players(3),
373373+ powerup_chance: 0,
374374+ powerup_minutes_cooldown: 1,
375375+ powerup_locations: vec![TEST_LOC.clone()],
376376+ };
377377+378378+ // A test match with 5 players, player 0 (host) is a hider, players 1 and 2 are seekers.
379379+ let test_match = TestMatch::new(5, 2, false, settings);
380380+381381+ let correct_caught_state = HashMap::<PlayerId, bool>::from_iter([
382382+ (0, false),
383383+ (1, true),
384384+ (2, true),
385385+ (3, false),
386386+ (4, false),
387387+ ]);
388388+389389+ // Let's make sure our initial `caught_state` is correct
390390+ test_match
391391+ .assert_all_player_states(|s| assert_eq!(s.public.caught_state, correct_caught_state))
392392+ .await;
393393+394394+ test_match.start();
395395+396396+ // Wait for seekers to be released, and then assert all player states properly reflect this
397397+ test_match.wait_assert_seekers_released().await;
398398+399399+ test_match.wait_tick().await;
400400+401401+ // After a tick, all players should have at least one location in [PlayerState::locations]
402402+ test_match
403403+ .assert_all_player_states(|s| assert!(!s.player.locations.is_empty()))
404404+ .await;
405405+406406+ // Now, let's see if we can mark player 3 as caught
407407+ let player_3 = test_match.game(3);
408408+ player_3.mark_caught().await;
409409+ yield_now().await;
410410+411411+ // All states should be updated to reflect this
412412+ test_match
413413+ .assert_all_player_states(|s| {
414414+ assert_eq!(s.public.caught_state.get(&3).copied(), Some(true))
415415+ })
416416+ .await;
417417+418418+ test_match.wait_tick().await;
419419+420420+ // And now, 3 players have been caught, meaning our [PingStartCondition] has been met,
421421+ // let's check the host state to make sure it's starting to perform pings
422422+ test_match
423423+ .assert_host_state(|h| assert!(h.last_ping.is_some()))
424424+ .await;
425425+426426+ test_match.wait_tick().await;
427427+428428+ // Value represents if the [Option] should be [Option::Some]
429429+ let correct_pings = HashMap::<u32, bool>::from_iter([
430430+ (0, true),
431431+ (1, false),
432432+ (2, false),
433433+ (3, false),
434434+ (4, true),
435435+ ]);
436436+437437+ // Now let's make sure the hiders are being pinged (3 was just caught, triggering pings.
438438+ // Therefore, 3 should not be pinged)
439439+ test_match
440440+ .assert_all_player_states(|s| {
441441+ for (k, v) in s.public.pings.iter() {
442442+ assert_eq!(v.is_some(), correct_pings[k]);
443443+ }
444444+ })
445445+ .await;
446446+ }
447447+}
+5-6
backend/src/lib.rs
···11-// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
22-#[tauri::command]
33-fn greet(name: &str) -> String {
44- format!("Hello, {}! You've been greeted from Rust!", name)
55-}
11+#[allow(unused)]
22+mod game;
33+mod powerup;
44+mod state;
6576#[cfg_attr(mobile, tauri::mobile_entry_point)]
87pub fn run() {
98 tauri::Builder::default()
109 .plugin(tauri_plugin_opener::init())
1111- .invoke_handler(tauri::generate_handler![greet])
1010+ .invoke_handler(tauri::generate_handler![])
1211 .run(tauri::generate_context!())
1312 .expect("error while running tauri application");
1413}
+65
backend/src/powerup.rs
···11+use crate::state::{Location, PlayerId};
22+33+#[derive(Clone, Copy)]
44+/// Type of powerup
55+pub enum PowerUpType {
66+ /// Ping a random seeker instead of a hider
77+ PingSeeker,
88+99+ /// Pings all seekers locations on the map for hiders
1010+ PingAllSeekers,
1111+1212+ /// Ping another random hider instantly
1313+ ForcePingOther,
1414+}
1515+1616+impl PowerUpType {
1717+ pub const ALL_TYPES: [Self; 3] = [
1818+ PowerUpType::ForcePingOther,
1919+ PowerUpType::PingAllSeekers,
2020+ PowerUpType::PingSeeker,
2121+ ];
2222+}
2323+2424+#[derive(Clone)]
2525+/// Usage of a powerup as reported to the host
2626+pub enum PowerUpUsage {
2727+ /// The hider will have their location replaced with a random seeker's
2828+ PingSeeker,
2929+ /// No additional args
3030+ PingAllSeekers,
3131+ /// Instantly ping another random hider, contains the unlucky person that is being pinged
3232+ ForcePingOther(PlayerId),
3333+}
3434+3535+#[derive(Clone, PartialEq, Eq)]
3636+/// When a plugin is used
3737+pub enum PowerUpTiming {
3838+ /// Used the second it's activated
3939+ Instant,
4040+ /// Used during the next global ping
4141+ NextPing,
4242+}
4343+4444+impl PowerUpUsage {
4545+ pub fn timing(&self) -> PowerUpTiming {
4646+ match self {
4747+ PowerUpUsage::PingSeeker => PowerUpTiming::NextPing,
4848+ PowerUpUsage::ForcePingOther(_) => PowerUpTiming::Instant,
4949+ PowerUpUsage::PingAllSeekers => PowerUpTiming::Instant,
5050+ }
5151+ }
5252+}
5353+5454+#[derive(Clone)]
5555+/// An on-map powerup that can be picked up by hiders
5656+pub struct PowerUp {
5757+ loc: Location,
5858+ pub typ: PowerUpType,
5959+}
6060+6161+impl PowerUp {
6262+ pub fn new(loc: Location, typ: PowerUpType) -> Self {
6363+ Self { loc, typ }
6464+ }
6565+}
+475
backend/src/state.rs
···11+use std::collections::HashMap;
22+33+use chrono::{DateTime, Utc};
44+55+use crate::{
66+ game::LocationService,
77+ powerup::{PowerUp, PowerUpTiming, PowerUpType, PowerUpUsage},
88+};
99+1010+/// UTC DateTime;
1111+pub type DT = DateTime<Utc>;
1212+1313+/// Type used to uniquely identify players in the game
1414+pub type PlayerId = u32;
1515+1616+/// Type used for latitude and longitude
1717+pub type LocationComponent = f64;
1818+1919+#[derive(Debug, Clone)]
2020+/// The starting condition for global pings to begin
2121+pub enum PingStartCondition {
2222+ /// Wait For X players to be caught before beginning global pings
2323+ Players(u32),
2424+ /// Wait for X minutes after game start to begin global pings
2525+ Minutes(u32),
2626+ /// Don't wait at all, ping location after seekers are released
2727+ Instant,
2828+}
2929+3030+#[derive(Debug, Clone)]
3131+/// Settings for the game, host is the only person able to change these
3232+pub struct GameSettings {
3333+ /// The number of seconds to wait before seekers are allowed to go
3434+ pub hiding_time_seconds: u64,
3535+ /// Condition to wait for global pings to begin
3636+ pub ping_start: PingStartCondition,
3737+ /// Time between pings after the condition is met (first ping is either after the interval or
3838+ /// instantly after the condition is met depending on the condition)
3939+ pub ping_minutes_interval: u64,
4040+ /// Condition for powerups to start spawning
4141+ pub powerup_start: PingStartCondition,
4242+ /// Chance (after cooldown) each minute of a powerup spawning, out of 100
4343+ pub powerup_chance: u32,
4444+ /// Hard cooldown between powerups spawning
4545+ pub powerup_minutes_cooldown: u32,
4646+ /// Locations that powerups may spawn at
4747+ pub powerup_locations: Vec<Location>,
4848+}
4949+5050+#[derive(Debug, Clone, Copy)]
5151+/// Some location in the world as gotten from the Geolocation API
5252+pub struct Location {
5353+ /// Latitude
5454+ pub lat: LocationComponent,
5555+ /// Longitude
5656+ pub long: LocationComponent,
5757+ /// The bearing (float normalized from 0 to 1) optional as GPS can't always determine
5858+ pub heading: Option<LocationComponent>,
5959+}
6060+6161+/// State for each player during the game, the host also has this
6262+pub struct PlayerState {
6363+ /// The id of this player in this game
6464+ pub id: PlayerId,
6565+ /// All previous locations of this player, used in replay screen and when a ping happens
6666+ pub locations: Vec<Location>,
6767+ /// Whether the local player is a seeker
6868+ pub seeker: bool,
6969+ /// The powerup the player is currently holding
7070+ pub held_powerup: Option<PowerUpType>,
7171+}
7272+7373+/// Host state that determines when "privileged" events happen
7474+pub struct HostState {
7575+ /// The last time a location global ping occurred. If this is [Option::None] it means we're not
7676+ /// pinging yet
7777+ pub last_ping: Option<DT>,
7878+7979+ /// The last time a power-up has spawned.
8080+ pub last_powerup: Option<DT>,
8181+8282+ /// Last time a roll was done for a powerup to spawn, if this is [Option::None] it means we're
8383+ /// not spawning powerups yet.
8484+ pub last_powerup_proc: Option<DT>,
8585+8686+ /// Set of users that will not be pinged / ping someone else next ping
8787+ pub ping_power_usages: HashMap<PlayerId, PowerUpUsage>,
8888+8989+ /// A list of all events that happened in this game, and their times
9090+ pub event_history: Vec<(PlayerId, DT, GameEvent)>,
9191+}
9292+9393+impl HostState {
9494+ pub fn new() -> Self {
9595+ Self {
9696+ last_ping: None,
9797+ last_powerup: None,
9898+ last_powerup_proc: None,
9999+ ping_power_usages: HashMap::with_capacity(4),
100100+ event_history: Vec::with_capacity(50),
101101+ }
102102+ }
103103+}
104104+105105+#[derive(Clone)]
106106+/// An on-map ping of a player
107107+pub struct PlayerPing {
108108+ /// Location of the ping
109109+ loc: Location,
110110+ /// Time the ping happened
111111+ time: DT,
112112+ /// The player to display who initialized this ping
113113+ player: PlayerId,
114114+ /// The actual player that initialized this ping
115115+ real_player: PlayerId,
116116+}
117117+118118+impl PlayerPing {
119119+ pub fn new(loc: Location, player: PlayerId, real_player: PlayerId) -> Self {
120120+ Self {
121121+ loc,
122122+ player,
123123+ real_player,
124124+ time: Utc::now(),
125125+ }
126126+ }
127127+}
128128+129129+/// State meant to be updated and synced
130130+pub struct PublicState {
131131+ /// When the game started
132132+ pub game_started: DT,
133133+134134+ /// When seekers were allowed to begin
135135+ pub seekers_started: Option<DT>,
136136+137137+ /// Hashmap tracking if a player is a seeker (true) or a hider (false)
138138+ pub caught_state: HashMap<PlayerId, bool>,
139139+140140+ /// A map of the latest global ping results for each player
141141+ pub pings: HashMap<PlayerId, Option<PlayerPing>>,
142142+143143+ /// Powerup on the map that players can grab. Only one at a time
144144+ pub available_powerup: Option<PowerUp>,
145145+}
146146+147147+impl PublicState {
148148+ pub fn new(players: HashMap<PlayerId, bool>) -> Self {
149149+ Self {
150150+ game_started: Utc::now(),
151151+ seekers_started: None,
152152+ pings: HashMap::from_iter(players.keys().map(|id| (*id, None))),
153153+ caught_state: players,
154154+ available_powerup: None,
155155+ }
156156+ }
157157+158158+ pub fn iter_seekers(&self) -> impl Iterator<Item = PlayerId> + use<'_> {
159159+ self.caught_state
160160+ .iter()
161161+ .filter_map(|(k, v)| if *v { Some(*k) } else { None })
162162+ }
163163+164164+ pub fn iter_hiders(&self) -> impl Iterator<Item = PlayerId> + use<'_> {
165165+ self.caught_state
166166+ .iter()
167167+ .filter_map(|(k, v)| if !*v { Some(*k) } else { None })
168168+ }
169169+}
170170+171171+impl PlayerState {
172172+ pub fn new(id: PlayerId, seeker: bool) -> Self {
173173+ Self {
174174+ id,
175175+ locations: Vec::with_capacity(20),
176176+ seeker,
177177+ held_powerup: None,
178178+ }
179179+ }
180180+181181+ /// Create a [PlayerPing] with the latest location saved for the player
182182+ pub fn create_self_ping(&self) -> Option<PlayerPing> {
183183+ self.create_ping(self.id)
184184+ }
185185+186186+ /// Create a [PlayerPing] with the latest location as another player, used when powerups are
187187+ /// active
188188+ pub fn create_ping(&self, id: PlayerId) -> Option<PlayerPing> {
189189+ self.get_loc()
190190+ .map(|loc| PlayerPing::new(loc.clone(), id, self.id))
191191+ }
192192+193193+ /// Push a new player location
194194+ pub fn push_loc(&mut self, loc: Location) {
195195+ self.locations.push(loc);
196196+ }
197197+198198+ /// Get the latest player location
199199+ pub fn get_loc(&self) -> Option<&Location> {
200200+ self.locations.last()
201201+ }
202202+}
203203+204204+/// Central struct for managing the entire game's state
205205+pub struct GameState<L: LocationService> {
206206+ /// Player state, different for each player
207207+ pub player: PlayerState,
208208+ /// Public state, kept in sync via events
209209+ pub public: PublicState,
210210+ /// Host state, only for host, this being [Option::None] implies not being host
211211+ pub host: Option<HostState>,
212212+ /// The settings for the current game, read only
213213+ pub settings: GameSettings,
214214+ loc: L,
215215+}
216216+217217+#[derive(Clone)]
218218+/// Enum representing all events that can be published, some are host only although
219219+/// implicit trust is given to all players because uh who cares.
220220+pub enum GameEvent {
221221+ /// Seekers are now active and can see the map
222222+ SeekersReleased(DT),
223223+ /// (Host) A request for a given player to ping, optionally includes another player to ping
224224+ /// *as* (e.g. when [PowerUpType::PingSeeker] is used)
225225+ PingReq(Option<PlayerId>),
226226+ /// A [PlayerPing] was published to [PublicState]
227227+ Ping(PlayerPing),
228228+ /// The given hider has been caught
229229+ HiderCaught(PlayerId),
230230+ /// (Host) The powerup has spawned and is available to grab
231231+ PowerUpSpawn(PowerUp),
232232+ /// The powerup has despawned (Was grabbed or timed out)
233233+ PowerUpDespawn,
234234+ /// A player has activated a powerup, some powerups will be published globally and some will be
235235+ /// handled only by the host (such as [PowerUpType::PingSeeker])
236236+ PowerUpActivate(PowerUpUsage),
237237+ /// (Host) The game has ended (all players were caught or the host cancelled the game)
238238+ GameEnd(DT),
239239+ /// (Players) After the game has ended, players send this as the final game message
240240+ /// to the host with their entire location history
241241+ PostGameSync(Vec<Location>),
242242+ /// (Host) After the game has ended and all players have sent their location histories to the
243243+ /// host, the host will send this back to all players. Contains the entire history of the game
244244+ /// to be saved and replayed.
245245+ HostHistorySync(
246246+ (
247247+ HashMap<PlayerId, Vec<Location>>,
248248+ Vec<(PlayerId, DT, GameEvent)>,
249249+ ),
250250+ ),
251251+}
252252+253253+impl<L: LocationService> GameState<L> {
254254+ /// Create a new game state (starting a game). Needs the ID of the current player and a HashMap
255255+ /// of other player ids to their caught state (whether they start out as seeker).
256256+ pub fn new(
257257+ host: bool,
258258+ id: PlayerId,
259259+ players: HashMap<PlayerId, bool>,
260260+ settings: GameSettings,
261261+ loc: L,
262262+ ) -> Self {
263263+ let is_seeker = players.get(&id).copied().unwrap_or_default();
264264+ Self {
265265+ player: PlayerState::new(id, is_seeker),
266266+ public: PublicState::new(players),
267267+ host: if host { Some(HostState::new()) } else { None },
268268+ settings,
269269+ loc,
270270+ }
271271+ }
272272+273273+ pub fn random_other_hider(&mut self) -> Option<PlayerId> {
274274+ let hiders = self
275275+ .public
276276+ .iter_hiders()
277277+ .filter(|i| *i != self.player.id)
278278+ .collect::<Vec<_>>();
279279+ let choice = rand::random_range(0..hiders.len());
280280+ hiders.get(choice).copied()
281281+ }
282282+283283+ fn host_tick(&mut self, events: &mut Vec<(Option<PlayerId>, GameEvent)>) {
284284+ if let Some(host) = self.host.as_mut() {
285285+ let now = Utc::now();
286286+287287+ // Do seekers need to be released?
288288+ if self.public.seekers_started.is_none()
289289+ && (now - self.public.game_started)
290290+ .num_seconds()
291291+ .unsigned_abs()
292292+ >= self.settings.hiding_time_seconds
293293+ {
294294+ events.push((None, GameEvent::SeekersReleased(now)));
295295+ }
296296+297297+ // Do we need to start doing global pings?
298298+ if host.last_ping.is_none() {
299299+ let should_start = match self.settings.ping_start {
300300+ PingStartCondition::Players(players) => {
301301+ self.public.caught_state.values().filter(|v| **v).count()
302302+ >= (players as usize)
303303+ }
304304+ PingStartCondition::Minutes(min) => {
305305+ let delta = now - self.public.game_started;
306306+ delta.num_minutes() >= (min as i64)
307307+ }
308308+ PingStartCondition::Instant => true,
309309+ };
310310+ if should_start {
311311+ host.last_ping = Some(now);
312312+ }
313313+ }
314314+315315+ // Do we need to do a global ping?
316316+ if let Some(last_ping) = host.last_ping.as_mut() {
317317+ if (now - *last_ping).num_minutes().unsigned_abs()
318318+ >= self.settings.ping_minutes_interval
319319+ {
320320+ events.extend(self.public.caught_state.iter().filter_map(
321321+ |(player, caught)| {
322322+ // If caught, don't send a ping request
323323+ if *caught {
324324+ None
325325+ } else {
326326+ // If the player is pinging as someone else, do that here.
327327+ if let Some(PowerUpUsage::PingSeeker) =
328328+ host.ping_power_usages.get(player)
329329+ {
330330+ host.ping_power_usages.remove(player);
331331+ let seekers = self.public.iter_seekers().collect::<Vec<_>>();
332332+ let choice = rand::random_range(0..seekers.len());
333333+ let seeker = seekers[choice];
334334+ return Some((Some(seeker), GameEvent::PingReq(Some(*player))));
335335+ }
336336+ Some((Some(*player), GameEvent::PingReq(None)))
337337+ }
338338+ },
339339+ ));
340340+341341+ *last_ping = now;
342342+ }
343343+ }
344344+345345+ // Do we need to start rolling for powerups?
346346+ if host.last_powerup_proc.is_none() {
347347+ let should_start = match self.settings.ping_start {
348348+ PingStartCondition::Players(players) => {
349349+ self.public.caught_state.values().filter(|v| **v).count()
350350+ >= (players as usize)
351351+ }
352352+ PingStartCondition::Minutes(min) => {
353353+ let delta = now - self.public.game_started;
354354+ delta.num_minutes() >= (min as i64)
355355+ }
356356+ PingStartCondition::Instant => true,
357357+ };
358358+ if should_start {
359359+ host.last_powerup_proc = Some(now);
360360+ }
361361+ }
362362+363363+ // Should we roll for a powerup?
364364+ if let Some(last_powerup_proc) = host.last_powerup_proc.as_mut() {
365365+ if (now - *last_powerup_proc).num_minutes() >= 1 {
366366+ // A minute has passed, roll to see if we should spawn a powerup
367367+ let cooldown_over = host.last_powerup.is_none_or(|d| {
368368+ (now - d).num_minutes() >= (self.settings.powerup_minutes_cooldown as i64)
369369+ });
370370+ let roll = rand::random_ratio(self.settings.powerup_chance, 100);
371371+372372+ if cooldown_over && roll {
373373+ // Cooldown is over and we rolled positive, choose and send out a powerup.
374374+ let typ_choice = rand::random_range(0..PowerUpType::ALL_TYPES.len());
375375+ let loc_choice =
376376+ rand::random_range(0..self.settings.powerup_locations.len());
377377+ let powerup = PowerUp::new(
378378+ self.settings.powerup_locations[loc_choice],
379379+ PowerUpType::ALL_TYPES[typ_choice],
380380+ );
381381+382382+ events.push((None, GameEvent::PowerUpSpawn(powerup)));
383383+384384+ host.last_powerup = Some(now);
385385+ }
386386+387387+ *last_powerup_proc = now;
388388+ }
389389+ }
390390+ }
391391+ }
392392+393393+ fn update_loc(&mut self) {
394394+ let loc = self.loc.get_loc();
395395+ self.player.push_loc(loc);
396396+ }
397397+398398+ /// Run a single game tick, returns any messages that need to be sent
399399+ pub fn tick(&mut self) -> Vec<(Option<PlayerId>, GameEvent)> {
400400+ let mut events = Vec::with_capacity(5);
401401+402402+ self.host_tick(&mut events);
403403+ self.update_loc();
404404+405405+ events
406406+ }
407407+408408+ /// Consume an event, optionally returns events to re-broadcast
409409+ pub fn consume_event(
410410+ &mut self,
411411+ time_sent: DT,
412412+ event: GameEvent,
413413+ player_id: PlayerId,
414414+ ) -> Option<GameEvent> {
415415+ if let Some(host) = self.host.as_mut() {
416416+ host.event_history
417417+ .push((player_id, time_sent, event.clone()));
418418+ }
419419+420420+ match event {
421421+ GameEvent::SeekersReleased(time) => {
422422+ self.public.seekers_started = Some(time);
423423+ }
424424+ GameEvent::PingReq(fake_player) => {
425425+ let ping = if let Some(fake_player) = fake_player {
426426+ self.player.create_ping(fake_player)
427427+ } else {
428428+ self.player.create_self_ping()
429429+ };
430430+431431+ return ping.map(|p| GameEvent::Ping(p));
432432+ }
433433+ GameEvent::Ping(ping) => {
434434+ if let Some(current) = self.public.pings.get_mut(&ping.player) {
435435+ *current = Some(ping);
436436+ }
437437+ }
438438+ GameEvent::HiderCaught(id) => {
439439+ if id == self.player.id {
440440+ self.player.seeker = true;
441441+ }
442442+ if let Some(state) = self.public.caught_state.get_mut(&id) {
443443+ *state = true;
444444+ }
445445+ if self.host.is_some() && self.public.caught_state.iter().all(|(_, k)| *k) {
446446+ return Some(GameEvent::GameEnd(Utc::now()));
447447+ }
448448+ }
449449+ GameEvent::GameEnd(_dt) => {
450450+ // [Game] handles this case, do nothing if we get here.
451451+ }
452452+ GameEvent::PowerUpSpawn(power_up) => {
453453+ self.public.available_powerup = Some(power_up);
454454+ }
455455+ GameEvent::PowerUpDespawn => {
456456+ self.public.available_powerup = None;
457457+ }
458458+ GameEvent::PowerUpActivate(usage) => {
459459+ if usage.timing() == PowerUpTiming::NextPing {
460460+ if let Some(host) = self.host.as_mut() {
461461+ if let Some(old_usage) = host.ping_power_usages.get_mut(&player_id) {
462462+ *old_usage = usage;
463463+ } else {
464464+ host.ping_power_usages.insert(player_id, usage);
465465+ }
466466+ }
467467+ }
468468+ }
469469+ GameEvent::PostGameSync(_) | GameEvent::HostHistorySync(_) => {
470470+ // Handled by [Game]
471471+ }
472472+ }
473473+ None
474474+ }
475475+}