Added rematch feature

This commit is contained in:
Pierre HUBERT 2022-09-16 16:55:29 +02:00
parent 13c03a8df0
commit 45060ae14b
10 changed files with 389 additions and 189 deletions

View File

@ -1,132 +0,0 @@
use std::error::Error;
use futures::{SinkExt, StreamExt};
use tokio_tungstenite::tungstenite::Message;
use crate::data::{BoatsLayout, GameRules};
use crate::human_player_ws::{ClientMessage, ServerMessage};
#[derive(Debug, Eq, PartialEq)]
pub enum ClientEndResult {
Finished,
InvalidBoatsLayout,
}
pub async fn run_client(
server: &str,
requested_rules: &GameRules,
layout: BoatsLayout,
) -> Result<ClientEndResult, Box<dyn Error>> {
let url = format!(
"{}/play/bot?{}",
server.replace("http", "ws"),
serde_urlencoded::to_string(requested_rules).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;
}
};
match message {
ServerMessage::WaitingForAnotherPlayer => log::debug!("Waiting for other player..."),
ServerMessage::QueryBoatsLayout { rules } => {
assert_eq!(&rules, requested_rules);
log::debug!("Server requested boats layout");
socket
.send(Message::Text(serde_json::to_string(
&ClientMessage::BoatsLayout {
layout: layout.clone(),
},
)?))
.await?;
}
ServerMessage::WaitingForOtherPlayerConfiguration => {
log::debug!("Waiting for other player configuration...")
}
ServerMessage::OtherPlayerReady => log::debug!("Other player is ready!"),
ServerMessage::GameStarting => log::debug!("The game is starting..."),
ServerMessage::OtherPlayerMustFire { 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_valid_random_fire_location();
log::debug!("Will fire at {:?}", location);
socket
.send(Message::Text(serde_json::to_string(
&ClientMessage::Fire { location },
)?))
.await?;
}
ServerMessage::StrikeResult { pos, result } => {
log::debug!("Strike at {} result: {:?}", pos.human_print(), result)
}
ServerMessage::OpponentStrikeResult { pos, result } => log::debug!(
"Opponent trike at {} result: {:?}",
pos.human_print(),
result
),
ServerMessage::LostGame {
your_map,
opponent_map,
} => {
log::debug!("We lost game :(");
log::debug!("Other game:\n{}\n", opponent_map.get_map());
log::debug!("Our game:\n{}\n", your_map.get_map());
break;
}
ServerMessage::WonGame {
your_map,
opponent_map,
} => {
log::debug!("We won the game !!!!");
log::debug!("Other game:\n{}\n", opponent_map.get_map());
log::debug!("Our game:\n{}\n", your_map.get_map());
break;
}
ServerMessage::RejectedBoatsLayout { errors } => {
log::warn!("Rejected boat layout: {:?}", errors);
return Ok(ClientEndResult::InvalidBoatsLayout);
}
ServerMessage::SetOpponentName { name } => log::debug!("Opponent name: {}", name),
}
}
Ok(ClientEndResult::Finished)
}

View File

@ -5,7 +5,7 @@ use crate::data::{BoatPosition, CurrentGameMapStatus, EndGameMap, GameRules};
pub enum FireResult { pub enum FireResult {
Missed, Missed,
Hit, Hit,
Sunk, Sunk(BoatPosition),
Rejected, Rejected,
AlreadyTargetedPosition, AlreadyTargetedPosition,
} }
@ -111,7 +111,7 @@ impl GameMap {
} }
} }
FireResult::Sunk FireResult::Sunk(*b)
} }
} }
} }

View File

