518 lines
15 KiB
Rust
518 lines
15 KiB
Rust
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<E>(x: E, y: E) -> Self
|
|
where
|
|
E: Into<i32>,
|
|
{
|
|
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<E>(&self, x: E) -> Self
|
|
where
|
|
E: Into<i32>,
|
|
{
|
|
Self {
|
|
x: self.x + x.into(),
|
|
y: self.y,
|
|
}
|
|
}
|
|
|
|
pub fn add_y<E>(&self, y: E) -> Self
|
|
where
|
|
E: Into<i32>,
|
|
{
|
|
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<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
|
|
}
|
|
|
|
/// 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
|
|
}
|
|
}
|
|
|
|
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, Default)]
|
|
pub struct BoatsLayout(pub Vec<BoatPosition>);
|
|
|
|
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<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) {
|
|
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<Self> {
|
|
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::<Vec<_>>();
|
|
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());
|
|
}
|
|
}
|