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 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 {
|
||||
format!(
|
||||
"{}:{}",
|
||||
@ -342,6 +346,26 @@ mod test {
|
||||
use crate::data::game_map::GameMap;
|
||||
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]
|
||||
fn get_boat_coordinates() {
|
||||
let position = BoatPosition {
|
||||
|
@ -20,6 +20,27 @@ impl CurrentGameMapStatus {
|
||||
pub fn number_of_fires(&self) -> usize {
|
||||
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);
|
||||
@ -65,9 +86,7 @@ pub struct CurrentGameStatus {
|
||||
impl CurrentGameStatus {
|
||||
/// Check if opponent can fire at a given location
|
||||
pub fn can_fire_at_location(&self, location: Coordinates) -> bool {
|
||||
location.is_valid(&self.rules)
|
||||
&& !self.opponent_map.successful_strikes.contains(&location)
|
||||
&& !self.opponent_map.failed_strikes.contains(&location)
|
||||
location.is_valid(&self.rules) && !self.opponent_map.did_fire_at_location(location)
|
||||
}
|
||||
|
||||
/// Find valid random fire location. Loop until one is found
|
||||
@ -98,29 +117,6 @@ impl CurrentGameStatus {
|
||||
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],
|
||||
@ -159,7 +155,7 @@ impl CurrentGameStatus {
|
||||
|
||||
/// Attempt to continue an attack, if possible
|
||||
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() {
|
||||
return None;
|
||||
}
|
||||
@ -301,9 +297,9 @@ mod test {
|
||||
direction: BoatDirection::Left,
|
||||
});
|
||||
|
||||
assert_eq!(status.get_sunk_locations(), vec![sunk]);
|
||||
assert_eq!(status.opponent_map.get_sunk_locations(), vec![sunk]);
|
||||
assert_eq!(
|
||||
status.get_successful_but_un_sunk_locations(),
|
||||
status.opponent_map.get_successful_but_un_sunk_locations(),
|
||||
vec![unfinished]
|
||||
);
|
||||
}
|
||||
@ -311,7 +307,10 @@ mod test {
|
||||
#[test]
|
||||
fn no_continue_attack() {
|
||||
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();
|
||||
assert!(next_fire.is_none());
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ use crate::consts::*;
|
||||
pub enum BotType {
|
||||
Random,
|
||||
Linear,
|
||||
// TODO : GridBot
|
||||
Intermediate,
|
||||
// TODO : SmartBot
|
||||
}
|
||||
|
||||
@ -47,7 +47,11 @@ impl Default for PlayConfiguration {
|
||||
},
|
||||
BotDescription {
|
||||
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,
|
||||
|
@ -149,7 +149,7 @@ impl Game {
|
||||
fn handle_fire(&mut self, c: Coordinates) {
|
||||
let result = self.player_map_mut(opponent(self.turn)).fire(c);
|
||||
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
|
||||
if result == FireResult::Missed {
|
||||
|
@ -61,14 +61,14 @@ impl Player for HumanPlayer {
|
||||
}
|
||||
|
||||
fn strike_result(&self, c: Coordinates, res: FireResult) {
|
||||
self.player.do_send(ServerMessage::StrikeResult {
|
||||
self.player.do_send(ServerMessage::FireResult {
|
||||
pos: c,
|
||||
result: res,
|
||||
});
|
||||
}
|
||||
|
||||
fn other_player_strike_result(&self, c: Coordinates, res: FireResult) {
|
||||
self.player.do_send(ServerMessage::OpponentStrikeResult {
|
||||
self.player.do_send(ServerMessage::OpponentFireResult {
|
||||
pos: c,
|
||||
result: res,
|
||||
});
|
||||
|
@ -7,6 +7,7 @@ use actix_web_actors::ws;
|
||||
use actix_web_actors::ws::{CloseCode, CloseReason, Message, ProtocolError, WebsocketContext};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::bots::intermediate_bot::IntermediateBot;
|
||||
use crate::bots::linear_bot::LinearBot;
|
||||
use crate::bots::random_bot::RandomBot;
|
||||
use crate::data::{BoatsLayout, BotType, Coordinates, CurrentGameStatus, FireResult, GameRules};
|
||||
@ -62,11 +63,11 @@ pub enum ServerMessage {
|
||||
RequestFire {
|
||||
status: CurrentGameStatus,
|
||||
},
|
||||
StrikeResult {
|
||||
FireResult {
|
||||
pos: Coordinates,
|
||||
result: FireResult,
|
||||
},
|
||||
OpponentStrikeResult {
|
||||
OpponentFireResult {
|
||||
pos: Coordinates,
|
||||
result: FireResult,
|
||||
},
|
||||
@ -147,6 +148,9 @@ impl Actor for HumanPlayerWS {
|
||||
BotType::Linear => {
|
||||
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 {
|
||||
|
@ -20,7 +20,7 @@ pub struct BotClient {
|
||||
requested_rules: GameRules,
|
||||
layout: Option<BoatsLayout>,
|
||||
number_plays: usize,
|
||||
server_msg_callback: Option<Box<dyn Fn(&ServerMessage)>>,
|
||||
server_msg_callback: Option<Box<dyn FnMut(&ServerMessage)>>,
|
||||
}
|
||||
|
||||
impl BotClient {
|
||||
@ -54,13 +54,13 @@ impl BotClient {
|
||||
|
||||
pub fn with_server_msg_callback<F>(mut self, cb: F) -> Self
|
||||
where
|
||||
F: Fn(&ServerMessage) + 'static,
|
||||
F: FnMut(&ServerMessage) + 'static,
|
||||
{
|
||||
self.server_msg_callback = Some(Box::new(cb));
|
||||
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 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)
|
||||
}
|
||||
|
||||
@ -152,10 +152,10 @@ impl BotClient {
|
||||
)?))
|
||||
.await?;
|
||||
}
|
||||
ServerMessage::StrikeResult { pos, result } => {
|
||||
ServerMessage::FireResult { pos, 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: {:?}",
|
||||
pos.human_print(),
|
||||
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,
|
||||
LinearBotFullGame,
|
||||
LinearBotNoReplayOnHit,
|
||||
IntermediateBotFullGame,
|
||||
}
|
||||
|
||||
impl TestPort {
|
||||
@ -35,6 +36,7 @@ impl Args {
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod bot_client;
|
||||
mod bot_client_bot_intermediate_play;
|
||||
mod bot_client_bot_linear_play;
|
||||
mod bot_client_bot_random_play;
|
||||
mod network_utils;
|
||||
|
Loading…
Reference in New Issue
Block a user