Live location tracking and playback for the game "manhunt"
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Start on integration testing

Ben C dcb97dcf f6c9fe02

+682 -5
+87
Cargo.lock
··· 971 971 ] 972 972 973 973 [[package]] 974 + name = "clap" 975 + version = "4.5.40" 976 + source = "registry+https://github.com/rust-lang/crates.io-index" 977 + checksum = "40b6887a1d8685cebccf115538db5c0efe625ccac9696ad45c409d96566e910f" 978 + dependencies = [ 979 + "clap_builder", 980 + "clap_derive", 981 + ] 982 + 983 + [[package]] 984 + name = "clap_builder" 985 + version = "4.5.40" 986 + source = "registry+https://github.com/rust-lang/crates.io-index" 987 + checksum = "e0c66c08ce9f0c698cbce5c0279d0bb6ac936d8674174fe48f736533b964f59e" 988 + dependencies = [ 989 + "anstream", 990 + "anstyle", 991 + "clap_lex", 992 + "strsim", 993 + ] 994 + 995 + [[package]] 996 + name = "clap_derive" 997 + version = "4.5.40" 998 + source = "registry+https://github.com/rust-lang/crates.io-index" 999 + checksum = "d2c7947ae4cc3d851207c1adb5b5e260ff0cca11446b1d6d1423788e442257ce" 1000 + dependencies = [ 1001 + "heck 0.5.0", 1002 + "proc-macro2", 1003 + "quote", 1004 + "syn 2.0.104", 1005 + ] 1006 + 1007 + [[package]] 1008 + name = "clap_lex" 1009 + version = "0.7.5" 1010 + source = "registry+https://github.com/rust-lang/crates.io-index" 1011 + checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" 1012 + 1013 + [[package]] 974 1014 name = "colog" 975 1015 version = "1.3.0" 976 1016 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1443 1483 ] 1444 1484 1445 1485 [[package]] 1486 + name = "doctest-file" 1487 + version = "1.0.0" 1488 + source = "registry+https://github.com/rust-lang/crates.io-index" 1489 + checksum = "aac81fa3e28d21450aa4d2ac065992ba96a1d7303efbce51a95f4fd175b67562" 1490 + 1491 + [[package]] 1446 1492 name = "dpi" 1447 1493 version = "0.1.2" 1448 1494 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2638 2684 ] 2639 2685 2640 2686 [[package]] 2687 + name = "interprocess" 2688 + version = "2.2.3" 2689 + source = "registry+https://github.com/rust-lang/crates.io-index" 2690 + checksum = "d941b405bd2322993887859a8ee6ac9134945a24ec5ec763a8a962fc64dfec2d" 2691 + dependencies = [ 2692 + "doctest-file", 2693 + "futures-core", 2694 + "libc", 2695 + "recvmsg", 2696 + "tokio", 2697 + "widestring", 2698 + "windows-sys 0.52.0", 2699 + ] 2700 + 2701 + [[package]] 2641 2702 name = "ipnet" 2642 2703 version = "2.11.0" 2643 2704 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2988 3049 ] 2989 3050 2990 3051 [[package]] 3052 + name = "manhunt-testing" 3053 + version = "0.1.0" 3054 + dependencies = [ 3055 + "anyhow", 3056 + "clap", 3057 + "interprocess", 3058 + "manhunt-logic", 3059 + "manhunt-transport", 3060 + "serde", 3061 + "serde_json", 3062 + "tokio", 3063 + ] 3064 + 3065 + [[package]] 2991 3066 name = "manhunt-transport" 2992 3067 version = "0.1.0" 2993 3068 dependencies = [ ··· 4339 4414 "x509-parser", 4340 4415 "yasna", 4341 4416 ] 4417 + 4418 + [[package]] 4419 + name = "recvmsg" 4420 + version = "1.0.0" 4421 + source = "registry+https://github.com/rust-lang/crates.io-index" 4422 + checksum = "d3edd4d5d42c92f0a659926464d4cce56b562761267ecf0f469d85b7de384175" 4342 4423 4343 4424 [[package]] 4344 4425 name = "redox_syscall" ··· 6937 7018 "windows", 6938 7019 "windows-core", 6939 7020 ] 7021 + 7022 + [[package]] 7023 + name = "widestring" 7024 + version = "1.2.0" 7025 + source = "registry+https://github.com/rust-lang/crates.io-index" 7026 + checksum = "dd7cf3379ca1aac9eea11fba24fd7e315d621f8dfe35c8d7d2be8b793726e07d" 6940 7027 6941 7028 [[package]] 6942 7029 name = "winapi"
+1 -1
Cargo.toml
··· 1 1 [workspace] 2 - members = ["manhunt-app", "manhunt-logic", "manhunt-signaling", "manhunt-transport"] 2 + members = ["manhunt-app", "manhunt-logic", "manhunt-signaling", "manhunt-testing", "manhunt-transport"] 3 3 resolver = "3" 4 4 5 5 [profile.release]
+5 -1
manhunt-logic/src/game.rs
··· 4 4 use tokio_util::sync::CancellationToken; 5 5 use uuid::Uuid; 6 6 7 - use tokio::sync::RwLock; 7 + use tokio::sync::{RwLock, RwLockWriteGuard}; 8 8 9 9 use crate::StartGameInfo; 10 10 use crate::{prelude::*, transport::TransportMessage}; ··· 312 312 self.transport.disconnect().await; 313 313 314 314 res 315 + } 316 + 317 + pub async fn lock_state(&self) -> RwLockWriteGuard<'_, GameState> { 318 + self.state.write().await 315 319 } 316 320 } 317 321
+2 -3
manhunt-logic/src/game_state.rs
··· 336 336 self.held_powerup = choice; 337 337 } 338 338 339 - #[cfg(test)] 340 - pub fn force_set_powerup(&mut self, typ: PowerUpType) { 341 - self.held_powerup = Some(typ); 339 + pub fn force_set_powerup(&mut self, powerup_type: PowerUpType) { 340 + self.held_powerup = Some(powerup_type); 342 341 } 343 342 344 343 pub fn peek_powerup(&self) -> Option<&PowerUpType> {
+1
manhunt-logic/src/lib.rs
··· 15 15 pub use game_state::{GameHistory, GameUiState}; 16 16 pub use lobby::{Lobby, LobbyMessage, LobbyState, StartGameInfo}; 17 17 pub use location::{Location, LocationService}; 18 + pub use powerups::PowerUpType; 18 19 pub use profile::PlayerProfile; 19 20 pub use settings::GameSettings; 20 21 pub use transport::{MsgPair, Transport, TransportMessage};
+26
manhunt-testing/Cargo.toml
··· 1 + [package] 2 + name = "manhunt-testing" 3 + version = "0.1.0" 4 + edition = "2024" 5 + 6 + [lib] 7 + name = "manhunt_test_shared" 8 + path = "src/lib.rs" 9 + 10 + [[bin]] 11 + name = "manhunt-test-daemon" 12 + path = "src/daemon.rs" 13 + 14 + [[bin]] 15 + name = "manhunt-test-driver" 16 + path = "src/driver.rs" 17 + 18 + [dependencies] 19 + anyhow = "1.0.98" 20 + clap = { version = "4.5.40", features = ["derive"] } 21 + interprocess = { version = "2.2.3", features = ["tokio"] } 22 + manhunt-logic = { version = "0.1.0", path = "../manhunt-logic" } 23 + manhunt-transport = { version = "0.1.0", path = "../manhunt-transport" } 24 + serde = { version = "1.0.219", features = ["derive"] } 25 + serde_json = "1.0.140" 26 + tokio = { version = "1.45.1", features = ["macros", "sync", "time", "rt", "test-util", "signal"] }
+329
manhunt-testing/src/daemon.rs
··· 1 + #![allow(clippy::result_large_err)] 2 + 3 + use manhunt_logic::{ 4 + Game as BaseGame, GameSettings, Lobby as BaseLobby, Location, LocationService, PlayerProfile, 5 + StartGameInfo, StateUpdateSender, 6 + }; 7 + use manhunt_test_shared::*; 8 + use manhunt_transport::{MatchboxTransport, request_room_code}; 9 + use std::{sync::Arc, time::Duration}; 10 + use tokio::{ 11 + io::{AsyncBufReadExt, AsyncWriteExt, BufReader}, 12 + sync::{Mutex, mpsc}, 13 + }; 14 + 15 + struct DummyLocationService; 16 + 17 + impl LocationService for DummyLocationService { 18 + fn get_loc(&self) -> Option<manhunt_logic::Location> { 19 + Some(Location { 20 + lat: 0.0, 21 + long: 0.0, 22 + heading: None, 23 + }) 24 + } 25 + } 26 + 27 + struct UpdateSender(mpsc::Sender<()>); 28 + 29 + impl StateUpdateSender for UpdateSender { 30 + fn send_update(&self) { 31 + let tx = self.0.clone(); 32 + tokio::spawn(async move { 33 + tx.send(()).await.expect("Failed to send"); 34 + }); 35 + } 36 + } 37 + 38 + type Game = BaseGame<DummyLocationService, MatchboxTransport, UpdateSender>; 39 + type Lobby = BaseLobby<MatchboxTransport, UpdateSender>; 40 + 41 + #[derive(Default)] 42 + enum DaemonScreen { 43 + #[default] 44 + PreConnect, 45 + Lobby(Arc<Lobby>), 46 + Game(Arc<Game>), 47 + } 48 + 49 + impl DaemonScreen { 50 + pub fn as_update(&self) -> ScreenUpdate { 51 + match self { 52 + Self::PreConnect => ScreenUpdate::PreConnect, 53 + Self::Game(_) => ScreenUpdate::Game, 54 + Self::Lobby(_) => ScreenUpdate::Lobby, 55 + } 56 + } 57 + } 58 + 59 + type StateHandle = Arc<Mutex<DaemonState>>; 60 + 61 + struct DaemonState { 62 + screen: DaemonScreen, 63 + profile: PlayerProfile, 64 + responses: mpsc::Sender<TestingResponse>, 65 + updates: (mpsc::Sender<()>, Mutex<mpsc::Receiver<()>>), 66 + } 67 + 68 + impl DaemonState { 69 + pub fn new(name: impl Into<String>, responses: mpsc::Sender<TestingResponse>) -> Self { 70 + tokio::time::pause(); 71 + let screen = DaemonScreen::default(); 72 + let (tx, rx) = mpsc::channel(2); 73 + Self { 74 + screen, 75 + responses, 76 + profile: PlayerProfile { 77 + display_name: name.into(), 78 + pfp_base64: None, 79 + }, 80 + updates: (tx, Mutex::new(rx)), 81 + } 82 + } 83 + 84 + async fn change_screen(&mut self, new_screen: DaemonScreen) { 85 + let update = new_screen.as_update(); 86 + self.screen = new_screen; 87 + self.push_resp(update).await; 88 + } 89 + 90 + async fn lobby_loop(&self, handle: StateHandle) { 91 + if let DaemonScreen::Lobby(lobby) = &self.screen { 92 + let lobby = lobby.clone(); 93 + tokio::spawn(async move { 94 + let res = lobby.main_loop().await; 95 + let handle2 = handle.clone(); 96 + let mut state = handle.lock().await; 97 + match res { 98 + Ok(Some(start)) => { 99 + state.start_game(handle2, start).await; 100 + } 101 + Ok(None) => { 102 + state.change_screen(DaemonScreen::PreConnect).await; 103 + } 104 + Err(why) => { 105 + state.push_resp(why).await; 106 + state.change_screen(DaemonScreen::PreConnect).await; 107 + } 108 + } 109 + }); 110 + } 111 + } 112 + 113 + async fn game_loop(&self, handle: StateHandle) { 114 + if let DaemonScreen::Game(game) = &self.screen { 115 + let game = game.clone(); 116 + tokio::spawn(async move { 117 + let res = game.main_loop().await; 118 + let mut state = handle.lock().await; 119 + match res { 120 + Ok(Some(history)) => { 121 + state.push_resp(history).await; 122 + } 123 + Ok(None) => {} 124 + Err(why) => { 125 + state.push_resp(why).await; 126 + } 127 + } 128 + state.change_screen(DaemonScreen::PreConnect).await; 129 + }); 130 + } 131 + } 132 + 133 + async fn push_resp(&self, resp: impl Into<TestingResponse>) { 134 + self.responses 135 + .send(resp.into()) 136 + .await 137 + .expect("Failed to push response"); 138 + } 139 + 140 + fn sender(&self) -> UpdateSender { 141 + UpdateSender(self.updates.0.clone()) 142 + } 143 + 144 + const INTERVAL: Duration = Duration::from_secs(1); 145 + 146 + async fn start_game(&mut self, handle: StateHandle, start: StartGameInfo) { 147 + if let DaemonScreen::Lobby(lobby) = &self.screen { 148 + let transport = lobby.clone_transport(); 149 + let updates = self.sender(); 150 + let location = DummyLocationService; 151 + 152 + let game = Game::new(Self::INTERVAL, start, transport, location, updates); 153 + 154 + self.change_screen(DaemonScreen::Game(Arc::new(game))).await; 155 + self.game_loop(handle).await; 156 + } 157 + } 158 + 159 + pub async fn create_lobby(&mut self, handle: StateHandle, settings: GameSettings) -> Result { 160 + let sender = self.sender(); 161 + 162 + let code = request_room_code() 163 + .await 164 + .context("Failed to get room code")?; 165 + 166 + let lobby = Lobby::new(&code, true, self.profile.clone(), settings, sender) 167 + .await 168 + .context("Failed to start lobby")?; 169 + 170 + self.change_screen(DaemonScreen::Lobby(lobby)).await; 171 + self.lobby_loop(handle).await; 172 + 173 + Ok(()) 174 + } 175 + 176 + pub async fn join_lobby(&mut self, handle: StateHandle, code: &str) -> Result { 177 + let sender = self.sender(); 178 + // TODO: Lobby should not require this on join, use an [Option]? 179 + let settings = GameSettings::default(); 180 + 181 + let lobby = Lobby::new(code, false, self.profile.clone(), settings, sender) 182 + .await 183 + .context("Failed to join lobby")?; 184 + 185 + self.change_screen(DaemonScreen::Lobby(lobby)).await; 186 + self.lobby_loop(handle).await; 187 + 188 + Ok(()) 189 + } 190 + 191 + fn assert_screen(&self, expected: ScreenUpdate) -> Result<(), TestingResponse> { 192 + if self.screen.as_update() == expected { 193 + Ok(()) 194 + } else { 195 + Err(TestingResponse::WrongScreen) 196 + } 197 + } 198 + 199 + async fn process_lobby_req(&mut self, req: LobbyRequest) { 200 + if let DaemonScreen::Lobby(lobby) = &self.screen { 201 + let lobby = lobby.clone(); 202 + match req { 203 + LobbyRequest::SwitchTeams(seeker) => lobby.switch_teams(seeker).await, 204 + LobbyRequest::HostStartGame => lobby.start_game().await, 205 + LobbyRequest::HostUpdateSettings(game_settings) => { 206 + lobby.update_settings(game_settings).await 207 + } 208 + LobbyRequest::Leave => lobby.quit_lobby().await, 209 + } 210 + } 211 + } 212 + 213 + async fn process_game_req(&mut self, req: GameRequest) { 214 + if let DaemonScreen::Game(game) = &self.screen { 215 + let game = game.clone(); 216 + match req { 217 + GameRequest::NextTick => tokio::time::sleep(Self::INTERVAL).await, 218 + GameRequest::MarkCaught => game.mark_caught().await, 219 + GameRequest::GetPowerup => game.get_powerup().await, 220 + GameRequest::UsePowerup => game.use_powerup().await, 221 + GameRequest::ForcePowerup(power_up_type) => { 222 + let mut state = game.lock_state().await; 223 + state.force_set_powerup(power_up_type); 224 + } 225 + GameRequest::Quit => game.quit_game().await, 226 + } 227 + } 228 + } 229 + 230 + pub async fn process_req( 231 + &mut self, 232 + handle: StateHandle, 233 + req: TestingRequest, 234 + ) -> Result<(), TestingResponse> { 235 + match req { 236 + TestingRequest::StartLobby(game_settings) => { 237 + self.assert_screen(ScreenUpdate::PreConnect)?; 238 + self.create_lobby(handle, game_settings).await?; 239 + } 240 + TestingRequest::JoinLobby(code) => { 241 + self.assert_screen(ScreenUpdate::PreConnect)?; 242 + self.join_lobby(handle, &code).await?; 243 + } 244 + TestingRequest::LobbyReq(lobby_request) => { 245 + self.assert_screen(ScreenUpdate::Lobby)?; 246 + self.process_lobby_req(lobby_request).await; 247 + } 248 + TestingRequest::GameReq(game_request) => { 249 + self.assert_screen(ScreenUpdate::Game)?; 250 + self.process_game_req(game_request).await; 251 + } 252 + } 253 + Ok(()) 254 + } 255 + } 256 + 257 + use interprocess::local_socket::{ListenerOptions, tokio::prelude::*}; 258 + 259 + const CLI_MSG: &str = "Usage: manhunt-test-daemon SOCKET_NAME PLAYER_NAME"; 260 + 261 + #[tokio::main(flavor = "current_thread")] 262 + pub async fn main() -> Result { 263 + let args = std::env::args().collect::<Vec<_>>(); 264 + let raw_socket_name = args.get(1).cloned().expect(CLI_MSG); 265 + let player_name = args.get(2).cloned().expect(CLI_MSG); 266 + let socket_name = get_socket_name(raw_socket_name)?; 267 + let opts = ListenerOptions::new().name(socket_name); 268 + let listener = opts.create_tokio().context("Failed to bind to socket")?; 269 + let (resp_tx, mut resp_rx) = mpsc::channel::<TestingResponse>(40); 270 + 271 + let handle = Arc::new(Mutex::new(DaemonState::new(player_name, resp_tx))); 272 + 273 + eprintln!("Testing Daemon Ready"); 274 + 275 + 'server: loop { 276 + let res = tokio::select! { 277 + res = listener.accept() => { 278 + res 279 + }, 280 + Ok(_) = tokio::signal::ctrl_c() => { 281 + break 'server; 282 + } 283 + }; 284 + 285 + match res { 286 + Ok(stream) => { 287 + let mut recv = BufReader::new(&stream); 288 + let mut send = &stream; 289 + 290 + let mut buffer = String::with_capacity(256); 291 + 292 + loop { 293 + tokio::select! { 294 + Ok(_) = tokio::signal::ctrl_c() => { 295 + break 'server; 296 + } 297 + res = recv.read_line(&mut buffer) => { 298 + match res { 299 + Ok(0) => { 300 + break; 301 + } 302 + Ok(_amnt) => { 303 + let req = serde_json::from_str(&buffer).expect("Failed to parse"); 304 + buffer.clear(); 305 + let handle2 = handle.clone(); 306 + let mut state = handle.lock().await; 307 + if let Err(resp) = state.process_req(handle2, req).await { 308 + let encoded = serde_json::to_vec(&resp).expect("Failed to encode"); 309 + send.write_all(&encoded).await.expect("Failed to send"); 310 + } 311 + } 312 + Err(why) => { 313 + eprintln!("Read Error: {why:?}"); 314 + } 315 + } 316 + } 317 + Some(resp) = resp_rx.recv() => { 318 + let encoded = serde_json::to_vec(&resp).expect("Failed to encode"); 319 + send.write_all(&encoded).await.expect("Failed to send"); 320 + } 321 + } 322 + } 323 + } 324 + Err(why) => eprintln!("Error from connection: {why:?}"), 325 + } 326 + } 327 + 328 + Ok(()) 329 + }
+105
manhunt-testing/src/driver.rs
··· 1 + use clap::{Parser, Subcommand, ValueEnum}; 2 + use interprocess::local_socket::{tokio::Stream, traits::tokio::Stream as _}; 3 + use manhunt_logic::PowerUpType; 4 + use manhunt_test_shared::{get_socket_name, prelude::*}; 5 + 6 + #[derive(Parser)] 7 + struct Cli { 8 + /// Path to the UNIX domain socket the test daemon is listening on 9 + socket: String, 10 + 11 + #[command(subcommand)] 12 + command: Commands, 13 + } 14 + 15 + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, ValueEnum)] 16 + enum Role { 17 + Seeker, 18 + Hider, 19 + } 20 + 21 + #[derive(Subcommand)] 22 + enum LobbyCommand { 23 + /// Switch teams between seekers and hiders 24 + SwitchTeams { 25 + /// The role you want to become 26 + #[arg(value_enum)] 27 + role: Role, 28 + }, 29 + /// (Host) Sync game settings to players 30 + SyncSettings, 31 + /// (Host) Start the game for everyone 32 + StartGame, 33 + /// Quit to the main menu 34 + Quit, 35 + } 36 + 37 + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, ValueEnum)] 38 + enum PowerUpTypeValue { 39 + PingSeeker, 40 + PingAllSeekers, 41 + ForcePingOther, 42 + } 43 + 44 + impl From<PowerUpTypeValue> for PowerUpType { 45 + fn from(value: PowerUpTypeValue) -> Self { 46 + match value { 47 + PowerUpTypeValue::PingSeeker => PowerUpType::PingSeeker, 48 + PowerUpTypeValue::PingAllSeekers => PowerUpType::PingAllSeekers, 49 + PowerUpTypeValue::ForcePingOther => PowerUpType::ForcePingOther, 50 + } 51 + } 52 + } 53 + 54 + #[derive(Subcommand)] 55 + enum GameCommand { 56 + /// Mark the local player as caught for everyone 57 + MarkCaught, 58 + /// Get a currently available powerup 59 + GetPowerup, 60 + /// Use the held powerup of the local player 61 + UsePowerup, 62 + /// Force set the held powerup to the given type 63 + ForcePowerup { 64 + #[arg(value_enum)] 65 + ptype: PowerUpTypeValue, 66 + }, 67 + /// Quit the game 68 + Quit, 69 + } 70 + 71 + #[derive(Subcommand)] 72 + enum Commands { 73 + /// Create a lobby 74 + Create, 75 + /// Join a lobby 76 + Join { 77 + /// The join code for the lobby 78 + join_code: String, 79 + }, 80 + /// Execute a command in an active lobby 81 + #[command(subcommand)] 82 + Lobby(LobbyCommand), 83 + /// Execute a command in an active game 84 + #[command(subcommand)] 85 + Game(GameCommand), 86 + } 87 + 88 + #[tokio::main] 89 + async fn main() -> Result { 90 + let cli = Cli::parse(); 91 + 92 + let socket_name = get_socket_name(cli.socket.clone()).context("Failed to get socket name")?; 93 + 94 + let stream = Stream::connect(socket_name) 95 + .await 96 + .context("Failed to connect to socket")?; 97 + 98 + let mut responses = Vec::with_capacity(5); 99 + 100 + loop { 101 + 102 + } 103 + 104 + Ok(()) 105 + }
+126
manhunt-testing/src/lib.rs
··· 1 + use interprocess::local_socket::{GenericNamespaced, Name, ToNsName}; 2 + use manhunt_logic::{GameHistory, GameSettings, GameUiState, LobbyState, PowerUpType}; 3 + use serde::{Deserialize, Serialize}; 4 + 5 + pub mod prelude { 6 + pub use anyhow::{Context, anyhow, bail}; 7 + pub type Result<T = (), E = anyhow::Error> = std::result::Result<T, E>; 8 + } 9 + 10 + pub use prelude::*; 11 + 12 + pub fn get_socket_name(base_name: String) -> Result<Name<'static>> { 13 + base_name 14 + .to_ns_name::<GenericNamespaced>() 15 + .context("Failed to parse socket name") 16 + } 17 + 18 + #[derive(Debug, Clone, Serialize, Deserialize)] 19 + pub enum LobbyRequest { 20 + SwitchTeams(bool), 21 + HostStartGame, 22 + HostUpdateSettings(GameSettings), 23 + Leave, 24 + } 25 + 26 + #[derive(Debug, Clone, Serialize, Deserialize)] 27 + pub enum GameRequest { 28 + NextTick, 29 + MarkCaught, 30 + GetPowerup, 31 + UsePowerup, 32 + ForcePowerup(PowerUpType), 33 + Quit, 34 + } 35 + 36 + #[derive(Debug, Clone, Serialize, Deserialize)] 37 + pub enum TestingRequest { 38 + StartLobby(GameSettings), 39 + JoinLobby(String), 40 + LobbyReq(LobbyRequest), 41 + GameReq(GameRequest), 42 + } 43 + 44 + impl From<LobbyRequest> for TestingRequest { 45 + fn from(val: LobbyRequest) -> Self { 46 + TestingRequest::LobbyReq(val) 47 + } 48 + } 49 + 50 + impl From<GameRequest> for TestingRequest { 51 + fn from(val: GameRequest) -> Self { 52 + TestingRequest::GameReq(val) 53 + } 54 + } 55 + 56 + impl TryInto<LobbyRequest> for TestingRequest { 57 + type Error = TestingResponse; 58 + 59 + fn try_into(self) -> Result<LobbyRequest, Self::Error> { 60 + if let Self::LobbyReq(lr) = self { 61 + Ok(lr) 62 + } else { 63 + Err(TestingResponse::WrongScreen) 64 + } 65 + } 66 + } 67 + 68 + impl TryInto<GameRequest> for TestingRequest { 69 + type Error = TestingResponse; 70 + 71 + fn try_into(self) -> Result<GameRequest, Self::Error> { 72 + if let Self::GameReq(gr) = self { 73 + Ok(gr) 74 + } else { 75 + Err(TestingResponse::WrongScreen) 76 + } 77 + } 78 + } 79 + 80 + #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] 81 + pub enum ScreenUpdate { 82 + PreConnect, 83 + Lobby, 84 + Game, 85 + } 86 + 87 + #[derive(Debug, Clone, Serialize, Deserialize)] 88 + pub enum TestingResponse { 89 + Complete, 90 + ScreenChanged(ScreenUpdate), 91 + LobbyStateUpdate(LobbyState), 92 + GameStateUpdate(GameUiState), 93 + GameOver(GameHistory), 94 + WrongScreen, 95 + Error(String), 96 + } 97 + 98 + impl From<GameHistory> for TestingResponse { 99 + fn from(val: GameHistory) -> Self { 100 + TestingResponse::GameOver(val) 101 + } 102 + } 103 + 104 + impl From<anyhow::Error> for TestingResponse { 105 + fn from(value: anyhow::Error) -> Self { 106 + TestingResponse::Error(value.to_string()) 107 + } 108 + } 109 + 110 + impl From<ScreenUpdate> for TestingResponse { 111 + fn from(val: ScreenUpdate) -> Self { 112 + TestingResponse::ScreenChanged(val) 113 + } 114 + } 115 + 116 + impl From<LobbyState> for TestingResponse { 117 + fn from(val: LobbyState) -> Self { 118 + TestingResponse::LobbyStateUpdate(val) 119 + } 120 + } 121 + 122 + impl From<GameUiState> for TestingResponse { 123 + fn from(val: GameUiState) -> Self { 124 + TestingResponse::GameStateUpdate(val) 125 + } 126 + }