Skip to main content

solverforge_solver/heuristic/move/
pillar_change.rs

1//! PillarChangeMove - assigns a value to all entities in a pillar.
2//!
3//! A pillar is a group of entities that share the same variable value.
4//! This move changes all of them to a new value atomically.
5//!
6//! # Zero-Erasure Design
7//!
8//! PillarChangeMove uses typed function pointers instead of `dyn Any` for complete
9//! compile-time type safety. No runtime type checks or downcasting.
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 all entities in a pillar.
19///
20/// Stores entity indices and typed function pointers for zero-erasure access.
21/// Undo is handled by `RecordingScoreDirector`, not by this move.
22///
23/// # Type Parameters
24/// * `S` - The planning solution type
25/// * `V` - The variable value type
26pub struct PillarChangeMove<S, V> {
27    entity_indices: Vec<usize>,
28    descriptor_index: usize,
29    variable_name: &'static str,
30    to_value: Option<V>,
31    /// Typed getter function pointer - zero erasure.
32    getter: fn(&S, usize) -> Option<V>,
33    /// Typed setter function pointer - zero erasure.
34    setter: fn(&mut S, usize, Option<V>),
35}
36
37impl<S, V: Clone> Clone for PillarChangeMove<S, V> {
38    fn clone(&self) -> Self {
39        Self {
40            entity_indices: self.entity_indices.clone(),
41            descriptor_index: self.descriptor_index,
42            variable_name: self.variable_name,
43            to_value: self.to_value.clone(),
44            getter: self.getter,
45            setter: self.setter,
46        }
47    }
48}
49
50impl<S, V: Debug> Debug for PillarChangeMove<S, V> {
51    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
52        f.debug_struct("PillarChangeMove")
53            .field("entity_indices", &self.entity_indices)
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> PillarChangeMove<S, V> {
62    /// Creates a new pillar change move with typed function pointers.
63    ///
64    /// # Arguments
65    /// * `entity_indices` - Indices of entities in the pillar
66    /// * `to_value` - The new value to assign to all entities
67    /// * `getter` - Typed getter function pointer
68    /// * `setter` - Typed setter function pointer
69    /// * `variable_name` - Name of the variable being changed
70    /// * `descriptor_index` - Index in the entity descriptor
71    pub fn new(
72        entity_indices: Vec<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_indices,
81            descriptor_index,
82            variable_name,
83            to_value,
84            getter,
85            setter,
86        }
87    }
88
89    /// Returns the pillar size.
90    pub fn pillar_size(&self) -> usize {
91        self.entity_indices.len()
92    }
93
94    /// Returns the target value.
95    pub fn to_value(&self) -> Option<&V> {
96        self.to_value.as_ref()
97    }
98}
99
100impl<S, V> Move<S> for PillarChangeMove<S, V>
101where
102    S: PlanningSolution,
103    V: Clone + PartialEq + Send + Sync + Debug + 'static,
104{
105    fn is_doable<D: ScoreDirector<S>>(&self, score_director: &D) -> bool {
106        if self.entity_indices.is_empty() {
107            return false;
108        }
109
110        // Check first entity exists
111        let count = score_director.entity_count(self.descriptor_index);
112        if let Some(&first_idx) = self.entity_indices.first() {
113            if count.is_none_or(|c| first_idx >= c) {
114                return false;
115            }
116
117            // Get current value using typed getter - zero erasure
118            let current = (self.getter)(score_director.working_solution(), first_idx);
119
120            match (&current, &self.to_value) {
121                (None, None) => false,
122                (Some(cur), Some(target)) => cur != target,
123                _ => true,
124            }
125        } else {
126            false
127        }
128    }
129
130    fn do_move<D: ScoreDirector<S>>(&self, score_director: &mut D) {
131        // Capture old values using typed getter - zero erasure
132        let old_values: Vec<(usize, Option<V>)> = self
133            .entity_indices
134            .iter()
135            .map(|&idx| (idx, (self.getter)(score_director.working_solution(), idx)))
136            .collect();
137
138        // Notify before changes for all entities
139        for &idx in &self.entity_indices {
140            score_director.before_variable_changed(self.descriptor_index, idx, self.variable_name);
141        }
142
143        // Apply new value to all entities using typed setter - zero erasure
144        for &idx in &self.entity_indices {
145            (self.setter)(
146                score_director.working_solution_mut(),
147                idx,
148                self.to_value.clone(),
149            );
150        }
151
152        // Notify after changes
153        for &idx in &self.entity_indices {
154            score_director.after_variable_changed(self.descriptor_index, idx, self.variable_name);
155        }
156
157        // Register typed undo closure
158        let setter = self.setter;
159        score_director.register_undo(Box::new(move |s: &mut S| {
160            for (idx, old_value) in old_values {
161                setter(s, idx, old_value);
162            }
163        }));
164    }
165
166    fn descriptor_index(&self) -> usize {
167        self.descriptor_index
168    }
169
170    fn entity_indices(&self) -> &[usize] {
171        &self.entity_indices
172    }
173
174    fn variable_name(&self) -> &str {
175        self.variable_name
176    }
177}