Add intermediate bot

This commit is contained in:
Pierre HUBERT 2022-09-22 17:53:25 +02:00
parent 7d0fed27a9
commit e71b724278
11 changed files with 215 additions and 43 deletions

View File

@ -0,0 +1,90 @@
use actix::Addr;
use uuid::Uuid;
use crate::data::{BoatsLayout, Coordinates, CurrentGameStatus, FireResult, GameRules};
use crate::game::{Fire, Game, Player, RespondRequestRematch, SetBoatsLayout};
#[derive(Clone)]
pub struct IntermediateBot {
game: Addr<Game>,
uuid: Uuid,
}
impl IntermediateBot {
pub fn new(game: Addr<Game>) -> Self {
Self {
game,
uuid: Uuid::new_v4(),
}
}
}
impl Player for IntermediateBot {
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(|| 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)");
}
}

View File

@ -1,2 +1,3 @@
pub mod intermediate_bot;
pub mod linear_bot; pub mod linear_bot;
pub mod random_bot; pub mod random_bot;

View File

@ -117,6 +117,10 @@ impl Coordinates {
} }
} }
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 { pub fn human_print(&self) -> String {
format!( format!(
"{}:{}", "{}:{}",
@ -342,6 +346,26 @@ mod test {
use crate::data::game_map::GameMap; use crate::data::game_map::GameMap;
use crate::data::{BotType, GameRules, PlayConfiguration, PrintableMap}; 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] #[test]
fn get_boat_coordinates() { fn get_boat_coordinates() {
let position = BoatPosition { let position = BoatPosition {

View File

@ -20,6 +20,27 @@ impl CurrentGameMapStatus {
pub fn number_of_fires(&self) -> usize { pub fn number_of_fires(&self) -> usize {
self.successful_strikes.len() + self.failed_strikes.len() 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); struct PrintableCurrentGameMapStatus(GameRules, CurrentGameMapStatus);
@ -65,9 +86,7 @@ pub struct CurrentGameStatus {
impl CurrentGameStatus { impl CurrentGameStatus {
/// Check if opponent can fire at a given location /// Check if opponent can fire at a given location
pub fn can_fire_at_location(&self, location: Coordinates) -> bool { pub fn can_fire_at_location(&self, location: Coordinates) -> bool {
location.is_valid(&self.rules) location.is_valid(&self.rules) && !self.opponent_map.did_fire_at_location(location)
&& !self.opponent_map.successful_strikes.contains(&location)
&& !self.opponent_map.failed_strikes.contains(&location)
} }
/// Find valid random fire location. Loop until one is found /// Find valid random fire location. Loop until one is found
@ -98,29 +117,6 @@ impl CurrentGameStatus {
panic!("Could not find fire location!") 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( fn test_attack_direction(
&self, &self,
pos: &[Coordinates], pos: &[Coordinates],
@ -159,7 +155,7 @@ impl CurrentGameStatus {
/// Attempt to continue an attack, if possible /// Attempt to continue an attack, if possible
pub fn continue_attack_boat(&self) -> Option<Coordinates> { pub fn continue_attack_boat(&self) -> Option<Coordinates> {
let pos = self.get_successful_but_un_sunk_locations(); let pos = self.opponent_map.get_successful_but_un_sunk_locations();
if pos.is_empty() { if pos.is_empty() {
return None; return None;
} }
@ -301,9 +297,9 @@ mod test {
direction: BoatDirection::Left, direction: BoatDirection::Left,
}); });
assert_eq!(status.get_sunk_locations(), vec![sunk]); assert_eq!(status.opponent_map.get_sunk_locations(), vec![sunk]);
assert_eq!( assert_eq!(
status.get_successful_but_un_sunk_locations(), status.opponent_map.get_successful_but_un_sunk_locations(),
vec![unfinished] vec![unfinished]
); );
} }
@ -311,7 +307,10 @@ mod test {
#[test] #[test]
fn no_continue_attack() { fn no_continue_attack() {
let status = CurrentGameStatus::default(); let status = CurrentGameStatus::default();
assert!(status.get_successful_but_un_sunk_locations().is_empty()); assert!(status
.opponent_map
.get_successful_but_un_sunk_locations()
.is_empty());
let next_fire = status.continue_attack_boat(); let next_fire = status.continue_attack_boat();
assert!(next_fire.is_none()); assert!(next_fire.is_none());
} }

View File

@ -5,7 +5,7 @@ use crate::consts::*;
pub enum BotType { pub enum BotType {
Random, Random,
Linear, Linear,
// TODO : GridBot Intermediate,
// TODO : SmartBot // TODO : SmartBot
} }
@ -47,7 +47,11 @@ impl Default for PlayConfiguration {
}, },
BotDescription { BotDescription {
r#type: BotType::Random, r#type: BotType::Random,
description: "Random strike. All the time.".to_string(), description: "Random search. Random strike.".to_string(),
},
BotDescription {
r#type: BotType::Intermediate,
description: "Randome search. Intelligent strike.".to_string(),
}, },
], ],
ordinate_alphabet: ALPHABET, ordinate_alphabet: ALPHABET,

View File

@ -149,7 +149,7 @@ impl Game {
fn handle_fire(&mut self, c: Coordinates) { fn handle_fire(&mut self, c: Coordinates) {
let result = self.player_map_mut(opponent(self.turn)).fire(c); let result = self.player_map_mut(opponent(self.turn)).fire(c);
self.players[self.turn].strike_result(c, result); self.players[self.turn].strike_result(c, result);
self.players[opponent(self.turn)].strike_result(c, result); self.players[opponent(self.turn)].other_player_strike_result(c, result);
// Easiest case : player missed his fire // Easiest case : player missed his fire
if result == FireResult::Missed { if result == FireResult::Missed {

View File

@ -61,14 +61,14 @@ impl Player for HumanPlayer {
} }
fn strike_result(&self, c: Coordinates, res: FireResult) { fn strike_result(&self, c: Coordinates, res: FireResult) {
self.player.do_send(ServerMessage::StrikeResult { self.player.do_send(ServerMessage::FireResult {
pos: c, pos: c,
result: res, result: res,
}); });
} }
fn other_player_strike_result(&self, c: Coordinates, res: FireResult) { fn other_player_strike_result(&self, c: Coordinates, res: FireResult) {
self.player.do_send(ServerMessage::OpponentStrikeResult { self.player.do_send(ServerMessage::OpponentFireResult {
pos: c, pos: c,
result: res, result: res,
}); });

View File

@ -7,6 +7,7 @@ use actix_web_actors::ws;
use actix_web_actors::ws::{CloseCode, CloseReason, Message, ProtocolError, WebsocketContext}; use actix_web_actors::ws::{CloseCode, CloseReason, Message, ProtocolError, WebsocketContext};
use uuid::Uuid; use uuid::Uuid;
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::data::{BoatsLayout, BotType, Coordinates, CurrentGameStatus, FireResult, GameRules}; use crate::data::{BoatsLayout, BotType, Coordinates, CurrentGameStatus, FireResult, GameRules};
@ -62,11 +63,11 @@ pub enum ServerMessage {
RequestFire { RequestFire {
status: CurrentGameStatus, status: CurrentGameStatus,
}, },
StrikeResult { FireResult {
pos: Coordinates, pos: Coordinates,
result: FireResult, result: FireResult,
}, },
OpponentStrikeResult { OpponentFireResult {
pos: Coordinates, pos: Coordinates,
result: FireResult, result: FireResult,
}, },
@ -147,6 +148,9 @@ impl Actor for HumanPlayerWS {
BotType::Linear => { BotType::Linear => {
game.do_send(AddPlayer(Arc::new(LinearBot::new(game.clone())))); game.do_send(AddPlayer(Arc::new(LinearBot::new(game.clone()))));
} }
BotType::Intermediate => {
game.do_send(AddPlayer(Arc::new(IntermediateBot::new(game.clone()))));
}
}; };
let player = Arc::new(HumanPlayer { let player = Arc::new(HumanPlayer {

View File

@ -20,7 +20,7 @@ pub struct BotClient {
requested_rules: GameRules, requested_rules: GameRules,
layout: Option<BoatsLayout>, layout: Option<BoatsLayout>,
number_plays: usize, number_plays: usize,
server_msg_callback: Option<Box<dyn Fn(&ServerMessage)>>, server_msg_callback: Option<Box<dyn FnMut(&ServerMessage)>>,
} }
impl BotClient { impl BotClient {
@ -54,13 +54,13 @@ impl BotClient {
pub fn with_server_msg_callback<F>(mut self, cb: F) -> Self pub fn with_server_msg_callback<F>(mut self, cb: F) -> Self
where where
F: Fn(&ServerMessage) + 'static, F: FnMut(&ServerMessage) + 'static,
{ {
self.server_msg_callback = Some(Box::new(cb)); self.server_msg_callback = Some(Box::new(cb));
self self
} }
pub async fn run_client(&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 url = format!( let url = format!(
@ -107,7 +107,7 @@ impl BotClient {
} }
}; };
if let Some(cb) = &self.server_msg_callback { if let Some(cb) = &mut self.server_msg_callback {
(cb)(&message) (cb)(&message)
} }
@ -152,10 +152,10 @@ impl BotClient {
)?)) )?))
.await?; .await?;
} }
ServerMessage::StrikeResult { pos, result } => { ServerMessage::FireResult { pos, result } => {
log::debug!("Strike at {} result: {:?}", pos.human_print(), result) log::debug!("Strike at {} result: {:?}", pos.human_print(), result)
} }
ServerMessage::OpponentStrikeResult { pos, result } => log::debug!( ServerMessage::OpponentFireResult { pos, result } => log::debug!(
"Opponent trike at {} result: {:?}", "Opponent trike at {} result: {:?}",
pos.human_print(), pos.human_print(),
result result

View File

@ -0,0 +1,48 @@
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::Intermediate))
.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_eq!(res, ClientEndResult::Finished);
})
.await;
}

View File

@ -12,6 +12,7 @@ enum TestPort {
RandomBotNoReplayOnHit, RandomBotNoReplayOnHit,
LinearBotFullGame, LinearBotFullGame,
LinearBotNoReplayOnHit, LinearBotNoReplayOnHit,
IntermediateBotFullGame,
} }
impl TestPort { impl TestPort {
@ -35,6 +36,7 @@ impl Args {
#[cfg(test)] #[cfg(test)]
pub mod bot_client; pub mod bot_client;
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 network_utils; mod network_utils;