Add new test on invite mode

This commit is contained in:
Pierre HUBERT 2022-09-24 12:12:40 +02:00
parent 45b6a24eda
commit dfdf6d9952
13 changed files with 208 additions and 319 deletions

View File

@ -1,25 +1,27 @@
use actix::Addr; use actix::Addr;
use uuid::Uuid; use uuid::Uuid;
use crate::data::{BoatsLayout, Coordinates, CurrentGameStatus, FireResult, GameRules}; use crate::data::{BoatsLayout, BotType, Coordinates, CurrentGameStatus, FireResult, GameRules};
use crate::game::{Fire, Game, Player, RespondRequestRematch, SetBoatsLayout}; use crate::game::{Fire, Game, Player, RespondRequestRematch, SetBoatsLayout};
#[derive(Clone)] #[derive(Clone)]
pub struct IntermediateBot { pub struct BotPlayer {
game: Addr<Game>, game: Addr<Game>,
kind: BotType,
uuid: Uuid, uuid: Uuid,
} }
impl IntermediateBot { impl BotPlayer {
pub fn new(game: Addr<Game>) -> Self { pub fn new(kind: BotType, game: Addr<Game>) -> Self {
Self { Self {
game, game,
kind,
uuid: Uuid::new_v4(), uuid: Uuid::new_v4(),
} }
} }
} }
impl Player for IntermediateBot { impl Player for BotPlayer {
fn get_name(&self) -> &str { fn get_name(&self) -> &str {
"Intermediate Bot" "Intermediate Bot"
} }
@ -54,11 +56,10 @@ impl Player for IntermediateBot {
fn notify_game_starting(&self) {} fn notify_game_starting(&self) {}
fn request_fire(&self, status: CurrentGameStatus) { fn request_fire(&self, status: CurrentGameStatus) {
let coordinates = status self.game.do_send(Fire(
.continue_attack_boat() self.uuid,
.unwrap_or_else(|| status.find_valid_random_fire_location()); status.find_fire_coordinates_for_bot_type(self.kind),
));
self.game.do_send(Fire(self.uuid, coordinates));
} }
fn opponent_must_fire(&self, _status: CurrentGameStatus) {} fn opponent_must_fire(&self, _status: CurrentGameStatus) {}

View File

@ -1,87 +0,0 @@
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 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, _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,4 +0,0 @@
pub mod intermediate_bot;
pub mod linear_bot;
pub mod random_bot;
pub mod smart_bot;

View File

@ -1,87 +0,0 @@
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 RandomBot {
game: Addr<Game>,
uuid: Uuid,
}
impl RandomBot {
pub fn new(game: Addr<Game>) -> Self {
Self {
game,
uuid: Uuid::new_v4(),
}
}
}
impl Player for RandomBot {
fn get_name(&self) -> &str {
"Random 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_valid_random_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, _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,97 +0,0 @@
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)");
}
}

View File

@ -1,7 +1,7 @@
use rand::RngCore; use rand::RngCore;
use crate::data::{ use crate::data::{
BoatPosition, BoatsLayout, Coordinates, GameRules, MapCellContent, PrintableMap, BoatPosition, BoatsLayout, BotType, Coordinates, GameRules, MapCellContent, PrintableMap,
}; };
#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, Default)] #[derive(Debug, serde::Serialize, serde::Deserialize, Clone, Default)]
@ -274,6 +274,32 @@ impl CurrentGameStatus {
pub fn print_opponent_map(&self) { pub fn print_opponent_map(&self) {
print!("{}", self.get_opponent_map()); print!("{}", self.get_opponent_map());
} }
pub fn find_intermediate_bot_fire_location(&self) -> Coordinates {
self.continue_attack_boat()
.unwrap_or_else(|| self.find_valid_random_fire_location())
}
pub fn find_smart_bot_fire_location(&self) -> Coordinates {
self.continue_attack_boat().unwrap_or_else(|| {
let coordinates = self.get_relevant_grid_locations();
if !coordinates.is_empty() {
let pos = rand::thread_rng().next_u32() as usize;
coordinates[pos % coordinates.len()]
} else {
self.find_valid_random_fire_location()
}
})
}
pub fn find_fire_coordinates_for_bot_type(&self, t: BotType) -> Coordinates {
match t {
BotType::Random => self.find_valid_random_fire_location(),
BotType::Linear => self.find_first_valid_fire_location(),
BotType::Intermediate => self.find_intermediate_bot_fire_location(),
BotType::Smart => self.find_smart_bot_fire_location(),
}
}
} }
#[cfg(test)] #[cfg(test)]

View File

@ -4,7 +4,7 @@ use actix::prelude::*;
use actix::{Actor, Context, Handler}; use actix::{Actor, Context, Handler};
use uuid::Uuid; use uuid::Uuid;
use crate::bots::random_bot::RandomBot; use crate::bot_player::BotPlayer;
use crate::data::*; use crate::data::*;
pub trait Player { pub trait Player {
@ -362,7 +362,8 @@ impl Handler<PlayerLeftGame> for Game {
ctx.stop(); ctx.stop();
} else { } else {
// Replace the player with a bot // Replace the player with a bot
self.players[offline_player] = Arc::new(RandomBot::new(ctx.address())); self.players[offline_player] =
Arc::new(BotPlayer::new(self.rules.bot_type, ctx.address()));
self.players[opponent(offline_player)].opponent_replaced_by_bot(); self.players[opponent(offline_player)].opponent_replaced_by_bot();
if self.turn == offline_player { if self.turn == offline_player {

View File

@ -7,11 +7,8 @@ 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::bot_player::BotPlayer;
use crate::bots::linear_bot::LinearBot; use crate::data::{BoatsLayout, Coordinates, CurrentGameStatus, FireResult, GameRules};
use crate::bots::random_bot::RandomBot;
use crate::bots::smart_bot::SmartBot;
use crate::data::{BoatsLayout, BotType, Coordinates, CurrentGameStatus, FireResult, GameRules};
use crate::dispatcher_actor::{AcceptInvite, CreateInvite, DispatcherActor}; use crate::dispatcher_actor::{AcceptInvite, CreateInvite, DispatcherActor};
use crate::game::{AddPlayer, Game}; use crate::game::{AddPlayer, Game};
use crate::human_player::HumanPlayer; use crate::human_player::HumanPlayer;
@ -156,20 +153,10 @@ impl Actor for HumanPlayerWS {
log::debug!("Start play with a bot"); log::debug!("Start play with a bot");
let game = Game::new(rules.clone()).start(); let game = Game::new(rules.clone()).start();
match rules.bot_type { game.do_send(AddPlayer(Arc::new(BotPlayer::new(
BotType::Random => { rules.bot_type,
game.do_send(AddPlayer(Arc::new(RandomBot::new(game.clone())))); game.clone(),
} ))));
BotType::Linear => {
game.do_send(AddPlayer(Arc::new(LinearBot::new(game.clone()))));
}
BotType::Intermediate => {
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 {
name: self.name.to_string(), name: self.name.to_string(),

View File

@ -1,7 +1,7 @@
extern crate core; extern crate core;
pub mod args; pub mod args;
pub mod bots; pub mod bot_player;
pub mod consts; pub mod consts;
pub mod data; pub mod data;
pub mod dispatcher_actor; pub mod dispatcher_actor;

View File

@ -4,7 +4,7 @@ use std::fmt::Display;
use futures::{SinkExt, StreamExt}; use futures::{SinkExt, StreamExt};
use tokio_tungstenite::tungstenite::Message; use tokio_tungstenite::tungstenite::Message;
use crate::data::{BoatsLayout, GameRules}; use crate::data::{BoatsLayout, BotType, GameRules};
use crate::human_player_ws::{ClientMessage, ServerMessage}; use crate::human_player_ws::{ClientMessage, ServerMessage};
use crate::server::AcceptInviteQuery; use crate::server::AcceptInviteQuery;
@ -37,6 +37,7 @@ pub struct BotClient {
layout: Option<BoatsLayout>, layout: Option<BoatsLayout>,
number_plays: usize, number_plays: usize,
server_msg_callback: Option<Box<dyn FnMut(&ServerMessage)>>, server_msg_callback: Option<Box<dyn FnMut(&ServerMessage)>>,
play_as_bot_type: BotType,
} }
impl BotClient { impl BotClient {
@ -51,6 +52,7 @@ impl BotClient {
layout: None, layout: None,
number_plays: 1, number_plays: 1,
server_msg_callback: None, server_msg_callback: None,
play_as_bot_type: BotType::Random,
} }
} }
@ -82,6 +84,11 @@ impl BotClient {
self self
} }
pub fn with_play_as_bot_type(mut self, t: BotType) -> Self {
self.play_as_bot_type = t;
self
}
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_victories = 0;
@ -196,7 +203,7 @@ impl BotClient {
ServerMessage::RequestFire { status } => { ServerMessage::RequestFire { status } => {
assert_eq!(status.opponent_map.boats.number_of_boats(), 0); assert_eq!(status.opponent_map.boats.number_of_boats(), 0);
let location = status.find_valid_random_fire_location(); let location = status.find_fire_coordinates_for_bot_type(self.play_as_bot_type);
log::debug!("Will fire at {:?}", location); log::debug!("Will fire at {:?}", location);
socket socket
.send(Message::Text(serde_json::to_string( .send(Message::Text(serde_json::to_string(
@ -253,7 +260,12 @@ impl BotClient {
ServerMessage::SetOpponentName { name } => log::debug!("Opponent name: {}", name), ServerMessage::SetOpponentName { name } => log::debug!("Opponent name: {}", name),
ServerMessage::OpponentRequestedRematch => { ServerMessage::OpponentRequestedRematch => {
log::debug!("Opponent rejected rematch."); log::debug!("Opponent requested rematch.");
socket
.send(Message::Text(serde_json::to_string(
&ClientMessage::AcceptRematch,
)?))
.await?;
} }
ServerMessage::OpponentAcceptedRematch => { ServerMessage::OpponentAcceptedRematch => {
log::debug!("Opponent accepted rematch"); log::debug!("Opponent accepted rematch");

View File

@ -5,6 +5,7 @@ use tokio::sync::mpsc::Sender;
use tokio::task; use tokio::task;
use crate::args::Args; use crate::args::Args;
use crate::data::BotType;
use crate::human_player_ws::ServerMessage; use crate::human_player_ws::ServerMessage;
use crate::server::start_server; use crate::server::start_server;
use crate::test::bot_client::{ClientEndResult, RunMode}; use crate::test::bot_client::{ClientEndResult, RunMode};
@ -40,9 +41,13 @@ async fn run_other_invite_side(
sender: Sender<Result<ClientEndResult, Box<dyn Error>>>, sender: Sender<Result<ClientEndResult, Box<dyn Error>>>,
port: TestPort, port: TestPort,
code: String, code: String,
play_as_bot_type: BotType,
number_plays: usize,
) { ) {
let res = bot_client::BotClient::new(port.as_url()) let res = bot_client::BotClient::new(port.as_url())
.with_run_mode(RunMode::AcceptInvite { code }) .with_run_mode(RunMode::AcceptInvite { code })
.with_play_as_bot_type(play_as_bot_type)
.with_number_plays(number_plays)
.run_client() .run_client()
.await; .await;
@ -69,6 +74,8 @@ async fn full_game() {
sender.clone(), sender.clone(),
TestPort::InviteModeFullGame, TestPort::InviteModeFullGame,
code.clone(), code.clone(),
BotType::Random,
1,
)); ));
} }
}) })
@ -82,3 +89,127 @@ async fn full_game() {
}) })
.await; .await;
} }
#[tokio::test]
async fn first_player_win() {
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::InviteModeFirstPlayerWin,
)));
wait_for_port(TestPort::InviteModeFirstPlayerWin.port()).await;
let (sender, mut receiver) = mpsc::channel(1);
let res = bot_client::BotClient::new(TestPort::InviteModeFirstPlayerWin.as_url())
.with_run_mode(RunMode::CreateInvite)
.with_play_as_bot_type(BotType::Smart)
.with_number_plays(10)
.with_server_msg_callback(move |msg| {
if let ServerMessage::SetInviteCode { code } = msg {
task::spawn_local(run_other_invite_side(
sender.clone(),
TestPort::InviteModeFirstPlayerWin,
code.clone(),
BotType::Linear,
10,
));
}
})
.run_client()
.await
.unwrap();
let other_side_res = receiver.recv().await.unwrap().unwrap();
assert!(matches!(res, ClientEndResult::Finished { .. }));
assert!(matches!(other_side_res, ClientEndResult::Finished { .. }));
match (res, other_side_res) {
(
ClientEndResult::Finished {
number_defeats: d1,
number_victories: v1,
},
ClientEndResult::Finished {
number_defeats: d2,
number_victories: v2,
},
) => {
assert_eq!(d1, v2);
assert_eq!(v1, d2);
assert!(v1 > 6);
}
(_, _) => unreachable!(),
}
})
.await;
}
#[tokio::test]
async fn second_player_win() {
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::InviteModeSecondPlayerWin,
)));
wait_for_port(TestPort::InviteModeSecondPlayerWin.port()).await;
let (sender, mut receiver) = mpsc::channel(1);
let res = bot_client::BotClient::new(TestPort::InviteModeSecondPlayerWin.as_url())
.with_run_mode(RunMode::CreateInvite)
.with_play_as_bot_type(BotType::Linear)
.with_number_plays(10)
.with_server_msg_callback(move |msg| {
if let ServerMessage::SetInviteCode { code } = msg {
task::spawn_local(run_other_invite_side(
sender.clone(),
TestPort::InviteModeSecondPlayerWin,
code.clone(),
BotType::Smart,
10,
));
}
})
.run_client()
.await
.unwrap();
let other_side_res = receiver.recv().await.unwrap().unwrap();
assert!(matches!(res, ClientEndResult::Finished { .. }));
assert!(matches!(other_side_res, ClientEndResult::Finished { .. }));
match (res, other_side_res) {
(
ClientEndResult::Finished {
number_defeats: d1,
number_victories: v1,
},
ClientEndResult::Finished {
number_defeats: d2,
number_victories: v2,
},
) => {
assert_eq!(d1, v2);
assert_eq!(v1, d2);
assert!(v2 > 6);
}
(_, _) => unreachable!(),
}
})
.await;
}

