From 663a9c2d716120722ff2d6eb6d102481707a47ea Mon Sep 17 00:00:00 2001 From: Pierre Hubert Date: Thu, 22 Sep 2022 18:12:23 +0200 Subject: [PATCH] Add smart bot --- sea_battle_backend/src/bots/mod.rs | 1 + sea_battle_backend/src/bots/smart_bot.rs | 97 +++++++++++++++++++ sea_battle_backend/src/data/play_config.rs | 19 +++- sea_battle_backend/src/human_player_ws.rs | 4 + sea_battle_backend/src/test/bot_client.rs | 16 ++- .../test/bot_client_bot_intermediate_play.rs | 2 +- .../src/test/bot_client_bot_linear_play.rs | 8 +- .../src/test/bot_client_bot_random_play.rs | 8 +- .../src/test/bot_client_bot_smart_play.rs | 96 ++++++++++++++++++ sea_battle_backend/src/test/mod.rs | 1 + 10 files changed, 236 insertions(+), 16 deletions(-) create mode 100644 sea_battle_backend/src/bots/smart_bot.rs create mode 100644 sea_battle_backend/src/test/bot_client_bot_smart_play.rs diff --git a/sea_battle_backend/src/bots/mod.rs b/sea_battle_backend/src/bots/mod.rs index 25ee9b7..361f62a 100644 --- a/sea_battle_backend/src/bots/mod.rs +++ b/sea_battle_backend/src/bots/mod.rs @@ -1,3 +1,4 @@ pub mod intermediate_bot; pub mod linear_bot; pub mod random_bot; +pub mod smart_bot; diff --git a/sea_battle_backend/src/bots/smart_bot.rs b/sea_battle_backend/src/bots/smart_bot.rs new file mode 100644 index 0000000..a2b81f6 --- /dev/null +++ b/sea_battle_backend/src/bots/smart_bot.rs @@ -0,0 +1,97 @@ +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/play_config.rs b/sea_battle_backend/src/data/play_config.rs index 0fadf24..72458ca 100644 --- a/sea_battle_backend/src/data/play_config.rs +++ b/sea_battle_backend/src/data/play_config.rs @@ -6,13 +6,14 @@ pub enum BotType { Random, Linear, Intermediate, - // TODO : SmartBot + Smart, } #[derive(serde::Serialize)] pub struct BotDescription { r#type: BotType, - description: String, + name: &'static str, + description: &'static str, } #[derive(serde::Serialize)] @@ -43,15 +44,23 @@ impl Default for PlayConfiguration { bot_types: vec![ BotDescription { r#type: BotType::Linear, - description: "Linear strike. Shoot A1, A2, A3, ..., B1, B2, ...".to_string(), + name: "Linear", + description: "Linear strike. Shoot A1, A2, A3, ..., B1, B2, ...", }, BotDescription { r#type: BotType::Random, - description: "Random search. Random strike.".to_string(), + name: "Ranom", + description: "Random search. Random strike.", }, BotDescription { r#type: BotType::Intermediate, - description: "Randome search. Intelligent strike.".to_string(), + name: "Intermediate", + description: "Random search. Intelligent strike.", + }, + BotDescription { + r#type: BotType::Smart, + name: "Smart", + description: "Smart search. Smart strike.", }, ], ordinate_alphabet: ALPHABET, diff --git a/sea_battle_backend/src/human_player_ws.rs b/sea_battle_backend/src/human_player_ws.rs index b2d3eaa..94e6a40 100644 --- a/sea_battle_backend/src/human_player_ws.rs +++ b/sea_battle_backend/src/human_player_ws.rs @@ -10,6 +10,7 @@ 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::game::{AddPlayer, Game}; use crate::human_player::HumanPlayer; @@ -151,6 +152,9 @@ impl Actor for HumanPlayerWS { BotType::Intermediate => { game.do_send(AddPlayer(Arc::new(IntermediateBot::new(game.clone())))); } + BotType::Smart => { + game.do_send(AddPlayer(Arc::new(SmartBot::new(game.clone())))); + } }; let player = Arc::new(HumanPlayer { diff --git a/sea_battle_backend/src/test/bot_client.rs b/sea_battle_backend/src/test/bot_client.rs index fda80e6..7974da0 100644 --- a/sea_battle_backend/src/test/bot_client.rs +++ b/sea_battle_backend/src/test/bot_client.rs @@ -9,7 +9,10 @@ use crate::human_player_ws::{ClientMessage, ServerMessage}; #[derive(Debug, Eq, PartialEq)] pub enum ClientEndResult { - Finished, + Finished { + number_victories: usize, + number_defeats: usize, + }, InvalidBoatsLayout, OpponentRejectedRematch, OpponentLeftGame, @@ -62,6 +65,8 @@ impl BotClient { pub async fn run_client(&mut self) -> Result> { let mut remaining_games = self.number_plays; + let mut number_victories = 0; + let mut number_defeats = 0; let url = format!( "{}/play/bot?{}", @@ -161,6 +166,8 @@ impl BotClient { result ), ServerMessage::LostGame { status } => { + number_defeats += 1; + log::debug!("We lost game :("); log::debug!("Opponent map:\n{}", status.get_opponent_map()); log::debug!("Our map:\n{}\n", status.get_your_map()); @@ -176,6 +183,8 @@ impl BotClient { } } ServerMessage::WonGame { status } => { + number_victories += 1; + log::debug!("We won the game !!!!"); log::debug!("Opponent map:\n{}\n", status.get_opponent_map()); log::debug!("Our map:\n{}\n", status.get_your_map()); @@ -216,6 +225,9 @@ impl BotClient { } } - Ok(ClientEndResult::Finished) + Ok(ClientEndResult::Finished { + number_victories, + number_defeats, + }) } } diff --git a/sea_battle_backend/src/test/bot_client_bot_intermediate_play.rs b/sea_battle_backend/src/test/bot_client_bot_intermediate_play.rs index 1d027f7..8188967 100644 --- a/sea_battle_backend/src/test/bot_client_bot_intermediate_play.rs +++ b/sea_battle_backend/src/test/bot_client_bot_intermediate_play.rs @@ -42,7 +42,7 @@ async fn full_game() { .run_client() .await .unwrap(); - assert_eq!(res, ClientEndResult::Finished); + assert!(matches!(res, ClientEndResult::Finished { .. })); }) .await; } diff --git a/sea_battle_backend/src/test/bot_client_bot_linear_play.rs b/sea_battle_backend/src/test/bot_client_bot_linear_play.rs index 711eb5c..77e87b9 100644 --- a/sea_battle_backend/src/test/bot_client_bot_linear_play.rs +++ b/sea_battle_backend/src/test/bot_client_bot_linear_play.rs @@ -46,7 +46,7 @@ async fn full_game() { .run_client() .await .unwrap(); - assert_eq!(res, ClientEndResult::Finished); + assert!(matches!(res, ClientEndResult::Finished { .. })); }) .await; } @@ -73,7 +73,7 @@ async fn full_game_no_replay_on_hit() { .run_client() .await .unwrap(); - assert_eq!(res, ClientEndResult::Finished); + assert!(matches!(res, ClientEndResult::Finished { .. })); }) .await; } @@ -101,7 +101,7 @@ async fn full_game_no_replay_on_hit_two() { .run_client() .await .unwrap(); - assert_eq!(res, ClientEndResult::Finished); + assert!(matches!(res, ClientEndResult::Finished { .. })); }) .await; } @@ -138,7 +138,7 @@ async fn full_game_with_replay_on_hit() { .run_client() .await .unwrap(); - assert_eq!(res, ClientEndResult::Finished); + assert!(matches!(res, ClientEndResult::Finished { .. })); }) .await; } diff --git a/sea_battle_backend/src/test/bot_client_bot_random_play.rs b/sea_battle_backend/src/test/bot_client_bot_random_play.rs index ffb9214..0344605 100644 --- a/sea_battle_backend/src/test/bot_client_bot_random_play.rs +++ b/sea_battle_backend/src/test/bot_client_bot_random_play.rs @@ -60,7 +60,7 @@ async fn full_game() { .run_client() .await .unwrap(); - assert_eq!(res, ClientEndResult::Finished); + assert!(matches!(res, ClientEndResult::Finished { .. })); }) .await; } @@ -86,7 +86,7 @@ async fn full_game_no_touching_boats() { .await .unwrap(); - assert_eq!(res, ClientEndResult::Finished); + assert!(matches!(res, ClientEndResult::Finished { .. })); }) .await; } @@ -174,7 +174,7 @@ async fn full_game_multiple_rematches() { .run_client() .await .unwrap(); - assert_eq!(res, ClientEndResult::Finished); + assert!(matches!(res, ClientEndResult::Finished { .. })); }) .await; } @@ -197,7 +197,7 @@ async fn full_game_no_replay_on_hit() { .run_client() .await .unwrap(); - assert_eq!(res, ClientEndResult::Finished); + assert!(matches!(res, ClientEndResult::Finished { .. })); }) .await; } diff --git a/sea_battle_backend/src/test/bot_client_bot_smart_play.rs b/sea_battle_backend/src/test/bot_client_bot_smart_play.rs new file mode 100644 index 0000000..ad9ce1b --- /dev/null +++ b/sea_battle_backend/src/test/bot_client_bot_smart_play.rs @@ -0,0 +1,96 @@ +use tokio::task; + +use crate::args::Args; +use crate::data::{BotType, CurrentGameStatus, GameRules}; +use crate::human_player_ws::ServerMessage; +use crate::server::start_server; +use crate::test::bot_client::ClientEndResult; +use crate::test::network_utils::wait_for_port; +use crate::test::{bot_client, TestPort}; + +#[tokio::test] +async fn full_game() { + 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::IntermediateBotFullGame, + ))); + wait_for_port(TestPort::IntermediateBotFullGame.port()).await; + + let mut curr_status = CurrentGameStatus::default(); + + let res = bot_client::BotClient::new(TestPort::IntermediateBotFullGame.as_url()) + .with_rules(GameRules::random_players_rules().with_bot_type(BotType::Smart)) + .with_server_msg_callback(move |msg| match msg { + ServerMessage::OpponentMustFire { status } => { + curr_status = status.clone(); + } + ServerMessage::OpponentFireResult { pos, .. } => { + let pending_sunk_loc = + curr_status.your_map.get_successful_but_un_sunk_locations(); + + if !pending_sunk_loc.is_empty() { + log::debug!("Check if fire was smart..."); + assert!(pending_sunk_loc.iter().any(|l| pos.dist_with(l) <= 1)) + } + } + _ => {} + }) + .run_client() + .await + .unwrap(); + assert!(matches!(res, ClientEndResult::Finished { .. })); + }) + .await; +} + +#[tokio::test] +async fn full_game_multiple_rematches() { + 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::IntermediateBotFullGame, + ))); + wait_for_port(TestPort::IntermediateBotFullGame.port()).await; + + let mut curr_status = CurrentGameStatus::default(); + + let res = bot_client::BotClient::new(TestPort::IntermediateBotFullGame.as_url()) + .with_rules(GameRules::random_players_rules().with_bot_type(BotType::Smart)) + .with_server_msg_callback(move |msg| match msg { + ServerMessage::OpponentMustFire { status } => { + curr_status = status.clone(); + } + ServerMessage::OpponentFireResult { pos, .. } => { + let pending_sunk_loc = + curr_status.your_map.get_successful_but_un_sunk_locations(); + + if !pending_sunk_loc.is_empty() { + log::debug!("Check if fire was smart..."); + assert!(pending_sunk_loc.iter().any(|l| pos.dist_with(l) <= 1)) + } + } + _ => {} + }) + .with_number_plays(20) + .run_client() + .await + .unwrap(); + if let ClientEndResult::Finished { number_defeats, .. } = res { + assert!( + number_defeats > 15, + "number of defeats = {} which is < 15", + number_defeats + ) + } else { + assert_eq!(0, 1, "Client did not finish correctly"); + } + }) + .await; +} diff --git a/sea_battle_backend/src/test/mod.rs b/sea_battle_backend/src/test/mod.rs index 6d10af4..7ea11b2 100644 --- a/sea_battle_backend/src/test/mod.rs +++ b/sea_battle_backend/src/test/mod.rs @@ -39,5 +39,6 @@ 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 network_utils; mod play_utils;