Start to build cli player
This commit is contained in:
314
rust/sea_battle_backend/src/test/bot_client.rs
Normal file
314
rust/sea_battle_backend/src/test/bot_client.rs
Normal file
@ -0,0 +1,314 @@
|
||||
use std::error::Error;
|
||||
use std::fmt::Display;
|
||||
|
||||
use futures::{SinkExt, StreamExt};
|
||||
use tokio_tungstenite::tungstenite::Message;
|
||||
|
||||
use crate::data::{BoatsLayout, BotType, GameRules};
|
||||
use crate::human_player_ws::{ClientMessage, ServerMessage};
|
||||
use crate::server::{AcceptInviteQuery, BotPlayQuery, CreateInviteQuery, PlayRandomQuery};
|
||||
|
||||
const PLAYER_NAME: &str = "Bot client";
|
||||
|
||||
#[derive(Default)]
|
||||
pub enum RunMode {
|
||||
#[default]
|
||||
AgainstBot,
|
||||
CreateInvite,
|
||||
AcceptInvite {
|
||||
code: String,
|
||||
},
|
||||
Random,
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
pub enum ClientEndResult {
|
||||
Finished {
|
||||
number_victories: usize,
|
||||
number_defeats: usize,
|
||||
},
|
||||
InvalidBoatsLayout,
|
||||
OpponentRejectedRematch,
|
||||
OpponentLeftGame,
|
||||
InvalidInviteCode,
|
||||
}
|
||||
|
||||
pub struct BotClient {
|
||||
server: String,
|
||||
run_mode: RunMode,
|
||||
requested_rules: GameRules,
|
||||
layout: Option<BoatsLayout>,
|
||||
number_plays: usize,
|
||||
server_msg_callback: Option<Box<dyn FnMut(&ServerMessage)>>,
|
||||
play_as_bot_type: BotType,
|
||||
}
|
||||
|
||||
impl BotClient {
|
||||
pub fn new<D>(server: D) -> Self
|
||||
where
|
||||
D: Display,
|
||||
{
|
||||
Self {
|
||||
server: server.to_string(),
|
||||
run_mode: RunMode::default(),
|
||||
requested_rules: GameRules::random_players_rules(),
|
||||
layout: None,
|
||||
number_plays: 1,
|
||||
server_msg_callback: None,
|
||||
play_as_bot_type: BotType::Random,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_run_mode(mut self, mode: RunMode) -> Self {
|
||||
self.run_mode = mode;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_rules(mut self, rules: GameRules) -> Self {
|
||||
self.requested_rules = rules;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_layout(mut self, layout: BoatsLayout) -> Self {
|
||||
self.layout = Some(layout);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_number_plays(mut self, number: usize) -> Self {
|
||||
self.number_plays = number;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_server_msg_callback<F>(mut self, cb: F) -> Self
|
||||
where
|
||||
F: FnMut(&ServerMessage) + 'static,
|
||||
{
|
||||
self.server_msg_callback = Some(Box::new(cb));
|
||||
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>> {
|
||||
let mut remaining_games = self.number_plays;
|
||||
let mut number_victories = 0;
|
||||
let mut number_defeats = 0;
|
||||
|
||||
let url = match &self.run_mode {
|
||||
RunMode::AgainstBot => {
|
||||
format!(
|
||||
"{}/play/bot?{}",
|
||||
self.server.replace("http", "ws"),
|
||||
serde_urlencoded::to_string(&BotPlayQuery {
|
||||
rules: self.requested_rules.clone(),
|
||||
player_name: PLAYER_NAME.to_string()
|
||||
})
|
||||
.unwrap()
|
||||
)
|
||||
}
|
||||
RunMode::CreateInvite => {
|
||||
format!(
|
||||
"{}/play/create_invite?{}",
|
||||
self.server.replace("http", "ws"),
|
||||
serde_urlencoded::to_string(&CreateInviteQuery {
|
||||
rules: self.requested_rules.clone(),
|
||||
player_name: PLAYER_NAME.to_string(),
|
||||
})
|
||||
.unwrap()
|
||||
)
|
||||
}
|
||||
RunMode::AcceptInvite { code } => {
|
||||
format!(
|
||||
"{}/play/accept_invite?{}",
|
||||
self.server.replace("http", "ws"),
|
||||
serde_urlencoded::to_string(&AcceptInviteQuery {
|
||||
code: code.to_string(),
|
||||
player_name: PLAYER_NAME.to_string()
|
||||
})
|
||||
.unwrap()
|
||||
)
|
||||
}
|
||||
RunMode::Random => {
|
||||
format!(
|
||||
"{}/play/random?{}",
|
||||
self.server.replace("http", "ws"),
|
||||
serde_urlencoded::to_string(&PlayRandomQuery {
|
||||
player_name: PLAYER_NAME.to_string()
|
||||
})
|
||||
.unwrap()
|
||||
)
|
||||
}
|
||||
};
|
||||
log::debug!("Connecting to {}...", url);
|
||||
let (mut socket, _) = match tokio_tungstenite::connect_async(url).await {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
log::error!("Failed to establish WebSocket connection! {:?}", e);
|
||||
return Err(Box::new(e));
|
||||
}
|
||||
};
|
||||
|
||||
while let Some(chunk) = socket.next().await {
|
||||
let message = match chunk? {
|
||||
Message::Text(message) => {
|
||||
log::trace!("TEXT message from server: {}", message);
|
||||
|
||||
let msg: ServerMessage = serde_json::from_str(&message)?;
|
||||
msg
|
||||
}
|
||||
Message::Binary(_) => {
|
||||
log::debug!("BINARY message from server");
|
||||
continue;
|
||||
}
|
||||
Message::Ping(_) => {
|
||||
log::debug!("PING from server");
|
||||
continue;
|
||||
}
|
||||
Message::Pong(_) => {
|
||||
log::debug!("PONG from server");
|
||||
continue;
|
||||
}
|
||||
Message::Close(_) => {
|
||||
log::debug!("CLOSE message request from server");
|
||||
break;
|
||||
}
|
||||
Message::Frame(_) => {
|
||||
log::debug!("Frame from server");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(cb) = &mut self.server_msg_callback {
|
||||
(cb)(&message)
|
||||
}
|
||||
|
||||
match message {
|
||||
ServerMessage::WaitingForAnotherPlayer => {
|
||||
log::debug!("Waiting for other player...")
|
||||
}
|
||||
ServerMessage::SetInviteCode { code } => {
|
||||
log::debug!("Got invite code: {}", code);
|
||||
}
|
||||
ServerMessage::InvalidInviteCode => {
|
||||
log::debug!("Got invalid invite code!");
|
||||
return Ok(ClientEndResult::InvalidInviteCode);
|
||||
}
|
||||
ServerMessage::QueryBoatsLayout { rules } => {
|
||||
assert_eq!(&rules, &self.requested_rules);
|
||||
log::debug!("Server requested boats layout");
|
||||
socket
|
||||
.send(Message::Text(serde_json::to_string(
|
||||
&ClientMessage::BoatsLayout {
|
||||
layout: self.layout.clone().unwrap_or_else(|| {
|
||||
BoatsLayout::gen_random_for_rules(&self.requested_rules)
|
||||
.unwrap()
|
||||
}),
|
||||
},
|
||||
)?))
|
||||
.await?;
|
||||
}
|
||||
ServerMessage::WaitingForOtherPlayerConfiguration => {
|
||||
log::debug!("Waiting for other player configuration...")
|
||||
}
|
||||
ServerMessage::OpponentReady => log::debug!("Other player is ready!"),
|
||||
ServerMessage::GameStarting => {
|
||||
log::debug!("The game is starting...");
|
||||
remaining_games -= 1;
|
||||
}
|
||||
ServerMessage::OpponentMustFire { status } => {
|
||||
assert_eq!(status.opponent_map.boats.number_of_boats(), 0);
|
||||
log::debug!("Other player must fire!")
|
||||
}
|
||||
ServerMessage::RequestFire { status } => {
|
||||
assert_eq!(status.opponent_map.boats.number_of_boats(), 0);
|
||||
|
||||
let location = status.find_fire_coordinates_for_bot_type(self.play_as_bot_type);
|
||||
log::debug!("Will fire at {:?}", location);
|
||||
socket
|
||||
.send(Message::Text(serde_json::to_string(
|
||||
&ClientMessage::Fire { location },
|
||||
)?))
|
||||
.await?;
|
||||
}
|
||||
ServerMessage::FireResult { pos, result } => {
|
||||
log::debug!("Strike at {} result: {:?}", pos.human_print(), result)
|
||||
}
|
||||
ServerMessage::OpponentFireResult { pos, result } => log::debug!(
|
||||
"Opponent trike at {} result: {:?}",
|
||||
pos.human_print(),
|
||||
result
|
||||
),
|
||||
ServerMessage::LostGame { status } => {
|
||||
number_defeats += 1;
|
||||
|
||||
log::debug!("We lost game :(");
|
||||
log::debug!("Opponent map:\n{}", status.get_opponent_map());
|
||||
log::debug!("Our map:\n{}\n", status.get_your_map());
|
||||
|
||||
if remaining_games > 0 {
|
||||
socket
|
||||
.send(Message::Text(serde_json::to_string(
|
||||
&ClientMessage::RequestRematch,
|
||||
)?))
|
||||
.await?;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
ServerMessage::WonGame { status } => {
|
||||
number_victories += 1;
|
||||
|
||||
log::debug!("We won the game !!!!");
|
||||
log::debug!("Opponent map:\n{}\n", status.get_opponent_map());
|
||||
log::debug!("Our map:\n{}\n", status.get_your_map());
|
||||
|
||||
if remaining_games > 0 {
|
||||
socket
|
||||
.send(Message::Text(serde_json::to_string(
|
||||
&ClientMessage::RequestRematch,
|
||||
)?))
|
||||
.await?;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
ServerMessage::RejectedBoatsLayout { errors } => {
|
||||
log::warn!("Rejected boat layout: {:?}", errors);
|
||||
return Ok(ClientEndResult::InvalidBoatsLayout);
|
||||
}
|
||||
ServerMessage::SetOpponentName { name } => log::debug!("Opponent name: {}", name),
|
||||
|
||||
ServerMessage::OpponentRequestedRematch => {
|
||||
log::debug!("Opponent requested rematch.");
|
||||
socket
|
||||
.send(Message::Text(serde_json::to_string(
|
||||
&ClientMessage::AcceptRematch,
|
||||
)?))
|
||||
.await?;
|
||||
}
|
||||
ServerMessage::OpponentAcceptedRematch => {
|
||||
log::debug!("Opponent accepted rematch");
|
||||
}
|
||||
ServerMessage::OpponentRejectedRematch => {
|
||||
log::debug!("Opponent rejected rematch");
|
||||
return Ok(ClientEndResult::OpponentRejectedRematch);
|
||||
}
|
||||
ServerMessage::OpponentLeftGame => {
|
||||
log::debug!("Opponent left game");
|
||||
return Ok(ClientEndResult::OpponentLeftGame);
|
||||
}
|
||||
ServerMessage::OpponentReplacedByBot => {
|
||||
log::debug!("Opponent replaced by bot");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(ClientEndResult::Finished {
|
||||
number_victories,
|
||||
number_defeats,
|
||||
})
|
||||
}
|
||||
}
|
48
rust/sea_battle_backend/src/test/bot_intermediate_play.rs
Normal file
48
rust/sea_battle_backend/src/test/bot_intermediate_play.rs
Normal 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!(matches!(res, ClientEndResult::Finished { .. }));
|
||||
})
|
||||
.await;
|
||||
}
|
144
rust/sea_battle_backend/src/test/bot_linear_play.rs
Normal file
144
rust/sea_battle_backend/src/test/bot_linear_play.rs
Normal file
@ -0,0 +1,144 @@
|
||||
use tokio::task;
|
||||
|
||||
use crate::args::Args;
|
||||
use crate::data::{BoatsLayout, BotType, Coordinates, 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::play_utils::check_no_replay_on_hit;
|
||||
use crate::test::{bot_client, TestPort};
|
||||
|
||||
fn check_strikes_are_linear(msg: &ServerMessage) {
|
||||
if let ServerMessage::RequestFire { status } = msg {
|
||||
let mut in_fire_location = true;
|
||||
for y in 0..status.rules.map_height {
|
||||
for x in 0..status.rules.map_width {
|
||||
let c = Coordinates::new(x as i32, y as i32);
|
||||
|
||||
if in_fire_location {
|
||||
in_fire_location = status.your_map.did_fire_at_location(c);
|
||||
} else if status.your_map.did_fire_at_location(c) {
|
||||
println!("Your map:");
|
||||
status.print_your_map();
|
||||
println!("Opponent map:");
|
||||
status.print_opponent_map();
|
||||
panic!("Found invalid fire location for linear bot!");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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::LinearBotFullGame)));
|
||||
wait_for_port(TestPort::LinearBotFullGame.port()).await;
|
||||
|
||||
let res = bot_client::BotClient::new(TestPort::LinearBotFullGame.as_url())
|
||||
.with_rules(GameRules::random_players_rules().with_bot_type(BotType::Linear))
|
||||
.with_server_msg_callback(check_strikes_are_linear)
|
||||
.run_client()
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(matches!(res, ClientEndResult::Finished { .. }));
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn full_game_no_replay_on_hit() {
|
||||
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::LinearBotNoReplayOnHit,
|
||||
)));
|
||||
wait_for_port(TestPort::LinearBotNoReplayOnHit.port()).await;
|
||||
|
||||
let res = bot_client::BotClient::new(TestPort::LinearBotNoReplayOnHit.as_url())
|
||||
.with_rules(
|
||||
GameRules::random_players_rules()
|
||||
.with_player_continue_on_hit(false)
|
||||
.with_bot_type(BotType::Linear),
|
||||
)
|
||||
.with_server_msg_callback(check_no_replay_on_hit)
|
||||
.run_client()
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(matches!(res, ClientEndResult::Finished { .. }));
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn full_game_no_replay_on_hit_two() {
|
||||
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::LinearBotNoReplayOnHit,
|
||||
)));
|
||||
wait_for_port(TestPort::LinearBotNoReplayOnHit.port()).await;
|
||||
|
||||
let rules = GameRules::random_players_rules()
|
||||
.with_player_continue_on_hit(false)
|
||||
.with_bot_type(BotType::Linear);
|
||||
let layout = BoatsLayout::layout_for_boats_at_beginning_of_map(&rules).unwrap();
|
||||
let res = bot_client::BotClient::new(TestPort::LinearBotNoReplayOnHit.as_url())
|
||||
.with_rules(rules.clone())
|
||||
.with_layout(layout)
|
||||
.with_server_msg_callback(check_no_replay_on_hit)
|
||||
.run_client()
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(matches!(res, ClientEndResult::Finished { .. }));
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn full_game_with_replay_on_hit() {
|
||||
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::LinearBotNoReplayOnHit,
|
||||
)));
|
||||
wait_for_port(TestPort::LinearBotNoReplayOnHit.port()).await;
|
||||
|
||||
let rules = GameRules::random_players_rules()
|
||||
.with_player_continue_on_hit(true)
|
||||
.with_bot_type(BotType::Linear);
|
||||
let layout = BoatsLayout::layout_for_boats_at_beginning_of_map(&rules).unwrap();
|
||||
let res = bot_client::BotClient::new(TestPort::LinearBotNoReplayOnHit.as_url())
|
||||
.with_rules(rules.clone())
|
||||
.with_layout(layout)
|
||||
.with_server_msg_callback(|msg| {
|
||||
if let ServerMessage::LostGame { status } | ServerMessage::WonGame { status } =
|
||||
msg
|
||||
{
|
||||
assert!(
|
||||
status.opponent_map.number_of_fires()
|
||||
< status.your_map.number_of_fires()
|
||||
);
|
||||
}
|
||||
})
|
||||
.run_client()
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(matches!(res, ClientEndResult::Finished { .. }));
|
||||
})
|
||||
.await;
|
||||
}
|
203
rust/sea_battle_backend/src/test/bot_random_play.rs
Normal file
203
rust/sea_battle_backend/src/test/bot_random_play.rs
Normal file
@ -0,0 +1,203 @@
|
||||
use tokio::task;
|
||||
|
||||
use crate::args::Args;
|
||||
use crate::data::{BoatsLayout, GameRules};
|
||||
use crate::server::start_server;
|
||||
use crate::test::bot_client;
|
||||
use crate::test::bot_client::ClientEndResult;
|
||||
use crate::test::network_utils::wait_for_port;
|
||||
use crate::test::play_utils::check_no_replay_on_hit;
|
||||
use crate::test::TestPort;
|
||||
|
||||
#[tokio::test]
|
||||
async fn invalid_port() {
|
||||
let _ = env_logger::builder().is_test(true).try_init();
|
||||
|
||||
bot_client::BotClient::new(TestPort::RandomBotClientInvalidPort.as_url())
|
||||
.run_client()
|
||||
.await
|
||||
.unwrap_err();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn invalid_rules() {
|
||||
let _ = env_logger::builder().is_test(true).try_init();
|
||||
|
||||
let local_set = task::LocalSet::new();
|
||||
local_set
|
||||
.run_until(async move {
|
||||
let mut rules = GameRules::random_players_rules();
|
||||
rules.map_width = 0;
|
||||
|
||||
task::spawn_local(start_server(Args::for_test(
|
||||
TestPort::RandomBotClientInvalidRules,
|
||||
)));
|
||||
wait_for_port(TestPort::RandomBotClientInvalidRules.port()).await;
|
||||
|
||||
bot_client::BotClient::new(TestPort::RandomBotClientInvalidRules.as_url())
|
||||
.with_rules(rules.clone())
|
||||
.with_layout(
|
||||
BoatsLayout::gen_random_for_rules(&GameRules::random_players_rules()).unwrap(),
|
||||
)
|
||||
.run_client()
|
||||
.await
|
||||
.unwrap_err();
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[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::RandomBotFullGame)));
|
||||
wait_for_port(TestPort::RandomBotFullGame.port()).await;
|
||||
|
||||
let res = bot_client::BotClient::new(TestPort::RandomBotFullGame.as_url())
|
||||
.run_client()
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(matches!(res, ClientEndResult::Finished { .. }));
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn full_game_no_touching_boats() {
|
||||
let _ = env_logger::builder().is_test(true).try_init();
|
||||
|
||||
let local_set = task::LocalSet::new();
|
||||
local_set
|
||||
.run_until(async move {
|
||||
let mut rules = GameRules::random_players_rules();
|
||||
rules.boats_can_touch = false;
|
||||
task::spawn_local(start_server(Args::for_test(
|
||||
TestPort::RandomBotFullGameNoTouchingBoats,
|
||||
)));
|
||||
wait_for_port(TestPort::RandomBotFullGameNoTouchingBoats.port()).await;
|
||||
|
||||
let res =
|
||||
bot_client::BotClient::new(TestPort::RandomBotFullGameNoTouchingBoats.as_url())
|
||||
.with_rules(rules)
|
||||
.run_client()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(matches!(res, ClientEndResult::Finished { .. }));
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn invalid_boats_layout_number_of_boats() {
|
||||
let _ = env_logger::builder().is_test(true).try_init();
|
||||
|
||||
let local_set = task::LocalSet::new();
|
||||
local_set
|
||||
.run_until(async move {
|
||||
let mut rules = GameRules::random_players_rules();
|
||||
rules.boats_can_touch = false;
|
||||
task::spawn_local(start_server(Args::for_test(
|
||||
TestPort::RandomBotInvalidBoatsLayoutNumberOfBoats,
|
||||
)));
|
||||
wait_for_port(TestPort::RandomBotInvalidBoatsLayoutNumberOfBoats.port()).await;
|
||||
|
||||
let mut rules_modified = rules.clone();
|
||||
rules_modified.remove_last_boat();
|
||||
let layout = BoatsLayout::gen_random_for_rules(&rules_modified).unwrap();
|
||||
|
||||
let res = bot_client::BotClient::new(
|
||||
&TestPort::RandomBotInvalidBoatsLayoutNumberOfBoats.as_url(),
|
||||
)
|
||||
.with_rules(rules)
|
||||
.with_layout(layout)
|
||||
.run_client()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(res, ClientEndResult::InvalidBoatsLayout);
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn invalid_boats_layout_len_of_a_boat() {
|
||||
let _ = env_logger::builder().is_test(true).try_init();
|
||||
|
||||
let local_set = task::LocalSet::new();
|
||||
local_set
|
||||
.run_until(async move {
|
||||
let mut rules = GameRules::random_players_rules();
|
||||
rules.boats_can_touch = false;
|
||||
task::spawn_local(start_server(Args::for_test(
|
||||
TestPort::RandomBotInvalidBoatsLayoutLenOfABoat,
|
||||
)));
|
||||
wait_for_port(TestPort::RandomBotInvalidBoatsLayoutLenOfABoat.port()).await;
|
||||
|
||||
let mut rules_modified = rules.clone();
|
||||
let previous = rules_modified.remove_last_boat();
|
||||
rules_modified.add_boat(previous - 1);
|
||||
let layout = BoatsLayout::gen_random_for_rules(&rules_modified).unwrap();
|
||||
|
||||
let res = bot_client::BotClient::new(
|
||||
&TestPort::RandomBotInvalidBoatsLayoutLenOfABoat.as_url(),
|
||||
)
|
||||
.with_rules(rules)
|
||||
.with_layout(layout)
|
||||
.run_client()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(res, ClientEndResult::InvalidBoatsLayout);
|
||||
})
|
||||
.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::RandomBotFullGameMultipleRematch,
|
||||
)));
|
||||
wait_for_port(TestPort::RandomBotFullGameMultipleRematch.port()).await;
|
||||
|
||||
let res =
|
||||
bot_client::BotClient::new(TestPort::RandomBotFullGameMultipleRematch.as_url())
|
||||
.with_number_plays(5)
|
||||
.run_client()
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(matches!(res, ClientEndResult::Finished { .. }));
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn full_game_no_replay_on_hit() {
|
||||
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::RandomBotNoReplayOnHit,
|
||||
)));
|
||||
wait_for_port(TestPort::RandomBotNoReplayOnHit.port()).await;
|
||||
|
||||
let res = bot_client::BotClient::new(TestPort::RandomBotNoReplayOnHit.as_url())
|
||||
.with_rules(GameRules::random_players_rules().with_player_continue_on_hit(false))
|
||||
.with_server_msg_callback(check_no_replay_on_hit)
|
||||
.run_client()
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(matches!(res, ClientEndResult::Finished { .. }));
|
||||
})
|
||||
.await;
|
||||
}
|
96
rust/sea_battle_backend/src/test/bot_smart_play.rs
Normal file
96
rust/sea_battle_backend/src/test/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;
|
||||
}
|
215
rust/sea_battle_backend/src/test/invite_mode.rs
Normal file
215
rust/sea_battle_backend/src/test/invite_mode.rs
Normal file
@ -0,0 +1,215 @@
|
||||
use std::error::Error;
|
||||
|
||||
use tokio::sync::mpsc;
|
||||
use tokio::sync::mpsc::Sender;
|
||||
use tokio::task;
|
||||
|
||||
use crate::args::Args;
|
||||
use crate::data::BotType;
|
||||
use crate::human_player_ws::ServerMessage;
|
||||
use crate::server::start_server;
|
||||
use crate::test::bot_client::{ClientEndResult, RunMode};
|
||||
use crate::test::network_utils::wait_for_port;
|
||||
use crate::test::{bot_client, TestPort};
|
||||
|
||||
#[tokio::test]
|
||||
async fn invalid_accept_code() {
|
||||
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::InviteModeInvalidCode,
|
||||
)));
|
||||
wait_for_port(TestPort::InviteModeInvalidCode.port()).await;
|
||||
|
||||
let res = bot_client::BotClient::new(TestPort::InviteModeInvalidCode.as_url())
|
||||
.with_run_mode(RunMode::AcceptInvite {
|
||||
code: "BadCode".to_string(),
|
||||
})
|
||||
.run_client()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(res, ClientEndResult::InvalidInviteCode)
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn run_other_invite_side(
|
||||
sender: Sender<Result<ClientEndResult, Box<dyn Error>>>,
|
||||
port: TestPort,
|
||||
code: String,
|
||||
play_as_bot_type: BotType,
|
||||
number_plays: usize,
|
||||
) {
|
||||
let res = bot_client::BotClient::new(port.as_url())
|
||||
.with_run_mode(RunMode::AcceptInvite { code })
|
||||
.with_play_as_bot_type(play_as_bot_type)
|
||||
.with_number_plays(number_plays)
|
||||
.run_client()
|
||||
.await;
|
||||
|
||||
sender.send(res).await.unwrap()
|
||||
}
|
||||
|
||||
#[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::InviteModeFullGame)));
|
||||
wait_for_port(TestPort::InviteModeFullGame.port()).await;
|
||||
|
||||
let (sender, mut receiver) = mpsc::channel(1);
|
||||
|
||||
let res = bot_client::BotClient::new(TestPort::InviteModeFullGame.as_url())
|
||||
.with_run_mode(RunMode::CreateInvite)
|
||||
.with_server_msg_callback(move |msg| {
|
||||
if let ServerMessage::SetInviteCode { code } = msg {
|
||||
task::spawn_local(run_other_invite_side(
|
||||
sender.clone(),
|
||||
TestPort::InviteModeFullGame,
|
||||
code.clone(),
|
||||
BotType::Random,
|
||||
1,
|
||||
));
|
||||
}
|
||||
})
|
||||
.run_client()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(matches!(res, ClientEndResult::Finished { .. }));
|
||||
let other_side_res = receiver.recv().await.unwrap().unwrap();
|
||||
assert!(matches!(other_side_res, ClientEndResult::Finished { .. }));
|
||||
})
|
||||
.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(3)
|
||||
.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,
|
||||
3,
|
||||
));
|
||||
}
|
||||
})
|
||||
.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 > 1);
|
||||
}
|
||||
|
||||
(_, _) => 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(3)
|
||||
.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,
|
||||
3,
|
||||
));
|
||||
}
|
||||
})
|
||||
.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 > 1);
|
||||
}
|
||||
|
||||
(_, _) => unreachable!(),
|
||||
}
|
||||
})
|
||||
.await;
|
||||
}
|
51
rust/sea_battle_backend/src/test/mod.rs
Normal file
51
rust/sea_battle_backend/src/test/mod.rs
Normal file
@ -0,0 +1,51 @@
|
||||
use crate::args::Args;
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
enum TestPort {
|
||||
RandomBotClientInvalidPort = 20000,
|
||||
RandomBotClientInvalidRules,
|
||||
RandomBotFullGame,
|
||||
RandomBotFullGameNoTouchingBoats,
|
||||
RandomBotInvalidBoatsLayoutNumberOfBoats,
|
||||
RandomBotInvalidBoatsLayoutLenOfABoat,
|
||||
RandomBotFullGameMultipleRematch,
|
||||
RandomBotNoReplayOnHit,
|
||||
LinearBotFullGame,
|
||||
LinearBotNoReplayOnHit,
|
||||
IntermediateBotFullGame,
|
||||
InviteModeInvalidCode,
|
||||
InviteModeFullGame,
|
||||
InviteModeFirstPlayerWin,
|
||||
InviteModeSecondPlayerWin,
|
||||
RandomModeFourGames,
|
||||
}
|
||||
|
||||
impl TestPort {
|
||||
pub fn port(&self) -> u16 {
|
||||
(*self as u32) as u16
|
||||
}
|
||||
|
||||
pub fn as_url(&self) -> String {
|
||||
format!("http://127.0.0.1:{}", self.port())
|
||||
}
|
||||
}
|
||||
|
||||
impl Args {
|
||||
fn for_test(port: TestPort) -> Self {
|
||||
Self {
|
||||
listen_address: format!("127.0.0.1:{}", port.port()),
|
||||
cors: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod bot_client;
|
||||
mod bot_intermediate_play;
|
||||
mod bot_linear_play;
|
||||
mod bot_random_play;
|
||||
mod bot_smart_play;
|
||||
mod invite_mode;
|
||||
mod network_utils;
|
||||
mod play_utils;
|
||||
mod random_mode;
|
25
rust/sea_battle_backend/src/test/network_utils.rs
Normal file
25
rust/sea_battle_backend/src/test/network_utils.rs
Normal file
@ -0,0 +1,25 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use tokio::net::TcpStream;
|
||||
use tokio::time;
|
||||
|
||||
/// Check whether a given port is open or not
|
||||
pub async fn is_port_open(port: u16) -> bool {
|
||||
match TcpStream::connect(("127.0.0.1", port)).await {
|
||||
Ok(_) => true,
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Wait for a port to become available
|
||||
pub async fn wait_for_port(port: u16) {
|
||||
for _ in 0..50 {
|
||||
if is_port_open(port).await {
|
||||
return;
|
||||
}
|
||||
time::sleep(Duration::from_millis(10)).await;
|
||||
}
|
||||
|
||||
eprintln!("Port {} did not open in time!", port);
|
||||
std::process::exit(2);
|
||||
}
|
12
rust/sea_battle_backend/src/test/play_utils.rs
Normal file
12
rust/sea_battle_backend/src/test/play_utils.rs
Normal file
@ -0,0 +1,12 @@
|
||||
use crate::human_player_ws::ServerMessage;
|
||||
|
||||
/// Make sure player can not replay after successful hit
|
||||
pub fn check_no_replay_on_hit(msg: &ServerMessage) {
|
||||
if let ServerMessage::OpponentMustFire { status } | ServerMessage::RequestFire { status } = msg
|
||||
{
|
||||
let diff =
|
||||
status.opponent_map.number_of_fires() as i32 - status.your_map.number_of_fires() as i32;
|
||||
|
||||
assert!(diff <= 1);
|
||||
}
|
||||
}
|
39
rust/sea_battle_backend/src/test/random_mode.rs
Normal file
39
rust/sea_battle_backend/src/test/random_mode.rs
Normal file
@ -0,0 +1,39 @@
|
||||
use crate::args::Args;
|
||||
use crate::server::start_server;
|
||||
use crate::test::bot_client::{ClientEndResult, RunMode};
|
||||
use crate::test::network_utils::wait_for_port;
|
||||
use crate::test::{bot_client, TestPort};
|
||||
use std::error::Error;
|
||||
use tokio::task;
|
||||
|
||||
async fn play_random(port: TestPort) -> Result<ClientEndResult, Box<dyn Error>> {
|
||||
bot_client::BotClient::new(port.as_url())
|
||||
.with_run_mode(RunMode::Random)
|
||||
.run_client()
|
||||
.await
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn four_games() {
|
||||
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::RandomModeFourGames)));
|
||||
wait_for_port(TestPort::RandomModeFourGames.port()).await;
|
||||
|
||||
let mut fut = vec![];
|
||||
for _ in 0..4 {
|
||||
fut.push(task::spawn_local(play_random(
|
||||
TestPort::RandomModeFourGames,
|
||||
)));
|
||||
}
|
||||
|
||||
for handle in fut {
|
||||
let res = handle.await.unwrap().unwrap();
|
||||
assert!(matches!(res, ClientEndResult::Finished { .. }));
|
||||
}
|
||||
})
|
||||
.await;
|
||||
}
|
Reference in New Issue
Block a user