use rand::RngCore; use crate::data::{ BoatPosition, BoatsLayout, BotType, 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) } 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); 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 { location.is_valid(&self.rules) && !self.opponent_map.did_fire_at_location(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!") } 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.opponent_map.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 } /// Get the size of the smallest un-sunk boat pub fn get_size_of_smallest_un_sunk_boat(&self) -> Option { let mut boats_size = self.rules.boats_list(); boats_size.sort(); for boat in &self.opponent_map.sunk_boats { let index = boats_size.iter().position(|b| *b == boat.len)?; boats_size.remove(index); } boats_size.first().cloned() } fn get_unshoot_room(&self, start: Coordinates, diff_x: i32, diff_y: i32) -> usize { let mut size = 0; let mut c = start; loop { c = c.add_x(diff_x).add_y(diff_y); if !self.can_fire_at_location(c) { break; } size += 1; } size } /// Check whether it is relevant to fire a location or not pub fn should_fire_at_location(&self, c: Coordinates) -> bool { if !self.can_fire_at_location(c) { return false; } let smallest_boat = self.get_size_of_smallest_un_sunk_boat().unwrap_or_default(); (self.get_unshoot_room(c, -1, 0) + 1 + self.get_unshoot_room(c, 0, 1)) >= smallest_boat || (self.get_unshoot_room(c, 0, -1) + 1 + self.get_unshoot_room(c, 0, 1)) >= smallest_boat } /// Get the locations on the grid where the player could fire pub fn get_relevant_grid_locations(&self) -> Vec { let min_boat_size = self.get_size_of_smallest_un_sunk_boat().unwrap_or_default(); let mut coordinates = vec![]; let mut y = 0; while y < self.rules.map_height { let mut x = (min_boat_size - 1 + y) % min_boat_size; while x < self.rules.map_width { let c = Coordinates::new(x as i32, y as i32); if self.should_fire_at_location(c) { coordinates.push(c); } x += min_boat_size } y += 1; } coordinates } pub fn get_your_map(&self) -> String { PrintableCurrentGameMapStatus(self.rules.clone(), self.your_map.clone()).get_map() } pub fn get_opponent_map(&self) -> String { PrintableCurrentGameMapStatus(self.rules.clone(), self.opponent_map.clone()).get_map() } pub fn print_your_map(&self) { print!("{}", self.get_your_map()); } 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)] mod test { use std::fmt::Write as _; 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.opponent_map.get_sunk_locations(), vec![sunk]); assert_eq!( status.opponent_map.get_successful_but_un_sunk_locations(), vec![unfinished] ); } #[test] fn no_continue_attack() { let status = CurrentGameStatus::default(); assert!(status .opponent_map .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())) } #[test] fn get_size_of_smallest_unsunk_boat_start_of_game() { let status = CurrentGameStatus::default(); let min_val = *status.rules.boats_list().iter().min().unwrap(); assert_eq!(min_val, status.get_size_of_smallest_un_sunk_boat().unwrap()); } #[test] fn get_size_of_smallest_unsunk_boat_bigger_boat_sunk() { let mut status = CurrentGameStatus::default(); let min_val = *status.rules.boats_list().iter().min().unwrap(); status.opponent_map.sunk_boats.push(BoatPosition { start: Coordinates::new(0, 0), len: min_val + 1, direction: BoatDirection::Left, }); assert_eq!(min_val, status.get_size_of_smallest_un_sunk_boat().unwrap()); } #[test] fn get_size_of_smallest_unsunk_boat_smallest_boat_sunk() { let mut status = CurrentGameStatus::default(); let mut boats_size = status.rules.boats_list(); boats_size.sort(); status.opponent_map.sunk_boats.push(BoatPosition { start: Coordinates::new(0, 0), len: boats_size[0], direction: BoatDirection::Left, }); assert_eq!( boats_size[1], status.get_size_of_smallest_un_sunk_boat().unwrap() ); } #[test] fn relevant_fire_location_empty_game() { let status = CurrentGameStatus::default(); assert!(status.can_fire_at_location(Coordinates::new(0, 0))); assert!(status.should_fire_at_location(Coordinates::new(0, 0))); } #[test] fn relevant_fire_location_two_failed_attempt_around() { let mut status = CurrentGameStatus::default(); status .opponent_map .failed_strikes .push(Coordinates::new(0, 1)); status .opponent_map .failed_strikes .push(Coordinates::new(1, 0)); assert!(status.can_fire_at_location(Coordinates::new(0, 0))); assert!(!status.should_fire_at_location(Coordinates::new(0, 0))); } #[test] fn relevant_fire_location_two_failed_attempt_smallest_boat_not_sunk() { let mut status = CurrentGameStatus::default(); status .opponent_map .failed_strikes .push(Coordinates::new(0, 2)); status .opponent_map .failed_strikes .push(Coordinates::new(2, 0)); assert!(status.can_fire_at_location(Coordinates::new(0, 0))); assert!(status.should_fire_at_location(Coordinates::new(0, 0))); } #[test] fn relevant_fire_location_two_failed_attempt_smallest_boat_sunk() { let mut status = CurrentGameStatus::default(); status .opponent_map .failed_strikes .push(Coordinates::new(0, 2)); status .opponent_map .failed_strikes .push(Coordinates::new(2, 0)); status.opponent_map.sunk_boats.push(BoatPosition { start: Coordinates::new( status.rules.map_width as i32 - 1, status.rules.map_height as i32 - 1, ), len: 2, direction: BoatDirection::Left, }); assert!(status.can_fire_at_location(Coordinates::new(0, 0))); assert!(!status.should_fire_at_location(Coordinates::new(0, 0))); } fn print_coordinates_grid(status: &CurrentGameStatus, locs: &[Coordinates]) -> String { let mut s = String::with_capacity(200); s.push('\n'); for y in 0..status.rules.map_height { for x in 0..status.rules.map_width { let c = Coordinates::new(x as i32, y as i32); write!( s, "{}", match locs.contains(&c) { true => "X", false => ".", } ) .unwrap(); } s.push('\n'); } s } #[test] fn relevant_grid_locations_new_game_min_boat_of_size_2() { let _ = env_logger::builder().is_test(true).try_init(); let status = CurrentGameStatus::default(); let locs = status.get_relevant_grid_locations(); log::debug!("{}", print_coordinates_grid(&status, &locs)); for y in 0..status.rules.map_height { for x in 0..status.rules.map_width { let c = Coordinates::new(x as i32, y as i32); if (x + y) % 2 == 1 { assert!(locs.contains(&c), "Missing {}", c.human_print()); } else { assert!( !locs.contains(&c), "Unwanted presence of {}", c.human_print() ); } } } } #[test] fn relevant_grid_locations_new_game_min_boat_of_size_3() { let _ = env_logger::builder().is_test(true).try_init(); let mut status = CurrentGameStatus::default(); status.rules.set_boats_list(&vec![3, 4, 5]); let locs = status.get_relevant_grid_locations(); log::debug!("{}", print_coordinates_grid(&status, &locs)); assert!(!locs.contains(&Coordinates::new(0, 0))); assert!(!locs.contains(&Coordinates::new(1, 0))); assert!(locs.contains(&Coordinates::new(2, 0))); assert!(locs.contains(&Coordinates::new(0, 1))); assert!(!locs.contains(&Coordinates::new(1, 1))); assert!(!locs.contains(&Coordinates::new(2, 1))); assert!(locs.contains(&Coordinates::new(3, 1))); } #[test] fn relevant_grid_locations_partial_play() { let _ = env_logger::builder().is_test(true).try_init(); let mut status = CurrentGameStatus::default(); status.rules.set_boats_list(&vec![3, 4, 5]); status .opponent_map .failed_strikes .push(Coordinates::new(0, 0)); status .opponent_map .failed_strikes .push(Coordinates::new(3, 0)); status .opponent_map .failed_strikes .push(Coordinates::new(2, 1)); let locs = status.get_relevant_grid_locations(); log::debug!("{}", print_coordinates_grid(&status, &locs)); assert!(!locs.contains(&Coordinates::new(1, 0))); assert!(!locs.contains(&Coordinates::new(2, 0))); } }