team04_server/lobby/game/
lightsaber_purchase.rs

1use std::{sync::Arc, time::Duration};
2
3use tokio::sync::Semaphore;
4
5use crate::{
6    lobby::state::{LobbyPhase, LockedLobbyState, SharedLobbyState},
7    log,
8    messages::{MessageTx, lightsaber_options::LightsaberOptions},
9    unit::LightsaberType,
10};
11
12impl SharedLobbyState {
13    /// Sends the LIGHTSABER_OPTIONS message to all players,
14    /// then waits for up to `timeout` for the players to respond.
15    /// If they have not responded after `timeout`, a random
16    /// choice is made on their behalf.
17    ///
18    /// It is guaranteed that, after this function completes,
19    ///
20    /// - every living player has a `lightsaber_choice` value of `Ok(_)`.
21    /// - the lightsaber options which were sent to clients are the last entry in `hist_lightsaber_options`.
22    ///
23    /// To ensure no race condition can break these conditions, the function returns
24    /// a held lock to the lobby state with all the above guarantees upheld.
25    pub async fn lightsaber_purchase(&self) -> LockedLobbyState {
26        let mut pfx = log::pfx();
27        pfx.lobby(self.id());
28
29        let mut lock = self.lock().await;
30        assert!(lock.game_started());
31
32        let timeout = Duration::from_millis(lock.configs.game_config.timeout_lightsaber_shop_phase);
33        lock.phase = LobbyPhase::LightsaberShopPhase;
34        lock.broadcast_gamestate().await;
35
36        // generate lightsaber options (random values)
37        let options = {
38            let cfg = &lock.configs.game_config;
39            LightsaberOptions {
40                red: rand::random_range(cfg.lightsaber_modifier_red.interval()),
41                green: rand::random_range(cfg.lightsaber_modifier_green.interval()),
42                blue: rand::random_range(cfg.lightsaber_modifier_blue.interval()),
43            }
44        };
45        lock.hist_lightsaber_options.push(options);
46        log::debug!(
47            "Lightsaber purchase phase {} starting with\n    R={}\n    G={}\n    B={}",
48            lock.hist_lightsaber_options.len(),
49            options.red, options.green, options.blue; &pfx
50        );
51
52        // setup players so they can make lightsaber choices
53        let player_count = lock.clients.players.len();
54        let semaphore = Arc::new(Semaphore::new(player_count));
55        for player in lock.clients.players.players_alive_mut() {
56            let permit = Arc::clone(&semaphore)
57                .try_acquire_owned()
58                .expect("there are enough permits available for all players");
59            player.lightsaber_choice = Err(Some(permit));
60            player.lightsaber_choice_allowed = true;
61        }
62
63        // send the LIGHTSABER_OPTIONS message to all players
64        lock.clients.broadcast_message(&options.serialize()).await;
65
66        drop(lock);
67
68        // wait until all players have dropped their permit or the timeout has elapsed
69        let hit_timeout =
70            tokio::time::timeout(timeout, semaphore.acquire_many(player_count as u32))
71                .await
72                .is_err();
73        log::debug!("Lightsaber purchase phase ended {}", if hit_timeout { "due to timeout" } else { "early (all players have purchased)" }; &pfx);
74
75        // pick at random for players who have not picked yet and prevent players from changing their minds
76        let mut lock = self.lock().await;
77        for player in lock.clients.players.players_alive_mut() {
78            player.lightsaber_choice_allowed = false;
79            if player.lightsaber_choice.is_err() {
80                player.lightsaber_choice = Ok(LightsaberType::random());
81            }
82        }
83        lock
84    }
85}
86
87#[cfg(test)]
88mod test {
89
90    use crate::{
91        lobby::test::{
92            FakeCon, config_set_modified, get_server_and_lobby, get_server_and_lobby_with_config,
93            player_join,
94        },
95        messages::{RxMessage, lightsaber_chosen::LightsaberChosen},
96        unit::LightsaberType,
97    };
98
99    #[tokio::test]
100    async fn test_timeout() {
101        let (server, lobby) = get_server_and_lobby_with_config(config_set_modified(
102            |game| {
103                game.timeout_lightsaber_shop_phase = 0;
104            },
105            |_| {},
106            |_| {},
107        ));
108        player_join(&server, &lobby).await;
109        player_join(&server, &lobby).await;
110        lobby.lock().await.start_game_now().await;
111        let lobby_after_lightsaber_purchase = lobby.lightsaber_purchase().await;
112        let lightsabers = lobby_after_lightsaber_purchase
113            .clients
114            .players
115            .players_alive()
116            .map(|p| *p.lightsaber_choice.as_ref().unwrap())
117            .collect::<Vec<_>>();
118        assert_eq!(2, lightsabers.len());
119    }
120
121    #[tokio::test]
122    async fn test_making_choice() {
123        let (server, lobby) = get_server_and_lobby();
124        let (p1id, _, mut p1con) = player_join(&server, &lobby).await;
125        let (p2id, _, mut p2con) = player_join(&server, &lobby).await;
126        lobby.lock().await.start_game_now().await;
127        p1con.clear();
128        p2con.clear();
129
130        // simulate a lightsaber purchase phase
131        let phase = tokio::spawn({
132            let lobby = lobby.clone();
133            async move {
134                let _ = lobby.lightsaber_purchase().await;
135            }
136        });
137
138        // wait for the GAMESTATE message
139        p1con.recv().await;
140        p2con.recv().await;
141
142        // wait for the LIGHTSABER_OPTIONS message
143        p1con.recv().await;
144        p2con.recv().await;
145
146        // choose lightsabers
147        lobby
148            .lock()
149            .await
150            .message_from(
151                &p1id,
152                RxMessage::LightsaberChosen(LightsaberChosen {
153                    choice: LightsaberType::Red,
154                }),
155                String::new(),
156            )
157            .await;
158        lobby
159            .lock()
160            .await
161            .message_from(
162                &p2id,
163                RxMessage::LightsaberChosen(LightsaberChosen {
164                    choice: LightsaberType::Green,
165                }),
166                String::new(),
167            )
168            .await;
169
170        phase.await.unwrap();
171        let lightsabers = lobby
172            .lock()
173            .await
174            .clients
175            .players
176            .players_alive()
177            .map(|p| *p.lightsaber_choice.as_ref().unwrap())
178            .collect::<Vec<_>>();
179        assert!(
180            lightsabers == vec![LightsaberType::Red, LightsaberType::Green]
181                || lightsabers == vec![LightsaberType::Green, LightsaberType::Red]
182        );
183    }
184}