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 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 let misses = inspect.misses();
41
42 let catches_needed = (acc * f64::from(total_objects)).round_ties_even() as u32;
46
47 let max_fruit_droplet_catches = (n_fruits + n_droplets).saturating_sub(misses);
49
50 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 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 (Some(_), Some(_), Some(_), Some(_)) => {
75 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 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 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 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 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 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 (Some(_), Some(_), Some(_), None) => {
144 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 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 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 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 _ => {
203 let provided_catches = clamped_fruits + clamped_droplets + provided_tiny_droplets;
205 let mut remain_catches = catches_needed.saturating_sub(provided_catches);
206
207 let fruits = if inspect.fruits.is_none() {
211 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 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 let droplets_by_pool = (n_fruits + n_droplets).saturating_sub(fruits + misses);
240 let droplets = cmp::min(droplets_by_pool, n_droplets);
241
242 remain_catches = remain_catches.saturating_sub(droplets);
244
245 droplets
246 };
247
248 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 let adjusted = cmp::min(n_fruits, fruits + (expected - pool_sum));
257 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 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; 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 assert_eq!(result.total_hits(), N_FRUITS + N_DROPLETS + N_TINY_DROPLETS);
363 assert_eq!(result.misses, MISSES);
364
365 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 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), 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 assert_eq!(result.fruits, 36); assert_eq!(result.droplets, 20); assert_eq!(result.misses, 4); assert_eq!(result.tiny_droplets, 73); assert_eq!(result.tiny_droplet_misses, 7); 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 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), };
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 assert_eq!(result.tiny_droplet_misses, N_TINY_DROPLETS - 47);
602
603 }
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), };
629
630 let result = <Fast as HitResultGenerator<Catch>>::generate_hitresults(inspect);
631
632 assert_eq!(result.misses, N_FRUITS + N_DROPLETS);
634 assert_eq!(result.fruits, 0);
635 assert_eq!(result.droplets, 0);
636 }
637}