@ -23,7 +23,7 @@ pub trait Player {
fn request_fire(&self, status: CurrentGameStatus); fn request_fire(&self, status: CurrentGameStatus);
fn other_player_must_fire(&self, status: CurrentGameStatus); fn opponent_must_fire(&self, status: CurrentGameStatus);
fn strike_result(&self, c: Coordinates, res: FireResult); fn strike_result(&self, c: Coordinates, res: FireResult);
@ -32,6 +32,12 @@ pub trait Player {
fn lost_game(&self, your_map: EndGameMap, opponent_map: EndGameMap); fn lost_game(&self, your_map: EndGameMap, opponent_map: EndGameMap);
fn won_game(&self, your_map: EndGameMap, opponent_map: EndGameMap); fn won_game(&self, your_map: EndGameMap, opponent_map: EndGameMap);
fn opponent_requested_rematch(&self);
fn opponent_rejected_rematch(&self);
fn opponent_accepted_rematch(&self);
} }
fn opponent(index: usize) -> usize { fn opponent(index: usize) -> usize {
@ -49,6 +55,8 @@ enum GameStatus {
WaitingForBoatsDisposition, WaitingForBoatsDisposition,
Started, Started,
Finished, Finished,
RematchRequested,
RematchRejected,
} }
pub struct Game { pub struct Game {
@ -109,7 +117,7 @@ impl Game {
fn request_fire(&self) { fn request_fire(&self) {
self.players[self.turn].request_fire(self.get_game_status_for_player(self.turn)); self.players[self.turn].request_fire(self.get_game_status_for_player(self.turn));
self.players[opponent(self.turn)] self.players[opponent(self.turn)]
.other_player_must_fire(self.get_game_status_for_player(opponent(self.turn))); .opponent_must_fire(self.get_game_status_for_player(opponent(self.turn)));
} }
fn player_map(&self, id: usize) -> &GameMap { fn player_map(&self, id: usize) -> &GameMap {
@ -143,7 +151,9 @@ impl Game {
return; return;
} }
if result == FireResult::Sunk && self.player_map(opponent(self.turn)).are_all_boat_sunk() { if matches!(result, FireResult::Sunk(_))
&& self.player_map(opponent(self.turn)).are_all_boat_sunk()
{
self.status = GameStatus::Finished; self.status = GameStatus::Finished;
let winner_map = self.player_map(self.turn).final_map(); let winner_map = self.player_map(self.turn).final_map();
@ -155,7 +165,7 @@ impl Game {
return; return;
} }
if (result == FireResult::Sunk || result == FireResult::Hit) if matches!(result, FireResult::Sunk(_) | FireResult::Hit)
&& !self.rules.player_continue_on_hit && !self.rules.player_continue_on_hit
{ {
self.turn = opponent(self.turn); self.turn = opponent(self.turn);
@ -164,6 +174,35 @@ impl Game {
self.request_fire(); self.request_fire();
} }
fn handle_request_rematch(&mut self, player_id: Uuid) {
self.status = GameStatus::RematchRequested;
self.turn = opponent(self.player_id_by_uuid(player_id));
self.players[self.turn].opponent_requested_rematch();
}
fn handle_request_rematch_response(&mut self, accepted: bool) {
if !accepted {
self.players[opponent(self.turn)].opponent_rejected_rematch();
self.status = GameStatus::RematchRejected;
return;
}
self.players[opponent(self.turn)].opponent_accepted_rematch();
// Swap players
let swap = self.players[1].clone();
self.players[1] = self.players[0].clone();
self.players[0] = swap;
// "Forget everything"
self.status = GameStatus::Created;
self.map_0 = None;
self.map_1 = None;
self.query_boats_disposition();
}
/// Get current game status for a specific player /// Get current game status for a specific player
fn get_game_status_for_player(&self, id: usize) -> CurrentGameStatus { fn get_game_status_for_player(&self, id: usize) -> CurrentGameStatus {
CurrentGameStatus { CurrentGameStatus {
@ -260,3 +299,42 @@ impl Handler<Fire> for Game {
self.handle_fire(msg.1) self.handle_fire(msg.1)
} }
} }
#[derive(Message, Debug)]
#[rtype(result = "()")]
pub struct RequestRematch(pub Uuid);
impl Handler<RequestRematch> for Game {
type Result = ();
fn handle(&mut self, msg: RequestRematch, _ctx: &mut Self::Context) -> Self::Result {
if self.status != GameStatus::Finished {
log::error!("Player attempted to request rematch on invalid step!");
return;
}
self.handle_request_rematch(msg.0);
}
}
#[derive(Message, Debug)]
#[rtype(result = "()")]
pub struct RespondRequestRematch(pub Uuid, pub bool);
impl Handler<RespondRequestRematch> for Game {
type Result = ();
fn handle(&mut self, msg: RespondRequestRematch, _ctx: &mut Self::Context) -> Self::Result {
if self.status != GameStatus::RematchRequested {
log::error!("Player attempted to respond to request rematch on invalid step!");
return;
}
if self.player_id_by_uuid(msg.0) != self.turn {
log::error!("Player can not respond to its own rematch request!");
return;
}
self.handle_request_rematch_response(msg.1);
}
}

View File

@ -2,7 +2,7 @@ use actix::Addr;
use uuid::Uuid; use uuid::Uuid;
use crate::data::{Coordinates, CurrentGameStatus, EndGameMap, FireResult, GameRules}; use crate::data::{Coordinates, CurrentGameStatus, EndGameMap, FireResult, GameRules};
use crate::game::{Fire, Game, Player, SetBoatsLayout}; use crate::game::*;
use crate::human_player_ws::{ClientMessage, HumanPlayerWS, ServerMessage}; use crate::human_player_ws::{ClientMessage, HumanPlayerWS, ServerMessage};
pub struct HumanPlayer { pub struct HumanPlayer {
@ -40,7 +40,7 @@ impl Player for HumanPlayer {
} }
fn notify_other_player_ready(&self) { fn notify_other_player_ready(&self) {
self.player.do_send(ServerMessage::OtherPlayerReady); self.player.do_send(ServerMessage::OpponentReady);
} }
fn notify_game_starting(&self) { fn notify_game_starting(&self) {
@ -51,9 +51,9 @@ impl Player for HumanPlayer {
self.player.do_send(ServerMessage::RequestFire { status }); self.player.do_send(ServerMessage::RequestFire { status });
} }
fn other_player_must_fire(&self, status: CurrentGameStatus) { fn opponent_must_fire(&self, status: CurrentGameStatus) {
self.player self.player
.do_send(ServerMessage::OtherPlayerMustFire { status }); .do_send(ServerMessage::OpponentMustFire { status });
} }
fn strike_result(&self, c: Coordinates, res: FireResult) { fn strike_result(&self, c: Coordinates, res: FireResult) {
@ -83,6 +83,18 @@ impl Player for HumanPlayer {
opponent_map, opponent_map,
}); });
} }
fn opponent_requested_rematch(&self) {
self.player.do_send(ServerMessage::OpponentRequestedRematch);
}
fn opponent_rejected_rematch(&self) {
self.player.do_send(ServerMessage::OpponentRejectedRematch);
}
fn opponent_accepted_rematch(&self) {
self.player.do_send(ServerMessage::OpponentAcceptedRematch);
}
} }
impl HumanPlayer { impl HumanPlayer {
@ -95,6 +107,14 @@ impl HumanPlayer {
self.game.do_send(SetBoatsLayout(self.uuid, layout)) self.game.do_send(SetBoatsLayout(self.uuid, layout))
} }
ClientMessage::Fire { location } => self.game.do_send(Fire(self.uuid, location)), ClientMessage::Fire { location } => self.game.do_send(Fire(self.uuid, location)),
ClientMessage::RequestRematch => self.game.do_send(RequestRematch(self.uuid)),
ClientMessage::AcceptRematch => {
self.game.do_send(RespondRequestRematch(self.uuid, true))
}
ClientMessage::RejectRematch => {
self.game.do_send(RespondRequestRematch(self.uuid, false))
}
} }
} }
} }

