use crate::item::Scoreable;
use crate::signal::Signal;
use crate::state::{ProfileState, OPTIMISM_CEIL, OPTIMISM_FLOOR};
pub trait StateUpdater: Send + Sync {
fn update(
&self,
state: &ProfileState,
item: &dyn Scoreable,
signal: &Signal,
now: u64,
) -> ProfileState;
}
pub struct DefaultStateUpdater {
pub alpha: f32,
}
impl DefaultStateUpdater {
pub fn new(alpha: f32) -> Self {
Self { alpha }
}
}
impl Default for DefaultStateUpdater {
fn default() -> Self {
Self { alpha: 0.05 }
}
}
impl StateUpdater for DefaultStateUpdater {
fn update(
&self,
state: &ProfileState,
item: &dyn Scoreable,
signal: &Signal,
now: u64,
) -> ProfileState {
let mut next = state.clone();
let performance = signal.performance();
next.skill = (state.skill + self.alpha * (performance - state.skill)).clamp(0.0, 1.0);
next.optimism_bias = if signal.success && signal.effort < 0.4 {
(state.optimism_bias + 0.02).clamp(OPTIMISM_FLOOR, OPTIMISM_CEIL)
} else if !signal.success {
(state.optimism_bias - 0.01).clamp(OPTIMISM_FLOOR, OPTIMISM_CEIL)
} else {
state.optimism_bias
};
next.last_seen.insert(item.id().to_string(), now);
*next.category_count.entry(item.category().to_string()).or_insert(0) += 1;
if signal.success {
next.resolved_set.insert(item.id().to_string());
}
next.interaction_count += 1;
next
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::item::Item;
use crate::state::{DEFAULT_OPTIMISM, DEFAULT_SKILL};
fn base_state() -> ProfileState {
ProfileState::new()
}
#[test]
fn skill_increases_on_success() {
let updater = DefaultStateUpdater::default();
let state = base_state();
let item = Item::new("x", 0.3, "cat");
let signal = Signal::new(true, 0.5);
let next = updater.update(&state, &item, &signal, 100);
assert!(next.skill > DEFAULT_SKILL);
}
#[test]
fn skill_stays_at_zero_on_failure_from_zero() {
let updater = DefaultStateUpdater::default();
let state = base_state();
let item = Item::new("x", 0.5, "cat");
let signal = Signal::new(false, 0.8);
let next = updater.update(&state, &item, &signal, 100);
assert!((next.skill - 0.0).abs() < 1e-6);
}
#[test]
fn optimism_increases_on_easy_success() {
let updater = DefaultStateUpdater::default();
let state = base_state();
let item = Item::new("x", 0.1, "cat");
let signal = Signal::new(true, 0.1); let next = updater.update(&state, &item, &signal, 100);
assert!(next.optimism_bias > DEFAULT_OPTIMISM);
}
#[test]
fn optimism_decreases_on_failure() {
let updater = DefaultStateUpdater::default();
let state = base_state();
let item = Item::new("x", 0.5, "cat");
let signal = Signal::new(false, 0.9);
let next = updater.update(&state, &item, &signal, 100);
assert!(next.optimism_bias < DEFAULT_OPTIMISM);
}
#[test]
fn optimism_never_below_floor() {
let updater = DefaultStateUpdater::default();
let mut state = base_state();
state.optimism_bias = OPTIMISM_FLOOR; let item = Item::new("x", 0.5, "cat");
let signal = Signal::new(false, 1.0);
let next = updater.update(&state, &item, &signal, 100);
assert!(next.optimism_bias >= OPTIMISM_FLOOR);
}
#[test]
fn resolved_set_updated_on_success() {
let updater = DefaultStateUpdater::default();
let state = base_state();
let item = Item::new("x", 0.5, "cat");
let signal = Signal::new(true, 0.5);
let next = updater.update(&state, &item, &signal, 100);
assert!(next.resolved_set.contains("x"));
}
#[test]
fn resolved_set_not_updated_on_failure() {
let updater = DefaultStateUpdater::default();
let state = base_state();
let item = Item::new("x", 0.5, "cat");
let signal = Signal::new(false, 0.5);
let next = updater.update(&state, &item, &signal, 100);
assert!(!next.resolved_set.contains("x"));
}
#[test]
fn last_seen_updated() {
let updater = DefaultStateUpdater::default();
let state = base_state();
let item = Item::new("x", 0.5, "cat");
let signal = Signal::new(true, 0.5);
let next = updater.update(&state, &item, &signal, 9999);
assert_eq!(next.last_seen["x"], 9999);
}
}