diff --git a/sea_battle_backend/src/bots/intermediate_bot.rs b/sea_battle_backend/src/bots/intermediate_bot.rs new file mode 100644 index 0000000..18eb2e6 --- /dev/null +++ b/sea_battle_backend/src/bots/intermediate_bot.rs @@ -0,0 +1,90 @@ +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 IntermediateBot { + game: Addr, + uuid: Uuid, +} + +impl IntermediateBot { + pub fn new(game: Addr) -> Self { + Self { + game, + uuid: Uuid::new_v4(), + } + } +} + +impl Player for IntermediateBot { + 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(|| 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/bots/mod.rs b/sea_battle_backend/src/bots/mod.rs index 6a1ea0f..25ee9b7 100644 --- a/sea_battle_backend/src/bots/mod.rs +++ b/sea_battle_backend/src/bots/mod.rs @@ -1,2 +1,3 @@ +pub mod intermediate_bot; pub mod linear_bot; pub mod random_bot; diff --git a/sea_battle_backend/src/data/boats_layout.rs b/sea_battle_backend/src/data/boats_layout.rs index 4effaf4..50c7261 100644 --- a/sea_battle_backend/src/data/boats_layout.rs +++ b/sea_battle_backend/src/data/boats_layout.rs @@ -117,6 +117,10 @@ impl Coordinates { } } + pub fn dist_with(&self, other: &Self) -> usize { + (self.x.abs_diff(other.x) + self.y.abs_diff(other.y)) as usize + } + pub fn human_print(&self) -> String { format!( "{}:{}", @@ -342,6 +346,26 @@ mod test { use crate::data::game_map::GameMap; use crate::data::{BotType, GameRules, PlayConfiguration, PrintableMap}; + #[test] + fn dist_coordinates_eq() { + let c = Coordinates::new(1, 1); + assert_eq!(c.dist_with(&c), 0); + } + + #[test] + fn dist_neighbor_coordinates() { + let c1 = Coordinates::new(1, 1); + let c2 = Coordinates::new(1, 2); + assert_eq!(c1.dist_with(&c2), 1); + } + + #[test] + fn dist_diagonal_coordinates() { + let c1 = Coordinates::new(1, 1); + let c2 = Coordinates::new(2, 2); + assert_eq!(c1.dist_with(&c2), 2); + } + #[test] fn get_boat_coordinates() { let position = BoatPosition { diff --git a/sea_battle_backend/src/data/current_game_status.rs b/sea_battle_backend/src/data/current_game_status.rs index 2e2aab6..ec76872 100644 --- a/sea_battle_backend/src/data/current_game_status.rs +++ b/sea_battle_backend/src/data/current_game_status.rs @@ -20,6 +20,27 @@ impl CurrentGameMapStatus { pub fn number_of_fires(&self) -> usize { self.successful_strikes.len() + self.failed_strikes.len() } + + pub fn get_sunk_locations(&self) -> Vec { + self.sunk_boats + .iter() + .map(|f| f.all_coordinates()) + .reduce(|mut a, mut b| { + a.append(&mut b); + a + }) + .unwrap_or_default() + } + + pub fn get_successful_but_un_sunk_locations(&self) -> Vec { + let sunk_location = self.get_sunk_locations(); + + self.successful_strikes + .iter() + .filter(|c| !sunk_location.contains(c)) + .map(Coordinates::clone) + .collect() + } } struct PrintableCurrentGameMapStatus(GameRules, CurrentGameMapStatus); @@ -65,9 +86,7 @@ pub struct CurrentGameStatus { impl CurrentGameStatus { /// Check if opponent can fire at a given location pub fn can_fire_at_location(&self, location: Coordinates) -> bool { - location.is_valid(&self.rules) - && !self.opponent_map.successful_strikes.contains(&location) - && !self.opponent_map.failed_strikes.contains(&location) + location.is_valid(&self.rules) && !self.opponent_map.did_fire_at_location(location) } /// Find valid random fire location. Loop until one is found @@ -98,29 +117,6 @@ impl CurrentGameStatus { panic!("Could not find fire location!") } - pub fn get_sunk_locations(&self) -> Vec { - self.opponent_map - .sunk_boats - .iter() - .map(|f| f.all_coordinates()) - .reduce(|mut a, mut b| { - a.append(&mut b); - a - }) - .unwrap_or_default() - } - - pub fn get_successful_but_un_sunk_locations(&self) -> Vec { - let sunk_location = self.get_sunk_locations(); - - self.opponent_map - .successful_strikes - .iter() - .filter(|c| !sunk_location.contains(c)) - .map(Coordinates::clone) - .collect() - } - fn test_attack_direction( &self, pos: &[Coordinates], @@ -159,7 +155,7 @@ impl CurrentGameStatus { /// Attempt to continue an attack, if possible pub fn continue_attack_boat(&self) -> Option { - let pos = self.get_successful_but_un_sunk_locations(); + let pos = self.opponent_map.get_successful_but_un_sunk_locations(); if pos.is_empty() { return None; } @@ -301,9 +297,9 @@ mod test { direction: BoatDirection::Left, }); - assert_eq!(status.get_sunk_locations(), vec![sunk]); + assert_eq!(status.opponent_map.get_sunk_locations(), vec![sunk]); assert_eq!( - status.get_successful_but_un_sunk_locations(), + status.opponent_map.get_successful_but_un_sunk_locations(), vec![unfinished] ); } @@ -311,7 +307,10 @@ mod test { #[test] fn no_continue_attack() { let status = CurrentGameStatus::default(); - assert!(status.get_successful_but_un_sunk_locations().is_empty()); + assert!(status + .opponent_map + .get_successful_but_un_sunk_locations() + .is_empty()); let next_fire = status.continue_attack_boat(); assert!(next_fire.is_none()); } diff --git a/sea_battle_backend/src/data/play_config.rs b/sea_battle_backend/src/data/play_config.rs index 05b93fd..0fadf24 100644 --- a/sea_battle_backend/src/data/play_config.rs +++ b/sea_battle_backend/src/data/play_config.rs @@ -5,7 +5,7 @@ use crate::consts::*; pub enum BotType { Random, Linear, - // TODO : GridBot + Intermediate, // TODO : SmartBot } @@ -47,7 +47,11 @@ impl Default for PlayConfiguration { }, BotDescription { r#type: BotType::Random, - description: "Random strike. All the time.".to_string(), + description: "Random search. Random strike.".to_string(), + }, + BotDescription { + r#type: BotType::Intermediate, + description: "Randome search. Intelligent strike.".to_string(), }, ], ordinate_alphabet: ALPHABET, diff --git a/sea_battle_backend/src/game.rs b/sea_battle_backend/src/game.rs index 548326f..277c65e 100644 --- a/sea_battle_backend/src/game.rs +++ b/sea_battle_backend/src/game.rs @@ -149,7 +149,7 @@ impl Game { fn handle_fire(&mut self, c: Coordinates) { let result = self.player_map_mut(opponent(self.turn)).fire(c); self.players[self.turn].strike_result(c, result); - self.players[opponent(self.turn)].strike_result(c, result); + self.players[opponent(self.turn)].other_player_strike_result(c, result); // Easiest case : player missed his fire if result == FireResult::Missed { diff --git a/sea_battle_backend/src/human_player.rs b/sea_battle_backend/src/human_player.rs index 933a1aa..e299bff 100644 --- a/sea_battle_backend/src/human_player.rs +++ b/sea_battle_backend/src/human_player.rs @@ -61,14 +61,14 @@ impl Player for HumanPlayer { } fn strike_result(&self, c: Coordinates, res: FireResult) { - self.player.do_send(ServerMessage::StrikeResult { + self.player.do_send(ServerMessage::FireResult { pos: c, result: res, }); } fn other_player_strike_result(&self, c: Coordinates, res: FireResult) { - self.player.do_send(ServerMessage::OpponentStrikeResult { + self.player.do_send(ServerMessage::OpponentFireResult { pos: c, result: res, }); diff --git a/sea_battle_backend/src/human_player_ws.rs b/sea_battle_backend/src/human_player_ws.rs index 4d31da7..b2d3eaa 100644 --- a/sea_battle_backend/src/human_player_ws.rs +++ b/sea_battle_backend/src/human_player_ws.rs @@ -7,6 +7,7 @@ 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::data::{BoatsLayout, BotType, Coordinates, CurrentGameStatus, FireResult, GameRules}; @@ -62,11 +63,11 @@ pub enum ServerMessage { RequestFire { status: CurrentGameStatus, }, - StrikeResult { + FireResult { pos: Coordinates, result: FireResult, }, - OpponentStrikeResult { + OpponentFireResult { pos: Coordinates, result: FireResult, }, @@ -147,6 +148,9 @@ impl Actor for HumanPlayerWS { BotType::Linear => { game.do_send(AddPlayer(Arc::new(LinearBot::new(game.clone())))); } + BotType::Intermediate => { + game.do_send(AddPlayer(Arc::new(IntermediateBot::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 f8ed1b8..fda80e6 100644 --- a/sea_battle_backend/src/test/bot_client.rs +++ b/sea_battle_backend/src/test/bot_client.rs @@ -20,7 +20,7 @@ pub struct BotClient { requested_rules: GameRules, layout: Option, number_plays: usize, - server_msg_callback: Option>, + server_msg_callback: Option>, } impl BotClient { @@ -54,13 +54,13 @@ impl BotClient { pub fn with_server_msg_callback(mut self, cb: F) -> Self where - F: Fn(&ServerMessage) + 'static, + F: FnMut(&ServerMessage) + 'static, { self.server_msg_callback = Some(Box::new(cb)); self } - pub async fn run_client(&self) -> Result> { + pub async fn run_client(&mut self) -> Result> { let mut remaining_games = self.number_plays; let url = format!( @@ -107,7 +107,7 @@ impl BotClient { } }; - if let Some(cb) = &self.server_msg_callback { + if let Some(cb) = &mut self.server_msg_callback { (cb)(&message) } @@ -152,10 +152,10 @@ impl BotClient { )?)) .await?; } - ServerMessage::StrikeResult { pos, result } => { + ServerMessage::FireResult { pos, result } => { log::debug!("Strike at {} result: {:?}", pos.human_print(), result) } - ServerMessage::OpponentStrikeResult { pos, result } => log::debug!( + ServerMessage::OpponentFireResult { pos, result } => log::debug!( "Opponent trike at {} result: {:?}", pos.human_print(), result 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 new file mode 100644 index 0000000..1d027f7 --- /dev/null +++ b/sea_battle_backend/src/test/bot_client_bot_intermediate_play.rs @@ -0,0 +1,48 @@ +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::Intermediate)) + .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_eq!(res, ClientEndResult::Finished); + }) + .await; +} diff --git a/sea_battle_backend/src/test/mod.rs b/sea_battle_backend/src/test/mod.rs index 1a83f0a..6d10af4 100644 --- a/sea_battle_backend/src/test/mod.rs +++ b/sea_battle_backend/src/test/mod.rs @@ -12,6 +12,7 @@ enum TestPort { RandomBotNoReplayOnHit, LinearBotFullGame, LinearBotNoReplayOnHit, + IntermediateBotFullGame, } impl TestPort { @@ -35,6 +36,7 @@ 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 network_utils;