View File

@ -34,6 +34,9 @@ pub enum ClientMessage {
StopGame, StopGame,
BoatsLayout { layout: BoatsLayout }, BoatsLayout { layout: BoatsLayout },
Fire { location: Coordinates }, Fire { location: Coordinates },
RequestRematch,
AcceptRematch,
RejectRematch,
} }
#[derive(Message)] #[derive(Message)]
@ -52,9 +55,9 @@ pub enum ServerMessage {
errors: Vec<String>, errors: Vec<String>,
}, },
WaitingForOtherPlayerConfiguration, WaitingForOtherPlayerConfiguration,
OtherPlayerReady, OpponentReady,
GameStarting, GameStarting,
OtherPlayerMustFire { OpponentMustFire {
status: CurrentGameStatus, status: CurrentGameStatus,
}, },
RequestFire { RequestFire {
@ -76,6 +79,9 @@ pub enum ServerMessage {
your_map: EndGameMap, your_map: EndGameMap,
opponent_map: EndGameMap, opponent_map: EndGameMap,
}, },
OpponentRequestedRematch,
OpponentAcceptedRematch,
OpponentRejectedRematch,
} }
pub struct HumanPlayerWS { pub struct HumanPlayerWS {

View File

@ -1,8 +1,6 @@
extern crate core; extern crate core;
pub mod args; pub mod args;
#[cfg(test)]
pub mod bot_client;
pub mod consts; pub mod consts;
pub mod data; pub mod data;
pub mod game; pub mod game;

View File

@ -2,7 +2,7 @@ use actix::Addr;
use uuid::Uuid; use uuid::Uuid;
use crate::data::{BoatsLayout, Coordinates, CurrentGameStatus, EndGameMap, FireResult, GameRules}; use crate::data::{BoatsLayout, Coordinates, CurrentGameStatus, EndGameMap, FireResult, GameRules};
use crate::game::{Fire, Game, Player, SetBoatsLayout}; use crate::game::{Fire, Game, Player, RespondRequestRematch, SetBoatsLayout};
#[derive(Clone)] #[derive(Clone)]
pub struct RandomBot { pub struct RandomBot {
@ -54,7 +54,7 @@ impl Player for RandomBot {
.do_send(Fire(self.uuid, status.find_valid_random_fire_location())); .do_send(Fire(self.uuid, status.find_valid_random_fire_location()));
} }
fn other_player_must_fire(&self, _status: CurrentGameStatus) {} fn opponent_must_fire(&self, _status: CurrentGameStatus) {}
fn strike_result(&self, _c: Coordinates, _res: FireResult) {} fn strike_result(&self, _c: Coordinates, _res: FireResult) {}
@ -63,4 +63,12 @@ impl Player for RandomBot {
fn lost_game(&self, _your_map: EndGameMap, _opponent_map: EndGameMap) {} fn lost_game(&self, _your_map: EndGameMap, _opponent_map: EndGameMap) {}
fn won_game(&self, _your_map: EndGameMap, _opponent_map: EndGameMap) {} fn won_game(&self, _your_map: EndGameMap, _opponent_map: EndGameMap) {}
fn opponent_requested_rematch(&self) {
self.game.do_send(RespondRequestRematch(self.uuid, true));
}
fn opponent_rejected_rematch(&self) {}
fn opponent_accepted_rematch(&self) {}
} }

View File

@ -0,0 +1,205 @@
use std::error::Error;
use std::fmt::Display;
use futures::{SinkExt, StreamExt};
use tokio_tungstenite::tungstenite::Message;
use crate::data::{BoatsLayout, GameRules};
use crate::human_player_ws::{ClientMessage, ServerMessage};
#[derive(Debug, Eq, PartialEq)]
pub enum ClientEndResult {
Finished,
InvalidBoatsLayout,
OpponentRejectedRematch,
}
pub struct BotClient {
server: String,
requested_rules: GameRules,
layout: Option<BoatsLayout>,
number_plays: usize,
}
impl BotClient {
pub fn new<D>(server: D) -> Self
where
D: Display,
{
Self {
server: server.to_string(),
requested_rules: GameRules::random_players_rules(),
layout: None,
number_plays: 1,
}
}
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 async fn run_client(&self) -> Result<ClientEndResult, Box<dyn Error>> {
let mut remaining_games = self.number_plays;
let url = format!(
"{}/play/bot?{}",
self.server.replace("http", "ws"),
serde_urlencoded::to_string(&self.requested_rules).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;
}
};
match message {
ServerMessage::WaitingForAnotherPlayer => {
log::debug!("Waiting for other player...")
}
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_valid_random_fire_location();
log::debug!("Will fire at {:?}", location);
socket
.send(Message::Text(serde_json::to_string(
&ClientMessage::Fire { location },
)?))
.await?;
}
ServerMessage::StrikeResult { pos, result } => {
log::debug!("Strike at {} result: {:?}", pos.human_print(), result)
}
ServerMessage::OpponentStrikeResult { pos, result } => log::debug!(
"Opponent trike at {} result: {:?}",
pos.human_print(),
result
),
ServerMessage::LostGame {
your_map,
opponent_map,
} => {
log::debug!("We lost game :(");
log::debug!("Other game:\n{}\n", opponent_map.get_map());
log::debug!("Our game:\n{}\n", your_map.get_map());
if remaining_games > 0 {
socket
.send(Message::Text(serde_json::to_string(
&ClientMessage::RequestRematch,
)?))
.await?;
} else {
break;
}
}
ServerMessage::WonGame {
your_map,
opponent_map,
} => {
log::debug!("We won the game !!!!");
log::debug!("Other game:\n{}\n", opponent_map.get_map());
log::debug!("Our game:\n{}\n", your_map.get_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 rejected rematch.");
}
ServerMessage::OpponentAcceptedRematch => {
log::debug!("Opponent accepted rematch");
}
ServerMessage::OpponentRejectedRematch => {
log::debug!("Opponent rejected rematch");
return Ok(ClientEndResult::OpponentRejectedRematch);
}
}
}
Ok(ClientEndResult::Finished)
}
}

