Start to build cli player

This commit is contained in:
2022-10-01 19:25:41 +02:00
parent 65af3b0bba
commit 003296a782
39 changed files with 414 additions and 125 deletions

View File

@ -0,0 +1,501 @@
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; 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::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 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(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 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() {
// 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))
}
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::{BotType, GameRules, PlayConfiguration, PrintableMap};
#[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,
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());
}
}

View File

@ -0,0 +1,587 @@
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<Coordinates>,
pub failed_strikes: Vec<Coordinates>,
pub sunk_boats: Vec<BoatPosition>,
}
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<Coordinates> {
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<Coordinates> {
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<Coordinates> {
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<Coordinates> {
// 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<Coordinates> {
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<usize> {
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<Coordinates> {
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)));
}
}

View File

@ -0,0 +1,117 @@
use crate::data::boats_layout::{BoatsLayout, Coordinates};
use crate::data::*;
#[derive(Debug, Copy, Clone, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
pub enum FireResult {
Missed,
Hit,
Sunk(BoatPosition),
Rejected,
AlreadyTargetedPosition,
}
pub struct GameMap {
rules: GameRules,
boats_config: BoatsLayout,
failed_strikes: Vec<Coordinates>,
successful_strikes: Vec<Coordinates>,
sunk_boats: Vec<BoatPosition>,
}
impl GameMap {
pub fn new(rules: GameRules, boats_config: BoatsLayout) -> Self {
Self {
rules,
boats_config,
failed_strikes: vec![],
successful_strikes: vec![],
sunk_boats: vec![],
}
}
pub fn get_cell_content(&self, c: Coordinates) -> MapCellContent {
if !c.is_valid(&self.rules) {
return MapCellContent::Invalid;
}
if self.failed_strikes.contains(&c) {
return MapCellContent::FailedStrike;
}
if let Some(b) = self.boats_config.find_boat_at_position(c) {
if !self.successful_strikes.contains(&c) {
return MapCellContent::Boat;
}
if self.sunk_boats.contains(b) {
return MapCellContent::SunkBoat;
}
return MapCellContent::TouchedBoat;
}
MapCellContent::Nothing
}
pub fn fire(&mut self, c: Coordinates) -> FireResult {
if !c.is_valid(&self.rules) {
return FireResult::Rejected;
}
if self.failed_strikes.contains(&c) || self.successful_strikes.contains(&c) {
return FireResult::AlreadyTargetedPosition;
}
match self.boats_config.find_boat_at_position(c) {
None => {
self.failed_strikes.push(c);
FireResult::Missed
}
Some(b) => {
self.successful_strikes.push(c);
if !b
.all_coordinates()
.iter()
.all(|c| self.successful_strikes.contains(c))
{
return FireResult::Hit;
}
self.sunk_boats.push(*b);
if !self.rules.boats_can_touch {
for c in b.neighbor_coordinates(&self.rules) {
if !self.failed_strikes.contains(&c) {
self.failed_strikes.push(c);
}
}
}
FireResult::Sunk(*b)
}
}
}
pub fn are_all_boat_sunk(&self) -> bool {
self.sunk_boats.len() == self.boats_config.number_of_boats()
}
pub fn current_map_status(&self, for_opponent: bool) -> CurrentGameMapStatus {
CurrentGameMapStatus {
boats: match for_opponent {
true => BoatsLayout::new_invalid(),
false => self.boats_config.clone(),
},
successful_strikes: self.successful_strikes.clone(),
failed_strikes: self.failed_strikes.clone(),
sunk_boats: self.sunk_boats.clone(),
}
}
}
impl PrintableMap for GameMap {
fn map_cell_content(&self, c: Coordinates) -> MapCellContent {
self.get_cell_content(c)
}
}

View File

@ -0,0 +1,122 @@
use crate::consts::*;
use crate::data::{BotType, PlayConfiguration};
use serde_with::{serde_as, DisplayFromStr};
#[serde_as]
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, Eq, PartialEq)]
pub struct GameRules {
#[serde_as(as = "DisplayFromStr")]
pub map_width: usize,
#[serde_as(as = "DisplayFromStr")]
pub map_height: usize,
#[serde_as(as = "DisplayFromStr")]
pub boats_str: String,
#[serde_as(as = "DisplayFromStr")]
pub boats_can_touch: bool,
#[serde_as(as = "DisplayFromStr")]
pub player_continue_on_hit: bool,
pub bot_type: BotType,
}
impl Default for GameRules {
fn default() -> Self {
Self::random_players_rules()
}
}
impl GameRules {
pub fn random_players_rules() -> Self {
Self {
map_width: MULTI_PLAYER_MAP_WIDTH,
map_height: MULTI_PLAYER_MAP_HEIGHT,
boats_str: MULTI_PLAYER_PLAYER_BOATS
.iter()
.map(usize::to_string)
.collect::<Vec<_>>()
.join(","),
boats_can_touch: MULTI_PLAYER_BOATS_CAN_TOUCH,
player_continue_on_hit: MULTI_PLAYER_PLAYER_CAN_CONTINUE_AFTER_HIT,
bot_type: BotType::Random,
}
}
pub fn with_bot_type(mut self, t: BotType) -> Self {
self.bot_type = t;
self
}
pub fn with_player_continue_on_hit(mut self, c: bool) -> Self {
self.player_continue_on_hit = c;
self
}
/// 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::<Vec<_>>()
.join(",");
}
/// Get the list of boats for this configuration
pub fn boats_list(&self) -> Vec<usize> {
self.boats_str
.split(',')
.map(|s| s.parse::<usize>().unwrap_or_default())
.collect()
}
/// Remove last boat
pub fn remove_last_boat(&mut self) -> usize {
let list = self.boats_list();
self.set_boats_list(&list[0..list.len() - 1]);
*list.last().unwrap()
}
/// Add a boat to the list of boats
pub fn add_boat(&mut self, len: usize) {
let mut list = self.boats_list();
list.push(len);
self.set_boats_list(&list[0..list.len() - 1]);
}
/// Check game rules errors
pub fn get_errors(&self) -> Vec<&str> {
let config = PlayConfiguration::default();
let mut errors = vec![];
if self.map_width < config.min_map_width || self.map_width > config.max_map_width {
errors.push("Map width is outside bounds!");
}
if self.map_height < config.min_map_height || self.map_height > config.max_map_height {
errors.push("Map height is outside bounds!");
}
if self.boats_list().len() < config.min_boats_number
|| self.boats_list().len() > config.max_boats_number
{
errors.push("Number of boats is invalid!");
}
for boat in self.boats_list() {
if boat < config.min_boat_len || boat > config.max_boat_len {
errors.push("A boat has an invalid length");
}
}
errors
}
}
#[cfg(test)]
mod test {
use crate::data::GameRules;
#[test]
fn multi_players_config() {
assert!(GameRules::random_players_rules().get_errors().is_empty());
}
}

