SeaBattle/sea_battle_backend/src/data/boats_layout.rs

400 lines
12 KiB
Rust
Raw Normal View History

2022-09-13 15:25:14 +00:00
use std::io::ErrorKind;
use rand::{Rng, RngCore};
2022-09-12 14:38:14 +00:00
use crate::data::GameRules;
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, Copy)]
pub enum BoatDirection {
Left,
Right,
2022-09-13 15:25:14 +00:00
Up,
Down,
UpLeft,
UpRight,
BottomLeft,
BottomRight,
2022-09-12 14:38:14 +00:00
}
2022-09-13 15:25:14 +00:00
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,
];
2022-09-12 14:38:14 +00:00
impl BoatDirection {
2022-09-13 15:25:14 +00:00
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
)
}
2022-09-12 14:38:14 +00:00
pub fn shift_coordinates(&self, coordinates: Coordinates, nth: usize) -> Coordinates {
let shift = match self {
BoatDirection::Left => (1, 0),
BoatDirection::Right => (-1, 0),
2022-09-13 15:25:14 +00:00
BoatDirection::Up => (0, -1),
BoatDirection::Down => (0, 1),
BoatDirection::UpLeft => (-1, -1),
BoatDirection::UpRight => (1, -1),
BoatDirection::BottomLeft => (-1, 1),
BoatDirection::BottomRight => (1, 1),
2022-09-12 14:38:14 +00:00
};
Coordinates::new(
coordinates.x + shift.0 * nth as i32,
coordinates.y + shift.1 * nth as i32,
)
}
}
2022-09-13 15:25:14 +00:00
#[derive(
2022-09-13 15:26:40 +00:00
serde::Serialize, serde::Deserialize, Debug, Clone, Copy, Eq, PartialEq, Hash, Ord, PartialOrd,
2022-09-13 15:25:14 +00:00
)]
2022-09-12 14:38:14 +00:00
pub struct Coordinates {
y: i32,
2022-09-13 15:25:14 +00:00
x: i32,
2022-09-12 14:38:14 +00:00
}
impl Coordinates {
pub fn new<E>(x: E, y: E) -> Self
2022-09-13 15:26:40 +00:00
where
E: Into<i32>,
2022-09-12 14:38:14 +00:00
{
Self {
x: x.into(),
y: y.into(),
}
}
2022-09-13 15:25:14 +00:00
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
}
2022-09-12 14:38:14 +00:00
}
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, Copy)]
pub struct BoatPosition {
start: Coordinates,
len: usize,
direction: BoatDirection,
}
impl BoatPosition {
2022-09-13 15:25:14 +00:00
/// Get all the coordinates occupied by the boat
2022-09-12 14:38:14 +00:00
pub fn all_coordinates(&self) -> Vec<Coordinates> {
let mut positions = Vec::with_capacity(self.len);
for i in 0..self.len {
positions.push(self.direction.shift_coordinates(self.start, i));
}
positions
}
2022-09-13 15:25:14 +00:00
/// Get all the coordinates around the boats
pub fn neighbor_coordinates(&self, rules: &GameRules) -> Vec<Coordinates> {
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
}
2022-09-12 14:38:14 +00:00
}
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
pub struct BoatsLayout(Vec<BoatPosition>);
impl BoatsLayout {
2022-09-13 15:25:14 +00:00
/// Generate random boats layout for given game rules
pub fn gen_random_for_rules(rules: &GameRules) -> std::io::Result<Self> {
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::<usize>() % directions.len()],
};
if boats.is_acceptable_boat_position(&position, rules) {
2022-09-13 15:25:14 +00:00
boats.0.push(position);
break;
}
2022-09-13 15:26:40 +00:00
if attempt >= rules.map_width * rules.map_height * 4 {
return Err(std::io::Error::new(
ErrorKind::Other,
"Un-usable game rules!",
));
2022-09-13 15:25:14 +00:00
}
}
}
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)
2022-09-13 15:26:40 +00:00
.iter()
.any(|c| self.find_boat_at_position(*c).is_some())
2022-09-13 15:25:14 +00:00
{
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
2022-09-13 15:26:40 +00:00
let mut len = self.0.iter().map(|l| l.len).collect::<Vec<_>>();
2022-09-13 15:25:14 +00:00
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)) {
2022-09-13 15:25:14 +00:00
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!");
}
2022-09-13 15:26:40 +00:00
if !rules.boats_can_touch
&& self.0[boat_i]
.neighbor_coordinates(rules)
2022-09-13 15:26:40 +00:00
.iter()
.any(|c| boat_j_coords.contains(c))
{
2022-09-13 15:25:14 +00:00
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))
2022-09-12 14:38:14 +00:00
}
}
#[cfg(test)]
mod test {
2022-09-13 15:25:14 +00:00
use crate::data::boats_layout::{BoatDirection, BoatPosition, BoatsLayout, Coordinates};
use crate::data::game_map::GameMap;
2022-09-13 15:26:40 +00:00
use crate::data::{BotType, GameRules, PlayConfiguration};
2022-09-12 14:38:14 +00:00
#[test]
fn get_boat_coordinates() {
let position = BoatPosition {
start: Coordinates { x: 1, y: 1 },
len: 3,
2022-09-13 15:25:14 +00:00
direction: BoatDirection::Down,
2022-09-12 14:38:14 +00:00
};
let coordinates = position.all_coordinates();
assert_eq!(
coordinates,
vec![
Coordinates::new(1, 1),
Coordinates::new(1, 2),
Coordinates::new(1, 3),
]
)
}
2022-09-13 15:25:14 +00:00
#[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();
}
2022-09-13 15:25:14 +00:00
#[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 {
2022-09-13 15:25:14 +00:00
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());
}
2022-09-12 14:38:14 +00:00
}