diff --git a/sea_battle_backend/src/data/boats_layout.rs b/sea_battle_backend/src/data/boats_layout.rs index d093821..dd5c6af 100644 --- a/sea_battle_backend/src/data/boats_layout.rs +++ b/sea_battle_backend/src/data/boats_layout.rs @@ -97,6 +97,26 @@ impl Coordinates { && self.y < rules.map_height as i32 } + pub fn add_x(&self, x: E) -> Self + where + E: Into, + { + Self { + x: self.x + x.into(), + y: self.y, + } + } + + pub fn add_y(&self, y: E) -> Self + where + E: Into, + { + Self { + x: self.x, + y: self.y + y.into(), + } + } + pub fn human_print(&self) -> String { format!( "{}:{}", @@ -111,9 +131,9 @@ impl Coordinates { #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, Copy, Eq, PartialEq)] pub struct BoatPosition { - start: Coordinates, - len: usize, - direction: BoatDirection, + pub start: Coordinates, + pub len: usize, + pub direction: BoatDirection, } impl BoatPosition { @@ -147,7 +167,7 @@ impl BoatPosition { } } -#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, Default)] pub struct BoatsLayout(Vec); impl BoatsLayout { diff --git a/sea_battle_backend/src/data/current_game_status.rs b/sea_battle_backend/src/data/current_game_status.rs index d8316e3..d97cf99 100644 --- a/sea_battle_backend/src/data/current_game_status.rs +++ b/sea_battle_backend/src/data/current_game_status.rs @@ -4,7 +4,7 @@ use crate::data::{ BoatPosition, BoatsLayout, Coordinates, GameRules, MapCellContent, PrintableMap, }; -#[derive(Debug, serde::Serialize, serde::Deserialize, Clone)] +#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, Default)] pub struct CurrentGameMapStatus { pub boats: BoatsLayout, pub successful_strikes: Vec, @@ -51,7 +51,7 @@ impl PrintableMap for PrintableCurrentGameMapStatus { } } -#[derive(Debug, serde::Serialize, serde::Deserialize, Clone)] +#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, Default)] pub struct CurrentGameStatus { pub rules: GameRules, pub your_map: CurrentGameMapStatus, @@ -93,6 +93,107 @@ 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], + mut point: Coordinates, + add_x: i32, + add_y: i32, + ) -> Option { + while pos.contains(&point) { + point = point.add_x(add_x).add_y(add_y); + } + if self.can_fire_at_location(point) { + return Some(point); + } + + None + } + + fn horizontal_attack_attempt( + &self, + pos: &[Coordinates], + left: Coordinates, + right: Coordinates, + ) -> Option { + // Try right + if let Some(point) = self.test_attack_direction(pos, right, 1, 0) { + return Some(point); + } + + // Try left + if let Some(point) = self.test_attack_direction(pos, left, -1, 0) { + return Some(point); + } + + None + } + + /// Attempt to continue an attack, if possible + pub fn continue_attack_boat(&self) -> Option { + let pos = self.get_successful_but_un_sunk_locations(); + if pos.is_empty() { + return None; + } + + let start = pos[0]; + + let up = start.add_y(-1); + let down = start.add_y(1); + let left = start.add_x(-1); + let right = start.add_x(1); + + // Try to do it horizontally + if !pos.contains(&up) && !pos.contains(&down) { + if let Some(c) = self.horizontal_attack_attempt(&pos, left, right) { + return Some(c); + } + } + + // Try to do it vertically + + // Try up + if let Some(point) = self.test_attack_direction(&pos, up, 0, -1) { + return Some(point); + } + + // Try down + if let Some(point) = self.test_attack_direction(&pos, down, 0, 1) { + return Some(point); + } + + // Try to do it horizontally again, but unconditionally + if let Some(c) = self.horizontal_attack_attempt(&pos, left, right) { + return Some(c); + } + + // We could even panic here + None + } + pub fn print_your_map(&self) { PrintableCurrentGameMapStatus(self.rules.clone(), self.your_map.clone()).print_map() } @@ -101,3 +202,94 @@ impl CurrentGameStatus { PrintableCurrentGameMapStatus(self.rules.clone(), self.opponent_map.clone()).print_map() } } + +#[cfg(test)] +mod test { + use crate::data::*; + + #[test] + fn get_successful_but_un_sunk_locations() { + let unfinished = Coordinates::new(0, 0); + let sunk = Coordinates::new(3, 3); + + let mut status = CurrentGameStatus::default(); + status.opponent_map.successful_strikes.push(sunk); + status.opponent_map.successful_strikes.push(unfinished); + + status.opponent_map.sunk_boats.push(BoatPosition { + start: sunk, + len: 1, + direction: BoatDirection::Left, + }); + + assert_eq!(status.get_sunk_locations(), vec![sunk]); + assert_eq!( + status.get_successful_but_un_sunk_locations(), + vec![unfinished] + ); + } + + #[test] + fn no_continue_attack() { + let status = CurrentGameStatus::default(); + assert!(status.get_successful_but_un_sunk_locations().is_empty()); + let next_fire = status.continue_attack_boat(); + assert!(next_fire.is_none()); + } + + #[test] + fn continue_attack_unknown_direction() { + let unfinished = Coordinates::new(2, 2); + let possible_next_strikes = vec![ + unfinished.add_x(-1), + unfinished.add_x(1), + unfinished.add_y(-1), + unfinished.add_y(1), + ]; + + let mut status = CurrentGameStatus::default(); + status.opponent_map.successful_strikes.push(unfinished); + + let next_fire = status.continue_attack_boat(); + + assert!(next_fire.is_some()); + assert!(possible_next_strikes.contains(&next_fire.unwrap())) + } + + #[test] + fn continue_attack_vertically_only() { + let unfinished = Coordinates::new(2, 2); + let possible_next_strikes = vec![unfinished.add_x(-1), unfinished.add_x(2)]; + + let mut status = CurrentGameStatus::default(); + status.opponent_map.successful_strikes.push(unfinished); + status + .opponent_map + .successful_strikes + .push(unfinished.add_x(1)); + + let next_fire = status.continue_attack_boat(); + + assert!(next_fire.is_some()); + assert!(possible_next_strikes.contains(&next_fire.unwrap())) + } + + #[test] + fn continue_attack_vertically_after_horizontal_fail() { + let unfinished = Coordinates::new(2, 2); + let possible_next_strikes = vec![unfinished.add_y(-1), unfinished.add_y(1)]; + + let mut status = CurrentGameStatus::default(); + status.opponent_map.successful_strikes.push(unfinished); + status.opponent_map.failed_strikes.push(unfinished.add_x(1)); + status + .opponent_map + .failed_strikes + .push(unfinished.add_x(-1)); + + let next_fire = status.continue_attack_boat(); + + assert!(next_fire.is_some()); + assert!(possible_next_strikes.contains(&next_fire.unwrap())) + } +} diff --git a/sea_battle_backend/src/data/game_rules.rs b/sea_battle_backend/src/data/game_rules.rs index 9c82867..4437558 100644 --- a/sea_battle_backend/src/data/game_rules.rs +++ b/sea_battle_backend/src/data/game_rules.rs @@ -11,6 +11,12 @@ pub struct GameRules { pub bot_type: BotType, } +impl Default for GameRules { + fn default() -> Self { + Self::random_players_rules() + } +} + impl GameRules { pub fn random_players_rules() -> Self { Self {