use rand::RngCore; use crate::data::{ BoatPosition, BoatsLayout, Coordinates, GameRules, MapCellContent, PrintableMap, }; #[derive(Debug, serde::Serialize, serde::Deserialize, Clone, Default)] pub struct CurrentGameMapStatus { pub boats: BoatsLayout, pub successful_strikes: Vec, pub failed_strikes: Vec, pub sunk_boats: Vec, } impl CurrentGameMapStatus { pub fn did_fire_at_location(&self, c: Coordinates) -> bool { self.successful_strikes.contains(&c) || self.failed_strikes.contains(&c) } } struct PrintableCurrentGameMapStatus(GameRules, CurrentGameMapStatus); impl PrintableMap for PrintableCurrentGameMapStatus { fn map_cell_content(&self, c: Coordinates) -> MapCellContent { if !c.is_valid(&self.0) { return MapCellContent::Invalid; } if self.1.failed_strikes.contains(&c) { return MapCellContent::FailedStrike; } if self .1 .sunk_boats .iter() .any(|b| b.all_coordinates().contains(&c)) { return MapCellContent::SunkBoat; } if self.1.successful_strikes.contains(&c) { return MapCellContent::TouchedBoat; } if self.1.boats.find_boat_at_position(c).is_some() { return MapCellContent::Boat; } MapCellContent::Nothing } } #[derive(Debug, serde::Serialize, serde::Deserialize, Clone, Default)] pub struct CurrentGameStatus { pub rules: GameRules, pub your_map: CurrentGameMapStatus, pub opponent_map: CurrentGameMapStatus, } impl CurrentGameStatus { /// Check if opponent can fire at a given location pub fn can_fire_at_location(&self, location: Coordinates) -> bool { !self.opponent_map.successful_strikes.contains(&location) && !self.opponent_map.failed_strikes.contains(&location) } /// Find valid random fire location. Loop until one is found pub fn find_valid_random_fire_location(&self) -> Coordinates { let mut rng = rand::thread_rng(); loop { let coordinates = Coordinates::new( (rng.next_u32() % self.rules.map_width as u32) as i32, (rng.next_u32() % self.rules.map_height as u32) as i32, ); if coordinates.is_valid(&self.rules) && self.can_fire_at_location(coordinates) { return coordinates; } } } /// Find valid linear fire location. Loop until one is found pub fn find_first_valid_fire_location(&self) -> Coordinates { for y in 0..self.rules.map_height { for x in 0..self.rules.map_width { let coordinates = Coordinates::new(x as i32, y as i32); if self.can_fire_at_location(coordinates) { return coordinates; } } } 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() } pub fn print_opponent_map(&self) { 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())) } }