use crate::error::Error; use crate::models::improvements::{ImprovementType, Improvements}; use crate::user::User; use chrono::{DateTime, Days, NaiveTime, TimeZone, Utc}; use j_db::database::Database; use log::info; use poise::serenity_prelude::UserId; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use thiserror::Error; const OPENING_HOUR: u32 = 7; const UPDATE_HOUR: u32 = 12; const CLOSING_HOUR: u32 = 18; const NIGHT_MARKET_CLOSE: u32 = 23; #[derive(Debug, Error)] pub enum GogurtError { #[error("Wow, you really don't have enough gogurt. What are you 12?? Just got buy some poor.")] NotEnoughGogurt, #[error("Sorry, gogurt can only be bought and sold when the market is open!")] OutsideOfGogurtTradingHours, } #[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct GogurtReserves { id: Option, pub reserve_contributors: HashMap, pub gogurt_rate_per_pound: Option, } impl j_db::model::JdbModel for GogurtReserves { fn id(&self) -> Option { self.id } fn set_id(&mut self, id: u64) { self.id = Some(id) } fn tree() -> String { "GogurtReserves".to_string() } } impl GogurtReserves { pub fn get_gogurt_reserve(db: &Database) -> Result { Ok(db .filter(|_, _: &Self| true)? .next() .unwrap_or(Self::default())) } fn market_time(time: u32) -> NaiveTime { NaiveTime::from_hms_opt(time, 0, 0).unwrap() } pub fn market_opening_time() -> NaiveTime { Self::market_time(OPENING_HOUR) } pub fn market_update_time() -> NaiveTime { Self::market_time(UPDATE_HOUR) } pub fn normal_market_closing_time() -> NaiveTime { Self::market_time(CLOSING_HOUR) } pub fn night_market_closing_time() -> NaiveTime { Self::market_time(NIGHT_MARKET_CLOSE) } pub fn market_closing_time(db: &Database) -> Result { let closing_time = if Self::has_night_market(db)? { Self::night_market_closing_time() } else { Self::normal_market_closing_time() }; Ok(closing_time) } pub fn has_night_market(db: &Database) -> Result { Improvements::check_is_completed(db, ImprovementType::GogurtNightMarket) } pub fn get_next_market_update_time(db: &Database) -> Result, Error> { let chicago_time = chrono_tz::America::Chicago.from_utc_datetime(&Utc::now().naive_utc()); let mut update_times = vec![Self::market_opening_time(), Self::market_update_time()]; if Self::has_night_market(db)? { update_times.push(Self::normal_market_closing_time()) } let last_update_of_the_day = *update_times.last().unwrap(); let next_check = if chicago_time.time() > last_update_of_the_day { chicago_time .with_time(update_times[0]) .unwrap() .checked_add_days(Days::new(1)) .unwrap() } else { let time = update_times .into_iter() .find(|t| &chicago_time.time() < t) .unwrap(); chicago_time.with_time(time).unwrap() }; let next_check = next_check.with_timezone(&Utc); Ok(next_check) } pub fn check_if_in_trading_hours(db: &Database, time: &DateTime) -> Result { let chicago_time = chrono_tz::America::Chicago.from_utc_datetime(&time.naive_utc()); Ok(chicago_time.time() >= Self::market_opening_time() && chicago_time.time() < Self::market_closing_time(db)?) } pub fn add_contribution(db: &Database, user: UserId, fc_amount: u64) -> Result { if !Self::check_if_in_trading_hours(db, &Utc::now())? { return Err(GogurtError::OutsideOfGogurtTradingHours.into()); } let mut gogurt_reserve = Self::get_gogurt_reserve(db)?; let gogurt_amount = gogurt_reserve.convert_fc_to_pounds(fc_amount); let contribution = gogurt_reserve .reserve_contributors .entry(user) .or_insert_with(|| 0.0); User::try_take_funds(db, user, fc_amount as u32)?; *contribution += gogurt_amount; db.insert(gogurt_reserve)?; Ok(gogurt_amount) } pub fn take_contribution( db: &Database, user: UserId, pounds_of_gogurt: f64, ) -> Result { if !Self::check_if_in_trading_hours(db, &Utc::now())? { return Err(GogurtError::OutsideOfGogurtTradingHours.into()); } let mut gogurt_reserve = Self::get_gogurt_reserve(db)?; let contribution = gogurt_reserve .reserve_contributors .entry(user) .or_insert_with(|| 0.0); if pounds_of_gogurt.is_sign_negative() { return Err(Error::CommandError( "You can't take negative gogurt, stop trying".to_string(), )); } if pounds_of_gogurt > *contribution { Err(GogurtError::NotEnoughGogurt.into()) } else { *contribution -= pounds_of_gogurt; let fc_profit = gogurt_reserve.convert_pounds_to_fc(pounds_of_gogurt); db.insert(gogurt_reserve)?; User::give_funds(db, user, fc_profit as u32)?; Ok(fc_profit) } } pub fn get_contributors(&self) -> Vec<(UserId, f64)> { let mut contributors: Vec<(UserId, f64)> = self.reserve_contributors.clone().into_iter().collect(); contributors.sort_by(|(_, a), (_, b)| b.total_cmp(a)); contributors } pub fn get_total_amount(&self) -> f64 { self.reserve_contributors.values().sum() } pub fn get_total_worth(&self) -> u64 { (self.get_total_amount() * self.gogurt_rate_per_pound()) as u64 } pub fn convert_pounds_to_fc(&self, pounds: f64) -> u64 { (pounds * self.gogurt_rate_per_pound()) as u64 } pub fn convert_fc_to_pounds(&self, fc: u64) -> f64 { fc as f64 / self.gogurt_rate_per_pound() } pub fn gogurt_rate_per_pound(&self) -> f64 { if let Some(rate_per_pound) = self.gogurt_rate_per_pound && rate_per_pound.is_normal() { rate_per_pound } else { 0.01 } } pub fn market_function() -> f64 { match rand::random_range(0..100) { 0..10 => rand::random_range(8.5..10.0), 90..98 => rand::random_range(10.0..12.0), 98..100 => rand::random_range(12.0..14.0), _ => rand::random_range(9.5..10.1), } } pub fn calculate_new_rate() -> f64 { Self::market_function() } pub fn update_rate(db: &Database) -> Result<(), Error> { let mut reserves = Self::get_gogurt_reserve(db)?; let new_rate = Self::calculate_new_rate(); info!("Updating rate to '{new_rate}'"); reserves.gogurt_rate_per_pound = Some(new_rate); db.insert(reserves)?; Ok(()) } } #[cfg(test)] mod test { use crate::models::gogurt_reserves::GogurtReserves; use chrono::{DateTime, NaiveTime, TimeZone, Utc}; #[test] fn test_avg_market_function() { let sum: f64 = (0..100000).map(|_| GogurtReserves::market_function()).sum(); let avg = sum / 100000.0; println!("Average = {avg}"); assert!(avg < 11.0 && avg > 9.0) } fn gen_time(hour: u32, min: u32, sec: u32) -> DateTime { chrono_tz::America::Chicago .from_utc_datetime(&Utc::now().naive_utc()) .with_time(NaiveTime::from_hms_opt(hour, min, sec).unwrap()) .unwrap() .to_utc() } #[test] fn test_is_market_open_before_open() { assert!(!GogurtReserves::check_if_in_trading_hours(&gen_time( 7, 59, 0 ))); } #[test] fn test_is_market_open_after_open() { assert!(GogurtReserves::check_if_in_trading_hours(&gen_time( 8, 0, 0 ))); } #[test] fn test_is_market_open_before_close() { assert!(GogurtReserves::check_if_in_trading_hours(&gen_time( 15, 59, 0 ))); } #[test] fn test_is_market_open_after_close() { assert!(!GogurtReserves::check_if_in_trading_hours(&gen_time( 16, 0, 0 ))); } }