score, initialize core functionality and types
Adds Chrono package Adds a "Result" type to lib.rs creates helpful type aliases for Score, Boost, and Gravity. Creates a "TimedScore" which is a value and time data for calculating score over time. There are two forms of this, Boosted and Decaying. Since a Score can either be decaying over time or have been boosted recently and be kept from decaying for some time. Also implements a BoostedScore and a DecayingScore, which are both TimedScores which represent the different states. These are important because you can't boost a BoostedScore for instance.
This commit is contained in:
parent
38d1cbbd50
commit
a42853ac5a
4 changed files with 707 additions and 0 deletions
|
|
@ -0,0 +1,5 @@
|
|||
use std::error::Error;
|
||||
|
||||
type Result<T> = std::result::Result<T, Box<dyn Error>>;
|
||||
|
||||
pub mod score;
|
||||
193
koucha/src/score.rs
Normal file
193
koucha/src/score.rs
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
use chrono::{DateTime, Utc, TimeDelta};
|
||||
use crate::{Result};
|
||||
use std::ops::{Add, Sub};
|
||||
|
||||
mod default {
|
||||
use crate::score::SECONDS_IN_A_DAY;
|
||||
|
||||
pub const INITIAL_SCORE: i64 = 70;
|
||||
pub const GRAVITY: i64 = -10;
|
||||
pub const BOOST: i64 = 12;
|
||||
pub const BOOST_FREEZE_IN_SECONDS: i64 = SECONDS_IN_A_DAY;
|
||||
}
|
||||
const SECONDS_IN_A_DAY: i64 = 60 * 60 * 24;
|
||||
|
||||
macro_rules! rich_i64 {
|
||||
($name:ident) => {
|
||||
#[derive(PartialOrd, PartialEq, Debug, Copy, Clone)]
|
||||
pub struct $name(i64);
|
||||
impl From<$name> for i64 { fn from(id: $name) -> Self { id.0 } }
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! defaulting_i64 {
|
||||
($name:ident, $default:expr) => {
|
||||
rich_i64!($name);
|
||||
impl $name {
|
||||
pub fn new(value: Option<i64>) -> Self {
|
||||
Self(value.unwrap_or($default))
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! addable_i64s {
|
||||
($lhs:ident, $rhs:ident) => {
|
||||
impl Add<$rhs> for $lhs {
|
||||
type Output = Self;
|
||||
fn add(self, other: $rhs) -> Self::Output { Self(self.0 + other.0) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
defaulting_i64!(Score, default::INITIAL_SCORE);
|
||||
addable_i64s!(Score, Score);
|
||||
addable_i64s!(Score, Boost);
|
||||
impl Sub<Boost> for Score {
|
||||
type Output = Self;
|
||||
fn sub(self, other: Boost) -> Self::Output { Self(self.0 - other.0) }
|
||||
}
|
||||
addable_i64s!(Score, GravityOverDuration);
|
||||
defaulting_i64!(Boost, default::BOOST);
|
||||
defaulting_i64!(Gravity, default::GRAVITY);
|
||||
rich_i64!(GravityOverDuration);
|
||||
impl Gravity {
|
||||
fn over_duration(
|
||||
&self, start: DateTime<Utc>, end: DateTime<Utc>
|
||||
) -> GravityOverDuration {
|
||||
let elapsed_time = end.signed_duration_since(start);
|
||||
GravityOverDuration(
|
||||
self.0 * (elapsed_time.num_seconds() / SECONDS_IN_A_DAY)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum TimedScore {
|
||||
Decaying(DecayingScore),
|
||||
Boosted(BoostedScore),
|
||||
}
|
||||
|
||||
impl TimedScore {
|
||||
pub fn new() -> DecayingScore {
|
||||
Self::new_with_initial(Score::new(None))
|
||||
}
|
||||
|
||||
pub fn new_with_initial(initial_score: Score) -> DecayingScore {
|
||||
Self::new_with_initial_and_time(initial_score, Utc::now())
|
||||
}
|
||||
|
||||
pub fn new_with_initial_and_time(
|
||||
initial: Score, time: DateTime<Utc>
|
||||
) -> DecayingScore {
|
||||
DecayingScore {
|
||||
value: initial,
|
||||
last_updated: time,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_score(&self) -> Score {
|
||||
match self {
|
||||
Self::Decaying(s) => s.get_score(),
|
||||
Self::Boosted(b) => b.get_score(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_score(self, gravity: Gravity) -> Self {
|
||||
self.update_score_at_time(gravity, Utc::now())
|
||||
}
|
||||
|
||||
fn update_score_at_time(self, gravity: Gravity, time: DateTime<Utc>) -> Self {
|
||||
match self {
|
||||
Self::Decaying(d) => TimedScore::Decaying(
|
||||
d.apply_gravity_to_time(gravity, time)
|
||||
),
|
||||
Self::Boosted(b) => {
|
||||
let try_unfrozen = b.try_unfreeze_at_time(time);
|
||||
match try_unfrozen {
|
||||
Self::Decaying(s) => TimedScore::Decaying(
|
||||
s.apply_gravity_to_time(gravity, time)
|
||||
),
|
||||
Self::Boosted(b) => TimedScore::Boosted(b),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_decaying(self) -> Result<DecayingScore> {
|
||||
match self {
|
||||
Self::Decaying(s) => Ok(s),
|
||||
Self::Boosted(_) => Err("Attempted to get_decaying() of a boosted score".into()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_boosted(self) -> Result<BoostedScore> {
|
||||
match self {
|
||||
Self::Decaying(_) => Err("Attempted to get_boosted() of a decaying score".into()),
|
||||
Self::Boosted(b) => Ok(b),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct BoostedScore {
|
||||
value: Score,
|
||||
boosted_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct DecayingScore {
|
||||
value: Score,
|
||||
last_updated: DateTime<Utc>,
|
||||
}
|
||||
|
||||
impl DecayingScore {
|
||||
fn get_score(&self) -> Score {
|
||||
self.value
|
||||
}
|
||||
|
||||
fn apply_gravity_to_time(
|
||||
self, gravity: Gravity, update_time: DateTime<Utc>
|
||||
) -> Self {
|
||||
Self {
|
||||
last_updated: update_time,
|
||||
value: self.value + gravity.over_duration(self.last_updated, update_time),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn boost(self, boost: Boost) -> BoostedScore {
|
||||
self.boost_at_time(boost, Utc::now())
|
||||
}
|
||||
|
||||
fn boost_at_time(self, boost: Boost, boost_time: DateTime<Utc>) -> BoostedScore {
|
||||
BoostedScore {
|
||||
value: self.value + boost,
|
||||
boosted_at: boost_time,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl BoostedScore {
|
||||
fn get_score(&self) -> Score {
|
||||
self.value
|
||||
}
|
||||
|
||||
pub fn unboost(self, boost: Boost) -> DecayingScore {
|
||||
DecayingScore {
|
||||
value: self.value - boost,
|
||||
last_updated: self.boosted_at,
|
||||
}
|
||||
}
|
||||
|
||||
fn try_unfreeze_at_time(self, update_time: DateTime<Utc>) -> TimedScore {
|
||||
let boost_end = self.boosted_at + TimeDelta::seconds(default::BOOST_FREEZE_IN_SECONDS);
|
||||
if boost_end < update_time {
|
||||
TimedScore::Decaying(DecayingScore {
|
||||
value: self.value,
|
||||
last_updated: boost_end,
|
||||
})
|
||||
} else {
|
||||
TimedScore::Boosted(self)
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue