solverforge_solver/termination/
move_count.rs

1//! Move count termination.
2
3use std::fmt::Debug;
4
5use solverforge_core::domain::PlanningSolution;
6
7use super::Termination;
8use crate::scope::SolverScope;
9
10/// Terminates when a maximum number of moves have been evaluated.
11///
12/// This termination condition requires a `StatisticsCollector` to be attached
13/// to the `SolverScope`. If no collector is attached, it will never terminate.
14///
15/// # Example
16///
17/// ```
18/// use solverforge_solver::termination::MoveCountTermination;
19/// use solverforge_core::score::SimpleScore;
20/// use solverforge_core::domain::PlanningSolution;
21///
22/// #[derive(Clone)]
23/// struct MySolution;
24/// impl PlanningSolution for MySolution {
25///     type Score = SimpleScore;
26///     fn score(&self) -> Option<Self::Score> { None }
27///     fn set_score(&mut self, _: Option<Self::Score>) {}
28/// }
29///
30/// // Terminate after evaluating 100,000 moves
31/// let termination = MoveCountTermination::<MySolution>::new(100_000);
32/// ```
33#[derive(Clone)]
34pub struct MoveCountTermination<S: PlanningSolution> {
35    /// Maximum number of moves before termination.
36    move_count_limit: u64,
37    _phantom: std::marker::PhantomData<fn() -> S>,
38}
39
40impl<S: PlanningSolution> Debug for MoveCountTermination<S> {
41    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
42        f.debug_struct("MoveCountTermination")
43            .field("move_count_limit", &self.move_count_limit)
44            .finish()
45    }
46}
47
48impl<S: PlanningSolution> MoveCountTermination<S> {
49    /// Creates a new move count termination.
50    ///
51    /// # Arguments
52    /// * `move_count_limit` - Maximum moves to evaluate before terminating
53    pub fn new(move_count_limit: u64) -> Self {
54        Self {
55            move_count_limit,
56            _phantom: std::marker::PhantomData,
57        }
58    }
59}
60
61impl<S: PlanningSolution> Termination<S> for MoveCountTermination<S> {
62    fn is_terminated(&self, solver_scope: &SolverScope<S>) -> bool {
63        if let Some(stats) = solver_scope.statistics() {
64            stats.current_moves_evaluated() >= self.move_count_limit
65        } else {
66            false // No statistics collector, never terminate based on this
67        }
68    }
69}
70
71#[cfg(test)]
72mod tests {
73    use super::*;
74    use crate::statistics::StatisticsCollector;
75    use solverforge_core::domain::{EntityDescriptor, SolutionDescriptor, TypedEntityExtractor};
76    use solverforge_core::score::SimpleScore;
77    use solverforge_scoring::SimpleScoreDirector;
78    use std::any::TypeId;
79    use std::sync::Arc;
80
81    #[derive(Clone, Debug)]
82    struct Entity {}
83
84    #[derive(Clone, Debug)]
85    struct TestSolution {
86        entities: Vec<Entity>,
87        score: Option<SimpleScore>,
88    }
89
90    impl PlanningSolution for TestSolution {
91        type Score = SimpleScore;
92        fn score(&self) -> Option<Self::Score> {
93            self.score
94        }
95        fn set_score(&mut self, score: Option<Self::Score>) {
96            self.score = score;
97        }
98    }
99
100    fn get_entities(s: &TestSolution) -> &Vec<Entity> {
101        &s.entities
102    }
103    fn get_entities_mut(s: &mut TestSolution) -> &mut Vec<Entity> {
104        &mut s.entities
105    }
106
107    fn create_scope_with_stats() -> SolverScope<TestSolution> {
108        let solution = TestSolution {
109            entities: vec![],
110            score: None,
111        };
112        let extractor = Box::new(TypedEntityExtractor::new(
113            "Entity",
114            "entities",
115            get_entities,
116            get_entities_mut,
117        ));
118        let entity_desc = EntityDescriptor::new("Entity", TypeId::of::<Entity>(), "entities")
119            .with_extractor(extractor);
120        let descriptor = SolutionDescriptor::new("TestSolution", TypeId::of::<TestSolution>())
121            .with_entity(entity_desc);
122        let director =
123            SimpleScoreDirector::with_calculator(solution, descriptor, |_| SimpleScore::of(0));
124        let collector = Arc::new(StatisticsCollector::<SimpleScore>::new());
125        SolverScope::new(Box::new(director)).with_statistics(collector)
126    }
127
128    #[test]
129    fn test_not_terminated_initially() {
130        let scope = create_scope_with_stats();
131        let termination = MoveCountTermination::<TestSolution>::new(100);
132        assert!(!termination.is_terminated(&scope));
133    }
134
135    #[test]
136    fn test_terminates_at_limit() {
137        let scope = create_scope_with_stats();
138        let stats = scope.statistics().unwrap().clone();
139
140        // Record 99 moves - not yet terminated
141        for _ in 0..99 {
142            stats.record_move_evaluated();
143        }
144        let termination = MoveCountTermination::<TestSolution>::new(100);
145        assert!(!termination.is_terminated(&scope));
146
147        // Record one more - now terminated
148        stats.record_move_evaluated();
149        assert!(termination.is_terminated(&scope));
150    }
151
152    #[test]
153    fn test_no_stats_never_terminates() {
154        let solution = TestSolution {
155            entities: vec![],
156            score: None,
157        };
158        let extractor = Box::new(TypedEntityExtractor::new(
159            "Entity",
160            "entities",
161            get_entities,
162            get_entities_mut,
163        ));
164        let entity_desc = EntityDescriptor::new("Entity", TypeId::of::<Entity>(), "entities")
165            .with_extractor(extractor);
166        let descriptor = SolutionDescriptor::new("TestSolution", TypeId::of::<TestSolution>())
167            .with_entity(entity_desc);
168        let director =
169            SimpleScoreDirector::with_calculator(solution, descriptor, |_| SimpleScore::of(0));
170        let scope = SolverScope::new(Box::new(director));
171
172        let termination = MoveCountTermination::<TestSolution>::new(1);
173        // Without statistics collector, never terminates
174        assert!(!termination.is_terminated(&scope));
175    }
176}