SeaBattle/sea_battle_backend/src/data/current_game_status.rs

296 lines
8.6 KiB
Rust
Raw Normal View History

2022-09-14 18:30:33 +02:00
use rand::RngCore;
2022-09-17 12:02:13 +02:00
use crate::data::{
BoatPosition, BoatsLayout, Coordinates, GameRules, MapCellContent, PrintableMap,
};
2022-09-15 20:13:06 +02:00
#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, Default)]
2022-09-15 20:13:06 +02:00
pub struct CurrentGameMapStatus {
pub boats: BoatsLayout,
pub successful_strikes: Vec<Coordinates>,
pub failed_strikes: Vec<Coordinates>,
pub sunk_boats: Vec<BoatPosition>,
}
2022-09-17 12:02:13 +02:00
impl CurrentGameMapStatus {
pub fn did_fire_at_location(&self, c: Coordinates) -> bool {
self.successful_strikes.contains(&c) || self.failed_strikes.contains(&c)
}
}
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)]
2022-09-14 18:30:33 +02:00
pub struct CurrentGameStatus {
pub rules: GameRules,
2022-09-15 20:13:06 +02:00
pub your_map: CurrentGameMapStatus,
pub opponent_map: CurrentGameMapStatus,
2022-09-14 18:30:33 +02:00
}
impl CurrentGameStatus {
2022-09-15 20:13:06 +02:00
/// Check if opponent can fire at a given location
pub fn can_fire_at_location(&self, location: Coordinates) -> bool {
!self.opponent_map.successful_strikes.contains(&location)
&& !self.opponent_map.failed_strikes.contains(&location)
2022-09-14 18:30:33 +02:00
}
/// 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;
}
}
}
2022-09-17 12:02:13 +02:00
/// 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!")
}
pub fn get_sunk_locations(&self) -> Vec<Coordinates> {
self.opponent_map
.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.opponent_map
.successful_strikes
.iter()
.filter(|c| !sunk_location.contains(c))
.map(Coordinates::clone)
.collect()
}
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.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
}
2022-09-17 12:02:13 +02:00
pub fn print_your_map(&self) {
PrintableCurrentGameMapStatus(self.rules.clone(), self.your_map.clone()).print_map()
}
pub fn print_opponent_map(&self) {
PrintableCurrentGameMapStatus(self.rules.clone(), self.opponent_map.clone()).print_map()
}
2022-09-14 18:30:33 +02:00
}
#[cfg(test)]
mod test {
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.get_sunk_locations(), vec![sunk]);
assert_eq!(
status.get_successful_but_un_sunk_locations(),
vec![unfinished]
);
}
#[test]
fn no_continue_attack() {
let status = CurrentGameStatus::default();
assert!(status.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()))
}
}