···1515pub use game_state::{GameHistory, GameUiState};
1616pub use lobby::{Lobby, LobbyMessage, LobbyState, StartGameInfo};
1717pub use location::{Location, LocationService};
1818+pub use powerups::PowerUpType;
1819pub use profile::PlayerProfile;
1920pub use settings::GameSettings;
2021pub use transport::{MsgPair, Transport, TransportMessage};
···11+#![allow(clippy::result_large_err)]
22+33+use manhunt_logic::{
44+ Game as BaseGame, GameSettings, Lobby as BaseLobby, Location, LocationService, PlayerProfile,
55+ StartGameInfo, StateUpdateSender,
66+};
77+use manhunt_test_shared::*;
88+use manhunt_transport::{MatchboxTransport, request_room_code};
99+use std::{sync::Arc, time::Duration};
1010+use tokio::{
1111+ io::{AsyncBufReadExt, AsyncWriteExt, BufReader},
1212+ sync::{Mutex, mpsc},
1313+};
1414+1515+struct DummyLocationService;
1616+1717+impl LocationService for DummyLocationService {
1818+ fn get_loc(&self) -> Option<manhunt_logic::Location> {
1919+ Some(Location {
2020+ lat: 0.0,
2121+ long: 0.0,
2222+ heading: None,
2323+ })
2424+ }
2525+}
2626+2727+struct UpdateSender(mpsc::Sender<()>);
2828+2929+impl StateUpdateSender for UpdateSender {
3030+ fn send_update(&self) {
3131+ let tx = self.0.clone();
3232+ tokio::spawn(async move {
3333+ tx.send(()).await.expect("Failed to send");
3434+ });
3535+ }
3636+}
3737+3838+type Game = BaseGame<DummyLocationService, MatchboxTransport, UpdateSender>;
3939+type Lobby = BaseLobby<MatchboxTransport, UpdateSender>;
4040+4141+#[derive(Default)]
4242+enum DaemonScreen {
4343+ #[default]
4444+ PreConnect,
4545+ Lobby(Arc<Lobby>),
4646+ Game(Arc<Game>),
4747+}
4848+4949+impl DaemonScreen {
5050+ pub fn as_update(&self) -> ScreenUpdate {
5151+ match self {
5252+ Self::PreConnect => ScreenUpdate::PreConnect,
5353+ Self::Game(_) => ScreenUpdate::Game,
5454+ Self::Lobby(_) => ScreenUpdate::Lobby,
5555+ }
5656+ }
5757+}
5858+5959+type StateHandle = Arc<Mutex<DaemonState>>;
6060+6161+struct DaemonState {
6262+ screen: DaemonScreen,
6363+ profile: PlayerProfile,
6464+ responses: mpsc::Sender<TestingResponse>,
6565+ updates: (mpsc::Sender<()>, Mutex<mpsc::Receiver<()>>),
6666+}
6767+6868+impl DaemonState {
6969+ pub fn new(name: impl Into<String>, responses: mpsc::Sender<TestingResponse>) -> Self {
7070+ tokio::time::pause();
7171+ let screen = DaemonScreen::default();
7272+ let (tx, rx) = mpsc::channel(2);
7373+ Self {
7474+ screen,
7575+ responses,
7676+ profile: PlayerProfile {
7777+ display_name: name.into(),
7878+ pfp_base64: None,
7979+ },
8080+ updates: (tx, Mutex::new(rx)),
8181+ }
8282+ }
8383+8484+ async fn change_screen(&mut self, new_screen: DaemonScreen) {
8585+ let update = new_screen.as_update();
8686+ self.screen = new_screen;
8787+ self.push_resp(update).await;
8888+ }
8989+9090+ async fn lobby_loop(&self, handle: StateHandle) {
9191+ if let DaemonScreen::Lobby(lobby) = &self.screen {
9292+ let lobby = lobby.clone();
9393+ tokio::spawn(async move {
9494+ let res = lobby.main_loop().await;
9595+ let handle2 = handle.clone();
9696+ let mut state = handle.lock().await;
9797+ match res {
9898+ Ok(Some(start)) => {
9999+ state.start_game(handle2, start).await;
100100+ }
101101+ Ok(None) => {
102102+ state.change_screen(DaemonScreen::PreConnect).await;
103103+ }
104104+ Err(why) => {
105105+ state.push_resp(why).await;
106106+ state.change_screen(DaemonScreen::PreConnect).await;
107107+ }
108108+ }
109109+ });
110110+ }
111111+ }
112112+113113+ async fn game_loop(&self, handle: StateHandle) {
114114+ if let DaemonScreen::Game(game) = &self.screen {
115115+ let game = game.clone();
116116+ tokio::spawn(async move {
117117+ let res = game.main_loop().await;
118118+ let mut state = handle.lock().await;
119119+ match res {
120120+ Ok(Some(history)) => {
121121+ state.push_resp(history).await;
122122+ }
123123+ Ok(None) => {}
124124+ Err(why) => {
125125+ state.push_resp(why).await;
126126+ }
127127+ }
128128+ state.change_screen(DaemonScreen::PreConnect).await;
129129+ });
130130+ }
131131+ }
132132+133133+ async fn push_resp(&self, resp: impl Into<TestingResponse>) {
134134+ self.responses
135135+ .send(resp.into())
136136+ .await
137137+ .expect("Failed to push response");
138138+ }
139139+140140+ fn sender(&self) -> UpdateSender {
141141+ UpdateSender(self.updates.0.clone())
142142+ }
143143+144144+ const INTERVAL: Duration = Duration::from_secs(1);
145145+146146+ async fn start_game(&mut self, handle: StateHandle, start: StartGameInfo) {
147147+ if let DaemonScreen::Lobby(lobby) = &self.screen {
148148+ let transport = lobby.clone_transport();
149149+ let updates = self.sender();
150150+ let location = DummyLocationService;
151151+152152+ let game = Game::new(Self::INTERVAL, start, transport, location, updates);
153153+154154+ self.change_screen(DaemonScreen::Game(Arc::new(game))).await;
155155+ self.game_loop(handle).await;
156156+ }
157157+ }
158158+159159+ pub async fn create_lobby(&mut self, handle: StateHandle, settings: GameSettings) -> Result {
160160+ let sender = self.sender();
161161+162162+ let code = request_room_code()
163163+ .await
164164+ .context("Failed to get room code")?;
165165+166166+ let lobby = Lobby::new(&code, true, self.profile.clone(), settings, sender)
167167+ .await
168168+ .context("Failed to start lobby")?;
169169+170170+ self.change_screen(DaemonScreen::Lobby(lobby)).await;
171171+ self.lobby_loop(handle).await;
172172+173173+ Ok(())
174174+ }
175175+176176+ pub async fn join_lobby(&mut self, handle: StateHandle, code: &str) -> Result {
177177+ let sender = self.sender();
178178+ // TODO: Lobby should not require this on join, use an [Option]?
179179+ let settings = GameSettings::default();
180180+181181+ let lobby = Lobby::new(code, false, self.profile.clone(), settings, sender)
182182+ .await
183183+ .context("Failed to join lobby")?;
184184+185185+ self.change_screen(DaemonScreen::Lobby(lobby)).await;
186186+ self.lobby_loop(handle).await;
187187+188188+ Ok(())
189189+ }
190190+191191+ fn assert_screen(&self, expected: ScreenUpdate) -> Result<(), TestingResponse> {
192192+ if self.screen.as_update() == expected {
193193+ Ok(())
194194+ } else {
195195+ Err(TestingResponse::WrongScreen)
196196+ }
197197+ }
198198+199199+ async fn process_lobby_req(&mut self, req: LobbyRequest) {
200200+ if let DaemonScreen::Lobby(lobby) = &self.screen {
201201+ let lobby = lobby.clone();
202202+ match req {
203203+ LobbyRequest::SwitchTeams(seeker) => lobby.switch_teams(seeker).await,
204204+ LobbyRequest::HostStartGame => lobby.start_game().await,
205205+ LobbyRequest::HostUpdateSettings(game_settings) => {
206206+ lobby.update_settings(game_settings).await
207207+ }
208208+ LobbyRequest::Leave => lobby.quit_lobby().await,
209209+ }
210210+ }
211211+ }
212212+213213+ async fn process_game_req(&mut self, req: GameRequest) {
214214+ if let DaemonScreen::Game(game) = &self.screen {
215215+ let game = game.clone();
216216+ match req {
217217+ GameRequest::NextTick => tokio::time::sleep(Self::INTERVAL).await,
218218+ GameRequest::MarkCaught => game.mark_caught().await,
219219+ GameRequest::GetPowerup => game.get_powerup().await,
220220+ GameRequest::UsePowerup => game.use_powerup().await,
221221+ GameRequest::ForcePowerup(power_up_type) => {
222222+ let mut state = game.lock_state().await;
223223+ state.force_set_powerup(power_up_type);
224224+ }
225225+ GameRequest::Quit => game.quit_game().await,
226226+ }
227227+ }
228228+ }
229229+230230+ pub async fn process_req(
231231+ &mut self,
232232+ handle: StateHandle,
233233+ req: TestingRequest,
234234+ ) -> Result<(), TestingResponse> {
235235+ match req {
236236+ TestingRequest::StartLobby(game_settings) => {
237237+ self.assert_screen(ScreenUpdate::PreConnect)?;
238238+ self.create_lobby(handle, game_settings).await?;
239239+ }
240240+ TestingRequest::JoinLobby(code) => {
241241+ self.assert_screen(ScreenUpdate::PreConnect)?;
242242+ self.join_lobby(handle, &code).await?;
243243+ }
244244+ TestingRequest::LobbyReq(lobby_request) => {
245245+ self.assert_screen(ScreenUpdate::Lobby)?;
246246+ self.process_lobby_req(lobby_request).await;
247247+ }
248248+ TestingRequest::GameReq(game_request) => {
249249+ self.assert_screen(ScreenUpdate::Game)?;
250250+ self.process_game_req(game_request).await;
251251+ }
252252+ }
253253+ Ok(())
254254+ }
255255+}
256256+257257+use interprocess::local_socket::{ListenerOptions, tokio::prelude::*};
258258+259259+const CLI_MSG: &str = "Usage: manhunt-test-daemon SOCKET_NAME PLAYER_NAME";
260260+261261+#[tokio::main(flavor = "current_thread")]
262262+pub async fn main() -> Result {
263263+ let args = std::env::args().collect::<Vec<_>>();
264264+ let raw_socket_name = args.get(1).cloned().expect(CLI_MSG);
265265+ let player_name = args.get(2).cloned().expect(CLI_MSG);
266266+ let socket_name = get_socket_name(raw_socket_name)?;
267267+ let opts = ListenerOptions::new().name(socket_name);
268268+ let listener = opts.create_tokio().context("Failed to bind to socket")?;
269269+ let (resp_tx, mut resp_rx) = mpsc::channel::<TestingResponse>(40);
270270+271271+ let handle = Arc::new(Mutex::new(DaemonState::new(player_name, resp_tx)));
272272+273273+ eprintln!("Testing Daemon Ready");
274274+275275+ 'server: loop {
276276+ let res = tokio::select! {
277277+ res = listener.accept() => {
278278+ res
279279+ },
280280+ Ok(_) = tokio::signal::ctrl_c() => {
281281+ break 'server;
282282+ }
283283+ };
284284+285285+ match res {
286286+ Ok(stream) => {
287287+ let mut recv = BufReader::new(&stream);
288288+ let mut send = &stream;
289289+290290+ let mut buffer = String::with_capacity(256);
291291+292292+ loop {
293293+ tokio::select! {
294294+ Ok(_) = tokio::signal::ctrl_c() => {
295295+ break 'server;
296296+ }
297297+ res = recv.read_line(&mut buffer) => {
298298+ match res {
299299+ Ok(0) => {
300300+ break;
301301+ }
302302+ Ok(_amnt) => {
303303+ let req = serde_json::from_str(&buffer).expect("Failed to parse");
304304+ buffer.clear();
305305+ let handle2 = handle.clone();
306306+ let mut state = handle.lock().await;
307307+ if let Err(resp) = state.process_req(handle2, req).await {
308308+ let encoded = serde_json::to_vec(&resp).expect("Failed to encode");
309309+ send.write_all(&encoded).await.expect("Failed to send");
310310+ }
311311+ }
312312+ Err(why) => {
313313+ eprintln!("Read Error: {why:?}");
314314+ }
315315+ }
316316+ }
317317+ Some(resp) = resp_rx.recv() => {
318318+ let encoded = serde_json::to_vec(&resp).expect("Failed to encode");
319319+ send.write_all(&encoded).await.expect("Failed to send");
320320+ }
321321+ }
322322+ }
323323+ }
324324+ Err(why) => eprintln!("Error from connection: {why:?}"),
325325+ }
326326+ }
327327+328328+ Ok(())
329329+}
+105
manhunt-testing/src/driver.rs
···11+use clap::{Parser, Subcommand, ValueEnum};
22+use interprocess::local_socket::{tokio::Stream, traits::tokio::Stream as _};
33+use manhunt_logic::PowerUpType;
44+use manhunt_test_shared::{get_socket_name, prelude::*};
55+66+#[derive(Parser)]
77+struct Cli {
88+ /// Path to the UNIX domain socket the test daemon is listening on
99+ socket: String,
1010+1111+ #[command(subcommand)]
1212+ command: Commands,
1313+}
1414+1515+#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
1616+enum Role {
1717+ Seeker,
1818+ Hider,
1919+}
2020+2121+#[derive(Subcommand)]
2222+enum LobbyCommand {
2323+ /// Switch teams between seekers and hiders
2424+ SwitchTeams {
2525+ /// The role you want to become
2626+ #[arg(value_enum)]
2727+ role: Role,
2828+ },
2929+ /// (Host) Sync game settings to players
3030+ SyncSettings,
3131+ /// (Host) Start the game for everyone
3232+ StartGame,
3333+ /// Quit to the main menu
3434+ Quit,
3535+}
3636+3737+#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
3838+enum PowerUpTypeValue {
3939+ PingSeeker,
4040+ PingAllSeekers,
4141+ ForcePingOther,
4242+}
4343+4444+impl From<PowerUpTypeValue> for PowerUpType {
4545+ fn from(value: PowerUpTypeValue) -> Self {
4646+ match value {
4747+ PowerUpTypeValue::PingSeeker => PowerUpType::PingSeeker,
4848+ PowerUpTypeValue::PingAllSeekers => PowerUpType::PingAllSeekers,
4949+ PowerUpTypeValue::ForcePingOther => PowerUpType::ForcePingOther,
5050+ }
5151+ }
5252+}
5353+5454+#[derive(Subcommand)]
5555+enum GameCommand {
5656+ /// Mark the local player as caught for everyone
5757+ MarkCaught,
5858+ /// Get a currently available powerup
5959+ GetPowerup,
6060+ /// Use the held powerup of the local player
6161+ UsePowerup,
6262+ /// Force set the held powerup to the given type
6363+ ForcePowerup {
6464+ #[arg(value_enum)]
6565+ ptype: PowerUpTypeValue,
6666+ },
6767+ /// Quit the game
6868+ Quit,
6969+}
7070+7171+#[derive(Subcommand)]
7272+enum Commands {
7373+ /// Create a lobby
7474+ Create,
7575+ /// Join a lobby
7676+ Join {
7777+ /// The join code for the lobby
7878+ join_code: String,
7979+ },
8080+ /// Execute a command in an active lobby
8181+ #[command(subcommand)]
8282+ Lobby(LobbyCommand),
8383+ /// Execute a command in an active game
8484+ #[command(subcommand)]
8585+ Game(GameCommand),
8686+}
8787+8888+#[tokio::main]
8989+async fn main() -> Result {
9090+ let cli = Cli::parse();
9191+9292+ let socket_name = get_socket_name(cli.socket.clone()).context("Failed to get socket name")?;
9393+9494+ let stream = Stream::connect(socket_name)
9595+ .await
9696+ .context("Failed to connect to socket")?;
9797+9898+ let mut responses = Vec::with_capacity(5);
9999+100100+ loop {
101101+102102+ }
103103+104104+ Ok(())
105105+}