Skip to main content

oxihuman_core/
state_machine_v2.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! A simple finite state machine with named states and guarded transitions.
6
7/// A guard function: returns true if the transition is allowed.
8pub type GuardFn = fn(&str, &str) -> bool;
9
10/// A transition definition.
11#[allow(dead_code)]
12#[derive(Clone)]
13pub struct Transition {
14    pub from: String,
15    pub to: String,
16    pub event: String,
17    pub guard: Option<GuardFn>,
18}
19
20/// A finite state machine with named states and transitions.
21#[allow(dead_code)]
22pub struct StateMachineV2 {
23    current: String,
24    states: Vec<String>,
25    transitions: Vec<Transition>,
26    history: Vec<String>,
27    fire_count: u64,
28}
29
30#[allow(dead_code)]
31impl StateMachineV2 {
32    pub fn new(initial: &str) -> Self {
33        Self {
34            current: initial.to_string(),
35            states: vec![initial.to_string()],
36            transitions: Vec::new(),
37            history: vec![initial.to_string()],
38            fire_count: 0,
39        }
40    }
41
42    pub fn add_state(&mut self, name: &str) {
43        if !self.states.contains(&name.to_string()) {
44            self.states.push(name.to_string());
45        }
46    }
47
48    pub fn add_transition(&mut self, from: &str, to: &str, event: &str) {
49        self.transitions.push(Transition {
50            from: from.to_string(),
51            to: to.to_string(),
52            event: event.to_string(),
53            guard: None,
54        });
55    }
56
57    pub fn add_guarded_transition(&mut self, from: &str, to: &str, event: &str, guard: GuardFn) {
58        self.transitions.push(Transition {
59            from: from.to_string(),
60            to: to.to_string(),
61            event: event.to_string(),
62            guard: Some(guard),
63        });
64    }
65
66    pub fn fire(&mut self, event: &str) -> bool {
67        let current = self.current.clone();
68        for t in &self.transitions {
69            if t.from != current || t.event != event {
70                continue;
71            }
72            let allowed = t.guard.is_none_or(|g| g(&current, &t.to));
73            if allowed {
74                self.current = t.to.clone();
75                self.history.push(t.to.clone());
76                self.fire_count += 1;
77                return true;
78            }
79        }
80        false
81    }
82
83    pub fn current(&self) -> &str {
84        &self.current
85    }
86
87    pub fn state_count(&self) -> usize {
88        self.states.len()
89    }
90
91    pub fn transition_count(&self) -> usize {
92        self.transitions.len()
93    }
94
95    pub fn history(&self) -> &[String] {
96        &self.history
97    }
98
99    pub fn fire_count(&self) -> u64 {
100        self.fire_count
101    }
102
103    pub fn is_in(&self, state: &str) -> bool {
104        self.current == state
105    }
106
107    pub fn can_fire(&self, event: &str) -> bool {
108        let current = &self.current;
109        self.transitions.iter().any(|t| {
110            t.from == *current && t.event == event && t.guard.is_none_or(|g| g(current, &t.to))
111        })
112    }
113
114    pub fn reachable_events(&self) -> Vec<String> {
115        let current = &self.current;
116        let mut evts: Vec<String> = self
117            .transitions
118            .iter()
119            .filter(|t| t.from == *current)
120            .map(|t| t.event.clone())
121            .collect();
122        evts.sort();
123        evts.dedup();
124        evts
125    }
126}
127
128pub fn new_state_machine(initial: &str) -> StateMachineV2 {
129    StateMachineV2::new(initial)
130}
131
132pub fn sm_add_state(sm: &mut StateMachineV2, name: &str) {
133    sm.add_state(name);
134}
135
136pub fn sm_add_transition(sm: &mut StateMachineV2, from: &str, to: &str, event: &str) {
137    sm.add_transition(from, to, event);
138}
139
140pub fn sm_fire(sm: &mut StateMachineV2, event: &str) -> bool {
141    sm.fire(event)
142}
143
144pub fn sm_current(sm: &StateMachineV2) -> &str {
145    sm.current()
146}
147
148#[cfg(test)]
149mod tests {
150    use super::*;
151
152    fn traffic_light() -> StateMachineV2 {
153        let mut sm = new_state_machine("red");
154        sm.add_state("green");
155        sm.add_state("yellow");
156        sm_add_transition(&mut sm, "red", "green", "go");
157        sm_add_transition(&mut sm, "green", "yellow", "slow");
158        sm_add_transition(&mut sm, "yellow", "red", "stop");
159        sm
160    }
161
162    #[test]
163    fn initial_state() {
164        let sm = traffic_light();
165        assert_eq!(sm_current(&sm), "red");
166    }
167
168    #[test]
169    fn valid_transition() {
170        let mut sm = traffic_light();
171        assert!(sm_fire(&mut sm, "go"));
172        assert_eq!(sm_current(&sm), "green");
173    }
174
175    #[test]
176    fn invalid_event_returns_false() {
177        let mut sm = traffic_light();
178        assert!(!sm_fire(&mut sm, "stop"));
179        assert_eq!(sm_current(&sm), "red");
180    }
181
182    #[test]
183    fn full_cycle() {
184        let mut sm = traffic_light();
185        sm_fire(&mut sm, "go");
186        sm_fire(&mut sm, "slow");
187        sm_fire(&mut sm, "stop");
188        assert!(sm.is_in("red"));
189    }
190
191    #[test]
192    fn history_tracks_states() {
193        let mut sm = traffic_light();
194        sm_fire(&mut sm, "go");
195        sm_fire(&mut sm, "slow");
196        assert_eq!(sm.history().len(), 3);
197    }
198
199    #[test]
200    fn fire_count() {
201        let mut sm = traffic_light();
202        sm_fire(&mut sm, "go");
203        sm_fire(&mut sm, "slow");
204        assert_eq!(sm.fire_count(), 2);
205    }
206
207    #[test]
208    fn can_fire() {
209        let sm = traffic_light();
210        assert!(sm.can_fire("go"));
211        assert!(!sm.can_fire("slow"));
212    }
213
214    #[test]
215    fn reachable_events() {
216        let sm = traffic_light();
217        assert_eq!(sm.reachable_events(), vec!["go".to_string()]);
218    }
219
220    #[test]
221    fn guarded_transition_blocked() {
222        let mut sm = new_state_machine("idle");
223        sm.add_state("active");
224        sm.add_guarded_transition("idle", "active", "start", |_, _| false);
225        assert!(!sm_fire(&mut sm, "start"));
226        assert!(sm.is_in("idle"));
227    }
228
229    #[test]
230    fn state_count() {
231        let sm = traffic_light();
232        assert_eq!(sm.state_count(), 3);
233    }
234}