Compare commits
9 Commits
9a38a634eb
...
ccb3d36fae
Author | SHA1 | Date | |
---|---|---|---|
ccb3d36fae | |||
fcc7f30e10 | |||
171c88f303 | |||
9162c5eb24 | |||
b4772aa88e | |||
42b0d84f9d | |||
ba1ed84b33 | |||
8c1a3f2c5f | |||
25871de084 |
@@ -6,7 +6,7 @@ use hyper_rustls::ConfigBuilderExt;
|
||||
use sea_battle_backend::data::GameRules;
|
||||
use sea_battle_backend::human_player_ws::{ClientMessage, ServerMessage};
|
||||
use sea_battle_backend::server::{BotPlayQuery, PlayRandomQuery};
|
||||
use sea_battle_backend::utils::{boxed_error, Res};
|
||||
use sea_battle_backend::utils::res_utils::{boxed_error, Res};
|
||||
use std::fmt::Display;
|
||||
use std::sync::mpsc::TryRecvError;
|
||||
use std::sync::{mpsc, Arc};
|
||||
|
@@ -23,7 +23,7 @@ use cli_player::ui_screens::select_play_mode_screen::{SelectPlayModeResult, Sele
|
||||
use cli_player::ui_screens::*;
|
||||
use sea_battle_backend::consts::{MAX_PLAYER_NAME_LENGTH, MIN_PLAYER_NAME_LENGTH};
|
||||
use sea_battle_backend::data::GameRules;
|
||||
use sea_battle_backend::utils::Res;
|
||||
use sea_battle_backend::utils::res_utils::Res;
|
||||
|
||||
/// Test code screens
|
||||
async fn run_dev<B: Backend>(
|
||||
|
@@ -5,6 +5,9 @@ use std::time::{Duration, Instant};
|
||||
|
||||
use crossterm::event;
|
||||
use crossterm::event::{Event, KeyCode};
|
||||
use sea_battle_backend::consts::{
|
||||
MAX_BOATS_NUMBER, MAX_MAP_HEIGHT, MAX_MAP_WIDTH, MAX_STRIKE_TIMEOUT,
|
||||
};
|
||||
use tui::backend::Backend;
|
||||
use tui::layout::{Constraint, Direction, Layout, Margin};
|
||||
use tui::style::{Color, Style};
|
||||
@@ -26,6 +29,7 @@ enum EditingField {
|
||||
MapWidth = 0,
|
||||
MapHeight,
|
||||
BoatsList,
|
||||
StrikeTimeout,
|
||||
BoatsCanTouch,
|
||||
PlayerContinueOnHit,
|
||||
BotType,
|
||||
@@ -114,24 +118,46 @@ impl GameRulesConfigurationScreen {
|
||||
{
|
||||
self.rules.remove_last_boat();
|
||||
}
|
||||
|
||||
if self.curr_field == EditingField::StrikeTimeout {
|
||||
match self.rules.strike_timeout.unwrap_or(0) / 10 {
|
||||
0 => self.rules.strike_timeout = None,
|
||||
v => self.rules.strike_timeout = Some(v),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
KeyCode::Char(c) if ('0'..='9').contains(&c) => {
|
||||
let val = c.to_string().parse::<usize>().unwrap_or_default();
|
||||
|
||||
if self.curr_field == EditingField::MapWidth {
|
||||
if self.curr_field == EditingField::MapWidth
|
||||
&& self.rules.map_width <= MAX_MAP_WIDTH
|
||||
{
|
||||
self.rules.map_width *= 10;
|
||||
self.rules.map_width += val;
|
||||
}
|
||||
|
||||
if self.curr_field == EditingField::MapHeight {
|
||||
if self.curr_field == EditingField::MapHeight
|
||||
&& self.rules.map_height <= MAX_MAP_HEIGHT
|
||||
{
|
||||
self.rules.map_height *= 10;
|
||||
self.rules.map_height += val;
|
||||
}
|
||||
|
||||
if self.curr_field == EditingField::BoatsList {
|
||||
if self.curr_field == EditingField::BoatsList
|
||||
&& self.rules.boats_list().len() < MAX_BOATS_NUMBER
|
||||
{
|
||||
self.rules.add_boat(val);
|
||||
}
|
||||
|
||||
if self.curr_field == EditingField::StrikeTimeout {
|
||||
let mut timeout = self.rules.strike_timeout.unwrap_or(0);
|
||||
if timeout <= MAX_STRIKE_TIMEOUT {
|
||||
timeout *= 10;
|
||||
timeout += val as u64;
|
||||
self.rules.strike_timeout = Some(timeout);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_ => {}
|
||||
@@ -153,7 +179,7 @@ impl GameRulesConfigurationScreen {
|
||||
}
|
||||
|
||||
fn ui<B: Backend>(&mut self, f: &mut Frame<B>) {
|
||||
let area = centered_rect_size(50, 20, &f.size());
|
||||
let area = centered_rect_size(50, 23, &f.size());
|
||||
|
||||
let block = Block::default().title("Game rules").borders(Borders::ALL);
|
||||
f.render_widget(block, area);
|
||||
@@ -161,11 +187,12 @@ impl GameRulesConfigurationScreen {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([
|
||||
Constraint::Length(3),
|
||||
Constraint::Length(3),
|
||||
Constraint::Length(3),
|
||||
Constraint::Length(1),
|
||||
Constraint::Length(1),
|
||||
Constraint::Length(3), // Map width
|
||||
Constraint::Length(3), // Map height
|
||||
Constraint::Length(3), // Boats list
|
||||
Constraint::Length(3), // Strike timeout
|
||||
Constraint::Length(1), // Boats can touch
|
||||
Constraint::Length(1), // Player continue on hit
|
||||
Constraint::Length(3), // Bot type
|
||||
Constraint::Length(1), // Margin
|
||||
Constraint::Length(1), // Buttons
|
||||
@@ -203,6 +230,13 @@ impl GameRulesConfigurationScreen {
|
||||
);
|
||||
f.render_widget(editor, chunks[EditingField::BoatsList as usize]);
|
||||
|
||||
let editor = TextEditorWidget::new(
|
||||
"Strike timeout (0 to disable)",
|
||||
&self.rules.strike_timeout.unwrap_or(0).to_string(),
|
||||
self.curr_field == EditingField::StrikeTimeout,
|
||||
);
|
||||
f.render_widget(editor, chunks[EditingField::StrikeTimeout as usize]);
|
||||
|
||||
let editor = CheckboxWidget::new(
|
||||
"Boats can touch",
|
||||
self.rules.boats_can_touch,
|
||||
|
@@ -12,7 +12,8 @@ use tui::{Frame, Terminal};
|
||||
|
||||
use sea_battle_backend::data::{Coordinates, CurrentGameMapStatus, CurrentGameStatus};
|
||||
use sea_battle_backend::human_player_ws::{ClientMessage, ServerMessage};
|
||||
use sea_battle_backend::utils::Res;
|
||||
use sea_battle_backend::utils::res_utils::Res;
|
||||
use sea_battle_backend::utils::time_utils::time;
|
||||
|
||||
use crate::client::Client;
|
||||
use crate::constants::*;
|
||||
@@ -60,8 +61,8 @@ impl GameStatus {
|
||||
GameStatus::Starting => "Game is starting...",
|
||||
GameStatus::MustFire => "You must fire!",
|
||||
GameStatus::OpponentMustFire => "### must fire!",
|
||||
GameStatus::WonGame => "You won the game!",
|
||||
GameStatus::LostGame => "### won the game. You loose.",
|
||||
GameStatus::WonGame => "You win the game!",
|
||||
GameStatus::LostGame => "### wins the game. You loose.",
|
||||
GameStatus::RematchRequestedByOpponent => "Rematch requested by ###",
|
||||
GameStatus::RematchRequestedByPlayer => "Rematch requested by you",
|
||||
GameStatus::RematchAccepted => "Rematch accepted!",
|
||||
@@ -95,6 +96,7 @@ pub struct GameScreen {
|
||||
invite_code: Option<String>,
|
||||
status: GameStatus,
|
||||
opponent_name: Option<String>,
|
||||
game_last_update: u64,
|
||||
game: CurrentGameStatus,
|
||||
curr_shoot_position: Coordinates,
|
||||
last_opponent_fire_position: Coordinates,
|
||||
@@ -108,6 +110,7 @@ impl GameScreen {
|
||||
invite_code: None,
|
||||
status: GameStatus::Connecting,
|
||||
opponent_name: None,
|
||||
game_last_update: 0,
|
||||
game: Default::default(),
|
||||
curr_shoot_position: Coordinates::new(0, 0),
|
||||
last_opponent_fire_position: Coordinates::invalid(),
|
||||
@@ -288,11 +291,13 @@ impl GameScreen {
|
||||
|
||||
ServerMessage::OpponentMustFire { status } => {
|
||||
self.status = GameStatus::OpponentMustFire;
|
||||
self.game_last_update = time();
|
||||
self.game = status;
|
||||
}
|
||||
|
||||
ServerMessage::RequestFire { status } => {
|
||||
self.status = GameStatus::MustFire;
|
||||
self.game_last_update = time();
|
||||
self.game = status;
|
||||
}
|
||||
|
||||
@@ -303,11 +308,13 @@ impl GameScreen {
|
||||
}
|
||||
|
||||
ServerMessage::LostGame { status } => {
|
||||
self.game_last_update = time();
|
||||
self.game = status;
|
||||
self.status = GameStatus::LostGame;
|
||||
}
|
||||
|
||||
ServerMessage::WonGame { status } => {
|
||||
self.game_last_update = time();
|
||||
self.game = status;
|
||||
self.status = GameStatus::WonGame;
|
||||
}
|
||||
@@ -476,6 +483,17 @@ impl GameScreen {
|
||||
return HashMap::default();
|
||||
}
|
||||
|
||||
// Add timeout (if required)
|
||||
let mut timeout_str = String::new();
|
||||
if self.status == GameStatus::MustFire || self.status == GameStatus::OpponentMustFire {
|
||||
if let Some(remaining) = self.game.remaining_time_for_strike {
|
||||
let timeout = self.game_last_update + remaining;
|
||||
if time() < timeout {
|
||||
timeout_str = format!(" {} seconds left", timeout - time());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Draw main ui (default play UI)
|
||||
let player_map = self
|
||||
.player_map(&self.game.your_map, false)
|
||||
@@ -519,8 +537,10 @@ impl GameScreen {
|
||||
|
||||
let buttons_width = buttons.iter().fold(0, |a, b| a + b.estimated_size().0 + 4);
|
||||
|
||||
let max_width = max(maps_width, status_text.len() as u16).max(buttons_width);
|
||||
let total_height = 3 + maps_height + 3;
|
||||
let max_width = max(maps_width, status_text.len() as u16)
|
||||
.max(buttons_width)
|
||||
.max(timeout_str.len() as u16);
|
||||
let total_height = 3 + 1 + maps_height + 3;
|
||||
|
||||
// Check if frame is too small
|
||||
if max_width > f.size().width || total_height > f.size().height {
|
||||
@@ -531,7 +551,8 @@ impl GameScreen {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([
|
||||
Constraint::Length(3),
|
||||
Constraint::Length(2),
|
||||
Constraint::Length(2),
|
||||
Constraint::Length(maps_height),
|
||||
Constraint::Length(3),
|
||||
])
|
||||
@@ -541,6 +562,10 @@ impl GameScreen {
|
||||
let paragraph = Paragraph::new(status_text.as_str());
|
||||
f.render_widget(paragraph, centered_text(&status_text, &chunks[0]));
|
||||
|
||||
// Render timeout
|
||||
let paragraph = Paragraph::new(timeout_str.as_str());
|
||||
f.render_widget(paragraph, centered_text(&timeout_str, &chunks[1]));
|
||||
|
||||
// Render maps
|
||||
if show_both_maps {
|
||||
let maps_chunks = Layout::default()
|
||||
@@ -550,16 +575,16 @@ impl GameScreen {
|
||||
Constraint::Length(3),
|
||||
Constraint::Length(opponent_map_size.0),
|
||||
])
|
||||
.split(chunks[1]);
|
||||
.split(chunks[2]);
|
||||
|
||||
f.render_widget(player_map, maps_chunks[0]);
|
||||
f.render_widget(opponent_map, maps_chunks[2]);
|
||||
} else {
|
||||
// Render a single map
|
||||
if self.can_fire() {
|
||||
f.render_widget(opponent_map, chunks[1]);
|
||||
f.render_widget(opponent_map, chunks[2]);
|
||||
} else {
|
||||
f.render_widget(player_map, chunks[1]);
|
||||
f.render_widget(player_map, chunks[2]);
|
||||
drop(opponent_map);
|
||||
}
|
||||
}
|
||||
@@ -573,7 +598,7 @@ impl GameScreen {
|
||||
.map(|_| Constraint::Percentage(100 / buttons.len() as u16))
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
.split(chunks[2]);
|
||||
.split(chunks[3]);
|
||||
|
||||
for (idx, b) in buttons.into_iter().enumerate() {
|
||||
let target = centered_rect_size(
|
||||
|
@@ -24,3 +24,6 @@ pub const INVITE_CODE_LENGTH: usize = 5;
|
||||
|
||||
pub const MIN_PLAYER_NAME_LENGTH: usize = 1;
|
||||
pub const MAX_PLAYER_NAME_LENGTH: usize = 10;
|
||||
|
||||
pub const MIN_STRIKE_TIMEOUT: u64 = 5;
|
||||
pub const MAX_STRIKE_TIMEOUT: u64 = 90;
|
||||
|
@@ -489,6 +489,7 @@ mod test {
|
||||
boats_str: "1,1".to_string(),
|
||||
boats_can_touch: false,
|
||||
player_continue_on_hit: false,
|
||||
strike_timeout: None,
|
||||
bot_type: BotType::Random,
|
||||
};
|
||||
|
||||
|
@@ -78,6 +78,7 @@ impl PrintableMap for PrintableCurrentGameMapStatus {
|
||||
|
||||
#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, Default)]
|
||||
pub struct CurrentGameStatus {
|
||||
pub remaining_time_for_strike: Option<u64>,
|
||||
pub rules: GameRules,
|
||||
pub your_map: CurrentGameMapStatus,
|
||||
pub opponent_map: CurrentGameMapStatus,
|
||||
|
@@ -1,6 +1,6 @@
|
||||
use crate::consts::*;
|
||||
use crate::data::{BotType, PlayConfiguration};
|
||||
use serde_with::{serde_as, DisplayFromStr};
|
||||
use serde_with::{serde_as, DisplayFromStr, NoneAsEmptyString};
|
||||
|
||||
#[serde_as]
|
||||
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, Eq, PartialEq)]
|
||||
@@ -15,6 +15,8 @@ pub struct GameRules {
|
||||
pub boats_can_touch: bool,
|
||||
#[serde_as(as = "DisplayFromStr")]
|
||||
pub player_continue_on_hit: bool,
|
||||
#[serde_as(as = "NoneAsEmptyString")]
|
||||
pub strike_timeout: Option<u64>,
|
||||
pub bot_type: BotType,
|
||||
}
|
||||
|
||||
@@ -36,6 +38,7 @@ impl GameRules {
|
||||
.join(","),
|
||||
boats_can_touch: MULTI_PLAYER_BOATS_CAN_TOUCH,
|
||||
player_continue_on_hit: MULTI_PLAYER_PLAYER_CAN_CONTINUE_AFTER_HIT,
|
||||
strike_timeout: Some(30),
|
||||
bot_type: BotType::Smart,
|
||||
}
|
||||
}
|
||||
@@ -50,6 +53,11 @@ impl GameRules {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_strike_timeout(mut self, timeout: u64) -> Self {
|
||||
self.strike_timeout = Some(timeout);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the list of boats for this configuration
|
||||
pub fn set_boats_list(&mut self, boats: &[usize]) {
|
||||
self.boats_str = boats
|
||||
@@ -121,6 +129,16 @@ impl GameRules {
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(timeout) = self.strike_timeout {
|
||||
if timeout < config.min_strike_timeout {
|
||||
errors.push("Strike timeout is too short!");
|
||||
}
|
||||
|
||||
if timeout > config.max_strike_timeout {
|
||||
errors.push("Strike timeout is too long!");
|
||||
}
|
||||
}
|
||||
|
||||
errors
|
||||
}
|
||||
|
||||
|
@@ -59,6 +59,8 @@ pub struct PlayConfiguration {
|
||||
pub ordinate_alphabet: &'static str,
|
||||
pub min_player_name_len: usize,
|
||||
pub max_player_name_len: usize,
|
||||
pub min_strike_timeout: u64,
|
||||
pub max_strike_timeout: u64,
|
||||
}
|
||||
|
||||
impl Default for PlayConfiguration {
|
||||
@@ -76,6 +78,8 @@ impl Default for PlayConfiguration {
|
||||
ordinate_alphabet: ALPHABET,
|
||||
min_player_name_len: MIN_PLAYER_NAME_LENGTH,
|
||||
max_player_name_len: MAX_PLAYER_NAME_LENGTH,
|
||||
min_strike_timeout: MIN_STRIKE_TIMEOUT,
|
||||
max_strike_timeout: MAX_STRIKE_TIMEOUT,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,4 +1,5 @@
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use actix::prelude::*;
|
||||
use actix::{Actor, Context, Handler};
|
||||
@@ -6,6 +7,7 @@ use uuid::Uuid;
|
||||
|
||||
use crate::bot_player::BotPlayer;
|
||||
use crate::data::*;
|
||||
use crate::utils::time_utils::time;
|
||||
|
||||
pub trait Player {
|
||||
fn get_name(&self) -> &str;
|
||||
@@ -51,6 +53,9 @@ pub trait Player {
|
||||
fn opponent_replaced_by_bot(&self);
|
||||
}
|
||||
|
||||
/// How often strike timeout controller is run
|
||||
const STRIKE_TIMEOUT_CONTROL: Duration = Duration::from_secs(1);
|
||||
|
||||
fn opponent(index: usize) -> usize {
|
||||
match index {
|
||||
0 => 1,
|
||||
@@ -85,6 +90,7 @@ pub struct Game {
|
||||
map_0: Option<GameMap>,
|
||||
map_1: Option<GameMap>,
|
||||
turn: usize,
|
||||
curr_strike_request_started: u64,
|
||||
}
|
||||
|
||||
impl Game {
|
||||
@@ -96,6 +102,7 @@ impl Game {
|
||||
map_0: None,
|
||||
map_1: None,
|
||||
turn: 0,
|
||||
curr_strike_request_started: 0,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,10 +137,14 @@ impl Game {
|
||||
self.turn
|
||||
);
|
||||
|
||||
self.request_fire();
|
||||
self.request_fire(true);
|
||||
}
|
||||
|
||||
fn request_fire(&self) {
|
||||
fn request_fire(&mut self, reset_counter: bool) {
|
||||
if reset_counter {
|
||||
self.curr_strike_request_started = time();
|
||||
}
|
||||
|
||||
self.players[self.turn].request_fire(self.get_game_status_for_player(self.turn));
|
||||
self.players[opponent(self.turn)]
|
||||
.opponent_must_fire(self.get_game_status_for_player(opponent(self.turn)));
|
||||
@@ -148,6 +159,27 @@ impl Game {
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// Replace user for a fire in case of timeout
|
||||
fn force_fire_in_case_of_timeout(&mut self) {
|
||||
if self.status != GameStatus::Started || self.rules.strike_timeout.is_none() {
|
||||
return;
|
||||
}
|
||||
|
||||
let timeout = self.rules.strike_timeout.unwrap_or_default();
|
||||
|
||||
if time() <= self.curr_strike_request_started + timeout {
|
||||
return;
|
||||
}
|
||||
|
||||
// Determine target of fire
|
||||
let target = self
|
||||
.get_game_status_for_player(self.turn)
|
||||
.find_fire_coordinates_for_bot_type(self.rules.bot_type);
|
||||
|
||||
// Fire as player
|
||||
self.handle_fire(target);
|
||||
}
|
||||
|
||||
fn player_map_mut(&mut self, id: usize) -> &mut GameMap {
|
||||
match id {
|
||||
0 => self.map_0.as_mut(),
|
||||
@@ -166,7 +198,7 @@ impl Game {
|
||||
// Easiest case : player missed his fire
|
||||
if result == FireResult::Missed {
|
||||
self.turn = opponent(self.turn);
|
||||
self.request_fire();
|
||||
self.request_fire(true);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -188,7 +220,9 @@ impl Game {
|
||||
self.turn = opponent(self.turn);
|
||||
}
|
||||
|
||||
self.request_fire();
|
||||
self.request_fire(
|
||||
result != FireResult::AlreadyTargetedPosition && result != FireResult::Rejected,
|
||||
);
|
||||
}
|
||||
|
||||
fn handle_request_rematch(&mut self, player_id: Uuid) {
|
||||
@@ -223,6 +257,9 @@ impl Game {
|
||||
/// Get current game status for a specific player
|
||||
fn get_game_status_for_player(&self, id: usize) -> CurrentGameStatus {
|
||||
CurrentGameStatus {
|
||||
remaining_time_for_strike: self.rules.strike_timeout.map(|v| {
|
||||
((self.curr_strike_request_started + v) as i64 - time() as i64).max(0) as u64
|
||||
}),
|
||||
rules: self.rules.clone(),
|
||||
your_map: self.player_map(id).current_map_status(false),
|
||||
opponent_map: self
|
||||
@@ -234,6 +271,14 @@ impl Game {
|
||||
|
||||
impl Actor for Game {
|
||||
type Context = Context<Self>;
|
||||
|
||||
fn started(&mut self, ctx: &mut Self::Context) {
|
||||
if self.rules.strike_timeout.is_some() {
|
||||
ctx.run_interval(STRIKE_TIMEOUT_CONTROL, |act, _ctx| {
|
||||
act.force_fire_in_case_of_timeout();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Message)]
|
||||
@@ -387,7 +432,7 @@ impl Handler<PlayerLeftGame> for Game {
|
||||
|
||||
// Re-do current action
|
||||
if self.status == GameStatus::Started {
|
||||
self.request_fire();
|
||||
self.request_fire(true);
|
||||
} else if self.status == GameStatus::WaitingForBoatsDisposition {
|
||||
self.players[offline_player].query_boats_layout(&self.rules);
|
||||
}
|
||||
|
@@ -152,6 +152,7 @@ impl Actor for HumanPlayerWS {
|
||||
fn started(&mut self, ctx: &mut Self::Context) {
|
||||
// Check player name length
|
||||
if self.name.len() < MIN_PLAYER_NAME_LENGTH || self.name.len() > MAX_PLAYER_NAME_LENGTH {
|
||||
log::error!("Close connection due to invalid user name!");
|
||||
ctx.stop();
|
||||
return;
|
||||
}
|
||||
|
@@ -163,6 +163,7 @@ pub async fn start_server(args: Args) -> std::io::Result<()> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::data::GameRules;
|
||||
use crate::server::BotPlayQuery;
|
||||
|
||||
#[test]
|
||||
@@ -177,4 +178,20 @@ mod test {
|
||||
|
||||
assert_eq!(query, des)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn simple_bot_request_serialize_deserialize_no_timeout() {
|
||||
let query = BotPlayQuery {
|
||||
rules: GameRules {
|
||||
strike_timeout: None,
|
||||
..Default::default()
|
||||
},
|
||||
player_name: "Player".to_string(),
|
||||
};
|
||||
|
||||
let string = serde_urlencoded::to_string(&query).unwrap();
|
||||
let des = serde_urlencoded::from_str(&string).unwrap();
|
||||
|
||||
assert_eq!(query, des)
|
||||
}
|
||||
}
|
||||
|
@@ -39,7 +39,7 @@ pub struct BotClient {
|
||||
requested_rules: GameRules,
|
||||
layout: Option<BoatsLayout>,
|
||||
number_plays: usize,
|
||||
server_msg_callback: Option<Box<dyn FnMut(&ServerMessage)>>,
|
||||
server_msg_callback: Option<Box<dyn FnMut(&mut ServerMessage)>>,
|
||||
play_as_bot_type: BotType,
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@ impl BotClient {
|
||||
|
||||
pub fn with_server_msg_callback<F>(mut self, cb: F) -> Self
|
||||
where
|
||||
F: FnMut(&ServerMessage) + 'static,
|
||||
F: FnMut(&mut ServerMessage) + 'static,
|
||||
{
|
||||
self.server_msg_callback = Some(Box::new(cb));
|
||||
self
|
||||
@@ -152,7 +152,7 @@ impl BotClient {
|
||||
};
|
||||
|
||||
while let Some(chunk) = socket.next().await {
|
||||
let message = match chunk? {
|
||||
let mut message = match chunk? {
|
||||
Message::Text(message) => {
|
||||
log::trace!("TEXT message from server: {}", message);
|
||||
|
||||
@@ -182,7 +182,7 @@ impl BotClient {
|
||||
};
|
||||
|
||||
if let Some(cb) = &mut self.server_msg_callback {
|
||||
(cb)(&message)
|
||||
(cb)(&mut message)
|
||||
}
|
||||
|
||||
match message {
|
||||
|
@@ -9,7 +9,7 @@ use crate::test::play_utils::check_no_replay_on_hit;
|
||||
use crate::test::{bot_client, TestPort};
|
||||
use crate::utils::network_utils::wait_for_port;
|
||||
|
||||
fn check_strikes_are_linear(msg: &ServerMessage) {
|
||||
fn check_strikes_are_linear(msg: &mut ServerMessage) {
|
||||
if let ServerMessage::RequestFire { status } = msg {
|
||||
let mut in_fire_location = true;
|
||||
for y in 0..status.rules.map_height {
|
||||
|
@@ -1,13 +1,16 @@
|
||||
use tokio::task;
|
||||
|
||||
use crate::args::Args;
|
||||
use crate::consts::MIN_STRIKE_TIMEOUT;
|
||||
use crate::data::{BoatsLayout, GameRules};
|
||||
use crate::human_player_ws::ServerMessage;
|
||||
use crate::server::start_server;
|
||||
use crate::test::bot_client;
|
||||
use crate::test::bot_client::ClientEndResult;
|
||||
use crate::test::play_utils::check_no_replay_on_hit;
|
||||
use crate::test::TestPort;
|
||||
use crate::utils::network_utils::wait_for_port;
|
||||
use crate::utils::time_utils::time;
|
||||
|
||||
#[tokio::test]
|
||||
async fn invalid_port() {
|
||||
@@ -201,3 +204,37 @@ async fn full_game_no_replay_on_hit() {
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn check_fire_time_out() {
|
||||
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::RandomCheckTimeout)));
|
||||
wait_for_port(TestPort::RandomCheckTimeout.port()).await;
|
||||
|
||||
let start = time();
|
||||
|
||||
let mut did_skip_one = false;
|
||||
let res = bot_client::BotClient::new(TestPort::RandomCheckTimeout.as_url())
|
||||
.with_rules(
|
||||
GameRules::random_players_rules().with_strike_timeout(MIN_STRIKE_TIMEOUT),
|
||||
)
|
||||
.with_server_msg_callback(move |msg| {
|
||||
if matches!(msg, ServerMessage::RequestFire { .. }) && !did_skip_one {
|
||||
*msg = ServerMessage::OpponentReplacedByBot;
|
||||
did_skip_one = true;
|
||||
}
|
||||
})
|
||||
.run_client()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(matches!(res, ClientEndResult::Finished { .. }));
|
||||
|
||||
assert!(time() - start >= MIN_STRIKE_TIMEOUT);
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
@@ -10,6 +10,7 @@ enum TestPort {
|
||||
RandomBotInvalidBoatsLayoutLenOfABoat,
|
||||
RandomBotFullGameMultipleRematch,
|
||||
RandomBotNoReplayOnHit,
|
||||
RandomCheckTimeout,
|
||||
LinearBotFullGame,
|
||||
LinearBotNoReplayOnHit,
|
||||
IntermediateBotFullGame,
|
||||
|
@@ -1,7 +1,7 @@
|
||||
use crate::human_player_ws::ServerMessage;
|
||||
|
||||
/// Make sure player can not replay after successful hit
|
||||
pub fn check_no_replay_on_hit(msg: &ServerMessage) {
|
||||
pub fn check_no_replay_on_hit(msg: &mut ServerMessage) {
|
||||
if let ServerMessage::OpponentMustFire { status } | ServerMessage::RequestFire { status } = msg
|
||||
{
|
||||
let diff =
|
||||
|
@@ -1,12 +1,4 @@
|
||||
use std::error::Error;
|
||||
use std::fmt::Display;
|
||||
use std::io::ErrorKind;
|
||||
|
||||
pub mod network_utils;
|
||||
pub mod res_utils;
|
||||
pub mod string_utils;
|
||||
|
||||
pub type Res<E = ()> = Result<E, Box<dyn Error>>;
|
||||
|
||||
pub fn boxed_error<D: Display>(msg: D) -> Box<dyn Error> {
|
||||
Box::new(std::io::Error::new(ErrorKind::Other, msg.to_string()))
|
||||
}
|
||||
pub mod time_utils;
|
||||
|
9
rust/sea_battle_backend/src/utils/res_utils.rs
Normal file
9
rust/sea_battle_backend/src/utils/res_utils.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
use std::error::Error;
|
||||
use std::fmt::Display;
|
||||
use std::io::ErrorKind;
|
||||
|
||||
pub type Res<E = ()> = Result<E, Box<dyn Error>>;
|
||||
|
||||
pub fn boxed_error<D: Display>(msg: D) -> Box<dyn Error> {
|
||||
Box::new(std::io::Error::new(ErrorKind::Other, msg.to_string()))
|
||||
}
|
9
rust/sea_battle_backend/src/utils/time_utils.rs
Normal file
9
rust/sea_battle_backend/src/utils/time_utils.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
/// Get the current time since epoch
|
||||
pub fn time() -> u64 {
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs()
|
||||
}
|
Reference in New Issue
Block a user