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:
Julia Lange 2026-02-05 12:05:43 -08:00
parent 38d1cbbd50
commit a42853ac5a
Signed by: Julia
SSH key fingerprint: SHA256:5DJcfxa5/fKCYn57dcabJa2vN2e6eT0pBerYi5SUbto
4 changed files with 707 additions and 0 deletions

View file

@ -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
View 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)
}
}
}