solverforge_core/score/
bendable.rs

1//! BendableScore - Compile-time configurable multi-level score
2//!
3//! Uses const generics for zero-erasure. Level counts are determined at compile time.
4
5use std::cmp::Ordering;
6use std::fmt;
7use std::ops::{Add, Neg, Sub};
8
9use super::traits::Score;
10
11/// A score with a configurable number of hard and soft levels.
12///
13/// Level counts are const generic parameters, enabling Copy and zero heap allocation.
14///
15/// # Type Parameters
16///
17/// * `H` - Number of hard score levels
18/// * `S` - Number of soft score levels
19///
20/// # Examples
21///
22/// ```
23/// use solverforge_core::score::{BendableScore, Score};
24///
25/// // Create a score with 2 hard levels and 3 soft levels
26/// let score: BendableScore<2, 3> = BendableScore::of([-1, -2], [-10, -20, -30]);
27///
28/// assert_eq!(score.hard_levels_count(), 2);
29/// assert_eq!(score.soft_levels_count(), 3);
30/// assert!(!score.is_feasible());  // Negative hard scores
31/// ```
32#[derive(Clone, Copy, PartialEq, Eq, Hash)]
33pub struct BendableScore<const H: usize, const S: usize> {
34    hard: [i64; H],
35    soft: [i64; S],
36}
37
38impl<const H: usize, const S: usize> BendableScore<H, S> {
39    /// Creates a new BendableScore with the given hard and soft score arrays.
40    pub const fn of(hard: [i64; H], soft: [i64; S]) -> Self {
41        BendableScore { hard, soft }
42    }
43
44    /// Creates a zero score.
45    pub const fn zero() -> Self {
46        BendableScore {
47            hard: [0; H],
48            soft: [0; S],
49        }
50    }
51
52    /// Returns the number of hard score levels.
53    pub const fn hard_levels_count(&self) -> usize {
54        H
55    }
56
57    /// Returns the number of soft score levels.
58    pub const fn soft_levels_count(&self) -> usize {
59        S
60    }
61
62    /// Returns the hard score at the given level.
63    ///
64    /// # Panics
65    /// Panics if the level is out of bounds.
66    pub const fn hard_score(&self, level: usize) -> i64 {
67        self.hard[level]
68    }
69
70    /// Returns the soft score at the given level.
71    ///
72    /// # Panics
73    /// Panics if the level is out of bounds.
74    pub const fn soft_score(&self, level: usize) -> i64 {
75        self.soft[level]
76    }
77
78    /// Returns all hard scores as a slice.
79    pub const fn hard_scores(&self) -> &[i64; H] {
80        &self.hard
81    }
82
83    /// Returns all soft scores as a slice.
84    pub const fn soft_scores(&self) -> &[i64; S] {
85        &self.soft
86    }
87
88    /// Creates a score with a single hard level penalty at the given index.
89    pub const fn one_hard(level: usize) -> Self {
90        let mut hard = [0; H];
91        hard[level] = 1;
92        BendableScore { hard, soft: [0; S] }
93    }
94
95    /// Creates a score with a single soft level penalty at the given index.
96    pub const fn one_soft(level: usize) -> Self {
97        let mut soft = [0; S];
98        soft[level] = 1;
99        BendableScore { hard: [0; H], soft }
100    }
101}
102
103impl<const H: usize, const S: usize> Default for BendableScore<H, S> {
104    fn default() -> Self {
105        Self::zero()
106    }
107}
108
109impl<const H: usize, const S: usize> Score for BendableScore<H, S> {
110    fn is_feasible(&self) -> bool {
111        self.hard.iter().all(|&s| s >= 0)
112    }
113
114    fn zero() -> Self {
115        BendableScore::zero()
116    }
117
118    fn levels_count() -> usize {
119        H + S
120    }
121
122    fn to_level_numbers(&self) -> Vec<i64> {
123        let mut levels = Vec::with_capacity(H + S);
124        levels.extend_from_slice(&self.hard);
125        levels.extend_from_slice(&self.soft);
126        levels
127    }
128
129    fn from_level_numbers(levels: &[i64]) -> Self {
130        assert!(levels.len() >= H + S, "Not enough levels provided");
131        let mut hard = [0; H];
132        let mut soft = [0; S];
133        hard.copy_from_slice(&levels[..H]);
134        soft.copy_from_slice(&levels[H..H + S]);
135        BendableScore { hard, soft }
136    }
137
138    fn multiply(&self, multiplicand: f64) -> Self {
139        let mut hard = [0; H];
140        let mut soft = [0; S];
141        for (i, item) in hard.iter_mut().enumerate().take(H) {
142            *item = (self.hard[i] as f64 * multiplicand).round() as i64;
143        }
144        for (i, item) in soft.iter_mut().enumerate().take(S) {
145            *item = (self.soft[i] as f64 * multiplicand).round() as i64;
146        }
147        BendableScore { hard, soft }
148    }
149
150    fn divide(&self, divisor: f64) -> Self {
151        let mut hard = [0; H];
152        let mut soft = [0; S];
153        for (i, item) in hard.iter_mut().enumerate().take(H) {
154            *item = (self.hard[i] as f64 / divisor).round() as i64;
155        }
156        for (i, item) in soft.iter_mut().enumerate().take(S) {
157            *item = (self.soft[i] as f64 / divisor).round() as i64;
158        }
159        BendableScore { hard, soft }
160    }
161
162    fn abs(&self) -> Self {
163        let mut hard = [0; H];
164        let mut soft = [0; S];
165        for (i, item) in hard.iter_mut().enumerate().take(H) {
166            *item = self.hard[i].abs();
167        }
168        for (i, item) in soft.iter_mut().enumerate().take(S) {
169            *item = self.soft[i].abs();
170        }
171        BendableScore { hard, soft }
172    }
173}
174
175impl<const H: usize, const S: usize> Ord for BendableScore<H, S> {
176    fn cmp(&self, other: &Self) -> Ordering {
177        // Compare hard scores first (highest priority first)
178        for i in 0..H {
179            match self.hard[i].cmp(&other.hard[i]) {
180                Ordering::Equal => continue,
181                ord => return ord,
182            }
183        }
184
185        // Then compare soft scores
186        for i in 0..S {
187            match self.soft[i].cmp(&other.soft[i]) {
188                Ordering::Equal => continue,
189                ord => return ord,
190            }
191        }
192
193        Ordering::Equal
194    }
195}
196
197impl<const H: usize, const S: usize> PartialOrd for BendableScore<H, S> {
198    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
199        Some(self.cmp(other))
200    }
201}
202
203impl<const H: usize, const S: usize> Add for BendableScore<H, S> {
204    type Output = Self;
205
206    fn add(self, other: Self) -> Self {
207        let mut hard = [0; H];
208        let mut soft = [0; S];
209        for (i, item) in hard.iter_mut().enumerate().take(H) {
210            *item = self.hard[i] + other.hard[i];
211        }
212        for (i, item) in soft.iter_mut().enumerate().take(S) {
213            *item = self.soft[i] + other.soft[i];
214        }
215        BendableScore { hard, soft }
216    }
217}
218
219impl<const H: usize, const S: usize> Sub for BendableScore<H, S> {
220    type Output = Self;
221
222    fn sub(self, other: Self) -> Self {
223        let mut hard = [0; H];
224        let mut soft = [0; S];
225        for (i, item) in hard.iter_mut().enumerate().take(H) {
226            *item = self.hard[i] - other.hard[i];
227        }
228        for (i, item) in soft.iter_mut().enumerate().take(S) {
229            *item = self.soft[i] - other.soft[i];
230        }
231        BendableScore { hard, soft }
232    }
233}
234
235impl<const H: usize, const S: usize> Neg for BendableScore<H, S> {
236    type Output = Self;
237
238    fn neg(self) -> Self {
239        let mut hard = [0; H];
240        let mut soft = [0; S];
241        for (i, item) in hard.iter_mut().enumerate().take(H) {
242            *item = -self.hard[i];
243        }
244        for (i, item) in soft.iter_mut().enumerate().take(S) {
245            *item = -self.soft[i];
246        }
247        BendableScore { hard, soft }
248    }
249}
250
251impl<const H: usize, const S: usize> fmt::Debug for BendableScore<H, S> {
252    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
253        write!(
254            f,
255            "BendableScore(hard: {:?}, soft: {:?})",
256            self.hard, self.soft
257        )
258    }
259}
260
261impl<const H: usize, const S: usize> fmt::Display for BendableScore<H, S> {
262    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
263        let hard_str: Vec<String> = self.hard.iter().map(|s| s.to_string()).collect();
264        let soft_str: Vec<String> = self.soft.iter().map(|s| s.to_string()).collect();
265
266        write!(
267            f,
268            "[{}]hard/[{}]soft",
269            hard_str.join("/"),
270            soft_str.join("/")
271        )
272    }
273}
274
275#[cfg(test)]
276mod tests {
277    use super::*;
278
279    #[test]
280    fn test_creation() {
281        let score: BendableScore<2, 3> = BendableScore::of([-1, -2], [-10, -20, -30]);
282        assert_eq!(score.hard_levels_count(), 2);
283        assert_eq!(score.soft_levels_count(), 3);
284        assert_eq!(score.hard_score(0), -1);
285        assert_eq!(score.hard_score(1), -2);
286        assert_eq!(score.soft_score(2), -30);
287    }
288
289    #[test]
290    fn test_feasibility() {
291        let feasible: BendableScore<2, 2> = BendableScore::of([0, 0], [-10, -20]);
292        let infeasible: BendableScore<2, 2> = BendableScore::of([0, -1], [0, 0]);
293
294        assert!(feasible.is_feasible());
295        assert!(!infeasible.is_feasible());
296    }
297
298    #[test]
299    fn test_comparison() {
300        // First hard level dominates
301        let s1: BendableScore<2, 1> = BendableScore::of([-1, 0], [0]);
302        let s2: BendableScore<2, 1> = BendableScore::of([0, -100], [-1000]);
303        assert!(s2 > s1);
304
305        // Second hard level matters when first is equal
306        let s3: BendableScore<2, 1> = BendableScore::of([0, -10], [0]);
307        let s4: BendableScore<2, 1> = BendableScore::of([0, -5], [-100]);
308        assert!(s4 > s3);
309    }
310
311    #[test]
312    fn test_arithmetic() {
313        let s1: BendableScore<1, 2> = BendableScore::of([-1], [-10, -20]);
314        let s2: BendableScore<1, 2> = BendableScore::of([-2], [-5, -10]);
315
316        let sum = s1 + s2;
317        assert_eq!(sum.hard_scores(), &[-3]);
318        assert_eq!(sum.soft_scores(), &[-15, -30]);
319
320        let neg = -s1;
321        assert_eq!(neg.hard_scores(), &[1]);
322        assert_eq!(neg.soft_scores(), &[10, 20]);
323    }
324
325    #[test]
326    fn test_copy() {
327        let s1: BendableScore<1, 1> = BendableScore::of([-1], [-10]);
328        let s2 = s1; // Copy
329        assert_eq!(s1, s2); // s1 still valid
330    }
331}