Add intermediate bot
This commit is contained in:
parent
7d0fed27a9
commit
e71b724278
90
sea_battle_backend/src/bots/intermediate_bot.rs
Normal file
90
sea_battle_backend/src/bots/intermediate_bot.rs
Normal 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)");
|
||||||
|
}
|
||||||
|
}
|
@ -1,2 +1,3 @@
|
|||||||
|
pub mod intermediate_bot;
|
||||||
pub mod linear_bot;
|
pub mod linear_bot;
|
||||||
pub mod random_bot;
|
pub mod random_bot;
|
||||||
|
@ -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 {
|
||||||
|
@ -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());
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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 {
|
||||||
|
@ -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,
|
||||||
});
|
});
|
||||||
|
@ -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 {
|
||||||
|
@ -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
|
||||||
|
@ -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;
|
||||||
|
}
|
@ -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;
|
||||||
|
Loading…
Reference in New Issue
Block a user