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