From dfdf6d9952bdcea61495f04048bed02b3e31c3d5 Mon Sep 17 00:00:00 2001 From: Pierre Hubert Date: Sat, 24 Sep 2022 12:12:40 +0200 Subject: [PATCH] Add new test on invite mode --- .../intermediate_bot.rs => bot_player.rs} | 21 +-- sea_battle_backend/src/bots/linear_bot.rs | 87 ------------ sea_battle_backend/src/bots/mod.rs | 4 - sea_battle_backend/src/bots/random_bot.rs | 87 ------------ sea_battle_backend/src/bots/smart_bot.rs | 97 ------------- .../src/data/current_game_status.rs | 28 +++- sea_battle_backend/src/game.rs | 5 +- sea_battle_backend/src/human_player_ws.rs | 25 +--- sea_battle_backend/src/lib.rs | 2 +- sea_battle_backend/src/test/bot_client.rs | 18 ++- sea_battle_backend/src/test/invite_mode.rs | 131 ++++++++++++++++++ sea_battle_backend/src/test/mod.rs | 2 + sea_battle_backend/src/utils.rs | 20 +-- 13 files changed, 208 insertions(+), 319 deletions(-) rename sea_battle_backend/src/{bots/intermediate_bot.rs => bot_player.rs} (81%) delete mode 100644 sea_battle_backend/src/bots/linear_bot.rs delete mode 100644 sea_battle_backend/src/bots/mod.rs delete mode 100644 sea_battle_backend/src/bots/random_bot.rs delete mode 100644 sea_battle_backend/src/bots/smart_bot.rs diff --git a/sea_battle_backend/src/bots/intermediate_bot.rs b/sea_battle_backend/src/bot_player.rs similarity index 81% rename from sea_battle_backend/src/bots/intermediate_bot.rs rename to sea_battle_backend/src/bot_player.rs index 18eb2e6..f03b4f1 100644 --- a/sea_battle_backend/src/bots/intermediate_bot.rs +++ b/sea_battle_backend/src/bot_player.rs @@ -1,25 +1,27 @@ use actix::Addr; use uuid::Uuid; -use crate::data::{BoatsLayout, Coordinates, CurrentGameStatus, FireResult, GameRules}; +use crate::data::{BoatsLayout, BotType, Coordinates, CurrentGameStatus, FireResult, GameRules}; use crate::game::{Fire, Game, Player, RespondRequestRematch, SetBoatsLayout}; #[derive(Clone)] -pub struct IntermediateBot { +pub struct BotPlayer { game: Addr, + kind: BotType, uuid: Uuid, } -impl IntermediateBot { - pub fn new(game: Addr) -> Self { +impl BotPlayer { + pub fn new(kind: BotType, game: Addr) -> Self { Self { game, + kind, uuid: Uuid::new_v4(), } } } -impl Player for IntermediateBot { +impl Player for BotPlayer { fn get_name(&self) -> &str { "Intermediate Bot" } @@ -54,11 +56,10 @@ impl Player for IntermediateBot { fn notify_game_starting(&self) {} fn request_fire(&self, status: CurrentGameStatus) { - let coordinates = status - .continue_attack_boat() - .unwrap_or_else(|| status.find_valid_random_fire_location()); - - self.game.do_send(Fire(self.uuid, coordinates)); + self.game.do_send(Fire( + self.uuid, + status.find_fire_coordinates_for_bot_type(self.kind), + )); } fn opponent_must_fire(&self, _status: CurrentGameStatus) {} diff --git a/sea_battle_backend/src/bots/linear_bot.rs b/sea_battle_backend/src/bots/linear_bot.rs deleted file mode 100644 index 8c3fbf7..0000000 --- a/sea_battle_backend/src/bots/linear_bot.rs +++ /dev/null @@ -1,87 +0,0 @@ -use actix::Addr; -use uuid::Uuid; - -use crate::data::{BoatsLayout, Coordinates, CurrentGameStatus, FireResult, GameRules}; -use crate::game::{Fire, Game, Player, RespondRequestRematch, SetBoatsLayout}; - -#[derive(Clone)] -pub struct LinearBot { - game: Addr, - uuid: Uuid, -} - -impl LinearBot { - pub fn new(game: Addr) -> Self { - Self { - game, - uuid: Uuid::new_v4(), - } - } -} - -impl Player for LinearBot { - fn get_name(&self) -> &str { - "Linear Bot" - } - - fn get_uid(&self) -> Uuid { - self.uuid - } - - fn is_bot(&self) -> bool { - true - } - - fn set_other_player_name(&self, _name: &str) {} - - fn query_boats_layout(&self, rules: &GameRules) { - match BoatsLayout::gen_random_for_rules(rules) { - Ok(layout) => self.game.do_send(SetBoatsLayout(self.uuid, layout)), - - Err(e) => log::error!( - "Failed to use game rules to construct boats layout: {:?}", - e - ), - } - } - - fn rejected_boats_layout(&self, _errors: Vec<&'static str>) { - unreachable!() - } - - fn notify_other_player_ready(&self) {} - - fn notify_game_starting(&self) {} - - fn request_fire(&self, status: CurrentGameStatus) { - self.game - .do_send(Fire(self.uuid, status.find_first_valid_fire_location())); - } - - fn opponent_must_fire(&self, _status: CurrentGameStatus) {} - - fn strike_result(&self, _c: Coordinates, _res: FireResult) {} - - fn other_player_strike_result(&self, _c: Coordinates, _res: FireResult) {} - - fn lost_game(&self, _status: CurrentGameStatus) {} - - fn won_game(&self, _status: CurrentGameStatus) {} - - fn opponent_requested_rematch(&self) { - self.game.do_send(RespondRequestRematch(self.uuid, true)); - } - - fn opponent_rejected_rematch(&self) {} - - fn opponent_accepted_rematch(&self) {} - - fn opponent_left_game(&self) { - // Human are not reliable lol - } - - fn opponent_replaced_by_bot(&self) { - // Not such a good idea. will panic, just in case - panic!("Bot shall not play against each other (it is completely useless)"); - } -} diff --git a/sea_battle_backend/src/bots/mod.rs b/sea_battle_backend/src/bots/mod.rs deleted file mode 100644 index 361f62a..0000000 --- a/sea_battle_backend/src/bots/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub mod intermediate_bot; -pub mod linear_bot; -pub mod random_bot; -pub mod smart_bot; diff --git a/sea_battle_backend/src/bots/random_bot.rs b/sea_battle_backend/src/bots/random_bot.rs deleted file mode 100644 index dd7aab2..0000000 --- a/sea_battle_backend/src/bots/random_bot.rs +++ /dev/null @@ -1,87 +0,0 @@ -use actix::Addr; -use uuid::Uuid; - -use crate::data::{BoatsLayout, Coordinates, CurrentGameStatus, FireResult, GameRules}; -use crate::game::{Fire, Game, Player, RespondRequestRematch, SetBoatsLayout}; - -#[derive(Clone)] -pub struct RandomBot { - game: Addr, - uuid: Uuid, -} - -impl RandomBot { - pub fn new(game: Addr) -> Self { - Self { - game, - uuid: Uuid::new_v4(), - } - } -} - -impl Player for RandomBot { - fn get_name(&self) -> &str { - "Random Bot" - } - - fn get_uid(&self) -> Uuid { - self.uuid - } - - fn is_bot(&self) -> bool { - true - } - - fn set_other_player_name(&self, _name: &str) {} - - fn query_boats_layout(&self, rules: &GameRules) { - match BoatsLayout::gen_random_for_rules(rules) { - Ok(layout) => self.game.do_send(SetBoatsLayout(self.uuid, layout)), - - Err(e) => log::error!( - "Failed to use game rules to construct boats layout: {:?}", - e - ), - } - } - - fn rejected_boats_layout(&self, _errors: Vec<&'static str>) { - unreachable!() - } - - fn notify_other_player_ready(&self) {} - - fn notify_game_starting(&self) {} - - fn request_fire(&self, status: CurrentGameStatus) { - self.game - .do_send(Fire(self.uuid, status.find_valid_random_fire_location())); - } - - fn opponent_must_fire(&self, _status: CurrentGameStatus) {} - - fn strike_result(&self, _c: Coordinates, _res: FireResult) {} - - fn other_player_strike_result(&self, _c: Coordinates, _res: FireResult) {} - - fn lost_game(&self, _status: CurrentGameStatus) {} - - fn won_game(&self, _status: CurrentGameStatus) {} - - fn opponent_requested_rematch(&self) { - self.game.do_send(RespondRequestRematch(self.uuid, true)); - } - - fn opponent_rejected_rematch(&self) {} - - fn opponent_accepted_rematch(&self) {} - - fn opponent_left_game(&self) { - // Human are not reliable lol - } - - fn opponent_replaced_by_bot(&self) { - // Not such a good idea. will panic, just in case - panic!("Bot shall not play against each other (it is completely useless)"); - } -} diff --git a/sea_battle_backend/src/bots/smart_bot.rs b/sea_battle_backend/src/bots/smart_bot.rs deleted file mode 100644 index a2b81f6..0000000 --- a/sea_battle_backend/src/bots/smart_bot.rs +++ /dev/null @@ -1,97 +0,0 @@ -use actix::Addr; -use rand::RngCore; -use uuid::Uuid; - -use crate::data::{BoatsLayout, Coordinates, CurrentGameStatus, FireResult, GameRules}; -use crate::game::{Fire, Game, Player, RespondRequestRematch, SetBoatsLayout}; - -#[derive(Clone)] -pub struct SmartBot { - game: Addr, - uuid: Uuid, -} - -impl SmartBot { - pub fn new(game: Addr) -> Self { - Self { - game, - uuid: Uuid::new_v4(), - } - } -} - -impl Player for SmartBot { - fn get_name(&self) -> &str { - "Intermediate Bot" - } - - fn get_uid(&self) -> Uuid { - self.uuid - } - - fn is_bot(&self) -> bool { - true - } - - fn set_other_player_name(&self, _name: &str) {} - - fn query_boats_layout(&self, rules: &GameRules) { - match BoatsLayout::gen_random_for_rules(rules) { - Ok(layout) => self.game.do_send(SetBoatsLayout(self.uuid, layout)), - - Err(e) => log::error!( - "Failed to use game rules to construct boats layout: {:?}", - e - ), - } - } - - fn rejected_boats_layout(&self, _errors: Vec<&'static str>) { - unreachable!() - } - - fn notify_other_player_ready(&self) {} - - fn notify_game_starting(&self) {} - - fn request_fire(&self, status: CurrentGameStatus) { - let coordinates = status.continue_attack_boat().unwrap_or_else(|| { - let coordinates = status.get_relevant_grid_locations(); - if !coordinates.is_empty() { - let pos = rand::thread_rng().next_u32() as usize; - coordinates[pos % coordinates.len()] - } else { - status.find_valid_random_fire_location() - } - }); - - self.game.do_send(Fire(self.uuid, coordinates)); - } - - fn opponent_must_fire(&self, _status: CurrentGameStatus) {} - - fn strike_result(&self, _c: Coordinates, _res: FireResult) {} - - fn other_player_strike_result(&self, _c: Coordinates, _res: FireResult) {} - - fn lost_game(&self, _status: CurrentGameStatus) {} - - fn won_game(&self, _status: CurrentGameStatus) {} - - fn opponent_requested_rematch(&self) { - self.game.do_send(RespondRequestRematch(self.uuid, true)); - } - - fn opponent_rejected_rematch(&self) {} - - fn opponent_accepted_rematch(&self) {} - - fn opponent_left_game(&self) { - // Human are not reliable lol - } - - fn opponent_replaced_by_bot(&self) { - // Not such a good idea. will panic, just in case - panic!("Bot shall not play against each other (it is completely useless)"); - } -} diff --git a/sea_battle_backend/src/data/current_game_status.rs b/sea_battle_backend/src/data/current_game_status.rs index ec76872..f424b7f 100644 --- a/sea_battle_backend/src/data/current_game_status.rs +++ b/sea_battle_backend/src/data/current_game_status.rs @@ -1,7 +1,7 @@ use rand::RngCore; use crate::data::{ - BoatPosition, BoatsLayout, Coordinates, GameRules, MapCellContent, PrintableMap, + BoatPosition, BoatsLayout, BotType, Coordinates, GameRules, MapCellContent, PrintableMap, }; #[derive(Debug, serde::Serialize, serde::Deserialize, Clone, Default)] @@ -274,6 +274,32 @@ impl CurrentGameStatus { pub fn print_opponent_map(&self) { print!("{}", self.get_opponent_map()); } + + pub fn find_intermediate_bot_fire_location(&self) -> Coordinates { + self.continue_attack_boat() + .unwrap_or_else(|| self.find_valid_random_fire_location()) + } + + pub fn find_smart_bot_fire_location(&self) -> Coordinates { + self.continue_attack_boat().unwrap_or_else(|| { + let coordinates = self.get_relevant_grid_locations(); + if !coordinates.is_empty() { + let pos = rand::thread_rng().next_u32() as usize; + coordinates[pos % coordinates.len()] + } else { + self.find_valid_random_fire_location() + } + }) + } + + pub fn find_fire_coordinates_for_bot_type(&self, t: BotType) -> Coordinates { + match t { + BotType::Random => self.find_valid_random_fire_location(), + BotType::Linear => self.find_first_valid_fire_location(), + BotType::Intermediate => self.find_intermediate_bot_fire_location(), + BotType::Smart => self.find_smart_bot_fire_location(), + } + } } #[cfg(test)] diff --git a/sea_battle_backend/src/game.rs b/sea_battle_backend/src/game.rs index 277c65e..15f2519 100644 --- a/sea_battle_backend/src/game.rs +++ b/sea_battle_backend/src/game.rs @@ -4,7 +4,7 @@ use actix::prelude::*; use actix::{Actor, Context, Handler}; use uuid::Uuid; -use crate::bots::random_bot::RandomBot; +use crate::bot_player::BotPlayer; use crate::data::*; pub trait Player { @@ -362,7 +362,8 @@ impl Handler for Game { ctx.stop(); } else { // Replace the player with a bot - self.players[offline_player] = Arc::new(RandomBot::new(ctx.address())); + self.players[offline_player] = + Arc::new(BotPlayer::new(self.rules.bot_type, ctx.address())); self.players[opponent(offline_player)].opponent_replaced_by_bot(); if self.turn == offline_player { diff --git a/sea_battle_backend/src/human_player_ws.rs b/sea_battle_backend/src/human_player_ws.rs index 18b126f..2f44c73 100644 --- a/sea_battle_backend/src/human_player_ws.rs +++ b/sea_battle_backend/src/human_player_ws.rs @@ -7,11 +7,8 @@ use actix_web_actors::ws; use actix_web_actors::ws::{CloseCode, CloseReason, Message, ProtocolError, WebsocketContext}; use uuid::Uuid; -use crate::bots::intermediate_bot::IntermediateBot; -use crate::bots::linear_bot::LinearBot; -use crate::bots::random_bot::RandomBot; -use crate::bots::smart_bot::SmartBot; -use crate::data::{BoatsLayout, BotType, Coordinates, CurrentGameStatus, FireResult, GameRules}; +use crate::bot_player::BotPlayer; +use crate::data::{BoatsLayout, Coordinates, CurrentGameStatus, FireResult, GameRules}; use crate::dispatcher_actor::{AcceptInvite, CreateInvite, DispatcherActor}; use crate::game::{AddPlayer, Game}; use crate::human_player::HumanPlayer; @@ -156,20 +153,10 @@ impl Actor for HumanPlayerWS { log::debug!("Start play with a bot"); let game = Game::new(rules.clone()).start(); - match rules.bot_type { - BotType::Random => { - game.do_send(AddPlayer(Arc::new(RandomBot::new(game.clone())))); - } - BotType::Linear => { - game.do_send(AddPlayer(Arc::new(LinearBot::new(game.clone())))); - } - BotType::Intermediate => { - game.do_send(AddPlayer(Arc::new(IntermediateBot::new(game.clone())))); - } - BotType::Smart => { - game.do_send(AddPlayer(Arc::new(SmartBot::new(game.clone())))); - } - }; + game.do_send(AddPlayer(Arc::new(BotPlayer::new( + rules.bot_type, + game.clone(), + )))); let player = Arc::new(HumanPlayer { name: self.name.to_string(), diff --git a/sea_battle_backend/src/lib.rs b/sea_battle_backend/src/lib.rs index 32a9c10..7f95ac1 100644 --- a/sea_battle_backend/src/lib.rs +++ b/sea_battle_backend/src/lib.rs @@ -1,7 +1,7 @@ extern crate core; pub mod args; -pub mod bots; +pub mod bot_player; pub mod consts; pub mod data; pub mod dispatcher_actor; diff --git a/sea_battle_backend/src/test/bot_client.rs b/sea_battle_backend/src/test/bot_client.rs index 95b2936..a22d75a 100644 --- a/sea_battle_backend/src/test/bot_client.rs +++ b/sea_battle_backend/src/test/bot_client.rs @@ -4,7 +4,7 @@ use std::fmt::Display; use futures::{SinkExt, StreamExt}; use tokio_tungstenite::tungstenite::Message; -use crate::data::{BoatsLayout, GameRules}; +use crate::data::{BoatsLayout, BotType, GameRules}; use crate::human_player_ws::{ClientMessage, ServerMessage}; use crate::server::AcceptInviteQuery; @@ -37,6 +37,7 @@ pub struct BotClient { layout: Option, number_plays: usize, server_msg_callback: Option>, + play_as_bot_type: BotType, } impl BotClient { @@ -51,6 +52,7 @@ impl BotClient { layout: None, number_plays: 1, server_msg_callback: None, + play_as_bot_type: BotType::Random, } } @@ -82,6 +84,11 @@ impl BotClient { self } + pub fn with_play_as_bot_type(mut self, t: BotType) -> Self { + self.play_as_bot_type = t; + self + } + pub async fn run_client(&mut self) -> Result> { let mut remaining_games = self.number_plays; let mut number_victories = 0; @@ -196,7 +203,7 @@ impl BotClient { ServerMessage::RequestFire { status } => { assert_eq!(status.opponent_map.boats.number_of_boats(), 0); - let location = status.find_valid_random_fire_location(); + let location = status.find_fire_coordinates_for_bot_type(self.play_as_bot_type); log::debug!("Will fire at {:?}", location); socket .send(Message::Text(serde_json::to_string( @@ -253,7 +260,12 @@ impl BotClient { ServerMessage::SetOpponentName { name } => log::debug!("Opponent name: {}", name), ServerMessage::OpponentRequestedRematch => { - log::debug!("Opponent rejected rematch."); + log::debug!("Opponent requested rematch."); + socket + .send(Message::Text(serde_json::to_string( + &ClientMessage::AcceptRematch, + )?)) + .await?; } ServerMessage::OpponentAcceptedRematch => { log::debug!("Opponent accepted rematch"); diff --git a/sea_battle_backend/src/test/invite_mode.rs b/sea_battle_backend/src/test/invite_mode.rs index ab053b7..3cdf55a 100644 --- a/sea_battle_backend/src/test/invite_mode.rs +++ b/sea_battle_backend/src/test/invite_mode.rs @@ -5,6 +5,7 @@ use tokio::sync::mpsc::Sender; use tokio::task; use crate::args::Args; +use crate::data::BotType; use crate::human_player_ws::ServerMessage; use crate::server::start_server; use crate::test::bot_client::{ClientEndResult, RunMode}; @@ -40,9 +41,13 @@ async fn run_other_invite_side( sender: Sender>>, port: TestPort, code: String, + play_as_bot_type: BotType, + number_plays: usize, ) { let res = bot_client::BotClient::new(port.as_url()) .with_run_mode(RunMode::AcceptInvite { code }) + .with_play_as_bot_type(play_as_bot_type) + .with_number_plays(number_plays) .run_client() .await; @@ -69,6 +74,8 @@ async fn full_game() { sender.clone(), TestPort::InviteModeFullGame, code.clone(), + BotType::Random, + 1, )); } }) @@ -82,3 +89,127 @@ async fn full_game() { }) .await; } + +#[tokio::test] +async fn first_player_win() { + 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::InviteModeFirstPlayerWin, + ))); + wait_for_port(TestPort::InviteModeFirstPlayerWin.port()).await; + + let (sender, mut receiver) = mpsc::channel(1); + + let res = bot_client::BotClient::new(TestPort::InviteModeFirstPlayerWin.as_url()) + .with_run_mode(RunMode::CreateInvite) + .with_play_as_bot_type(BotType::Smart) + .with_number_plays(10) + .with_server_msg_callback(move |msg| { + if let ServerMessage::SetInviteCode { code } = msg { + task::spawn_local(run_other_invite_side( + sender.clone(), + TestPort::InviteModeFirstPlayerWin, + code.clone(), + BotType::Linear, + 10, + )); + } + }) + .run_client() + .await + .unwrap(); + + let other_side_res = receiver.recv().await.unwrap().unwrap(); + + assert!(matches!(res, ClientEndResult::Finished { .. })); + + assert!(matches!(other_side_res, ClientEndResult::Finished { .. })); + + match (res, other_side_res) { + ( + ClientEndResult::Finished { + number_defeats: d1, + number_victories: v1, + }, + ClientEndResult::Finished { + number_defeats: d2, + number_victories: v2, + }, + ) => { + assert_eq!(d1, v2); + assert_eq!(v1, d2); + + assert!(v1 > 6); + } + + (_, _) => unreachable!(), + } + }) + .await; +} + +#[tokio::test] +async fn second_player_win() { + 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::InviteModeSecondPlayerWin, + ))); + wait_for_port(TestPort::InviteModeSecondPlayerWin.port()).await; + + let (sender, mut receiver) = mpsc::channel(1); + + let res = bot_client::BotClient::new(TestPort::InviteModeSecondPlayerWin.as_url()) + .with_run_mode(RunMode::CreateInvite) + .with_play_as_bot_type(BotType::Linear) + .with_number_plays(10) + .with_server_msg_callback(move |msg| { + if let ServerMessage::SetInviteCode { code } = msg { + task::spawn_local(run_other_invite_side( + sender.clone(), + TestPort::InviteModeSecondPlayerWin, + code.clone(), + BotType::Smart, + 10, + )); + } + }) + .run_client() + .await + .unwrap(); + + let other_side_res = receiver.recv().await.unwrap().unwrap(); + + assert!(matches!(res, ClientEndResult::Finished { .. })); + + assert!(matches!(other_side_res, ClientEndResult::Finished { .. })); + + match (res, other_side_res) { + ( + ClientEndResult::Finished { + number_defeats: d1, + number_victories: v1, + }, + ClientEndResult::Finished { + number_defeats: d2, + number_victories: v2, + }, + ) => { + assert_eq!(d1, v2); + assert_eq!(v1, d2); + + assert!(v2 > 6); + } + + (_, _) => unreachable!(), + } + }) + .await; +} diff --git a/sea_battle_backend/src/test/mod.rs b/sea_battle_backend/src/test/mod.rs index 34691da..4b934a3 100644 --- a/sea_battle_backend/src/test/mod.rs +++ b/sea_battle_backend/src/test/mod.rs @@ -15,6 +15,8 @@ enum TestPort { IntermediateBotFullGame, InviteModeInvalidCode, InviteModeFullGame, + InviteModeFirstPlayerWin, + InviteModeSecondPlayerWin, } impl TestPort { diff --git a/sea_battle_backend/src/utils.rs b/sea_battle_backend/src/utils.rs index 831f3be..aa34d9e 100644 --- a/sea_battle_backend/src/utils.rs +++ b/sea_battle_backend/src/utils.rs @@ -2,14 +2,6 @@ use rand::distributions::Alphanumeric; use rand::{thread_rng, Rng}; /// Generate a random string of a given size -/// -/// ``` -/// use sea_battle_backend::utils::rand_str; -/// -/// let size = 10; -/// let rand = rand_str(size); -/// assert_eq!(size, rand.len()); -/// ``` pub fn rand_str(len: usize) -> String { thread_rng() .sample_iter(&Alphanumeric) @@ -17,3 +9,15 @@ pub fn rand_str(len: usize) -> String { .take(len) .collect() } + +#[cfg(test)] +mod test { + use crate::utils::rand_str; + + #[test] + fn test_rand_str() { + let size = 10; + let rand = rand_str(size); + assert_eq!(size, rand.len()); + } +}