View File

@ -15,6 +15,8 @@ enum TestPort {
IntermediateBotFullGame, IntermediateBotFullGame,
InviteModeInvalidCode, InviteModeInvalidCode,
InviteModeFullGame, InviteModeFullGame,
InviteModeFirstPlayerWin,
InviteModeSecondPlayerWin,
} }
impl TestPort { impl TestPort {

View File

@ -2,14 +2,6 @@ use rand::distributions::Alphanumeric;
use rand::{thread_rng, Rng}; use rand::{thread_rng, Rng};
/// Generate a random string of a given size /// Generate a random string of a given size
///
/// ```
/// use sea_battle_backend::utils::rand_str;
///
/// let size = 10;
/// let rand = rand_str(size);
/// assert_eq!(size, rand.len());
/// ```
pub fn rand_str(len: usize) -> String { pub fn rand_str(len: usize) -> String {
thread_rng() thread_rng()
.sample_iter(&Alphanumeric) .sample_iter(&Alphanumeric)
@ -17,3 +9,15 @@ pub fn rand_str(len: usize) -> String {
.take(len) .take(len)
.collect() .collect()
} }
#[cfg(test)]
mod test {
use crate::utils::rand_str;
#[test]
fn test_rand_str() {
let size = 10;
let rand = rand_str(size);
assert_eq!(size, rand.len());
}
}