1use std::cmp;
2
3use rosu_map::section::general::GameMode;
4
5use self::calculator::ManiaPerformanceCalculator;
6
7use crate::{
8 any::{Difficulty, HitResultPriority, IntoModePerformance, IntoPerformance},
9 model::{mode::ConvertError, mods::GameMods},
10 osu::OsuPerformance,
11 util::map_or_attrs::MapOrAttrs,
12 Performance,
13};
14
15use super::{attributes::ManiaPerformanceAttributes, score_state::ManiaScoreState, Mania};
16
17mod calculator;
18pub mod gradual;
19
20#[derive(Clone, Debug, PartialEq)]
22#[must_use]
23pub struct ManiaPerformance<'map> {
24 map_or_attrs: MapOrAttrs<'map, Mania>,
25 difficulty: Difficulty,
26 n320: Option<u32>,
27 n300: Option<u32>,
28 n200: Option<u32>,
29 n100: Option<u32>,
30 n50: Option<u32>,
31 misses: Option<u32>,
32 acc: Option<f64>,
33 hitresult_priority: HitResultPriority,
34}
35
36impl<'map> ManiaPerformance<'map> {
37 pub fn new(map_or_attrs: impl IntoModePerformance<'map, Mania>) -> Self {
55 map_or_attrs.into_performance()
56 }
57
58 pub fn try_new(map_or_attrs: impl IntoPerformance<'map>) -> Option<Self> {
69 if let Performance::Mania(calc) = map_or_attrs.into_performance() {
70 Some(calc)
71 } else {
72 None
73 }
74 }
75
76 pub fn mods(mut self, mods: impl Into<GameMods>) -> Self {
87 self.difficulty = self.difficulty.mods(mods);
88
89 self
90 }
91
92 pub fn difficulty(mut self, difficulty: Difficulty) -> Self {
94 self.difficulty = difficulty;
95
96 self
97 }
98
99 pub fn passed_objects(mut self, passed_objects: u32) -> Self {
107 self.difficulty = self.difficulty.passed_objects(passed_objects);
108
109 self
110 }
111
112 pub fn clock_rate(mut self, clock_rate: f64) -> Self {
121 self.difficulty = self.difficulty.clock_rate(clock_rate);
122
123 self
124 }
125
126 pub fn hp(mut self, hp: f32, with_mods: bool) -> Self {
136 self.difficulty = self.difficulty.hp(hp, with_mods);
137
138 self
139 }
140
141 pub fn od(mut self, od: f32, with_mods: bool) -> Self {
151 self.difficulty = self.difficulty.od(od, with_mods);
152
153 self
154 }
155
156 pub fn accuracy(mut self, acc: f64) -> Self {
159 self.acc = Some(acc.clamp(0.0, 100.0) / 100.0);
160
161 self
162 }
163
164 pub const fn hitresult_priority(mut self, priority: HitResultPriority) -> Self {
168 self.hitresult_priority = priority;
169
170 self
171 }
172
173 pub fn lazer(mut self, lazer: bool) -> Self {
184 self.difficulty = self.difficulty.lazer(lazer);
185
186 self
187 }
188
189 pub const fn n320(mut self, n320: u32) -> Self {
191 self.n320 = Some(n320);
192
193 self
194 }
195
196 pub const fn n300(mut self, n300: u32) -> Self {
198 self.n300 = Some(n300);
199
200 self
201 }
202
203 pub const fn n200(mut self, n200: u32) -> Self {
205 self.n200 = Some(n200);
206
207 self
208 }
209
210 pub const fn n100(mut self, n100: u32) -> Self {
212 self.n100 = Some(n100);
213
214 self
215 }
216
217 pub const fn n50(mut self, n50: u32) -> Self {
219 self.n50 = Some(n50);
220
221 self
222 }
223
224 pub const fn misses(mut self, n_misses: u32) -> Self {
226 self.misses = Some(n_misses);
227
228 self
229 }
230
231 #[allow(clippy::needless_pass_by_value)]
233 pub const fn state(mut self, state: ManiaScoreState) -> Self {
234 let ManiaScoreState {
235 n320,
236 n300,
237 n200,
238 n100,
239 n50,
240 misses,
241 } = state;
242
243 self.n320 = Some(n320);
244 self.n300 = Some(n300);
245 self.n200 = Some(n200);
246 self.n100 = Some(n100);
247 self.n50 = Some(n50);
248 self.misses = Some(misses);
249
250 self
251 }
252
253 #[allow(clippy::too_many_lines, clippy::similar_names)]
255 pub fn generate_state(&mut self) -> Result<ManiaScoreState, ConvertError> {
256 let attrs = match self.map_or_attrs {
257 MapOrAttrs::Map(ref map) => {
258 let attrs = self.difficulty.calculate_for_mode::<Mania>(map)?;
259
260 self.map_or_attrs.insert_attrs(attrs)
261 }
262 MapOrAttrs::Attrs(ref attrs) => attrs,
263 };
264
265 let priority = self.hitresult_priority;
266 let mut n_objects = cmp::min(self.difficulty.get_passed_objects() as u32, attrs.n_objects);
267 let misses = self.misses.map_or(0, |n| cmp::min(n, n_objects));
268 let classic = !self.difficulty.get_lazer() || self.difficulty.get_mods().cl();
269
270 if !classic {
271 n_objects += attrs.n_hold_notes;
272 }
273
274 let n_remaining = n_objects - misses;
275
276 let min_remaining = |n: u32| cmp::min(n, n_remaining);
277
278 let mut n320 = self.n320.map_or(0, min_remaining);
279 let mut n300 = self.n300.map_or(0, min_remaining);
280 let mut n200 = self.n200.map_or(0, min_remaining);
281 let mut n100 = self.n100.map_or(0, min_remaining);
282 let mut n50 = self.n50.map_or(0, min_remaining);
283
284 let generate_fast = |acc: f64| {
285 let target = i32::max(
286 0,
287 f64::round(acc * f64::from(if classic { 60 } else { 61 } * n_objects)) as i32,
288 ) as u32;
289
290 let mut remaining_hits = n_remaining;
291 let mut delta = target - 10 * remaining_hits;
292
293 let perfect_factor = if classic { 50 } else { 51 };
294
295 if let Some(n320) = self.n320 {
296 delta = delta.saturating_sub(n320 * perfect_factor);
297 remaining_hits = remaining_hits.saturating_sub(n320);
298 }
299
300 if let Some(n300) = self.n300 {
301 delta = delta.saturating_sub(n300 * 50);
302 remaining_hits = remaining_hits.saturating_sub(n300);
303 }
304
305 if let Some(n200) = self.n200 {
306 delta = delta.saturating_sub(n200 * 30);
307 remaining_hits = remaining_hits.saturating_sub(n200);
308 }
309
310 if let Some(n100) = self.n100 {
311 delta = delta.saturating_sub(n100 * 10);
312 remaining_hits = remaining_hits.saturating_sub(n100);
313 }
314
315 if let Some(n50) = self.n50 {
316 remaining_hits = remaining_hits.saturating_sub(n50);
318 }
319
320 let mut perfects = if let Some(n320) = self.n320 {
321 n320
322 } else {
323 let perfects = u32::min(delta / perfect_factor, remaining_hits);
324 delta = delta.saturating_sub(perfects * perfect_factor);
325 remaining_hits = remaining_hits.saturating_sub(perfects);
326
327 perfects
328 };
329
330 let mut greats = if let Some(n300) = self.n300 {
331 n300
332 } else {
333 let greats = u32::min(delta / 50, remaining_hits);
334 delta = delta.saturating_sub(greats * 50);
335 remaining_hits = remaining_hits.saturating_sub(greats);
336
337 greats
338 };
339
340 let mut goods = if let Some(n200) = self.n200 {
341 n200
342 } else {
343 let goods = u32::min(delta / 30, remaining_hits);
344 delta = delta.saturating_sub(goods * 30);
345 remaining_hits = remaining_hits.saturating_sub(goods);
346
347 goods
348 };
349
350 let mut oks = if let Some(n100) = self.n100 {
351 n100
352 } else {
353 let oks = u32::min(delta / 10, remaining_hits);
354 remaining_hits = remaining_hits.saturating_sub(oks);
355
356 oks
357 };
358
359 let mehs = if let Some(mut n50) = self.n50 {
360 if remaining_hits > 0 {
361 if self.n100.is_none() {
362 oks += remaining_hits;
363 } else if self.n200.is_none() {
364 goods += remaining_hits;
365 } else if self.n300.is_none() {
366 greats += remaining_hits;
367 } else if self.n320.is_none() {
368 perfects += remaining_hits;
369 } else {
370 n50 += remaining_hits;
371 }
372 }
373
374 n50
375 } else {
376 remaining_hits
377 };
378
379 ManiaScoreState {
380 n320: perfects,
381 n300: greats,
382 n200: goods,
383 n100: oks,
384 n50: mehs,
385 misses,
386 }
387 };
388
389 let generate_slow = |acc: f64| {
390 let target = acc * f64::from(if classic { 60 } else { 61 } * n_objects);
391
392 let mut best = ManiaScoreState {
393 n320,
394 n300,
395 n200,
396 n100,
397 n50: n_remaining.saturating_sub(n320 + n300 + n200 + n100),
398 misses,
399 };
400
401 let mut best_dist = f64::INFINITY;
402
403 let remaining = n_remaining.saturating_sub(n300 + n200 + n100 + n50);
404
405 let mut min_n320 = cmp::min(
406 (if classic {
407 ((target - f64::from(40 * n_remaining) + f64::from(20 * n100 + 30 * n50))
408 / 20.0)
409 - f64::from(n300)
410 } else {
411 target - f64::from(60 * n_remaining)
412 + f64::from(20 * n200 + 40 * n100 + 50 * n50)
413 })
414 .floor() as u32,
415 remaining,
416 );
417
418 let mut max_n320 = cmp::min(
419 ((target - f64::from(10 * n_remaining + 50 * n300 + 30 * n200 + 10 * n100))
420 / if classic { 50.0 } else { 51.0 })
421 .ceil() as u32,
422 remaining,
423 );
424
425 if let Some(n320) = self.n320 {
426 min_n320 = min_remaining(n320);
427 max_n320 = min_remaining(n320);
428 }
429
430 for n320 in min_n320..=max_n320 {
431 let remaining = n_remaining.saturating_sub(n320 + n200 + n100 + n50);
432
433 let mut min_n300 = cmp::min(
434 (if classic && self.n320.is_none() {
435 0.0
439 } else {
440 let n320_weight = if classic { 20 } else { 21 };
441
442 (target - f64::from(40 * n_remaining + n320_weight * n320)
443 + f64::from(20 * n100 + 30 * n50))
444 / 20.0
445 })
446 .floor() as u32,
447 remaining,
448 );
449
450 let mut max_n300 = cmp::min(
451 (if classic && self.n320.is_none() {
452 0.0
453 } else {
454 let n320_weight = if classic { 50 } else { 51 };
455
456 (target
457 - f64::from(
458 10 * n_remaining + n320_weight * n320 + 30 * n200 + 10 * n100,
459 ))
460 / 50.0
461 })
462 .ceil() as u32,
463 remaining,
464 );
465
466 if let Some(n300) = self.n300 {
467 min_n300 = min_remaining(n300);
468 max_n300 = min_remaining(n300);
469 }
470
471 for n300 in min_n300..=max_n300 {
472 let remaining = n_remaining.saturating_sub(n320 + n300 + n100 + n50);
473
474 let n320_weight = if classic { 50 } else { 51 };
475
476 let mut min_n200 = cmp::min(
477 ((target - f64::from(20 * n_remaining + n320_weight * n320 + 50 * n300)
478 + f64::from(10 * n50))
479 / 30.0)
480 .floor() as u32,
481 remaining,
482 );
483
484 let mut max_n200 = cmp::min(
485 ((target
486 - f64::from(
487 10 * n_remaining + n320_weight * n320 + 50 * n300 + 10 * n100,
488 ))
489 / 30.0)
490 .ceil() as u32,
491 remaining,
492 );
493
494 if let Some(n200) = self.n200 {
495 min_n200 = min_remaining(n200);
496 max_n200 = min_remaining(n200);
497 }
498
499 for n200 in min_n200..=max_n200 {
500 let n100s = if let Some(n100) = self.n100 {
501 [min_remaining(n100), min_remaining(n100)]
502 } else {
503 let remaining = n_remaining.saturating_sub(n320 + n300 + n200 + n50);
504
505 let n100_raw = if self.n50.is_some() {
506 let n320_weight = if classic { 41 } else { 42 };
507
508 target
509 - f64::from(
510 19 * n_remaining
511 + n320_weight * n320
512 + 41 * n300
513 + 21 * n200,
514 )
515 + f64::from(9 * n50)
516 } else {
517 let n320_weight = if classic { 50 } else { 51 };
518
519 (target
520 - f64::from(
521 10 * n_remaining
522 + n320_weight * n320
523 + 50 * n300
524 + 30 * n200,
525 ))
526 / 10.0
527 };
528
529 let min = cmp::min(n100_raw.floor() as u32, remaining);
530 let max = cmp::min(n100_raw.ceil() as u32, remaining);
531
532 [min, max]
533 };
534
535 for n100 in n100s {
536 let n50 = if let Some(n50) = self.n50 {
537 min_remaining(n50)
538 } else {
539 n_remaining.saturating_sub(n320 + n300 + n200 + n100)
540 };
541
542 let mut curr = ManiaScoreState {
543 n320,
544 n300,
545 n200,
546 n100,
547 n50,
548 misses,
549 };
550
551 if curr.total_hits() < n_objects {
552 let remaining = n_objects - curr.total_hits();
553
554 match (self.n50, self.n100, self.n200, self.n300, self.n320) {
555 (None, ..) => curr.n50 += remaining,
556 (_, None, ..) => curr.n100 += remaining,
557 (_, _, None, ..) => curr.n200 += remaining,
558 (.., None, _) => curr.n300 += remaining,
559 (.., None) => curr.n320 += remaining,
560 _ => curr.n50 += remaining,
561 }
562 }
563
564 let curr_acc = curr.accuracy(classic);
565 let curr_dist = (acc - curr_acc).abs();
566
567 if curr_dist < best_dist {
568 best_dist = curr_dist;
569 best = curr;
570 }
571 }
572 }
573 }
574 }
575
576 if classic && self.n320.is_none() {
579 if self.n300.is_none() {
583 best.n320 += best.n300;
584 best.n300 = 0;
585 }
586
587 match priority {
588 HitResultPriority::BestCase | HitResultPriority::Fastest => {
589 if self.n100.is_none() && self.n200.is_none() {
590 let n = best.n200 / 2;
591 best.n320 += n;
592 best.n200 -= 2 * n;
593 best.n100 += n;
594 }
595
596 if self.n50.is_none() && self.n200.is_none() {
597 let n = best.n200 / 5;
598 best.n320 += n * 3;
599 best.n200 -= n * 5;
600 best.n50 += n * 2;
601 }
602
603 if self.n300.is_none() {
604 best.n320 += best.n300;
605 best.n300 = 0;
606 }
607 }
608 HitResultPriority::WorstCase => {
609 if self.n100.is_none() && self.n200.is_none() {
610 let n = cmp::min(best.n320, best.n100);
611 best.n320 -= n;
612 best.n200 += 2 * n;
613 best.n100 -= n;
614 }
615
616 if self.n50.is_none() && self.n200.is_none() {
617 let n = cmp::min(best.n320 / 3, best.n50 / 2);
618 best.n320 -= n * 3;
619 best.n200 += n * 5;
620 best.n50 -= n * 2;
621 }
622
623 if self.n300.is_none() {
624 best.n300 += best.n320;
625 best.n320 = 0;
626 }
627 }
628 }
629 }
630
631 best
632 };
633
634 if let Some(acc) = self.acc {
635 match (self.n320, self.n300, self.n200, self.n100, self.n50) {
636 (Some(_), Some(_), Some(_), Some(_), Some(_)) => {
638 let remaining =
639 n_objects.saturating_sub(n320 + n300 + n200 + n100 + n50 + misses);
640
641 match priority {
642 HitResultPriority::BestCase | HitResultPriority::Fastest => {
643 n320 += remaining;
644 }
645 HitResultPriority::WorstCase => n50 += remaining,
646 }
647 }
648
649 (None, Some(_), Some(_), Some(_), Some(_)) => n320 = n_remaining,
651 (Some(_), None, Some(_), Some(_), Some(_)) => n300 = n_remaining,
652 (Some(_), Some(_), None, Some(_), Some(_)) => n200 = n_remaining,
653 (Some(_), Some(_), Some(_), None, Some(_)) => n100 = n_remaining,
654 (Some(_), Some(_), Some(_), Some(_), None) => n50 = n_remaining,
655
656 _ => {
658 let best = match priority {
659 HitResultPriority::Fastest => generate_fast(acc),
660 _ => generate_slow(acc),
661 };
662
663 n320 = best.n320;
664 n300 = best.n300;
665 n200 = best.n200;
666 n100 = best.n100;
667 n50 = best.n50;
668 }
669 }
670 } else {
671 let remaining = n_remaining.saturating_sub(n320 + n300 + n200 + n100 + n50);
672
673 match priority {
674 HitResultPriority::BestCase | HitResultPriority::Fastest => {
675 match (self.n320, self.n300, self.n200, self.n100, self.n50) {
676 (None, ..) => n320 = remaining,
677 (_, None, ..) => n300 = remaining,
678 (_, _, None, ..) => n200 = remaining,
679 (.., None, _) => n100 = remaining,
680 (.., None) => n50 = remaining,
681 _ => n320 += remaining,
682 }
683 }
684 HitResultPriority::WorstCase => {
685 match (self.n50, self.n100, self.n200, self.n300, self.n320) {
686 (None, ..) => n50 = remaining,
687 (_, None, ..) => n100 = remaining,
688 (_, _, None, ..) => n200 = remaining,
689 (.., None, _) => n300 = remaining,
690 (.., None) => n320 = remaining,
691 _ => n50 += remaining,
692 }
693 }
694 }
695 }
696
697 self.n320 = Some(n320);
698 self.n300 = Some(n300);
699 self.n200 = Some(n200);
700 self.n100 = Some(n100);
701 self.n50 = Some(n50);
702 self.misses = Some(misses);
703
704 Ok(ManiaScoreState {
705 n320,
706 n300,
707 n200,
708 n100,
709 n50,
710 misses,
711 })
712 }
713
714 pub fn calculate(mut self) -> Result<ManiaPerformanceAttributes, ConvertError> {
716 let state = self.generate_state()?;
717
718 let attrs = match self.map_or_attrs {
719 MapOrAttrs::Attrs(attrs) => attrs,
720 MapOrAttrs::Map(ref map) => self.difficulty.calculate_for_mode::<Mania>(map)?,
721 };
722
723 Ok(ManiaPerformanceCalculator::new(attrs, self.difficulty.get_mods(), state).calculate())
724 }
725
726 pub(crate) const fn from_map_or_attrs(map_or_attrs: MapOrAttrs<'map, Mania>) -> Self {
727 Self {
728 map_or_attrs,
729 difficulty: Difficulty::new(),
730 n320: None,
731 n300: None,
732 n200: None,
733 n100: None,
734 n50: None,
735 misses: None,
736 acc: None,
737 hitresult_priority: HitResultPriority::DEFAULT,
738 }
739 }
740}
741
742impl<'map> TryFrom<OsuPerformance<'map>> for ManiaPerformance<'map> {
743 type Error = OsuPerformance<'map>;
744
745 fn try_from(mut osu: OsuPerformance<'map>) -> Result<Self, Self::Error> {
751 let mods = osu.difficulty.get_mods();
752
753 let map = match OsuPerformance::try_convert_map(osu.map_or_attrs, GameMode::Mania, mods) {
754 Ok(map) => map,
755 Err(map_or_attrs) => {
756 osu.map_or_attrs = map_or_attrs;
757
758 return Err(osu);
759 }
760 };
761
762 let OsuPerformance {
763 map_or_attrs: _,
764 difficulty,
765 acc,
766 combo: _,
767 large_tick_hits: _,
768 small_tick_hits: _,
769 slider_end_hits: _,
770 n300,
771 n100,
772 n50,
773 misses,
774 hitresult_priority,
775 } = osu;
776
777 Ok(Self {
778 map_or_attrs: MapOrAttrs::Map(map),
779 difficulty,
780 n320: None,
781 n300,
782 n200: None,
783 n100,
784 n50,
785 misses,
786 acc,
787 hitresult_priority,
788 })
789 }
790}
791
792impl<'map, T: IntoModePerformance<'map, Mania>> From<T> for ManiaPerformance<'map> {
793 fn from(into: T) -> Self {
794 into.into_performance()
795 }
796}
797
798#[cfg(test)]
799mod tests {
800 use std::{cmp::Ordering, sync::OnceLock, time::Instant};
801
802 use proptest::{
803 prelude::*,
804 test_runner::{RngAlgorithm, TestRng},
805 };
806 use rosu_map::section::general::GameMode;
807 use rosu_mods::GameMod;
808
809 use crate::{
810 any::{DifficultyAttributes, PerformanceAttributes},
811 mania::ManiaDifficultyAttributes,
812 osu::{OsuDifficultyAttributes, OsuPerformanceAttributes},
813 Beatmap,
814 };
815
816 use super::{calculator::custom_accuracy, *};
817
818 static ATTRS: OnceLock<ManiaDifficultyAttributes> = OnceLock::new();
819
820 const N_OBJECTS: u32 = 594;
821 const N_HOLD_NOTES: u32 = 121;
822
823 fn beatmap() -> Beatmap {
824 Beatmap::from_path("./resources/1638954.osu").unwrap()
825 }
826
827 fn attrs() -> ManiaDifficultyAttributes {
828 ATTRS
829 .get_or_init(|| {
830 let map = beatmap();
831 let attrs = Difficulty::new().calculate_for_mode::<Mania>(&map).unwrap();
832
833 assert_eq!(N_OBJECTS, map.hit_objects.len() as u32);
834 assert_eq!(
835 N_HOLD_NOTES,
836 map.hit_objects.iter().filter(|h| !h.is_circle()).count() as u32
837 );
838
839 attrs
840 })
841 .to_owned()
842 }
843
844 fn mods(classic: bool) -> rosu_mods::GameMods {
847 if classic {
848 let mut mods = rosu_mods::GameMods::new();
849 mods.insert(GameMod::ClassicMania(Default::default()));
850
851 mods
852 } else {
853 rosu_mods::GameMods::new()
854 }
855 }
856
857 #[allow(clippy::too_many_arguments, clippy::too_many_lines)]
863 fn brute_force_best(
864 classic: bool,
865 acc: f64,
866 n320: Option<u32>,
867 n300: Option<u32>,
868 n200: Option<u32>,
869 n100: Option<u32>,
870 n50: Option<u32>,
871 misses: u32,
872 best_case: bool,
873 ) -> ManiaScoreState {
874 let misses = cmp::min(misses, N_OBJECTS);
875
876 let mut best_state = ManiaScoreState {
877 misses,
878 ..Default::default()
879 };
880
881 let mut best_dist = f64::INFINITY;
882 let mut best_custom_acc = 0.0;
883
884 let multiple_given = (usize::from(n320.is_some())
885 + usize::from(n300.is_some())
886 + usize::from(n200.is_some())
887 + usize::from(n100.is_some())
888 + usize::from(n50.is_some()))
889 > 1;
890
891 let mut n_objects = N_OBJECTS;
892
893 if !classic {
894 n_objects += N_HOLD_NOTES;
895 }
896
897 let n_remaining = n_objects - misses;
898
899 let target = acc * f64::from(if classic { 60 } else { 61 } * n_objects);
900
901 let max_left = n_objects.saturating_sub(
902 if classic { 0 } else { n300.unwrap_or(0) }
903 + n200.unwrap_or(0)
904 + n100.unwrap_or(0)
905 + n50.unwrap_or(0)
906 + misses,
907 );
908
909 let min_n320 = cmp::min(
910 max_left,
911 if classic {
912 (target - f64::from(40 * n_remaining)) / 20.0
913 } else {
914 target - f64::from(60 * n_remaining)
915 }
916 .floor() as u32,
917 );
918
919 let max_n320 = cmp::min(
920 max_left,
921 ((target - f64::from(10 * n_remaining)) / if classic { 50.0 } else { 51.0 }).ceil()
922 as u32,
923 );
924
925 let (min_n320, max_n320) = match (n320, n300) {
926 (Some(n320), _) if !classic => {
927 (cmp::min(n_remaining, n320), cmp::min(n_remaining, n320))
928 }
929 (None, _) if !classic => (min_n320, max_n320),
930 (Some(n320), Some(n300)) => (
931 cmp::min(n_remaining, n320 + n300),
932 cmp::min(n_remaining, n320 + n300),
933 ),
934 (Some(n320), None) => (
935 cmp::max(cmp::min(n_remaining, n320), min_n320),
936 cmp::max(max_n320, cmp::min(n320, n_remaining)),
937 ),
938 (None, Some(n300)) => (
939 cmp::max(cmp::min(n_remaining, n300), min_n320),
940 cmp::max(max_n320, cmp::min(n300, n_remaining)),
941 ),
942 (None, None) => (min_n320, max_n320),
943 };
944
945 let mut n300_iters = 0;
946 let mut n300_skips = 0;
947
948 for new320 in min_n320..=max_n320 {
949 let max_left = n_remaining
950 .saturating_sub(new320 + n200.unwrap_or(0) + n100.unwrap_or(0) + n50.unwrap_or(0));
951
952 let (min_n300, max_n300) = match n300 {
953 _ if classic => (0, 0),
954 Some(n300) if multiple_given => {
955 (cmp::min(n_remaining, n300), cmp::min(n_remaining, n300))
956 }
957 Some(n300) => (cmp::min(max_left, n300), cmp::min(max_left, n300)),
958 None if n200.and(n100).and(n50).is_some() => (max_left, max_left),
959 None => (0, max_left),
960 };
961
962 for new300 in min_n300..=max_n300 {
963 let max_left = n_remaining
964 .saturating_sub(new320 + new300 + n100.unwrap_or(0) + n50.unwrap_or(0));
965
966 let min_state = {
967 let n50 = n50.unwrap_or(max_left);
968 let n100 = n100.unwrap_or(
969 n_remaining.saturating_sub(new320 + new300 + n200.unwrap_or(0) + n50),
970 );
971 let n200 =
972 n200.unwrap_or(n_remaining.saturating_sub(new320 + new300 + n100 + n50));
973
974 ManiaScoreState {
975 n320: new320,
976 n300: new300,
977 n200,
978 n100,
979 n50,
980 misses,
981 }
982 };
983
984 let max_state = {
985 let n200 = n200.unwrap_or(max_left);
986 let n100 = n100.unwrap_or(
987 n_remaining.saturating_sub(new320 + new300 + n200 + n50.unwrap_or(0)),
988 );
989 let n50 =
990 n50.unwrap_or(n_remaining.saturating_sub(new320 + new300 + n200 + n100));
991
992 ManiaScoreState {
993 n320: new320,
994 n300: new300,
995 n200,
996 n100,
997 n50,
998 misses,
999 }
1000 };
1001
1002 n300_iters += 1;
1003
1004 if min_state.accuracy(classic) - best_dist > acc
1007 || max_state.accuracy(classic) + best_dist < acc
1008 {
1009 n300_skips += 1;
1010
1011 continue;
1012 }
1013
1014 let (min_n200, max_n200) = match (n200, n100, n50) {
1015 (Some(n200), ..) if multiple_given => {
1016 (cmp::min(n_remaining, n200), cmp::min(n_remaining, n200))
1017 }
1018 (Some(n200), ..) => (cmp::min(max_left, n200), cmp::min(max_left, n200)),
1019 (None, Some(_), Some(_)) => (max_left, max_left),
1020 _ => (0, max_left),
1021 };
1022
1023 for new200 in min_n200..=max_n200 {
1024 let max_left =
1025 n_remaining.saturating_sub(new320 + new300 + new200 + n50.unwrap_or(0));
1026
1027 let (min_n100, max_n100) = match (n100, n50) {
1028 (Some(n100), _) if multiple_given => {
1029 (cmp::min(n_remaining, n100), cmp::min(n_remaining, n100))
1030 }
1031 (Some(n100), _) => (cmp::min(max_left, n100), cmp::min(max_left, n100)),
1032 (None, Some(_)) => (max_left, max_left),
1033 (None, None) => (0, max_left),
1034 };
1035
1036 for new100 in min_n100..=max_n100 {
1037 let max_left =
1038 n_remaining.saturating_sub(new320 + new300 + new200 + new100);
1039
1040 let new50 = match n50 {
1041 Some(n50) if multiple_given => cmp::min(n_remaining, n50),
1042 Some(n50) => cmp::min(max_left, n50),
1043 None => max_left,
1044 };
1045
1046 let (new320, new300) = if classic {
1047 match (n320, n300) {
1048 (Some(n320), Some(n300)) => {
1049 (cmp::min(n_remaining, n320), cmp::min(n_remaining, n300))
1050 }
1051 (Some(n320), None) => (
1052 cmp::min(n320, n_remaining),
1053 new320 - cmp::min(n320, n_remaining),
1054 ),
1055 (None, Some(n300)) => (
1056 new320 - cmp::min(n300, n_remaining),
1057 cmp::min(n300, n_remaining),
1058 ),
1059 (None, None) if best_case => (new320, 0),
1060 (None, None) => (0, new320),
1061 }
1062 } else {
1063 (new320, new300)
1064 };
1065
1066 let curr_acc = ManiaScoreState {
1067 n320: new320,
1068 n300: new300,
1069 n200: new200,
1070 n100: new100,
1071 n50: new50,
1072 misses,
1073 }
1074 .accuracy(classic);
1075
1076 let curr_dist = (acc - curr_acc).abs();
1077
1078 let curr_custom_acc =
1079 custom_accuracy(new320, new300, new200, new100, new50, n_objects);
1080
1081 match curr_dist.total_cmp(&best_dist) {
1082 Ordering::Less => {
1083 best_dist = curr_dist;
1084 best_custom_acc = curr_custom_acc;
1085 best_state.n320 = new320;
1086 best_state.n300 = new300;
1087 best_state.n200 = new200;
1088 best_state.n100 = new100;
1089 best_state.n50 = new50;
1090 }
1091 Ordering::Equal if curr_custom_acc < best_custom_acc => {
1092 best_custom_acc = curr_custom_acc;
1093 best_state.n320 = new320;
1094 best_state.n300 = new300;
1095 best_state.n200 = new200;
1096 best_state.n100 = new100;
1097 best_state.n50 = new50;
1098 }
1099 _ => {}
1100 }
1101 }
1102 }
1103 }
1104 }
1105
1106 eprintln!("Bruteforce skipped {n300_skips}/{n300_iters} n300 iterations");
1107
1108 if best_state.n320 + best_state.n300 + best_state.n200 + best_state.n100 + best_state.n50
1109 < n_remaining
1110 {
1111 let n_remaining = n_remaining
1112 - (best_state.n320
1113 + best_state.n300
1114 + best_state.n200
1115 + best_state.n100
1116 + best_state.n50);
1117
1118 if best_case {
1119 match (n320, n300, n200, n100, n50) {
1120 (None, ..) => best_state.n320 += n_remaining,
1121 (_, None, ..) => best_state.n300 += n_remaining,
1122 (_, _, None, ..) => best_state.n200 += n_remaining,
1123 (.., None, _) => best_state.n100 += n_remaining,
1124 (.., None) => best_state.n50 += n_remaining,
1125 _ => best_state.n320 += n_remaining,
1126 }
1127 } else {
1128 match (n50, n100, n200, n300, n320) {
1129 (None, ..) => best_state.n50 += n_remaining,
1130 (_, None, ..) => best_state.n100 += n_remaining,
1131 (_, _, None, ..) => best_state.n200 += n_remaining,
1132 (.., None, _) => best_state.n300 += n_remaining,
1133 (.., None) => best_state.n320 += n_remaining,
1134 _ => best_state.n50 += n_remaining,
1135 }
1136 }
1137 }
1138
1139 if classic && n320.is_none() {
1140 let before = best_state.clone();
1141
1142 if n300.is_none() {
1143 best_state.n320 += best_state.n300;
1144 best_state.n300 = 0;
1145 }
1146
1147 if best_case {
1148 if n100.is_none() && n200.is_none() {
1149 let n = best_state.n200 / 2;
1150 best_state.n320 += n;
1151 best_state.n200 -= 2 * n;
1152 best_state.n100 += n;
1153 }
1154
1155 if n50.is_none() && n200.is_none() {
1156 let n = best_state.n200 / 5;
1157 best_state.n320 += n * 3;
1158 best_state.n200 -= n * 5;
1159 best_state.n50 += n * 2;
1160 }
1161
1162 if n300.is_none() {
1163 best_state.n320 += best_state.n300;
1164 best_state.n300 = 0;
1165 }
1166 } else {
1167 if n100.is_none() && n200.is_none() {
1168 let n = cmp::min(best_state.n320, best_state.n100);
1169 best_state.n320 -= n;
1170 best_state.n200 += 2 * n;
1171 best_state.n100 -= n;
1172 }
1173
1174 if n50.is_none() && n200.is_none() {
1175 let n = cmp::min(best_state.n320 / 3, best_state.n50 / 2);
1176 best_state.n320 -= n * 3;
1177 best_state.n200 += n * 5;
1178 best_state.n50 -= n * 2;
1179 }
1180
1181 if n300.is_none() {
1182 best_state.n300 += best_state.n320;
1183 best_state.n320 = 0;
1184 }
1185 }
1186
1187 assert_eq!(best_state.accuracy(classic), before.accuracy(classic));
1188 }
1189
1190 best_state
1191 }
1192
1193 proptest! {
1194 #![proptest_config(ProptestConfig {
1195 cases: 20,
1196 ..Default::default()
1197 })]
1198
1199 #[test]
1200 #[ignore = "cannot skip persistent failure cases for some reason which run way too slowly"]
1201 fn mania_hitresults(
1202 classic in prop::bool::ANY,
1203 acc in 0.0_f64..=1.0,
1204 n320 in prop::option::weighted(0.10, 0_u32..=N_OBJECTS + N_HOLD_NOTES + 10),
1205 n300 in prop::option::weighted(0.10, 0_u32..=N_OBJECTS + N_HOLD_NOTES + 10),
1206 n200 in prop::option::weighted(0.10, 0_u32..=N_OBJECTS + N_HOLD_NOTES + 10),
1207 n100 in prop::option::weighted(0.10, 0_u32..=N_OBJECTS + N_HOLD_NOTES + 10),
1208 n50 in prop::option::weighted(0.10, 0_u32..=N_OBJECTS + N_HOLD_NOTES + 10),
1209 n_misses in prop::option::weighted(0.15, 0_u32..=N_OBJECTS + N_HOLD_NOTES + 10),
1210 best_case in prop::bool::ANY,
1211 ) {
1212 exec_mania_hitresults(classic, acc, n320, n300, n200, n100, n50, n_misses, best_case);
1213 }
1214 }
1215
1216 #[test]
1217 fn rng_mania_hitresults() {
1218 fn generate_seed() -> [u8; 16] {
1221 let start = Instant::now();
1222
1223 const LIMIT: usize = 10_000;
1224 let mut is_prime = vec![true; LIMIT + 1];
1225 is_prime.iter_mut().step_by(2).for_each(|n| *n = false);
1226 is_prime[1] = false;
1227 is_prime[2] = true;
1228
1229 for n in (3..=LIMIT).step_by(2) {
1230 if !is_prime[n] {
1231 continue;
1232 }
1233
1234 for m in (n * n..=LIMIT).step_by(n) {
1235 is_prime[m] = false;
1236 }
1237 }
1238
1239 start.elapsed().as_nanos().to_le_bytes()
1240 }
1241
1242 let seed = generate_seed();
1243 eprintln!("seed={seed:?}");
1244 let mut rng = TestRng::from_seed(RngAlgorithm::XorShift, &seed);
1245
1246 const CASES: usize = 4;
1249
1250 for _ in 0..CASES {
1251 const LIMIT: u32 = N_OBJECTS + N_HOLD_NOTES + 10;
1252
1253 let classic = rng.gen();
1254 let acc = rng.gen_range(0.0..=1.0);
1255 let n320 = rng.gen_bool(0.1).then(|| rng.gen_range(0..=LIMIT));
1256 let n300 = rng.gen_bool(0.1).then(|| rng.gen_range(0..=LIMIT));
1257 let n200 = rng.gen_bool(0.1).then(|| rng.gen_range(0..=LIMIT));
1258 let n100 = rng.gen_bool(0.1).then(|| rng.gen_range(0..=LIMIT));
1259 let n50 = rng.gen_bool(0.1).then(|| rng.gen_range(0..=LIMIT));
1260 let n_misses = rng.gen_bool(0.2).then(|| rng.gen_range(0..=LIMIT));
1261 let best_case = rng.gen();
1262
1263 eprintln!(
1264 "classic={} | acc={} | n320={:?} | n300={:?} | n200={:?} | \
1265 n100={:?} | n50={:?} | n_misses={:?} | best_case={}",
1266 classic, acc, n320, n300, n200, n100, n50, n_misses, best_case,
1267 );
1268
1269 exec_mania_hitresults(
1270 classic, acc, n320, n300, n200, n100, n50, n_misses, best_case,
1271 );
1272 }
1273 }
1274
1275 fn exec_mania_hitresults(
1276 classic: bool,
1277 acc: f64,
1278 n320: Option<u32>,
1279 n300: Option<u32>,
1280 n200: Option<u32>,
1281 n100: Option<u32>,
1282 n50: Option<u32>,
1283 n_misses: Option<u32>,
1284 best_case: bool,
1285 ) {
1286 let priority = if best_case {
1287 HitResultPriority::BestCase
1288 } else {
1289 HitResultPriority::WorstCase
1290 };
1291
1292 let mut state = ManiaPerformance::from(attrs())
1293 .accuracy(acc * 100.0)
1294 .lazer(!classic)
1295 .mods(mods(classic))
1296 .hitresult_priority(priority);
1297
1298 if let Some(n320) = n320 {
1299 state = state.n320(n320);
1300 }
1301
1302 if let Some(n300) = n300 {
1303 state = state.n300(n300);
1304 }
1305
1306 if let Some(n200) = n200 {
1307 state = state.n200(n200);
1308 }
1309
1310 if let Some(n100) = n100 {
1311 state = state.n100(n100);
1312 }
1313
1314 if let Some(n50) = n50 {
1315 state = state.n50(n50);
1316 }
1317
1318 if let Some(misses) = n_misses {
1319 state = state.misses(misses);
1320 }
1321
1322 let start = Instant::now();
1323 let first = state.generate_state().unwrap();
1324 let state_elapsed = start.elapsed();
1325 let state = state.generate_state().unwrap();
1326 assert_eq!(first, state);
1327
1328 let start = Instant::now();
1329 let expected = brute_force_best(
1330 classic,
1331 acc,
1332 n320,
1333 n300,
1334 n200,
1335 n100,
1336 n50,
1337 n_misses.unwrap_or(0),
1338 best_case,
1339 );
1340 let bf_elapsed = start.elapsed();
1341
1342 eprintln!("Elapsed: state={state_elapsed:?} bf={bf_elapsed:?}");
1343
1344 assert_eq!(
1345 state,
1346 expected,
1347 "dist: {} vs {}",
1348 (state.accuracy(classic) - acc).abs(),
1349 (expected.accuracy(classic) - acc).abs(),
1350 );
1351 }
1352
1353 #[test]
1354 fn hitresults_n320_misses_best() {
1355 let classic = true;
1356
1357 let state = ManiaPerformance::from(attrs())
1358 .lazer(!classic)
1359 .mods(mods(classic))
1360 .n320(500)
1361 .misses(2)
1362 .hitresult_priority(HitResultPriority::BestCase)
1363 .generate_state()
1364 .unwrap();
1365
1366 let expected = ManiaScoreState {
1367 n320: 500,
1368 n300: 92,
1369 n200: 0,
1370 n100: 0,
1371 n50: 0,
1372 misses: 2,
1373 };
1374
1375 assert_eq!(state, expected);
1376 }
1377
1378 #[test]
1379 fn hitresults_n100_n50_misses_worst() {
1380 let classic = true;
1381
1382 let state = ManiaPerformance::from(attrs())
1383 .lazer(!classic)
1384 .mods(mods(classic))
1385 .n100(200)
1386 .n50(50)
1387 .misses(2)
1388 .hitresult_priority(HitResultPriority::WorstCase)
1389 .generate_state()
1390 .unwrap();
1391
1392 let expected = ManiaScoreState {
1393 n320: 0,
1394 n300: 0,
1395 n200: 342,
1396 n100: 200,
1397 n50: 50,
1398 misses: 2,
1399 };
1400
1401 assert_eq!(state, expected);
1402 }
1403
1404 #[test]
1405 fn create() {
1406 let mut map = beatmap();
1407
1408 let _ = ManiaPerformance::new(ManiaDifficultyAttributes::default());
1409 let _ = ManiaPerformance::new(ManiaPerformanceAttributes::default());
1410 let _ = ManiaPerformance::new(&map);
1411 let _ = ManiaPerformance::new(map.clone());
1412
1413 let _ = ManiaPerformance::try_new(ManiaDifficultyAttributes::default()).unwrap();
1414 let _ = ManiaPerformance::try_new(ManiaPerformanceAttributes::default()).unwrap();
1415 let _ = ManiaPerformance::try_new(DifficultyAttributes::Mania(
1416 ManiaDifficultyAttributes::default(),
1417 ))
1418 .unwrap();
1419 let _ = ManiaPerformance::try_new(PerformanceAttributes::Mania(
1420 ManiaPerformanceAttributes::default(),
1421 ))
1422 .unwrap();
1423 let _ = ManiaPerformance::try_new(&map).unwrap();
1424 let _ = ManiaPerformance::try_new(map.clone()).unwrap();
1425
1426 let _ = ManiaPerformance::from(ManiaDifficultyAttributes::default());
1427 let _ = ManiaPerformance::from(ManiaPerformanceAttributes::default());
1428 let _ = ManiaPerformance::from(&map);
1429 let _ = ManiaPerformance::from(map.clone());
1430
1431 let _ = ManiaDifficultyAttributes::default().performance();
1432 let _ = ManiaPerformanceAttributes::default().performance();
1433
1434 assert!(map
1435 .convert_mut(GameMode::Osu, &GameMods::default())
1436 .is_err());
1437
1438 assert!(ManiaPerformance::try_new(OsuDifficultyAttributes::default()).is_none());
1439 assert!(ManiaPerformance::try_new(OsuPerformanceAttributes::default()).is_none());
1440 assert!(ManiaPerformance::try_new(DifficultyAttributes::Osu(
1441 OsuDifficultyAttributes::default()
1442 ))
1443 .is_none());
1444 assert!(ManiaPerformance::try_new(PerformanceAttributes::Osu(
1445 OsuPerformanceAttributes::default()
1446 ))
1447 .is_none());
1448 }
1449}