Add support for linear bots
This commit is contained in:
		
							
								
								
									
										87
									
								
								sea_battle_backend/src/bots/linear_bot.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								sea_battle_backend/src/bots/linear_bot.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,87 @@
 | 
			
		||||
use actix::Addr;
 | 
			
		||||
use uuid::Uuid;
 | 
			
		||||
 | 
			
		||||
use crate::data::{BoatsLayout, Coordinates, CurrentGameStatus, EndGameMap, FireResult, GameRules};
 | 
			
		||||
use crate::game::{Fire, Game, Player, RespondRequestRematch, SetBoatsLayout};
 | 
			
		||||
 | 
			
		||||
#[derive(Clone)]
 | 
			
		||||
pub struct LinearBot {
 | 
			
		||||
    game: Addr<Game>,
 | 
			
		||||
    uuid: Uuid,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl LinearBot {
 | 
			
		||||
    pub fn new(game: Addr<Game>) -> Self {
 | 
			
		||||
        Self {
 | 
			
		||||
            game,
 | 
			
		||||
            uuid: Uuid::new_v4(),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Player for LinearBot {
 | 
			
		||||
    fn get_name(&self) -> &str {
 | 
			
		||||
        "Linear Bot"
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn get_uid(&self) -> Uuid {
 | 
			
		||||
        self.uuid
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn is_bot(&self) -> bool {
 | 
			
		||||
        true
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn set_other_player_name(&self, _name: &str) {}
 | 
			
		||||
 | 
			
		||||
    fn query_boats_layout(&self, rules: &GameRules) {
 | 
			
		||||
        match BoatsLayout::gen_random_for_rules(rules) {
 | 
			
		||||
            Ok(layout) => self.game.do_send(SetBoatsLayout(self.uuid, layout)),
 | 
			
		||||
 | 
			
		||||
            Err(e) => log::error!(
 | 
			
		||||
                "Failed to use game rules to construct boats layout: {:?}",
 | 
			
		||||
                e
 | 
			
		||||
            ),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn rejected_boats_layout(&self, _errors: Vec<&'static str>) {
 | 
			
		||||
        unreachable!()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn notify_other_player_ready(&self) {}
 | 
			
		||||
 | 
			
		||||
    fn notify_game_starting(&self) {}
 | 
			
		||||
 | 
			
		||||
    fn request_fire(&self, status: CurrentGameStatus) {
 | 
			
		||||
        self.game
 | 
			
		||||
            .do_send(Fire(self.uuid, status.find_first_valid_fire_location()));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn opponent_must_fire(&self, _status: CurrentGameStatus) {}
 | 
			
		||||
 | 
			
		||||
    fn strike_result(&self, _c: Coordinates, _res: FireResult) {}
 | 
			
		||||
 | 
			
		||||
    fn other_player_strike_result(&self, _c: Coordinates, _res: FireResult) {}
 | 
			
		||||
 | 
			
		||||
    fn lost_game(&self, _your_map: EndGameMap, _opponent_map: EndGameMap) {}
 | 
			
		||||
 | 
			
		||||
    fn won_game(&self, _your_map: EndGameMap, _opponent_map: EndGameMap) {}
 | 
			
		||||
 | 
			
		||||
    fn opponent_requested_rematch(&self) {
 | 
			
		||||
        self.game.do_send(RespondRequestRematch(self.uuid, true));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn opponent_rejected_rematch(&self) {}
 | 
			
		||||
 | 
			
		||||
    fn opponent_accepted_rematch(&self) {}
 | 
			
		||||
 | 
			
		||||
    fn opponent_left_game(&self) {
 | 
			
		||||
        // Human are not reliable lol
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn opponent_replaced_by_bot(&self) {
 | 
			
		||||
        // Not such a good idea. will panic, just in case
 | 
			
		||||
        panic!("Bot shall not play against each other (it is completely useless)");
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										2
									
								
								sea_battle_backend/src/bots/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								sea_battle_backend/src/bots/mod.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,2 @@
 | 
			
		||||
pub mod linear_bot;
 | 
			
		||||
pub mod random_bot;
 | 
			
		||||
@@ -284,7 +284,7 @@ impl BoatsLayout {
 | 
			
		||||
mod test {
 | 
			
		||||
    use crate::data::boats_layout::{BoatDirection, BoatPosition, BoatsLayout, Coordinates};
 | 
			
		||||
    use crate::data::game_map::GameMap;
 | 
			
		||||
    use crate::data::{BotType, GameRules, PlayConfiguration};
 | 
			
		||||
    use crate::data::{BotType, GameRules, PlayConfiguration, PrintableMap};
 | 
			
		||||
 | 
			
		||||
    #[test]
 | 
			
		||||
    fn get_boat_coordinates() {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,10 @@
 | 
			
		||||
use rand::RngCore;
 | 
			
		||||
 | 
			
		||||
use crate::data::{BoatPosition, BoatsLayout, Coordinates, GameRules};
 | 
			
		||||
use crate::data::{
 | 
			
		||||
    BoatPosition, BoatsLayout, Coordinates, GameRules, MapCellContent, PrintableMap,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, serde::Serialize, serde::Deserialize)]
 | 
			
		||||
#[derive(Debug, serde::Serialize, serde::Deserialize, Clone)]
 | 
			
		||||
pub struct CurrentGameMapStatus {
 | 
			
		||||
    pub boats: BoatsLayout,
 | 
			
		||||
    pub successful_strikes: Vec<Coordinates>,
 | 
			
		||||
@@ -10,7 +12,46 @@ pub struct CurrentGameMapStatus {
 | 
			
		||||
    pub sunk_boats: Vec<BoatPosition>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, serde::Serialize, serde::Deserialize)]
 | 
			
		||||
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)]
 | 
			
		||||
pub struct CurrentGameStatus {
 | 
			
		||||
    pub rules: GameRules,
 | 
			
		||||
    pub your_map: CurrentGameMapStatus,
 | 
			
		||||
@@ -37,4 +78,26 @@ impl CurrentGameStatus {
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// 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 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()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,7 @@
 | 
			
		||||
use crate::data::boats_layout::{BoatsLayout, Coordinates};
 | 
			
		||||
use crate::data::{BoatPosition, CurrentGameMapStatus, EndGameMap, GameRules};
 | 
			
		||||
use crate::data::{
 | 
			
		||||
    BoatPosition, CurrentGameMapStatus, EndGameMap, GameRules, MapCellContent, PrintableMap,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
 | 
			
		||||
pub enum FireResult {
 | 
			
		||||
@@ -10,29 +12,6 @@ pub enum FireResult {
 | 
			
		||||
    AlreadyTargetedPosition,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[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 struct GameMap {
 | 
			
		||||
    rules: GameRules,
 | 
			
		||||
    boats_config: BoatsLayout,
 | 
			
		||||
@@ -120,19 +99,6 @@ impl GameMap {
 | 
			
		||||
        self.sunk_boats.len() == self.boats_config.number_of_boats()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn print_map(&self) {
 | 
			
		||||
        for y in 0..self.rules.map_height {
 | 
			
		||||
            for x in 0..self.rules.map_width {
 | 
			
		||||
                print!(
 | 
			
		||||
                    "{} ",
 | 
			
		||||
                    self.get_cell_content(Coordinates::new(x as i32, y as i32))
 | 
			
		||||
                        .letter()
 | 
			
		||||
                );
 | 
			
		||||
            }
 | 
			
		||||
            println!();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn current_map_status(&self, for_opponent: bool) -> CurrentGameMapStatus {
 | 
			
		||||
        CurrentGameMapStatus {
 | 
			
		||||
            boats: match for_opponent {
 | 
			
		||||
@@ -158,3 +124,9 @@ impl GameMap {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl PrintableMap for GameMap {
 | 
			
		||||
    fn map_cell_content(&self, c: Coordinates) -> MapCellContent {
 | 
			
		||||
        self.get_cell_content(c)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -27,6 +27,11 @@ impl GameRules {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn with_bot_type(mut self, t: BotType) -> Self {
 | 
			
		||||
        self.bot_type = t;
 | 
			
		||||
        self
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Set the list of boats for this configuration
 | 
			
		||||
    pub fn set_boats_list(&mut self, boats: &[usize]) {
 | 
			
		||||
        self.boats_str = boats
 | 
			
		||||
 
 | 
			
		||||
@@ -4,6 +4,7 @@ pub use end_game_map::*;
 | 
			
		||||
pub use game_map::*;
 | 
			
		||||
pub use game_rules::*;
 | 
			
		||||
pub use play_config::*;
 | 
			
		||||
pub use printable_map::*;
 | 
			
		||||
 | 
			
		||||
mod boats_layout;
 | 
			
		||||
mod current_game_status;
 | 
			
		||||
@@ -11,3 +12,4 @@ mod end_game_map;
 | 
			
		||||
mod game_map;
 | 
			
		||||
mod game_rules;
 | 
			
		||||
mod play_config;
 | 
			
		||||
mod printable_map;
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,7 @@ use crate::consts::*;
 | 
			
		||||
#[derive(serde::Serialize, serde::Deserialize, Debug, Copy, Clone, Eq, PartialEq)]
 | 
			
		||||
pub enum BotType {
 | 
			
		||||
    Random,
 | 
			
		||||
    // TODO : LinearShooting
 | 
			
		||||
    Linear,
 | 
			
		||||
    // TODO : GridBot
 | 
			
		||||
    // TODO : SmartBot
 | 
			
		||||
}
 | 
			
		||||
@@ -40,10 +40,16 @@ impl Default for PlayConfiguration {
 | 
			
		||||
            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::Random,
 | 
			
		||||
                description: "Random strike. All the time.".to_string(),
 | 
			
		||||
            }],
 | 
			
		||||
            bot_types: vec![
 | 
			
		||||
                BotDescription {
 | 
			
		||||
                    r#type: BotType::Linear,
 | 
			
		||||
                    description: "Linear strike. Shoot A1, A2, A3, ..., B1, B2, ...".to_string(),
 | 
			
		||||
                },
 | 
			
		||||
                BotDescription {
 | 
			
		||||
                    r#type: BotType::Random,
 | 
			
		||||
                    description: "Random strike. All the time.".to_string(),
 | 
			
		||||
                },
 | 
			
		||||
            ],
 | 
			
		||||
            ordinate_alphabet: ALPHABET,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										55
									
								
								sea_battle_backend/src/data/printable_map.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								sea_battle_backend/src/data/printable_map.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,55 @@
 | 
			
		||||
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) {
 | 
			
		||||
        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;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                print!("{} ", content.letter());
 | 
			
		||||
                x += 1;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            println!();
 | 
			
		||||
 | 
			
		||||
            // x == 0 <=> we reached the end of the map
 | 
			
		||||
            if x == 0 {
 | 
			
		||||
                break;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            y += 1;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -4,8 +4,8 @@ use actix::prelude::*;
 | 
			
		||||
use actix::{Actor, Context, Handler};
 | 
			
		||||
use uuid::Uuid;
 | 
			
		||||
 | 
			
		||||
use crate::bots::random_bot::RandomBot;
 | 
			
		||||
use crate::data::*;
 | 
			
		||||
use crate::random_bot::RandomBot;
 | 
			
		||||
 | 
			
		||||
pub trait Player {
 | 
			
		||||
    fn get_name(&self) -> &str;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,18 +1,19 @@
 | 
			
		||||
use std::sync::Arc;
 | 
			
		||||
use std::time::{Duration, Instant};
 | 
			
		||||
 | 
			
		||||
use crate::bots::linear_bot::LinearBot;
 | 
			
		||||
use actix::prelude::*;
 | 
			
		||||
use actix::{Actor, Handler, StreamHandler};
 | 
			
		||||
use actix_web_actors::ws;
 | 
			
		||||
use actix_web_actors::ws::{CloseCode, CloseReason, Message, ProtocolError, WebsocketContext};
 | 
			
		||||
use uuid::Uuid;
 | 
			
		||||
 | 
			
		||||
use crate::bots::random_bot::RandomBot;
 | 
			
		||||
use crate::data::{
 | 
			
		||||
    BoatsLayout, BotType, Coordinates, CurrentGameStatus, EndGameMap, FireResult, GameRules,
 | 
			
		||||
};
 | 
			
		||||
use crate::game::{AddPlayer, Game};
 | 
			
		||||
use crate::human_player::HumanPlayer;
 | 
			
		||||
use crate::random_bot::RandomBot;
 | 
			
		||||
 | 
			
		||||
/// How often heartbeat pings are sent
 | 
			
		||||
const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(10);
 | 
			
		||||
@@ -41,7 +42,7 @@ pub enum ClientMessage {
 | 
			
		||||
 | 
			
		||||
#[derive(Message)]
 | 
			
		||||
#[rtype(result = "()")]
 | 
			
		||||
#[derive(serde::Serialize, serde::Deserialize, Debug)]
 | 
			
		||||
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
 | 
			
		||||
#[serde(tag = "type")]
 | 
			
		||||
pub enum ServerMessage {
 | 
			
		||||
    WaitingForAnotherPlayer,
 | 
			
		||||
@@ -147,6 +148,9 @@ impl Actor for HumanPlayerWS {
 | 
			
		||||
                    BotType::Random => {
 | 
			
		||||
                        game.do_send(AddPlayer(Arc::new(RandomBot::new(game.clone()))));
 | 
			
		||||
                    }
 | 
			
		||||
                    BotType::Linear => {
 | 
			
		||||
                        game.do_send(AddPlayer(Arc::new(LinearBot::new(game.clone()))));
 | 
			
		||||
                    }
 | 
			
		||||
                };
 | 
			
		||||
 | 
			
		||||
                let player = Arc::new(HumanPlayer {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,12 +1,12 @@
 | 
			
		||||
extern crate core;
 | 
			
		||||
 | 
			
		||||
pub mod args;
 | 
			
		||||
pub mod bots;
 | 
			
		||||
pub mod consts;
 | 
			
		||||
pub mod data;
 | 
			
		||||
pub mod game;
 | 
			
		||||
pub mod human_player;
 | 
			
		||||
pub mod human_player_ws;
 | 
			
		||||
pub mod random_bot;
 | 
			
		||||
pub mod server;
 | 
			
		||||
#[cfg(test)]
 | 
			
		||||
mod test;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,10 +1,11 @@
 | 
			
		||||
use crate::args::Args;
 | 
			
		||||
use crate::data::{GameRules, PlayConfiguration};
 | 
			
		||||
use crate::human_player_ws::{HumanPlayerWS, StartMode};
 | 
			
		||||
use actix_cors::Cors;
 | 
			
		||||
use actix_web::{web, App, HttpRequest, HttpResponse, HttpServer, Responder};
 | 
			
		||||
use actix_web_actors::ws;
 | 
			
		||||
 | 
			
		||||
use crate::args::Args;
 | 
			
		||||
use crate::data::{GameRules, PlayConfiguration};
 | 
			
		||||
use crate::human_player_ws::{HumanPlayerWS, StartMode};
 | 
			
		||||
 | 
			
		||||
/// The default '/' route
 | 
			
		||||
async fn index() -> impl Responder {
 | 
			
		||||
    HttpResponse::Ok().json("Sea battle backend")
 | 
			
		||||
@@ -54,6 +55,8 @@ pub async fn start_server(args: Args) -> std::io::Result<()> {
 | 
			
		||||
            .wrap(cors)
 | 
			
		||||
            .route("/config", web::get().to(game_configuration))
 | 
			
		||||
            .route("/play/bot", web::get().to(start_bot_play))
 | 
			
		||||
            // TODO : create & accept invite
 | 
			
		||||
            // TODO : join random
 | 
			
		||||
            .route("/", web::get().to(index))
 | 
			
		||||
            .route("{tail:.*}", web::get().to(not_found))
 | 
			
		||||
    })
 | 
			
		||||
 
 | 
			
		||||
@@ -20,6 +20,7 @@ pub struct BotClient {
 | 
			
		||||
    requested_rules: GameRules,
 | 
			
		||||
    layout: Option<BoatsLayout>,
 | 
			
		||||
    number_plays: usize,
 | 
			
		||||
    server_msg_callback: Option<Box<dyn Fn(&ServerMessage)>>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl BotClient {
 | 
			
		||||
@@ -32,6 +33,7 @@ impl BotClient {
 | 
			
		||||
            requested_rules: GameRules::random_players_rules(),
 | 
			
		||||
            layout: None,
 | 
			
		||||
            number_plays: 1,
 | 
			
		||||
            server_msg_callback: None,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -50,6 +52,14 @@ impl BotClient {
 | 
			
		||||
        self
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn with_server_msg_callback<F>(mut self, cb: F) -> Self
 | 
			
		||||
    where
 | 
			
		||||
        F: Fn(&ServerMessage) + 'static,
 | 
			
		||||
    {
 | 
			
		||||
        self.server_msg_callback = Some(Box::new(cb));
 | 
			
		||||
        self
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub async fn run_client(&self) -> Result<ClientEndResult, Box<dyn Error>> {
 | 
			
		||||
        let mut remaining_games = self.number_plays;
 | 
			
		||||
 | 
			
		||||
@@ -97,6 +107,10 @@ impl BotClient {
 | 
			
		||||
                }
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            if let Some(cb) = &self.server_msg_callback {
 | 
			
		||||
                (cb)(&message)
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            match message {
 | 
			
		||||
                ServerMessage::WaitingForAnotherPlayer => {
 | 
			
		||||
                    log::debug!("Waiting for other player...")
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										51
									
								
								sea_battle_backend/src/test/bot_client_bot_linear_play.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								sea_battle_backend/src/test/bot_client_bot_linear_play.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,51 @@
 | 
			
		||||
use tokio::task;
 | 
			
		||||
 | 
			
		||||
use crate::args::Args;
 | 
			
		||||
use crate::data::{BotType, Coordinates, GameRules};
 | 
			
		||||
use crate::human_player_ws::ServerMessage;
 | 
			
		||||
use crate::server::start_server;
 | 
			
		||||
use crate::test::bot_client::ClientEndResult;
 | 
			
		||||
use crate::test::network_utils::wait_for_port;
 | 
			
		||||
use crate::test::{bot_client, TestPort};
 | 
			
		||||
 | 
			
		||||
fn check_strikes_are_linear(msg: &ServerMessage) {
 | 
			
		||||
    if let ServerMessage::RequestFire { status } = msg {
 | 
			
		||||
        let mut in_fire_location = true;
 | 
			
		||||
        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 in_fire_location {
 | 
			
		||||
                    in_fire_location = status.your_map.did_fire_at_location(c);
 | 
			
		||||
                } else if status.your_map.did_fire_at_location(c) {
 | 
			
		||||
                    println!("Your map:");
 | 
			
		||||
                    status.print_your_map();
 | 
			
		||||
                    println!("Opponent map:");
 | 
			
		||||
                    status.print_opponent_map();
 | 
			
		||||
                    panic!("Found invalid fire location for linear bot!");
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[tokio::test]
 | 
			
		||||
async fn full_game() {
 | 
			
		||||
    let _ = env_logger::builder().is_test(true).try_init();
 | 
			
		||||
 | 
			
		||||
    let local_set = task::LocalSet::new();
 | 
			
		||||
    local_set
 | 
			
		||||
        .run_until(async move {
 | 
			
		||||
            task::spawn_local(start_server(Args::for_test(TestPort::LinearBotFullGame)));
 | 
			
		||||
            wait_for_port(TestPort::LinearBotFullGame.port()).await;
 | 
			
		||||
 | 
			
		||||
            let res = bot_client::BotClient::new(TestPort::LinearBotFullGame.as_url())
 | 
			
		||||
                .with_rules(GameRules::random_players_rules().with_bot_type(BotType::Linear))
 | 
			
		||||
                .with_server_msg_callback(check_strikes_are_linear)
 | 
			
		||||
                .run_client()
 | 
			
		||||
                .await
 | 
			
		||||
                .unwrap();
 | 
			
		||||
            assert_eq!(res, ClientEndResult::Finished);
 | 
			
		||||
        })
 | 
			
		||||
        .await;
 | 
			
		||||
}
 | 
			
		||||
@@ -72,11 +72,11 @@ async fn full_game_no_touching_boats() {
 | 
			
		||||
            let mut rules = GameRules::random_players_rules();
 | 
			
		||||
            rules.boats_can_touch = false;
 | 
			
		||||
            task::spawn_local(start_server(Args::for_test(
 | 
			
		||||
                TestPort::FullGameTouchingBoats,
 | 
			
		||||
                TestPort::FullGameNoTouchingBoats,
 | 
			
		||||
            )));
 | 
			
		||||
            wait_for_port(TestPort::FullGameTouchingBoats.port()).await;
 | 
			
		||||
            wait_for_port(TestPort::FullGameNoTouchingBoats.port()).await;
 | 
			
		||||
 | 
			
		||||
            let res = bot_client::BotClient::new(TestPort::FullGameTouchingBoats.as_url())
 | 
			
		||||
            let res = bot_client::BotClient::new(TestPort::FullGameNoTouchingBoats.as_url())
 | 
			
		||||
                .with_rules(rules)
 | 
			
		||||
                .run_client()
 | 
			
		||||
                .await
 | 
			
		||||
@@ -97,20 +97,21 @@ async fn invalid_boats_layout_number_of_boats() {
 | 
			
		||||
            let mut rules = GameRules::random_players_rules();
 | 
			
		||||
            rules.boats_can_touch = false;
 | 
			
		||||
            task::spawn_local(start_server(Args::for_test(
 | 
			
		||||
                TestPort::FullGameTouchingBoats,
 | 
			
		||||
                TestPort::InvalidBoatsLayoutNumberOfBoats,
 | 
			
		||||
            )));
 | 
			
		||||
            wait_for_port(TestPort::FullGameTouchingBoats.port()).await;
 | 
			
		||||
            wait_for_port(TestPort::InvalidBoatsLayoutNumberOfBoats.port()).await;
 | 
			
		||||
 | 
			
		||||
            let mut rules_modified = rules.clone();
 | 
			
		||||
            rules_modified.pop_boat();
 | 
			
		||||
            let layout = BoatsLayout::gen_random_for_rules(&rules_modified).unwrap();
 | 
			
		||||
 | 
			
		||||
            let res = bot_client::BotClient::new(&TestPort::FullGameTouchingBoats.as_url())
 | 
			
		||||
                .with_rules(rules)
 | 
			
		||||
                .with_layout(layout)
 | 
			
		||||
                .run_client()
 | 
			
		||||
                .await
 | 
			
		||||
                .unwrap();
 | 
			
		||||
            let res =
 | 
			
		||||
                bot_client::BotClient::new(&TestPort::InvalidBoatsLayoutNumberOfBoats.as_url())
 | 
			
		||||
                    .with_rules(rules)
 | 
			
		||||
                    .with_layout(layout)
 | 
			
		||||
                    .run_client()
 | 
			
		||||
                    .await
 | 
			
		||||
                    .unwrap();
 | 
			
		||||
 | 
			
		||||
            assert_eq!(res, ClientEndResult::InvalidBoatsLayout);
 | 
			
		||||
        })
 | 
			
		||||
@@ -127,16 +128,16 @@ async fn invalid_boats_layout_len_of_a_boat() {
 | 
			
		||||
            let mut rules = GameRules::random_players_rules();
 | 
			
		||||
            rules.boats_can_touch = false;
 | 
			
		||||
            task::spawn_local(start_server(Args::for_test(
 | 
			
		||||
                TestPort::FullGameTouchingBoats,
 | 
			
		||||
                TestPort::InvalidBoatsLayoutLenOfABoat,
 | 
			
		||||
            )));
 | 
			
		||||
            wait_for_port(TestPort::FullGameTouchingBoats.port()).await;
 | 
			
		||||
            wait_for_port(TestPort::InvalidBoatsLayoutLenOfABoat.port()).await;
 | 
			
		||||
 | 
			
		||||
            let mut rules_modified = rules.clone();
 | 
			
		||||
            let previous = rules_modified.pop_boat();
 | 
			
		||||
            rules_modified.add_boat(previous - 1);
 | 
			
		||||
            let layout = BoatsLayout::gen_random_for_rules(&rules_modified).unwrap();
 | 
			
		||||
 | 
			
		||||
            let res = bot_client::BotClient::new(&TestPort::FullGameTouchingBoats.as_url())
 | 
			
		||||
            let res = bot_client::BotClient::new(&TestPort::InvalidBoatsLayoutLenOfABoat.as_url())
 | 
			
		||||
                .with_rules(rules)
 | 
			
		||||
                .with_layout(layout)
 | 
			
		||||
                .run_client()
 | 
			
		||||
@@ -155,10 +156,12 @@ async fn full_game_multiple_rematches() {
 | 
			
		||||
    let local_set = task::LocalSet::new();
 | 
			
		||||
    local_set
 | 
			
		||||
        .run_until(async move {
 | 
			
		||||
            task::spawn_local(start_server(Args::for_test(TestPort::FullGame)));
 | 
			
		||||
            wait_for_port(TestPort::FullGame.port()).await;
 | 
			
		||||
            task::spawn_local(start_server(Args::for_test(
 | 
			
		||||
                TestPort::FullGameMultipleRematch,
 | 
			
		||||
            )));
 | 
			
		||||
            wait_for_port(TestPort::FullGameMultipleRematch.port()).await;
 | 
			
		||||
 | 
			
		||||
            let res = bot_client::BotClient::new(TestPort::FullGame.as_url())
 | 
			
		||||
            let res = bot_client::BotClient::new(TestPort::FullGameMultipleRematch.as_url())
 | 
			
		||||
                .with_number_plays(5)
 | 
			
		||||
                .run_client()
 | 
			
		||||
                .await
 | 
			
		||||
@@ -5,7 +5,11 @@ enum TestPort {
 | 
			
		||||
    ClientInvalidPort = 20000,
 | 
			
		||||
    ClientInvalidRules,
 | 
			
		||||
    FullGame,
 | 
			
		||||
    FullGameTouchingBoats,
 | 
			
		||||
    FullGameNoTouchingBoats,
 | 
			
		||||
    InvalidBoatsLayoutNumberOfBoats,
 | 
			
		||||
    InvalidBoatsLayoutLenOfABoat,
 | 
			
		||||
    FullGameMultipleRematch,
 | 
			
		||||
    LinearBotFullGame,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl TestPort {
 | 
			
		||||
@@ -29,5 +33,6 @@ impl Args {
 | 
			
		||||
 | 
			
		||||
#[cfg(test)]
 | 
			
		||||
pub mod bot_client;
 | 
			
		||||
mod bot_client_bot_play;
 | 
			
		||||
mod bot_client_bot_linear_play;
 | 
			
		||||
mod bot_client_bot_random_play;
 | 
			
		||||
mod network_utils;
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user