588 lines
18 KiB
Rust
588 lines
18 KiB
Rust
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)));
|
|
}
|
|
}
|