team04_server/lobby/game/
placement.rs

1use std::{sync::Arc, time::Duration};
2
3use tokio::sync::Semaphore;
4
5use crate::{
6    lobby::state::{LobbyPhase, LockedLobbyState, SharedLobbyState},
7    log,
8};
9
10impl SharedLobbyState {
11    /// Waits for up to `timeout` for the players to respond.
12    /// If they have not responded after `timeout`, the previous
13    /// placement is kept without making any changes.
14    ///
15    /// It is guaranteed that, after this function completes,
16    ///
17    /// - every alive player has a `placement` value of `Ok(_)`, where `_` is a valid placement consisting of placed and banked units.
18    ///
19    /// To ensure no race condition can break these conditions, the function returns
20    /// a held lock to the lobby state with all the above guarantees upheld.
21    pub async fn placements(&self) -> LockedLobbyState {
22        let mut pfx = log::pfx();
23        pfx.lobby(self.id());
24
25        let mut lock = self.lock().await;
26        assert!(lock.game_started());
27
28        let timeout = Duration::from_millis(lock.configs.game_config.timeout_placement_phase);
29        lock.phase = LobbyPhase::PlacementPhase;
30        lock.broadcast_gamestate().await;
31
32        log::debug!("Unit placement phase starting"; &pfx);
33
34        // setup players
35        let player_count = lock.clients.players.len();
36        let semaphore = Arc::new(Semaphore::new(player_count));
37        for player in lock.clients.players.players_alive_mut() {
38            let permit = Arc::clone(&semaphore)
39                .try_acquire_owned()
40                .expect("there are enough permits available for all players");
41            player.new_placement = Err(Some(permit));
42            player.new_placement_allowed = true;
43        }
44        drop(lock);
45
46        // wait until all players have dropped their permit or the timeout has elapsed
47        let hit_timeout =
48            tokio::time::timeout(timeout, semaphore.acquire_many(player_count as u32))
49                .await
50                .is_err();
51        log::debug!("Unit placement phase ended {}", if hit_timeout { "due to timeout" } else { "early (all players have confirmed their placement)" }; &pfx);
52
53        // prevent players from changing their minds
54        let mut lock = self.lock().await;
55        for player in lock.clients.players.players_alive_mut() {
56            player.new_placement_allowed = false;
57            if player.new_placement.is_err() {
58                player.new_placement =
59                    Ok((player.unit_placement.clone(), player.unit_bank.clone()));
60            }
61        }
62        lock
63    }
64}
65
66#[cfg(test)]
67mod test {
68    use std::sync::Arc;
69
70    use serde::Deserialize;
71    use tokio::{sync::mpsc::UnboundedReceiver, task::JoinHandle};
72
73    use crate::{
74        lobby::{
75            state::{SharedLobbyState, clients::PlayerId, players::PlayerInMatchup},
76            test::{FakeCon, config_set_modified, get_server_and_lobby_with_config, player_join},
77        },
78        log,
79        messages::{
80            RxMessage,
81            error::error_code,
82            placement_complete::{PlacedUnit, PlacementComplete},
83        },
84        server::state::SyncedServerState,
85        unit::UnitType,
86    };
87
88    async fn placement_phase(
89        unit_bank: Vec<UnitType>,
90        player_in_matchup: PlayerInMatchup,
91        placement: PlacementComplete,
92    ) -> (
93        JoinHandle<()>,
94        Arc<SyncedServerState>,
95        SharedLobbyState,
96        PlayerId,
97        UnboundedReceiver<String>,
98    ) {
99        let (server, lobby) = get_server_and_lobby_with_config(config_set_modified(
100            |cfg| cfg.timeout_placement_phase = 10,
101            |_| {},
102            |_| {},
103        ));
104        let (pid, _, mut con) = player_join(&server, &lobby).await;
105        lobby.lock().await.start_game_now().await;
106        let mut lock = lobby.lock().await;
107        let player = lock.clients.players.players_all_mut().next().unwrap();
108        player.unit_bank = unit_bank;
109        player.player_in_matchup = player_in_matchup;
110        drop(lock);
111        con.clear();
112
113        // simulate a unit placement phase
114        let phase = tokio::spawn({
115            let lobby = lobby.clone();
116            async move {
117                let _ = crate::lobby::game::placements(&lobby, &log::pfx()).await;
118            }
119        });
120
121        // wait for the GAMESTATE message
122        con.recv().await;
123
124        // place things
125        lobby
126            .lock()
127            .await
128            .message_from(&pid, RxMessage::PlacementComplete(placement), String::new())
129            .await;
130
131        (phase, server, lobby, pid, con)
132    }
133
134    #[tokio::test]
135    async fn valid_placement() {
136        let (phase, _, lobby, _, _) = placement_phase(
137            vec![UnitType::Wookie, UnitType::Stormtrooper],
138            PlayerInMatchup::Player1,
139            PlacementComplete {
140                unit_placement: vec![
141                    PlacedUnit {
142                        unit: UnitType::Stormtrooper,
143                        position: [0, 0],
144                    },
145                    PlacedUnit {
146                        unit: UnitType::Wookie,
147                        position: [1, 0],
148                    },
149                ],
150                unit_bank: vec![],
151            },
152        )
153        .await;
154
155        phase.await.unwrap();
156
157        let lock = lobby.lock().await;
158        let player = lock.clients.players.players_all().next().unwrap();
159        assert_eq!(player.unit_placement[0].unit_type, UnitType::Stormtrooper);
160        assert_eq!(player.unit_placement[1].unit_type, UnitType::Wookie);
161        assert!(player.unit_bank.is_empty());
162    }
163
164    #[tokio::test]
165    async fn wrong_units_placement() {
166        let (phase, _, lobby, _, mut con) = placement_phase(
167            vec![UnitType::Wookie, UnitType::Stormtrooper],
168            PlayerInMatchup::Player1,
169            PlacementComplete {
170                unit_placement: vec![
171                    PlacedUnit {
172                        unit: UnitType::Droid,
173                        position: [0, 0],
174                    },
175                    PlacedUnit {
176                        unit: UnitType::Wookie,
177                        position: [0, 1],
178                    },
179                ],
180                unit_bank: vec![],
181            },
182        )
183        .await;
184
185        #[derive(Deserialize)]
186        struct ErrorMessage {
187            pub code: String,
188        }
189        let ErrorMessage { code, .. } = serde_json::from_str(&con.recv().await.unwrap())
190            .expect("should receive an error message");
191        assert_eq!(code.as_str(), error_code::INVALID_PLACEMENT_UNITS);
192
193        phase.await.unwrap();
194
195        let lock = lobby.lock().await;
196        let player = lock.clients.players.players_all().next().unwrap();
197        // should be unchanged because our PLACEMENT_COMPLETE was invalid
198        assert!(player.unit_placement.is_empty());
199        assert!(player.unit_bank.len() == 2);
200    }
201
202    #[tokio::test]
203    async fn double_placement() {
204        let (phase, _, lobby, _, mut con) = placement_phase(
205            vec![UnitType::Wookie, UnitType::Stormtrooper],
206            PlayerInMatchup::Player1,
207            PlacementComplete {
208                unit_placement: vec![
209                    PlacedUnit {
210                        unit: UnitType::Stormtrooper,
211                        position: [0, 0],
212                    },
213                    PlacedUnit {
214                        unit: UnitType::Wookie,
215                        position: [0, 0],
216                    },
217                ],
218                unit_bank: vec![],
219            },
220        )
221        .await;
222
223        #[derive(Deserialize)]
224        struct ErrorMessage {
225            pub code: String,
226        }
227        let ErrorMessage { code, .. } = serde_json::from_str(&con.recv().await.unwrap())
228            .expect("should receive an error message");
229        assert_eq!(
230            code.as_str(),
231            error_code::INVALID_PLACEMENT_TWO_UNITS_ON_ONE_FIELD
232        );
233
234        phase.await.unwrap();
235
236        let lock = lobby.lock().await;
237        let player = lock.clients.players.players_all().next().unwrap();
238        // should be unchanged because our PLACEMENT_COMPLETE was invalid
239        assert!(player.unit_placement.is_empty());
240        assert!(player.unit_bank.len() == 2);
241    }
242}