solverforge_solver/heuristic/move/
change.rs

1//! ChangeMove - assigns a value to a planning variable.
2//!
3//! This is the most fundamental move type. It takes a value and assigns
4//! it to a planning variable on an entity.
5//!
6//! # Zero-Erasure Design
7//!
8//! This move stores typed function pointers that operate directly on
9//! the solution. No `Arc<dyn>`, no `Box<dyn Any>`, no `downcast_ref`.
10
11use std::fmt::Debug;
12
13use solverforge_core::domain::PlanningSolution;
14use solverforge_scoring::ScoreDirector;
15
16use super::Move;
17
18/// A move that assigns a value to an entity's variable.
19///
20/// Stores typed function pointers for zero-erasure execution.
21/// No trait objects, no boxing - all operations are fully typed at compile time.
22///
23/// # Type Parameters
24/// * `S` - The planning solution type
25/// * `V` - The variable value type
26#[derive(Clone, Copy)]
27pub struct ChangeMove<S, V> {
28    entity_index: usize,
29    to_value: Option<V>,
30    getter: fn(&S, usize) -> Option<V>,
31    setter: fn(&mut S, usize, Option<V>),
32    variable_name: &'static str,
33    descriptor_index: usize,
34}
35
36impl<S, V: Debug> Debug for ChangeMove<S, V> {
37    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
38        f.debug_struct("ChangeMove")
39            .field("entity_index", &self.entity_index)
40            .field("descriptor_index", &self.descriptor_index)
41            .field("variable_name", &self.variable_name)
42            .field("to_value", &self.to_value)
43            .finish()
44    }
45}
46
47impl<S, V> ChangeMove<S, V> {
48    /// Creates a new change move with typed function pointers.
49    ///
50    /// # Arguments
51    /// * `entity_index` - Index of the entity in its collection
52    /// * `to_value` - The value to assign (None to unassign)
53    /// * `getter` - Function pointer to get current value from solution
54    /// * `setter` - Function pointer to set value on solution
55    /// * `variable_name` - Name of the variable (for debugging)
56    /// * `descriptor_index` - Index of the entity descriptor
57    pub fn new(
58        entity_index: usize,
59        to_value: Option<V>,
60        getter: fn(&S, usize) -> Option<V>,
61        setter: fn(&mut S, usize, Option<V>),
62        variable_name: &'static str,
63        descriptor_index: usize,
64    ) -> Self {
65        Self {
66            entity_index,
67            to_value,
68            getter,
69            setter,
70            variable_name,
71            descriptor_index,
72        }
73    }
74
75    /// Returns the entity index.
76    pub fn entity_index(&self) -> usize {
77        self.entity_index
78    }
79
80    /// Returns the target value.
81    pub fn to_value(&self) -> Option<&V> {
82        self.to_value.as_ref()
83    }
84
85    /// Returns the getter function pointer.
86    pub fn getter(&self) -> fn(&S, usize) -> Option<V> {
87        self.getter
88    }
89
90    /// Returns the setter function pointer.
91    pub fn setter(&self) -> fn(&mut S, usize, Option<V>) {
92        self.setter
93    }
94}
95
96impl<S, V> Move<S> for ChangeMove<S, V>
97where
98    S: PlanningSolution,
99    V: Clone + PartialEq + Send + Sync + Debug + 'static,
100{
101    fn is_doable(&self, score_director: &dyn ScoreDirector<S>) -> bool {
102        // Get current value using typed getter - no boxing, no downcast
103        let current = (self.getter)(score_director.working_solution(), self.entity_index);
104
105        // Compare directly - fully typed comparison
106        match (&current, &self.to_value) {
107            (None, None) => false,                      // Both unassigned
108            (Some(cur), Some(target)) => cur != target, // Different values
109            _ => true,                                  // One assigned, one not
110        }
111    }
112
113    fn do_move(&self, score_director: &mut dyn ScoreDirector<S>) {
114        // Capture old value using typed getter - zero erasure
115        let old_value = (self.getter)(score_director.working_solution(), self.entity_index);
116
117        // Notify before change
118        score_director.before_variable_changed(
119            self.descriptor_index,
120            self.entity_index,
121            self.variable_name,
122        );
123
124        // Set value using typed setter - no boxing
125        (self.setter)(
126            score_director.working_solution_mut(),
127            self.entity_index,
128            self.to_value.clone(),
129        );
130
131        // Notify after change
132        score_director.after_variable_changed(
133            self.descriptor_index,
134            self.entity_index,
135            self.variable_name,
136        );
137
138        // Register typed undo closure - zero erasure
139        let setter = self.setter;
140        let idx = self.entity_index;
141        score_director.register_undo(Box::new(move |s: &mut S| {
142            setter(s, idx, old_value);
143        }));
144    }
145
146    fn descriptor_index(&self) -> usize {
147        self.descriptor_index
148    }
149
150    fn entity_indices(&self) -> &[usize] {
151        std::slice::from_ref(&self.entity_index)
152    }
153
154    fn variable_name(&self) -> &str {
155        self.variable_name
156    }
157}
158
159#[cfg(test)]
160mod tests {
161    use super::*;
162    use solverforge_core::domain::SolutionDescriptor;
163    use solverforge_core::score::SimpleScore;
164    use solverforge_scoring::SimpleScoreDirector;
165    use std::any::TypeId;
166
167    #[derive(Clone, Debug, PartialEq)]
168    struct Task {
169        id: usize,
170        priority: Option<i32>,
171    }
172
173    #[derive(Clone, Debug)]
174    struct TaskSolution {
175        tasks: Vec<Task>,
176        score: Option<SimpleScore>,
177    }
178
179    impl PlanningSolution for TaskSolution {
180        type Score = SimpleScore;
181        fn score(&self) -> Option<Self::Score> {
182            self.score
183        }
184        fn set_score(&mut self, score: Option<Self::Score>) {
185            self.score = score;
186        }
187    }
188
189    // Typed getter: extracts priority from task at index
190    fn get_priority(s: &TaskSolution, i: usize) -> Option<i32> {
191        s.tasks.get(i).and_then(|t| t.priority)
192    }
193
194    // Typed setter: sets priority on task at index
195    fn set_priority(s: &mut TaskSolution, i: usize, v: Option<i32>) {
196        if let Some(task) = s.tasks.get_mut(i) {
197            task.priority = v;
198        }
199    }
200
201    fn create_director(
202        tasks: Vec<Task>,
203    ) -> SimpleScoreDirector<TaskSolution, impl Fn(&TaskSolution) -> SimpleScore> {
204        let solution = TaskSolution { tasks, score: None };
205        let descriptor = SolutionDescriptor::new("TaskSolution", TypeId::of::<TaskSolution>());
206        SimpleScoreDirector::with_calculator(solution, descriptor, |_| SimpleScore::of(0))
207    }
208
209    #[test]
210    fn test_change_move_is_doable() {
211        let tasks = vec![
212            Task {
213                id: 0,
214                priority: Some(1),
215            },
216            Task {
217                id: 1,
218                priority: Some(2),
219            },
220        ];
221        let director = create_director(tasks);
222
223        // Different value - doable
224        let m = ChangeMove::new(0, Some(5), get_priority, set_priority, "priority", 0);
225        assert!(m.is_doable(&director));
226
227        // Same value - not doable
228        let m = ChangeMove::new(0, Some(1), get_priority, set_priority, "priority", 0);
229        assert!(!m.is_doable(&director));
230    }
231
232    #[test]
233    fn test_change_move_do_move() {
234        let tasks = vec![Task {
235            id: 0,
236            priority: Some(1),
237        }];
238        let mut director = create_director(tasks);
239
240        let m = ChangeMove::new(0, Some(5), get_priority, set_priority, "priority", 0);
241        m.do_move(&mut director);
242
243        // Verify change using typed getter directly
244        let val = get_priority(director.working_solution(), 0);
245        assert_eq!(val, Some(5));
246    }
247
248    #[test]
249    fn test_change_move_to_none() {
250        let tasks = vec![Task {
251            id: 0,
252            priority: Some(5),
253        }];
254        let mut director = create_director(tasks);
255
256        let m = ChangeMove::new(0, None, get_priority, set_priority, "priority", 0);
257        assert!(m.is_doable(&director));
258
259        m.do_move(&mut director);
260
261        let val = get_priority(director.working_solution(), 0);
262        assert_eq!(val, None);
263    }
264
265    #[test]
266    fn test_change_move_entity_indices() {
267        let m = ChangeMove::new(3, Some(5), get_priority, set_priority, "priority", 0);
268        assert_eq!(m.entity_indices(), &[3]);
269    }
270
271    #[test]
272    fn test_change_move_clone() {
273        let m1 = ChangeMove::new(0, Some(5), get_priority, set_priority, "priority", 0);
274        let m2 = m1.clone();
275        assert_eq!(m1.entity_index, m2.entity_index);
276        assert_eq!(m1.to_value, m2.to_value);
277    }
278}