Add smart bot
This commit is contained in:
parent
e71b724278
commit
663a9c2d71
@ -1,3 +1,4 @@
|
|||||||
pub mod intermediate_bot;
|
pub mod intermediate_bot;
|
||||||
pub mod linear_bot;
|
pub mod linear_bot;
|
||||||
pub mod random_bot;
|
pub mod random_bot;
|
||||||
|
pub mod smart_bot;
|
||||||
|
97
sea_battle_backend/src/bots/smart_bot.rs
Normal file
97
sea_battle_backend/src/bots/smart_bot.rs
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
use actix::Addr;
|
||||||
|
use rand::RngCore;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::data::{BoatsLayout, Coordinates, CurrentGameStatus, FireResult, GameRules};
|
||||||
|
use crate::game::{Fire, Game, Player, RespondRequestRematch, SetBoatsLayout};
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct SmartBot {
|
||||||
|
game: Addr<Game>,
|
||||||
|
uuid: Uuid,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SmartBot {
|
||||||
|
pub fn new(game: Addr<Game>) -> Self {
|
||||||
|
Self {
|
||||||
|
game,
|
||||||
|
uuid: Uuid::new_v4(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Player for SmartBot {
|
||||||
|
fn get_name(&self) -> &str {
|
||||||
|
"Intermediate 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) {
|
||||||
|
let coordinates = status.continue_attack_boat().unwrap_or_else(|| {
|
||||||
|
let coordinates = status.get_relevant_grid_locations();
|
||||||
|
if !coordinates.is_empty() {
|
||||||
|
let pos = rand::thread_rng().next_u32() as usize;
|
||||||
|
coordinates[pos % coordinates.len()]
|
||||||
|
} else {
|
||||||
|
status.find_valid_random_fire_location()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
self.game.do_send(Fire(self.uuid, coordinates));
|
||||||
|
}
|
||||||
|
|
||||||
|
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, _status: CurrentGameStatus) {}
|
||||||
|
|
||||||
|
fn won_game(&self, _status: CurrentGameStatus) {}
|
||||||
|
|
||||||
|
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)");
|
||||||
|
}
|
||||||
|
}
|
@ -6,13 +6,14 @@ pub enum BotType {
|
|||||||
Random,
|
Random,
|
||||||
Linear,
|
Linear,
|
||||||
Intermediate,
|
Intermediate,
|
||||||
// TODO : SmartBot
|
Smart,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(serde::Serialize)]
|
#[derive(serde::Serialize)]
|
||||||
pub struct BotDescription {
|
pub struct BotDescription {
|
||||||
r#type: BotType,
|
r#type: BotType,
|
||||||
description: String,
|
name: &'static str,
|
||||||
|
description: &'static str,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(serde::Serialize)]
|
#[derive(serde::Serialize)]
|
||||||
@ -43,15 +44,23 @@ impl Default for PlayConfiguration {
|
|||||||
bot_types: vec![
|
bot_types: vec![
|
||||||
BotDescription {
|
BotDescription {
|
||||||
r#type: BotType::Linear,
|
r#type: BotType::Linear,
|
||||||
description: "Linear strike. Shoot A1, A2, A3, ..., B1, B2, ...".to_string(),
|
name: "Linear",
|
||||||
|
description: "Linear strike. Shoot A1, A2, A3, ..., B1, B2, ...",
|
||||||
},
|
},
|
||||||
BotDescription {
|
BotDescription {
|
||||||
r#type: BotType::Random,
|
r#type: BotType::Random,
|
||||||
description: "Random search. Random strike.".to_string(),
|
name: "Ranom",
|
||||||
|
description: "Random search. Random strike.",
|
||||||
},
|
},
|
||||||
BotDescription {
|
BotDescription {
|
||||||
r#type: BotType::Intermediate,
|
r#type: BotType::Intermediate,
|
||||||
description: "Randome search. Intelligent strike.".to_string(),
|
name: "Intermediate",
|
||||||
|
description: "Random search. Intelligent strike.",
|
||||||
|
},
|
||||||
|
BotDescription {
|
||||||
|
r#type: BotType::Smart,
|
||||||
|
name: "Smart",
|
||||||
|
description: "Smart search. Smart strike.",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
ordinate_alphabet: ALPHABET,
|
ordinate_alphabet: ALPHABET,
|
||||||
|
@ -10,6 +10,7 @@ use uuid::Uuid;
|
|||||||
use crate::bots::intermediate_bot::IntermediateBot;
|
use crate::bots::intermediate_bot::IntermediateBot;
|
||||||
use crate::bots::linear_bot::LinearBot;
|
use crate::bots::linear_bot::LinearBot;
|
||||||
use crate::bots::random_bot::RandomBot;
|
use crate::bots::random_bot::RandomBot;
|
||||||
|
use crate::bots::smart_bot::SmartBot;
|
||||||
use crate::data::{BoatsLayout, BotType, Coordinates, CurrentGameStatus, FireResult, GameRules};
|
use crate::data::{BoatsLayout, BotType, Coordinates, CurrentGameStatus, FireResult, GameRules};
|
||||||
use crate::game::{AddPlayer, Game};
|
use crate::game::{AddPlayer, Game};
|
||||||
use crate::human_player::HumanPlayer;
|
use crate::human_player::HumanPlayer;
|
||||||
@ -151,6 +152,9 @@ impl Actor for HumanPlayerWS {
|
|||||||
BotType::Intermediate => {
|
BotType::Intermediate => {
|
||||||
game.do_send(AddPlayer(Arc::new(IntermediateBot::new(game.clone()))));
|
game.do_send(AddPlayer(Arc::new(IntermediateBot::new(game.clone()))));
|
||||||
}
|
}
|
||||||
|
BotType::Smart => {
|
||||||
|
game.do_send(AddPlayer(Arc::new(SmartBot::new(game.clone()))));
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let player = Arc::new(HumanPlayer {
|
let player = Arc::new(HumanPlayer {
|
||||||
|
@ -9,7 +9,10 @@ use crate::human_player_ws::{ClientMessage, ServerMessage};
|
|||||||
|
|
||||||
#[derive(Debug, Eq, PartialEq)]
|
#[derive(Debug, Eq, PartialEq)]
|
||||||
pub enum ClientEndResult {
|
pub enum ClientEndResult {
|
||||||
Finished,
|
Finished {
|
||||||
|
number_victories: usize,
|
||||||
|
number_defeats: usize,
|
||||||
|
},
|
||||||
InvalidBoatsLayout,
|
InvalidBoatsLayout,
|
||||||
OpponentRejectedRematch,
|
OpponentRejectedRematch,
|
||||||
OpponentLeftGame,
|
OpponentLeftGame,
|
||||||
@ -62,6 +65,8 @@ impl BotClient {
|
|||||||
|
|
||||||
pub async fn run_client(&mut self) -> Result<ClientEndResult, Box<dyn Error>> {
|
pub async fn run_client(&mut self) -> Result<ClientEndResult, Box<dyn Error>> {
|
||||||
let mut remaining_games = self.number_plays;
|
let mut remaining_games = self.number_plays;
|
||||||
|
let mut number_victories = 0;
|
||||||
|
let mut number_defeats = 0;
|
||||||
|
|
||||||
let url = format!(
|
let url = format!(
|
||||||
"{}/play/bot?{}",
|
"{}/play/bot?{}",
|
||||||
@ -161,6 +166,8 @@ impl BotClient {
|
|||||||
result
|
result
|
||||||
),
|
),
|
||||||
ServerMessage::LostGame { status } => {
|
ServerMessage::LostGame { status } => {
|
||||||
|
number_defeats += 1;
|
||||||
|
|
||||||
log::debug!("We lost game :(");
|
log::debug!("We lost game :(");
|
||||||
log::debug!("Opponent map:\n{}", status.get_opponent_map());
|
log::debug!("Opponent map:\n{}", status.get_opponent_map());
|
||||||
log::debug!("Our map:\n{}\n", status.get_your_map());
|
log::debug!("Our map:\n{}\n", status.get_your_map());
|
||||||
@ -176,6 +183,8 @@ impl BotClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
ServerMessage::WonGame { status } => {
|
ServerMessage::WonGame { status } => {
|
||||||
|
number_victories += 1;
|
||||||
|
|
||||||
log::debug!("We won the game !!!!");
|
log::debug!("We won the game !!!!");
|
||||||
log::debug!("Opponent map:\n{}\n", status.get_opponent_map());
|
log::debug!("Opponent map:\n{}\n", status.get_opponent_map());
|
||||||
log::debug!("Our map:\n{}\n", status.get_your_map());
|
log::debug!("Our map:\n{}\n", status.get_your_map());
|
||||||
@ -216,6 +225,9 @@ impl BotClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(ClientEndResult::Finished)
|
Ok(ClientEndResult::Finished {
|
||||||
|
number_victories,
|
||||||
|
number_defeats,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -42,7 +42,7 @@ async fn full_game() {
|
|||||||
.run_client()
|
.run_client()
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(res, ClientEndResult::Finished);
|
assert!(matches!(res, ClientEndResult::Finished { .. }));
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
|
@ -46,7 +46,7 @@ async fn full_game() {
|
|||||||
.run_client()
|
.run_client()
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(res, ClientEndResult::Finished);
|
assert!(matches!(res, ClientEndResult::Finished { .. }));
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
@ -73,7 +73,7 @@ async fn full_game_no_replay_on_hit() {
|
|||||||
.run_client()
|
.run_client()
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(res, ClientEndResult::Finished);
|
assert!(matches!(res, ClientEndResult::Finished { .. }));
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
@ -101,7 +101,7 @@ async fn full_game_no_replay_on_hit_two() {
|
|||||||
.run_client()
|
.run_client()
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(res, ClientEndResult::Finished);
|
assert!(matches!(res, ClientEndResult::Finished { .. }));
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
@ -138,7 +138,7 @@ async fn full_game_with_replay_on_hit() {
|
|||||||
.run_client()
|
.run_client()
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(res, ClientEndResult::Finished);
|
assert!(matches!(res, ClientEndResult::Finished { .. }));
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
|
@ -60,7 +60,7 @@ async fn full_game() {
|
|||||||
.run_client()
|
.run_client()
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(res, ClientEndResult::Finished);
|
assert!(matches!(res, ClientEndResult::Finished { .. }));
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
@ -86,7 +86,7 @@ async fn full_game_no_touching_boats() {
|
|||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(res, ClientEndResult::Finished);
|
assert!(matches!(res, ClientEndResult::Finished { .. }));
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
@ -174,7 +174,7 @@ async fn full_game_multiple_rematches() {
|
|||||||
.run_client()
|
.run_client()
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(res, ClientEndResult::Finished);
|
assert!(matches!(res, ClientEndResult::Finished { .. }));
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
@ -197,7 +197,7 @@ async fn full_game_no_replay_on_hit() {
|
|||||||
.run_client()
|
.run_client()
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(res, ClientEndResult::Finished);
|
assert!(matches!(res, ClientEndResult::Finished { .. }));
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
|
96
sea_battle_backend/src/test/bot_client_bot_smart_play.rs
Normal file
96
sea_battle_backend/src/test/bot_client_bot_smart_play.rs
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
use tokio::task;
|
||||||
|
|
||||||
|
use crate::args::Args;
|
||||||
|
use crate::data::{BotType, CurrentGameStatus, 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};
|
||||||
|
|
||||||
|
#[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::IntermediateBotFullGame,
|
||||||
|
)));
|
||||||
|
wait_for_port(TestPort::IntermediateBotFullGame.port()).await;
|
||||||
|
|
||||||
|
let mut curr_status = CurrentGameStatus::default();
|
||||||
|
|
||||||
|
let res = bot_client::BotClient::new(TestPort::IntermediateBotFullGame.as_url())
|
||||||
|
.with_rules(GameRules::random_players_rules().with_bot_type(BotType::Smart))
|
||||||
|
.with_server_msg_callback(move |msg| match msg {
|
||||||
|
ServerMessage::OpponentMustFire { status } => {
|
||||||
|
curr_status = status.clone();
|
||||||
|
}
|
||||||
|
ServerMessage::OpponentFireResult { pos, .. } => {
|
||||||
|
let pending_sunk_loc =
|
||||||
|
curr_status.your_map.get_successful_but_un_sunk_locations();
|
||||||
|
|
||||||
|
if !pending_sunk_loc.is_empty() {
|
||||||
|
log::debug!("Check if fire was smart...");
|
||||||
|
assert!(pending_sunk_loc.iter().any(|l| pos.dist_with(l) <= 1))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
})
|
||||||
|
.run_client()
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert!(matches!(res, ClientEndResult::Finished { .. }));
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn full_game_multiple_rematches() {
|
||||||
|
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::IntermediateBotFullGame,
|
||||||
|
)));
|
||||||
|
wait_for_port(TestPort::IntermediateBotFullGame.port()).await;
|
||||||
|
|
||||||
|
let mut curr_status = CurrentGameStatus::default();
|
||||||
|
|
||||||
|
let res = bot_client::BotClient::new(TestPort::IntermediateBotFullGame.as_url())
|
||||||
|
.with_rules(GameRules::random_players_rules().with_bot_type(BotType::Smart))
|
||||||
|
.with_server_msg_callback(move |msg| match msg {
|
||||||
|
ServerMessage::OpponentMustFire { status } => {
|
||||||
|
curr_status = status.clone();
|
||||||
|
}
|
||||||
|
ServerMessage::OpponentFireResult { pos, .. } => {
|
||||||
|
let pending_sunk_loc =
|
||||||
|
curr_status.your_map.get_successful_but_un_sunk_locations();
|
||||||
|
|
||||||
|
if !pending_sunk_loc.is_empty() {
|
||||||
|
log::debug!("Check if fire was smart...");
|
||||||
|
assert!(pending_sunk_loc.iter().any(|l| pos.dist_with(l) <= 1))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
})
|
||||||
|
.with_number_plays(20)
|
||||||
|
.run_client()
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
if let ClientEndResult::Finished { number_defeats, .. } = res {
|
||||||
|
assert!(
|
||||||
|
number_defeats > 15,
|
||||||
|
"number of defeats = {} which is < 15",
|
||||||
|
number_defeats
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
assert_eq!(0, 1, "Client did not finish correctly");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
}
|
@ -39,5 +39,6 @@ pub mod bot_client;
|
|||||||
mod bot_client_bot_intermediate_play;
|
mod bot_client_bot_intermediate_play;
|
||||||
mod bot_client_bot_linear_play;
|
mod bot_client_bot_linear_play;
|
||||||
mod bot_client_bot_random_play;
|
mod bot_client_bot_random_play;
|
||||||
|
mod bot_client_bot_smart_play;
|
||||||
mod network_utils;
|
mod network_utils;
|
||||||
mod play_utils;
|
mod play_utils;
|
||||||
|
Loading…
Reference in New Issue
Block a user