Added rematch feature
This commit is contained in:
parent
13c03a8df0
commit
45060ae14b
@ -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)
|
|
||||||
}
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
@ -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;
|
||||||
|
@ -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) {}
|
||||||
}
|
}
|
||||||
|
205
sea_battle_backend/src/test/bot_client.rs
Normal file
205
sea_battle_backend/src/test/bot_client.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
@ -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;
|
||||||
|
Loading…
Reference in New Issue
Block a user