diff --git a/Cargo.lock b/Cargo.lock index ab05154..eb3c17b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1019,6 +1019,7 @@ dependencies = [ "clap", "env_logger", "log", + "rand", "serde", "serde_json", "uuid", diff --git a/Cargo.toml b/Cargo.toml index 5cb42ff..60897cf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,4 +16,5 @@ actix-cors = "0.6.2" actix = "0.13.0" actix-web-actors = "4.1.0" actix-rt = "2.7.0" -uuid = { version = "1.1.2", features = ["v4"] } \ No newline at end of file +uuid = { version = "1.1.2", features = ["v4"] } +rand = "0.8.5" \ No newline at end of file diff --git a/src/data/boats_layout.rs b/src/data/boats_layout.rs index a186990..181f8c3 100644 --- a/src/data/boats_layout.rs +++ b/src/data/boats_layout.rs @@ -1,20 +1,66 @@ +use std::io::ErrorKind; + +use rand::{Rng, RngCore}; + use crate::data::GameRules; #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, Copy)] pub enum BoatDirection { Left, Right, - Top, - Bottom, + Up, + Down, + + UpLeft, + UpRight, + BottomLeft, + BottomRight, } +const _BOATS_DIRECTION: [BoatDirection; 4] = [ + BoatDirection::Left, + BoatDirection::Right, + BoatDirection::Up, + 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::Top => (0, -1), - BoatDirection::Bottom => (0, 1), + 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( @@ -24,22 +70,31 @@ impl BoatDirection { } } -#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, Copy, Eq, PartialEq)] +#[derive( +serde::Serialize, serde::Deserialize, Debug, Clone, Copy, Eq, PartialEq, Hash, Ord, PartialOrd, +)] pub struct Coordinates { - x: i32, y: i32, + x: i32, } impl Coordinates { pub fn new(x: E, y: E) -> Self - where - E: Into, + where + E: Into, { Self { x: x.into(), y: y.into(), } } + + 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 + } } #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, Copy)] @@ -50,6 +105,7 @@ pub struct BoatPosition { } 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 { @@ -57,27 +113,160 @@ impl BoatPosition { } 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)] pub struct BoatsLayout(Vec); impl BoatsLayout { - pub fn gen_random_for_rules(rules: &GameRules) -> Self { - todo!() + /// 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 { + 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 + } + + pub fn errors(&self, rules: &GameRules) -> Vec<&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() { + // Check boat coordinates + let boat_i_coordinates = self.0[boat_i].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_i == 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_i].neighbor_coordinates(&rules).iter() + .any(|c| boat_j_coords.contains(c)) { + errors.push("A collision between two boats has been detected!"); + } + } + } + + + errors + } + + pub fn find_boat_at_position(&self, pos: Coordinates) -> Option<&BoatPosition> { + self.0.iter().find(|f| f.all_coordinates().contains(&pos)) } } #[cfg(test)] mod test { - use crate::data::boats_layout::{BoatDirection, BoatPosition, Coordinates}; + use crate::data::{BotType, GameRules, PlayConfiguration}; + use crate::data::boats_layout::{BoatDirection, BoatPosition, BoatsLayout, Coordinates}; + use crate::data::game_map::GameMap; + use crate::game::Game; #[test] fn get_boat_coordinates() { let position = BoatPosition { start: Coordinates { x: 1, y: 1 }, len: 3, - direction: BoatDirection::Bottom, + direction: BoatDirection::Down, }; let coordinates = position.all_coordinates(); assert_eq!( @@ -89,4 +278,109 @@ mod test { ] ) } + + #[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 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, + player_continue_on_hit: false, + bot_type: BotType::Random, + }; + + 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()); + } } diff --git a/src/data/game_map.rs b/src/data/game_map.rs index 7d2e552..e8a9231 100644 --- a/src/data/game_map.rs +++ b/src/data/game_map.rs @@ -1,4 +1,4 @@ -use crate::data::boats_layout::BoatsLayout; +use crate::data::boats_layout::{BoatsLayout, Coordinates}; use crate::data::GameRules; enum MapCellContent { @@ -13,7 +13,7 @@ impl MapCellContent { fn letter(&self) -> &'static str { match self { MapCellContent::Invalid => "!", - MapCellContent::Nothing => " ", + MapCellContent::Nothing => ".", MapCellContent::TouchedBoat => "T", MapCellContent::Boat => "B", MapCellContent::FailedStrike => "X", @@ -34,15 +34,24 @@ impl GameMap { } } - pub fn get_cell_content(&self, x: usize, y: usize) -> MapCellContent { - // TODO : improve this + pub fn get_cell_content(&self, c: Coordinates) -> MapCellContent { + //TODO : improve this + + if self.boats_config.find_boat_at_position(c).is_some() { + return MapCellContent::Boat; + } + return MapCellContent::Nothing; } pub fn print_map(&self) { for y in 0..self.rules.map_height { for x in 0..self.rules.map_width { - print!("{} ", self.get_cell_content(x, y).letter()); + print!( + "{} ", + self.get_cell_content(Coordinates::new(x as i32, y as i32)) + .letter() + ); } println!(); } diff --git a/src/data/game_rules.rs b/src/data/game_rules.rs index 8335553..e15608d 100644 --- a/src/data/game_rules.rs +++ b/src/data/game_rules.rs @@ -27,6 +27,11 @@ impl GameRules { } } + /// Set the list of boats for this configuration + pub fn set_boats_list(&mut self, boats: &[usize]) { + self.boats_str = boats.iter().map(usize::to_string).collect::>().join(","); + } + /// Get the list of boats for this configuration pub fn boats_list(&self) -> Vec { self.boats_str