team04_server/lobby/state/
handle_messages.rs

1use std::collections::BTreeSet;
2
3use crate::board::{BoardSize, Coord, TileType};
4use crate::messages::client_role::PlayerRole;
5use crate::messages::error::{ErrorInvalid, ErrorMessage, error_code};
6use crate::messages::{MessageTx, RxMessage};
7
8use super::LobbyPhase;
9use super::players::PlayerInMatchup;
10use super::{LobbyState, clients::PlayerId};
11
12impl LobbyState {
13    /// handles non-chat messages sent by playing clients
14    pub async fn message_from(
15        &mut self,
16        player_id: &PlayerId,
17        msg: RxMessage,
18        original_message: String,
19    ) {
20        if let Some(player) = self.clients.players.get_mut(player_id) {
21            match msg {
22                RxMessage::CharacterChosen(_)
23                | RxMessage::ConnectGame(_)
24                | RxMessage::Reconnect(_)
25                | RxMessage::HelloServer
26                | RxMessage::LeaveLobby
27                | RxMessage::Ready => {
28                    player
29                        .send_message(
30                            &ErrorInvalid {
31                                original_message: &original_message,
32                            }
33                            .serialize(),
34                        )
35                        .await;
36                }
37                RxMessage::PauseRequest(msg) => {
38                    if player.role != PlayerRole::Player {
39                        player
40                            .send_message(
41                                &ErrorInvalid {
42                                    original_message: &original_message,
43                                }
44                                .serialize(),
45                            )
46                            .await;
47                    } else if msg.pause {
48                        if player.lives > 0 {
49                            // player wants to pause the game
50                            self.pause_requested = true;
51                            // this will have an effect after the current phase
52                        }
53                    } else {
54                        // player wants to unpause the game
55                        if self.paused() {
56                            // unpause the game
57                            self.pause_requested = false;
58                            // this will start the next phase, which will then broadcast a new gamestate with `paused: false`
59                            self.paused.send(false).ok();
60                        } else {
61                            // game was not paused, prevent a pause from happening if one was requested
62                            self.pause_requested = false;
63                        }
64                    }
65                }
66                RxMessage::LightsaberChosen(msg) => {
67                    if matches!(self.phase, LobbyPhase::LightsaberShopPhase)
68                        && player.lightsaber_choice_allowed
69                    {
70                        // if the player hasn't picked a lightsaber yet, this will drop the `Err(_)`
71                        // value, decreasing the semaphore's count by one. Once all players have chosen
72                        // a lightsaber, the semaphore drops to a count of zero and the lobby processing
73                        // task is no longer blocked, causing the game to go on immediately.
74                        // If the player has already picked a lightsaber, their choice is simply changed
75                        // (or may be ignored, if the lobby task has already processed the players' choices).
76                        player.lightsaber_choice = Ok(msg.choice);
77                    } else {
78                        // TODO: Strike!
79                        // NOTE: This includes the player being dead.
80                        player
81                            .send_message(
82                                &ErrorMessage {
83                                    reason: "received LIGHTSABER_CHOSEN message at the wrong time",
84                                    code: error_code::MESSAGE_AT_WRONG_TIME_LIGHTSABER_CHOSEN,
85                                }
86                                .serialize(),
87                            )
88                            .await;
89                    }
90                }
91                RxMessage::UnitChosen(msg) => {
92                    if matches!(self.phase, LobbyPhase::UnitShopPhase) && player.unit_choice_allowed
93                    {
94                        if self
95                            .hist_unit_options
96                            .last()
97                            .is_some_and(|options| options.contains(&msg.choice))
98                        {
99                            player.unit_choice = Ok(msg.choice);
100                        } else {
101                            player
102                                .send_message(
103                                    &ErrorMessage {
104                                        reason: "received UNIT_CHOSEN message with a unit which was not in the UNIT_OPTIONS",
105                                        code: error_code::UNIT_CHOSEN_NOT_IN_OPTIONS,
106                                    }
107                                    .serialize(),
108                                )
109                                .await;
110                        }
111                    } else {
112                        // TODO: Strike!
113                        // NOTE: This includes the player being dead.
114                        player
115                            .send_message(
116                                &ErrorMessage {
117                                    reason: "received UNIT_CHOSEN message at the wrong time",
118                                    code: error_code::MESSAGE_AT_WRONG_TIME_UNIT_CHOSEN,
119                                }
120                                .serialize(),
121                            )
122                            .await;
123                    }
124                }
125                RxMessage::PlacementComplete(msg) => 'placement_complete_msg_handling: {
126                    if matches!(self.phase, LobbyPhase::PlacementPhase)
127                        && player.new_placement_allowed
128                    {
129                        let mut placed_units = msg
130                            .unit_bank
131                            .iter()
132                            .chain(msg.unit_placement.iter().map(|p| &p.unit))
133                            .copied()
134                            .collect::<Vec<_>>();
135                        let mut owned_units = player
136                            .unit_bank
137                            .iter()
138                            .chain(player.unit_placement.iter().map(|p| &p.unit_type))
139                            .copied()
140                            .collect::<Vec<_>>();
141                        placed_units.sort_unstable();
142                        owned_units.sort_unstable();
143                        // check that the client hasn't added or removed any units
144                        if placed_units == owned_units {
145                            let BoardSize { width, height } =
146                                self.configs.board_config.board.get_size();
147                            // check that each individual placement is valid
148                            // and that there are no duplicates
149                            let mut coords = BTreeSet::new();
150                            for placement in &msg.unit_placement {
151                                #[allow(unused_mut)]
152                                let mut x = placement.coord().x;
153                                #[allow(unused_mut)]
154                                let mut y = placement.coord().y;
155                                if y < height && x < height {
156                                    if (y < height / 2)
157                                        != (player.player_in_matchup == PlayerInMatchup::Player1)
158                                    {
159                                        // received coordinates on the wrong half of the board
160                                        #[cfg(feature = "fix-placement-coords")]
161                                        {
162                                            // auto-fix feature enabled, try to fix coords
163                                            y = height - 1 - y;
164                                            x = width - 1 - x;
165                                        }
166                                        #[cfg(not(feature = "fix-placement-coords"))]
167                                        {
168                                            // auto-fix feature disabled, send an error message to the client
169                                            // TODO: Strike
170                                            player
171                                            .send_message(
172                                                &ErrorMessage {
173                                                    reason: &format!("received PLACEMENT_COMPLETE message with coords on the wrong half of the board: x={x},y={y}"),
174                                                    code: error_code::INVALID_PLACEMENT_UNIT_WRONG_BOARD_HALF,
175                                                }
176                                                .serialize(),
177                                            )
178                                            .await;
179                                            break 'placement_complete_msg_handling;
180                                        }
181                                    }
182                                    if self
183                                        .configs
184                                        .board_config
185                                        .board
186                                        .get(Coord { x, y })
187                                        .is_some_and(|t| match t {
188                                            TileType::Rock => false,
189                                            TileType::Grass => true,
190                                            TileType::Force => true,
191                                            TileType::MedicCenter => true,
192                                            TileType::Lava => true,
193                                        })
194                                    {
195                                        if !coords.contains(&(x, y)) {
196                                            // this placement is fine
197                                            coords.insert((x, y));
198                                        } else {
199                                            // TODO: Strike
200                                            player
201                                                    .send_message(
202                                                        &ErrorMessage {
203                                                            reason: &format!("received PLACEMENT_COMPLETE message with two units placed on the same field: x={x},y={y}"),
204                                                            code: error_code::INVALID_PLACEMENT_TWO_UNITS_ON_ONE_FIELD,
205                                                        }
206                                                        .serialize(),
207                                                    )
208                                                    .await;
209                                            break 'placement_complete_msg_handling;
210                                        }
211                                    } else {
212                                        // TODO: Strike
213                                        player
214                                                .send_message(
215                                                    &ErrorMessage {
216                                                        reason: &format!("received PLACEMENT_COMPLETE message with a unit placed on a rock: x={x},y={y}"),
217                                                        code: error_code::INVALID_PLACEMENT_UNIT_ON_ROCK,
218                                                    }
219                                                    .serialize(),
220                                                )
221                                                .await;
222                                        break 'placement_complete_msg_handling;
223                                    }
224                                } else {
225                                    // TODO: Strike
226                                    player
227                                        .send_message(
228                                            &ErrorMessage {
229                                                reason: &format!("received PLACEMENT_COMPLETE message with out-of-bounds positions: x={x},y={y}, width={width},height={height}"),
230                                                code: error_code::INVALID_PLACEMENT_COORD_OUT_OF_BOUNDS,
231                                            }
232                                            .serialize(),
233                                        )
234                                        .await;
235                                    break 'placement_complete_msg_handling;
236                                }
237                            }
238                            // placements are fine
239                            let new_placed = msg
240                                .unit_placement
241                                .iter()
242                                .map(|p| crate::lobby::state::players::PlacedUnit {
243                                    unit_type: p.unit,
244                                    position: p.coord(),
245                                })
246                                .collect();
247                            player.new_placement = Ok((new_placed, msg.unit_bank));
248                        } else {
249                            // TODO: Strike
250                            player
251                                .send_message(
252                                    &ErrorMessage {
253                                        reason: &format!("received PLACEMENT_COMPLETE message with units that are different to the ones you actually own: want to place {placed_units:?} but have {owned_units:?}"),
254                                        code: error_code::INVALID_PLACEMENT_UNITS,
255                                    }
256                                    .serialize(),
257                                )
258                                .await;
259                        }
260                    } else {
261                        // TODO: Strike!
262                        // NOTE: This includes the player being dead.
263                        player
264                            .send_message(
265                                &ErrorMessage {
266                                    reason: "received PLACEMENT_COMPLETE message at the wrong time",
267                                    code: error_code::MESSAGE_AT_WRONG_TIME_PLACEMENT_COMPLETE,
268                                }
269                                .serialize(),
270                            )
271                            .await;
272                    }
273                }
274                RxMessage::TextMessage(_) => unreachable!("handled in server::connection"),
275            }
276        }
277    }
278}
279
280#[cfg(test)]
281mod test {
282    use tokio::sync::mpsc::UnboundedReceiver;
283
284    use crate::{
285        lobby::{
286            state::{LobbyPhase, SharedLobbyState, clients::PlayerId},
287            test::{FakeCon, get_server_and_lobby, player_join},
288        },
289        messages::{
290            RxMessage, client_role::ClientRole, connect_game::ConnectGame,
291            lightsaber_chosen::LightsaberChosen, placement_complete::PlacementComplete,
292            unit_chosen::UnitChosen,
293        },
294        server::state::LobbyId,
295        unit::{LightsaberType, UnitType},
296    };
297
298    async fn lobby() -> (
299        SharedLobbyState,
300        PlayerId,
301        PlayerId,
302        UnboundedReceiver<String>,
303        UnboundedReceiver<String>,
304    ) {
305        let (server, lobby) = get_server_and_lobby();
306        let (p1id, _, p1con) = player_join(&server, &lobby).await;
307        let (p2id, _, p2con) = player_join(&server, &lobby).await;
308        (lobby, p1id, p2id, p1con, p2con)
309    }
310
311    #[tokio::test]
312    async fn wrong_time() {
313        let (lobby, p1id, _, mut p1con, _) = lobby().await;
314        let mut lock = lobby.lock().await;
315        lock.start_game_now().await;
316        p1con.clear();
317        lock.phase = LobbyPhase::CompletionPhase;
318        for msg in [
319            RxMessage::Ready,
320            RxMessage::ConnectGame(ConnectGame {
321                name: "joe".to_owned(),
322                role: ClientRole::Player,
323                lobby_id: LobbyId::random(),
324            }),
325            RxMessage::HelloServer,
326        ] {
327            lock.message_from(&p1id, msg, String::new()).await;
328            let resp = p1con.recv().await.unwrap();
329            assert!(resp.contains("INVALID_MESSAGE"), "response: {resp}");
330        }
331        for msg in [
332            RxMessage::LightsaberChosen(LightsaberChosen {
333                choice: LightsaberType::Red,
334            }),
335            RxMessage::UnitChosen(UnitChosen {
336                choice: UnitType::Wookie,
337            }),
338            RxMessage::PlacementComplete(PlacementComplete {
339                unit_placement: vec![],
340                unit_bank: vec![],
341            }),
342        ] {
343            lock.message_from(&p1id, msg, String::new()).await;
344            let resp = p1con.recv().await.unwrap();
345            assert!(
346                resp.to_lowercase().contains("wrong time"),
347                "response: {resp}"
348            );
349        }
350    }
351
352    #[tokio::test]
353    async fn lightsaber_chosen() {
354        let (lobby, p1id, p2id, _, _) = lobby().await;
355        let mut lock = lobby.lock().await;
356        lock.start_game_now().await;
357        lock.phase = LobbyPhase::LightsaberShopPhase;
358        for p in lock.clients.players.players_all_mut() {
359            p.lightsaber_choice = Err(None);
360            p.lightsaber_choice_allowed = true;
361        }
362        lock.message_from(
363            &p1id,
364            RxMessage::LightsaberChosen(LightsaberChosen {
365                choice: LightsaberType::Red,
366            }),
367            String::new(),
368        )
369        .await;
370        lock.message_from(
371            &p2id,
372            RxMessage::LightsaberChosen(LightsaberChosen {
373                choice: LightsaberType::Red,
374            }),
375            String::new(),
376        )
377        .await;
378        assert!(
379            lock.clients
380                .players
381                .players_all()
382                .all(|p| p.lightsaber_choice.as_ref().ok().copied() == Some(LightsaberType::Red))
383        );
384    }
385
386    #[tokio::test]
387    async fn unit_chosen() {
388        let (lobby, p1id, p2id, _, _) = lobby().await;
389        let mut lock = lobby.lock().await;
390        lock.start_game_now().await;
391        lock.phase = LobbyPhase::UnitShopPhase;
392        for p in lock.clients.players.players_all_mut() {
393            p.unit_choice = Err(None);
394            p.unit_choice_allowed = true;
395        }
396        let choices = lock
397            .configs
398            .unit_config
399            .level1
400            .iter()
401            .chain(lock.configs.unit_config.level2.iter())
402            .chain(lock.configs.unit_config.level3.iter())
403            .map(|u| u.unit_type)
404            .collect::<Vec<_>>();
405        lock.hist_unit_options
406            .push([choices[0], choices[1], choices[2]]);
407        lock.message_from(
408            &p1id,
409            RxMessage::UnitChosen(UnitChosen { choice: choices[0] }),
410            String::new(),
411        )
412        .await;
413        lock.message_from(
414            &p2id,
415            RxMessage::UnitChosen(UnitChosen { choice: choices[3] }),
416            String::new(),
417        )
418        .await;
419        // sent a valid UNIT_CHOSEN message
420        assert!(
421            lock.clients
422                .players
423                .players_all()
424                .any(|p| p.unit_choice.as_ref().ok().copied() == Some(choices[0]))
425        );
426        // sent an invalid UNIT_CHOSEN message (unit 4, when only 1-3 were in UNIT_OPTIONS)
427        assert!(
428            lock.clients
429                .players
430                .players_all()
431                .any(|p| p.unit_choice.as_ref().is_err())
432        );
433    }
434}