Skip to main content

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