bloop_server_framework/
player.rs

1//! Player management module.
2//!
3//! This module provides traits and structures for managing player information,
4//! including read-only access ([`PlayerInfo`]), mutation capabilities
5//! ([`PlayerMutator`]), and a concurrent registry ([`PlayerRegistry`]) to store
6//! and lookup players by both UUID and NFC UID.
7//!
8//! The `PlayerRegistry` maintains internal consistency by updating NFC UID
9//! mappings automatically when players' NFC UIDs change, enabling safe
10//! concurrent access via `Arc<RwLock<Player>>` wrappers.
11//!
12//! It supports operations to add, remove, mutate, and query players efficiently.
13
14use crate::nfc_uid::NfcUid;
15use chrono::{DateTime, Utc};
16use std::collections::HashMap;
17use std::sync::{Arc, RwLock, RwLockReadGuard};
18use uuid::Uuid;
19
20/// Provides read-only information about a player.
21pub trait PlayerInfo {
22    /// Returns the unique player ID.
23    fn id(&self) -> Uuid;
24
25    /// Returns the player's NFC UID.
26    fn nfc_uid(&self) -> NfcUid;
27
28    /// Returns the total number of bloops the player has collected.
29    fn total_bloops(&self) -> usize;
30
31    /// Returns a reference to the map of awarded achievements and their timestamps.
32    fn awarded_achievements(&self) -> &HashMap<Uuid, DateTime<Utc>>;
33}
34
35/// Provides mutation methods for a player.
36pub trait PlayerMutator {
37    /// Increments the bloop counter by one.
38    fn increment_bloops(&mut self);
39
40    /// Adds an awarded achievement to the player.
41    fn add_awarded_achievement(&mut self, achievement_id: Uuid, awarded_at: DateTime<Utc>);
42}
43
44/// A registry for managing players by both their UUID and NFC UID.
45///
46/// The registry ensures that updates to players' NFC UIDs automatically keep
47/// internal lookup maps in sync.
48#[derive(Debug)]
49pub struct PlayerRegistry<Player: PlayerInfo> {
50    by_id: HashMap<Uuid, Arc<RwLock<Player>>>,
51    by_nfc_uid: HashMap<NfcUid, Arc<RwLock<Player>>>,
52}
53
54impl<Player: PlayerInfo> From<Vec<Player>> for PlayerRegistry<Player> {
55    fn from(players: Vec<Player>) -> Self {
56        Self::new(players)
57    }
58}
59
60impl<Player: PlayerInfo> PlayerRegistry<Player> {
61    /// Creates a new [`PlayerRegistry`] from a list of players
62    pub fn new(players: Vec<Player>) -> Self {
63        let mut by_id = HashMap::new();
64        let mut by_nfc_uid = HashMap::new();
65
66        for player in players {
67            let id = player.id();
68            let nfc_uid = player.nfc_uid();
69            let player = Arc::new(RwLock::new(player));
70
71            by_id.insert(id, player.clone());
72            by_nfc_uid.insert(nfc_uid, player);
73        }
74
75        Self { by_id, by_nfc_uid }
76    }
77
78    /// Returns the shared player reference (Arc) by their UUID.
79    ///
80    /// Use [`Self::read_by_id()`] if you only need read access.
81    pub fn get_by_id(&self, id: Uuid) -> Option<Arc<RwLock<Player>>> {
82        self.by_id.get(&id).cloned()
83    }
84
85    /// Returns the shared player reference (Arc) by their NFC UID.
86    ///
87    /// /// Use [`Self::read_by_nfc_uid()`] if you only need read access.
88    pub fn get_by_nfc_uid(&self, nfc_uid: NfcUid) -> Option<Arc<RwLock<Player>>> {
89        self.by_nfc_uid.get(&nfc_uid).cloned()
90    }
91
92    /// Returns a read-only lock on a player by their UUID.
93    ///
94    /// Returns `None` if no player with the given ID exists.
95    pub fn read_by_id(&self, id: Uuid) -> Option<RwLockReadGuard<'_, Player>> {
96        self.by_id.get(&id).map(|player| player.read().unwrap())
97    }
98
99    /// Returns a read-only lock on a player by their NFC UID.
100    ///
101    /// Returns `None` if no player with the given NFC UID exists.
102    pub fn read_by_nfc_uid(&self, nfc_uid: NfcUid) -> Option<RwLockReadGuard<'_, Player>> {
103        self.by_nfc_uid
104            .get(&nfc_uid)
105            .map(|player| player.read().unwrap())
106    }
107
108    /// Returns `true` if a player with the given UUID exists in the registry.
109    pub fn contains_id(&self, id: Uuid) -> bool {
110        self.by_id.contains_key(&id)
111    }
112
113    /// Mutates a player by their UUID.
114    ///
115    /// Automatically updates the internal NFC UID lookup map if the player's
116    /// NFC UID changes.
117    ///
118    /// If no player with the given ID exists, this method does nothing.
119    pub fn mutate_by_id<F>(&mut self, id: Uuid, mutator: F)
120    where
121        F: FnOnce(&mut Player),
122    {
123        if let Some(player_arc) = self.by_id.get(&id) {
124            let mut player = player_arc.write().unwrap();
125            let old_nfc_uid = player.nfc_uid();
126
127            mutator(&mut player);
128
129            let new_nfc_uid = player.nfc_uid();
130
131            if old_nfc_uid != new_nfc_uid {
132                self.by_nfc_uid.remove(&old_nfc_uid);
133                self.by_nfc_uid.insert(new_nfc_uid, player_arc.clone());
134            }
135        }
136    }
137
138    /// Adds a new player to the registry.
139    ///
140    /// This method inserts the player into both internal lookup maps.
141    /// If a player with the same ID or NFC UID already exists, it will be
142    /// overwritten.
143    pub fn add(&mut self, player: Player) {
144        let id = player.id();
145        let nfc_uid = player.nfc_uid();
146        let player = Arc::new(RwLock::new(player));
147
148        self.by_id.insert(id, player.clone());
149        self.by_nfc_uid.insert(nfc_uid, player);
150    }
151
152    /// Removes a player from the registry by their UUID.
153    ///
154    /// This also removes the player from the NFC UID lookup map.
155    /// If no such player exists, this method does nothing.
156    pub fn remove(&mut self, id: Uuid) {
157        let Some(player) = self.by_id.remove(&id) else {
158            return;
159        };
160
161        let player = player.read().unwrap();
162        self.by_nfc_uid.remove(&player.nfc_uid());
163    }
164}
165
166#[cfg(test)]
167mod tests {
168    use super::*;
169    use crate::test_utils::MockPlayer;
170
171    #[test]
172    fn adding_and_reading_player_by_nfc_uid_works() {
173        let (player, id) = MockPlayer::builder().bloops_count(5).build();
174        let player = Arc::try_unwrap(player).unwrap().into_inner().unwrap();
175
176        let registry = PlayerRegistry::new(vec![player]);
177
178        let player_read = registry.read_by_nfc_uid(NfcUid::default()).unwrap();
179        assert_eq!(player_read.total_bloops(), 5);
180        assert!(registry.contains_id(id));
181    }
182
183    #[test]
184    fn mutating_player_updates_nfc_uid_and_bloops_correctly() {
185        let (player, id) = MockPlayer::builder().bloops_count(0).build();
186        let player = Arc::try_unwrap(player).unwrap().into_inner().unwrap();
187
188        let mut registry = PlayerRegistry::new(vec![player]);
189
190        registry.mutate_by_id(id, |player| {
191            player.bloops_count = 42;
192        });
193
194        let player_read = registry.read_by_id(id).unwrap();
195        assert_eq!(player_read.total_bloops(), 42);
196    }
197
198    #[test]
199    fn removing_player_removes_from_all_indexes() {
200        let (player, id) = MockPlayer::builder().bloops_count(1).build();
201        let nfc_uid = NfcUid::default();
202        let player = Arc::try_unwrap(player).unwrap().into_inner().unwrap();
203
204        let mut registry = PlayerRegistry::new(vec![player]);
205
206        registry.remove(id);
207
208        assert!(registry.read_by_id(id).is_none());
209        assert!(registry.read_by_nfc_uid(nfc_uid).is_none());
210    }
211
212    #[test]
213    fn adding_player_overwrites_existing_player_with_same_id_or_nfc_uid() {
214        let (player1, id) = MockPlayer::builder().bloops_count(5).build();
215        let player1 = Arc::try_unwrap(player1).unwrap().into_inner().unwrap();
216
217        let (player2, _) = MockPlayer::builder().bloops_count(10).build();
218        let mut player2 = Arc::try_unwrap(player2).unwrap().into_inner().unwrap();
219
220        player2.id = id;
221        player2.registration_number = 99;
222
223        let mut registry = PlayerRegistry::new(vec![player1]);
224        registry.add(player2);
225
226        let player_read = registry.read_by_id(id).unwrap();
227        assert_eq!(player_read.total_bloops(), 10);
228        assert_eq!(player_read.registration_number, 99);
229    }
230}