Compare commits
	
		
			159 Commits
		
	
	
		
			0.1.0
			...
			5a3f42ffae
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 5a3f42ffae | |||
| 09b40c8138 | |||
| 242f8e32df | |||
| 89a5f104c2 | |||
| dcb3f9113f | |||
| f3ba509cc1 | |||
| 221f43ea46 | |||
| 56d824504d | |||
| 7cda8c9dc1 | |||
| cfedebd2f6 | |||
| d768650f23 | |||
| 21d8e0c01a | |||
| 3dc46fc862 | |||
| d6c6d57fee | |||
| e9e272c19c | |||
| 44d5437c5f | |||
| df09e7ff8d | |||
| 92a4f8b2f7 | |||
| 440a81c307 | |||
| c476171e7a | |||
| 83c146e788 | |||
| 79bfeb2597 | |||
| 52f16f5c33 | |||
| d75a5d7d5b | |||
| 473e6c0d1e | |||
| 9a93a29804 | |||
| dad50314b0 | |||
| b51aa8b7cb | |||
| 5a2fd31fa0 | |||
| efcf33c539 | |||
| 652a6d162b | |||
| 2ab3b5e55d | |||
| f52dc84b45 | |||
| 567473a223 | |||
| e95830f644 | |||
| 5d2b3e55ef | |||
| c1f5f5f624 | |||
| c5b549244f | |||
| 7a93a1e3c6 | |||
| 1fff258248 | |||
| 0494847f2e | |||
| c9dd2fce12 | |||
| a2b629d218 | |||
| 5379b84470 | |||
| ef8772be97 | |||
| 007dae6fae | |||
| 29012b0f32 | |||
| bf4aaada69 | |||
| c58c782219 | |||
| 90ae4a5193 | |||
| b9085771a6 | |||
| e7629f50e3 | |||
| 00603e4386 | |||
| 09eaabe43c | |||
| 09b2058934 | |||
| c6143b3bec | |||
| f48c34c234 | |||
| f1a179e12d | |||
| 067332b116 | |||
| 1257a637b1 | |||
| e0822e3585 | |||
| 42bea6bba4 | |||
| 50e2db5256 | |||
| 7ef779d804 | |||
| 13bb37fa51 | |||
| 144848563b | |||
| 99bcf6e5ac | |||
| daafca93a9 | |||
| 04c4813cee | |||
| 764f6f5112 | |||
| 0cfd2fc3f2 | |||
| 46be9732de | |||
| 6b4bd56684 | |||
| 9a19272ba6 | |||
| c7f0be52cd | |||
| 37f7c20e36 | |||
| 75d55b4a23 | |||
| 0c394f301a | |||
| c87a549a8a | |||
| 98cc8884af | |||
| 03c842017c | |||
| d87e5d816c | |||
| eaf8d1c24e | |||
| c4aa47f199 | |||
| 9a623633cb | |||
| d1a28a0802 | |||
| f6aa9977ba | |||
| 7dd5464391 | |||
| 9f58d98a69 | |||
| 841c6709cc | |||
| e63566c6a9 | |||
| ac6f8987f4 | |||
| 356fa75604 | |||
| 563f33971f | |||
| 8f0db5cbe6 | |||
| 5442536768 | |||
| f6b0962ec3 | |||
| 2909bbc1c9 | |||
| fd1025c4a8 | |||
| ccadddaf15 | |||
| 2a0987defd | |||
| cb6ca75515 | |||
| 964c90e0d8 | |||
| 81a05b8f66 | |||
| d2801c6b50 | |||
| 1328baaba7 | |||
| d06585ad71 | |||
| 3ab8201c13 | |||
| c0232f602e | |||
| 5f2b7654be | |||
| 08a449352b | |||
| 50fbbe50bf | |||
| f4356c656b | |||
| b12657ef3a | |||
| ad868a7961 | |||
| 221d1dfa13 | |||
| 33ed27b892 | |||
| 9e6df3c78d | |||
| 6faf38003f | |||
| cee12d89f6 | |||
| 887dd849c6 | |||
| 2c8686e9d2 | |||
| 7d92555a85 | |||
| 0a7e9b9661 | |||
| 3af6fd730e | |||
| b532324654 | |||
| de218d2ba1 | |||
| bb902cda9e | |||
| 77d5b18f79 | |||
| 7125076f1f | |||
| 4ed1c9c200 | |||
| fa4b0bcdc2 | |||
| 40bff4f8e4 | |||
| 283ea7d422 | |||
| c746313c04 | |||
| 86d45ad992 | |||
| 5ba2e78fd0 | |||
| 1265d7f099 | |||
| b713117a70 | |||
| a001017821 | |||
| 5acbe069c2 | |||
| 6defd8edd2 | |||
| ef598dbff4 | |||
| 2abe1c95d0 | |||
| f7bb0ddda2 | |||
| d1114f0295 | |||
| f94a51027b | |||
| f5f2efcfde | |||
| eb9999b85b | |||
| df1d678ab9 | |||
| e88d64ff63 | |||
| 3ca6c43c9a | |||
| d4223be8b4 | |||
| 83d0780954 | |||
| 6be3eae863 | |||
| c763a24ca9 | |||
| 10c099e03b | |||
| eea2ecbf63 | |||
| 915426849b | 
@@ -1,4 +1,10 @@
 | 
				
			|||||||
{
 | 
					{
 | 
				
			||||||
  "$schema": "https://docs.renovatebot.com/renovate-schema.json",
 | 
					  "$schema": "https://docs.renovatebot.com/renovate-schema.json",
 | 
				
			||||||
  "ignorePaths": ["**/flutter/**", "**/react/**"]
 | 
					  "ignorePaths": ["**/flutter/**", "**/react/**"],
 | 
				
			||||||
 | 
					  "packageRules": [
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      "matchUpdateTypes": ["minor", "patch"],
 | 
				
			||||||
 | 
					      "automerge": true
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  ]
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										1305
									
								
								rust/Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1305
									
								
								rust/Cargo.lock
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -1,6 +1,6 @@
 | 
				
			|||||||
[package]
 | 
					[package]
 | 
				
			||||||
name = "sea_battle_backend"
 | 
					name = "sea_battle_backend"
 | 
				
			||||||
version = "0.1.0"
 | 
					version = "0.2.0"
 | 
				
			||||||
edition = "2021"
 | 
					edition = "2021"
 | 
				
			||||||
license = "GPL-2.0-or-later"
 | 
					license = "GPL-2.0-or-later"
 | 
				
			||||||
description = "A Sea Battle game backend server"
 | 
					description = "A Sea Battle game backend server"
 | 
				
			||||||
@@ -12,23 +12,24 @@ categories = [ "games" ]
 | 
				
			|||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 | 
					# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[dependencies]
 | 
					[dependencies]
 | 
				
			||||||
clap = { version = "4.0.15", features = ["derive"] }
 | 
					clap = { version = "4.4.1", features = ["derive"] }
 | 
				
			||||||
log = "0.4.17"
 | 
					log = "0.4.20"
 | 
				
			||||||
env_logger = "0.9.0"
 | 
					env_logger = "0.10.1"
 | 
				
			||||||
serde = { version = "1.0.145", features = ["derive"] }
 | 
					serde = { version = "1.0.186", features = ["derive"] }
 | 
				
			||||||
serde_json = "1.0.85"
 | 
					serde_json = "1.0.105"
 | 
				
			||||||
actix-web = "4.1.0"
 | 
					actix-web = "4.4.0"
 | 
				
			||||||
actix-cors = "0.6.2"
 | 
					actix-cors = "0.6.2"
 | 
				
			||||||
actix = "0.13.0"
 | 
					actix = "0.13.1"
 | 
				
			||||||
actix-web-actors = "4.1.0"
 | 
					actix-web-actors = "4.1.0"
 | 
				
			||||||
actix-rt = "2.7.0"
 | 
					actix-rt = "2.9.0"
 | 
				
			||||||
uuid = { version = "1.1.2", features = ["v4"] }
 | 
					uuid = { version = "1.6.1", features = ["v4"] }
 | 
				
			||||||
rand = "0.8.5"
 | 
					rand = "0.8.5"
 | 
				
			||||||
serde_with = "2.0.1"
 | 
					serde_with = "3.4.0"
 | 
				
			||||||
tokio = { version = "1", features = ["full"] }
 | 
					tokio = { version = "1", features = ["full"] }
 | 
				
			||||||
 | 
					semver = "1.0.20"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[dev-dependencies]
 | 
					[dev-dependencies]
 | 
				
			||||||
#reqwest = { version = "0.11.11", default-features = false, features = ["json", "rustls-tls"] }
 | 
					#reqwest = { version = "0.11.11", default-features = false, features = ["json", "rustls-tls"] }
 | 
				
			||||||
tokio-tungstenite = "0.17.2"
 | 
					tokio-tungstenite = "0.20.1"
 | 
				
			||||||
serde_urlencoded = "0.7.1"
 | 
					serde_urlencoded = "0.7.1"
 | 
				
			||||||
futures = "0.3.23"
 | 
					futures = "0.3.29"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -12,3 +12,14 @@ pub struct Args {
 | 
				
			|||||||
    #[clap(short, long, value_parser)]
 | 
					    #[clap(short, long, value_parser)]
 | 
				
			||||||
    pub cors: Option<String>,
 | 
					    pub cors: Option<String>,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[cfg(test)]
 | 
				
			||||||
 | 
					mod test {
 | 
				
			||||||
 | 
					    use crate::args::Args;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    #[test]
 | 
				
			||||||
 | 
					    fn verify_cli() {
 | 
				
			||||||
 | 
					        use clap::CommandFactory;
 | 
				
			||||||
 | 
					        Args::command().debug_assert()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +1,8 @@
 | 
				
			|||||||
//! # Project constants
 | 
					//! # Project constants
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub const MIN_REQUIRED_VERSION: &str = "0.1.0";
 | 
				
			||||||
 | 
					pub const CURRENT_VERSION: &str = env!("CARGO_PKG_VERSION");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub const MIN_BOATS_NUMBER: usize = 1;
 | 
					pub const MIN_BOATS_NUMBER: usize = 1;
 | 
				
			||||||
pub const MAX_BOATS_NUMBER: usize = 10;
 | 
					pub const MAX_BOATS_NUMBER: usize = 10;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -361,7 +361,7 @@ impl BoatsLayout {
 | 
				
			|||||||
mod test {
 | 
					mod test {
 | 
				
			||||||
    use crate::data::boats_layout::{BoatDirection, BoatPosition, BoatsLayout, Coordinates};
 | 
					    use crate::data::boats_layout::{BoatDirection, BoatPosition, BoatsLayout, Coordinates};
 | 
				
			||||||
    use crate::data::game_map::GameMap;
 | 
					    use crate::data::game_map::GameMap;
 | 
				
			||||||
    use crate::data::{BotType, GameRules, PlayConfiguration, PrintableMap};
 | 
					    use crate::data::*;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    #[test]
 | 
					    #[test]
 | 
				
			||||||
    fn dist_coordinates_eq() {
 | 
					    fn dist_coordinates_eq() {
 | 
				
			||||||
@@ -488,9 +488,7 @@ mod test {
 | 
				
			|||||||
            map_height: 5,
 | 
					            map_height: 5,
 | 
				
			||||||
            boats_str: "1,1".to_string(),
 | 
					            boats_str: "1,1".to_string(),
 | 
				
			||||||
            boats_can_touch: false,
 | 
					            boats_can_touch: false,
 | 
				
			||||||
            player_continue_on_hit: false,
 | 
					            ..Default::default()
 | 
				
			||||||
            strike_timeout: None,
 | 
					 | 
				
			||||||
            bot_type: BotType::Random,
 | 
					 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        let mut boats = BoatsLayout(vec![
 | 
					        let mut boats = BoatsLayout(vec![
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,6 +4,7 @@ pub use game_map::*;
 | 
				
			|||||||
pub use game_rules::*;
 | 
					pub use game_rules::*;
 | 
				
			||||||
pub use play_config::*;
 | 
					pub use play_config::*;
 | 
				
			||||||
pub use printable_map::*;
 | 
					pub use printable_map::*;
 | 
				
			||||||
 | 
					pub use version::*;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
mod boats_layout;
 | 
					mod boats_layout;
 | 
				
			||||||
mod current_game_status;
 | 
					mod current_game_status;
 | 
				
			||||||
@@ -11,3 +12,4 @@ mod game_map;
 | 
				
			|||||||
mod game_rules;
 | 
					mod game_rules;
 | 
				
			||||||
mod play_config;
 | 
					mod play_config;
 | 
				
			||||||
mod printable_map;
 | 
					mod printable_map;
 | 
				
			||||||
 | 
					mod version;
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										32
									
								
								rust/sea_battle_backend/src/data/version.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								rust/sea_battle_backend/src/data/version.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,32 @@
 | 
				
			|||||||
 | 
					//! # Version Info
 | 
				
			||||||
 | 
					//!
 | 
				
			||||||
 | 
					//! Contains server version requirements information
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::consts;
 | 
				
			||||||
 | 
					use crate::utils::res_utils::Res;
 | 
				
			||||||
 | 
					use semver::Version;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(serde::Serialize, serde::Deserialize)]
 | 
				
			||||||
 | 
					pub struct VersionInfo {
 | 
				
			||||||
 | 
					    current: String,
 | 
				
			||||||
 | 
					    min_required: String,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl VersionInfo {
 | 
				
			||||||
 | 
					    pub fn load_static() -> Self {
 | 
				
			||||||
 | 
					        Self {
 | 
				
			||||||
 | 
					            current: consts::CURRENT_VERSION.to_string(),
 | 
				
			||||||
 | 
					            min_required: consts::MIN_REQUIRED_VERSION.to_string(),
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Check if builtin version is compatible with a remote version or not
 | 
				
			||||||
 | 
					    pub fn is_compatible_with_static_version(&self) -> Res<bool> {
 | 
				
			||||||
 | 
					        let static_version = Self::load_static();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let local_current = Version::parse(&static_version.current)?;
 | 
				
			||||||
 | 
					        let min_required = Version::parse(&self.min_required)?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Ok(min_required <= local_current)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -4,7 +4,7 @@ use actix_web::{web, App, HttpRequest, HttpResponse, HttpServer, Responder};
 | 
				
			|||||||
use actix_web_actors::ws;
 | 
					use actix_web_actors::ws;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::args::Args;
 | 
					use crate::args::Args;
 | 
				
			||||||
use crate::data::{GameRules, PlayConfiguration};
 | 
					use crate::data::{BoatsLayout, GameRules, PlayConfiguration, VersionInfo};
 | 
				
			||||||
use crate::dispatcher_actor::DispatcherActor;
 | 
					use crate::dispatcher_actor::DispatcherActor;
 | 
				
			||||||
use crate::human_player_ws::{HumanPlayerWS, StartMode};
 | 
					use crate::human_player_ws::{HumanPlayerWS, StartMode};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -18,11 +18,45 @@ async fn not_found() -> impl Responder {
 | 
				
			|||||||
    HttpResponse::NotFound().json("You missed your strike lol")
 | 
					    HttpResponse::NotFound().json("You missed your strike lol")
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// Get bot configuration
 | 
					/// Get version information
 | 
				
			||||||
 | 
					async fn version_information() -> impl Responder {
 | 
				
			||||||
 | 
					    HttpResponse::Ok().json(VersionInfo::load_static())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Get game configuration
 | 
				
			||||||
async fn game_configuration() -> impl Responder {
 | 
					async fn game_configuration() -> impl Responder {
 | 
				
			||||||
    HttpResponse::Ok().json(PlayConfiguration::default())
 | 
					    HttpResponse::Ok().json(PlayConfiguration::default())
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Get default game rules
 | 
				
			||||||
 | 
					async fn default_game_rules() -> impl Responder {
 | 
				
			||||||
 | 
					    HttpResponse::Ok().json(GameRules::random_players_rules())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Validate game rules
 | 
				
			||||||
 | 
					async fn validate_game_rules(rules: web::Json<GameRules>) -> impl Responder {
 | 
				
			||||||
 | 
					    HttpResponse::Ok().json(rules.get_errors())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Generate random boats layout
 | 
				
			||||||
 | 
					async fn gen_boats_layout(rules: web::Json<GameRules>) -> impl Responder {
 | 
				
			||||||
 | 
					    let errors = rules.get_errors();
 | 
				
			||||||
 | 
					    if !errors.is_empty() {
 | 
				
			||||||
 | 
					        return HttpResponse::BadRequest().json(errors);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    match BoatsLayout::gen_random_for_rules(&rules) {
 | 
				
			||||||
 | 
					        Ok(l) => HttpResponse::Ok().json(l),
 | 
				
			||||||
 | 
					        Err(e) => {
 | 
				
			||||||
 | 
					            log::error!(
 | 
				
			||||||
 | 
					                "Failed to generate boats layout for valid game rules: {} ! / Rules: {:?}",
 | 
				
			||||||
 | 
					                e,
 | 
				
			||||||
 | 
					                rules
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					            HttpResponse::InternalServerError().json("Failed to generate random layout!")
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[derive(serde::Serialize, serde::Deserialize, Eq, PartialEq, Debug)]
 | 
					#[derive(serde::Serialize, serde::Deserialize, Eq, PartialEq, Debug)]
 | 
				
			||||||
pub struct BotPlayQuery {
 | 
					pub struct BotPlayQuery {
 | 
				
			||||||
    #[serde(flatten)]
 | 
					    #[serde(flatten)]
 | 
				
			||||||
@@ -130,6 +164,7 @@ async fn start_random(
 | 
				
			|||||||
    log::info!("New random play");
 | 
					    log::info!("New random play");
 | 
				
			||||||
    resp
 | 
					    resp
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub async fn start_server(args: Args) -> std::io::Result<()> {
 | 
					pub async fn start_server(args: Args) -> std::io::Result<()> {
 | 
				
			||||||
    log::info!("Start to listen on {}", args.listen_address);
 | 
					    log::info!("Start to listen on {}", args.listen_address);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -148,7 +183,11 @@ pub async fn start_server(args: Args) -> std::io::Result<()> {
 | 
				
			|||||||
        App::new()
 | 
					        App::new()
 | 
				
			||||||
            .app_data(web::Data::new(dispatcher_actor.clone()))
 | 
					            .app_data(web::Data::new(dispatcher_actor.clone()))
 | 
				
			||||||
            .wrap(cors)
 | 
					            .wrap(cors)
 | 
				
			||||||
 | 
					            .route("/version", web::get().to(version_information))
 | 
				
			||||||
            .route("/config", web::get().to(game_configuration))
 | 
					            .route("/config", web::get().to(game_configuration))
 | 
				
			||||||
 | 
					            .route("/game_rules/default", web::get().to(default_game_rules))
 | 
				
			||||||
 | 
					            .route("/game_rules/validate", web::post().to(validate_game_rules))
 | 
				
			||||||
 | 
					            .route("/generate_boats_layout", web::post().to(gen_boats_layout))
 | 
				
			||||||
            .route("/play/bot", web::get().to(start_bot_play))
 | 
					            .route("/play/bot", web::get().to(start_bot_play))
 | 
				
			||||||
            .route("/play/create_invite", web::get().to(start_create_invite))
 | 
					            .route("/play/create_invite", web::get().to(start_create_invite))
 | 
				
			||||||
            .route("/play/accept_invite", web::get().to(start_accept_invite))
 | 
					            .route("/play/accept_invite", web::get().to(start_accept_invite))
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,6 @@
 | 
				
			|||||||
[package]
 | 
					[package]
 | 
				
			||||||
name = "sea_battle_cli_player"
 | 
					name = "sea_battle_cli_player"
 | 
				
			||||||
version = "0.1.0"
 | 
					version = "0.2.0"
 | 
				
			||||||
edition = "2021"
 | 
					edition = "2021"
 | 
				
			||||||
license = "GPL-2.0-or-later"
 | 
					license = "GPL-2.0-or-later"
 | 
				
			||||||
description = "A Sea Battle game shell client"
 | 
					description = "A Sea Battle game shell client"
 | 
				
			||||||
@@ -12,22 +12,23 @@ categories = [ "games" ]
 | 
				
			|||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 | 
					# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[dependencies]
 | 
					[dependencies]
 | 
				
			||||||
sea_battle_backend = { path = "../sea_battle_backend", version = "0.1.0" }
 | 
					sea_battle_backend = { path = "../sea_battle_backend", version = "0.2.0" }
 | 
				
			||||||
clap = { version = "4.0.15", features = ["derive"] }
 | 
					clap = { version = "4.4.1", features = ["derive"] }
 | 
				
			||||||
log = "0.4.17"
 | 
					log = "0.4.20"
 | 
				
			||||||
env_logger = "0.9.0"
 | 
					env_logger = "0.10.1"
 | 
				
			||||||
tui = "0.19.0"
 | 
					tui = "0.19.0"
 | 
				
			||||||
crossterm = "0.25.0"
 | 
					crossterm = "0.27.0"
 | 
				
			||||||
lazy_static = "1.4.0"
 | 
					lazy_static = "1.4.0"
 | 
				
			||||||
tokio = "1.21.2"
 | 
					tokio = "1.34.0"
 | 
				
			||||||
num = "0.4.0"
 | 
					num = "0.4.1"
 | 
				
			||||||
num-traits = "0.2.15"
 | 
					num-traits = "0.2.17"
 | 
				
			||||||
num-derive = "0.3.3"
 | 
					num-derive = "0.4.0"
 | 
				
			||||||
textwrap = "0.15.1"
 | 
					textwrap = "0.16.0"
 | 
				
			||||||
tokio-tungstenite = { version = "0.17.2", features = ["__rustls-tls", "rustls-tls-native-roots"] }
 | 
					tokio-tungstenite = { version = "0.20.1", features = ["__rustls-tls", "rustls-tls-native-roots"] }
 | 
				
			||||||
serde_urlencoded = "0.7.1"
 | 
					serde_urlencoded = "0.7.1"
 | 
				
			||||||
futures = "0.3.23"
 | 
					futures = "0.3.29"
 | 
				
			||||||
serde_json = "1.0.85"
 | 
					serde_json = "1.0.105"
 | 
				
			||||||
hostname = "0.3.1"
 | 
					hostname = "0.3.1"
 | 
				
			||||||
rustls = "0.20.6"
 | 
					rustls = "0.21.9"
 | 
				
			||||||
hyper-rustls = { version = "0.23.0", features = ["rustls-native-certs"] }
 | 
					hyper-rustls = { version = "0.24.2", features = ["rustls-native-certs"] }
 | 
				
			||||||
 | 
					reqwest = { version = "0.11", features = ["json", "rustls-tls"], default-features = false }
 | 
				
			||||||
@@ -20,7 +20,7 @@ pub struct CliArgs {
 | 
				
			|||||||
        value_parser,
 | 
					        value_parser,
 | 
				
			||||||
        default_value = "https://seabattleapi.communiquons.org"
 | 
					        default_value = "https://seabattleapi.communiquons.org"
 | 
				
			||||||
    )]
 | 
					    )]
 | 
				
			||||||
    pub remote_server_uri: String,
 | 
					    pub remote_server: String,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /// Local server listen address
 | 
					    /// Local server listen address
 | 
				
			||||||
    #[clap(short, long, default_value = "127.0.0.1:5679")]
 | 
					    #[clap(short, long, default_value = "127.0.0.1:5679")]
 | 
				
			||||||
@@ -61,3 +61,14 @@ lazy_static::lazy_static! {
 | 
				
			|||||||
pub fn cli_args() -> &'static CliArgs {
 | 
					pub fn cli_args() -> &'static CliArgs {
 | 
				
			||||||
    &ARGS
 | 
					    &ARGS
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[cfg(test)]
 | 
				
			||||||
 | 
					mod test {
 | 
				
			||||||
 | 
					    use crate::cli_args::CliArgs;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    #[test]
 | 
				
			||||||
 | 
					    fn verify_cli() {
 | 
				
			||||||
 | 
					        use clap::CommandFactory;
 | 
				
			||||||
 | 
					        CliArgs::command().debug_assert()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,12 +3,13 @@ use crate::server;
 | 
				
			|||||||
use futures::stream::{SplitSink, SplitStream};
 | 
					use futures::stream::{SplitSink, SplitStream};
 | 
				
			||||||
use futures::{SinkExt, StreamExt};
 | 
					use futures::{SinkExt, StreamExt};
 | 
				
			||||||
use hyper_rustls::ConfigBuilderExt;
 | 
					use hyper_rustls::ConfigBuilderExt;
 | 
				
			||||||
use sea_battle_backend::data::GameRules;
 | 
					use sea_battle_backend::data::*;
 | 
				
			||||||
use sea_battle_backend::human_player_ws::{ClientMessage, ServerMessage};
 | 
					use sea_battle_backend::human_player_ws::{ClientMessage, ServerMessage};
 | 
				
			||||||
use sea_battle_backend::server::{
 | 
					use sea_battle_backend::server::{
 | 
				
			||||||
    AcceptInviteQuery, BotPlayQuery, CreateInviteQuery, PlayRandomQuery,
 | 
					    AcceptInviteQuery, BotPlayQuery, CreateInviteQuery, PlayRandomQuery,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
use sea_battle_backend::utils::res_utils::{boxed_error, Res};
 | 
					use sea_battle_backend::utils::res_utils::{boxed_error, Res};
 | 
				
			||||||
 | 
					use std::error::Error;
 | 
				
			||||||
use std::fmt::Display;
 | 
					use std::fmt::Display;
 | 
				
			||||||
use std::sync::mpsc::TryRecvError;
 | 
					use std::sync::mpsc::TryRecvError;
 | 
				
			||||||
use std::sync::{mpsc, Arc};
 | 
					use std::sync::{mpsc, Arc};
 | 
				
			||||||
@@ -18,6 +19,11 @@ use tokio_tungstenite::{MaybeTlsStream, WebSocketStream};
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
type WsStream = WebSocketStream<MaybeTlsStream<TcpStream>>;
 | 
					type WsStream = WebSocketStream<MaybeTlsStream<TcpStream>>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub enum GetRemoteVersionError {
 | 
				
			||||||
 | 
					    ConnectionFailed,
 | 
				
			||||||
 | 
					    Other(Box<dyn Error>),
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// Connection client
 | 
					/// Connection client
 | 
				
			||||||
///
 | 
					///
 | 
				
			||||||
/// This structure acts as a wrapper around websocket connection that handles automatically parsing
 | 
					/// This structure acts as a wrapper around websocket connection that handles automatically parsing
 | 
				
			||||||
@@ -28,6 +34,24 @@ pub struct Client {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
impl Client {
 | 
					impl Client {
 | 
				
			||||||
 | 
					    /// Get remote server version
 | 
				
			||||||
 | 
					    pub async fn get_server_version() -> Result<VersionInfo, GetRemoteVersionError> {
 | 
				
			||||||
 | 
					        let url = format!("{}/version", cli_args().remote_server);
 | 
				
			||||||
 | 
					        log::debug!("Getting remote information from {} ...", url);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let res = match reqwest::get(url).await {
 | 
				
			||||||
 | 
					            Ok(r) => r,
 | 
				
			||||||
 | 
					            Err(e) if e.is_timeout() || e.is_connect() => {
 | 
				
			||||||
 | 
					                return Err(GetRemoteVersionError::ConnectionFailed)
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            Err(e) => return Err(GetRemoteVersionError::Other(Box::new(e))),
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        res.json()
 | 
				
			||||||
 | 
					            .await
 | 
				
			||||||
 | 
					            .map_err(|e| GetRemoteVersionError::Other(Box::new(e)))
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /// Start to play against a bot
 | 
					    /// Start to play against a bot
 | 
				
			||||||
    ///
 | 
					    ///
 | 
				
			||||||
    /// When playing against a bot, local server is always used
 | 
					    /// When playing against a bot, local server is always used
 | 
				
			||||||
@@ -38,7 +62,7 @@ impl Client {
 | 
				
			|||||||
            &cli_args().local_server_address(),
 | 
					            &cli_args().local_server_address(),
 | 
				
			||||||
            &format!(
 | 
					            &format!(
 | 
				
			||||||
                "/play/bot?{}",
 | 
					                "/play/bot?{}",
 | 
				
			||||||
                serde_urlencoded::to_string(&BotPlayQuery {
 | 
					                serde_urlencoded::to_string(BotPlayQuery {
 | 
				
			||||||
                    rules: rules.clone(),
 | 
					                    rules: rules.clone(),
 | 
				
			||||||
                    player_name: "Human".to_string()
 | 
					                    player_name: "Human".to_string()
 | 
				
			||||||
                })
 | 
					                })
 | 
				
			||||||
@@ -51,10 +75,10 @@ impl Client {
 | 
				
			|||||||
    /// Start to play against a random player
 | 
					    /// Start to play against a random player
 | 
				
			||||||
    pub async fn start_random_play<D: Display>(player_name: D) -> Res<Self> {
 | 
					    pub async fn start_random_play<D: Display>(player_name: D) -> Res<Self> {
 | 
				
			||||||
        Self::connect_url(
 | 
					        Self::connect_url(
 | 
				
			||||||
            &cli_args().remote_server_uri,
 | 
					            &cli_args().remote_server,
 | 
				
			||||||
            &format!(
 | 
					            &format!(
 | 
				
			||||||
                "/play/random?{}",
 | 
					                "/play/random?{}",
 | 
				
			||||||
                serde_urlencoded::to_string(&PlayRandomQuery {
 | 
					                serde_urlencoded::to_string(PlayRandomQuery {
 | 
				
			||||||
                    player_name: player_name.to_string()
 | 
					                    player_name: player_name.to_string()
 | 
				
			||||||
                })
 | 
					                })
 | 
				
			||||||
                .unwrap()
 | 
					                .unwrap()
 | 
				
			||||||
@@ -66,10 +90,10 @@ impl Client {
 | 
				
			|||||||
    /// Start a play by creating an invite
 | 
					    /// Start a play by creating an invite
 | 
				
			||||||
    pub async fn start_create_invite<D: Display>(rules: &GameRules, player_name: D) -> Res<Self> {
 | 
					    pub async fn start_create_invite<D: Display>(rules: &GameRules, player_name: D) -> Res<Self> {
 | 
				
			||||||
        Self::connect_url(
 | 
					        Self::connect_url(
 | 
				
			||||||
            &cli_args().remote_server_uri,
 | 
					            &cli_args().remote_server,
 | 
				
			||||||
            &format!(
 | 
					            &format!(
 | 
				
			||||||
                "/play/create_invite?{}",
 | 
					                "/play/create_invite?{}",
 | 
				
			||||||
                serde_urlencoded::to_string(&CreateInviteQuery {
 | 
					                serde_urlencoded::to_string(CreateInviteQuery {
 | 
				
			||||||
                    rules: rules.clone(),
 | 
					                    rules: rules.clone(),
 | 
				
			||||||
                    player_name: player_name.to_string()
 | 
					                    player_name: player_name.to_string()
 | 
				
			||||||
                })
 | 
					                })
 | 
				
			||||||
@@ -82,10 +106,10 @@ impl Client {
 | 
				
			|||||||
    /// Start a play by accepting an invite
 | 
					    /// Start a play by accepting an invite
 | 
				
			||||||
    pub async fn start_accept_invite<D: Display>(code: String, player_name: D) -> Res<Self> {
 | 
					    pub async fn start_accept_invite<D: Display>(code: String, player_name: D) -> Res<Self> {
 | 
				
			||||||
        Self::connect_url(
 | 
					        Self::connect_url(
 | 
				
			||||||
            &cli_args().remote_server_uri,
 | 
					            &cli_args().remote_server,
 | 
				
			||||||
            &format!(
 | 
					            &format!(
 | 
				
			||||||
                "/play/accept_invite?{}",
 | 
					                "/play/accept_invite?{}",
 | 
				
			||||||
                serde_urlencoded::to_string(&AcceptInviteQuery {
 | 
					                serde_urlencoded::to_string(AcceptInviteQuery {
 | 
				
			||||||
                    code,
 | 
					                    code,
 | 
				
			||||||
                    player_name: player_name.to_string()
 | 
					                    player_name: player_name.to_string()
 | 
				
			||||||
                })
 | 
					                })
 | 
				
			||||||
@@ -109,7 +133,7 @@ impl Client {
 | 
				
			|||||||
                .with_no_client_auth();
 | 
					                .with_no_client_auth();
 | 
				
			||||||
            let connector = tokio_tungstenite::Connector::Rustls(Arc::new(config));
 | 
					            let connector = tokio_tungstenite::Connector::Rustls(Arc::new(config));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            tokio_tungstenite::connect_async_tls_with_config(ws_url, None, Some(connector)).await?
 | 
					            tokio_tungstenite::connect_async_tls_with_config(ws_url, None, false,Some(connector)).await?
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
            // Perform an unsecure connection
 | 
					            // Perform an unsecure connection
 | 
				
			||||||
            tokio_tungstenite::connect_async(ws_url).await?
 | 
					            tokio_tungstenite::connect_async(ws_url).await?
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,7 +2,7 @@ extern crate core;
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
pub mod cli_args;
 | 
					pub mod cli_args;
 | 
				
			||||||
pub mod client;
 | 
					pub mod client;
 | 
				
			||||||
pub mod constants;
 | 
					pub mod consts;
 | 
				
			||||||
pub mod server;
 | 
					pub mod server;
 | 
				
			||||||
pub mod ui_screens;
 | 
					pub mod ui_screens;
 | 
				
			||||||
pub mod ui_widgets;
 | 
					pub mod ui_widgets;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -18,7 +18,7 @@ use sea_battle_backend::consts::{
 | 
				
			|||||||
use sea_battle_backend::data::GameRules;
 | 
					use sea_battle_backend::data::GameRules;
 | 
				
			||||||
use sea_battle_backend::utils::res_utils::Res;
 | 
					use sea_battle_backend::utils::res_utils::Res;
 | 
				
			||||||
use sea_battle_cli_player::cli_args::{cli_args, TestDevScreen};
 | 
					use sea_battle_cli_player::cli_args::{cli_args, TestDevScreen};
 | 
				
			||||||
use sea_battle_cli_player::client::Client;
 | 
					use sea_battle_cli_player::client::{Client, GetRemoteVersionError};
 | 
				
			||||||
use sea_battle_cli_player::server::run_server;
 | 
					use sea_battle_cli_player::server::run_server;
 | 
				
			||||||
use sea_battle_cli_player::ui_screens::configure_game_rules::GameRulesConfigurationScreen;
 | 
					use sea_battle_cli_player::ui_screens::configure_game_rules::GameRulesConfigurationScreen;
 | 
				
			||||||
use sea_battle_cli_player::ui_screens::game_screen::GameScreen;
 | 
					use sea_battle_cli_player::ui_screens::game_screen::GameScreen;
 | 
				
			||||||
@@ -35,10 +35,10 @@ async fn run_dev<B: Backend>(
 | 
				
			|||||||
    d: TestDevScreen,
 | 
					    d: TestDevScreen,
 | 
				
			||||||
) -> Result<(), Box<dyn Error>> {
 | 
					) -> Result<(), Box<dyn Error>> {
 | 
				
			||||||
    let res = match d {
 | 
					    let res = match d {
 | 
				
			||||||
        TestDevScreen::Popup => popup_screen::PopupScreen::new("Welcome there!!")
 | 
					        TestDevScreen::Popup => PopupScreen::new("Welcome there!!")
 | 
				
			||||||
            .show(terminal)?
 | 
					            .show(terminal)?
 | 
				
			||||||
            .as_string(),
 | 
					            .as_string(),
 | 
				
			||||||
        TestDevScreen::Input => input_screen::InputScreen::new("What it your name ?")
 | 
					        TestDevScreen::Input => InputScreen::new("What it your name ?")
 | 
				
			||||||
            .set_title("A custom title")
 | 
					            .set_title("A custom title")
 | 
				
			||||||
            .show(terminal)?
 | 
					            .show(terminal)?
 | 
				
			||||||
            .as_string(),
 | 
					            .as_string(),
 | 
				
			||||||
@@ -50,9 +50,9 @@ async fn run_dev<B: Backend>(
 | 
				
			|||||||
        TestDevScreen::SelectBotType => select_bot_type_screen::SelectBotTypeScreen::default()
 | 
					        TestDevScreen::SelectBotType => select_bot_type_screen::SelectBotTypeScreen::default()
 | 
				
			||||||
            .show(terminal)?
 | 
					            .show(terminal)?
 | 
				
			||||||
            .as_string(),
 | 
					            .as_string(),
 | 
				
			||||||
        TestDevScreen::SelectPlayMode => select_play_mode_screen::SelectPlayModeScreen::default()
 | 
					        TestDevScreen::SelectPlayMode => {
 | 
				
			||||||
            .show(terminal)?
 | 
					            SelectPlayModeScreen::default().show(terminal)?.as_string()
 | 
				
			||||||
            .as_string(),
 | 
					        }
 | 
				
			||||||
        TestDevScreen::SetBoatsLayout => {
 | 
					        TestDevScreen::SetBoatsLayout => {
 | 
				
			||||||
            let rules = GameRules {
 | 
					            let rules = GameRules {
 | 
				
			||||||
                boats_can_touch: true,
 | 
					                boats_can_touch: true,
 | 
				
			||||||
@@ -64,7 +64,7 @@ async fn run_dev<B: Backend>(
 | 
				
			|||||||
                .as_string()
 | 
					                .as_string()
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        TestDevScreen::ConfigureGameRules => {
 | 
					        TestDevScreen::ConfigureGameRules => {
 | 
				
			||||||
            configure_game_rules::GameRulesConfigurationScreen::new(GameRules::default())
 | 
					            GameRulesConfigurationScreen::new(GameRules::default())
 | 
				
			||||||
                .show(terminal)?
 | 
					                .show(terminal)?
 | 
				
			||||||
                .as_string()
 | 
					                .as_string()
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@@ -99,14 +99,41 @@ async fn run_app<B: Backend>(terminal: &mut Terminal<B>) -> Res {
 | 
				
			|||||||
        return run_dev(terminal, d).await;
 | 
					        return run_dev(terminal, d).await;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let mut checked_online_compatibility = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let mut rules = GameRules::default();
 | 
					    let mut rules = GameRules::default();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let mut username = "".to_string();
 | 
					    let mut username = "".to_string();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    loop {
 | 
					    loop {
 | 
				
			||||||
 | 
					        terminal.clear()?;
 | 
				
			||||||
        let choice = SelectPlayModeScreen::default().show(terminal)?;
 | 
					        let choice = SelectPlayModeScreen::default().show(terminal)?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if let ScreenResult::Ok(c) = choice {
 | 
					        if let ScreenResult::Ok(c) = choice {
 | 
				
			||||||
 | 
					            // Check compatibility
 | 
				
			||||||
 | 
					            if c.is_online_play_mode() && !checked_online_compatibility {
 | 
				
			||||||
 | 
					                PopupScreen::new("🖥 Checking remote server version...").show_once(terminal)?;
 | 
				
			||||||
 | 
					                let valid_version = match Client::get_server_version().await {
 | 
				
			||||||
 | 
					                    Ok(v) => v.is_compatible_with_static_version().unwrap_or(false),
 | 
				
			||||||
 | 
					                    Err(GetRemoteVersionError::ConnectionFailed) => {
 | 
				
			||||||
 | 
					                        PopupScreen::new("❌ Could not connect to remote server!")
 | 
				
			||||||
 | 
					                            .show(terminal)?;
 | 
				
			||||||
 | 
					                        continue;
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    Err(GetRemoteVersionError::Other(e)) => {
 | 
				
			||||||
 | 
					                        log::error!("Could not load remote server information! {:?}", e);
 | 
				
			||||||
 | 
					                        false
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if !valid_version {
 | 
				
			||||||
 | 
					                    PopupScreen::new("❌ Unfortunately, it seems that your version of Sea Battle Cli player is too old to be used online...\n\nPlease update it before trying to play online...").show(terminal)?;
 | 
				
			||||||
 | 
					                    continue;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                checked_online_compatibility = true;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if c.need_player_name() && username.is_empty() {
 | 
					            if c.need_player_name() && username.is_empty() {
 | 
				
			||||||
                username = query_player_name(terminal)?;
 | 
					                username = query_player_name(terminal)?;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
@@ -123,17 +150,17 @@ async fn run_app<B: Backend>(terminal: &mut Terminal<B>) -> Res {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        let client = match choice {
 | 
					        let client = match choice {
 | 
				
			||||||
            ScreenResult::Ok(SelectPlayModeResult::PlayRandom) => {
 | 
					            ScreenResult::Ok(SelectPlayModeResult::PlayRandom) => {
 | 
				
			||||||
                Client::start_random_play(&username).await?
 | 
					                Client::start_random_play(&username).await
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            // Play against bot
 | 
					            // Play against bot
 | 
				
			||||||
            ScreenResult::Ok(SelectPlayModeResult::PlayAgainstBot) => {
 | 
					            ScreenResult::Ok(SelectPlayModeResult::PlayAgainstBot) => {
 | 
				
			||||||
                Client::start_bot_play(&rules).await?
 | 
					                Client::start_bot_play(&rules).await
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            // Create invite
 | 
					            // Create invite
 | 
				
			||||||
            ScreenResult::Ok(SelectPlayModeResult::CreateInvite) => {
 | 
					            ScreenResult::Ok(SelectPlayModeResult::CreateInvite) => {
 | 
				
			||||||
                Client::start_create_invite(&rules, &username).await?
 | 
					                Client::start_create_invite(&rules, &username).await
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            // Join invite
 | 
					            // Join invite
 | 
				
			||||||
@@ -149,15 +176,23 @@ async fn run_app<B: Backend>(terminal: &mut Terminal<B>) -> Res {
 | 
				
			|||||||
                };
 | 
					                };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                PopupScreen::new("🔌 Connecting...").show_once(terminal)?;
 | 
					                PopupScreen::new("🔌 Connecting...").show_once(terminal)?;
 | 
				
			||||||
                Client::start_accept_invite(code, &username).await?
 | 
					                Client::start_accept_invite(code, &username).await
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            ScreenResult::Canceled | ScreenResult::Ok(SelectPlayModeResult::Exit) => return Ok(()),
 | 
					            ScreenResult::Canceled | ScreenResult::Ok(SelectPlayModeResult::Exit) => return Ok(()),
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        match client {
 | 
				
			||||||
 | 
					            Ok(client) => {
 | 
				
			||||||
                // Display game screen
 | 
					                // Display game screen
 | 
				
			||||||
                GameScreen::new(client).show(terminal).await?;
 | 
					                GameScreen::new(client).show(terminal).await?;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					            Err(e) => {
 | 
				
			||||||
 | 
					                log::error!("Failed to connect to server: {}", e);
 | 
				
			||||||
 | 
					                PopupScreen::new("❌ Failed to connect to server!").show(terminal)?;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[tokio::main]
 | 
					#[tokio::main]
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -16,7 +16,7 @@ use tui::{Frame, Terminal};
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
use sea_battle_backend::data::GameRules;
 | 
					use sea_battle_backend::data::GameRules;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::constants::{HIGHLIGHT_COLOR, TICK_RATE};
 | 
					use crate::consts::{HIGHLIGHT_COLOR, TICK_RATE};
 | 
				
			||||||
use crate::ui_screens::popup_screen::show_screen_too_small_popup;
 | 
					use crate::ui_screens::popup_screen::show_screen_too_small_popup;
 | 
				
			||||||
use crate::ui_screens::select_bot_type_screen::SelectBotTypeScreen;
 | 
					use crate::ui_screens::select_bot_type_screen::SelectBotTypeScreen;
 | 
				
			||||||
use crate::ui_screens::utils::centered_rect_size;
 | 
					use crate::ui_screens::utils::centered_rect_size;
 | 
				
			||||||
@@ -25,7 +25,7 @@ use crate::ui_widgets::button_widget::ButtonWidget;
 | 
				
			|||||||
use crate::ui_widgets::checkbox_widget::CheckboxWidget;
 | 
					use crate::ui_widgets::checkbox_widget::CheckboxWidget;
 | 
				
			||||||
use crate::ui_widgets::text_editor_widget::TextEditorWidget;
 | 
					use crate::ui_widgets::text_editor_widget::TextEditorWidget;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[derive(num_derive::FromPrimitive, num_derive::ToPrimitive, Eq, PartialEq)]
 | 
					#[derive(num_derive::FromPrimitive, num_derive::ToPrimitive, Eq, PartialEq, Copy, Clone)]
 | 
				
			||||||
enum EditingField {
 | 
					enum EditingField {
 | 
				
			||||||
    MapWidth = 0,
 | 
					    MapWidth = 0,
 | 
				
			||||||
    MapHeight,
 | 
					    MapHeight,
 | 
				
			||||||
@@ -69,7 +69,7 @@ impl GameRulesConfigurationScreen {
 | 
				
			|||||||
                if let Event::Key(key) = event::read()? {
 | 
					                if let Event::Key(key) = event::read()? {
 | 
				
			||||||
                    match key.code {
 | 
					                    match key.code {
 | 
				
			||||||
                        // Quit app
 | 
					                        // Quit app
 | 
				
			||||||
                        KeyCode::Char('q') => return Ok(ScreenResult::Canceled),
 | 
					                        KeyCode::Char('q') | KeyCode::Esc => return Ok(ScreenResult::Canceled),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                        // Navigate between fields
 | 
					                        // Navigate between fields
 | 
				
			||||||
                        KeyCode::Up | KeyCode::Left => cursor_pos -= 1,
 | 
					                        KeyCode::Up | KeyCode::Left => cursor_pos -= 1,
 | 
				
			||||||
@@ -128,7 +128,7 @@ impl GameRulesConfigurationScreen {
 | 
				
			|||||||
                            }
 | 
					                            }
 | 
				
			||||||
                        }
 | 
					                        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                        KeyCode::Char(c) if ('0'..='9').contains(&c) => {
 | 
					                        KeyCode::Char(c) if c.is_ascii_digit() => {
 | 
				
			||||||
                            let val = c.to_string().parse::<usize>().unwrap_or_default();
 | 
					                            let val = c.to_string().parse::<usize>().unwrap_or_default();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                            if self.curr_field == EditingField::MapWidth
 | 
					                            if self.curr_field == EditingField::MapWidth
 | 
				
			||||||
@@ -283,10 +283,10 @@ impl GameRulesConfigurationScreen {
 | 
				
			|||||||
            .constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
 | 
					            .constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
 | 
				
			||||||
            .split(chunks[EditingField::OK as usize]);
 | 
					            .split(chunks[EditingField::OK as usize]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        let button = ButtonWidget::new("Cancel", self.curr_field == EditingField::Cancel);
 | 
					        let button = ButtonWidget::cancel(self.curr_field == EditingField::Cancel);
 | 
				
			||||||
        f.render_widget(button, buttons_chunk[0]);
 | 
					        f.render_widget(button, buttons_chunk[0]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        let button = ButtonWidget::new("OK", self.curr_field == EditingField::OK)
 | 
					        let button = ButtonWidget::ok(self.curr_field == EditingField::OK)
 | 
				
			||||||
            .set_disabled(!self.rules.is_valid());
 | 
					            .set_disabled(!self.rules.is_valid());
 | 
				
			||||||
        f.render_widget(button, buttons_chunk[1]);
 | 
					        f.render_widget(button, buttons_chunk[1]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -9,7 +9,7 @@ use tui::text::*;
 | 
				
			|||||||
use tui::widgets::*;
 | 
					use tui::widgets::*;
 | 
				
			||||||
use tui::{Frame, Terminal};
 | 
					use tui::{Frame, Terminal};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::constants::*;
 | 
					use crate::consts::*;
 | 
				
			||||||
use crate::ui_screens::utils::centered_rect_size;
 | 
					use crate::ui_screens::utils::centered_rect_size;
 | 
				
			||||||
use crate::ui_screens::ScreenResult;
 | 
					use crate::ui_screens::ScreenResult;
 | 
				
			||||||
use crate::ui_widgets::button_widget::ButtonWidget;
 | 
					use crate::ui_widgets::button_widget::ButtonWidget;
 | 
				
			||||||
@@ -18,6 +18,7 @@ use crate::ui_widgets::button_widget::ButtonWidget;
 | 
				
			|||||||
pub fn confirm<B: Backend>(terminal: &mut Terminal<B>, msg: &str) -> bool {
 | 
					pub fn confirm<B: Backend>(terminal: &mut Terminal<B>, msg: &str) -> bool {
 | 
				
			||||||
    matches!(
 | 
					    matches!(
 | 
				
			||||||
        ConfirmDialogScreen::new(msg)
 | 
					        ConfirmDialogScreen::new(msg)
 | 
				
			||||||
 | 
					            .set_can_escape(true)
 | 
				
			||||||
            .show(terminal)
 | 
					            .show(terminal)
 | 
				
			||||||
            .unwrap_or(ScreenResult::Canceled),
 | 
					            .unwrap_or(ScreenResult::Canceled),
 | 
				
			||||||
        ScreenResult::Ok(true)
 | 
					        ScreenResult::Ok(true)
 | 
				
			||||||
@@ -28,19 +29,24 @@ pub struct ConfirmDialogScreen<'a> {
 | 
				
			|||||||
    title: &'a str,
 | 
					    title: &'a str,
 | 
				
			||||||
    msg: &'a str,
 | 
					    msg: &'a str,
 | 
				
			||||||
    is_confirm: bool,
 | 
					    is_confirm: bool,
 | 
				
			||||||
    can_cancel: bool,
 | 
					    can_escape: bool,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
impl<'a> ConfirmDialogScreen<'a> {
 | 
					impl<'a> ConfirmDialogScreen<'a> {
 | 
				
			||||||
    pub fn new(msg: &'a str) -> Self {
 | 
					    pub fn new(msg: &'a str) -> Self {
 | 
				
			||||||
        Self {
 | 
					        Self {
 | 
				
			||||||
            title: "Confirmation Request",
 | 
					            title: "❔ Confirmation Request",
 | 
				
			||||||
            msg,
 | 
					            msg,
 | 
				
			||||||
            is_confirm: true,
 | 
					            is_confirm: true,
 | 
				
			||||||
            can_cancel: false,
 | 
					            can_escape: false,
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn set_can_escape(mut self, v: bool) -> Self {
 | 
				
			||||||
 | 
					        self.can_escape = v;
 | 
				
			||||||
 | 
					        self
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    pub fn show<B: Backend>(
 | 
					    pub fn show<B: Backend>(
 | 
				
			||||||
        mut self,
 | 
					        mut self,
 | 
				
			||||||
        terminal: &mut Terminal<B>,
 | 
					        terminal: &mut Terminal<B>,
 | 
				
			||||||
@@ -56,7 +62,7 @@ impl<'a> ConfirmDialogScreen<'a> {
 | 
				
			|||||||
            if event::poll(timeout)? {
 | 
					            if event::poll(timeout)? {
 | 
				
			||||||
                if let Event::Key(key) = event::read()? {
 | 
					                if let Event::Key(key) = event::read()? {
 | 
				
			||||||
                    match key.code {
 | 
					                    match key.code {
 | 
				
			||||||
                        KeyCode::Esc | KeyCode::Char('q') if self.can_cancel => {
 | 
					                        KeyCode::Esc | KeyCode::Char('q') if self.can_escape => {
 | 
				
			||||||
                            return Ok(ScreenResult::Canceled)
 | 
					                            return Ok(ScreenResult::Canceled)
 | 
				
			||||||
                        }
 | 
					                        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -117,10 +123,10 @@ impl<'a> ConfirmDialogScreen<'a> {
 | 
				
			|||||||
            .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
 | 
					            .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
 | 
				
			||||||
            .split(chunks[1]);
 | 
					            .split(chunks[1]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        let cancel_button = ButtonWidget::new("Cancel", true).set_disabled(self.is_confirm);
 | 
					        let cancel_button = ButtonWidget::cancel(true).set_disabled(self.is_confirm);
 | 
				
			||||||
        f.render_widget(cancel_button, buttons_area[0]);
 | 
					        f.render_widget(cancel_button, buttons_area[0]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        let ok_button = ButtonWidget::new("Confirm", true).set_disabled(!self.is_confirm);
 | 
					        let ok_button = ButtonWidget::new("✅ Confirm", true).set_disabled(!self.is_confirm);
 | 
				
			||||||
        f.render_widget(ok_button, buttons_area[1]);
 | 
					        f.render_widget(ok_button, buttons_area[1]);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -16,7 +16,7 @@ use sea_battle_backend::utils::res_utils::Res;
 | 
				
			|||||||
use sea_battle_backend::utils::time_utils::time;
 | 
					use sea_battle_backend::utils::time_utils::time;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::client::Client;
 | 
					use crate::client::Client;
 | 
				
			||||||
use crate::constants::*;
 | 
					use crate::consts::*;
 | 
				
			||||||
use crate::ui_screens::confirm_dialog_screen::confirm;
 | 
					use crate::ui_screens::confirm_dialog_screen::confirm;
 | 
				
			||||||
use crate::ui_screens::popup_screen::{show_screen_too_small_popup, PopupScreen};
 | 
					use crate::ui_screens::popup_screen::{show_screen_too_small_popup, PopupScreen};
 | 
				
			||||||
use crate::ui_screens::set_boats_layout_screen::SetBoatsLayoutScreen;
 | 
					use crate::ui_screens::set_boats_layout_screen::SetBoatsLayoutScreen;
 | 
				
			||||||
@@ -147,7 +147,7 @@ impl GameScreen {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
                    match key.code {
 | 
					                    match key.code {
 | 
				
			||||||
                        // Leave game
 | 
					                        // Leave game
 | 
				
			||||||
                        KeyCode::Char('q')
 | 
					                        KeyCode::Char('q') | KeyCode::Esc
 | 
				
			||||||
                            if confirm(terminal, "Do you really want to leave game?") =>
 | 
					                            if confirm(terminal, "Do you really want to leave game?") =>
 | 
				
			||||||
                        {
 | 
					                        {
 | 
				
			||||||
                            self.client.close_connection().await;
 | 
					                            self.client.close_connection().await;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -10,7 +10,7 @@ use tui::layout::*;
 | 
				
			|||||||
use tui::widgets::*;
 | 
					use tui::widgets::*;
 | 
				
			||||||
use tui::{Frame, Terminal};
 | 
					use tui::{Frame, Terminal};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::constants::*;
 | 
					use crate::consts::*;
 | 
				
			||||||
use crate::ui_screens::utils::*;
 | 
					use crate::ui_screens::utils::*;
 | 
				
			||||||
use crate::ui_screens::ScreenResult;
 | 
					use crate::ui_screens::ScreenResult;
 | 
				
			||||||
use crate::ui_widgets::button_widget::ButtonWidget;
 | 
					use crate::ui_widgets::button_widget::ButtonWidget;
 | 
				
			||||||
@@ -163,12 +163,12 @@ impl<'a> InputScreen<'a> {
 | 
				
			|||||||
            .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
 | 
					            .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
 | 
				
			||||||
            .split(*chunks.last().unwrap());
 | 
					            .split(*chunks.last().unwrap());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        let cancel_button = ButtonWidget::new("Cancel", self.is_cancel_hovered)
 | 
					        let cancel_button = ButtonWidget::cancel(self.is_cancel_hovered)
 | 
				
			||||||
            .set_disabled(!self.can_cancel)
 | 
					            .set_disabled(!self.can_cancel)
 | 
				
			||||||
            .set_min_width(8);
 | 
					            .set_min_width(8);
 | 
				
			||||||
        f.render_widget(cancel_button, buttons_area[0]);
 | 
					        f.render_widget(cancel_button, buttons_area[0]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        let ok_button = ButtonWidget::new("OK", !self.is_cancel_hovered)
 | 
					        let ok_button = ButtonWidget::ok(!self.is_cancel_hovered)
 | 
				
			||||||
            .set_min_width(8)
 | 
					            .set_min_width(8)
 | 
				
			||||||
            .set_disabled(error.is_some());
 | 
					            .set_disabled(error.is_some());
 | 
				
			||||||
        f.render_widget(ok_button, buttons_area[1]);
 | 
					        f.render_widget(ok_button, buttons_area[1]);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -9,7 +9,7 @@ use tui::text::*;
 | 
				
			|||||||
use tui::widgets::*;
 | 
					use tui::widgets::*;
 | 
				
			||||||
use tui::{Frame, Terminal};
 | 
					use tui::{Frame, Terminal};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::constants::*;
 | 
					use crate::consts::*;
 | 
				
			||||||
use crate::ui_screens::utils::centered_rect_size;
 | 
					use crate::ui_screens::utils::centered_rect_size;
 | 
				
			||||||
use crate::ui_screens::ScreenResult;
 | 
					use crate::ui_screens::ScreenResult;
 | 
				
			||||||
use crate::ui_widgets::button_widget::ButtonWidget;
 | 
					use crate::ui_widgets::button_widget::ButtonWidget;
 | 
				
			||||||
@@ -52,7 +52,7 @@ impl<'a> PopupScreen<'a> {
 | 
				
			|||||||
            if event::poll(timeout)? {
 | 
					            if event::poll(timeout)? {
 | 
				
			||||||
                if let Event::Key(key) = event::read()? {
 | 
					                if let Event::Key(key) = event::read()? {
 | 
				
			||||||
                    match key.code {
 | 
					                    match key.code {
 | 
				
			||||||
                        KeyCode::Char('q') => return Ok(ScreenResult::Canceled),
 | 
					                        KeyCode::Char('q') | KeyCode::Esc => return Ok(ScreenResult::Canceled),
 | 
				
			||||||
                        KeyCode::Enter => {
 | 
					                        KeyCode::Enter => {
 | 
				
			||||||
                            return Ok(ScreenResult::Ok(()));
 | 
					                            return Ok(ScreenResult::Ok(()));
 | 
				
			||||||
                        }
 | 
					                        }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -11,7 +11,7 @@ use tui::{Frame, Terminal};
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
use sea_battle_backend::data::{BotDescription, BotType, PlayConfiguration};
 | 
					use sea_battle_backend::data::{BotDescription, BotType, PlayConfiguration};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::constants::{HIGHLIGHT_COLOR, TICK_RATE};
 | 
					use crate::consts::{HIGHLIGHT_COLOR, TICK_RATE};
 | 
				
			||||||
use crate::ui_screens::utils::centered_rect_size;
 | 
					use crate::ui_screens::utils::centered_rect_size;
 | 
				
			||||||
use crate::ui_screens::ScreenResult;
 | 
					use crate::ui_screens::ScreenResult;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -49,7 +49,7 @@ impl SelectBotTypeScreen {
 | 
				
			|||||||
            if event::poll(timeout)? {
 | 
					            if event::poll(timeout)? {
 | 
				
			||||||
                if let Event::Key(key) = event::read()? {
 | 
					                if let Event::Key(key) = event::read()? {
 | 
				
			||||||
                    match key.code {
 | 
					                    match key.code {
 | 
				
			||||||
                        KeyCode::Char('q') => return Ok(ScreenResult::Canceled),
 | 
					                        KeyCode::Char('q') | KeyCode::Esc => return Ok(ScreenResult::Canceled),
 | 
				
			||||||
                        KeyCode::Enter => {
 | 
					                        KeyCode::Enter => {
 | 
				
			||||||
                            return Ok(ScreenResult::Ok(self.types[self.curr_selection].r#type));
 | 
					                            return Ok(ScreenResult::Ok(self.types[self.curr_selection].r#type));
 | 
				
			||||||
                        }
 | 
					                        }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,7 +1,7 @@
 | 
				
			|||||||
use std::io;
 | 
					use std::io;
 | 
				
			||||||
use std::time::{Duration, Instant};
 | 
					use std::time::{Duration, Instant};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::constants::{HIGHLIGHT_COLOR, TICK_RATE};
 | 
					use crate::consts::{HIGHLIGHT_COLOR, TICK_RATE};
 | 
				
			||||||
use crate::ui_screens::utils::centered_rect_size;
 | 
					use crate::ui_screens::utils::centered_rect_size;
 | 
				
			||||||
use crate::ui_screens::ScreenResult;
 | 
					use crate::ui_screens::ScreenResult;
 | 
				
			||||||
use crossterm::event;
 | 
					use crossterm::event;
 | 
				
			||||||
@@ -33,6 +33,11 @@ impl SelectPlayModeResult {
 | 
				
			|||||||
    pub fn need_custom_rules(&self) -> bool {
 | 
					    pub fn need_custom_rules(&self) -> bool {
 | 
				
			||||||
        self == &SelectPlayModeResult::PlayAgainstBot || self == &SelectPlayModeResult::CreateInvite
 | 
					        self == &SelectPlayModeResult::PlayAgainstBot || self == &SelectPlayModeResult::CreateInvite
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Specify whether a play mode is to be played online or not
 | 
				
			||||||
 | 
					    pub fn is_online_play_mode(&self) -> bool {
 | 
				
			||||||
 | 
					        self != &SelectPlayModeResult::PlayAgainstBot && self != &SelectPlayModeResult::Exit
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[derive(Debug, Clone)]
 | 
					#[derive(Debug, Clone)]
 | 
				
			||||||
@@ -87,7 +92,7 @@ impl SelectPlayModeScreen {
 | 
				
			|||||||
            if crossterm::event::poll(timeout)? {
 | 
					            if crossterm::event::poll(timeout)? {
 | 
				
			||||||
                if let Event::Key(key) = event::read()? {
 | 
					                if let Event::Key(key) = event::read()? {
 | 
				
			||||||
                    match key.code {
 | 
					                    match key.code {
 | 
				
			||||||
                        KeyCode::Char('q') => return Ok(ScreenResult::Canceled),
 | 
					                        KeyCode::Char('q') | KeyCode::Esc => return Ok(ScreenResult::Canceled),
 | 
				
			||||||
                        KeyCode::Enter => {
 | 
					                        KeyCode::Enter => {
 | 
				
			||||||
                            return Ok(ScreenResult::Ok(
 | 
					                            return Ok(ScreenResult::Ok(
 | 
				
			||||||
                                AVAILABLE_PLAY_MODES[self.curr_selection].value,
 | 
					                                AVAILABLE_PLAY_MODES[self.curr_selection].value,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -12,7 +12,7 @@ use tui::{Frame, Terminal};
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
use sea_battle_backend::data::*;
 | 
					use sea_battle_backend::data::*;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::constants::*;
 | 
					use crate::consts::*;
 | 
				
			||||||
use crate::ui_screens::confirm_dialog_screen::confirm;
 | 
					use crate::ui_screens::confirm_dialog_screen::confirm;
 | 
				
			||||||
use crate::ui_screens::popup_screen::show_screen_too_small_popup;
 | 
					use crate::ui_screens::popup_screen::show_screen_too_small_popup;
 | 
				
			||||||
use crate::ui_screens::utils::{centered_rect_size, centered_text};
 | 
					use crate::ui_screens::utils::{centered_rect_size, centered_text};
 | 
				
			||||||
@@ -66,7 +66,7 @@ impl<'a> SetBoatsLayoutScreen<'a> {
 | 
				
			|||||||
                let event = event::read()?;
 | 
					                let event = event::read()?;
 | 
				
			||||||
                if let Event::Key(key) = &event {
 | 
					                if let Event::Key(key) = &event {
 | 
				
			||||||
                    match key.code {
 | 
					                    match key.code {
 | 
				
			||||||
                        KeyCode::Char('q') => {
 | 
					                        KeyCode::Char('q') | KeyCode::Esc => {
 | 
				
			||||||
                            if !self.confirm_on_cancel
 | 
					                            if !self.confirm_on_cancel
 | 
				
			||||||
                                || confirm(terminal, "Do you really want to quit?")
 | 
					                                || confirm(terminal, "Do you really want to quit?")
 | 
				
			||||||
                            {
 | 
					                            {
 | 
				
			||||||
@@ -202,7 +202,7 @@ impl<'a> SetBoatsLayoutScreen<'a> {
 | 
				
			|||||||
            .add_colored_cells(current_boat)
 | 
					            .add_colored_cells(current_boat)
 | 
				
			||||||
            .add_colored_cells(invalid_boats)
 | 
					            .add_colored_cells(invalid_boats)
 | 
				
			||||||
            .add_colored_cells(other_boats)
 | 
					            .add_colored_cells(other_boats)
 | 
				
			||||||
            .set_title("Choose your boat layout")
 | 
					            .set_title("🛥  Set your boats layout")
 | 
				
			||||||
            .set_yield_func(|c, r| {
 | 
					            .set_yield_func(|c, r| {
 | 
				
			||||||
                for i in 0..r.width {
 | 
					                for i in 0..r.width {
 | 
				
			||||||
                    for j in 0..r.height {
 | 
					                    for j in 0..r.height {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,6 @@
 | 
				
			|||||||
use std::fmt::Display;
 | 
					use std::fmt::Display;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::constants::HIGHLIGHT_COLOR;
 | 
					use crate::consts::HIGHLIGHT_COLOR;
 | 
				
			||||||
use tui::buffer::Buffer;
 | 
					use tui::buffer::Buffer;
 | 
				
			||||||
use tui::layout::Rect;
 | 
					use tui::layout::Rect;
 | 
				
			||||||
use tui::style::{Color, Style};
 | 
					use tui::style::{Color, Style};
 | 
				
			||||||
@@ -13,6 +13,7 @@ pub struct ButtonWidget {
 | 
				
			|||||||
    label: String,
 | 
					    label: String,
 | 
				
			||||||
    disabled: bool,
 | 
					    disabled: bool,
 | 
				
			||||||
    min_width: usize,
 | 
					    min_width: usize,
 | 
				
			||||||
 | 
					    hover_bg_color: Color,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
impl ButtonWidget {
 | 
					impl ButtonWidget {
 | 
				
			||||||
@@ -22,9 +23,18 @@ impl ButtonWidget {
 | 
				
			|||||||
            is_hovered,
 | 
					            is_hovered,
 | 
				
			||||||
            disabled: false,
 | 
					            disabled: false,
 | 
				
			||||||
            min_width: 0,
 | 
					            min_width: 0,
 | 
				
			||||||
 | 
					            hover_bg_color: HIGHLIGHT_COLOR,
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn cancel(is_hovered: bool) -> Self {
 | 
				
			||||||
 | 
					        Self::new("❌ Cancel", is_hovered).set_hover_bg_color(Color::Red)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn ok(is_hovered: bool) -> Self {
 | 
				
			||||||
 | 
					        Self::new("✅ OK", is_hovered)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    pub fn set_disabled(mut self, disabled: bool) -> Self {
 | 
					    pub fn set_disabled(mut self, disabled: bool) -> Self {
 | 
				
			||||||
        self.disabled = disabled;
 | 
					        self.disabled = disabled;
 | 
				
			||||||
        self
 | 
					        self
 | 
				
			||||||
@@ -35,6 +45,11 @@ impl ButtonWidget {
 | 
				
			|||||||
        self
 | 
					        self
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn set_hover_bg_color(mut self, v: Color) -> Self {
 | 
				
			||||||
 | 
					        self.hover_bg_color = v;
 | 
				
			||||||
 | 
					        self
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    pub fn estimated_size(&self) -> (u16, u16) {
 | 
					    pub fn estimated_size(&self) -> (u16, u16) {
 | 
				
			||||||
        ((self.label.len() + 2).max(self.min_width) as u16, 1)
 | 
					        ((self.label.len() + 2).max(self.min_width) as u16, 1)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -55,7 +70,7 @@ impl Widget for ButtonWidget {
 | 
				
			|||||||
        let input = Paragraph::new(label.as_ref()).style(match (self.disabled, self.is_hovered) {
 | 
					        let input = Paragraph::new(label.as_ref()).style(match (self.disabled, self.is_hovered) {
 | 
				
			||||||
            (true, _) => Style::default(),
 | 
					            (true, _) => Style::default(),
 | 
				
			||||||
            (_, false) => Style::default().bg(Color::DarkGray),
 | 
					            (_, false) => Style::default().bg(Color::DarkGray),
 | 
				
			||||||
            (_, true) => Style::default().fg(Color::White).bg(HIGHLIGHT_COLOR),
 | 
					            (_, true) => Style::default().fg(Color::White).bg(self.hover_bg_color),
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        input.render(area, buf);
 | 
					        input.render(area, buf);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
use crate::constants::HIGHLIGHT_COLOR;
 | 
					use crate::consts::HIGHLIGHT_COLOR;
 | 
				
			||||||
use std::fmt::Display;
 | 
					use std::fmt::Display;
 | 
				
			||||||
use tui::buffer::Buffer;
 | 
					use tui::buffer::Buffer;
 | 
				
			||||||
use tui::layout::Rect;
 | 
					use tui::layout::Rect;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,6 @@
 | 
				
			|||||||
use std::fmt::Display;
 | 
					use std::fmt::Display;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::constants::HIGHLIGHT_COLOR;
 | 
					use crate::consts::HIGHLIGHT_COLOR;
 | 
				
			||||||
use tui::buffer::Buffer;
 | 
					use tui::buffer::Buffer;
 | 
				
			||||||
use tui::layout::Rect;
 | 
					use tui::layout::Rect;
 | 
				
			||||||
use tui::style::*;
 | 
					use tui::style::*;
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user