From 9406c09410a8557dd8e7e166e637963db053e5fd Mon Sep 17 00:00:00 2001 From: rakuja Date: Mon, 8 Jul 2024 00:13:59 +0200 Subject: [PATCH] feat: * add shield in random gen; * lower minimum size of dice to 1 (to allow fixed numbers aka 100d1 => 100; * add safe guarantees around n of forged items range gen, now it is formally proved that it will not panic and isolated in a method to reduce redundancy. --- src/db/data_providers/raw_query_builder.rs | 9 ++- src/db/data_providers/shop_fetcher.rs | 5 ++ src/models/routers_validator_structs.rs | 14 ++++- src/models/shop_structs.rs | 1 + src/routes/shop.rs | 2 + src/services/shop_service.rs | 64 ++++++++++++++++++---- 6 files changed, 81 insertions(+), 14 deletions(-) diff --git a/src/db/data_providers/raw_query_builder.rs b/src/db/data_providers/raw_query_builder.rs index 5e08ecc..90af231 100644 --- a/src/db/data_providers/raw_query_builder.rs +++ b/src/db/data_providers/raw_query_builder.rs @@ -39,9 +39,16 @@ pub fn prepare_filtered_get_items(shop_filter_query: &ShopFilterQuery) -> String max_level, &supported_pf_versions, ); + let shield_query = prepare_item_subquery( + &ItemTypeEnum::Shield, + shop_filter_query.n_of_shields, + min_level, + max_level, + &supported_pf_versions, + ); let query = format!( "SELECT * FROM ITEM_TABLE WHERE id IN ( {equipment_query} ) OR id IN ({consumable_query} ) - OR id IN ({weapon_query} ) OR id IN ({armor_query} )" + OR id IN ({weapon_query} ) OR id IN ({armor_query} ) OR id IN ({shield_query} )" ); debug!("{}", query); query diff --git a/src/db/data_providers/shop_fetcher.rs b/src/db/data_providers/shop_fetcher.rs index 25d2cc4..5b0d1fc 100644 --- a/src/db/data_providers/shop_fetcher.rs +++ b/src/db/data_providers/shop_fetcher.rs @@ -266,6 +266,10 @@ pub async fn fetch_items_with_filters( .iter() .filter(|x| x.item_type == ItemTypeEnum::Armor) .collect(); + let shields: Vec<&Item> = items + .iter() + .filter(|x| x.item_type == ItemTypeEnum::Shield) + .collect(); let consumables: Vec<&Item> = items .iter() .filter(|x| x.item_type == ItemTypeEnum::Consumable) @@ -281,6 +285,7 @@ pub async fn fetch_items_with_filters( item_vec.extend(fill_item_vec_to_len(&consumables, filters.n_of_consumables)); item_vec.extend(fill_item_vec_to_len(&weapons, filters.n_of_weapons)); item_vec.extend(fill_item_vec_to_len(&armors, filters.n_of_armors)); + item_vec.extend(fill_item_vec_to_len(&shields, filters.n_of_shields)); Ok(item_vec) } diff --git a/src/models/routers_validator_structs.rs b/src/models/routers_validator_structs.rs index ca86eb8..ea01311 100644 --- a/src/models/routers_validator_structs.rs +++ b/src/models/routers_validator_structs.rs @@ -102,15 +102,25 @@ impl Default for PaginatedRequest { pub struct Dice { #[validate(range(min = 1, max = 255))] pub n_of_dices: u8, - #[validate(range(min = 2, max = 255))] + // 1 needs to be an option, to allow 100d1 => 100 + #[validate(range(min = 1, max = 255))] pub dice_size: u8, } impl Dice { + /// Dice roll will roll n dices with each roll in the range of 1<=result<=dice_size. + /// It returns the sum of n_of_dices rolls. + /// IT SHOULD NEVER BE <1, OTHERWISE WE BREAK THE CONTRACT OF THE METHOD. pub fn roll(&self) -> i64 { let mut roll_result = 0; for _ in 0..self.n_of_dices { - roll_result += rand::thread_rng().gen_range(1..=self.dice_size) as i64 + // gen_range panics if n<2 (1..1), panic! + // so we directly return 1 if that's the case + roll_result += if self.dice_size > 1 { + rand::thread_rng().gen_range(1..=self.dice_size) as i64 + } else { + 1 + } } roll_result } diff --git a/src/models/shop_structs.rs b/src/models/shop_structs.rs index 3605fa8..de3592d 100644 --- a/src/models/shop_structs.rs +++ b/src/models/shop_structs.rs @@ -37,6 +37,7 @@ pub struct ShopFilterQuery { pub n_of_consumables: i64, pub n_of_weapons: i64, pub n_of_armors: i64, + pub n_of_shields: i64, pub pathfinder_version: PathfinderVersionEnum, } diff --git a/src/routes/shop.rs b/src/routes/shop.rs index ebc635e..682610b 100644 --- a/src/routes/shop.rs +++ b/src/routes/shop.rs @@ -2,6 +2,7 @@ use crate::models::item::armor_struct::ArmorData; use crate::models::item::item_metadata::type_enum::ItemTypeEnum; use crate::models::item::item_metadata::type_enum::WeaponTypeEnum; use crate::models::item::item_struct::Item; +use crate::models::item::shield_struct::ShieldData; use crate::models::item::weapon_struct::WeaponData; use crate::models::response_data::ResponseItem; use crate::models::routers_validator_structs::ItemFieldFilters; @@ -40,6 +41,7 @@ pub fn init_docs(doc: &mut utoipa::openapi::OpenApi) { ItemSortEnum, WeaponData, ArmorData, + ShieldData, WeaponTypeEnum )) )] diff --git a/src/services/shop_service.rs b/src/services/shop_service.rs index 53e8d68..e8c6337 100644 --- a/src/services/shop_service.rs +++ b/src/services/shop_service.rs @@ -49,26 +49,38 @@ pub async fn generate_random_shop_listing( let shop_type = shop_data.shop_type.clone().unwrap_or_default(); let n_of_consumables: i64 = shop_data.consumable_dices.iter().map(|x| x.roll()).sum(); let n_of_equipables: i64 = shop_data.equipment_dices.iter().map(|x| x.roll()).sum(); - let (n_of_equipment, n_of_armors, n_of_weapons) = match shop_type { + let (n_of_equipment, n_of_armors, n_of_weapons, n_of_shields) = match shop_type { ShopTypeEnum::Blacksmith => { + // This will never panic if n_of_equipables >= 1 and dice sum should always be at least 1. + // if 1<=n<2 => n/2 = 0..n + // TLDR we know that it will never panic. let n_of_forged_items = thread_rng().gen_range((n_of_equipables / 2)..=n_of_equipables); - let n_of_weapons = thread_rng().gen_range(0..=n_of_forged_items); - let n_of_armors = n_of_forged_items - n_of_weapons; + let forged_items_tuple = get_forged_items_tuple(n_of_forged_items); ( n_of_equipables - n_of_forged_items, - n_of_weapons, - n_of_armors, + forged_items_tuple.0, + forged_items_tuple.1, + forged_items_tuple.2, ) } - ShopTypeEnum::Alchemist => (n_of_equipables, 0, 0), + ShopTypeEnum::Alchemist => (n_of_equipables, 0, 0, 0), ShopTypeEnum::General => { - let n_of_forged_items = thread_rng().gen_range(0..=(n_of_equipables / 2)); - let n_of_weapons = thread_rng().gen_range(0..=n_of_forged_items); - let n_of_armors = n_of_forged_items - n_of_weapons; + // This can panic if n_of_equipables is <=1, + // n=1 => n/2 = 0, 0..0 panic! + // we manually set it as 1 in that case + let n_of_forged_items = thread_rng().gen_range( + 0..=if n_of_equipables > 1 { + n_of_equipables / 2 + } else { + 1 + }, + ); + let forged_items_tuple = get_forged_items_tuple(n_of_forged_items); ( n_of_equipables - n_of_forged_items, - n_of_weapons, - n_of_armors, + forged_items_tuple.0, + forged_items_tuple.1, + forged_items_tuple.2, ) } }; @@ -84,6 +96,7 @@ pub async fn generate_random_shop_listing( n_of_consumables, n_of_weapons, n_of_armors, + n_of_shields, pathfinder_version, }, ) @@ -111,6 +124,35 @@ pub async fn get_traits_list(app_state: &AppState) -> Vec { shop_proxy::get_all_possible_values_of_filter(app_state, ItemField::Traits).await } +/// Gets the n of: weapons, armors, shields (in this order). +/// Changing order is considered a BREAKING CHANGE. +/// Calculating it randomly from the n of forged items. +fn get_forged_items_tuple(n_of_forged_items: i64) -> (i64, i64, i64) { + // This can panic if n=0. + // n<2 => 0..1, ok! + // n<1 => 0..0, panic! + // if that's the case we return 0 manually + let n_of_weapons = if n_of_forged_items > 0 { + thread_rng().gen_range(n_of_forged_items / 2..=n_of_forged_items) + } else { + 0 + }; + let n_of_armors = n_of_forged_items - n_of_weapons; + // This can panic if we do not have enough armors (n<3). + // n<3 => 1..1, panic! + // n=3 => 1..2, ok! + // if that's the case we return 0 manually + // We take at least 1 shield if there are >3 armor + // (shield will never be > armor, + // with n>3 => (n/3)+1 is always < n + let n_of_shields = if n_of_armors >= 3 { + thread_rng().gen_range(1..(n_of_armors / 3) + 1) + } else { + 0 + }; + (n_of_weapons, n_of_armors - n_of_shields, n_of_shields) +} + fn convert_result_to_shop_response( field_filters: &ItemFieldFilters, pagination: &ShopPaginatedRequest,