use serde::{Deserialize, Serialize};
use crate::mood::MoodVector;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[non_exhaustive]
pub enum FlowPhase {
Inactive,
Building,
Active,
Disrupted,
}
impl_display!(FlowPhase {
Inactive => "inactive",
Building => "building",
Active => "in flow",
Disrupted => "disrupted",
});
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FlowState {
pub phase: FlowPhase,
pub accumulator: f32,
pub flow_duration: u32,
pub interest_threshold: f32,
pub frustration_ceiling: f32,
pub arousal_floor: f32,
pub arousal_ceiling: f32,
pub dominance_floor: f32,
pub energy_threshold: f32,
pub alertness_threshold: f32,
pub build_rate: f32,
pub entry_threshold: f32,
}
impl Default for FlowState {
fn default() -> Self {
Self {
phase: FlowPhase::Inactive,
accumulator: 0.0,
flow_duration: 0,
interest_threshold: 0.4,
frustration_ceiling: 0.3,
arousal_floor: 0.1,
arousal_ceiling: 0.7,
dominance_floor: 0.1,
energy_threshold: 0.3,
alertness_threshold: 0.3,
build_rate: 0.05,
entry_threshold: 1.0,
}
}
}
impl FlowState {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[cfg_attr(feature = "tracing", tracing::instrument(skip_all))]
#[must_use]
#[inline]
pub fn check_conditions(
&self,
mood: &MoodVector,
energy: f32,
alertness: f32,
) -> FlowConditions {
FlowConditions {
interest_met: mood.interest >= self.interest_threshold,
frustration_met: mood.frustration <= self.frustration_ceiling,
arousal_met: mood.arousal >= self.arousal_floor && mood.arousal <= self.arousal_ceiling,
dominance_met: mood.dominance >= self.dominance_floor,
energy_met: energy >= self.energy_threshold,
alertness_met: alertness >= self.alertness_threshold,
}
}
#[cfg_attr(feature = "tracing", tracing::instrument(skip_all))]
pub fn tick(&mut self, mood: &MoodVector, energy: f32, alertness: f32) {
let all_met = self.check_conditions(mood, energy, alertness).all_met();
match self.phase {
FlowPhase::Inactive => {
if all_met {
self.phase = FlowPhase::Building;
self.accumulator = self.build_rate;
}
}
FlowPhase::Building => {
if all_met {
self.accumulator += self.build_rate;
if self.accumulator >= self.entry_threshold {
self.phase = FlowPhase::Active;
self.flow_duration = 0;
}
} else {
self.accumulator = 0.0;
self.phase = FlowPhase::Inactive;
}
}
FlowPhase::Active => {
if all_met {
self.flow_duration = self.flow_duration.saturating_add(1);
} else {
self.phase = FlowPhase::Disrupted;
self.accumulator = 0.0;
}
}
FlowPhase::Disrupted => {
self.phase = FlowPhase::Inactive;
self.flow_duration = 0;
}
}
}
#[must_use]
#[inline]
pub fn is_in_flow(&self) -> bool {
self.phase == FlowPhase::Active
}
#[must_use]
#[inline]
pub fn is_building(&self) -> bool {
self.phase == FlowPhase::Building
}
#[must_use]
#[inline]
pub fn build_progress(&self) -> f32 {
if self.entry_threshold <= 0.0 {
return 0.0;
}
(self.accumulator / self.entry_threshold).clamp(0.0, 1.0)
}
#[must_use]
#[inline]
pub fn performance_bonus(&self) -> f32 {
if self.phase != FlowPhase::Active {
return 1.0;
}
let ramp = (self.flow_duration as f32 / 60.0).min(1.0);
1.1 + ramp * 0.2
}
#[must_use]
#[inline]
pub fn energy_drain_modifier(&self) -> f32 {
if self.phase == FlowPhase::Active {
0.5
} else {
1.0
}
}
#[must_use]
#[inline]
pub fn stress_accumulation_modifier(&self) -> f32 {
if self.phase == FlowPhase::Active {
0.3
} else {
1.0
}
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct FlowConditions {
pub interest_met: bool,
pub frustration_met: bool,
pub arousal_met: bool,
pub dominance_met: bool,
pub energy_met: bool,
pub alertness_met: bool,
}
impl FlowConditions {
#[must_use]
#[inline]
pub fn all_met(&self) -> bool {
self.interest_met
&& self.frustration_met
&& self.arousal_met
&& self.dominance_met
&& self.energy_met
&& self.alertness_met
}
#[must_use]
pub fn count_met(&self) -> u8 {
let bools = [
self.interest_met,
self.frustration_met,
self.arousal_met,
self.dominance_met,
self.energy_met,
self.alertness_met,
];
bools.iter().filter(|&&b| b).count() as u8
}
}
#[cfg(feature = "traits")]
#[cfg_attr(feature = "tracing", tracing::instrument(skip_all))]
#[must_use]
pub fn flow_from_personality(profile: &crate::traits::PersonalityProfile) -> FlowState {
use crate::traits::TraitKind;
let curiosity = profile.get_trait(TraitKind::Curiosity).normalized();
let creativity = profile.get_trait(TraitKind::Creativity).normalized();
let patience = profile.get_trait(TraitKind::Patience).normalized();
let confidence = profile.get_trait(TraitKind::Confidence).normalized();
let flow_affinity = (curiosity + creativity) / 2.0;
FlowState {
interest_threshold: (0.4 - flow_affinity * 0.1).clamp(0.2, 0.6),
frustration_ceiling: (0.3 + patience * 0.1).clamp(0.1, 0.5),
arousal_ceiling: (0.7 + confidence * 0.1).clamp(0.5, 0.9),
build_rate: (0.05 + patience * 0.02).clamp(0.02, 0.1),
..FlowState::default()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::mood::Emotion;
fn flow_mood() -> MoodVector {
let mut m = MoodVector::neutral();
m.set(Emotion::Interest, 0.6);
m.set(Emotion::Arousal, 0.3);
m.set(Emotion::Dominance, 0.3);
m.set(Emotion::Frustration, 0.1);
m
}
fn breaking_mood() -> MoodVector {
let mut m = MoodVector::neutral();
m.set(Emotion::Frustration, 0.8);
m
}
#[test]
fn test_flow_new() {
let f = FlowState::new();
assert_eq!(f.phase, FlowPhase::Inactive);
assert!(f.accumulator.abs() < f32::EPSILON);
assert_eq!(f.flow_duration, 0);
}
#[test]
fn test_conditions_all_met() {
let f = FlowState::new();
let cond = f.check_conditions(&flow_mood(), 0.5, 0.5);
assert!(cond.all_met());
assert_eq!(cond.count_met(), 6);
}
#[test]
fn test_conditions_interest_not_met() {
let f = FlowState::new();
let mut m = flow_mood();
m.set(Emotion::Interest, 0.1); let cond = f.check_conditions(&m, 0.5, 0.5);
assert!(!cond.interest_met);
assert!(!cond.all_met());
assert_eq!(cond.count_met(), 5);
}
#[test]
fn test_conditions_frustration_too_high() {
let f = FlowState::new();
let mut m = flow_mood();
m.set(Emotion::Frustration, 0.5);
let cond = f.check_conditions(&m, 0.5, 0.5);
assert!(!cond.frustration_met);
}
#[test]
fn test_conditions_arousal_too_low() {
let f = FlowState::new();
let mut m = flow_mood();
m.set(Emotion::Arousal, 0.0);
let cond = f.check_conditions(&m, 0.5, 0.5);
assert!(!cond.arousal_met);
}
#[test]
fn test_conditions_arousal_too_high() {
let f = FlowState::new();
let mut m = flow_mood();
m.set(Emotion::Arousal, 0.9);
let cond = f.check_conditions(&m, 0.5, 0.5);
assert!(!cond.arousal_met);
}
#[test]
fn test_conditions_low_energy() {
let f = FlowState::new();
let cond = f.check_conditions(&flow_mood(), 0.1, 0.5);
assert!(!cond.energy_met);
}
#[test]
fn test_conditions_low_alertness() {
let f = FlowState::new();
let cond = f.check_conditions(&flow_mood(), 0.5, 0.1);
assert!(!cond.alertness_met);
}
#[test]
fn test_flow_builds_gradually() {
let mut f = FlowState::new();
let mood = flow_mood();
for _ in 0..10 {
f.tick(&mood, 0.5, 0.5);
}
assert_eq!(f.phase, FlowPhase::Building, "should still be building");
assert!(!f.is_in_flow());
assert!(f.is_building());
assert!(f.build_progress() > 0.4);
}
#[test]
fn test_flow_enters_after_sustained_conditions() {
let mut f = FlowState::new();
let mood = flow_mood();
for _ in 0..25 {
f.tick(&mood, 0.5, 0.5);
}
assert_eq!(f.phase, FlowPhase::Active, "should be in flow");
assert!(f.is_in_flow());
}
#[test]
fn test_flow_breaks_instantly() {
let mut f = FlowState::new();
let mood = flow_mood();
for _ in 0..25 {
f.tick(&mood, 0.5, 0.5);
}
assert!(f.is_in_flow());
f.tick(&breaking_mood(), 0.5, 0.5);
assert_eq!(f.phase, FlowPhase::Disrupted);
assert!(!f.is_in_flow());
}
#[test]
fn test_flow_refractory() {
let mut f = FlowState::new();
let mood = flow_mood();
for _ in 0..25 {
f.tick(&mood, 0.5, 0.5);
}
f.tick(&breaking_mood(), 0.5, 0.5);
assert_eq!(f.phase, FlowPhase::Disrupted);
f.tick(&mood, 0.5, 0.5);
assert_eq!(f.phase, FlowPhase::Inactive);
}
#[test]
fn test_flow_reentry_after_disruption() {
let mut f = FlowState::new();
let mood = flow_mood();
for _ in 0..25 {
f.tick(&mood, 0.5, 0.5);
}
assert!(f.is_in_flow());
f.tick(&breaking_mood(), 0.5, 0.5);
assert_eq!(f.phase, FlowPhase::Disrupted);
f.tick(&mood, 0.5, 0.5);
assert_eq!(f.phase, FlowPhase::Inactive);
for _ in 0..25 {
f.tick(&mood, 0.5, 0.5);
}
assert!(f.is_in_flow(), "should re-enter flow after disruption");
}
#[test]
fn test_accumulator_resets_on_break() {
let mut f = FlowState::new();
let mood = flow_mood();
for _ in 0..10 {
f.tick(&mood, 0.5, 0.5);
}
assert!(f.accumulator > 0.0);
f.tick(&breaking_mood(), 0.5, 0.5);
assert!(f.accumulator.abs() < f32::EPSILON, "should reset");
}
#[test]
fn test_full_lifecycle() {
let mut f = FlowState::new();
let mood = flow_mood();
f.tick(&mood, 0.5, 0.5);
assert_eq!(f.phase, FlowPhase::Building);
for _ in 0..24 {
f.tick(&mood, 0.5, 0.5);
}
assert_eq!(f.phase, FlowPhase::Active);
f.tick(&breaking_mood(), 0.5, 0.5);
assert_eq!(f.phase, FlowPhase::Disrupted);
f.tick(&mood, 0.5, 0.5);
assert_eq!(f.phase, FlowPhase::Inactive);
}
#[test]
fn test_performance_bonus_inactive() {
let f = FlowState::new();
assert!((f.performance_bonus() - 1.0).abs() < f32::EPSILON);
}
#[test]
fn test_performance_bonus_ramps() {
let mut f = FlowState::new();
let mood = flow_mood();
for _ in 0..25 {
f.tick(&mood, 0.5, 0.5);
}
assert!(f.is_in_flow());
let early = f.performance_bonus();
assert!(early >= 1.1, "early bonus: {early}");
for _ in 0..60 {
f.tick(&mood, 0.5, 0.5);
}
let deep = f.performance_bonus();
assert!(deep > early, "deep={deep} should > early={early}");
assert!(
deep <= 1.3 + f32::EPSILON,
"deep bonus should cap at ~1.3: {deep}"
);
}
#[test]
fn test_energy_drain_modifier() {
let mut f = FlowState::new();
assert!((f.energy_drain_modifier() - 1.0).abs() < f32::EPSILON);
let mood = flow_mood();
for _ in 0..25 {
f.tick(&mood, 0.5, 0.5);
}
assert!((f.energy_drain_modifier() - 0.5).abs() < f32::EPSILON);
}
#[test]
fn test_stress_modifier() {
let mut f = FlowState::new();
assert!((f.stress_accumulation_modifier() - 1.0).abs() < f32::EPSILON);
let mood = flow_mood();
for _ in 0..25 {
f.tick(&mood, 0.5, 0.5);
}
assert!((f.stress_accumulation_modifier() - 0.3).abs() < f32::EPSILON);
}
#[test]
fn test_flow_requires_energy() {
let mut f = FlowState::new();
let mood = flow_mood();
for _ in 0..30 {
f.tick(&mood, 0.1, 0.5); }
assert!(!f.is_in_flow(), "should not enter flow with low energy");
}
#[test]
fn test_flow_requires_alertness() {
let mut f = FlowState::new();
let mood = flow_mood();
for _ in 0..30 {
f.tick(&mood, 0.5, 0.1); }
assert!(!f.is_in_flow(), "should not enter flow when sleepy");
}
#[test]
fn test_build_progress() {
let mut f = FlowState::new();
assert!(f.build_progress().abs() < f32::EPSILON);
let mood = flow_mood();
for _ in 0..10 {
f.tick(&mood, 0.5, 0.5);
}
let progress = f.build_progress();
assert!(progress > 0.4 && progress < 0.7, "progress: {progress}");
}
#[test]
fn test_build_progress_zero_threshold() {
let mut f = FlowState::new();
f.entry_threshold = 0.0;
assert!(f.build_progress().abs() < f32::EPSILON);
}
#[test]
fn test_flow_duration_saturates() {
let mut f = FlowState::new();
f.phase = FlowPhase::Active;
f.flow_duration = u32::MAX;
f.tick(&flow_mood(), 0.5, 0.5);
assert_eq!(f.flow_duration, u32::MAX); }
#[test]
fn test_phase_display() {
assert_eq!(FlowPhase::Inactive.to_string(), "inactive");
assert_eq!(FlowPhase::Building.to_string(), "building");
assert_eq!(FlowPhase::Active.to_string(), "in flow");
assert_eq!(FlowPhase::Disrupted.to_string(), "disrupted");
}
#[test]
fn test_serde() {
let mut f = FlowState::new();
f.phase = FlowPhase::Active;
f.flow_duration = 42;
let json = serde_json::to_string(&f).unwrap();
let f2: FlowState = serde_json::from_str(&json).unwrap();
assert_eq!(f2.phase, FlowPhase::Active);
assert_eq!(f2.flow_duration, 42);
}
#[cfg(feature = "traits")]
#[test]
fn test_flow_from_personality_curious() {
let mut p = crate::traits::PersonalityProfile::new("curious");
p.set_trait(
crate::traits::TraitKind::Curiosity,
crate::traits::TraitLevel::Highest,
);
p.set_trait(
crate::traits::TraitKind::Creativity,
crate::traits::TraitLevel::Highest,
);
let f = flow_from_personality(&p);
assert!(
f.interest_threshold < 0.4,
"curious should enter flow easier: {}",
f.interest_threshold
);
}
#[cfg(feature = "traits")]
#[test]
fn test_flow_from_personality_patient() {
let mut p = crate::traits::PersonalityProfile::new("patient");
p.set_trait(
crate::traits::TraitKind::Patience,
crate::traits::TraitLevel::Highest,
);
let f = flow_from_personality(&p);
assert!(
f.build_rate > 0.05,
"patient should build flow faster: {}",
f.build_rate
);
assert!(
f.frustration_ceiling > 0.3,
"patient should tolerate more frustration: {}",
f.frustration_ceiling
);
}
}