Skip to main content

rosu_pp/catch/performance/hitresult_generator/
fast.rs

1use std::cmp;
2
3use crate::{
4    any::{
5        HitResultGenerator,
6        hitresult_generator::{Fast, IgnoreAccuracy},
7    },
8    catch::{Catch, CatchHitResults, performance::inspect::InspectCatchPerformance},
9};
10
11impl HitResultGenerator<Catch> for Fast {
12    #[expect(clippy::too_many_lines, reason = "it is what it is /shrug")]
13    fn generate_hitresults(inspect: InspectCatchPerformance<'_>) -> CatchHitResults {
14        let Some(acc) = inspect.acc else {
15            return <IgnoreAccuracy as HitResultGenerator<Catch>>::generate_hitresults(inspect);
16        };
17
18        // In osu!catch:
19        // - total_objects = n_fruits + n_droplets + n_tiny_droplets
20        // - accuracy = (fruits + droplets + tiny_droplets) / total_objects
21        // - Constraint: fruits + droplets + misses = n_fruits + n_droplets
22        // - Constraint: tiny_droplets + tiny_droplet_misses = n_tiny_droplets
23
24        let n_fruits = inspect.attrs.n_fruits;
25        let n_droplets = inspect.attrs.n_droplets;
26        let n_tiny_droplets = inspect.attrs.n_tiny_droplets;
27        let total_objects = inspect.total_objects();
28
29        if total_objects == 0 {
30            return CatchHitResults {
31                fruits: 0,
32                droplets: 0,
33                tiny_droplets: 0,
34                tiny_droplet_misses: 0,
35                misses: 0,
36            };
37        }
38
39        // Get misses (fruits and droplets only, clamped)
40        let misses = inspect.misses();
41
42        // Calculate how many successful catches we need for target accuracy
43        // acc = catches / total_objects
44        // catches = acc * total_objects
45        let catches_needed = (acc * f64::from(total_objects)).round_ties_even() as u32;
46
47        // Maximum possible catches considering misses
48        let max_fruit_droplet_catches = (n_fruits + n_droplets).saturating_sub(misses);
49
50        // Clamp and validate provided values
51        let provided_fruits = inspect.fruits.map_or(0, |n| cmp::min(n, n_fruits));
52        let provided_droplets = inspect.droplets.map_or(0, |n| cmp::min(n, n_droplets));
53        let provided_tiny_droplets = inspect
54            .tiny_droplets
55            .map_or(0, |n| cmp::min(n, n_tiny_droplets));
56        let provided_tiny_droplet_misses = inspect.tiny_droplet_misses.unwrap_or(0);
57
58        // Make sure provided fruits + droplets don't exceed what's possible with the given misses
59        let clamped_fruits = cmp::min(provided_fruits, max_fruit_droplet_catches);
60        let clamped_droplets = cmp::min(
61            provided_droplets,
62            max_fruit_droplet_catches.saturating_sub(clamped_fruits),
63        );
64
65        match (
66            inspect.fruits,
67            inspect.droplets,
68            inspect.tiny_droplets,
69            inspect.tiny_droplet_misses,
70        ) {
71            // All provided - clamp and ensure pool constraints
72            // Priority: misses > fruits > droplets (for fruit/droplet pool)
73            // Priority: tiny_droplets > tiny_droplet_misses (for tiny droplet pool)
74            (Some(_), Some(_), Some(_), Some(_)) => {
75                // Handle fruit/droplet pool constraint
76                let pool_total = n_fruits + n_droplets;
77                let current_sum = clamped_fruits + clamped_droplets + misses;
78
79                let (final_fruits, final_droplets) = match current_sum.cmp(&pool_total) {
80                    cmp::Ordering::Less => {
81                        // Need to add more - prioritize droplets (adjust lower priority first)
82                        let needed = pool_total - current_sum;
83                        let new_droplets = cmp::min(clamped_droplets + needed, n_droplets);
84                        let still_needed =
85                            pool_total.saturating_sub(clamped_fruits + new_droplets + misses);
86                        let new_fruits = cmp::min(clamped_fruits + still_needed, n_fruits);
87
88                        (new_fruits, new_droplets)
89                    }
90                    cmp::Ordering::Equal => (clamped_fruits, clamped_droplets),
91                    cmp::Ordering::Greater => {
92                        // Have too many - reduce droplets first (adjust lower priority first)
93                        let excess = current_sum - pool_total;
94                        let new_droplets = clamped_droplets.saturating_sub(excess);
95                        let still_excess =
96                            (clamped_fruits + new_droplets + misses).saturating_sub(pool_total);
97                        let new_fruits = clamped_fruits.saturating_sub(still_excess);
98
99                        (new_fruits, new_droplets)
100                    }
101                };
102
103                // Handle tiny droplet pool constraint
104                let tiny_pool_total = n_tiny_droplets;
105                let tiny_current_sum = provided_tiny_droplets + provided_tiny_droplet_misses;
106
107                let (final_tiny_droplets, final_tiny_droplet_misses) = match tiny_current_sum
108                    .cmp(&tiny_pool_total)
109                {
110                    cmp::Ordering::Less => {
111                        // Need to add more - prioritize tiny_droplets (higher priority)
112                        let needed = tiny_pool_total - tiny_current_sum;
113                        let new_tiny_droplets =
114                            cmp::min(provided_tiny_droplets + needed, n_tiny_droplets);
115                        let still_needed = tiny_pool_total.saturating_sub(new_tiny_droplets);
116
117                        (new_tiny_droplets, still_needed)
118                    }
119                    cmp::Ordering::Equal => (provided_tiny_droplets, provided_tiny_droplet_misses),
120                    cmp::Ordering::Greater => {
121                        // Have too many - reduce tiny_droplet_misses first (lower priority)
122                        let excess = tiny_current_sum - tiny_pool_total;
123                        let new_tiny_droplet_misses =
124                            provided_tiny_droplet_misses.saturating_sub(excess);
125                        let still_excess = (provided_tiny_droplets + new_tiny_droplet_misses)
126                            .saturating_sub(tiny_pool_total);
127                        let new_tiny_droplets = provided_tiny_droplets.saturating_sub(still_excess);
128
129                        (new_tiny_droplets, new_tiny_droplet_misses)
130                    }
131                };
132
133                CatchHitResults {
134                    fruits: final_fruits,
135                    droplets: final_droplets,
136                    tiny_droplets: final_tiny_droplets,
137                    tiny_droplet_misses: final_tiny_droplet_misses,
138                    misses,
139                }
140            }
141
142            // Only one missing
143            (Some(_), Some(_), Some(_), None) => {
144                // tiny_droplet_misses is the only unknown
145                let tiny_droplet_misses = n_tiny_droplets.saturating_sub(provided_tiny_droplets);
146
147                CatchHitResults {
148                    fruits: clamped_fruits,
149                    droplets: clamped_droplets,
150                    tiny_droplets: provided_tiny_droplets,
151                    tiny_droplet_misses,
152                    misses,
153                }
154            }
155            (Some(_), Some(_), None, Some(_)) => {
156                // tiny_droplets is the only unknown
157                // We need to figure out how many tiny droplets to catch
158                let current_catches = clamped_fruits + clamped_droplets;
159                let remaining_catches = catches_needed.saturating_sub(current_catches);
160                let tiny_droplets = cmp::min(remaining_catches, n_tiny_droplets);
161
162                CatchHitResults {
163                    fruits: clamped_fruits,
164                    droplets: clamped_droplets,
165                    tiny_droplets,
166                    tiny_droplet_misses: provided_tiny_droplet_misses,
167                    misses,
168                }
169            }
170            (Some(_), None, Some(_), Some(_)) => {
171                // droplets is the only unknown
172                // Use pool constraint: droplets = n_fruits + n_droplets - fruits - misses
173                let droplets_by_pool =
174                    (n_fruits + n_droplets).saturating_sub(clamped_fruits + misses);
175                let droplets = cmp::min(droplets_by_pool, n_droplets);
176
177                CatchHitResults {
178                    fruits: clamped_fruits,
179                    droplets,
180                    tiny_droplets: provided_tiny_droplets,
181                    tiny_droplet_misses: provided_tiny_droplet_misses,
182                    misses,
183                }
184            }
185            (None, Some(_), Some(_), Some(_)) => {
186                // fruits is the only unknown
187                // Use pool constraint: fruits = n_fruits + n_droplets - droplets - misses
188                let fruits_by_pool =
189                    (n_fruits + n_droplets).saturating_sub(clamped_droplets + misses);
190                let fruits = cmp::min(fruits_by_pool, n_fruits);
191
192                CatchHitResults {
193                    fruits,
194                    droplets: clamped_droplets,
195                    tiny_droplets: provided_tiny_droplets,
196                    tiny_droplet_misses: provided_tiny_droplet_misses,
197                    misses,
198                }
199            }
200
201            // Two or more missing - use fast approximation
202            _ => {
203                // Calculate how many catches we still need after accounting for provided values
204                let provided_catches = clamped_fruits + clamped_droplets + provided_tiny_droplets;
205                let mut remain_catches = catches_needed.saturating_sub(provided_catches);
206
207                // We need to distribute remaining catches among missing types
208                // Priority: fruits > droplets > tiny_droplets (for performance)
209
210                let fruits = if inspect.fruits.is_none() {
211                    // Maximum fruits we can catch considering:
212                    // 1. How many fruits exist (n_fruits)
213                    // 2. How many fruit/droplet slots are available after misses and provided droplets
214                    let max_by_pool = max_fruit_droplet_catches.saturating_sub(clamped_droplets);
215                    let max_fruits = cmp::min(n_fruits, max_by_pool);
216                    let caught = cmp::min(remain_catches, max_fruits);
217                    remain_catches = remain_catches.saturating_sub(caught);
218
219                    caught
220                } else {
221                    clamped_fruits
222                };
223
224                let droplets = if inspect.droplets.is_some() {
225                    clamped_droplets
226                } else if inspect.fruits.is_none() {
227                    // If fruits is also missing, calculate based on remaining catches
228
229                    // Both fruits and droplets are missing
230                    let max_by_pool = max_fruit_droplet_catches.saturating_sub(fruits);
231                    let max_droplets = cmp::min(n_droplets, max_by_pool);
232                    let caught = cmp::min(remain_catches, max_droplets);
233                    remain_catches = remain_catches.saturating_sub(caught);
234
235                    caught
236                } else {
237                    // Only droplets is missing, fruits was provided
238                    // Use pool constraint: droplets = n_fruits + n_droplets - fruits - misses
239                    let droplets_by_pool = (n_fruits + n_droplets).saturating_sub(fruits + misses);
240                    let droplets = cmp::min(droplets_by_pool, n_droplets);
241
242                    // Decrement remaining_catches by the droplets we're catching
243                    remain_catches = remain_catches.saturating_sub(droplets);
244
245                    droplets
246                };
247
248                // If fruits was provided but droplets couldn't fill the pool,
249                // adjust fruits upward to satisfy the pool constraint
250                let fruits = if inspect.fruits.is_some() && inspect.droplets.is_none() {
251                    let pool_sum = fruits + droplets + misses;
252                    let expected = n_fruits + n_droplets;
253
254                    if pool_sum < expected {
255                        // Add the difference to fruits
256                        let adjusted = cmp::min(n_fruits, fruits + (expected - pool_sum));
257                        // Also update remaining_catches since we added more catches
258                        let added_catches = adjusted - fruits;
259                        remain_catches = remain_catches.saturating_sub(added_catches);
260
261                        adjusted
262                    } else {
263                        fruits
264                    }
265                } else {
266                    fruits
267                };
268
269                let tiny_droplets = if inspect.tiny_droplets.is_none() {
270                    cmp::min(remain_catches, n_tiny_droplets)
271                } else {
272                    provided_tiny_droplets
273                };
274
275                let tiny_droplet_misses = if inspect.tiny_droplet_misses.is_none() {
276                    // Calculate how many tiny droplets were missed
277                    n_tiny_droplets.saturating_sub(tiny_droplets)
278                } else {
279                    provided_tiny_droplet_misses
280                };
281
282                CatchHitResults {
283                    fruits,
284                    droplets,
285                    tiny_droplets,
286                    tiny_droplet_misses,
287                    misses,
288                }
289            }
290        }
291    }
292}
293
294#[cfg(test)]
295mod tests {
296    use crate::{Difficulty, catch::CatchDifficultyAttributes};
297
298    use super::*;
299
300    #[test]
301    fn perfect_accuracy() {
302        const N_FRUITS: u32 = 100;
303        const N_DROPLETS: u32 = 50;
304        const N_TINY_DROPLETS: u32 = 200;
305
306        let inspect = InspectCatchPerformance {
307            attrs: &CatchDifficultyAttributes {
308                n_fruits: N_FRUITS,
309                n_droplets: N_DROPLETS,
310                n_tiny_droplets: N_TINY_DROPLETS,
311                ..Default::default()
312            },
313            difficulty: &Difficulty::new(),
314            acc: Some(1.0),
315            combo: None,
316            fruits: None,
317            droplets: None,
318            tiny_droplets: None,
319            tiny_droplet_misses: None,
320            misses: Some(0),
321        };
322
323        let result = <Fast as HitResultGenerator<Catch>>::generate_hitresults(inspect);
324
325        assert_eq!(result.fruits, N_FRUITS);
326        assert_eq!(result.droplets, N_DROPLETS);
327        assert_eq!(result.tiny_droplets, N_TINY_DROPLETS);
328        assert_eq!(result.tiny_droplet_misses, 0);
329        assert_eq!(result.misses, 0);
330        assert_eq!(result.accuracy(), 1.0);
331        assert_eq!(result.total_hits(), N_FRUITS + N_DROPLETS + N_TINY_DROPLETS);
332    }
333
334    #[test]
335    fn high_accuracy_with_misses() {
336        const N_FRUITS: u32 = 80;
337        const N_DROPLETS: u32 = 40;
338        const N_TINY_DROPLETS: u32 = 100;
339        const MISSES: u32 = 5; // fruit/droplet misses
340        const ACC: f64 = 0.95;
341
342        let inspect = InspectCatchPerformance {
343            attrs: &CatchDifficultyAttributes {
344                n_fruits: N_FRUITS,
345                n_droplets: N_DROPLETS,
346                n_tiny_droplets: N_TINY_DROPLETS,
347                ..Default::default()
348            },
349            difficulty: &Difficulty::new(),
350            acc: Some(ACC),
351            combo: None,
352            fruits: None,
353            droplets: None,
354            tiny_droplets: None,
355            tiny_droplet_misses: None,
356            misses: Some(MISSES),
357        };
358
359        let result = <Fast as HitResultGenerator<Catch>>::generate_hitresults(inspect);
360
361        // Total objects is fixed
362        assert_eq!(result.total_hits(), N_FRUITS + N_DROPLETS + N_TINY_DROPLETS);
363        assert_eq!(result.misses, MISSES);
364
365        // Verify accuracy
366        let actual_acc = result.accuracy();
367        assert!(
368            (actual_acc - ACC).abs() < 0.01,
369            "Expected ~{ACC}, got {actual_acc}"
370        );
371    }
372
373    #[test]
374    fn medium_accuracy() {
375        const N_FRUITS: u32 = 60;
376        const N_DROPLETS: u32 = 30;
377        const N_TINY_DROPLETS: u32 = 150;
378        const MISSES: u32 = 10;
379        const ACC: f64 = 0.85;
380
381        let inspect = InspectCatchPerformance {
382            attrs: &CatchDifficultyAttributes {
383                n_fruits: N_FRUITS,
384                n_droplets: N_DROPLETS,
385                n_tiny_droplets: N_TINY_DROPLETS,
386                ..Default::default()
387            },
388            difficulty: &Difficulty::new(),
389            acc: Some(ACC),
390            combo: None,
391            fruits: None,
392            droplets: None,
393            tiny_droplets: None,
394            tiny_droplet_misses: None,
395            misses: Some(MISSES),
396        };
397
398        let result = <Fast as HitResultGenerator<Catch>>::generate_hitresults(inspect);
399
400        assert_eq!(result.total_hits(), N_FRUITS + N_DROPLETS + N_TINY_DROPLETS);
401        assert_eq!(result.misses, MISSES);
402
403        let actual_acc = result.accuracy();
404        assert!(
405            (actual_acc - ACC).abs() < 0.01,
406            "Expected ~{ACC}, got {actual_acc}"
407        );
408    }
409
410    #[test]
411    fn with_fruits_provided() {
412        const N_FRUITS: u32 = 50;
413        const N_DROPLETS: u32 = 25;
414        const N_TINY_DROPLETS: u32 = 100;
415        const ACC: f64 = 0.90;
416
417        let inspect = InspectCatchPerformance {
418            attrs: &CatchDifficultyAttributes {
419                n_fruits: N_FRUITS,
420                n_droplets: N_DROPLETS,
421                n_tiny_droplets: N_TINY_DROPLETS,
422                ..Default::default()
423            },
424            difficulty: &Difficulty::new(),
425            acc: Some(ACC),
426            combo: None,
427            fruits: Some(45),
428            droplets: None,
429            tiny_droplets: None,
430            tiny_droplet_misses: None,
431            misses: Some(3),
432        };
433
434        let result = <Fast as HitResultGenerator<Catch>>::generate_hitresults(inspect);
435
436        // 25 droplets are the max but with 45 fruits and 3 misses we're still
437        // missing 2 hits. Since droplets are at the max and misses have the
438        // highest priority, the amount of fruits needs to be adjusted to 47
439        // despite being specified to be lower.
440        assert_eq!(result.fruits, 47);
441        assert_eq!(result.misses, 3);
442        assert_eq!(result.droplets, 25);
443        assert_eq!(result.total_hits(), N_FRUITS + N_DROPLETS + N_TINY_DROPLETS);
444
445        let actual_acc = result.accuracy();
446        assert!(
447            (actual_acc - ACC).abs() < 0.02,
448            "Expected ~{ACC}, got {actual_acc}"
449        );
450    }
451
452    #[test]
453    fn all_values_provided() {
454        const N_FRUITS: u32 = 40;
455        const N_DROPLETS: u32 = 20;
456        const N_TINY_DROPLETS: u32 = 80;
457
458        let inspect = InspectCatchPerformance {
459            attrs: &CatchDifficultyAttributes {
460                n_fruits: N_FRUITS,
461                n_droplets: N_DROPLETS,
462                n_tiny_droplets: N_TINY_DROPLETS,
463                ..Default::default()
464            },
465            difficulty: &Difficulty::new(),
466            acc: Some(0.85), // Ignored when all provided
467            combo: None,
468            fruits: Some(35),
469            droplets: Some(18),
470            tiny_droplets: Some(70),
471            tiny_droplet_misses: Some(7),
472            misses: Some(4),
473        };
474
475        let result = <Fast as HitResultGenerator<Catch>>::generate_hitresults(inspect);
476
477        // Pool constraints will be enforced:
478        // Fruit/droplet pool: 40 + 20 = 60, provided sum: 35 + 18 + 4 = 57 (missing 3)
479        // Priority: misses > fruits > droplets
480        // - droplets are capped to 20 so we assign 2 + 18 but have 1 left
481        // - fruits are capped to 40 so we can assign 1 + 35
482        assert_eq!(result.fruits, 36); // Adjusted from 35
483        assert_eq!(result.droplets, 20); // Adjusted from 18
484        assert_eq!(result.misses, 4); // Remains as given
485
486        // Tiny droplet pool: 80, provided sum: 70 + 7 = 77 (missing 3)
487        // Priority: tiny_droplets_misses > tiny_droplet, so adjust tiny_droplets: 70 + 3 = 73
488        assert_eq!(result.tiny_droplets, 73); // Adjusted from 70
489        assert_eq!(result.tiny_droplet_misses, 7); // Remains as given
490
491        assert_eq!(result.total_hits(), N_FRUITS + N_DROPLETS + N_TINY_DROPLETS);
492    }
493
494    #[test]
495    fn low_accuracy_many_tiny_droplet_misses() {
496        const N_FRUITS: u32 = 30;
497        const N_DROPLETS: u32 = 15;
498        const N_TINY_DROPLETS: u32 = 60;
499        const MISSES: u32 = 10;
500        const ACC: f64 = 0.60;
501
502        let inspect = InspectCatchPerformance {
503            attrs: &CatchDifficultyAttributes {
504                n_fruits: N_FRUITS,
505                n_droplets: N_DROPLETS,
506                n_tiny_droplets: N_TINY_DROPLETS,
507                ..Default::default()
508            },
509            difficulty: &Difficulty::new(),
510            acc: Some(ACC),
511            combo: None,
512            fruits: None,
513            droplets: None,
514            tiny_droplets: None,
515            tiny_droplet_misses: None,
516            misses: Some(MISSES),
517        };
518
519        let result = <Fast as HitResultGenerator<Catch>>::generate_hitresults(inspect);
520
521        assert_eq!(result.total_hits(), N_FRUITS + N_DROPLETS + N_TINY_DROPLETS);
522        assert_eq!(result.misses, MISSES);
523
524        // With 60% acc on 105 objects, we need 63 catches
525        // We have 10 fruit/droplet misses, so max fruit+droplet = 35
526        // But we only need 63 total catches
527        let catches = result.fruits + result.droplets + result.tiny_droplets;
528        assert_eq!(catches, 63);
529
530        let actual_acc = result.accuracy();
531        assert!(
532            (actual_acc - ACC).abs() < 0.01,
533            "Expected ~{ACC}, got {actual_acc}"
534        );
535    }
536
537    #[test]
538    fn zero_accuracy_all_misses() {
539        const N_FRUITS: u32 = 20;
540        const N_DROPLETS: u32 = 10;
541        const N_TINY_DROPLETS: u32 = 40;
542
543        let inspect = InspectCatchPerformance {
544            attrs: &CatchDifficultyAttributes {
545                n_fruits: N_FRUITS,
546                n_droplets: N_DROPLETS,
547                n_tiny_droplets: N_TINY_DROPLETS,
548                ..Default::default()
549            },
550            difficulty: &Difficulty::new(),
551            acc: Some(0.0),
552            combo: None,
553            fruits: None,
554            droplets: None,
555            tiny_droplets: None,
556            tiny_droplet_misses: None,
557            misses: Some(N_FRUITS + N_DROPLETS), // All fruits and droplets missed
558        };
559
560        let result = <Fast as HitResultGenerator<Catch>>::generate_hitresults(inspect);
561
562        assert_eq!(result.fruits, 0);
563        assert_eq!(result.droplets, 0);
564        assert_eq!(result.tiny_droplets, 0);
565        assert_eq!(result.misses, 30);
566        assert_eq!(result.tiny_droplet_misses, N_TINY_DROPLETS);
567        assert_eq!(result.accuracy(), 0.0);
568    }
569
570    #[test]
571    fn only_tiny_droplet_misses_missing() {
572        const N_FRUITS: u32 = 25;
573        const N_DROPLETS: u32 = 15;
574        const N_TINY_DROPLETS: u32 = 50;
575
576        let inspect = InspectCatchPerformance {
577            attrs: &CatchDifficultyAttributes {
578                n_fruits: N_FRUITS,
579                n_droplets: N_DROPLETS,
580                n_tiny_droplets: N_TINY_DROPLETS,
581                ..Default::default()
582            },
583            difficulty: &Difficulty::new(),
584            acc: Some(0.88),
585            combo: None,
586            fruits: Some(20),
587            droplets: Some(12),
588            tiny_droplets: Some(47),
589            tiny_droplet_misses: None,
590            misses: Some(5),
591        };
592
593        let result = <Fast as HitResultGenerator<Catch>>::generate_hitresults(inspect);
594
595        assert_eq!(result.fruits, 20);
596        assert_eq!(result.droplets, 12);
597        assert_eq!(result.tiny_droplets, 47);
598        assert_eq!(result.misses, 5);
599
600        // tiny_droplet_misses = n_tiny_droplets - tiny_droplets
601        assert_eq!(result.tiny_droplet_misses, N_TINY_DROPLETS - 47);
602
603        // Note: total_hits() will be 87 not 90 because the provided values
604        // (fruits=20, droplets=12, misses=5) only account for 37 of the 40 fruits+droplets
605    }
606
607    #[test]
608    fn misses_clamped_to_fruits_plus_droplets() {
609        const N_FRUITS: u32 = 20;
610        const N_DROPLETS: u32 = 10;
611        const N_TINY_DROPLETS: u32 = 30;
612
613        let inspect = InspectCatchPerformance {
614            attrs: &CatchDifficultyAttributes {
615                n_fruits: N_FRUITS,
616                n_droplets: N_DROPLETS,
617                n_tiny_droplets: N_TINY_DROPLETS,
618                ..Default::default()
619            },
620            difficulty: &Difficulty::new(),
621            acc: Some(0.80),
622            combo: None,
623            fruits: None,
624            droplets: None,
625            tiny_droplets: None,
626            tiny_droplet_misses: None,
627            misses: Some(100), // Way more than possible
628        };
629
630        let result = <Fast as HitResultGenerator<Catch>>::generate_hitresults(inspect);
631
632        // Misses should be clamped to n_fruits + n_droplets
633        assert_eq!(result.misses, N_FRUITS + N_DROPLETS);
634        assert_eq!(result.fruits, 0);
635        assert_eq!(result.droplets, 0);
636    }
637}