diff --git a/sea_battle_backend/src/dispatcher_actor.rs b/sea_battle_backend/src/dispatcher_actor.rs index 95a4b58..790d501 100644 --- a/sea_battle_backend/src/dispatcher_actor.rs +++ b/sea_battle_backend/src/dispatcher_actor.rs @@ -24,6 +24,11 @@ pub struct CreateInvite(pub GameRules, pub Addr); #[rtype(result = "()")] pub struct AcceptInvite(pub String, pub Addr); +#[derive(Message)] +#[rtype(result = "()")] +pub struct PlayRandom(pub Addr); + +#[derive(Debug, Clone)] struct PendingPlayer { player: Addr, rules: GameRules, @@ -32,6 +37,32 @@ struct PendingPlayer { #[derive(Default)] pub struct DispatcherActor { with_invite: HashMap, + random_player: Option, +} + +impl DispatcherActor { + /// Run garbage collector + fn run_gc(&mut self) { + // Garbage collect invites + let ids = self + .with_invite + .iter() + .filter(|p| !p.1.player.connected()) + .map(|p| p.0.clone()) + .collect::>(); + + for id in ids { + log::debug!("Remove dead invite: {}", id); + self.with_invite.remove(&id); + } + + // Garbage collect random player + if let Some(player) = self.random_player.clone() { + if !player.player.connected() { + self.random_player = None; + } + } + } } impl Actor for DispatcherActor { @@ -39,18 +70,7 @@ impl Actor for DispatcherActor { fn started(&mut self, ctx: &mut Self::Context) { ctx.run_interval(HEARTBEAT_INTERVAL, |act, _ctx| { - // Garbage collect invites - let ids = act - .with_invite - .iter() - .filter(|p| !p.1.player.connected()) - .map(|p| p.0.clone()) - .collect::>(); - - for id in ids { - log::debug!("Remove dead invite: {}", id); - act.with_invite.remove(&id); - } + act.run_gc(); }); } } @@ -84,6 +104,8 @@ impl Handler for DispatcherActor { type Result = (); fn handle(&mut self, msg: AcceptInvite, _ctx: &mut Self::Context) -> Self::Result { + self.run_gc(); + let entry = match self.with_invite.remove(&msg.0) { None => { msg.1.do_send(ServerMessage::InvalidInviteCode); @@ -98,3 +120,27 @@ impl Handler for DispatcherActor { msg.1.do_send(SetGame(game)); } } + +impl Handler for DispatcherActor { + type Result = (); + + fn handle(&mut self, msg: PlayRandom, _ctx: &mut Self::Context) -> Self::Result { + self.run_gc(); + + match self.random_player.take() { + None => { + self.random_player = Some(PendingPlayer { + player: msg.0, + rules: GameRules::random_players_rules(), + }); + } + Some(p) => { + let game = Game::new(p.rules).start(); + p.player.do_send(SetGame(game.clone())); + msg.0.do_send(SetGame(game)); + + self.random_player = None; + } + } + } +} diff --git a/sea_battle_backend/src/human_player_ws.rs b/sea_battle_backend/src/human_player_ws.rs index 2f44c73..898e3b4 100644 --- a/sea_battle_backend/src/human_player_ws.rs +++ b/sea_battle_backend/src/human_player_ws.rs @@ -9,7 +9,7 @@ use uuid::Uuid; use crate::bot_player::BotPlayer; use crate::data::{BoatsLayout, Coordinates, CurrentGameStatus, FireResult, GameRules}; -use crate::dispatcher_actor::{AcceptInvite, CreateInvite, DispatcherActor}; +use crate::dispatcher_actor::{AcceptInvite, CreateInvite, DispatcherActor, PlayRandom}; use crate::game::{AddPlayer, Game}; use crate::human_player::HumanPlayer; @@ -23,7 +23,7 @@ pub enum StartMode { Bot(GameRules), CreateInvite(GameRules), AcceptInvite { code: String }, - // TODO : random human + PlayRandom, } #[derive(serde::Deserialize, serde::Serialize, Debug)] @@ -178,6 +178,10 @@ impl Actor for HumanPlayerWS { self.dispatcher .do_send(AcceptInvite(code.clone(), ctx.address())); } + StartMode::PlayRandom => { + log::info!("Start random play"); + self.dispatcher.do_send(PlayRandom(ctx.address())) + } } } diff --git a/sea_battle_backend/src/server.rs b/sea_battle_backend/src/server.rs index 66447da..e3ac334 100644 --- a/sea_battle_backend/src/server.rs +++ b/sea_battle_backend/src/server.rs @@ -112,6 +112,24 @@ async fn start_accept_invite( resp } +#[derive(serde::Serialize, serde::Deserialize)] +pub struct PlayRandomQuery { + pub player_name: String, +} + +/// Start game, playing against a random person +async fn start_random( + req: HttpRequest, + stream: web::Payload, + query: web::Query, + dispatcher: web::Data>, +) -> Result { + let player_ws = HumanPlayerWS::new(StartMode::PlayRandom, &dispatcher, query.0.player_name); + + let resp = ws::start(player_ws, &req, stream); + log::info!("New random play"); + resp +} pub async fn start_server(args: Args) -> std::io::Result<()> { let args_clone = args.clone(); @@ -132,7 +150,7 @@ pub async fn start_server(args: Args) -> std::io::Result<()> { .route("/play/bot", web::get().to(start_bot_play)) .route("/play/create_invite", web::get().to(start_create_invite)) .route("/play/accept_invite", web::get().to(start_accept_invite)) - // TODO : join random + .route("/play/random", web::get().to(start_random)) .route("/", web::get().to(index)) .route("{tail:.*}", web::get().to(not_found)) }) diff --git a/sea_battle_backend/src/test/bot_client.rs b/sea_battle_backend/src/test/bot_client.rs index 99fb05a..e01402f 100644 --- a/sea_battle_backend/src/test/bot_client.rs +++ b/sea_battle_backend/src/test/bot_client.rs @@ -6,7 +6,7 @@ use tokio_tungstenite::tungstenite::Message; use crate::data::{BoatsLayout, BotType, GameRules}; use crate::human_player_ws::{ClientMessage, ServerMessage}; -use crate::server::{AcceptInviteQuery, BotPlayQuery, CreateInviteQuery}; +use crate::server::{AcceptInviteQuery, BotPlayQuery, CreateInviteQuery, PlayRandomQuery}; const PLAYER_NAME: &str = "Bot client"; @@ -18,6 +18,7 @@ pub enum RunMode { AcceptInvite { code: String, }, + Random, } #[derive(Debug, Eq, PartialEq)] @@ -130,6 +131,16 @@ impl BotClient { .unwrap() ) } + RunMode::Random => { + format!( + "{}/play/random?{}", + self.server.replace("http", "ws"), + serde_urlencoded::to_string(&PlayRandomQuery { + player_name: PLAYER_NAME.to_string() + }) + .unwrap() + ) + } }; log::debug!("Connecting to {}...", url); let (mut socket, _) = match tokio_tungstenite::connect_async(url).await { diff --git a/sea_battle_backend/src/test/bot_client_bot_intermediate_play.rs b/sea_battle_backend/src/test/bot_intermediate_play.rs similarity index 100% rename from sea_battle_backend/src/test/bot_client_bot_intermediate_play.rs rename to sea_battle_backend/src/test/bot_intermediate_play.rs diff --git a/sea_battle_backend/src/test/bot_client_bot_linear_play.rs b/sea_battle_backend/src/test/bot_linear_play.rs similarity index 100% rename from sea_battle_backend/src/test/bot_client_bot_linear_play.rs rename to sea_battle_backend/src/test/bot_linear_play.rs diff --git a/sea_battle_backend/src/test/bot_client_bot_random_play.rs b/sea_battle_backend/src/test/bot_random_play.rs similarity index 100% rename from sea_battle_backend/src/test/bot_client_bot_random_play.rs rename to sea_battle_backend/src/test/bot_random_play.rs diff --git a/sea_battle_backend/src/test/bot_client_bot_smart_play.rs b/sea_battle_backend/src/test/bot_smart_play.rs similarity index 100% rename from sea_battle_backend/src/test/bot_client_bot_smart_play.rs rename to sea_battle_backend/src/test/bot_smart_play.rs diff --git a/sea_battle_backend/src/test/mod.rs b/sea_battle_backend/src/test/mod.rs index 4b934a3..6282283 100644 --- a/sea_battle_backend/src/test/mod.rs +++ b/sea_battle_backend/src/test/mod.rs @@ -17,6 +17,7 @@ enum TestPort { InviteModeFullGame, InviteModeFirstPlayerWin, InviteModeSecondPlayerWin, + RandomModeFourGames, } impl TestPort { @@ -40,10 +41,11 @@ impl Args { #[cfg(test)] pub mod bot_client; -mod bot_client_bot_intermediate_play; -mod bot_client_bot_linear_play; -mod bot_client_bot_random_play; -mod bot_client_bot_smart_play; +mod bot_intermediate_play; +mod bot_linear_play; +mod bot_random_play; +mod bot_smart_play; mod invite_mode; mod network_utils; mod play_utils; +mod random_mode; diff --git a/sea_battle_backend/src/test/random_mode.rs b/sea_battle_backend/src/test/random_mode.rs new file mode 100644 index 0000000..f0a98a9 --- /dev/null +++ b/sea_battle_backend/src/test/random_mode.rs @@ -0,0 +1,39 @@ +use crate::args::Args; +use crate::server::start_server; +use crate::test::bot_client::{ClientEndResult, RunMode}; +use crate::test::network_utils::wait_for_port; +use crate::test::{bot_client, TestPort}; +use std::error::Error; +use tokio::task; + +async fn play_random(port: TestPort) -> Result> { + bot_client::BotClient::new(port.as_url()) + .with_run_mode(RunMode::Random) + .run_client() + .await +} + +#[tokio::test] +async fn four_games() { + let _ = env_logger::builder().is_test(true).try_init(); + + let local_set = task::LocalSet::new(); + local_set + .run_until(async move { + task::spawn_local(start_server(Args::for_test(TestPort::RandomModeFourGames))); + wait_for_port(TestPort::RandomModeFourGames.port()).await; + + let mut fut = vec![]; + for _ in 0..4 { + fut.push(task::spawn_local(play_random( + TestPort::RandomModeFourGames, + ))); + } + + for handle in fut { + let res = handle.await.unwrap().unwrap(); + assert!(matches!(res, ClientEndResult::Finished { .. })); + } + }) + .await; +}