use std::io::ErrorKind; use rand::{Rng, RngCore}; use crate::consts::ALPHABET; use crate::data::GameRules; #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, Copy, Eq, PartialEq)] pub enum BoatDirection { Left, Right, Up, Down, UpLeft, UpRight, BottomLeft, BottomRight, } const _BOATS_DIRECTION: [BoatDirection; 2] = [BoatDirection::Right, BoatDirection::Down]; const _ALL_DIRECTIONS: [BoatDirection; 8] = [ BoatDirection::Left, BoatDirection::Right, BoatDirection::Up, BoatDirection::Down, BoatDirection::UpLeft, BoatDirection::UpRight, BoatDirection::BottomLeft, BoatDirection::BottomRight, ]; impl BoatDirection { pub fn boat_directions() -> &'static [BoatDirection] { &_BOATS_DIRECTION } pub fn all_directions() -> &'static [BoatDirection] { &_ALL_DIRECTIONS } pub fn is_valid_boat_direction(&self) -> bool { matches!( self, BoatDirection::Left | BoatDirection::Right | BoatDirection::Up | BoatDirection::Down ) } pub fn shift_coordinates(&self, coordinates: Coordinates, nth: usize) -> Coordinates { let shift = match self { BoatDirection::Left => (-1, 0), BoatDirection::Right => (1, 0), BoatDirection::Up => (0, -1), BoatDirection::Down => (0, 1), BoatDirection::UpLeft => (-1, -1), BoatDirection::UpRight => (1, -1), BoatDirection::BottomLeft => (-1, 1), BoatDirection::BottomRight => (1, 1), }; Coordinates::new( coordinates.x + shift.0 * nth as i32, coordinates.y + shift.1 * nth as i32, ) } } #[derive( serde::Serialize, serde::Deserialize, Debug, Clone, Copy, Eq, PartialEq, Hash, Ord, PartialOrd, )] pub struct Coordinates { y: i32, x: i32, } impl Coordinates { pub fn new(x: E, y: E) -> Self where E: Into, { Self { x: x.into(), y: y.into(), } } pub fn invalid() -> Self { Self { x: -1, y: -1 } } pub fn is_valid(&self, rules: &GameRules) -> bool { self.x >= 0 && self.y >= 0 && self.x < rules.map_width as i32 && 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 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!( "{}:{}", match self.y < 0 || self.y >= ALPHABET.len() as i32 { true => self.y.to_string(), false => ALPHABET.chars().nth(self.y as usize).unwrap().to_string(), }, self.x ) } } #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, Copy, Eq, PartialEq)] pub struct BoatPosition { pub start: Coordinates, pub len: usize, pub direction: BoatDirection, } impl BoatPosition { /// Get all the coordinates occupied by the boat pub fn all_coordinates(&self) -> Vec { let mut positions = Vec::with_capacity(self.len); for i in 0..self.len { positions.push(self.direction.shift_coordinates(self.start, i)); } positions } /// Get all the coordinates around the boats pub fn neighbor_coordinates(&self, rules: &GameRules) -> Vec { let boat_positions = self.all_coordinates(); let mut neighbors = Vec::with_capacity(self.len); for i in 0..self.len { let boat_point = self.direction.shift_coordinates(self.start, i); for dir in BoatDirection::all_directions() { let curr_point = dir.shift_coordinates(boat_point, 1); if !neighbors.contains(&curr_point) && !boat_positions.contains(&curr_point) && curr_point.is_valid(rules) { neighbors.push(curr_point); } } } neighbors } } #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, Default)] pub struct BoatsLayout(pub Vec); impl BoatsLayout { /// Generate a new invalid (empty) boats layout pub fn new_invalid() -> Self { Self(vec![]) } /// Generate random boats layout for given game rules pub fn gen_random_for_rules(rules: &GameRules) -> std::io::Result { let mut boats = Self(Vec::with_capacity(rules.boats_list().len())); let mut rng = rand::thread_rng(); for boat in rules.boats_list() { let mut attempt = 0; loop { attempt += 1; let directions = BoatDirection::boat_directions(); let position = BoatPosition { start: Coordinates::new( (rng.next_u32() % rules.map_width as u32) as i32, (rng.next_u32() % rules.map_height as u32) as i32, ), len: boat, direction: directions[rng.gen::() % directions.len()], }; if boats.is_acceptable_boat_position(&position, rules) { boats.0.push(position); break; } if attempt >= rules.map_width * rules.map_height * 4 { return Err(std::io::Error::new( ErrorKind::Other, "Un-usable game rules!", )); } } } Ok(boats) } /// Generate boats layout that put boats at the beginning of the map pub fn layout_for_boats_at_beginning_of_map(rules: &GameRules) -> std::io::Result { let mut boats = Self(Vec::with_capacity(rules.boats_list().len())); let mut list = rules.boats_list(); list.sort(); list.reverse(); for y in 0..rules.map_height { for x in 0..rules.map_width { if list.is_empty() { break; } let position = BoatPosition { start: Coordinates::new(x as i32, y as i32), len: list[0], direction: BoatDirection::Right, }; if boats.is_acceptable_boat_position(&position, rules) { list.remove(0); boats.0.push(position); } } } if !list.is_empty() { return Err(std::io::Error::new( ErrorKind::Other, "Un-usable game rules!", )); } Ok(boats) } fn is_acceptable_boat_position(&self, pos: &BoatPosition, rules: &GameRules) -> bool { // Check if boat coordinates are valid if pos.all_coordinates().iter().any(|c| !c.is_valid(rules)) { return false; } // Check if the boat would cross another boat if pos .all_coordinates() .iter() .any(|c| self.find_boat_at_position(*c).is_some()) { return false; } // Check if the boat touch another boat in a configuration where is it forbidden if !rules.boats_can_touch && pos .neighbor_coordinates(rules) .iter() .any(|c| self.find_boat_at_position(*c).is_some()) { return false; } true } /// Check a boat already present in the layout has a valid position pub fn check_present_boat_position(&self, boat: usize, rules: &GameRules) -> Vec<&'static str> { let mut errors = vec![]; // Check boat coordinates let boat_i_coordinates = self.0[boat].all_coordinates(); if boat_i_coordinates.iter().any(|c| !c.is_valid(rules)) { errors.push("A boat goes outside the game map!"); } for boat_j in 0..self.0.len() { if boat == boat_j { continue; } let boat_j_coords = self.0[boat_j].all_coordinates(); if boat_i_coordinates.iter().any(|c| boat_j_coords.contains(c)) { errors.push("A collision between two boats has been detected!"); } if !rules.boats_can_touch && self.0[boat] .neighbor_coordinates(rules) .iter() .any(|c| boat_j_coords.contains(c)) { errors.push("Two boats are touching!"); } } errors } /// Check if this boats layout is valid or not pub fn errors(&self, rules: &GameRules) -> Vec<&'static str> { let mut errors = vec![]; // Check the number of boats if self.0.len() != rules.boats_list().len() { errors.push("The number of boats is invalid!"); } // Check the length of the boats let mut len = self.0.iter().map(|l| l.len).collect::>(); len.sort(); let mut boats = rules.boats_list(); boats.sort(); if len != boats { errors.push("Boats lens mismatch!"); } for boat_i in 0..self.0.len() { errors.append(&mut self.check_present_boat_position(boat_i, rules)); } errors } pub fn is_valid(&self, rules: &GameRules) -> bool { self.errors(rules).is_empty() } /// Get the list of boats pub fn boats(&self) -> &[BoatPosition] { &self.0 } pub fn find_boat_at_position(&self, pos: Coordinates) -> Option<&BoatPosition> { self.0.iter().find(|f| f.all_coordinates().contains(&pos)) } pub fn number_of_boats(&self) -> usize { self.0.len() } } #[cfg(test)] mod test { use crate::data::boats_layout::{BoatDirection, BoatPosition, BoatsLayout, Coordinates}; use crate::data::game_map::GameMap; use crate::data::*; #[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 { start: Coordinates { x: 1, y: 1 }, len: 3, direction: BoatDirection::Down, }; let coordinates = position.all_coordinates(); assert_eq!( coordinates, vec![ Coordinates::new(1, 1), Coordinates::new(1, 2), Coordinates::new(1, 3), ] ) } #[test] fn get_boat_neighbor_coordinates() { let rules = GameRules::random_players_rules(); let position = BoatPosition { start: Coordinates { x: 0, y: 1 }, len: 3, direction: BoatDirection::Down, }; println!("{:?}", position.all_coordinates()); let mut coordinates = position.neighbor_coordinates(&rules); coordinates.sort(); assert_eq!( coordinates, vec![ Coordinates::new(0, 0), Coordinates::new(1, 0), Coordinates::new(1, 1), Coordinates::new(1, 2), Coordinates::new(1, 3), Coordinates::new(0, 4), Coordinates::new(1, 4), ] ) } #[test] fn generate_random_boats_layout() { let rules = GameRules::random_players_rules(); let boats = BoatsLayout::gen_random_for_rules(&rules).unwrap(); assert!(boats.errors(&rules).is_empty()); let game = GameMap::new(rules, boats); game.print_map(); } #[test] fn generate_random_boats_layout_without_touching_boats() { let mut rules = GameRules::random_players_rules(); rules.boats_can_touch = false; let boats = BoatsLayout::gen_random_for_rules(&rules).unwrap(); assert!(boats.errors(&rules).is_empty()); let game = GameMap::new(rules, boats); game.print_map(); } #[test] fn impossible_map() { let mut rules = GameRules::random_players_rules(); rules.map_height = PlayConfiguration::default().min_map_height; rules.map_width = PlayConfiguration::default().min_map_width; let mut boats = Vec::new(); for _i in 0..PlayConfiguration::default().max_boats_number { boats.push(PlayConfiguration::default().max_boat_len); } rules.set_boats_list(&boats); BoatsLayout::gen_random_for_rules(&rules).unwrap_err(); } #[test] fn empty_boats_layout() { let rules = GameRules::random_players_rules(); let boats = BoatsLayout(vec![]); assert!(!boats.errors(&rules).is_empty()); } #[test] fn invalid_number_of_boats() { let rules = GameRules::random_players_rules(); let mut boats = BoatsLayout::gen_random_for_rules(&rules).unwrap(); boats.0.remove(0); assert!(!boats.errors(&rules).is_empty()); } #[test] fn boats_crossing() { let rules = GameRules::random_players_rules(); let mut boats = BoatsLayout::gen_random_for_rules(&rules).unwrap(); boats.0[0].start = boats.0[1].start; assert!(!boats.errors(&rules).is_empty()); } #[test] fn boats_touching_in_forbidden_configuration() { let rules = GameRules { map_width: 5, map_height: 5, boats_str: "1,1".to_string(), boats_can_touch: false, ..Default::default() }; let mut boats = BoatsLayout(vec![ BoatPosition { start: Coordinates::new(0, 0), len: 1, direction: BoatDirection::Left, }, BoatPosition { start: Coordinates::new(0, 1), len: 1, direction: BoatDirection::Left, }, ]); boats.0[0].start = boats.0[1].start; assert!(!boats.errors(&rules).is_empty()); } #[test] fn boats_outside_map() { let rules = GameRules::random_players_rules(); let mut boats = BoatsLayout::gen_random_for_rules(&rules).unwrap(); boats.0[0].start = Coordinates::new(-1, -1); assert!(!boats.errors(&rules).is_empty()); } }