294 lines
8.3 KiB
Rust
294 lines
8.3 KiB
Rust
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<u64>,
|
|
pub reserve_contributors: HashMap<UserId, f64>,
|
|
pub gogurt_rate_per_pound: Option<f64>,
|
|
}
|
|
|
|
impl j_db::model::JdbModel for GogurtReserves {
|
|
fn id(&self) -> Option<u64> {
|
|
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<Self, Error> {
|
|
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<NaiveTime, Error> {
|
|
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<bool, Error> {
|
|
Improvements::check_is_completed(db, ImprovementType::GogurtNightMarket)
|
|
}
|
|
|
|
pub fn get_next_market_update_time(db: &Database) -> Result<DateTime<Utc>, 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<Utc>) -> Result<bool, Error> {
|
|
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<f64, Error> {
|
|
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<u64, Error> {
|
|
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<Utc> {
|
|
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
|
|
)));
|
|
}
|
|
}
|