use crate::core::config::{ AnimationStep, NextStateEntry, StateConfig, StateMachineConfig, StateMode, StateTransition, TriggerType, }; use anyhow::Result; use rand::Rng; pub struct StateMachine { pub config: StateMachineConfig, pub current_state: String, pub current_sequence_index: usize, pub current_loop_remaining: i32, pub pending_trigger_target: Option, } impl StateMachine { pub fn new(config: StateMachineConfig) -> Self { Self { current_state: config.initial_state.clone(), config, current_sequence_index: 0, current_loop_remaining: 0, pending_trigger_target: None, } } pub fn start(&mut self) -> Result<()> { self.current_state = self.config.initial_state.clone(); self.pending_trigger_target = None; self.reset_state_progress() } pub fn current_video_id(&self) -> Option { self.current_state_config() .and_then(|state| state.sequence.get(self.current_sequence_index)) .map(|step| step.video_id.clone()) } pub fn current_state_config(&self) -> Option<&StateConfig> { self.config.states.get(&self.current_state) } pub fn on_video_completed(&mut self) -> Result { self.ensure_current_state_valid()?; let old_state = self.current_state.clone(); if self.current_loop_remaining > 1 { self.current_loop_remaining -= 1; return Ok(false); } // 有待执行的触发器时,跳过 sequence 剩余 step,立即切换状态 if self.pending_trigger_target.is_some() { let target_state = self.pending_trigger_target.take().unwrap(); self.transition_to_state(&target_state)?; return Ok(self.current_state != old_state); } let sequence_len = self .current_state_config() .ok_or_else(|| anyhow::anyhow!("当前状态不存在: {}", self.current_state))? .sequence .len(); if self.current_sequence_index + 1 < sequence_len { self.current_sequence_index += 1; self.current_loop_remaining = self.resolve_current_loop_count()?; return Ok(false); } if let Some(target_state) = self.pending_trigger_target.take() { self.transition_to_state(&target_state)?; return Ok(self.current_state != old_state); } let next_state = self.select_next_state()?; self.transition_to_state(&next_state)?; Ok(self.current_state != old_state) } pub fn handle_trigger(&mut self, name: &str, value: &str) -> Result { let Some(state) = self.current_state_config() else { return Ok(false); }; if state.ignore_triggers { return Ok(false); } if self.pending_trigger_target.is_some() { return Ok(false); } let target_state = self .matching_transition(&state.transitions, name, value) .map(|transition| transition.target_state.clone()); let Some(target_state) = target_state else { return Ok(false); }; if state.defer_triggers { self.pending_trigger_target = Some(target_state); if self.current_loop_remaining > 1 { self.current_loop_remaining = 1; } return Ok(false); } self.transition_to_state(&target_state)?; Ok(true) } pub fn check_random_triggers(&mut self) -> Result { let Some(state) = self.current_state_config() else { return Ok(false); }; if state.ignore_triggers { return Ok(false); } if !matches!(state.mode, StateMode::FreeMode | StateMode::InteractiveMode) { return Ok(false); } let mut rng = rand::thread_rng(); let target_state = state .transitions .iter() .filter(|transition| match &transition.trigger { TriggerType::Random { probability } => { let probability = probability.clamp(0.0, 1.0); rng.gen_bool(probability) } _ => false, }) .max_by_key(|transition| transition.priority) .map(|transition| transition.target_state.clone()); let Some(target_state) = target_state else { return Ok(false); }; if state.defer_triggers { self.pending_trigger_target = Some(target_state); return Ok(true); } self.transition_to_state(&target_state)?; Ok(true) } pub fn has_pending_trigger(&self) -> bool { self.pending_trigger_target.is_some() } fn reset_state_progress(&mut self) -> Result<()> { self.current_sequence_index = 0; self.ensure_current_state_valid()?; self.current_loop_remaining = self.resolve_current_loop_count()?; Ok(()) } fn transition_to_state(&mut self, target_state: &str) -> Result<()> { if !self.config.states.contains_key(target_state) { anyhow::bail!("目标状态不存在: {}", target_state); } self.current_state = target_state.to_string(); self.pending_trigger_target = None; self.reset_state_progress() } fn ensure_current_state_valid(&self) -> Result<()> { let state = self .current_state_config() .ok_or_else(|| anyhow::anyhow!("当前状态不存在: {}", self.current_state))?; if state.sequence.is_empty() { anyhow::bail!("状态 '{}' 的 sequence 不能为空", state.name); } if self.current_sequence_index >= state.sequence.len() { anyhow::bail!( "状态 '{}' 的 sequence 索引越界: {} >= {}", state.name, self.current_sequence_index, state.sequence.len() ); } Ok(()) } fn resolve_current_loop_count(&self) -> Result { if self.pending_trigger_target.is_some() { return Ok(1); } let state = self .current_state_config() .ok_or_else(|| anyhow::anyhow!("当前状态不存在: {}", self.current_state))?; let step = state .sequence .get(self.current_sequence_index) .ok_or_else(|| { anyhow::anyhow!( "状态 '{}' 的 sequence 索引越界: {}", state.name, self.current_sequence_index ) })?; Ok(self.resolve_step_loop_count(step)) } fn resolve_step_loop_count(&self, step: &AnimationStep) -> i32 { if let Some([start, end]) = step.random_loop_range { let min = start.min(end); let max = start.max(end); return rand::thread_rng().gen_range(min..=max).max(1); } step.loop_count.unwrap_or(1).max(1) } fn select_next_state(&self) -> Result { let state = self .current_state_config() .ok_or_else(|| anyhow::anyhow!("当前状态不存在: {}", self.current_state))?; if let Some(entries) = &state.next_states { if let Some(next_state) = self.select_weighted_next_state(entries) { return Ok(next_state); } } if let Some(next_state) = &state.next_state { return Ok(next_state.clone()); } // FreeMode 状态没有配置 next_state/next_states 时,按权重随机选择一个 FreeMode 状态 match state.mode { StateMode::FreeMode => self.select_random_free_state(), StateMode::InteractiveMode => Ok(self.current_state.clone()), } } /// 按权重随机选择一个 FreeMode 状态(旧版行为回补) fn select_random_free_state(&self) -> Result { let free_states: Vec<(&String, &StateConfig)> = self .config .states .iter() .filter(|(_, state)| matches!(state.mode, StateMode::FreeMode)) .collect(); if free_states.is_empty() { anyhow::bail!("没有可用的 FreeMode 状态"); } let total_weight: f32 = free_states.iter().map(|(_, state)| state.weight).sum(); if total_weight <= 0.0 { return Ok(free_states[0].0.clone()); } let mut cursor = rand::thread_rng().gen_range(0.0..total_weight); for (name, state) in &free_states { cursor -= state.weight; if cursor <= 0.0 { return Ok((*name).clone()); } } Ok(free_states.last().unwrap().0.clone()) } fn select_weighted_next_state(&self, entries: &[NextStateEntry]) -> Option { let valid_entries: Vec<&NextStateEntry> = entries.iter().filter(|entry| entry.weight > 0.0).collect(); if valid_entries.is_empty() { return None; } let total_weight: f32 = valid_entries.iter().map(|entry| entry.weight).sum(); if total_weight <= 0.0 { return None; } let mut cursor = rand::thread_rng().gen_range(0.0..total_weight); for entry in &valid_entries { cursor -= entry.weight; if cursor <= 0.0 { return Some(entry.state.clone()); } } valid_entries.last().map(|entry| entry.state.clone()) } fn matching_transition<'a>( &self, transitions: &'a [StateTransition], name: &str, value: &str, ) -> Option<&'a StateTransition> { transitions .iter() .filter(|transition| Self::trigger_matches(&transition.trigger, name, value)) .max_by_key(|transition| transition.priority) } fn trigger_matches(trigger: &TriggerType, name: &str, value: &str) -> bool { match trigger { TriggerType::Button { name: expected } => { (name == "button" && value == expected) || expected == name } TriggerType::Voice { keyword } => keyword == value || keyword == name, TriggerType::Sensor { name: expected } => { (name == "sensor" && value == expected) || expected == name } TriggerType::Timer { .. } | TriggerType::Random { .. } => false, } } } #[cfg(test)] mod tests { use super::StateMachine; use crate::core::config::{ AnimationStep, NextStateEntry, StateConfig, StateMachineConfig, StateMode, StateTransition, TriggerType, }; use std::collections::HashMap; #[test] fn button_and_sensor_triggers_match_legacy_message_shape() { let mut machine = StateMachine::new(config_with_states([ state( "idle", vec![step("idle")], vec![ transition( TriggerType::Button { name: "button1".to_string(), }, "button_state", 10, ), transition( TriggerType::Sensor { name: "touch".to_string(), }, "sensor_state", 10, ), ], ), state("button_state", vec![step("button")], vec![]), state("sensor_state", vec![step("sensor")], vec![]), ])); machine.start().expect("state machine should start"); assert!(machine .handle_trigger("button", "button1") .expect("button trigger should work")); assert_eq!(machine.current_state, "button_state"); machine.transition_to_state("idle").expect("reset to idle"); assert!(machine .handle_trigger("sensor", "touch") .expect("sensor trigger should work")); assert_eq!(machine.current_state, "sensor_state"); } #[test] fn deferred_trigger_waits_for_sequence_end_and_forces_single_remaining_loops() { let mut machine = StateMachine::new(config_with_states([ StateConfig { name: "idle".to_string(), mode: StateMode::FreeMode, sequence: vec![ AnimationStep { video_id: "looping".to_string(), loop_count: Some(3), random_loop_range: None, }, AnimationStep { video_id: "tail".to_string(), loop_count: Some(5), random_loop_range: None, }, ], next_state: None, next_states: None, transitions: vec![transition( TriggerType::Voice { keyword: "name".to_string(), }, "called", 10, )], weight: 1.0, defer_triggers: true, ignore_triggers: false, }, state("called", vec![step("called")], vec![]), ])); machine.start().expect("state machine should start"); assert_eq!(machine.current_loop_remaining, 3); assert!(!machine .handle_trigger("voice", "name") .expect("deferred trigger should queue")); assert!(machine.has_pending_trigger()); assert_eq!(machine.current_state, "idle"); assert_eq!(machine.current_loop_remaining, 1); // 当前 step 播完后,有 pending trigger 应立即跳转,跳过后续 step assert!(machine .on_video_completed() .expect("pending trigger should fire after current step ends")); assert_eq!(machine.current_state, "called"); assert_eq!(machine.current_video_id().as_deref(), Some("called")); assert!(!machine.has_pending_trigger()); } #[test] fn on_video_completed_returns_false_when_staying_in_same_state() { let mut machine = StateMachine::new(config_with_states([StateConfig { name: "idle".to_string(), mode: StateMode::FreeMode, sequence: vec![step("a"), step("b")], next_state: Some("idle".to_string()), next_states: None, transitions: vec![], weight: 1.0, defer_triggers: false, ignore_triggers: false, }])); machine.start().expect("state machine should start"); assert!(!machine .on_video_completed() .expect("step transition should stay in same state")); assert_eq!(machine.current_video_id().as_deref(), Some("b")); } #[test] fn next_states_take_priority_over_next_state() { let mut machine = StateMachine::new(config_with_states([ StateConfig { name: "idle".to_string(), mode: StateMode::FreeMode, sequence: vec![step("idle")], next_state: Some("fallback".to_string()), next_states: Some(vec![NextStateEntry { state: "weighted".to_string(), weight: 1.0, }]), transitions: vec![], weight: 1.0, defer_triggers: false, ignore_triggers: false, }, state("weighted", vec![step("weighted")], vec![]), state("fallback", vec![step("fallback")], vec![]), ])); machine.start().expect("state machine should start"); assert!(machine .on_video_completed() .expect("state should change via next_states")); assert_eq!(machine.current_state, "weighted"); } #[test] fn free_mode_random_walk_when_no_next_state_configured() { let mut machine = StateMachine::new(config_with_states([ StateConfig { name: "idle".to_string(), mode: StateMode::FreeMode, sequence: vec![step("idle")], next_state: None, next_states: None, transitions: vec![], weight: 2.0, defer_triggers: false, ignore_triggers: false, }, StateConfig { name: "walk".to_string(), mode: StateMode::FreeMode, sequence: vec![step("walk")], next_state: None, next_states: None, transitions: vec![], weight: 3.0, defer_triggers: false, ignore_triggers: false, }, StateConfig { name: "interactive".to_string(), mode: StateMode::InteractiveMode, sequence: vec![step("interactive")], next_state: None, next_states: None, transitions: vec![], weight: 1.0, defer_triggers: false, ignore_triggers: false, }, ])); machine.start().expect("state machine should start"); let mut visited_states = std::collections::HashSet::new(); for _ in 0..20 { machine .on_video_completed() .expect("should transition to random free state"); visited_states.insert(machine.current_state.clone()); // 验证只会跳转到 FreeMode 状态 assert!( machine.current_state == "idle" || machine.current_state == "walk", "应该只跳转到 FreeMode 状态,但跳转到了: {}", machine.current_state ); } // 验证随机性:20 次跳转应该至少访问过两个状态 assert!( visited_states.len() >= 2, "随机游走应该访问多个状态,但只访问了: {:?}", visited_states ); } #[test] fn interactive_mode_stays_in_same_state_when_no_next_state_configured() { let states = vec![StateConfig { name: "interactive".to_string(), mode: StateMode::InteractiveMode, sequence: vec![step("interactive")], next_state: None, next_states: None, transitions: vec![], weight: 1.0, defer_triggers: false, ignore_triggers: false, }]; let states_map = states .into_iter() .map(|state| { let name = state.name.clone(); (name, state) }) .collect::>(); let mut machine = StateMachine::new(StateMachineConfig { initial_state: "interactive".to_string(), states: states_map, }); machine.start().expect("state machine should start"); assert!(!machine .on_video_completed() .expect("interactive mode should stay in same state")); assert_eq!(machine.current_state, "interactive"); } fn config_with_states(states: [StateConfig; N]) -> StateMachineConfig { let states = Vec::from(states) .into_iter() .map(|state| { let name = state.name.clone(); (name, state) }) .collect::>(); StateMachineConfig { initial_state: "idle".to_string(), states, } } fn state( name: &str, sequence: Vec, transitions: Vec, ) -> StateConfig { StateConfig { name: name.to_string(), mode: StateMode::FreeMode, sequence, next_state: None, next_states: None, transitions, weight: 1.0, defer_triggers: false, ignore_triggers: false, } } fn step(video_id: &str) -> AnimationStep { AnimationStep { video_id: video_id.to_string(), loop_count: Some(1), random_loop_range: None, } } fn transition(trigger: TriggerType, target_state: &str, priority: i32) -> StateTransition { StateTransition { trigger, target_state: target_state.to_string(), priority, } } }