1use serde::{Deserialize, Serialize};
15
16use crate::protocol::GameType;
17
18use super::types::{
19 AdminGameInfo, DebugBackendGameState, DebugHandlerGameState, DebugLobbyState,
20 DebugPlayerInfo, DebugWebsocketContext, ErrorCode, GameChange, GamePlayerInfo, GameSnapshot,
21 Grid, LobbyChange, LobbyGameInfo, LobbyPlayerInfo, LobbyType, PlayerInfo,
22 RematchCountdownState, ScoreInfo, SpectatorInfo, TimerVoteState,
23};
24
25#[serde_with::serde_as]
27#[derive(Debug, Clone, Serialize, Deserialize)]
28#[serde(tag = "type", rename_all = "snake_case")]
29pub enum ServerMessage {
30 Hello {
37 heartbeat_interval_ms: u32,
39 #[serde(skip_serializing_if = "Option::is_none")]
41 server_version: Option<String>,
42 },
43
44 Ready {
48 session_id: String,
50 #[serde_as(as = "serde_with::DisplayFromStr")]
52 player_id: i64,
53 #[serde(skip_serializing_if = "Option::is_none")]
55 lobby: Option<LobbySnapshot>,
56 #[serde(skip_serializing_if = "Option::is_none")]
58 game: Option<GameSnapshot>,
59 },
60
61 Resumed {
65 missed_events: Vec<ServerMessage>,
67 },
68
69 HeartbeatAck {
73 server_time: u64,
75 },
76
77 InvalidSession { reason: String },
81
82 LobbyJoined {
87 lobby_id: String,
88 #[serde(skip_serializing_if = "Option::is_none")]
90 lobby_code: Option<String>,
91 lobby: LobbySnapshot,
93 },
94
95 LobbySnapshot { lobby: LobbySnapshot },
99
100 LobbyDelta { changes: Vec<LobbyChange> },
104
105 LobbyLeft,
107
108 CustomLobbyCreated {
110 lobby_id: String,
111 lobby_code: String,
112 },
113 GameStarted {
120 game_id: String,
121 grid: Grid,
122 players: Vec<GamePlayerInfo>,
123 your_turn_order: u8,
125 #[serde_as(as = "serde_with::DisplayFromStr")]
127 current_turn: i64,
128 round: u8,
129 max_rounds: u8,
130 #[serde(skip_serializing_if = "Option::is_none")]
132 turn_time_limit: Option<u32>,
133 },
134
135 GameSnapshot { game_id: String, game: GameSnapshot },
139
140 GameDelta {
142 game_id: String,
143 changes: Vec<GameChange>,
144 },
145
146 GameOver {
148 game_id: String,
149 final_scores: Vec<ScoreInfo>,
151 #[serde_as(as = "serde_with::DisplayFromStr")]
153 winner_id: i64,
154 #[serde(default)]
156 is_draw: bool,
157 },
158
159 GameCancelled { game_id: String, reason: String },
161
162 PlayerJoined { player: LobbyPlayerInfo },
167
168 PlayerLeft {
170 #[serde_as(as = "serde_with::DisplayFromStr")]
171 player_id: i64,
172 #[serde(skip_serializing_if = "Option::is_none")]
173 reason: Option<String>,
174 },
175
176 PlayerReconnected {
178 #[serde_as(as = "serde_with::DisplayFromStr")]
179 player_id: i64,
180 },
181
182 PlayerDisconnected {
184 #[serde(skip_serializing_if = "Option::is_none")]
185 game_id: Option<String>,
186 #[serde_as(as = "serde_with::DisplayFromStr")]
187 player_id: i64,
188 grace_period_seconds: u32,
190 },
191
192 WordScored {
194 #[serde_as(as = "serde_with::DisplayFromStr")]
195 player_id: i64,
196 game_id: String,
197 word: String,
198 score: i32,
199 path: Vec<super::types::Position>,
201 total_score: i32,
203 gems_earned: i32,
205 total_gems: i32,
207 new_grid: Grid,
209 },
210
211 TurnChanged {
213 #[serde_as(as = "serde_with::DisplayFromStr")]
214 player_id: i64,
215 game_id: String,
216 round: u8,
217 #[serde(skip_serializing_if = "Option::is_none")]
219 time_remaining: Option<u32>,
220 },
221
222 TurnPassed {
224 #[serde_as(as = "serde_with::DisplayFromStr")]
225 player_id: i64,
226 game_id: String,
227 },
228
229 RoundChanged {
231 game_id: String,
232 round: u8,
233 max_rounds: u8,
234 #[serde(skip_serializing_if = "Option::is_none")]
237 new_grid: Option<Grid>,
238 },
239
240 BoardShuffled {
242 #[serde_as(as = "serde_with::DisplayFromStr")]
243 player_id: i64,
244 game_id: String,
245 new_grid: Grid,
246 gems_spent: i32,
247 total_gems: i32,
249 },
250
251 TileSwapped {
253 #[serde_as(as = "serde_with::DisplayFromStr")]
254 player_id: i64,
255 game_id: String,
256 row: usize,
257 col: usize,
258 old_letter: char,
259 new_letter: char,
260 gems_spent: i32,
261 total_gems: i32,
263 },
264
265 SwapModeEntered {
267 #[serde_as(as = "serde_with::DisplayFromStr")]
268 player_id: i64,
269 game_id: String,
270 },
271
272 SwapModeExited {
274 #[serde_as(as = "serde_with::DisplayFromStr")]
275 player_id: i64,
276 game_id: String,
277 },
278
279 SpectatorJoined {
284 game_id: String,
285 game: GameSnapshot,
287 },
288
289 SpectatorAdded {
291 spectator: SpectatorInfo,
292 game_id: String,
293 },
294
295 SpectatorRemoved {
297 #[serde_as(as = "serde_with::DisplayFromStr")]
298 spectator_id: i64,
299 game_id: String,
300 },
301
302 SpectatorBecamePlayer {
304 #[serde_as(as = "serde_with::DisplayFromStr")]
305 player_id: i64,
306 username: String,
307 game_id: String,
308 },
309
310 SpectatorLeft,
312
313 SelectionUpdate {
318 #[serde_as(as = "serde_with::DisplayFromStr")]
319 player_id: i64,
320 game_id: String,
321 positions: Vec<super::types::Position>,
322 },
323
324 TimerVoteUpdate {
329 state: TimerVoteState,
330 game_id: String,
331 },
332
333 TurnTimerStarted {
335 #[serde_as(as = "serde_with::DisplayFromStr")]
336 target_player_id: i64,
337 game_id: String,
338 seconds: u32,
339 },
340
341 TurnTimerExpired {
343 #[serde_as(as = "serde_with::DisplayFromStr")]
344 player_id: i64,
345 game_id: String,
346 },
347
348 RematchCountdownUpdate {
355 state: RematchCountdownState,
357 previous_game_id: String,
359 },
360
361 PlayerLeftRematch {
365 #[serde_as(as = "serde_with::DisplayFromStr")]
366 player_id: i64,
367 previous_game_id: String,
369 },
370
371 RematchStarting {
375 #[serde(skip_serializing_if = "Option::is_none")]
377 #[serde_as(as = "Option<serde_with::DisplayFromStr>")]
378 triggered_by: Option<i64>,
379 previous_game_id: String,
381 },
382
383 PlayerPoolChanged {
388 #[serde_as(as = "serde_with::DisplayFromStr")]
389 player_id: i64,
390 old_pool: Option<GameType>,
391 new_pool: Option<GameType>,
392 },
393
394 PoolJoined {
399 position: i32,
400 total_in_pool: i32,
401 game_id: String,
402 },
403
404 PoolUpdate {
406 position: i32,
407 total_in_pool: i32,
408 game_id: String,
409 },
410
411 PoolLeft,
413
414 AdminGamesList { games: Vec<AdminGameInfo> },
419
420 AdminGameDeleted { game_id: String },
422
423 #[serde(rename = "game_state")]
430 GameStateUpdate {
431 game_id: String,
432 state: String,
433 grid: Grid,
434 players: Vec<PlayerInfo>,
435 current_turn: i64,
436 round: i32,
437 max_rounds: i32,
438 used_words: Vec<String>,
439 spectators: Vec<SpectatorInfo>,
440 timer_vote_state: TimerVoteState,
441 },
442
443 #[serde(rename = "lobby_state")]
445 LobbyStateUpdate {
446 lobby_id: String,
447 players: Vec<LobbyPlayerInfo>,
448 games: Vec<LobbyGameInfo>,
449 },
450
451 DebugStateResponse {
456 timestamp: String,
457 player: DebugPlayerInfo,
458 websocket_context: DebugWebsocketContext,
459 lobby_state: Option<DebugLobbyState>,
460 backend_game_state: Option<DebugBackendGameState>,
461 handler_game_state: Option<DebugHandlerGameState>,
462 },
463
464 Error {
469 code: ErrorCode,
470 message: String,
471 #[serde(skip_serializing_if = "Option::is_none")]
473 details: Option<serde_json::Value>,
474 },
475}
476
477impl ServerMessage {
478 #[must_use]
480 pub fn error(code: ErrorCode) -> Self {
481 Self::Error {
482 message: code.message().to_string(),
483 code,
484 details: None,
485 }
486 }
487
488 pub fn error_with_message(code: ErrorCode, message: impl Into<String>) -> Self {
490 Self::Error {
491 code,
492 message: message.into(),
493 details: None,
494 }
495 }
496
497 pub fn error_with_details(
499 code: ErrorCode,
500 message: impl Into<String>,
501 details: serde_json::Value,
502 ) -> Self {
503 Self::Error {
504 code,
505 message: message.into(),
506 details: Some(details),
507 }
508 }
509
510 #[must_use]
512 pub fn message_type(&self) -> &'static str {
513 match self {
514 Self::Hello { .. } => "hello",
515 Self::Ready { .. } => "ready",
516 Self::Resumed { .. } => "resumed",
517 Self::HeartbeatAck { .. } => "heartbeat_ack",
518 Self::InvalidSession { .. } => "invalid_session",
519 Self::LobbyJoined { .. } => "lobby_joined",
520 Self::LobbySnapshot { .. } => "lobby_snapshot",
521 Self::LobbyDelta { .. } => "lobby_delta",
522 Self::LobbyLeft => "lobby_left",
523 Self::CustomLobbyCreated { .. } => "custom_lobby_created",
524 Self::GameStarted { .. } => "game_started",
525 Self::GameSnapshot { .. } => "game_snapshot",
526 Self::GameDelta { .. } => "game_delta",
527 Self::GameOver { .. } => "game_over",
528 Self::GameCancelled { .. } => "game_cancelled",
529 Self::PlayerJoined { .. } => "player_joined",
530 Self::PlayerLeft { .. } => "player_left",
531 Self::PlayerReconnected { .. } => "player_reconnected",
532 Self::PlayerDisconnected { .. } => "player_disconnected",
533 Self::PlayerPoolChanged { .. } => "player_pool_changed",
534 Self::WordScored { .. } => "word_scored",
535 Self::TurnChanged { .. } => "turn_changed",
536 Self::TurnPassed { .. } => "turn_passed",
537 Self::RoundChanged { .. } => "round_changed",
538 Self::BoardShuffled { .. } => "board_shuffled",
539 Self::TileSwapped { .. } => "tile_swapped",
540 Self::SwapModeEntered { .. } => "swap_mode_entered",
541 Self::SwapModeExited { .. } => "swap_mode_exited",
542 Self::SpectatorJoined { .. } => "spectator_joined",
543 Self::SpectatorAdded { .. } => "spectator_added",
544 Self::SpectatorRemoved { .. } => "spectator_removed",
545 Self::SpectatorLeft => "spectator_left",
546 Self::SpectatorBecamePlayer { .. } => "spectator_became_player",
547 Self::SelectionUpdate { .. } => "selection_update",
548 Self::TimerVoteUpdate { .. } => "timer_vote_update",
549 Self::TurnTimerStarted { .. } => "turn_timer_started",
550 Self::TurnTimerExpired { .. } => "turn_timer_expired",
551 Self::RematchCountdownUpdate { .. } => "rematch_countdown_update",
552 Self::PlayerLeftRematch { .. } => "player_left_rematch",
553 Self::RematchStarting { .. } => "rematch_starting",
554 Self::PoolJoined { .. } => "pool_joined",
555 Self::PoolUpdate { .. } => "pool_update",
556 Self::PoolLeft => "pool_left",
557 Self::AdminGamesList { .. } => "admin_games_list",
558 Self::AdminGameDeleted { .. } => "admin_game_deleted",
559 Self::GameStateUpdate { .. } => "game_state",
560 Self::LobbyStateUpdate { .. } => "lobby_state",
561 Self::DebugStateResponse { .. } => "debug_state_response",
562
563 Self::Error { .. } => "error",
564 }
565 }
566
567 #[must_use]
569 pub fn is_error(&self) -> bool {
570 matches!(self, Self::Error { .. })
571 }
572
573 #[must_use]
577 pub fn should_store_for_replay(&self) -> bool {
578 !matches!(
579 self,
580 Self::Hello { .. }
581 | Self::HeartbeatAck { .. }
582 | Self::SelectionUpdate { .. }
583 | Self::TimerVoteUpdate {
584 state: TimerVoteState::Idle,
585 ..
586 }
587 | Self::PlayerPoolChanged { .. }
588 | Self::TurnTimerStarted { .. }
589 | Self::TurnTimerExpired { .. }
590 | Self::DebugStateResponse { .. }
591 )
592 }
593}
594
595impl From<ServerMessage> for serde_json::Value {
597 fn from(msg: ServerMessage) -> Self {
598 serde_json::to_value(msg).unwrap()
599 }
600}
601
602impl TryFrom<serde_json::Value> for ServerMessage {
604 type Error = serde_json::Error;
605
606 fn try_from(
607 value: serde_json::Value,
608 ) -> Result<Self, <ServerMessage as TryFrom<serde_json::Value>>::Error> {
609 serde_json::from_value(value)
610 }
611}
612#[derive(Debug, Clone, Serialize, Deserialize)]
618pub struct LobbySnapshot {
619 pub lobby_id: String,
620 pub lobby_type: LobbyType,
621 #[serde(skip_serializing_if = "Option::is_none")]
622 pub lobby_code: Option<String>,
623 pub players: Vec<LobbyPlayerInfo>,
624 pub games: Vec<LobbyGameInfo>,
625 #[serde(default = "default_max_players")]
627 pub max_players: u8,
628}
629
630fn default_max_players() -> u8 {
631 6
632}
633
634#[cfg(test)]
635mod tests {
636 use super::*;
637 use crate::protocol::{types, GameType};
638
639 #[test]
640 fn test_server_message_into_json() {
641 let msg = ServerMessage::BoardShuffled {
642 player_id: 1_234_567_890,
643 game_id: "1234567890".to_string(),
644 new_grid: Grid::new(),
645 gems_spent: 0,
646 total_gems: 0,
647 };
648 let json = serde_json::to_string(&msg).unwrap();
649 assert!(json.contains(r#""type":"board_shuffled""#));
650 assert!(json.contains(r#""player_id":"1234567890""#));
651 assert!(json.contains(r#""game_id":"1234567890""#));
652 assert!(json.contains(r#""new_grid":[]"#));
653 assert!(json.contains(r#""gems_spent":0"#));
654 assert!(json.contains(r#""total_gems":0"#));
655 }
656
657 #[test]
658 fn test_heartbeat_ack_serialization() {
659 let msg = ServerMessage::HeartbeatAck {
660 server_time: 1_701_234_567_890,
661 };
662 let json = serde_json::to_string(&msg).unwrap();
663 assert!(json.contains(r#""type":"heartbeat_ack""#));
664 assert!(json.contains(r#""server_time":1701234567890"#));
665 }
666
667 #[test]
668 fn test_error_message_creation() {
669 let msg = ServerMessage::error(ErrorCode::NotYourTurn);
670 match msg {
671 ServerMessage::Error { code, message, .. } => {
672 assert_eq!(code, ErrorCode::NotYourTurn);
673 assert_eq!(message, "It's not your turn");
674 }
675 _ => panic!("Expected error message"),
676 }
677 }
678
679 #[test]
680 fn test_error_with_details() {
681 let msg = ServerMessage::error_with_details(
682 ErrorCode::InvalidPath,
683 "Tiles must be adjacent",
684 serde_json::json!({"positions": [[0,0], [2,2]]}),
685 );
686 match msg {
687 ServerMessage::Error { details, .. } => {
688 assert!(details.is_some());
689 }
690 _ => panic!("Expected error message"),
691 }
692 }
693
694 #[test]
695 fn test_player_joined_serialization() {
696 let msg = ServerMessage::PlayerJoined {
697 player: LobbyPlayerInfo {
698 user_id: 123,
699 username: "TestPlayer".to_string(),
700 avatar_url: None,
701 banner_url: None,
702 accent_color: None,
703 current_game_pool: None,
704 active_game_id: None,
705 spectate_game_id: None,
706 },
707 };
708 let json = serde_json::to_string(&msg).unwrap();
709 assert!(json.contains(r#""type":"player_joined""#));
710 assert!(json.contains(r#""user_id":"123""#));
711 }
712
713 #[test]
714 fn test_game_state_update_legacy() {
715 let json = r#"{"type":"game_state","game_id":"abc","state":"in_progress","grid":[],"players":[],"current_turn":123,"round":1,"max_rounds":5,"used_words":[],"spectators":[],"timer_vote_state":{"status":"idle"}}"#;
717 let msg: ServerMessage = serde_json::from_str(json).unwrap();
718 assert!(matches!(msg, ServerMessage::GameStateUpdate { .. }));
719 }
720
721 #[test]
722 fn test_should_store_for_replay() {
723 assert!(!ServerMessage::HeartbeatAck { server_time: 0 }.should_store_for_replay());
724 assert!(ServerMessage::PlayerJoined {
725 player: LobbyPlayerInfo {
726 user_id: 1,
727 username: "x".into(),
728 avatar_url: None,
729 banner_url: None,
730 accent_color: None,
731 current_game_pool: None,
732 active_game_id: None,
733 spectate_game_id: None,
734 }
735 }
736 .should_store_for_replay());
737 }
738
739 #[test]
740 fn test_message_type() {
741 assert_eq!(
742 ServerMessage::HeartbeatAck { server_time: 0 }.message_type(),
743 "heartbeat_ack"
744 );
745 assert_eq!(
746 ServerMessage::error(ErrorCode::NotYourTurn).message_type(),
747 "error"
748 );
749 }
750
751 #[test]
752 fn test_selection_update_serialization() {
753 let msg = ServerMessage::SelectionUpdate {
754 player_id: 11,
755 game_id: "game1".to_string(),
756 positions: vec![
757 types::Position { row: 0, col: 0 },
758 types::Position { row: 0, col: 1 },
759 ],
760 };
761 let json = serde_json::to_string(&msg).unwrap();
762 assert!(json.contains(r#""type":"selection_update""#));
763 assert!(json.contains(r#""player_id":"11""#));
764 assert!(json.contains(r#""game_id":"game1""#));
765 }
766
767 #[test]
768 fn test_player_queue_changed_serialization() {
769 let msg = ServerMessage::PlayerPoolChanged {
770 player_id: 123,
771 old_pool: Some(GameType::Open),
772 new_pool: Some(GameType::Adventure),
773 };
774 let json = serde_json::to_string(&msg).unwrap();
775 assert!(json.contains(r#""type":"player_pool_changed""#));
776 assert!(json.contains(r#""player_id":"123""#));
777 assert!(json.contains(r#""old_pool":"open""#));
778 assert!(json.contains(r#""new_pool":"adventure""#));
779 }
780
781 #[test]
782 fn test_debug_state_response_serialization() {
783 let msg = ServerMessage::DebugStateResponse {
784 timestamp: "2024-01-01T12:00:00Z".to_string(),
785 player: DebugPlayerInfo {
786 user_id: 987654321,
787 username: "debug_user".to_string(),
788 },
789 websocket_context: DebugWebsocketContext {
790 lobby_id: Some("lobby123".to_string()),
791 game_id: Some("game456".to_string()),
792 is_spectating: false,
793 },
794 lobby_state: Some(DebugLobbyState::Found {
795 lobby_id: "lobby123".to_string(),
796 player_in_lobby: true,
797 lobby_player_ids: vec![111, 222, 333],
798 active_game_id: Some("game456".to_string()),
799 }),
800 backend_game_state: Some(DebugBackendGameState::Found {
801 game_id: "game456".to_string(),
802 player_in_session_players: true,
803 spectator_in_session: false,
804 session_player_ids: vec![111, 222],
805 session_spectator_ids: vec![],
806 lobby_id: "lobby123".to_string(),
807 }),
808 handler_game_state: Some(DebugHandlerGameState::Found {
809 game_id: "game456".to_string(),
810 player_in_handler_game: true,
811 handler_player_ids: vec![111, 222],
812 current_turn_index: 0,
813 round: 1,
814 state: "in_progress".to_string(),
815 }),
816 };
817
818 let json = serde_json::to_string(&msg).unwrap();
820
821 assert!(json.contains(r#""type":"debug_state_response""#));
823
824 assert!(json.contains(r#""user_id":987654321"#));
826 assert!(json.contains(r#""lobby_player_ids":[111,222,333]"#));
827 assert!(json.contains(r#""session_player_ids":[111,222]"#));
828 assert!(json.contains(r#""handler_player_ids":[111,222]"#));
829
830 assert!(json.contains(r#""timestamp":"2024-01-01T12:00:00Z""#));
832 assert!(json.contains(r#""username":"debug_user""#));
833 assert!(json.contains(r#""lobby_id":"lobby123""#));
834 assert!(json.contains(r#""game_id":"game456""#));
835 assert!(json.contains(r#""is_spectating":false"#));
836 assert!(json.contains(r#""player_in_lobby":true"#));
837
838 let deserialized: ServerMessage = serde_json::from_str(&json).unwrap();
840 match deserialized {
841 ServerMessage::DebugStateResponse {
842 timestamp,
843 player,
844 websocket_context,
845 lobby_state,
846 backend_game_state,
847 handler_game_state,
848 } => {
849 assert_eq!(timestamp, "2024-01-01T12:00:00Z");
850 assert_eq!(player.user_id, 987654321);
851 assert_eq!(player.username, "debug_user");
852 assert_eq!(websocket_context.lobby_id, Some("lobby123".to_string()));
853 assert_eq!(websocket_context.game_id, Some("game456".to_string()));
854 assert!(!websocket_context.is_spectating);
855 assert!(lobby_state.is_some());
856 assert!(backend_game_state.is_some());
857 assert!(handler_game_state.is_some());
858 }
859 _ => panic!("Expected DebugStateResponse message"),
860 }
861 }
862
863 #[test]
864 fn test_debug_state_response_with_errors() {
865 let msg = ServerMessage::DebugStateResponse {
867 timestamp: "2024-01-01T12:00:00Z".to_string(),
868 player: DebugPlayerInfo {
869 user_id: 123,
870 username: "test".to_string(),
871 },
872 websocket_context: DebugWebsocketContext {
873 lobby_id: None,
874 game_id: None,
875 is_spectating: false,
876 },
877 lobby_state: Some(DebugLobbyState::Error {
878 error: "Lobby not found".to_string(),
879 }),
880 backend_game_state: Some(DebugBackendGameState::Error {
881 error: "Game not found".to_string(),
882 }),
883 handler_game_state: Some(DebugHandlerGameState::Error {
884 error: "Handler not found".to_string(),
885 }),
886 };
887
888 let json = serde_json::to_string(&msg).unwrap();
889
890 assert!(json.contains(r#""error":"Lobby not found""#));
892 assert!(json.contains(r#""error":"Game not found""#));
893 assert!(json.contains(r#""error":"Handler not found""#));
894
895 let deserialized: ServerMessage = serde_json::from_str(&json).unwrap();
897 assert!(matches!(deserialized, ServerMessage::DebugStateResponse { .. }));
898 }
899
900 #[test]
901 fn test_debug_state_response_minimal() {
902 let msg = ServerMessage::DebugStateResponse {
904 timestamp: "2024-01-01T12:00:00Z".to_string(),
905 player: DebugPlayerInfo {
906 user_id: 456,
907 username: "minimal_user".to_string(),
908 },
909 websocket_context: DebugWebsocketContext {
910 lobby_id: None,
911 game_id: None,
912 is_spectating: true,
913 },
914 lobby_state: None,
915 backend_game_state: None,
916 handler_game_state: None,
917 };
918
919 let json = serde_json::to_string(&msg).unwrap();
920
921 assert!(json.contains(r#""type":"debug_state_response""#));
923
924 assert!(json.contains(r#""user_id":456"#));
926 assert!(json.contains(r#""username":"minimal_user""#));
927 assert!(json.contains(r#""is_spectating":true"#));
928
929 let deserialized: ServerMessage = serde_json::from_str(&json).unwrap();
931 match deserialized {
932 ServerMessage::DebugStateResponse {
933 player,
934 lobby_state,
935 backend_game_state,
936 handler_game_state,
937 ..
938 } => {
939 assert_eq!(player.user_id, 456);
940 assert!(lobby_state.is_none());
941 assert!(backend_game_state.is_none());
942 assert!(handler_game_state.is_none());
943 }
944 _ => panic!("Expected DebugStateResponse message"),
945 }
946 }
947}