View File

@ -0,0 +1,13 @@
pub use boats_layout::*;
pub use current_game_status::*;
pub use game_map::*;
pub use game_rules::*;
pub use play_config::*;
pub use printable_map::*;
mod boats_layout;
mod current_game_status;
mod game_map;
mod game_rules;
mod play_config;
mod printable_map;

View File

@ -0,0 +1,69 @@
use crate::consts::*;
/// Specifies the kind of boat to use
#[derive(serde::Serialize, serde::Deserialize, Debug, Copy, Clone, Eq, PartialEq)]
pub enum BotType {
Random,
Linear,
Intermediate,
Smart,
}
#[derive(serde::Serialize)]
pub struct BotDescription {
r#type: BotType,
name: &'static str,
description: &'static str,
}
#[derive(serde::Serialize)]
pub struct PlayConfiguration {
pub min_boat_len: usize,
pub max_boat_len: usize,
pub min_map_width: usize,
pub max_map_width: usize,
pub min_map_height: usize,
pub max_map_height: usize,
pub min_boats_number: usize,
pub max_boats_number: usize,
pub bot_types: Vec<BotDescription>,
pub ordinate_alphabet: &'static str,
}
impl Default for PlayConfiguration {
fn default() -> Self {
Self {
min_boat_len: MIN_BOATS_LENGTH,
max_boat_len: MAX_BOATS_LENGTH,
min_map_width: MIN_MAP_WIDTH,
max_map_width: MAX_MAP_WIDTH,
min_map_height: MIN_MAP_HEIGHT,
max_map_height: MAX_MAP_HEIGHT,
min_boats_number: MIN_BOATS_NUMBER,
max_boats_number: MAX_BOATS_NUMBER,
bot_types: vec![
BotDescription {
r#type: BotType::Linear,
name: "Linear",
description: "Linear strike. Shoot A1, A2, A3, ..., B1, B2, ...",
},
BotDescription {
r#type: BotType::Random,
name: "Ranom",
description: "Random search. Random strike.",
},
BotDescription {
r#type: BotType::Intermediate,
name: "Intermediate",
description: "Random search. Intelligent strike.",
},
BotDescription {
r#type: BotType::Smart,
name: "Smart",
description: "Smart search. Smart strike.",
},
],
ordinate_alphabet: ALPHABET,
}
}
}

View File

@ -0,0 +1,66 @@
use std::fmt::Write;
use std::string::String;
use crate::data::Coordinates;
#[derive(Debug, Copy, Clone, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
pub enum MapCellContent {
Invalid,
Nothing,
TouchedBoat,
SunkBoat,
Boat,
FailedStrike,
}
impl MapCellContent {
pub fn letter(&self) -> &'static str {
match self {
MapCellContent::Invalid => "!",
MapCellContent::Nothing => ".",
MapCellContent::TouchedBoat => "T",
MapCellContent::SunkBoat => "S",
MapCellContent::Boat => "B",
MapCellContent::FailedStrike => "x",
}
}
}
pub trait PrintableMap {
fn map_cell_content(&self, c: Coordinates) -> MapCellContent;
fn print_map(&self) {
print!("{}", self.get_map());
}
fn get_map(&self) -> String {
let mut out = String::with_capacity(100);
let mut y = 0;
let mut x;
loop {
x = 0;
loop {
let content = self.map_cell_content(Coordinates::new(x, y));
if content == MapCellContent::Invalid {
break;
}
write!(out, "{} ", content.letter()).unwrap();
x += 1;
}
out.push('\n');
// x == 0 <=> we reached the end of the map
if x == 0 {
break;
}
y += 1;
}
out
}
}