Skip to main content

rosu_pp/catch/performance/hitresult_generator/
ignore_acc.rs

1use std::cmp;
2
3use crate::{
4    any::{HitResultGenerator, hitresult_generator::IgnoreAccuracy},
5    catch::{Catch, CatchHitResults, performance::inspect::InspectCatchPerformance},
6};
7
8impl HitResultGenerator<Catch> for IgnoreAccuracy {
9    fn generate_hitresults(inspect: InspectCatchPerformance<'_>) -> CatchHitResults {
10        let n_fruits = inspect.attrs.n_fruits;
11        let n_droplets = inspect.attrs.n_droplets;
12        let n_tiny_droplets = inspect.attrs.n_tiny_droplets;
13
14        // Get misses (clamped to fruits+droplets pool)
15        let misses = inspect.misses();
16
17        // Available catches from fruit/droplet pool
18        let mut fruit_droplet_remain = (n_fruits + n_droplets).saturating_sub(misses);
19
20        // Available from tiny droplet pool
21        let mut tiny_droplet_remain = n_tiny_droplets;
22
23        // Helper to assign a specified value from the fruit/droplet pool
24        let mut assign_fruit_droplet = |specified: Option<u32>, max: u32| -> Option<u32> {
25            let value = specified?;
26            let assigned = cmp::min(cmp::min(value, max), fruit_droplet_remain);
27            fruit_droplet_remain = fruit_droplet_remain.saturating_sub(assigned);
28
29            Some(assigned)
30        };
31
32        // Helper to assign from tiny droplet pool
33        let mut assign_tiny_droplet = |specified: Option<u32>| -> Option<u32> {
34            let value = specified?;
35            let assigned = cmp::min(value, tiny_droplet_remain);
36            tiny_droplet_remain = tiny_droplet_remain.saturating_sub(assigned);
37
38            Some(assigned)
39        };
40
41        // First pass: assign specified values in priority order (fruits > droplets > tiny_droplets)
42        let fruits = assign_fruit_droplet(inspect.fruits, n_fruits);
43        let droplets = assign_fruit_droplet(inspect.droplets, n_droplets);
44        let tiny_droplets = assign_tiny_droplet(inspect.tiny_droplets);
45        let tiny_droplet_misses = assign_tiny_droplet(inspect.tiny_droplet_misses);
46
47        // Second pass: fill first unspecified with remainder
48        let fruits = fruits.unwrap_or_else(|| {
49            let take = cmp::min(fruit_droplet_remain, n_fruits);
50            fruit_droplet_remain = fruit_droplet_remain.saturating_sub(take);
51
52            take
53        });
54
55        let droplets = droplets.unwrap_or_else(|| {
56            let take = cmp::min(fruit_droplet_remain, n_droplets);
57            fruit_droplet_remain = fruit_droplet_remain.saturating_sub(take);
58
59            take
60        });
61
62        let tiny_droplets = tiny_droplets.unwrap_or_else(|| {
63            let take = tiny_droplet_remain;
64            tiny_droplet_remain = 0;
65
66            take
67        });
68
69        let tiny_droplet_misses = tiny_droplet_misses.unwrap_or(tiny_droplet_remain);
70
71        // Enforce pool constraints with priority
72        // Fruit/droplet pool: misses > fruits > droplets
73        let pool_total = n_fruits + n_droplets;
74        let current_sum = fruits + droplets + misses;
75
76        let (fruits, droplets) = match current_sum.cmp(&pool_total) {
77            cmp::Ordering::Less => {
78                // Need to add more - prioritize droplets (lower priority)
79                let needed = pool_total - current_sum;
80                let new_droplets = cmp::min(droplets + needed, n_droplets);
81                let still_needed = pool_total.saturating_sub(fruits + new_droplets + misses);
82                let new_fruits = cmp::min(fruits + still_needed, n_fruits);
83
84                (new_fruits, new_droplets)
85            }
86            cmp::Ordering::Equal => (fruits, droplets),
87            cmp::Ordering::Greater => {
88                // Have too many - reduce droplets first (lower priority)
89                let excess = current_sum - pool_total;
90                let new_droplets = droplets.saturating_sub(excess);
91                let still_excess = (fruits + new_droplets + misses).saturating_sub(pool_total);
92                let new_fruits = fruits.saturating_sub(still_excess);
93
94                (new_fruits, new_droplets)
95            }
96        };
97
98        // Tiny droplet pool: tiny_droplets > tiny_droplet_misses
99        let tiny_pool_total = n_tiny_droplets;
100        let tiny_current_sum = tiny_droplets + tiny_droplet_misses;
101
102        let (tiny_droplets, tiny_droplet_misses) = match tiny_current_sum.cmp(&tiny_pool_total) {
103            cmp::Ordering::Less => {
104                // Need to add more - prioritize tiny_droplets (higher priority)
105                let needed = tiny_pool_total - tiny_current_sum;
106                let new_tiny_droplets = cmp::min(tiny_droplets + needed, n_tiny_droplets);
107                let still_needed = tiny_pool_total.saturating_sub(new_tiny_droplets);
108
109                (new_tiny_droplets, still_needed)
110            }
111            cmp::Ordering::Equal => (tiny_droplets, tiny_droplet_misses),
112            cmp::Ordering::Greater => {
113                // Have too many - reduce tiny_droplet_misses first (lower priority)
114                let excess = tiny_current_sum - tiny_pool_total;
115                let new_tiny_droplet_misses = tiny_droplet_misses.saturating_sub(excess);
116                let still_excess =
117                    (tiny_droplets + new_tiny_droplet_misses).saturating_sub(tiny_pool_total);
118                let new_tiny_droplets = tiny_droplets.saturating_sub(still_excess);
119
120                (new_tiny_droplets, new_tiny_droplet_misses)
121            }
122        };
123
124        CatchHitResults {
125            fruits,
126            droplets,
127            tiny_droplets,
128            tiny_droplet_misses,
129            misses,
130        }
131    }
132}
133
134#[cfg(test)]
135mod tests {
136    use crate::{Difficulty, catch::CatchDifficultyAttributes};
137
138    use super::*;
139
140    #[test]
141    fn all_missing() {
142        const N_FRUITS: u32 = 50;
143        const N_DROPLETS: u32 = 25;
144        const N_TINY_DROPLETS: u32 = 100;
145
146        let inspect = InspectCatchPerformance {
147            attrs: &CatchDifficultyAttributes {
148                n_fruits: N_FRUITS,
149                n_droplets: N_DROPLETS,
150                n_tiny_droplets: N_TINY_DROPLETS,
151                ..Default::default()
152            },
153            difficulty: &Difficulty::new(),
154            acc: None,
155            combo: None,
156            fruits: None,
157            droplets: None,
158            tiny_droplets: None,
159            tiny_droplet_misses: None,
160            misses: Some(5),
161        };
162
163        let result = <IgnoreAccuracy as HitResultGenerator<Catch>>::generate_hitresults(inspect);
164
165        // Priority: fruits, then droplets, then tiny_droplets
166        // With 5 misses, we have 70 slots for fruits+droplets
167        assert_eq!(result.fruits, N_FRUITS);
168        assert_eq!(result.droplets, 20); // 70 - 50 = 20
169        assert_eq!(result.tiny_droplets, N_TINY_DROPLETS);
170        assert_eq!(result.tiny_droplet_misses, 0);
171        assert_eq!(result.misses, 5);
172        assert_eq!(result.total_hits(), N_FRUITS + N_DROPLETS + N_TINY_DROPLETS);
173    }
174
175    #[test]
176    fn some_provided() {
177        const N_FRUITS: u32 = 50;
178        const N_DROPLETS: u32 = 25;
179        const N_TINY_DROPLETS: u32 = 100;
180
181        let inspect = InspectCatchPerformance {
182            attrs: &CatchDifficultyAttributes {
183                n_fruits: N_FRUITS,
184                n_droplets: N_DROPLETS,
185                n_tiny_droplets: N_TINY_DROPLETS,
186                ..Default::default()
187            },
188            difficulty: &Difficulty::new(),
189            acc: None,
190            combo: None,
191            fruits: Some(30),
192            droplets: None,
193            tiny_droplets: Some(50),
194            tiny_droplet_misses: None,
195            misses: Some(10),
196        };
197
198        let result = <IgnoreAccuracy as HitResultGenerator<Catch>>::generate_hitresults(inspect);
199
200        // fruits=30, tiny_droplets=50 provided
201        // Fruit/droplet pool: 75 - 10 misses = 65 available
202        // fruits=30 is unsatisfiable
203        // -> filling up 25 droplets (its max) still leaves 10
204        // -> increment fruits to 40
205        assert_eq!(result.fruits, 40);
206        assert_eq!(result.droplets, 25);
207        assert_eq!(result.tiny_droplets, 50);
208        assert_eq!(result.tiny_droplet_misses, 50); // 100 - 50 = 50
209        assert_eq!(result.misses, 10);
210    }
211
212    #[test]
213    fn droplets_provided() {
214        const N_FRUITS: u32 = 50;
215        const N_DROPLETS: u32 = 25;
216        const N_TINY_DROPLETS: u32 = 100;
217
218        let inspect = InspectCatchPerformance {
219            attrs: &CatchDifficultyAttributes {
220                n_fruits: N_FRUITS,
221                n_droplets: N_DROPLETS,
222                n_tiny_droplets: N_TINY_DROPLETS,
223                ..Default::default()
224            },
225            difficulty: &Difficulty::new(),
226            acc: None,
227            combo: None,
228            fruits: None,
229            droplets: Some(15),
230            tiny_droplets: None,
231            tiny_droplet_misses: Some(80),
232            misses: Some(8),
233        };
234
235        let result = <IgnoreAccuracy as HitResultGenerator<Catch>>::generate_hitresults(inspect);
236
237        // droplets=15, tiny_droplet_misses=80 provided
238        // Fruit/droplet pool: 75 - 8 misses = 67 available
239        // droplets=15 is unsatisfiable
240        // -> filling up 50 fruits (its max) still leaves 2
241        // -> increment droplets to 17
242        assert_eq!(result.fruits, 50);
243        assert_eq!(result.droplets, 17);
244        assert_eq!(result.tiny_droplets, 20); // 100 - 80 = 20
245        assert_eq!(result.tiny_droplet_misses, 80);
246        assert_eq!(result.misses, 8);
247    }
248
249    #[test]
250    fn all_provided() {
251        const N_FRUITS: u32 = 40;
252        const N_DROPLETS: u32 = 20;
253        const N_TINY_DROPLETS: u32 = 80;
254
255        let inspect = InspectCatchPerformance {
256            attrs: &CatchDifficultyAttributes {
257                n_fruits: N_FRUITS,
258                n_droplets: N_DROPLETS,
259                n_tiny_droplets: N_TINY_DROPLETS,
260                ..Default::default()
261            },
262            difficulty: &Difficulty::new(),
263            acc: None,
264            combo: None,
265            fruits: Some(35),
266            droplets: Some(18),
267            tiny_droplets: Some(70),
268            tiny_droplet_misses: Some(10),
269            misses: Some(3),
270        };
271
272        let result = <IgnoreAccuracy as HitResultGenerator<Catch>>::generate_hitresults(inspect);
273
274        // Pool constraints will be enforced:
275        // Fruit/droplet pool: 40 + 20 = 60, provided sum: 35 + 18 + 3 = 56 (missing 4)
276        // Priority: misses > fruits > droplets, so adjust droplets: 18 + 4 = 22
277        // But n_droplets = 20, so droplets = 20, still need 2 more
278        // So adjust fruits: 35 + 2 = 37
279        assert_eq!(result.fruits, 37);
280        assert_eq!(result.droplets, 20);
281        assert_eq!(result.misses, 3);
282
283        // Tiny droplet pool: 80, provided sum: 70 + 10 = 80 (correct)
284        assert_eq!(result.tiny_droplets, 70);
285        assert_eq!(result.tiny_droplet_misses, 10);
286    }
287
288    #[test]
289    fn no_misses() {
290        const N_FRUITS: u32 = 30;
291        const N_DROPLETS: u32 = 15;
292        const N_TINY_DROPLETS: u32 = 60;
293
294        let inspect = InspectCatchPerformance {
295            attrs: &CatchDifficultyAttributes {
296                n_fruits: N_FRUITS,
297                n_droplets: N_DROPLETS,
298                n_tiny_droplets: N_TINY_DROPLETS,
299                ..Default::default()
300            },
301            difficulty: &Difficulty::new(),
302            acc: None,
303            combo: None,
304            fruits: None,
305            droplets: None,
306            tiny_droplets: None,
307            tiny_droplet_misses: None,
308            misses: Some(0),
309        };
310
311        let result = <IgnoreAccuracy as HitResultGenerator<Catch>>::generate_hitresults(inspect);
312
313        // No misses: all fruits, all droplets, all tiny_droplets
314        assert_eq!(result.fruits, N_FRUITS);
315        assert_eq!(result.droplets, N_DROPLETS);
316        assert_eq!(result.tiny_droplets, N_TINY_DROPLETS);
317        assert_eq!(result.tiny_droplet_misses, 0);
318        assert_eq!(result.misses, 0);
319    }
320
321    #[test]
322    fn excess_values_clamped() {
323        const N_FRUITS: u32 = 20;
324        const N_DROPLETS: u32 = 10;
325        const N_TINY_DROPLETS: u32 = 40;
326
327        let inspect = InspectCatchPerformance {
328            attrs: &CatchDifficultyAttributes {
329                n_fruits: N_FRUITS,
330                n_droplets: N_DROPLETS,
331                n_tiny_droplets: N_TINY_DROPLETS,
332                ..Default::default()
333            },
334            difficulty: &Difficulty::new(),
335            acc: None,
336            combo: None,
337            fruits: Some(100), // Way more than available
338            droplets: Some(50),
339            tiny_droplets: Some(200),
340            tiny_droplet_misses: Some(100),
341            misses: Some(5),
342        };
343
344        let result = <IgnoreAccuracy as HitResultGenerator<Catch>>::generate_hitresults(inspect);
345
346        // Values should be clamped to available space
347        // Fruit/droplet pool: 30 - 5 = 25 available
348        // fruits tries to take 100, gets clamped to min(100, 20, 25) = 20
349        // That leaves 5 for droplets, so droplets gets min(50, 10, 5) = 5
350        assert_eq!(result.fruits, 20);
351        assert_eq!(result.droplets, 5);
352        assert_eq!(result.tiny_droplets, 40); // min(200, 40)
353        assert_eq!(result.tiny_droplet_misses, 0); // No space left in tiny pool
354        assert_eq!(result.misses, 5);
355    }
356
357    #[test]
358    fn missing_objects() {
359        const N_FRUITS: u32 = 728;
360        const N_DROPLETS: u32 = 2;
361        const N_TINY_DROPLETS: u32 = 263;
362
363        let inspect = InspectCatchPerformance {
364            attrs: &CatchDifficultyAttributes {
365                n_fruits: N_FRUITS,
366                n_droplets: N_DROPLETS,
367                n_tiny_droplets: N_TINY_DROPLETS,
368                ..Default::default()
369            },
370            difficulty: &Difficulty::new(),
371            acc: None,
372            combo: None,
373            fruits: Some(N_FRUITS - 10),
374            droplets: Some(N_DROPLETS - 1),
375            tiny_droplets: Some(N_TINY_DROPLETS - 50),
376            tiny_droplet_misses: Some(20),
377            misses: Some(2),
378        };
379
380        let result = <IgnoreAccuracy as HitResultGenerator<Catch>>::generate_hitresults(inspect);
381
382        // Enforcing pool constraints:
383        // Fruit/droplet pool: 728 + 2 = 730, provided sum: 718 + 1 + 2 = 721 (missing 9)
384        // Priority: misses > fruits > droplets
385        // - droplets are capped to 2 so we assign 1 + 1 but have 8 left
386        // - fruits are capped to 728 so we can assign 8 + 718 = 726
387        assert_eq!(result.fruits, 726);
388        assert_eq!(result.droplets, 2);
389
390        // Tiny droplet pool: 263, provided sum: 213 + 20 = 233 (missing 30)
391        // Priority: tiny_droplets_misses > tiny_droplet, so adjust tiny_droplets:
392        //   213 + 30 = N_TINY_DROPLETS - 20
393        assert_eq!(result.tiny_droplets, 243);
394        assert_eq!(result.tiny_droplet_misses, 20);
395        assert_eq!(result.misses, 2);
396    }
397}