Add tests for no replay after hit rule

This commit is contained in:
Pierre HUBERT 2022-09-19 19:29:11 +02:00
parent 7cfd7a4899
commit 6832aebedb
17 changed files with 280 additions and 132 deletions

View File

@ -1,7 +1,7 @@
use actix::Addr;
use uuid::Uuid;
use crate::data::{BoatsLayout, Coordinates, CurrentGameStatus, EndGameMap, FireResult, GameRules};
use crate::data::{BoatsLayout, Coordinates, CurrentGameStatus, FireResult, GameRules};
use crate::game::{Fire, Game, Player, RespondRequestRematch, SetBoatsLayout};
#[derive(Clone)]
@ -64,9 +64,9 @@ impl Player for LinearBot {
fn other_player_strike_result(&self, _c: Coordinates, _res: FireResult) {}
fn lost_game(&self, _your_map: EndGameMap, _opponent_map: EndGameMap) {}
fn lost_game(&self, _status: CurrentGameStatus) {}
fn won_game(&self, _your_map: EndGameMap, _opponent_map: EndGameMap) {}
fn won_game(&self, _status: CurrentGameStatus) {}
fn opponent_requested_rematch(&self) {
self.game.do_send(RespondRequestRematch(self.uuid, true));

View File

@ -1,7 +1,7 @@
use actix::Addr;
use uuid::Uuid;
use crate::data::{BoatsLayout, Coordinates, CurrentGameStatus, EndGameMap, FireResult, GameRules};
use crate::data::{BoatsLayout, Coordinates, CurrentGameStatus, FireResult, GameRules};
use crate::game::{Fire, Game, Player, RespondRequestRematch, SetBoatsLayout};
#[derive(Clone)]
@ -64,9 +64,9 @@ impl Player for RandomBot {
fn other_player_strike_result(&self, _c: Coordinates, _res: FireResult) {}
fn lost_game(&self, _your_map: EndGameMap, _opponent_map: EndGameMap) {}
fn lost_game(&self, _status: CurrentGameStatus) {}
fn won_game(&self, _your_map: EndGameMap, _opponent_map: EndGameMap) {}
fn won_game(&self, _status: CurrentGameStatus) {}
fn opponent_requested_rematch(&self) {
self.game.do_send(RespondRequestRematch(self.uuid, true));

View File

@ -213,6 +213,42 @@ impl BoatsLayout {
Ok(boats)
}
/// Generate boats layout that put boats at the beginning of the map
pub fn layout_for_boats_at_beginning_of_map(rules: &GameRules) -> std::io::Result<Self> {
let mut boats = Self(Vec::with_capacity(rules.boats_list().len()));
let mut list = rules.boats_list();
list.sort();
list.reverse();
for y in 0..rules.map_height {
for x in 0..rules.map_width {
if list.is_empty() {
break;
}
let position = BoatPosition {
start: Coordinates::new(x as i32, y as i32),
len: list[0],
direction: BoatDirection::Right,
};
if boats.is_acceptable_boat_position(&position, rules) {
list.remove(0);
boats.0.push(position);
}
}
}
if !list.is_empty() {
return Err(std::io::Error::new(
ErrorKind::Other,
"Un-usable game rules!",
));
}
Ok(boats)
}
fn is_acceptable_boat_position(&self, pos: &BoatPosition, rules: &GameRules) -> bool {
// Check if boat coordinates are valid
if pos.all_coordinates().iter().any(|c| !c.is_valid(rules)) {

View File

@ -16,6 +16,10 @@ impl CurrentGameMapStatus {
pub fn did_fire_at_location(&self, c: Coordinates) -> bool {
self.successful_strikes.contains(&c) || self.failed_strikes.contains(&c)
}
pub fn number_of_fires(&self) -> usize {
self.successful_strikes.len() + self.failed_strikes.len()
}
}
struct PrintableCurrentGameMapStatus(GameRules, CurrentGameMapStatus);
@ -207,12 +211,20 @@ impl CurrentGameStatus {
boats_size.first().cloned()
}
pub fn get_your_map(&self) -> String {
PrintableCurrentGameMapStatus(self.rules.clone(), self.your_map.clone()).get_map()
}
pub fn get_opponent_map(&self) -> String {
PrintableCurrentGameMapStatus(self.rules.clone(), self.opponent_map.clone()).get_map()
}
pub fn print_your_map(&self) {
PrintableCurrentGameMapStatus(self.rules.clone(), self.your_map.clone()).print_map()
print!("{}", self.get_your_map());
}
pub fn print_opponent_map(&self) {
PrintableCurrentGameMapStatus(self.rules.clone(), self.opponent_map.clone()).print_map()
print!("{}", self.get_opponent_map());
}
}

View File

@ -1,22 +0,0 @@
use std::fmt::Write;
use crate::data::{BoatsLayout, MapCellContent};
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct EndGameMap {
pub boats: BoatsLayout,
pub grid: Vec<Vec<MapCellContent>>,
}
impl EndGameMap {
pub fn get_map(&self) -> String {
let mut s = String::new();
for row in &self.grid {
for col in row {
write!(&mut s, "{} ", col.letter()).unwrap();
}
s.push('\n');
}
s
}
}

View File

@ -1,7 +1,5 @@
use crate::data::boats_layout::{BoatsLayout, Coordinates};
use crate::data::{
BoatPosition, CurrentGameMapStatus, EndGameMap, GameRules, MapCellContent, PrintableMap,
};
use crate::data::*;
#[derive(Debug, Copy, Clone, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
pub enum FireResult {
@ -110,19 +108,6 @@ impl GameMap {
sunk_boats: self.sunk_boats.clone(),
}
}
pub fn final_map(&self) -> EndGameMap {
EndGameMap {
boats: self.boats_config.clone(),
grid: (0..self.rules.map_height)
.map(|y| {
(0..self.rules.map_width)
.map(|x| self.get_cell_content(Coordinates::new(x as i32, y as i32)))
.collect::<Vec<_>>()
})
.collect::<Vec<_>>(),
}
}
}
impl PrintableMap for GameMap {

View File

@ -38,6 +38,11 @@ impl GameRules {
self
}
pub fn with_player_continue_on_hit(mut self, c: bool) -> Self {
self.player_continue_on_hit = c;
self
}
/// Set the list of boats for this configuration
pub fn set_boats_list(&mut self, boats: &[usize]) {
self.boats_str = boats

View File

@ -1,6 +1,5 @@
pub use boats_layout::*;
pub use current_game_status::*;
pub use end_game_map::*;
pub use game_map::*;
pub use game_rules::*;
pub use play_config::*;
@ -8,7 +7,6 @@ pub use printable_map::*;
mod boats_layout;
mod current_game_status;
mod end_game_map;
mod game_map;
mod game_rules;
mod play_config;

View File

@ -1,3 +1,6 @@
use std::fmt::Write;
use std::string::String;
use crate::data::Coordinates;
#[derive(Debug, Copy, Clone, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
@ -27,6 +30,12 @@ pub trait PrintableMap {
fn map_cell_content(&self, c: Coordinates) -> MapCellContent;
fn print_map(&self) {
print!("{}", self.get_map());
}
fn get_map(&self) -> String {
let mut out = String::with_capacity(100);
let mut y = 0;
let mut x;
loop {
@ -38,11 +47,11 @@ pub trait PrintableMap {
break;
}
print!("{} ", content.letter());
write!(out, "{} ", content.letter()).unwrap();
x += 1;
}
println!();
out.push('\n');
// x == 0 <=> we reached the end of the map
if x == 0 {
@ -51,5 +60,7 @@ pub trait PrintableMap {
y += 1;
}
out
}
}

View File

@ -32,9 +32,9 @@ pub trait Player {
fn other_player_strike_result(&self, c: Coordinates, res: FireResult);
fn lost_game(&self, your_map: EndGameMap, opponent_map: EndGameMap);
fn lost_game(&self, status: CurrentGameStatus);
fn won_game(&self, your_map: EndGameMap, opponent_map: EndGameMap);
fn won_game(&self, status: CurrentGameStatus);
fn opponent_requested_rematch(&self);
@ -163,11 +163,9 @@ impl Game {
{
self.status = GameStatus::Finished;
let winner_map = self.player_map(self.turn).final_map();
let looser_map = self.player_map(opponent(self.turn)).final_map();
self.players[self.turn].won_game(winner_map.clone(), looser_map.clone());
self.players[opponent(self.turn)].lost_game(looser_map, winner_map);
self.players[self.turn].won_game(self.get_game_status_for_player(self.turn));
self.players[opponent(self.turn)]
.lost_game(self.get_game_status_for_player(opponent(self.turn)));
return;
}
@ -215,7 +213,9 @@ impl Game {
CurrentGameStatus {
rules: self.rules.clone(),
your_map: self.player_map(id).current_map_status(false),
opponent_map: self.player_map(opponent(id)).current_map_status(true),
opponent_map: self
.player_map(opponent(id))
.current_map_status(self.status != GameStatus::Finished),
}
}
}

View File

@ -1,7 +1,7 @@
use actix::Addr;
use uuid::Uuid;
use crate::data::{Coordinates, CurrentGameStatus, EndGameMap, FireResult, GameRules};
use crate::data::*;
use crate::game::*;
use crate::human_player_ws::{ClientMessage, HumanPlayerWS, ServerMessage};
@ -74,18 +74,12 @@ impl Player for HumanPlayer {
});
}
fn lost_game(&self, your_map: EndGameMap, opponent_map: EndGameMap) {
self.player.do_send(ServerMessage::LostGame {
your_map,
opponent_map,
});
fn lost_game(&self, status: CurrentGameStatus) {
self.player.do_send(ServerMessage::LostGame { status });
}
fn won_game(&self, your_map: EndGameMap, opponent_map: EndGameMap) {
self.player.do_send(ServerMessage::WonGame {
your_map,
opponent_map,
});
fn won_game(&self, status: CurrentGameStatus) {
self.player.do_send(ServerMessage::WonGame { status });
}
fn opponent_requested_rematch(&self) {

View File

@ -1,17 +1,15 @@
use std::sync::Arc;
use std::time::{Duration, Instant};
use crate::bots::linear_bot::LinearBot;
use actix::prelude::*;
use actix::{Actor, Handler, StreamHandler};
use actix_web_actors::ws;
use actix_web_actors::ws::{CloseCode, CloseReason, Message, ProtocolError, WebsocketContext};
use uuid::Uuid;
use crate::bots::linear_bot::LinearBot;
use crate::bots::random_bot::RandomBot;
use crate::data::{
BoatsLayout, BotType, Coordinates, CurrentGameStatus, EndGameMap, FireResult, GameRules,
};
use crate::data::{BoatsLayout, BotType, Coordinates, CurrentGameStatus, FireResult, GameRules};
use crate::game::{AddPlayer, Game};
use crate::human_player::HumanPlayer;
@ -73,12 +71,10 @@ pub enum ServerMessage {
result: FireResult,
},
LostGame {
your_map: EndGameMap,
opponent_map: EndGameMap,
status: CurrentGameStatus,
},
WonGame {
your_map: EndGameMap,
opponent_map: EndGameMap,
status: CurrentGameStatus,
},
OpponentRequestedRematch,
OpponentAcceptedRematch,

View File

@ -160,13 +160,10 @@ impl BotClient {
pos.human_print(),
result
),
ServerMessage::LostGame {
your_map,
opponent_map,
} => {
ServerMessage::LostGame { status } => {
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());
log::debug!("Opponent map:\n{}", status.get_opponent_map());
log::debug!("Our map:\n{}\n", status.get_your_map());
if remaining_games > 0 {
socket
@ -178,13 +175,10 @@ impl BotClient {
break;
}
}
ServerMessage::WonGame {
your_map,
opponent_map,
} => {
ServerMessage::WonGame { status } => {
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());
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

View File

@ -1,11 +1,12 @@
use tokio::task;
use crate::args::Args;
use crate::data::{BotType, Coordinates, GameRules};
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) {
@ -49,3 +50,95 @@ async fn full_game() {
})
.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_eq!(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_eq!(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_eq!(res, ClientEndResult::Finished);
})
.await;
}

View File

@ -6,13 +6,14 @@ 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::ClientInvalidPort.as_url())
bot_client::BotClient::new(TestPort::RandomBotClientInvalidPort.as_url())
.run_client()
.await
.unwrap_err();
@ -28,10 +29,12 @@ async fn invalid_rules() {
let mut rules = GameRules::random_players_rules();
rules.map_width = 0;
task::spawn_local(start_server(Args::for_test(TestPort::ClientInvalidRules)));
wait_for_port(TestPort::ClientInvalidRules.port()).await;
task::spawn_local(start_server(Args::for_test(
TestPort::RandomBotClientInvalidRules,
)));
wait_for_port(TestPort::RandomBotClientInvalidRules.port()).await;
bot_client::BotClient::new(TestPort::ClientInvalidRules.as_url())
bot_client::BotClient::new(TestPort::RandomBotClientInvalidRules.as_url())
.with_rules(rules.clone())
.with_layout(
BoatsLayout::gen_random_for_rules(&GameRules::random_players_rules()).unwrap(),
@ -50,10 +53,10 @@ async fn full_game() {
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;
task::spawn_local(start_server(Args::for_test(TestPort::RandomBotFullGame)));
wait_for_port(TestPort::RandomBotFullGame.port()).await;
let res = bot_client::BotClient::new(TestPort::FullGame.as_url())
let res = bot_client::BotClient::new(TestPort::RandomBotFullGame.as_url())
.run_client()
.await
.unwrap();
@ -72,11 +75,12 @@ async fn full_game_no_touching_boats() {
let mut rules = GameRules::random_players_rules();
rules.boats_can_touch = false;
task::spawn_local(start_server(Args::for_test(
TestPort::FullGameNoTouchingBoats,
TestPort::RandomBotFullGameNoTouchingBoats,
)));
wait_for_port(TestPort::FullGameNoTouchingBoats.port()).await;
wait_for_port(TestPort::RandomBotFullGameNoTouchingBoats.port()).await;
let res = bot_client::BotClient::new(TestPort::FullGameNoTouchingBoats.as_url())
let res =
bot_client::BotClient::new(TestPort::RandomBotFullGameNoTouchingBoats.as_url())
.with_rules(rules)
.run_client()
.await
@ -97,16 +101,17 @@ async fn invalid_boats_layout_number_of_boats() {
let mut rules = GameRules::random_players_rules();
rules.boats_can_touch = false;
task::spawn_local(start_server(Args::for_test(
TestPort::InvalidBoatsLayoutNumberOfBoats,
TestPort::RandomBotInvalidBoatsLayoutNumberOfBoats,
)));
wait_for_port(TestPort::InvalidBoatsLayoutNumberOfBoats.port()).await;
wait_for_port(TestPort::RandomBotInvalidBoatsLayoutNumberOfBoats.port()).await;
let mut rules_modified = rules.clone();
rules_modified.pop_boat();
let layout = BoatsLayout::gen_random_for_rules(&rules_modified).unwrap();
let res =
bot_client::BotClient::new(&TestPort::InvalidBoatsLayoutNumberOfBoats.as_url())
let res = bot_client::BotClient::new(
&TestPort::RandomBotInvalidBoatsLayoutNumberOfBoats.as_url(),
)
.with_rules(rules)
.with_layout(layout)
.run_client()
@ -128,16 +133,18 @@ async fn invalid_boats_layout_len_of_a_boat() {
let mut rules = GameRules::random_players_rules();
rules.boats_can_touch = false;
task::spawn_local(start_server(Args::for_test(
TestPort::InvalidBoatsLayoutLenOfABoat,
TestPort::RandomBotInvalidBoatsLayoutLenOfABoat,
)));
wait_for_port(TestPort::InvalidBoatsLayoutLenOfABoat.port()).await;
wait_for_port(TestPort::RandomBotInvalidBoatsLayoutLenOfABoat.port()).await;
let mut rules_modified = rules.clone();
let previous = rules_modified.pop_boat();
rules_modified.add_boat(previous - 1);
let layout = BoatsLayout::gen_random_for_rules(&rules_modified).unwrap();
let res = bot_client::BotClient::new(&TestPort::InvalidBoatsLayoutLenOfABoat.as_url())
let res = bot_client::BotClient::new(
&TestPort::RandomBotInvalidBoatsLayoutLenOfABoat.as_url(),
)
.with_rules(rules)
.with_layout(layout)
.run_client()
@ -157,11 +164,12 @@ async fn full_game_multiple_rematches() {
local_set
.run_until(async move {
task::spawn_local(start_server(Args::for_test(
TestPort::FullGameMultipleRematch,
TestPort::RandomBotFullGameMultipleRematch,
)));
wait_for_port(TestPort::FullGameMultipleRematch.port()).await;
wait_for_port(TestPort::RandomBotFullGameMultipleRematch.port()).await;
let res = bot_client::BotClient::new(TestPort::FullGameMultipleRematch.as_url())
let res =
bot_client::BotClient::new(TestPort::RandomBotFullGameMultipleRematch.as_url())
.with_number_plays(5)
.run_client()
.await
@ -170,3 +178,26 @@ async fn full_game_multiple_rematches() {
})
.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_eq!(res, ClientEndResult::Finished);
})
.await;
}

View File

@ -2,14 +2,16 @@ use crate::args::Args;
#[derive(Copy, Clone)]
enum TestPort {
ClientInvalidPort = 20000,
ClientInvalidRules,
FullGame,
FullGameNoTouchingBoats,
InvalidBoatsLayoutNumberOfBoats,
InvalidBoatsLayoutLenOfABoat,
FullGameMultipleRematch,
RandomBotClientInvalidPort = 20000,
RandomBotClientInvalidRules,
RandomBotFullGame,
RandomBotFullGameNoTouchingBoats,
RandomBotInvalidBoatsLayoutNumberOfBoats,
RandomBotInvalidBoatsLayoutLenOfABoat,
RandomBotFullGameMultipleRematch,
RandomBotNoReplayOnHit,
LinearBotFullGame,
LinearBotNoReplayOnHit,
}
impl TestPort {
@ -36,3 +38,4 @@ pub mod bot_client;
mod bot_client_bot_linear_play;
mod bot_client_bot_random_play;
mod network_utils;
mod play_utils;

View 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);
}
}