1use crate::{
2 any::{
3 HitResultGenerator,
4 hitresult_generator::{Closest, Fast},
5 },
6 catch::{Catch, CatchHitResults, performance::inspect::InspectCatchPerformance},
7};
8
9impl HitResultGenerator<Catch> for Closest {
10 fn generate_hitresults(inspect: InspectCatchPerformance) -> CatchHitResults {
11 <Fast as HitResultGenerator<Catch>>::generate_hitresults(inspect)
12 }
13}
14
15#[cfg(test)]
16mod tests {
17 use crate::{
18 Difficulty,
19 any::HitResultGenerator,
20 catch::{
21 Catch, CatchDifficultyAttributes, CatchHitResults,
22 performance::inspect::InspectCatchPerformance,
23 },
24 };
25
26 use super::*;
27
28 fn verify_is_closest(inspect: &InspectCatchPerformance, result: &CatchHitResults) {
31 let actual_acc = result.accuracy();
32 let target_acc = inspect.acc.unwrap();
33 let current_dist = (actual_acc - target_acc).abs();
34
35 let n_fruits = inspect.attrs.n_fruits;
36 let n_droplets = inspect.attrs.n_droplets;
37 let n_tiny_droplets = inspect.attrs.n_tiny_droplets;
38 let misses = inspect.misses();
39
40 let variations = [
42 (1, -1, 0, 0),
44 (1, 0, -1, 0),
46 (-1, 1, 0, 0),
48 (0, 1, -1, 0),
50 (-1, 0, 1, 0),
52 (0, -1, 1, 0),
54 (0, 0, 1, -1),
56 (0, 0, -1, 1),
58 ];
59
60 for (d_fruits, d_droplets, d_tiny_droplets, d_tiny_droplet_misses) in variations {
61 let new_fruits = (result.fruits as i32 + d_fruits).max(0) as u32;
62 let new_droplets = (result.droplets as i32 + d_droplets).max(0) as u32;
63 let new_tiny_droplets = (result.tiny_droplets as i32 + d_tiny_droplets).max(0) as u32;
64 let new_tiny_droplet_misses =
65 (result.tiny_droplet_misses as i32 + d_tiny_droplet_misses).max(0) as u32;
66
67 if new_fruits > n_fruits
69 || new_droplets > n_droplets
70 || new_tiny_droplets > n_tiny_droplets
71 || new_tiny_droplet_misses > n_tiny_droplets
72 {
73 continue;
74 }
75
76 if new_fruits + new_droplets + misses != n_fruits + n_droplets {
78 continue;
79 }
80
81 if new_tiny_droplets + new_tiny_droplet_misses != n_tiny_droplets {
82 continue;
83 }
84
85 if let Some(n) = inspect.fruits {
87 if new_fruits != n {
88 continue;
89 }
90 }
91
92 if let Some(n) = inspect.droplets {
93 if new_droplets != n {
94 continue;
95 }
96 }
97
98 if let Some(n) = inspect.tiny_droplets {
99 if new_tiny_droplets != n {
100 continue;
101 }
102 }
103
104 if let Some(n) = inspect.tiny_droplet_misses {
105 if new_tiny_droplet_misses != n {
106 continue;
107 }
108 }
109
110 let neighbor = CatchHitResults {
111 fruits: new_fruits,
112 droplets: new_droplets,
113 tiny_droplets: new_tiny_droplets,
114 tiny_droplet_misses: new_tiny_droplet_misses,
115 misses: result.misses,
116 };
117
118 let neighbor_acc = neighbor.accuracy();
119 let neighbor_dist = (neighbor_acc - target_acc).abs();
120
121 assert!(
122 current_dist <= neighbor_dist + 1e-10,
123 "Found closer neighbor! \
124 Current: {result:?} (acc={actual_acc:.6}, dist={current_dist:.6}), \
125 Neighbor: {neighbor:?} (acc={neighbor_acc:.6}, dist={neighbor_dist:.6})",
126 );
127 }
128 }
129
130 #[test]
131 fn perfect_accuracy() {
132 const N_FRUITS: u32 = 100;
133 const N_DROPLETS: u32 = 50;
134 const N_TINY_DROPLETS: u32 = 200;
135
136 let inspect = InspectCatchPerformance {
137 attrs: &CatchDifficultyAttributes {
138 n_fruits: N_FRUITS,
139 n_droplets: N_DROPLETS,
140 n_tiny_droplets: N_TINY_DROPLETS,
141 ..Default::default()
142 },
143 difficulty: &Difficulty::new(),
144 acc: Some(1.0),
145 combo: None,
146 fruits: None,
147 droplets: None,
148 tiny_droplets: None,
149 tiny_droplet_misses: None,
150 misses: Some(0),
151 };
152
153 let result = <Closest as HitResultGenerator<Catch>>::generate_hitresults(inspect.clone());
154
155 assert_eq!(result.fruits, N_FRUITS);
156 assert_eq!(result.droplets, N_DROPLETS);
157 assert_eq!(result.tiny_droplets, N_TINY_DROPLETS);
158 assert_eq!(result.tiny_droplet_misses, 0);
159 assert_eq!(result.misses, 0);
160 assert_eq!(result.accuracy(), 1.0);
161
162 verify_is_closest(&inspect, &result);
163 }
164
165 #[test]
166 fn high_accuracy_with_misses() {
167 const N_FRUITS: u32 = 80;
168 const N_DROPLETS: u32 = 40;
169 const N_TINY_DROPLETS: u32 = 100;
170 const ACC: f64 = 0.95;
171
172 let inspect = InspectCatchPerformance {
173 attrs: &CatchDifficultyAttributes {
174 n_fruits: N_FRUITS,
175 n_droplets: N_DROPLETS,
176 n_tiny_droplets: N_TINY_DROPLETS,
177 ..Default::default()
178 },
179 difficulty: &Difficulty::new(),
180 acc: Some(ACC),
181 combo: None,
182 fruits: None,
183 droplets: None,
184 tiny_droplets: None,
185 tiny_droplet_misses: None,
186 misses: Some(5),
187 };
188
189 let result = <Closest as HitResultGenerator<Catch>>::generate_hitresults(inspect.clone());
190
191 assert_eq!(result.total_hits(), N_FRUITS + N_DROPLETS + N_TINY_DROPLETS);
192 assert_eq!(result.misses, 5);
193
194 let actual_acc = result.accuracy();
195 assert!(
196 (actual_acc - ACC).abs() < 0.01,
197 "Expected ~{ACC}, got {actual_acc}"
198 );
199
200 verify_is_closest(&inspect, &result);
201 }
202
203 #[test]
204 fn medium_accuracy() {
205 const N_FRUITS: u32 = 60;
206 const N_DROPLETS: u32 = 30;
207 const N_TINY_DROPLETS: u32 = 150;
208 const ACC: f64 = 0.75;
209
210 let inspect = InspectCatchPerformance {
211 attrs: &CatchDifficultyAttributes {
212 n_fruits: N_FRUITS,
213 n_droplets: N_DROPLETS,
214 n_tiny_droplets: N_TINY_DROPLETS,
215 ..Default::default()
216 },
217 difficulty: &Difficulty::new(),
218 acc: Some(ACC),
219 combo: None,
220 fruits: None,
221 droplets: None,
222 tiny_droplets: None,
223 tiny_droplet_misses: None,
224 misses: Some(15),
225 };
226
227 let result = <Closest as HitResultGenerator<Catch>>::generate_hitresults(inspect.clone());
228
229 assert_eq!(result.total_hits(), N_FRUITS + N_DROPLETS + N_TINY_DROPLETS);
230 assert_eq!(result.misses, 15);
231
232 verify_is_closest(&inspect, &result);
233 }
234
235 #[test]
236 fn low_accuracy() {
237 const N_FRUITS: u32 = 40;
238 const N_DROPLETS: u32 = 20;
239 const N_TINY_DROPLETS: u32 = 80;
240 const ACC: f64 = 0.50;
241
242 let inspect = InspectCatchPerformance {
243 attrs: &CatchDifficultyAttributes {
244 n_fruits: N_FRUITS,
245 n_droplets: N_DROPLETS,
246 n_tiny_droplets: N_TINY_DROPLETS,
247 ..Default::default()
248 },
249 difficulty: &Difficulty::new(),
250 acc: Some(ACC),
251 combo: None,
252 fruits: None,
253 droplets: None,
254 tiny_droplets: None,
255 tiny_droplet_misses: None,
256 misses: Some(20),
257 };
258
259 let result = <Closest as HitResultGenerator<Catch>>::generate_hitresults(inspect.clone());
260
261 assert_eq!(result.total_hits(), N_FRUITS + N_DROPLETS + N_TINY_DROPLETS);
262 assert_eq!(result.misses, 20);
263
264 verify_is_closest(&inspect, &result);
265 }
266
267 #[test]
268 fn fruits_provided() {
269 const N_FRUITS: u32 = 50;
270 const N_DROPLETS: u32 = 25;
271 const N_TINY_DROPLETS: u32 = 100;
272 const ACC: f64 = 0.90;
273
274 let inspect = InspectCatchPerformance {
275 attrs: &CatchDifficultyAttributes {
276 n_fruits: N_FRUITS,
277 n_droplets: N_DROPLETS,
278 n_tiny_droplets: N_TINY_DROPLETS,
279 ..Default::default()
280 },
281 difficulty: &Difficulty::new(),
282 acc: Some(ACC),
283 combo: None,
284 fruits: Some(45),
285 droplets: None,
286 tiny_droplets: None,
287 tiny_droplet_misses: None,
288 misses: Some(3),
289 };
290
291 let result = <Closest as HitResultGenerator<Catch>>::generate_hitresults(inspect.clone());
292
293 assert_eq!(result.fruits, 47);
295 assert_eq!(result.misses, 3);
296 assert_eq!(result.total_hits(), N_FRUITS + N_DROPLETS + N_TINY_DROPLETS);
297
298 verify_is_closest(&inspect, &result);
299 }
300
301 #[test]
302 fn droplets_provided() {
303 const N_FRUITS: u32 = 50;
304 const N_DROPLETS: u32 = 25;
305 const N_TINY_DROPLETS: u32 = 100;
306 const ACC: f64 = 0.85;
307
308 let inspect = InspectCatchPerformance {
309 attrs: &CatchDifficultyAttributes {
310 n_fruits: N_FRUITS,
311 n_droplets: N_DROPLETS,
312 n_tiny_droplets: N_TINY_DROPLETS,
313 ..Default::default()
314 },
315 difficulty: &Difficulty::new(),
316 acc: Some(ACC),
317 combo: None,
318 fruits: None,
319 droplets: Some(20),
320 tiny_droplets: None,
321 tiny_droplet_misses: None,
322 misses: Some(8),
323 };
324
325 let result = <Closest as HitResultGenerator<Catch>>::generate_hitresults(inspect.clone());
326
327 assert_eq!(result.droplets, 20);
328 assert_eq!(result.misses, 8);
329 assert_eq!(result.total_hits(), N_FRUITS + N_DROPLETS + N_TINY_DROPLETS);
330
331 verify_is_closest(&inspect, &result);
332 }
333
334 #[test]
335 fn tiny_droplets_provided() {
336 const N_FRUITS: u32 = 40;
337 const N_DROPLETS: u32 = 20;
338 const N_TINY_DROPLETS: u32 = 80;
339 const ACC: f64 = 0.80;
340
341 let inspect = InspectCatchPerformance {
342 attrs: &CatchDifficultyAttributes {
343 n_fruits: N_FRUITS,
344 n_droplets: N_DROPLETS,
345 n_tiny_droplets: N_TINY_DROPLETS,
346 ..Default::default()
347 },
348 difficulty: &Difficulty::new(),
349 acc: Some(ACC),
350 combo: None,
351 fruits: None,
352 droplets: None,
353 tiny_droplets: Some(60),
354 tiny_droplet_misses: None,
355 misses: Some(10),
356 };
357
358 let result = <Closest as HitResultGenerator<Catch>>::generate_hitresults(inspect.clone());
359
360 assert_eq!(result.tiny_droplets, 60);
361 assert_eq!(result.misses, 10);
362 assert_eq!(result.total_hits(), N_FRUITS + N_DROPLETS + N_TINY_DROPLETS);
363
364 verify_is_closest(&inspect, &result);
365 }
366
367 #[test]
368 fn all_values_provided() {
369 const N_FRUITS: u32 = 30;
370 const N_DROPLETS: u32 = 15;
371 const N_TINY_DROPLETS: u32 = 60;
372
373 let inspect = InspectCatchPerformance {
374 attrs: &CatchDifficultyAttributes {
375 n_fruits: N_FRUITS,
376 n_droplets: N_DROPLETS,
377 n_tiny_droplets: N_TINY_DROPLETS,
378 ..Default::default()
379 },
380 difficulty: &Difficulty::new(),
381 acc: Some(0.85),
382 combo: None,
383 fruits: Some(25),
384 droplets: Some(12),
385 tiny_droplets: Some(50),
386 tiny_droplet_misses: Some(10),
387 misses: Some(5),
388 };
389
390 let result = <Closest as HitResultGenerator<Catch>>::generate_hitresults(inspect.clone());
391
392 assert_eq!(result.misses, 5);
397 assert_eq!(result.fruits, 25);
398 assert_eq!(result.droplets, 15); assert_eq!(result.total_hits(), N_FRUITS + N_DROPLETS + N_TINY_DROPLETS);
400
401 verify_is_closest(&inspect, &result);
402 }
403
404 #[test]
405 fn zero_accuracy() {
406 const N_FRUITS: u32 = 20;
407 const N_DROPLETS: u32 = 10;
408 const N_TINY_DROPLETS: u32 = 40;
409
410 let inspect = InspectCatchPerformance {
411 attrs: &CatchDifficultyAttributes {
412 n_fruits: N_FRUITS,
413 n_droplets: N_DROPLETS,
414 n_tiny_droplets: N_TINY_DROPLETS,
415 ..Default::default()
416 },
417 difficulty: &Difficulty::new(),
418 acc: Some(0.0),
419 combo: None,
420 fruits: None,
421 droplets: None,
422 tiny_droplets: None,
423 tiny_droplet_misses: None,
424 misses: Some(30),
425 };
426
427 let result = <Closest as HitResultGenerator<Catch>>::generate_hitresults(inspect.clone());
428
429 assert_eq!(result.fruits, 0);
430 assert_eq!(result.droplets, 0);
431 assert_eq!(result.tiny_droplets, 0);
432 assert_eq!(result.tiny_droplet_misses, N_TINY_DROPLETS);
433 assert_eq!(result.misses, 30);
434 assert_eq!(result.accuracy(), 0.0);
435
436 verify_is_closest(&inspect, &result);
437 }
438
439 #[test]
440 fn accuracy_very_close_to_one() {
441 const N_FRUITS: u32 = 35;
442 const N_DROPLETS: u32 = 18;
443 const N_TINY_DROPLETS: u32 = 70;
444 const ACC: f64 = 0.9878;
445
446 let inspect = InspectCatchPerformance {
447 attrs: &CatchDifficultyAttributes {
448 n_fruits: N_FRUITS,
449 n_droplets: N_DROPLETS,
450 n_tiny_droplets: N_TINY_DROPLETS,
451 ..Default::default()
452 },
453 difficulty: &Difficulty::new(),
454 acc: Some(ACC),
455 combo: None,
456 fruits: None,
457 droplets: None,
458 tiny_droplets: None,
459 tiny_droplet_misses: None,
460 misses: Some(1),
461 };
462
463 let result = <Closest as HitResultGenerator<Catch>>::generate_hitresults(inspect.clone());
464
465 assert_eq!(result.total_hits(), N_FRUITS + N_DROPLETS + N_TINY_DROPLETS);
466 assert_eq!(result.misses, 1);
467
468 let actual_acc = result.accuracy();
469 assert!(
470 (actual_acc - ACC).abs() < 0.01,
471 "Expected ~{ACC}, got {actual_acc}"
472 );
473
474 verify_is_closest(&inspect, &result);
475 }
476
477 #[test]
478 fn small_map() {
479 const N_FRUITS: u32 = 10;
480 const N_DROPLETS: u32 = 5;
481 const N_TINY_DROPLETS: u32 = 20;
482 const ACC: f64 = 0.70;
483
484 let inspect = InspectCatchPerformance {
485 attrs: &CatchDifficultyAttributes {
486 n_fruits: N_FRUITS,
487 n_droplets: N_DROPLETS,
488 n_tiny_droplets: N_TINY_DROPLETS,
489 ..Default::default()
490 },
491 difficulty: &Difficulty::new(),
492 acc: Some(ACC),
493 combo: None,
494 fruits: None,
495 droplets: None,
496 tiny_droplets: None,
497 tiny_droplet_misses: None,
498 misses: Some(3),
499 };
500
501 let result = <Closest as HitResultGenerator<Catch>>::generate_hitresults(inspect.clone());
502
503 assert_eq!(result.total_hits(), N_FRUITS + N_DROPLETS + N_TINY_DROPLETS);
504 assert_eq!(result.misses, 3);
505
506 verify_is_closest(&inspect, &result);
507 }
508
509 #[test]
510 fn large_map() {
511 const N_FRUITS: u32 = 500;
512 const N_DROPLETS: u32 = 250;
513 const N_TINY_DROPLETS: u32 = 1000;
514 const ACC: f64 = 0.87;
515
516 let inspect = InspectCatchPerformance {
517 attrs: &CatchDifficultyAttributes {
518 n_fruits: N_FRUITS,
519 n_droplets: N_DROPLETS,
520 n_tiny_droplets: N_TINY_DROPLETS,
521 ..Default::default()
522 },
523 difficulty: &Difficulty::new(),
524 acc: Some(ACC),
525 combo: None,
526 fruits: None,
527 droplets: None,
528 tiny_droplets: None,
529 tiny_droplet_misses: None,
530 misses: Some(42),
531 };
532
533 let result = <Closest as HitResultGenerator<Catch>>::generate_hitresults(inspect.clone());
534
535 assert_eq!(result.total_hits(), N_FRUITS + N_DROPLETS + N_TINY_DROPLETS);
536 assert_eq!(result.misses, 42);
537
538 let actual_acc = result.accuracy();
539 assert!(
540 (actual_acc - ACC).abs() < 0.01,
541 "Expected ~{ACC}, got {actual_acc}"
542 );
543
544 verify_is_closest(&inspect, &result);
545 }
546
547 #[test]
548 fn mostly_tiny_droplets() {
549 const N_FRUITS: u32 = 20;
550 const N_DROPLETS: u32 = 10;
551 const N_TINY_DROPLETS: u32 = 200;
552 const ACC: f64 = 0.65;
553
554 let inspect = InspectCatchPerformance {
555 attrs: &CatchDifficultyAttributes {
556 n_fruits: N_FRUITS,
557 n_droplets: N_DROPLETS,
558 n_tiny_droplets: N_TINY_DROPLETS,
559 ..Default::default()
560 },
561 difficulty: &Difficulty::new(),
562 acc: Some(ACC),
563 combo: None,
564 fruits: None,
565 droplets: None,
566 tiny_droplets: None,
567 tiny_droplet_misses: None,
568 misses: Some(8),
569 };
570
571 let result = <Closest as HitResultGenerator<Catch>>::generate_hitresults(inspect.clone());
572
573 assert_eq!(result.total_hits(), N_FRUITS + N_DROPLETS + N_TINY_DROPLETS);
574 assert_eq!(result.misses, 8);
575
576 verify_is_closest(&inspect, &result);
577 }
578
579 #[test]
580 fn accuracy_requiring_rounding() {
581 const N_FRUITS: u32 = 33;
582 const N_DROPLETS: u32 = 17;
583 const N_TINY_DROPLETS: u32 = 66;
584 const ACC: f64 = 0.7241379; let inspect = InspectCatchPerformance {
587 attrs: &CatchDifficultyAttributes {
588 n_fruits: N_FRUITS,
589 n_droplets: N_DROPLETS,
590 n_tiny_droplets: N_TINY_DROPLETS,
591 ..Default::default()
592 },
593 difficulty: &Difficulty::new(),
594 acc: Some(ACC),
595 combo: None,
596 fruits: None,
597 droplets: None,
598 tiny_droplets: None,
599 tiny_droplet_misses: None,
600 misses: Some(7),
601 };
602
603 let result = <Closest as HitResultGenerator<Catch>>::generate_hitresults(inspect.clone());
604
605 assert_eq!(result.total_hits(), N_FRUITS + N_DROPLETS + N_TINY_DROPLETS);
606 assert_eq!(result.misses, 7);
607
608 verify_is_closest(&inspect, &result);
609 }
610}