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}