View File

@ -1,10 +1,10 @@
use tokio::task; use tokio::task;
use crate::args::Args; use crate::args::Args;
use crate::bot_client;
use crate::bot_client::ClientEndResult;
use crate::data::{BoatsLayout, GameRules}; use crate::data::{BoatsLayout, GameRules};
use crate::server::start_server; 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::network_utils::wait_for_port;
use crate::test::TestPort; use crate::test::TestPort;
@ -12,14 +12,10 @@ use crate::test::TestPort;
async fn invalid_port() { async fn invalid_port() {
let _ = env_logger::builder().is_test(true).try_init(); let _ = env_logger::builder().is_test(true).try_init();
let rules = GameRules::random_players_rules(); bot_client::BotClient::new(TestPort::ClientInvalidPort.as_url())
bot_client::run_client( .run_client()
&TestPort::ClientInvalidPort.as_url(), .await
&rules, .unwrap_err();
BoatsLayout::gen_random_for_rules(&rules).unwrap(),
)
.await
.unwrap_err();
} }
#[tokio::test] #[tokio::test]
@ -35,13 +31,14 @@ async fn invalid_rules() {
task::spawn_local(start_server(Args::for_test(TestPort::ClientInvalidRules))); task::spawn_local(start_server(Args::for_test(TestPort::ClientInvalidRules)));
wait_for_port(TestPort::ClientInvalidRules.port()).await; wait_for_port(TestPort::ClientInvalidRules.port()).await;
bot_client::run_client( bot_client::BotClient::new(TestPort::ClientInvalidRules.as_url())
&TestPort::ClientInvalidRules.as_url(), .with_rules(rules.clone())
&rules, .with_layout(
BoatsLayout::gen_random_for_rules(&GameRules::random_players_rules()).unwrap(), BoatsLayout::gen_random_for_rules(&GameRules::random_players_rules()).unwrap(),
) )
.await .run_client()
.unwrap_err(); .await
.unwrap_err();
}) })
.await; .await;
} }
@ -53,17 +50,13 @@ async fn full_game() {
let local_set = task::LocalSet::new(); let local_set = task::LocalSet::new();
local_set local_set
.run_until(async move { .run_until(async move {
let rules = GameRules::random_players_rules();
task::spawn_local(start_server(Args::for_test(TestPort::FullGame))); task::spawn_local(start_server(Args::for_test(TestPort::FullGame)));
wait_for_port(TestPort::FullGame.port()).await; wait_for_port(TestPort::FullGame.port()).await;
let res = bot_client::run_client( let res = bot_client::BotClient::new(TestPort::FullGame.as_url())
&TestPort::FullGame.as_url(), .run_client()
&rules, .await
BoatsLayout::gen_random_for_rules(&rules).unwrap(), .unwrap();
)
.await
.unwrap();
assert_eq!(res, ClientEndResult::Finished); assert_eq!(res, ClientEndResult::Finished);
}) })
.await; .await;
@ -83,13 +76,11 @@ async fn full_game_no_touching_boats() {
))); )));
wait_for_port(TestPort::FullGameTouchingBoats.port()).await; wait_for_port(TestPort::FullGameTouchingBoats.port()).await;
let res = bot_client::run_client( let res = bot_client::BotClient::new(TestPort::FullGameTouchingBoats.as_url())
&TestPort::FullGameTouchingBoats.as_url(), .with_rules(rules)
&rules, .run_client()
BoatsLayout::gen_random_for_rules(&rules).unwrap(), .await
) .unwrap();
.await
.unwrap();
assert_eq!(res, ClientEndResult::Finished); assert_eq!(res, ClientEndResult::Finished);
}) })
@ -114,10 +105,12 @@ async fn invalid_boats_layout_number_of_boats() {
rules_modified.pop_boat(); rules_modified.pop_boat();
let layout = BoatsLayout::gen_random_for_rules(&rules_modified).unwrap(); let layout = BoatsLayout::gen_random_for_rules(&rules_modified).unwrap();
let res = let res = bot_client::BotClient::new(&TestPort::FullGameTouchingBoats.as_url())
bot_client::run_client(&TestPort::FullGameTouchingBoats.as_url(), &rules, layout) .with_rules(rules)
.await .with_layout(layout)
.unwrap(); .run_client()
.await
.unwrap();
assert_eq!(res, ClientEndResult::InvalidBoatsLayout); assert_eq!(res, ClientEndResult::InvalidBoatsLayout);
}) })
@ -143,12 +136,34 @@ async fn invalid_boats_layout_len_of_a_boat() {
rules_modified.add_boat(previous - 1); rules_modified.add_boat(previous - 1);
let layout = BoatsLayout::gen_random_for_rules(&rules_modified).unwrap(); let layout = BoatsLayout::gen_random_for_rules(&rules_modified).unwrap();
let res = let res = bot_client::BotClient::new(&TestPort::FullGameTouchingBoats.as_url())
bot_client::run_client(&TestPort::FullGameTouchingBoats.as_url(), &rules, layout) .with_rules(rules)
.await .with_layout(layout)
.unwrap(); .run_client()
.await
.unwrap();
assert_eq!(res, ClientEndResult::InvalidBoatsLayout); assert_eq!(res, ClientEndResult::InvalidBoatsLayout);
}) })
.await; .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::FullGame)));
wait_for_port(TestPort::FullGame.port()).await;
let res = bot_client::BotClient::new(TestPort::FullGame.as_url())
.with_number_plays(5)
.run_client()
.await
.unwrap();
assert_eq!(res, ClientEndResult::Finished);
})
.await;
}

View File

@ -27,5 +27,7 @@ impl Args {
} }
} }
mod client; #[cfg(test)]
pub mod bot_client;
mod bot_client_bot_play;
mod network_utils; mod network_utils;