use serde::{Deserialize, Serialize};
use crate::error::{BodhError, Result, validate_finite};
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct BasicNeeds {
pub autonomy: f64,
pub competence: f64,
pub relatedness: f64,
}
impl BasicNeeds {
#[inline]
#[must_use]
pub fn satisfaction(&self) -> f64 {
(self.autonomy + self.competence + self.relatedness) / 3.0
}
#[must_use]
pub fn most_deprived(&self) -> NeedType {
if self.autonomy <= self.competence && self.autonomy <= self.relatedness {
NeedType::Autonomy
} else if self.competence <= self.relatedness {
NeedType::Competence
} else {
NeedType::Relatedness
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum NeedType {
Autonomy,
Competence,
Relatedness,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[non_exhaustive]
pub enum MotivationType {
Amotivation,
ExternalRegulation,
IntrojectedRegulation,
IdentifiedRegulation,
IntegratedRegulation,
IntrinsicMotivation,
}
#[must_use]
pub fn predict_motivation(needs: &BasicNeeds) -> MotivationType {
let sat = needs.satisfaction();
if sat < 0.15 {
MotivationType::Amotivation
} else if sat < 0.3 {
MotivationType::ExternalRegulation
} else if sat < 0.45 {
MotivationType::IntrojectedRegulation
} else if sat < 0.6 {
MotivationType::IdentifiedRegulation
} else if sat < 0.8 {
MotivationType::IntegratedRegulation
} else {
MotivationType::IntrinsicMotivation
}
}
#[must_use = "returns the autonomy index without side effects"]
pub fn relative_autonomy_index(
intrinsic: f64,
identified: f64,
introjected: f64,
external: f64,
) -> Result<f64> {
validate_finite(intrinsic, "intrinsic")?;
validate_finite(identified, "identified")?;
validate_finite(introjected, "introjected")?;
validate_finite(external, "external")?;
Ok(2.0 * intrinsic + identified - introjected - 2.0 * external)
}
#[inline]
#[must_use = "returns the motivation strength without side effects"]
pub fn expectancy_value(expectancy: f64, value: f64) -> Result<f64> {
validate_finite(expectancy, "expectancy")?;
validate_finite(value, "value")?;
Ok(expectancy * value)
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct TaskValue {
pub intrinsic_value: f64,
pub attainment_value: f64,
pub utility_value: f64,
pub cost: f64,
}
impl TaskValue {
#[inline]
#[must_use]
pub fn net_value(&self) -> f64 {
(self.intrinsic_value + self.attainment_value + self.utility_value) / 3.0 - self.cost
}
}
#[must_use = "returns the flow intensity without side effects"]
pub fn flow_state(challenge: f64, skill: f64) -> Result<f64> {
validate_finite(challenge, "challenge")?;
validate_finite(skill, "skill")?;
if !(0.0..=1.0).contains(&challenge) {
return Err(BodhError::InvalidParameter(
"challenge must be in [0, 1]".into(),
));
}
if !(0.0..=1.0).contains(&skill) {
return Err(BodhError::InvalidParameter(
"skill must be in [0, 1]".into(),
));
}
let match_factor = 1.0 - (challenge - skill).abs();
let intensity = (challenge + skill) / 2.0;
Ok(match_factor * intensity)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum FlowChannel {
Apathy,
Boredom,
Anxiety,
Flow,
}
#[must_use]
pub fn classify_flow_channel(challenge: f64, skill: f64) -> FlowChannel {
let high_challenge = challenge >= 0.5;
let high_skill = skill >= 0.5;
match (high_challenge, high_skill) {
(true, true) => FlowChannel::Flow,
(true, false) => FlowChannel::Anxiety,
(false, true) => FlowChannel::Boredom,
(false, false) => FlowChannel::Apathy,
}
}
#[inline]
#[must_use = "returns the motivation level without side effects"]
pub fn goal_gradient(progress: f64, base_motivation: f64, gradient: f64) -> Result<f64> {
validate_finite(progress, "progress")?;
validate_finite(base_motivation, "base_motivation")?;
validate_finite(gradient, "gradient")?;
if !(0.0..1.0).contains(&progress) {
return Err(BodhError::InvalidParameter(
"progress must be in [0, 1)".into(),
));
}
let boost = gradient * progress / (1.0 - progress + 1e-6);
Ok(base_motivation * (1.0 + boost))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_needs_satisfaction() {
let needs = BasicNeeds {
autonomy: 0.8,
competence: 0.6,
relatedness: 0.7,
};
assert!((needs.satisfaction() - 0.7).abs() < 1e-10);
}
#[test]
fn test_most_deprived() {
let needs = BasicNeeds {
autonomy: 0.9,
competence: 0.2,
relatedness: 0.5,
};
assert_eq!(needs.most_deprived(), NeedType::Competence);
}
#[test]
fn test_predict_motivation_intrinsic() {
let needs = BasicNeeds {
autonomy: 0.9,
competence: 0.9,
relatedness: 0.9,
};
assert_eq!(
predict_motivation(&needs),
MotivationType::IntrinsicMotivation
);
}
#[test]
fn test_predict_motivation_amotivation() {
let needs = BasicNeeds {
autonomy: 0.05,
competence: 0.05,
relatedness: 0.05,
};
assert_eq!(predict_motivation(&needs), MotivationType::Amotivation);
}
#[test]
fn test_predict_motivation_ordering() {
assert!(MotivationType::IntrinsicMotivation > MotivationType::ExternalRegulation);
assert!(MotivationType::IdentifiedRegulation > MotivationType::IntrojectedRegulation);
}
#[test]
fn test_relative_autonomy_index() {
let rai = relative_autonomy_index(5.0, 4.0, 2.0, 1.0).unwrap();
assert!(rai > 0.0);
let rai2 = relative_autonomy_index(1.0, 2.0, 4.0, 5.0).unwrap();
assert!(rai2 < 0.0);
}
#[test]
fn test_rai_known_value() {
let rai = relative_autonomy_index(3.0, 2.0, 1.0, 0.0).unwrap();
assert!((rai - 7.0).abs() < 1e-10);
}
#[test]
fn test_expectancy_value_basic() {
let m = expectancy_value(0.8, 10.0).unwrap();
assert!((m - 8.0).abs() < 1e-10);
}
#[test]
fn test_expectancy_value_zero() {
let m = expectancy_value(0.0, 10.0).unwrap();
assert!(m.abs() < 1e-10);
}
#[test]
fn test_task_value_net() {
let tv = TaskValue {
intrinsic_value: 0.8,
attainment_value: 0.6,
utility_value: 0.4,
cost: 0.3,
};
assert!((tv.net_value() - 0.3).abs() < 1e-10);
}
#[test]
fn test_task_value_high_cost() {
let tv = TaskValue {
intrinsic_value: 0.3,
attainment_value: 0.3,
utility_value: 0.3,
cost: 0.8,
};
assert!(tv.net_value() < 0.0); }
#[test]
fn test_flow_peak() {
let f = flow_state(0.9, 0.9).unwrap();
assert!(f > 0.8);
}
#[test]
fn test_flow_mismatch_reduces() {
let matched = flow_state(0.8, 0.8).unwrap();
let mismatched = flow_state(0.8, 0.2).unwrap();
assert!(matched > mismatched);
}
#[test]
fn test_flow_low_both() {
let f = flow_state(0.1, 0.1).unwrap();
assert!(f < 0.2);
}
#[test]
fn test_flow_invalid() {
assert!(flow_state(-0.1, 0.5).is_err());
assert!(flow_state(0.5, 1.5).is_err());
}
#[test]
fn test_classify_flow_channel() {
assert_eq!(classify_flow_channel(0.8, 0.8), FlowChannel::Flow);
assert_eq!(classify_flow_channel(0.8, 0.2), FlowChannel::Anxiety);
assert_eq!(classify_flow_channel(0.2, 0.8), FlowChannel::Boredom);
assert_eq!(classify_flow_channel(0.2, 0.2), FlowChannel::Apathy);
}
#[test]
fn test_goal_gradient_increases() {
let early = goal_gradient(0.2, 1.0, 1.0).unwrap();
let late = goal_gradient(0.8, 1.0, 1.0).unwrap();
assert!(late > early);
}
#[test]
fn test_goal_gradient_zero_progress() {
let m = goal_gradient(0.0, 1.0, 1.0).unwrap();
assert!((m - 1.0).abs() < 0.01); }
#[test]
fn test_goal_gradient_invalid() {
assert!(goal_gradient(1.0, 1.0, 1.0).is_err()); assert!(goal_gradient(-0.1, 1.0, 1.0).is_err());
}
#[test]
fn test_expectancy_value_known() {
let m = expectancy_value(0.7, 8.0).unwrap();
assert!((m - 5.6).abs() < 1e-10);
}
#[test]
fn test_flow_state_known_value() {
let f = flow_state(0.8, 0.6).unwrap();
assert!((f - 0.56).abs() < 1e-10);
}
#[test]
fn test_basic_needs_serde_roundtrip() {
let needs = BasicNeeds {
autonomy: 0.7,
competence: 0.5,
relatedness: 0.8,
};
let json = serde_json::to_string(&needs).unwrap();
let back: BasicNeeds = serde_json::from_str(&json).unwrap();
assert!((needs.autonomy - back.autonomy).abs() < 1e-10);
}
#[test]
fn test_need_type_serde_roundtrip() {
let n = NeedType::Competence;
let json = serde_json::to_string(&n).unwrap();
let back: NeedType = serde_json::from_str(&json).unwrap();
assert_eq!(n, back);
}
#[test]
fn test_motivation_type_serde_roundtrip() {
let m = MotivationType::IdentifiedRegulation;
let json = serde_json::to_string(&m).unwrap();
let back: MotivationType = serde_json::from_str(&json).unwrap();
assert_eq!(m, back);
}
#[test]
fn test_task_value_serde_roundtrip() {
let tv = TaskValue {
intrinsic_value: 0.8,
attainment_value: 0.6,
utility_value: 0.4,
cost: 0.2,
};
let json = serde_json::to_string(&tv).unwrap();
let back: TaskValue = serde_json::from_str(&json).unwrap();
assert!((tv.cost - back.cost).abs() < 1e-10);
}
#[test]
fn test_flow_channel_serde_roundtrip() {
let f = FlowChannel::Anxiety;
let json = serde_json::to_string(&f).unwrap();
let back: FlowChannel = serde_json::from_str(&json).unwrap();
assert_eq!(f, back);
}
}