fixed & extended test cases

This commit is contained in:
MaxOhn
2022-10-21 11:07:29 +02:00
parent 17ce0ef2c9
commit 335f0a7dde
27 changed files with 1677 additions and 898 deletions
+40 -24
View File
@@ -22,14 +22,13 @@ let map = match Beatmap::from_path("/path/to/file.osu") {
let result = map.pp()
.mods(24) // HDHR
.combo(1234)
.accuracy(99.2)
.misses(2)
.accuracy(99.2) // should be called last
.calculate();
println!("PP: {}", result.pp());
// If you intend to reuse the current map-mod combination,
// make use of the previous result!
// If you want to reuse the current map-mod combination, make use of the previous result!
// If attributes are given, then stars & co don't have to be recalculated.
let next_result = map.pp()
.mods(24) // HDHR
@@ -68,7 +67,7 @@ let map = match Beatmap::from_path("/path/to/file.osu").await {
let result = map.pp()
.mods(24) // HDHR
.combo(1234)
.misses(2)
.n_misses(2)
.accuracy(99.2)
.calculate();
@@ -113,12 +112,12 @@ let mut gradual_performance = map.gradual_performance(mods);
// The default score state is kinda chunky because it considers all modes.
let state = ScoreState {
max_combo: 1,
n_katu: 0, // only relevant for ctb
n_geki: 0, // only relevant for mania
n_katu: 0, // only relevant for mania and ctb
n300: 1,
n100: 0,
n50: 0,
misses: 0,
score: 300, // only relevant for mania
n_misses: 0,
};
// Process the score state after the first object
@@ -144,7 +143,7 @@ let state = TaikoScoreState {
max_combo: 11,
n300: 9,
n100: 1,
misses: 1,
n_misses: 1,
};
// Process the next 10 objects in one go
@@ -158,29 +157,46 @@ println!("PP after the first 11 objects: {}", curr_performance.pp());
### Features
| Flag | Description |
|-----|-----|
| `default` | Beatmap parsing will be non-async |
| `async_tokio` | Beatmap parsing will be async through [tokio](https://github.com/tokio-rs/tokio) |
| `async_std` | Beatmap parsing will be async through [async-std](https://github.com/async-rs/async-std) |
| Flag | Description |
| ------------- | ---------------------------------------------------------------------------------------- |
| `default` | Beatmap parsing will be non-async |
| `async_tokio` | Beatmap parsing will be async through [tokio](https://github.com/tokio-rs/tokio) |
| `async_std` | Beatmap parsing will be async through [async-std](https://github.com/async-rs/async-std) |
### Version
A large portion of this repository is a port of [osu!lazer](https://github.com/ppy/osu)'s difficulty and performance calculation.
- osu!:
- osu!lazer: Commit `85adfc2df7d931164181e145377a6ced8db2bfb3` (Wed Sep 28 18:26:36 2022 +0300)
- osu!tools: Commit `146d5916937161ef65906aa97f85d367035f3712` (Sat Oct 8 14:28:49 2022 +0900)
- taiko:
- osu!lazer: Commit `234c6ac7998fbc6742503e1a589536255554e56a` (Wed Oct 5 20:21:15 2022 +0900)
- osu!tools: Commit `146d5916937161ef65906aa97f85d367035f3712` (Sat Oct 8 14:28:49 2022 +0900)
- catch: (will be updated on the next rework)
- osu!lazer: -
- osu!tools: -
- mania:
- osu!lazer: Commit `7342fb7f51b34533a42bffda89c3d6c569cc69ce` (Tue Oct 11 14:34:50 2022 +0900)
- osu!tools: Commit `146d5916937161ef65906aa97f85d367035f3712` (Sat Oct 8 14:28:49 2022 +0900)
### Accuracy
Here are some plots showing the differences of `rosu-pp`'s values and osu!'s official [osu-tools](https://github.com/ppy/osu-tools).
The difficulty and performance attributes generated by [osu-tools](https://github.com/ppy/osu-tools) itself were compared with rosu-pp's results when running on `130,000` different maps. Additionally, multiple mod combinations were tested depending on the mode:
Note that osu-tools was used on [this commit](https://github.com/ppy/osu/commit/9fb2402781ad91c197d51aeec716b0000f52c4d1) which is currently (2021-11-14) accurate for osu!standard but for other modes it might include changes that were not applied into stable and thus not implemented in rosu-pp.
- osu!: NM, EZ, HD, HR, DT
- taiko: NM, HD, HR, DT (+ all osu! converts)
- catch: -
- mania: NM, DT (+ all osu! converts)
osu!standard: (very accurate, flashlight has the highest average but is still very small)
<img src="./pp-plot/osu_accuracy.svg">
For every (!) comparison of the star and pp values, the error margin was below `0.000000001`, ensuing a great accuracy.
osu!mania: (close to perfect values)
<img src="./pp-plot/mania_accuracy.svg">
### Benchmark
osu!catch: (pretty accurate)
<img src="./pp-plot/catch_accuracy.svg">
osu!taiko: (decently accurate, potentially more imprecise due to non-live changes in osu-tools)
<img src="./pp-plot/taiko_accuracy.svg">
To be done
### Bindings
-26
View File
@@ -209,29 +209,3 @@ impl Iterator for CatchObjectIter<'_> {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_map() {
let map = Beatmap::default();
let mut attributes = CatchGradualDifficultyAttributes::new(&map, 0);
assert!(attributes.next().is_none());
}
#[cfg(not(any(feature = "async_tokio", feature = "async_std")))]
#[test]
fn iter_end_eq_regular() {
let map = Beatmap::from_path("./maps/2118524.osu").expect("failed to parse map");
let mods = 64;
let regular = crate::CatchStars::new(&map).mods(mods).calculate();
let iter_end = CatchGradualDifficultyAttributes::new(&map, mods)
.last()
.expect("empty iter");
assert_eq!(regular, iter_end);
}
}
+2 -106
View File
@@ -78,7 +78,7 @@ impl CatchScoreState {
/// // Then comes a miss.
/// // Note that state's max combo won't be incremented for
/// // the next few objects because the combo is reset.
/// state.misses += 1;
/// state.n_misses += 1;
/// # /*
/// let performance = gradual_perf.process_next_object(state.clone()).unwrap();
/// println!("PP: {}", performance.pp);
@@ -114,7 +114,7 @@ impl CatchScoreState {
/// state.n_droplets = ...
/// state.n_tiny_droplets = ...
/// state.n_tiny_droplet_misses = ...
/// state.misses = ...
/// state.n_misses = ...
/// let final_performance = gradual_perf.process_next_n_objects(state.clone(), usize::MAX).unwrap();
/// println!("PP: {}", performance.pp);
/// # */
@@ -187,107 +187,3 @@ impl<'map> CatchGradualPerformanceAttributes<'map> {
Some(performance)
}
}
#[cfg(test)]
mod tests {
#[allow(unused_imports)]
use super::*;
#[cfg(not(any(feature = "async_tokio", feature = "async_std")))]
#[test]
fn correct_empty() {
let map = Beatmap::from_path("./maps/2118524.osu").expect("failed to parse map");
let mods = 64;
let mut gradual = CatchGradualPerformanceAttributes::new(&map, mods);
let state = CatchScoreState::default();
assert!(gradual
.process_next_n_objects(state.clone(), usize::MAX)
.is_some());
assert!(gradual.process_next_object(state).is_none());
}
#[cfg(not(any(feature = "async_tokio", feature = "async_std")))]
#[test]
fn next_and_next_n() {
let map = Beatmap::from_path("./maps/2118524.osu").expect("failed to parse map");
let mods = 64;
let state = CatchScoreState::default();
let mut gradual1 = CatchGradualPerformanceAttributes::new(&map, mods);
let mut gradual2 = CatchGradualPerformanceAttributes::new(&map, mods);
for _ in 0..20 {
let _ = gradual1.process_next_object(state.clone());
let _ = gradual2.process_next_object(state.clone());
}
let n = 80;
for _ in 1..n {
let _ = gradual1.process_next_object(state.clone());
}
let state = CatchScoreState {
max_combo: 101,
n_fruits: 99,
n_droplets: 2,
n_tiny_droplets: 68,
n_tiny_droplet_misses: 0,
n_misses: 0,
};
let next = gradual1.process_next_object(state.clone());
let next_n = gradual2.process_next_n_objects(state, n);
assert_eq!(next_n, next);
}
#[cfg(not(any(feature = "async_tokio", feature = "async_std")))]
#[test]
fn gradual_end_eq_regular() {
let map = Beatmap::from_path("./maps/2118524.osu").expect("failed to parse map");
let mods = 64;
let regular = CatchPP::new(&map).mods(mods).calculate();
let mut gradual = CatchGradualPerformanceAttributes::new(&map, mods);
let state = CatchScoreState {
max_combo: 730,
n_fruits: 728,
n_droplets: 2,
n_tiny_droplets: 291,
n_tiny_droplet_misses: 0,
n_misses: 0,
};
let gradual_end = gradual.process_next_n_objects(state, usize::MAX).unwrap();
assert_eq!(regular, gradual_end);
}
#[cfg(not(any(feature = "async_tokio", feature = "async_std")))]
#[test]
fn gradual_eq_regular_passed() {
let map = Beatmap::from_path("./maps/2118524.osu").expect("failed to parse map");
let mods = 64;
let n = 100;
let regular = CatchPP::new(&map).mods(mods).passed_objects(n).calculate();
let mut gradual = CatchGradualPerformanceAttributes::new(&map, mods);
let state = CatchScoreState {
max_combo: 101,
n_fruits: 99,
n_droplets: 2,
n_tiny_droplets: 68,
n_tiny_droplet_misses: 0,
n_misses: 0,
};
let gradual = gradual.process_next_n_objects(state, n).unwrap();
assert_eq!(regular, gradual);
}
}
+2 -2
View File
@@ -23,8 +23,8 @@ use crate::{Beatmap, DifficultyAttributes, Mods, OsuPP, PerformanceAttributes};
/// println!("PP: {} | Stars: {}", pp_result.pp(), pp_result.stars());
///
/// let next_result = CatchPP::new(&map)
/// .attributes(pp_result) // reusing previous results for performance
/// .mods(8 + 64) // has to be the same to reuse attributes
/// .attributes(pp_result) // reusing previous results for performance
/// .mods(8 + 64) // has to be the same to reuse attributes
/// .accuracy(99.5)
/// .calculate();
///
+1 -4
View File
@@ -214,7 +214,6 @@ impl From<ScoreState> for ManiaScoreState {
/// for _ in 0..10 {
/// state.n300 += 1;
/// state.max_combo += 1;
/// state.score += 123;
///
/// # /*
/// let performance = gradual_perf.process_next_object(state.clone()).unwrap();
@@ -226,7 +225,7 @@ impl From<ScoreState> for ManiaScoreState {
/// // Then comes a miss.
/// // Note that state's max combo won't be incremented for
/// // the next few objects because the combo is reset.
/// state.misses += 1;
/// state.n_misses += 1;
/// # /*
/// let performance = gradual_perf.process_next_object(state.clone()).unwrap();
/// println!("PP: {}", performance.pp);
@@ -238,7 +237,6 @@ impl From<ScoreState> for ManiaScoreState {
/// state.n300 += 2;
/// state.n100 += 7;
/// state.n50 += 1;
/// state.score += 987;
/// // Don't forget state.n_katu
/// # /*
/// let performance = gradual_perf.process_next_n_objects(state.clone(), 10).unwrap();
@@ -249,7 +247,6 @@ impl From<ScoreState> for ManiaScoreState {
/// // Now comes another 300. Note that the max combo gets incremented again.
/// state.n300 += 1;
/// state.max_combo += 1;
/// state.score += 123;
/// # /*
/// let performance = gradual_perf.process_next_object(state.clone()).unwrap();
/// println!("PP: {}", performance.pp);
+70 -73
View File
@@ -15,25 +15,24 @@
//! };
//! # */ let map = Beatmap::default();
//!
//! // If `BeatmapExt` is included, you can make use of
//! // some methods on `Beatmap` to make your life simpler.
//! // If `BeatmapExt` is included, you can make use of some methods
//! // on `Beatmap` to make your life simpler like `BeatmapExt::pp`.
//! let result = map.pp()
//! .mods(24) // HDHR
//! .combo(1234)
//! .misses(2)
//! .accuracy(99.2) // should be called last
//! .accuracy(99.2)
//! .n_misses(2)
//! .calculate();
//!
//! println!("PP: {}", result.pp());
//!
//! // If you intend to reuse the current map-mod combination,
//! // make use of the previous result!
//! // If you want to reuse the current map-mod combination, make use of the previous result!
//! // If attributes are given, then stars & co don't have to be recalculated.
//! let next_result = map.pp()
//! .mods(24) // HDHR
//! .attributes(result) // recycle
//! .combo(543)
//! .misses(5)
//! .n_misses(5)
//! .n50(3)
//! .accuracy(96.5)
//! .calculate();
@@ -68,7 +67,7 @@
//! let result = map.pp()
//! .mods(24) // HDHR
//! .combo(1234)
//! .misses(2)
//! .n_misses(2)
//! .accuracy(99.2)
//! .calculate();
//!
@@ -114,12 +113,12 @@
//! // The default score state is kinda chunky because it considers all modes.
//! let state = ScoreState {
//! max_combo: 1,
//! n_katu: 0, // only relevant for ctb
//! n_geki: 0, // only relevant for mania
//! n_katu: 0, // only relevant for mania and ctb
//! n300: 1,
//! n100: 0,
//! n50: 0,
//! misses: 0,
//! score: 300, // only relevant for mania
//! n_misses: 0,
//! };
//!
//! // Process the score state after the first object
@@ -145,7 +144,7 @@
//! max_combo: 11,
//! n300: 9,
//! n100: 1,
//! misses: 1,
//! n_misses: 1,
//! };
//!
//! // Process the next 10 objects in one go
@@ -362,23 +361,21 @@ impl DifficultyAttributes {
#[inline]
pub fn stars(&self) -> f64 {
match self {
Self::Catch(attributes) => attributes.stars,
Self::Mania(attributes) => attributes.stars,
Self::Osu(attributes) => attributes.stars,
Self::Taiko(attributes) => attributes.stars,
Self::Catch(attrs) => attrs.stars,
Self::Mania(attrs) => attrs.stars,
Self::Osu(attrs) => attrs.stars,
Self::Taiko(attrs) => attrs.stars,
}
}
/// The maximum combo of the map.
///
/// This will only be `None` for attributes of osu!mania maps.
#[inline]
pub fn max_combo(&self) -> Option<usize> {
pub fn max_combo(&self) -> usize {
match self {
Self::Catch(attributes) => Some(attributes.max_combo()),
Self::Mania(_) => None,
Self::Osu(attributes) => Some(attributes.max_combo),
Self::Taiko(attributes) => Some(attributes.max_combo),
Self::Catch(attrs) => attrs.max_combo(),
Self::Mania(attrs) => attrs.max_combo,
Self::Osu(attrs) => attrs.max_combo,
Self::Taiko(attrs) => attrs.max_combo,
}
}
}
@@ -429,10 +426,10 @@ impl PerformanceAttributes {
#[inline]
pub fn pp(&self) -> f64 {
match self {
Self::Catch(attributes) => attributes.pp,
Self::Mania(attributes) => attributes.pp,
Self::Osu(attributes) => attributes.pp,
Self::Taiko(attributes) => attributes.pp,
Self::Catch(attrs) => attrs.pp,
Self::Mania(attrs) => attrs.pp,
Self::Osu(attrs) => attrs.pp,
Self::Taiko(attrs) => attrs.pp,
}
}
@@ -440,10 +437,10 @@ impl PerformanceAttributes {
#[inline]
pub fn stars(&self) -> f64 {
match self {
Self::Catch(attributes) => attributes.stars(),
Self::Mania(attributes) => attributes.stars(),
Self::Osu(attributes) => attributes.stars(),
Self::Taiko(attributes) => attributes.stars(),
Self::Catch(attrs) => attrs.stars(),
Self::Mania(attrs) => attrs.stars(),
Self::Osu(attrs) => attrs.stars(),
Self::Taiko(attrs) => attrs.stars(),
}
}
@@ -451,23 +448,21 @@ impl PerformanceAttributes {
#[inline]
pub fn difficulty_attributes(&self) -> DifficultyAttributes {
match self {
Self::Catch(attributes) => DifficultyAttributes::Catch(attributes.difficulty.clone()),
Self::Mania(attributes) => DifficultyAttributes::Mania(attributes.difficulty),
Self::Osu(attributes) => DifficultyAttributes::Osu(attributes.difficulty.clone()),
Self::Taiko(attributes) => DifficultyAttributes::Taiko(attributes.difficulty.clone()),
Self::Catch(attrs) => DifficultyAttributes::Catch(attrs.difficulty.clone()),
Self::Mania(attrs) => DifficultyAttributes::Mania(attrs.difficulty),
Self::Osu(attrs) => DifficultyAttributes::Osu(attrs.difficulty.clone()),
Self::Taiko(attrs) => DifficultyAttributes::Taiko(attrs.difficulty.clone()),
}
}
#[inline]
/// The maximum combo of the map.
///
/// This will only be `None` for attributes of osu!mania maps.
pub fn max_combo(&self) -> Option<usize> {
pub fn max_combo(&self) -> usize {
match self {
Self::Catch(f) => Some(f.difficulty.max_combo()),
Self::Mania(_) => None,
Self::Osu(o) => Some(o.difficulty.max_combo),
Self::Taiko(t) => Some(t.difficulty.max_combo),
Self::Catch(attrs) => attrs.difficulty.max_combo(),
Self::Mania(attrs) => attrs.difficulty.max_combo,
Self::Osu(attrs) => attrs.difficulty.max_combo,
Self::Taiko(attrs) => attrs.difficulty.max_combo,
}
}
}
@@ -476,10 +471,10 @@ impl From<PerformanceAttributes> for DifficultyAttributes {
#[inline]
fn from(attributes: PerformanceAttributes) -> Self {
match attributes {
PerformanceAttributes::Catch(attributes) => Self::Catch(attributes.difficulty),
PerformanceAttributes::Mania(attributes) => Self::Mania(attributes.difficulty),
PerformanceAttributes::Osu(attributes) => Self::Osu(attributes.difficulty),
PerformanceAttributes::Taiko(attributes) => Self::Taiko(attributes.difficulty),
PerformanceAttributes::Catch(attrs) => Self::Catch(attrs.difficulty),
PerformanceAttributes::Mania(attrs) => Self::Mania(attrs.difficulty),
PerformanceAttributes::Osu(attrs) => Self::Osu(attrs.difficulty),
PerformanceAttributes::Taiko(attrs) => Self::Taiko(attrs.difficulty),
}
}
}
@@ -521,7 +516,7 @@ mod tests {
#[test]
fn custom() {
let path = "F:/osu!/beatmaps/1186086.osu";
let path = "F:/osu!/beatmaps/1028484.osu";
let map = Beatmap::from_path(path).unwrap();
let attrs = match OsuPP::new(&map).mode(GameMode::Taiko).mods(0).calculate() {
@@ -529,31 +524,33 @@ mod tests {
_ => unreachable!(),
};
println!(
"difficulty:\n\
stamina={}\n\
rhythm={}\n\
colour={}\n\
peak={}\n\
hit_window={}\n\
max_combo={}\n\
stars={}\n\
performance:\n\
difficulty={}\n\
acc={}\n\
effective={}\n\
pp={}\n",
attrs.difficulty.stamina,
attrs.difficulty.rhythm,
attrs.difficulty.colour,
attrs.difficulty.peak,
attrs.difficulty.hit_window,
attrs.difficulty.max_combo,
attrs.difficulty.stars,
attrs.pp_difficulty,
attrs.pp_acc,
attrs.effective_miss_count,
attrs.pp,
);
println!("{attrs:#?}");
// println!(
// "difficulty:\n\
// stamina={}\n\
// rhythm={}\n\
// colour={}\n\
// peak={}\n\
// hit_window={}\n\
// max_combo={}\n\
// stars={}\n\
// performance:\n\
// difficulty={}\n\
// acc={}\n\
// effective={}\n\
// pp={}\n",
// attrs.difficulty.stamina,
// attrs.difficulty.rhythm,
// attrs.difficulty.colour,
// attrs.difficulty.peak,
// attrs.difficulty.hit_window,
// attrs.difficulty.max_combo,
// attrs.difficulty.stars,
// attrs.pp_difficulty,
// attrs.pp_acc,
// attrs.effective_miss_count,
// attrs.pp,
// );
}
}
+44 -49
View File
@@ -1,4 +1,9 @@
use crate::{beatmap::BeatmapHitWindows, parse::HitObjectKind, util::FloatExt, Beatmap, Mods};
use crate::{
beatmap::BeatmapHitWindows,
parse::{HitObject, HitObjectKind},
util::FloatExt,
Beatmap, Mods,
};
use super::{
difficulty_object::ManiaDifficultyObject,
@@ -52,7 +57,6 @@ impl<'map> ManiaGradualDifficultyAttributes<'map> {
/// Create a new difficulty attributes iterator for osu!mania maps.
pub fn new(map: &'map Beatmap, mods: u32) -> Self {
let total_columns = map.cs.round_even().max(1.0);
let clock_rate = mods.clock_rate();
let strain = Strain::new(total_columns as usize);
@@ -82,6 +86,8 @@ impl<'map> ManiaGradualDifficultyAttributes<'map> {
}
};
let curr_combo = params.max_combo;
let diff_objects_iter = hit_objects.enumerate().scan(first, |last, (i, h)| {
let base = ManiaObject::new(h, total_columns, &mut params);
let diff_object = ManiaDifficultyObject::new(&base, &*last, clock_rate, i);
@@ -90,17 +96,6 @@ impl<'map> ManiaGradualDifficultyAttributes<'map> {
Some(diff_object)
});
let curr_combo = if let Some(h) = map.hit_objects.first() {
match &h.kind {
HitObjectKind::Hold { end_time } => {
1 + ((*end_time - h.start_time) / 100.0) as usize
}
_ => 1,
}
} else {
0
};
let mut diff_objects = Vec::with_capacity(map.hit_objects.len().saturating_sub(1));
diff_objects.extend(diff_objects_iter);
@@ -114,6 +109,24 @@ impl<'map> ManiaGradualDifficultyAttributes<'map> {
clock_rate,
}
}
fn increment_combo(
h: &HitObject,
diff_obj: &ManiaDifficultyObject,
curr_combo: &mut usize,
clock_rate: f64,
) {
match &h.kind {
HitObjectKind::Circle => *curr_combo += 1,
_ => {
let start_time = diff_obj.start_time * clock_rate;
let end_time = diff_obj.end_time * clock_rate;
let duration = end_time - start_time;
*curr_combo += 1 + (duration / 100.0) as usize;
}
}
}
}
impl Iterator for ManiaGradualDifficultyAttributes<'_> {
@@ -124,16 +137,7 @@ impl Iterator for ManiaGradualDifficultyAttributes<'_> {
self.idx += 1;
if let Some(h) = self.map.hit_objects.get(self.idx) {
match &h.kind {
HitObjectKind::Circle => self.curr_combo += 1,
_ => {
let start_time = curr.start_time * self.clock_rate;
let end_time = curr.end_time * self.clock_rate;
let duration = end_time - start_time;
self.curr_combo += 1 + (duration / 100.0) as usize;
}
}
Self::increment_combo(h, curr, &mut self.curr_combo, self.clock_rate);
}
self.strain.process(curr, &self.diff_objects);
@@ -151,6 +155,23 @@ impl Iterator for ManiaGradualDifficultyAttributes<'_> {
(len, Some(len))
}
fn nth(&mut self, n: usize) -> Option<Self::Item> {
let skip = n.min(self.len()).saturating_sub(1);
for _ in 0..skip {
let curr = self.diff_objects.get(self.idx)?;
self.idx += 1;
if let Some(h) = self.map.hit_objects.get(self.idx) {
Self::increment_combo(h, curr, &mut self.curr_combo, self.clock_rate);
}
self.strain.process(curr, &self.diff_objects);
}
self.next()
}
}
impl ExactSizeIterator for ManiaGradualDifficultyAttributes<'_> {
@@ -159,29 +180,3 @@ impl ExactSizeIterator for ManiaGradualDifficultyAttributes<'_> {
self.diff_objects.len() - self.idx
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_map() {
let map = Beatmap::default();
let mut attributes = ManiaGradualDifficultyAttributes::new(&map, 0);
assert!(attributes.next().is_none());
}
#[cfg(not(any(feature = "async_tokio", feature = "async_std")))]
#[test]
fn iter_end_eq_regular() {
let map = Beatmap::from_path("./maps/1974394.osu").expect("failed to parse map");
let mods = 64;
let regular = crate::ManiaStars::new(&map).mods(mods).calculate();
let iter_end = ManiaGradualDifficultyAttributes::new(&map, mods)
.last()
.expect("empty iter");
assert_eq!(regular, iter_end);
}
}
+27 -141
View File
@@ -2,7 +2,10 @@ use crate::{Beatmap, ManiaPP};
use super::{ManiaGradualDifficultyAttributes, ManiaPerformanceAttributes};
/// TODO: docs
/// Aggregation for a score's current state
/// i.e. what are the current hitresults.
///
/// This struct is used for [`ManiaGradualPerformanceAttributes`].
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct ManiaScoreState {
/// Amount of current 320s.
@@ -65,7 +68,7 @@ impl ManiaScoreState {
/// # Example
///
/// ```
/// use rosu_pp::{Beatmap, mania::ManiaGradualPerformanceAttributes};
/// use rosu_pp::{Beatmap, mania::{ManiaGradualPerformanceAttributes, ManiaScoreState}};
///
/// # /*
/// let map: Beatmap = ...
@@ -74,45 +77,51 @@ impl ManiaScoreState {
///
/// let mods = 64; // DT
/// let mut gradual_perf = ManiaGradualPerformanceAttributes::new(&map, mods);
/// let mut score = 0;
/// let mut state = ManiaScoreState::new(); // empty state, everything is on 0.
///
/// // The first 10 objects each increase the score by 123.
/// // The first 10 hitresults are 320s
/// for _ in 0..10 {
/// score += 123;
/// state.n320 += 1;
///
/// # /*
/// let performance = gradual_perf.process_next_object(score).unwrap();
/// println!("PP: {}", performance.pp);
/// # */
/// # let _ = gradual_perf.process_next_object(score);
/// # let _ = gradual_perf.process_next_object(state.clone());
/// }
///
/// // Then comes a miss so no additional score is added.
/// // Then comes a miss.
/// state.n_misses += 1;
/// # /*
/// let performance = gradual_perf.process_next_object(score).unwrap();
/// println!("PP: {}", performance.pp);
/// # */
/// # let _ = gradual_perf.process_next_object(score);
/// # let _ = gradual_perf.process_next_object(state.clone());
///
/// // The next 10 objects give a total of 987 score and will be processed in one go.
/// score += 987;
/// // The next 10 objects will be a mixture of 320s and 100s.
/// // Notice how all 10 objects will be processed in one go.
/// state.n320 += 3;
/// state.n100 += 7;
/// # /*
/// let performance = gradual_perf.process_next_n_objects(score, 10).unwrap();
/// println!("PP: {}", performance.pp);
/// # */
/// # let _ = gradual_perf.process_next_n_objects(score, 10);
/// # let _ = gradual_perf.process_next_n_objects(state.clone(), 10);
///
/// // Skip to the end
/// # /*
/// score = ...
/// let final_performance = gradual_perf.process_next_n_objects(score, usize::MAX).unwrap();
/// state.max_combo = ...
/// state.n300 = ...
/// state.n100 = ...
/// state.n_misses = ...
/// let final_performance = gradual_perf.process_next_n_objects(state.clone(), usize::MAX).unwrap();
/// println!("PP: {}", performance.pp);
/// # */
/// # let _ = gradual_perf.process_next_n_objects(score, usize::MAX);
/// # let _ = gradual_perf.process_next_n_objects(state.clone(), usize::MAX);
///
/// // Once the final performance was calculated,
/// // attempting to process further objects will return `None`.
/// assert!(gradual_perf.process_next_object(score).is_none());
/// assert!(gradual_perf.process_next_object(state).is_none());
/// ```
#[derive(Clone, Debug)]
pub struct ManiaGradualPerformanceAttributes<'map> {
@@ -152,140 +161,17 @@ impl<'map> ManiaGradualPerformanceAttributes<'map> {
state: ManiaScoreState,
n: usize,
) -> Option<ManiaPerformanceAttributes> {
let n = n.min(self.difficulty.len()).saturating_sub(1);
let difficulty = self.difficulty.nth(n)?;
self.performance.n320 = Some(state.n320);
self.performance.n300 = Some(state.n300);
self.performance.n200 = Some(state.n200);
self.performance.n100 = Some(state.n100);
self.performance.n50 = Some(state.n50);
self.performance.n_misses = Some(state.n_misses);
let sub = (self.difficulty.idx == 0) as usize;
let difficulty = self.difficulty.nth(n.saturating_sub(sub))?;
let performance = self
.performance
.clone()
.attributes(difficulty)
.state(state)
.passed_objects(self.difficulty.idx)
.calculate();
Some(performance)
}
}
#[cfg(test)]
mod tests {
#[allow(unused_imports)]
use super::*;
#[cfg(not(any(feature = "async_tokio", feature = "async_std")))]
#[test]
fn correct_empty() {
let map = Beatmap::from_path("./maps/1974394.osu").expect("failed to parse map");
let mods = 64;
let mut gradual = ManiaGradualPerformanceAttributes::new(&map, mods);
let state = ManiaScoreState {
n320: 0,
n300: 0,
n200: 0,
n100: 0,
n50: 0,
n_misses: 0,
};
assert!(gradual
.process_next_n_objects(state.clone(), usize::MAX)
.is_some());
assert!(gradual.process_next_object(state).is_none());
}
#[cfg(not(any(feature = "async_tokio", feature = "async_std")))]
#[test]
fn next_and_next_n() {
let map = Beatmap::from_path("./maps/1974394.osu").expect("failed to parse map");
let mods = 64;
let mut state = ManiaScoreState {
n320: 0,
n300: 0,
n200: 0,
n100: 0,
n50: 0,
n_misses: 0,
};
let mut gradual1 = ManiaGradualPerformanceAttributes::new(&map, mods);
let mut gradual2 = ManiaGradualPerformanceAttributes::new(&map, mods);
for _ in 0..20 {
let _ = gradual1.process_next_object(state.clone());
let _ = gradual2.process_next_object(state.clone());
state.n320 += 1;
}
let n = 80;
for _ in 1..n {
let _ = gradual1.process_next_object(state.clone());
state.n320 += 1;
}
let next = gradual1.process_next_object(state.clone());
let next_n = gradual2.process_next_n_objects(state, n);
assert_eq!(next_n, next);
}
#[cfg(not(any(feature = "async_tokio", feature = "async_std")))]
#[test]
fn gradual_end_eq_regular() {
let map = Beatmap::from_path("./maps/1974394.osu").expect("failed to parse map");
let mods = 64;
let regular = ManiaPP::new(&map).mods(mods).calculate();
let mut gradual = ManiaGradualPerformanceAttributes::new(&map, mods);
let state = ManiaScoreState {
n320: 3238,
n300: 0,
n200: 0,
n100: 0,
n50: 0,
n_misses: 0,
};
let gradual_end = gradual.process_next_n_objects(state, usize::MAX).unwrap();
assert_eq!(regular, gradual_end);
}
#[cfg(not(any(feature = "async_tokio", feature = "async_std")))]
#[test]
fn gradual_eq_regular_passed() {
let map = Beatmap::from_path("./maps/1974394.osu").expect("failed to parse map");
let mods = 64;
let n = 100;
let state = ManiaScoreState {
n320: 100,
n300: 0,
n200: 0,
n100: 0,
n50: 0,
n_misses: 0,
};
let regular = ManiaPP::new(&map)
.mods(mods)
.passed_objects(n)
.state(state.clone())
.calculate();
let mut gradual = ManiaGradualPerformanceAttributes::new(&map, mods);
let gradual = gradual.process_next_n_objects(state, n).unwrap();
assert_eq!(regular, gradual);
}
}
+7 -1
View File
@@ -175,7 +175,7 @@ fn calculate_result(params: ManiaStars<'_>) -> ManiaResult {
let clock_rate = clock_rate.unwrap_or_else(|| mods.clock_rate());
let mut strain = Strain::new(total_columns as usize);
let mut params = ObjectParameters::new(map.as_ref());
let mut hit_objects = map.hit_objects.iter();
let mut hit_objects = map.hit_objects.iter().take(take);
let first = match hit_objects.next() {
Some(h) => ManiaObject::new(h, total_columns, &mut params),
@@ -247,6 +247,12 @@ impl ManiaPerformanceAttributes {
pub fn pp(&self) -> f64 {
self.pp
}
/// Return the maximum combo of the map.
#[inline]
pub fn max_combo(&self) -> usize {
self.difficulty.max_combo
}
}
impl From<ManiaPerformanceAttributes> for ManiaDifficultyAttributes {
+647 -72
View File
@@ -20,15 +20,21 @@ use crate::{
///
/// let pp_result = ManiaPP::new(&map)
/// .mods(64) // DT
/// .score(765_432)
/// .n_misses(1)
/// .accuracy(98.5)
/// .calculate();
///
/// println!("PP: {} | Stars: {}", pp_result.pp(), pp_result.stars());
///
/// let next_result = ManiaPP::new(&map)
/// .attributes(pp_result) // reusing previous results for performance
/// .mods(8 + 64) // has to be the same to reuse attributes
/// .score(950_000)
/// .attributes(pp_result) // reusing previous results for performance
/// .mods(8 + 64) // has to be the same to reuse attributes
/// .n320(2000)
/// .n300(500)
/// .n200(200)
/// .n100(100)
/// .n50(10)
/// .n_misses(1)
/// .calculate();
///
/// println!("PP: {} | Stars: {}", next_result.pp(), next_result.stars());
@@ -122,7 +128,7 @@ impl<'map> ManiaPP<'map> {
/// This will be used to generate matching hitresults.
#[inline]
pub fn accuracy(mut self, acc: f64) -> Self {
self.acc = Some(acc);
self.acc = Some(acc / 100.0);
self
}
@@ -238,110 +244,258 @@ impl<'map> ManiaPP<'map> {
let n_objects = self.passed_objects.unwrap_or(self.map.hit_objects.len());
let priority = self.hitresult_priority.unwrap_or_default();
let mut state = ManiaScoreState {
n320: self.n320.unwrap_or(0),
n300: self.n300.unwrap_or(0),
n200: self.n200.unwrap_or(0),
n100: self.n100.unwrap_or(0),
n50: self.n50.unwrap_or(0),
n_misses: self.n_misses.unwrap_or(0),
};
let mut n320 = self.n320.unwrap_or(0);
let mut n300 = self.n300.unwrap_or(0);
let mut n200 = self.n200.unwrap_or(0);
let mut n100 = self.n100.unwrap_or(0);
let mut n50 = self.n50.unwrap_or(0);
let n_misses = self.n_misses.unwrap_or(0);
if let Some(acc) = self.acc {
// TODO: test
let target_total = (acc * (n_objects * 6) as f64).round() as usize;
let mut delta = target_total.saturating_sub(n_objects.saturating_sub(state.n_misses));
match (self.n320, self.n300, self.n200, self.n100, self.n50) {
(Some(_), Some(_), Some(_), Some(_), Some(_)) => {
let remaining =
n_objects.saturating_sub(n320 + n300 + n200 + n100 + n50 + n_misses);
if self.n50.is_some() {
delta /= 2;
}
if self.n100.is_some() {
delta /= 2;
}
if let Some(n320) = self.n320 {
delta = delta.saturating_sub(n320 * 6);
} else {
state.n320 = delta / 5;
}
if self.n100.is_none() {
state.n100 = delta % 5;
}
state.n50 += n_objects.saturating_sub(state.total_hits() - state.n50);
if let HitResultPriority::BestCase = priority {
// Shift n50 to n200
if self.n320.or(self.n300).or(self.n200).or(self.n50).is_none() {
let n = (state.n320 + state.n300).min(state.n50 / 2);
if n <= state.n300 {
state.n300 -= n;
} else {
state.n320 -= n - state.n300;
state.n300 = 0;
};
state.n200 += 2 * n;
state.n50 -= n;
match priority {
HitResultPriority::BestCase => n320 += remaining,
HitResultPriority::WorstCase => n50 += remaining,
}
}
(Some(_), None, Some(_), Some(_), Some(_)) => {
n300 = n_objects.saturating_sub(n320 + n200 + n100 + n50 + n_misses)
}
(None, Some(_), Some(_), Some(_), Some(_)) => {
n320 = n_objects.saturating_sub(n300 + n200 + n100 + n50 + n_misses)
}
(Some(_), _, Some(_), Some(_), None) | (_, Some(_), Some(_), Some(_), None) => {
n50 = n_objects.saturating_sub(n320 + n300 + n200 + n100 + n_misses);
}
(Some(_), _, _, None, None) | (_, Some(_), _, None, None) => {
let n3x0 = n320 + n300;
let delta = (target_total - n_objects.saturating_sub(n_misses))
.saturating_sub(n3x0 * 5 + n200 * 3);
// Shift n50 to n100
if self.n320.or(self.n300).or(self.n100).or(self.n50).is_none() {
let n = (state.n320 + state.n300).min(state.n50 / 4);
n100 = delta % 5;
n50 = n_objects.saturating_sub(n3x0 + n200 + n100 + n_misses);
if n <= state.n300 {
state.n300 -= n;
let curr_total = 6 * n3x0 + 4 * n200 + 2 * n100 + n50;
if curr_total < target_total {
let n = (target_total - curr_total).min(n50);
n50 -= n;
n100 += n;
} else {
state.n320 -= n - state.n300;
state.n300 = 0;
};
let n = (curr_total - target_total).min(n100);
n100 -= n;
n50 += n;
}
}
(Some(_), _, None, Some(_), None) | (_, Some(_), None, Some(_), None) => {
let n3x0 = n320 + n300;
let delta = (target_total - n_objects.saturating_sub(n_misses))
.saturating_sub(n3x0 * 5 + n100);
state.n100 += 5 * n;
state.n50 -= 4 * n;
n200 = delta / 3;
n50 = n_objects.saturating_sub(n3x0 + n200 + n100 + n_misses);
}
(Some(_), _, None, None, Some(_)) | (_, Some(_), None, None, Some(_)) => {
let remaining = n_objects.saturating_sub(n320 + n300 + n50 + n_misses);
match priority {
HitResultPriority::BestCase => n100 = remaining,
HitResultPriority::WorstCase => n200 = remaining,
}
}
(Some(_), _, None, Some(_), Some(_)) | (_, Some(_), None, Some(_), Some(_)) => {
n200 = n_objects.saturating_sub(n320 + n300 + n100 + n50 + n_misses);
}
(Some(_), _, Some(_), None, Some(_)) | (_, Some(_), Some(_), None, Some(_)) => {
n100 = n_objects.saturating_sub(n320 + n300 + n200 + n50 + n_misses);
}
(None, None, Some(_), Some(_), Some(_)) => {
let remaining = n_objects.saturating_sub(n200 + n100 + n50 + n_misses);
match priority {
HitResultPriority::BestCase => n320 = remaining,
HitResultPriority::WorstCase => n300 = remaining,
}
}
(None, None, None, Some(_), Some(_)) => {
let delta =
(target_total - n_objects.saturating_sub(n_misses)).saturating_sub(n100);
match priority {
HitResultPriority::BestCase => n320 = delta / 5,
HitResultPriority::WorstCase => n300 = delta / 5,
}
n200 = n_objects.saturating_sub(n320 + n100 + n50 + n_misses);
let curr_total = 6 * (n320 + n300) + 4 * n200 + 2 * n100 + n50;
if curr_total < target_total {
let n = n200.min((target_total - curr_total) / 2);
n200 -= n;
match priority {
HitResultPriority::BestCase => n320 += n,
HitResultPriority::WorstCase => n300 += n,
}
} else {
let n = (n320 + n300).min((curr_total - target_total) / 2);
n200 += n;
match priority {
HitResultPriority::BestCase => n320 -= n,
HitResultPriority::WorstCase => n300 -= n,
}
}
}
(None, None, Some(_), None, None) => {
let delta = (target_total - n_objects.saturating_sub(n_misses))
.saturating_sub(n200 * 3);
match priority {
HitResultPriority::BestCase => n320 = delta / 5,
HitResultPriority::WorstCase => n300 = delta / 5,
}
n100 = delta % 5;
n50 = n_objects.saturating_sub(n320 + n200 + n100 + n_misses);
let curr_total = 6 * (n320 + n300) + 4 * n200 * 2 * n100 + n50;
if curr_total < target_total {
let n = (target_total - curr_total).min(n50);
n50 -= n;
n100 += n;
} else {
let n = (curr_total - target_total).min(n100);
n100 -= n;
n50 += n;
}
if let HitResultPriority::BestCase = priority {
// Shift n50 to n100
let n = n320.min(n50 / 4);
n320 -= n;
n100 += 5 * n;
n50 -= 4 * n;
}
}
(None, None, _, Some(_), None) => {
let delta = (target_total - n_objects.saturating_sub(n_misses))
.saturating_sub(n200 * 3 + n100);
match priority {
HitResultPriority::BestCase => n320 = delta / 5,
HitResultPriority::WorstCase => n300 = delta / 5,
}
n50 = n_objects.saturating_sub(n320 + n300 + n200 + n100 + n_misses);
}
(None, None, _, None, Some(_)) => {
let delta =
target_total - n_objects.saturating_sub(n_misses).saturating_sub(n200 * 3);
match priority {
HitResultPriority::BestCase => n320 = delta / 5,
HitResultPriority::WorstCase => n300 = delta / 5,
}
n100 = delta % 5;
n100 += n_objects.saturating_sub(n320 + n300 + n200 + n100 + n50 + n_misses);
let curr_total = 6 * (n320 + n300) + 4 * n200 + 2 * n100 + n50;
if curr_total < target_total {
let n = n100.min((target_total - curr_total) / 4);
n100 -= n;
match priority {
HitResultPriority::BestCase => n320 += n,
HitResultPriority::WorstCase => n300 += n,
}
} else {
let n = (n320 + n300).min((curr_total - target_total) / 4);
n100 += n;
match priority {
HitResultPriority::BestCase => n320 -= n,
HitResultPriority::WorstCase => n300 -= n,
}
}
}
(None, None, None, None, None) => {
let delta = target_total - n_objects.saturating_sub(n_misses);
match priority {
HitResultPriority::BestCase => n320 = delta / 5,
HitResultPriority::WorstCase => n300 = delta / 5,
}
n100 = delta % 5;
n50 = n_objects.saturating_sub(n320 + n300 + n100 + n_misses);
if let HitResultPriority::BestCase = priority {
// Shift n50 to n100
let n = n320.min(n50 / 4);
n320 -= n;
n100 += 5 * n;
n50 -= 4 * n;
}
}
}
} else {
let remaining = n_objects.saturating_sub(state.total_hits());
let remaining = n_objects.saturating_sub(n320 + n300 + n200 + n100 + n50 + n_misses);
match priority {
HitResultPriority::BestCase => {
if self.n320.is_none() {
state.n320 = remaining;
n320 = remaining;
} else if self.n300.is_none() {
state.n300 = remaining;
n300 = remaining;
} else if self.n200.is_none() {
state.n200 = remaining;
n200 = remaining;
} else if self.n100.is_none() {
state.n100 = remaining;
n100 = remaining;
} else if self.n50.is_none() {
state.n50 = remaining;
n50 = remaining;
} else {
state.n320 = remaining;
n320 += remaining;
}
}
HitResultPriority::WorstCase => {
if self.n50.is_none() {
state.n50 = remaining;
n50 = remaining;
} else if self.n100.is_none() {
state.n100 = remaining;
n100 = remaining;
} else if self.n200.is_none() {
state.n200 = remaining;
n200 = remaining;
} else if self.n300.is_none() {
state.n300 = remaining;
n300 = remaining;
} else if self.n320.is_none() {
state.n320 = remaining;
n320 = remaining;
} else {
state.n50 = remaining;
n50 += remaining;
}
}
}
}
state
ManiaScoreState {
n320,
n300,
n200,
n100,
n50,
n_misses,
}
}
}
@@ -488,3 +642,424 @@ impl ManiaAttributeProvider for PerformanceAttributes {
}
}
}
#[cfg(not(any(feature = "async_tokio", feature = "async_str")))]
#[cfg(test)]
mod tests {
use super::*;
use crate::Beatmap;
fn test_data() -> (Beatmap, ManiaDifficultyAttributes) {
let path = "./maps/1974394.osu";
let map = Beatmap::from_path(path).unwrap();
let attrs = ManiaDifficultyAttributes {
stars: 4.824631127426499,
hit_window: 40.0,
max_combo: 5064,
};
(map, attrs)
}
#[test]
fn hitresults_acc_n320_n200_n_misses_best() {
let (map, attrs) = test_data();
let state = ManiaPP::new(&map)
.attributes(attrs)
.accuracy(90.0)
.n320(2600)
.n200(400)
.n_misses(2)
.hitresult_priority(HitResultPriority::BestCase)
.generate_hitresults();
let expected = ManiaScoreState {
n320: 2600,
n300: 0,
n200: 400,
n100: 49,
n50: 187,
n_misses: 2,
};
assert_eq!(
state,
expected,
"{}% vs {}%",
state.accuracy(),
expected.accuracy()
);
assert_eq!(state.total_hits(), 3238);
}
#[test]
fn hitresults_acc_n320_n300_n200_n_misses_best() {
let (map, attrs) = test_data();
let state = ManiaPP::new(&map)
.attributes(attrs)
.accuracy(90.0)
.n320(2250)
.n300(500)
.n200(100)
.n_misses(2)
.hitresult_priority(HitResultPriority::BestCase)
.generate_hitresults();
let expected = ManiaScoreState {
n320: 2250,
n300: 500,
n200: 100,
n100: 199,
n50: 187,
n_misses: 2,
};
assert_eq!(
state,
expected,
"{}% vs {}%",
state.accuracy(),
expected.accuracy()
);
assert_eq!(state.total_hits(), 3238);
}
#[test]
fn hitresults_acc_n320_n300_n100_n_misses_best() {
let (map, attrs) = test_data();
let state = ManiaPP::new(&map)
.attributes(attrs)
.accuracy(90.0)
.n320(2000)
.n300(500)
.n100(100)
.n_misses(2)
.hitresult_priority(HitResultPriority::BestCase)
.generate_hitresults();
let expected = ManiaScoreState {
n320: 2000,
n300: 500,
n200: 549,
n100: 100,
n50: 87,
n_misses: 2,
};
assert_eq!(
state,
expected,
"{}% vs {}%",
state.accuracy(),
expected.accuracy()
);
assert_eq!(state.total_hits(), 3238);
}
#[test]
fn hitresults_acc_n320_n100_n50_n_misses_best() {
let (map, attrs) = test_data();
let state = ManiaPP::new(&map)
.attributes(attrs)
.accuracy(90.0)
.n320(2700)
.n100(200)
.n50(10)
.n_misses(2)
.hitresult_priority(HitResultPriority::BestCase)
.generate_hitresults();
let expected = ManiaScoreState {
n320: 2700,
n300: 0,
n200: 326,
n100: 200,
n50: 10,
n_misses: 2,
};
assert_eq!(
state,
expected,
"{}% vs {}%",
state.accuracy(),
expected.accuracy()
);
assert_eq!(state.total_hits(), 3238);
}
#[test]
fn hitresults_acc_n320_n50_n_misses_worst() {
let (map, attrs) = test_data();
let state = ManiaPP::new(&map)
.attributes(attrs)
.accuracy(90.0)
.n320(2000)
.n50(50)
.n_misses(2)
.hitresult_priority(HitResultPriority::WorstCase)
.generate_hitresults();
let expected = ManiaScoreState {
n320: 2000,
n300: 0,
n200: 1186,
n100: 0,
n50: 50,
n_misses: 2,
};
assert_eq!(
state,
expected,
"{}% vs {}%",
state.accuracy(),
expected.accuracy()
);
assert_eq!(state.total_hits(), 3238);
}
#[test]
fn hitresults_acc_n100_n50_n_misses_best() {
let (map, attrs) = test_data();
let state = ManiaPP::new(&map)
.attributes(attrs)
.accuracy(90.0)
.n100(200)
.n50(50)
.n_misses(2)
.hitresult_priority(HitResultPriority::BestCase)
.generate_hitresults();
let expected = ManiaScoreState {
n320: 2546,
n300: 0,
n200: 440,
n100: 200,
n50: 50,
n_misses: 2,
};
assert_eq!(
state,
expected,
"{}% vs {}%",
state.accuracy(),
expected.accuracy()
);
assert_eq!(state.total_hits(), 3238);
}
#[test]
fn hitresults_acc_n200_n_misses_best() {
let (map, attrs) = test_data();
let state = ManiaPP::new(&map)
.attributes(attrs)
.accuracy(90.0)
.n200(500)
.n_misses(2)
.hitresult_priority(HitResultPriority::BestCase)
.generate_hitresults();
let expected = ManiaScoreState {
n320: 2503,
n300: 0,
n200: 500,
n100: 230,
n50: 3,
n_misses: 2,
};
assert_eq!(
state,
expected,
"{}% vs {}%",
state.accuracy(),
expected.accuracy()
);
assert_eq!(state.total_hits(), 3238);
}
#[test]
fn hitresults_acc_n200_n100_n_misses_best() {
let (map, attrs) = test_data();
let state = ManiaPP::new(&map)
.attributes(attrs)
.accuracy(90.0)
.n200(500)
.n100(200)
.n_misses(2)
.hitresult_priority(HitResultPriority::BestCase)
.generate_hitresults();
let expected = ManiaScoreState {
n320: 2509,
n300: 0,
n200: 500,
n100: 200,
n50: 27,
n_misses: 2,
};
assert_eq!(
state,
expected,
"{}% vs {}%",
state.accuracy(),
expected.accuracy()
);
assert_eq!(state.total_hits(), 3238);
}
#[test]
fn hitresults_acc_n50_n_misses_best() {
let (map, attrs) = test_data();
let state = ManiaPP::new(&map)
.attributes(attrs)
.accuracy(90.0)
.n50(200)
.n_misses(2)
.hitresult_priority(HitResultPriority::BestCase)
.generate_hitresults();
let expected = ManiaScoreState {
n320: 2804,
n300: 0,
n200: 0,
n100: 232,
n50: 200,
n_misses: 2,
};
assert_eq!(
state,
expected,
"{}% vs {}%",
state.accuracy(),
expected.accuracy()
);
assert_eq!(state.total_hits(), 3238);
}
#[test]
fn hitresults_acc_n200_n100_n50_n_misses_best() {
let (map, attrs) = test_data();
let state = ManiaPP::new(&map)
.attributes(attrs)
.accuracy(90.0)
.n200(500)
.n100(300)
.n50(100)
.n_misses(2)
.hitresult_priority(HitResultPriority::BestCase)
.generate_hitresults();
let expected = ManiaScoreState {
n320: 2336,
n300: 0,
n200: 500,
n100: 300,
n50: 100,
n_misses: 2,
};
assert_eq!(
state,
expected,
"{}% vs {}%",
state.accuracy(),
expected.accuracy()
);
assert_eq!(state.total_hits(), 3238);
}
#[test]
fn hitresults_acc_n_misses_worst() {
let (map, attrs) = test_data();
let state = ManiaPP::new(&map)
.attributes(attrs)
.accuracy(90.0)
.n_misses(2)
.hitresult_priority(HitResultPriority::WorstCase)
.generate_hitresults();
let expected = ManiaScoreState {
n320: 0,
n300: 2849,
n200: 0,
n100: 4,
n50: 383,
n_misses: 2,
};
assert_eq!(
state,
expected,
"{}% vs {}%",
state.accuracy(),
expected.accuracy()
);
assert_eq!(state.total_hits(), 3238);
}
#[test]
fn hitresults_n320_n_misses_best() {
let (map, attrs) = test_data();
let state = ManiaPP::new(&map)
.attributes(attrs)
.n320(2000)
.n_misses(2)
.hitresult_priority(HitResultPriority::BestCase)
.generate_hitresults();
let expected = ManiaScoreState {
n320: 2000,
n300: 1236,
n200: 0,
n100: 0,
n50: 0,
n_misses: 2,
};
assert_eq!(state, expected);
assert_eq!(state.total_hits(), 3238);
}
#[test]
fn hitresults_n100_n50_n_misses_worst() {
let (map, attrs) = test_data();
let state = ManiaPP::new(&map)
.attributes(attrs)
.n100(500)
.n50(100)
.n_misses(2)
.hitresult_priority(HitResultPriority::WorstCase)
.generate_hitresults();
let expected = ManiaScoreState {
n320: 0,
n300: 0,
n200: 2636,
n100: 500,
n50: 100,
n_misses: 2,
};
assert_eq!(state, expected);
assert_eq!(state.total_hits(), 3238);
}
}
+45 -47
View File
@@ -49,7 +49,9 @@ use super::{
pub struct OsuGradualDifficultyAttributes {
pub(crate) idx: usize,
mods: u32,
attributes: OsuDifficultyAttributes,
attrs: OsuDifficultyAttributes,
// Unused but `diff_objects`' lifetimes secretly depend on it
#[allow(unused)]
hit_objects: Vec<OsuObject>,
diff_objects: Vec<OsuDifficultyObject<'static>>,
skills: [Box<dyn Skill>; 4],
@@ -60,8 +62,8 @@ impl Debug for OsuGradualDifficultyAttributes {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
f.debug_struct("OsuGradualDifficultyAttributes")
.field("idx", &self.idx)
.field("attributes", &self.attributes)
.field("hit_objects", &self.hit_objects)
.field("attrs", &self.attrs)
.field("diff_objects", &self.diff_objects)
.field("skills", &"<cannot be displayed>")
.finish()
}
@@ -75,7 +77,7 @@ impl OsuGradualDifficultyAttributes {
let scaling_factor = ScalingFactor::new(map_attrs.cs);
let hr = mods.hr();
let hit_window = 2.0 * map_attrs.hit_windows.od;
let time_preempt = map_attrs.hit_windows.ar;
let time_preempt = (map_attrs.hit_windows.ar * clock_rate) as f32 as f64;
// * Preempt time can go below 450ms. Normally, this is achieved via the DT mod
// * which uniformly speeds up all animations game wide regardless of AR.
@@ -100,7 +102,7 @@ impl OsuGradualDifficultyAttributes {
let mut params = ObjectParameters {
map,
attributes: &mut attrs,
attrs: &mut attrs,
ticks: Vec::new(),
curve_bufs: CurveBuffers::default(),
};
@@ -140,7 +142,7 @@ impl OsuGradualDifficultyAttributes {
return Self {
idx: 0,
mods,
attributes: attrs,
attrs,
hit_objects: Vec::new(),
diff_objects: Vec::new(),
skills,
@@ -149,6 +151,8 @@ impl OsuGradualDifficultyAttributes {
}
};
Self::increment_combo(last, &mut attrs);
let mut last_last = None;
// Prepare `lazy_travel_dist` and `lazy_end_pos` for `last` manually
@@ -182,13 +186,26 @@ impl OsuGradualDifficultyAttributes {
Self {
idx: 0,
mods,
attributes: attrs,
attrs,
diff_objects: extend_lifetime(diff_objects),
hit_objects,
skills,
hit_window,
}
}
fn increment_combo(h: &OsuObject, attrs: &mut OsuDifficultyAttributes) {
attrs.max_combo += 1;
match &h.kind {
OsuObjectKind::Circle => attrs.n_circles += 1,
OsuObjectKind::Slider(slider) => {
attrs.n_sliders += 1;
attrs.max_combo += slider.nested_len();
}
OsuObjectKind::Spinner { .. } => attrs.n_spinners += 1,
}
}
}
fn extend_lifetime(
@@ -210,18 +227,7 @@ impl Iterator for OsuGradualDifficultyAttributes {
skill.process(curr, &self.diff_objects, self.hit_window);
}
let mut attrs = self.attributes.clone();
attrs.max_combo += 1;
match &curr.base.kind {
OsuObjectKind::Circle => attrs.n_circles += 1,
OsuObjectKind::Slider(slider) => {
attrs.n_sliders += 1;
attrs.max_combo += slider.nested_len();
}
OsuObjectKind::Spinner { .. } => attrs.n_spinners += 1,
}
Self::increment_combo(curr.base, &mut self.attrs);
let [aim, aim_no_sliders, speed, flashlight] = &self.skills;
@@ -289,6 +295,7 @@ impl Iterator for OsuGradualDifficultyAttributes {
0.0
};
let mut attrs = self.attrs.clone();
attrs.aim = aim_rating;
attrs.speed = speed_rating;
attrs.flashlight = flashlight_rating;
@@ -301,41 +308,32 @@ impl Iterator for OsuGradualDifficultyAttributes {
#[inline]
fn size_hint(&self) -> (usize, Option<usize>) {
let len = self.hit_objects.len() - self.idx;
let len = self.len();
(len, Some(len))
}
fn nth(&mut self, n: usize) -> Option<Self::Item> {
let skip = n.min(self.len()).saturating_sub(1);
for _ in 0..skip {
let curr = self.diff_objects.get(self.idx)?;
self.idx += 1;
for skill in self.skills.iter_mut() {
skill.process(curr, &self.diff_objects, self.hit_window);
}
Self::increment_combo(curr.base, &mut self.attrs);
}
self.next()
}
}
impl ExactSizeIterator for OsuGradualDifficultyAttributes {
#[inline]
fn len(&self) -> usize {
self.hit_objects.len()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_map() {
let map = Beatmap::default();
let mut attributes = OsuGradualDifficultyAttributes::new(&map, 0);
assert!(attributes.next().is_none());
}
#[cfg(not(any(feature = "async_tokio", feature = "async_std")))]
#[test]
fn iter_end_eq_regular() {
let map = Beatmap::from_path("./maps/2785319.osu").expect("failed to parse map");
let mods = 64;
let regular = crate::OsuStars::new(&map).mods(mods).calculate();
let iter_end = OsuGradualDifficultyAttributes::new(&map, mods)
.last()
.expect("empty iter");
assert_eq!(regular, iter_end);
self.diff_objects.len() - self.idx
}
}
+5 -106
View File
@@ -93,7 +93,7 @@ impl OsuScoreState {
/// // Then comes a miss.
/// // Note that state's max combo won't be incremented for
/// // the next few objects because the combo is reset.
/// state.misses += 1;
/// state.n_misses += 1;
/// # /*
/// let performance = gradual_perf.process_next_object(state.clone()).unwrap();
/// println!("PP: {}", performance.pp);
@@ -126,7 +126,7 @@ impl OsuScoreState {
/// state.n300 = ...
/// state.n100 = ...
/// state.n50 = ...
/// state.misses = ...
/// state.n_misses = ...
/// let final_performance = gradual_perf.process_next_n_objects(state.clone(), usize::MAX).unwrap();
/// println!("PP: {}", performance.pp);
/// # */
@@ -174,118 +174,17 @@ impl<'map> OsuGradualPerformanceAttributes<'map> {
state: OsuScoreState,
n: usize,
) -> Option<OsuPerformanceAttributes> {
let n = n.min(self.difficulty.len()).saturating_sub(1);
let difficulty = self.difficulty.nth(n)?;
let sub = (self.difficulty.idx == 0) as usize;
let difficulty = self.difficulty.nth(n.saturating_sub(sub))?;
let performance = self
.performance
.clone()
.attributes(difficulty)
.state(state)
.passed_objects(self.difficulty.idx)
.passed_objects(self.difficulty.idx + 1)
.calculate();
Some(performance)
}
}
#[cfg(test)]
mod tests {
#[allow(unused_imports)]
use super::*;
#[cfg(not(any(feature = "async_tokio", feature = "async_std")))]
#[test]
fn correct_empty() {
let map = Beatmap::from_path("./maps/2785319.osu").expect("failed to parse map");
let mods = 64;
let mut gradual = OsuGradualPerformanceAttributes::new(&map, mods);
let state = OsuScoreState::default();
assert!(gradual
.process_next_n_objects(state.clone(), usize::MAX)
.is_some());
assert!(gradual.process_next_object(state).is_none());
}
#[cfg(not(any(feature = "async_tokio", feature = "async_std")))]
#[test]
fn next_and_next_n() {
let map = Beatmap::from_path("./maps/2785319.osu").expect("failed to parse map");
let mods = 64;
let state = OsuScoreState::default();
let mut gradual1 = OsuGradualPerformanceAttributes::new(&map, mods);
let mut gradual2 = OsuGradualPerformanceAttributes::new(&map, mods);
for _ in 0..20 {
let _ = gradual1.process_next_object(state.clone());
let _ = gradual2.process_next_object(state.clone());
}
let n = 80;
for _ in 1..n {
let _ = gradual1.process_next_object(state.clone());
}
let state = OsuScoreState {
max_combo: 122,
n300: 88,
n100: 8,
n50: 2,
n_misses: 2,
};
let next = gradual1.process_next_object(state.clone());
let next_n = gradual2.process_next_n_objects(state, n);
assert_eq!(next_n, next);
}
#[cfg(not(any(feature = "async_tokio", feature = "async_std")))]
#[test]
fn gradual_end_eq_regular() {
let map = Beatmap::from_path("./maps/2785319.osu").expect("failed to parse map");
let mods = 64;
let regular = OsuPP::new(&map).mods(mods).calculate();
let mut gradual = OsuGradualPerformanceAttributes::new(&map, mods);
let state = OsuScoreState {
max_combo: 909,
n300: 601,
n100: 0,
n50: 0,
n_misses: 0,
};
let gradual_end = gradual.process_next_n_objects(state, usize::MAX).unwrap();
assert_eq!(regular, gradual_end);
}
#[cfg(not(any(feature = "async_tokio", feature = "async_std")))]
#[test]
fn gradual_eq_regular_passed() {
let map = Beatmap::from_path("./maps/2785319.osu").expect("failed to parse map");
let mods = 64;
let n = 100;
let regular = OsuPP::new(&map).mods(mods).passed_objects(n).calculate();
let mut gradual = OsuGradualPerformanceAttributes::new(&map, mods);
let state = OsuScoreState {
max_combo: 122,
n300: 100,
n100: 0,
n50: 0,
n_misses: 0,
};
let gradual = gradual.process_next_n_objects(state, n).unwrap();
assert_eq!(regular, gradual);
}
}
+4 -4
View File
@@ -260,7 +260,7 @@ fn calculate_skills(params: OsuStars<'_>) -> ([Box<dyn Skill>; 4], OsuDifficulty
400.0 * (time_preempt / PREEMPT_MIN).min(1.0)
};
let mut attributes = OsuDifficultyAttributes {
let mut attrs = OsuDifficultyAttributes {
ar: map_attrs.ar,
hp: map_attrs.hp,
od: map_attrs.od,
@@ -269,7 +269,7 @@ fn calculate_skills(params: OsuStars<'_>) -> ([Box<dyn Skill>; 4], OsuDifficulty
let mut params = ObjectParameters {
map,
attributes: &mut attributes,
attrs: &mut attrs,
ticks: Vec::new(),
curve_bufs: CurveBuffers::default(),
};
@@ -301,7 +301,7 @@ fn calculate_skills(params: OsuStars<'_>) -> ([Box<dyn Skill>; 4], OsuDifficulty
let last = match hit_objects.next() {
Some(prev) => prev,
None => return (skills, attributes),
None => return (skills, attrs),
};
let mut last_last = None;
@@ -340,7 +340,7 @@ fn calculate_skills(params: OsuStars<'_>) -> ([Box<dyn Skill>; 4], OsuDifficulty
}
}
(skills, attributes)
(skills, attrs)
}
fn stacking(hit_objects: &mut [OsuObject], stack_threshold: f64) {
+7 -7
View File
@@ -86,7 +86,7 @@ pub(crate) enum NestedObjectKind {
pub(crate) struct ObjectParameters<'a> {
pub(crate) map: &'a Beatmap,
pub(crate) attributes: &'a mut OsuDifficultyAttributes,
pub(crate) attrs: &'a mut OsuDifficultyAttributes,
pub(crate) ticks: Vec<(Pos2, f64)>,
pub(crate) curve_bufs: CurveBuffers,
}
@@ -95,17 +95,17 @@ impl OsuObject {
pub(crate) fn new(h: &HitObject, params: &mut ObjectParameters<'_>) -> Option<Self> {
let ObjectParameters {
map,
attributes,
attrs,
ticks,
curve_bufs,
} = params;
attributes.max_combo += 1; // hitcircle, slider head, or spinner
attrs.max_combo += 1; // hitcircle, slider head, or spinner
let pos = h.pos;
let obj = match &h.kind {
HitObjectKind::Circle => {
attributes.n_circles += 1;
attrs.n_circles += 1;
Self {
start_time: h.start_time,
@@ -121,7 +121,7 @@ impl OsuObject {
control_points,
..
} => {
attributes.n_sliders += 1;
attrs.n_sliders += 1;
let timing_point = map.timing_point_at(h.start_time);
let difficulty_point = map.difficulty_point_at(h.start_time).unwrap_or_default();
@@ -283,7 +283,7 @@ impl OsuObject {
_ => nested_objects.push(legacy_last_tick),
};
attributes.max_combo += nested_objects.len();
attrs.max_combo += nested_objects.len();
let last_time = nested_objects
.last()
@@ -318,7 +318,7 @@ impl OsuObject {
}
}
HitObjectKind::Spinner { end_time } => {
attributes.n_spinners += 1;
attrs.n_spinners += 1;
Self {
start_time: h.start_time,
+29 -49
View File
@@ -21,15 +21,15 @@ use crate::{
/// let pp_result = OsuPP::new(&map)
/// .mods(8 + 64) // HDDT
/// .combo(1234)
/// .misses(1)
/// .n_misses(1)
/// .accuracy(98.5) // should be set last
/// .calculate();
///
/// println!("PP: {} | Stars: {}", pp_result.pp(), pp_result.stars());
///
/// let next_result = OsuPP::new(&map)
/// .attributes(pp_result) // reusing previous results for performance
/// .mods(8 + 64) // has to be the same to reuse attributes
/// .attributes(pp_result) // reusing previous results for performance
/// .mods(8 + 64) // has to be the same to reuse attributes
/// .accuracy(99.5)
/// .calculate();
///
@@ -739,13 +739,17 @@ impl OsuAttributeProvider for PerformanceAttributes {
}
}
#[cfg(not(any(feature = "async_tokio", feature = "async_str")))]
#[cfg(test)]
mod test {
use super::*;
use crate::Beatmap;
fn test_attrs() -> OsuDifficultyAttributes {
OsuDifficultyAttributes {
fn test_data() -> (Beatmap, OsuDifficultyAttributes) {
let path = "./maps/2785319.osu";
let map = Beatmap::from_path(path).unwrap();
let attrs = OsuDifficultyAttributes {
aim: 2.8693628443424104,
speed: 2.533869745015772,
flashlight: 2.288770487900865,
@@ -759,38 +763,14 @@ mod test {
n_spinners: 1,
stars: 5.669858729379631,
max_combo: 909,
}
}
};
#[cfg(not(any(feature = "async_tokio", feature = "async_str")))]
fn test_map() -> (Beatmap, OsuDifficultyAttributes) {
let path = "./maps/2785319.osu";
let map = Beatmap::from_path(path).unwrap();
(map, test_attrs())
}
#[cfg(any(feature = "async_tokio", feature = "async_str"))]
async fn test_map() -> (Beatmap, OsuDifficultyAttributes) {
let path = "./maps/2785319.osu";
let map = Beatmap::from_path(path).await.unwrap();
(map, test_attrs())
}
#[rustfmt::skip]
macro_rules! test_data {
() => {{
#[cfg(not(any(feature = "async_tokio", feature = "async_str")))]
{ test_map() }
#[cfg(any(feature = "async_tokio", feature = "async_str"))]
{ test_map().await }
}};
(map, attrs)
}
#[test]
fn osu_hitresults_n300_n100_n_misses_best() {
let (map, attrs) = test_data!();
fn hitresults_n300_n100_n_misses_best() {
let (map, attrs) = test_data();
let max_combo = attrs.max_combo();
let state = OsuPP::new(&map)
@@ -814,8 +794,8 @@ mod test {
}
#[test]
fn osu_hitresults_n300_n50_n_misses_best() {
let (map, attrs) = test_data!();
fn hitresults_n300_n50_n_misses_best() {
let (map, attrs) = test_data();
let max_combo = attrs.max_combo();
let state = OsuPP::new(&map)
@@ -839,8 +819,8 @@ mod test {
}
#[test]
fn osu_hitresults_n50_n_misses_worst() {
let (map, attrs) = test_data!();
fn hitresults_n50_n_misses_worst() {
let (map, attrs) = test_data();
let max_combo = attrs.max_combo();
let state = OsuPP::new(&map)
@@ -863,8 +843,8 @@ mod test {
}
#[test]
fn osu_hitresults_n300_n100_n50_n_misses_worst() {
let (map, attrs) = test_data!();
fn hitresults_n300_n100_n50_n_misses_worst() {
let (map, attrs) = test_data();
let max_combo = attrs.max_combo();
let state = OsuPP::new(&map)
@@ -889,8 +869,8 @@ mod test {
}
#[test]
fn osu_hitresults_acc_n_misses_best() {
let (map, attrs) = test_data!();
fn hitresults_acc_n_misses_best() {
let (map, attrs) = test_data();
let max_combo = attrs.max_combo();
let state = OsuPP::new(&map)
@@ -919,8 +899,8 @@ mod test {
}
#[test]
fn osu_hitresults_acc_n100_n_misses_best() {
let (map, attrs) = test_data!();
fn hitresults_acc_n100_n_misses_best() {
let (map, attrs) = test_data();
let max_combo = attrs.max_combo();
let state = OsuPP::new(&map)
@@ -950,8 +930,8 @@ mod test {
}
#[test]
fn osu_hitresults_acc_n50_n_misses_best() {
let (map, attrs) = test_data!();
fn hitresults_acc_n50_n_misses_best() {
let (map, attrs) = test_data();
let max_combo = attrs.max_combo();
let state = OsuPP::new(&map)
@@ -981,8 +961,8 @@ mod test {
}
#[test]
fn osu_hitresults_acc_best() {
let (map, attrs) = test_data!();
fn hitresults_acc_best() {
let (map, attrs) = test_data();
let max_combo = attrs.max_combo();
let state = OsuPP::new(&map)
@@ -1010,8 +990,8 @@ mod test {
}
#[test]
fn osu_hitresults_acc_worst() {
let (map, attrs) = test_data!();
fn hitresults_acc_worst() {
let (map, attrs) = test_data();
let max_combo = attrs.max_combo();
let state = OsuPP::new(&map)
+5 -5
View File
@@ -21,7 +21,7 @@ use crate::{
/// let pp_result = AnyPP::new(&map)
/// .mods(8 + 64) // HDDT
/// .combo(1234)
/// .misses(1)
/// .n_misses(1)
/// .accuracy(98.5) // should be set last
/// .calculate();
///
@@ -287,10 +287,10 @@ impl AttributeProvider for PerformanceAttributes {
#[inline]
fn attributes(self) -> DifficultyAttributes {
match self {
Self::Catch(f) => DifficultyAttributes::Catch(f.difficulty),
Self::Mania(m) => DifficultyAttributes::Mania(m.difficulty),
Self::Osu(o) => DifficultyAttributes::Osu(o.difficulty),
Self::Taiko(t) => DifficultyAttributes::Taiko(t.difficulty),
Self::Catch(attrs) => DifficultyAttributes::Catch(attrs.difficulty),
Self::Mania(attrs) => DifficultyAttributes::Mania(attrs.difficulty),
Self::Osu(attrs) => DifficultyAttributes::Osu(attrs.difficulty),
Self::Taiko(attrs) => DifficultyAttributes::Taiko(attrs.difficulty),
}
}
}
+36 -36
View File
@@ -42,7 +42,6 @@ use super::{
/// ```
#[derive(Clone, Debug)]
pub struct TaikoGradualDifficultyAttributes {
pub(crate) idx: usize,
attrs: TaikoDifficultyAttributes,
hit_objects: IntoIter<Rc<RefCell<TaikoDifficultyObject>>>,
lists: ObjectLists,
@@ -61,7 +60,7 @@ impl TaikoGradualDifficultyAttributes {
.clock_rate(clock_rate)
.hit_windows();
let attrs = TaikoDifficultyAttributes {
let mut attrs = TaikoDifficultyAttributes {
stamina: 0.0,
rhythm: 0.0,
colour: 0.0,
@@ -71,15 +70,27 @@ impl TaikoGradualDifficultyAttributes {
max_combo: 0,
};
if map.hit_objects.len() < 2 {
return Self {
hit_objects: Vec::new().into_iter(),
lists: ObjectLists::default(),
peaks,
attrs,
};
}
attrs.max_combo += map.hit_objects[0].is_circle() as usize;
attrs.max_combo += map.hit_objects[1].is_circle() as usize;
let mut diff_objects = map
.taiko_objects()
.enumerate()
.skip(2)
.zip(map.hit_objects.iter().skip(1))
.zip(map.hit_objects.iter())
.enumerate()
.fold(
ObjectLists::default(),
|mut lists, (((idx, (base, base_start_time)), last), last_last)| {
|mut lists, (idx, (((base, base_start_time), last), last_last))| {
let diff_obj = TaikoDifficultyObject::new(
base,
base_start_time,
@@ -109,25 +120,27 @@ impl TaikoGradualDifficultyAttributes {
ColourDifficultyPreprocessor::process_and_assign(&mut diff_objects);
Self {
idx: 0,
hit_objects: diff_objects.all.clone().into_iter(),
lists: diff_objects,
peaks,
attrs,
}
}
pub(crate) fn passed_objects(&self) -> usize {
self.lists.all.len() - self.hit_objects.len()
}
}
impl Iterator for TaikoGradualDifficultyAttributes {
type Item = TaikoDifficultyAttributes;
fn next(&mut self) -> Option<Self::Item> {
let curr = self.hit_objects.next()?;
{
let curr = curr.borrow();
self.peaks.process(&curr, &self.lists);
self.attrs.max_combo += curr.base.is_hit as usize;
let curr = self.hit_objects.next()?;
let borrowed = curr.borrow();
self.peaks.process(&borrowed, &self.lists);
self.attrs.max_combo += borrowed.base.is_hit as usize;
}
let PeaksDifficultyValues {
@@ -174,6 +187,19 @@ impl Iterator for TaikoGradualDifficultyAttributes {
(len, Some(len))
}
fn nth(&mut self, n: usize) -> Option<Self::Item> {
let skip = n.min(self.len()).saturating_sub(1);
for _ in 0..skip {
let curr = self.hit_objects.next()?;
let borrowed = curr.borrow();
self.peaks.process(&borrowed, &self.lists);
self.attrs.max_combo += borrowed.base.is_hit as usize;
}
self.next()
}
}
impl ExactSizeIterator for TaikoGradualDifficultyAttributes {
@@ -182,29 +208,3 @@ impl ExactSizeIterator for TaikoGradualDifficultyAttributes {
self.hit_objects.len()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_map() {
let map = Beatmap::default();
let mut attributes = TaikoGradualDifficultyAttributes::new(&map, 0);
assert!(attributes.next().is_none());
}
#[cfg(not(any(feature = "async_tokio", feature = "async_std")))]
#[test]
fn iter_end_eq_regular() {
let map = Beatmap::from_path("./maps/1028484.osu").expect("failed to parse map");
let mods = 64;
let regular = crate::TaikoStars::new(&map).mods(mods).calculate();
let iter_end = TaikoGradualDifficultyAttributes::new(&map, mods)
.last()
.expect("empty iter");
assert_eq!(regular, iter_end);
}
}
+5 -103
View File
@@ -91,7 +91,7 @@ impl TaikoScoreState {
/// // Then comes a miss.
/// // Note that state's max combo won't be incremented for
/// // the next few objects because the combo is reset.
/// state.misses += 1;
/// state.n_misses += 1;
/// # /*
/// let performance = gradual_perf.process_next_object(state.clone()).unwrap();
/// println!("PP: {}", performance.pp);
@@ -122,7 +122,7 @@ impl TaikoScoreState {
/// state.max_combo = ...
/// state.n300 = ...
/// state.n100 = ...
/// state.misses = ...
/// state.n_misses = ...
/// let final_performance = gradual_perf.process_next_n_objects(state.clone(), usize::MAX).unwrap();
/// println!("PP: {}", performance.pp);
/// # */
@@ -170,115 +170,17 @@ impl<'map> TaikoGradualPerformanceAttributes<'map> {
state: TaikoScoreState,
n: usize,
) -> Option<TaikoPerformanceAttributes> {
let n = n.min(self.difficulty.len()).saturating_sub(1);
let difficulty = self.difficulty.nth(n)?;
let sub = 2 * (self.difficulty.passed_objects() == 0) as usize;
let difficulty = self.difficulty.nth(n.saturating_sub(sub))?;
let performance = self
.performance
.clone()
.attributes(difficulty)
.state(state)
.passed_objects(self.difficulty.idx)
.passed_objects(self.difficulty.passed_objects() + 2)
.calculate();
Some(performance)
}
}
#[cfg(test)]
mod tests {
#[allow(unused_imports)]
use super::*;
#[cfg(not(any(feature = "async_tokio", feature = "async_std")))]
#[test]
fn correct_empty() {
let map = Beatmap::from_path("./maps/1028484.osu").expect("failed to parse map");
let mods = 64;
let mut gradual = TaikoGradualPerformanceAttributes::new(&map, mods);
let state = TaikoScoreState::default();
assert!(gradual
.process_next_n_objects(state.clone(), usize::MAX)
.is_some());
assert!(gradual.process_next_object(state).is_none());
}
#[cfg(not(any(feature = "async_tokio", feature = "async_std")))]
#[test]
fn next_and_next_n() {
let map = Beatmap::from_path("./maps/1028484.osu").expect("failed to parse map");
let mods = 64;
let state = TaikoScoreState::default();
let mut gradual1 = TaikoGradualPerformanceAttributes::new(&map, mods);
let mut gradual2 = TaikoGradualPerformanceAttributes::new(&map, mods);
for _ in 0..50 {
let _ = gradual1.process_next_object(state.clone());
let _ = gradual2.process_next_object(state.clone());
}
let n = 200;
for _ in 1..n {
let _ = gradual1.process_next_object(state.clone());
}
let state = TaikoScoreState {
max_combo: 246,
n300: 200,
n100: 40,
n_misses: 6,
};
let next = gradual1.process_next_object(state.clone());
let next_n = gradual2.process_next_n_objects(state, n);
assert_eq!(next_n, next);
}
#[cfg(not(any(feature = "async_tokio", feature = "async_std")))]
#[test]
fn gradual_end_eq_regular() {
let map = Beatmap::from_path("./maps/1028484.osu").expect("failed to parse map");
let mods = 64;
let regular = TaikoPP::new(&map).mods(mods).calculate();
let mut gradual = TaikoGradualPerformanceAttributes::new(&map, mods);
let state = TaikoScoreState {
max_combo: 289,
n300: 289,
n100: 0,
n_misses: 0,
};
let gradual_end = gradual.process_next_n_objects(state, usize::MAX).unwrap();
assert_eq!(regular, gradual_end);
}
#[cfg(not(any(feature = "async_tokio", feature = "async_std")))]
#[test]
fn gradual_eq_regular_passed() {
let map = Beatmap::from_path("./maps/1028484.osu").expect("failed to parse map");
let mods = 64;
let n = 250;
let regular = TaikoPP::new(&map).mods(mods).passed_objects(n).calculate();
let mut gradual = TaikoGradualPerformanceAttributes::new(&map, mods);
let state = TaikoScoreState {
max_combo: 246,
n300: 246,
n100: 0,
n_misses: 0,
};
let gradual = gradual.process_next_n_objects(state, n).unwrap();
assert_eq!(regular, gradual);
}
}
+79 -37
View File
@@ -20,15 +20,15 @@ use crate::{
/// let pp_result = TaikoPP::new(&map)
/// .mods(8 + 64) // HDDT
/// .combo(1234)
/// .misses(1)
/// .n_misses(1)
/// .accuracy(98.5)
/// .calculate();
///
/// println!("PP: {} | Stars: {}", pp_result.pp(), pp_result.stars());
///
/// let next_result = TaikoPP::new(&map)
/// .attributes(pp_result) // reusing previous results for performance
/// .mods(8 + 64) // has to be the same to reuse attributes
/// .attributes(pp_result) // reusing previous results for performance
/// .mods(8 + 64) // has to be the same to reuse attributes
/// .accuracy(99.5)
/// .calculate();
///
@@ -225,7 +225,6 @@ impl<'map> TaikoPP<'map> {
let n_misses = self.n_misses.unwrap_or(0);
if let Some(acc) = self.acc {
// TODO: test
match (self.n300, self.n100) {
(Some(_), Some(_)) => {
let remaining = total_result_count.saturating_sub(n300 + n100 + n_misses);
@@ -249,15 +248,19 @@ impl<'map> TaikoPP<'map> {
match priority {
HitResultPriority::BestCase => match (self.n300, self.n100) {
(Some(_), None) => n100 = remaining,
_ => n300 = remaining,
(Some(_), Some(_)) => n300 += remaining,
(None, _) => n300 = remaining,
},
HitResultPriority::WorstCase => match (self.n300, self.n100) {
(None, Some(_)) => n300 = remaining,
_ => n100 = remaining,
(Some(_), Some(_)) => n100 += remaining,
(_, None) => n100 = remaining,
},
}
}
let max_combo = self.combo.map_or(max_combo, |combo| combo.min(max_combo));
TaikoScoreState {
max_combo,
n300,
@@ -462,61 +465,100 @@ impl TaikoAttributeProvider for PerformanceAttributes {
}
}
#[cfg(not(any(feature = "async_tokio", feature = "async_str")))]
#[cfg(test)]
mod test {
use super::*;
use crate::Beatmap;
fn test_attrs() -> TaikoDifficultyAttributes {
todo!()
}
#[cfg(not(any(feature = "async_tokio", feature = "async_str")))]
fn test_map() -> (Beatmap, TaikoDifficultyAttributes) {
fn test_data() -> (Beatmap, TaikoDifficultyAttributes) {
let path = "./maps/1028484.osu";
let map = Beatmap::from_path(path).unwrap();
(map, test_attrs())
}
let attrs = TaikoDifficultyAttributes {
stamina: 1.4528845068865617,
rhythm: 0.20130047251681948,
colour: 1.0487315549761433,
peak: 1.8881824429738323,
hit_window: 35.0,
stars: 2.9778030386845606,
max_combo: 289,
};
#[cfg(any(feature = "async_tokio", feature = "async_str"))]
async fn test_map() -> (Beatmap, TaikoDifficultyAttributes) {
let path = "./maps/1028484.osu";
let map = Beatmap::from_path(path).await.unwrap();
(map, test_attrs())
}
#[rustfmt::skip]
macro_rules! test_data {
() => {{
#[cfg(not(any(feature = "async_tokio", feature = "async_str")))]
{ test_map() }
#[cfg(any(feature = "async_tokio", feature = "async_str"))]
{ test_map().await }
}};
(map, attrs)
}
#[test]
fn taiko_hitresults_n300_n_misses_best() {
let (map, attrs) = test_data!();
fn hitresults_n300_n_misses_best() {
let (map, attrs) = test_data();
let max_combo = attrs.max_combo();
let state = TaikoPP::new(&map)
.attributes(attrs)
.combo(500)
.n300(300)
.combo(100)
.n300(150)
.n_misses(2)
.hitresult_priority(HitResultPriority::BestCase)
.generate_hitresults(max_combo);
let expected = TaikoScoreState {
max_combo: 500,
n300: 300,
n100: 20,
max_combo: 100,
n300: 150,
n100: 137,
n_misses: 2,
};
assert_eq!(state, expected);
}
#[test]
fn hitresults_n_misses_best() {
let (map, attrs) = test_data();
let max_combo = attrs.max_combo();
let state = TaikoPP::new(&map)
.attributes(attrs)
.combo(100)
.n_misses(2)
.hitresult_priority(HitResultPriority::BestCase)
.generate_hitresults(max_combo);
let expected = TaikoScoreState {
max_combo: 100,
n300: 287,
n100: 0,
n_misses: 2,
};
assert_eq!(state, expected);
}
#[test]
fn hitresults_acc_n_misses_worst() {
let (map, attrs) = test_data();
let max_combo = attrs.max_combo();
let state = TaikoPP::new(&map)
.attributes(attrs)
.combo(100)
.accuracy(97.2)
.n_misses(2)
.hitresult_priority(HitResultPriority::WorstCase)
.generate_hitresults(max_combo);
let expected = TaikoScoreState {
max_combo: 100,
n300: 275,
n100: 12,
n_misses: 2,
};
assert_eq!(
state,
expected,
"{}% vs {}%",
state.accuracy(),
expected.accuracy()
);
}
}
+6 -6
View File
@@ -27,12 +27,12 @@ impl Peaks {
}
pub(crate) fn difficulty_values(self) -> PeaksDifficultyValues {
let colour_rating =
StrainSkill::difficulty_value(self.colour.clone()) * Self::COLOUR_SKILL_MULTIPLIER;
let rhythm_rating =
StrainSkill::difficulty_value(self.rhythm.clone()) * Self::RHYTHM_SKILL_MULTIPLIER;
let stamina_rating =
StrainSkill::difficulty_value(self.stamina.clone()) * Self::STAMINA_SKILL_MULTIPLIER;
let colour_rating = <Colour as StrainSkill>::difficulty_value(self.colour.clone())
* Self::COLOUR_SKILL_MULTIPLIER;
let rhythm_rating = <Rhythm as StrainSkill>::difficulty_value(self.rhythm.clone())
* Self::RHYTHM_SKILL_MULTIPLIER;
let stamina_rating = <Stamina as StrainSkill>::difficulty_value(self.stamina.clone())
* Self::STAMINA_SKILL_MULTIPLIER;
PeaksDifficultyValues {
colour_rating,
+30
View File
@@ -0,0 +1,30 @@
mod mode;
use rosu_pp::Beatmap;
pub use self::mode::{Catch, Mania, Mode, Osu, Taiko};
#[macro_export]
#[rustfmt::skip]
macro_rules! test_map {
($mode:ident) => {{
#[cfg(not(any(feature = "async_tokio", feature = "async_str")))]
{ common::test_map::<$mode>() }
#[cfg(any(feature = "async_tokio", feature = "async_str"))]
{ common::test_map::<$mode>().await }
}};
}
#[cfg(not(any(feature = "async_tokio", feature = "async_str")))]
pub fn test_map<M: Mode>() -> Beatmap {
let path = format!("./maps/{}.osu", M::TEST_MAP_ID);
Beatmap::from_path(path).unwrap()
}
#[cfg(any(feature = "async_tokio", feature = "async_str"))]
pub async fn test_map() -> Beatmap {
let path = format!("./maps/{}.osu", M::TEST_MAP_ID);
Beatmap::from_path(path).await.unwrap()
}
+68
View File
@@ -0,0 +1,68 @@
use rosu_pp::{
catch::CatchDifficultyAttributes, mania::ManiaDifficultyAttributes,
osu::OsuDifficultyAttributes, taiko::TaikoDifficultyAttributes,
};
pub struct Osu;
pub struct Taiko;
pub struct Catch;
pub struct Mania;
pub trait Mode {
type DifficultyAttributes;
const TEST_MAP_ID: u32;
const TEST_DIFF_ATTRS: Self::DifficultyAttributes;
}
macro_rules! impl_mode {
( $( $mode:ident: $map_id:literal, $diff_attrs:ident $fields:tt; )* ) => {
$(
impl Mode for $mode {
type DifficultyAttributes = $diff_attrs;
const TEST_MAP_ID: u32 = $map_id;
const TEST_DIFF_ATTRS: Self::DifficultyAttributes = $diff_attrs $fields;
}
)*
};
}
impl_mode! {
Osu: 2785319, OsuDifficultyAttributes {
aim: 2.8693628443424104,
speed: 2.533869745015772,
flashlight: 2.288770487900865,
slider_factor: 0.9803052946037858,
speed_note_count: 210.36373973116545,
ar: 9.300000190734863,
od: 8.800000190734863,
hp: 5.0,
n_circles: 307,
n_sliders: 293,
n_spinners: 1,
stars: 5.669858729379628,
max_combo: 909,
};
Taiko: 1028484, TaikoDifficultyAttributes {
stamina: 1.4528845068865617,
rhythm: 0.20130047251681948,
colour: 1.0487315549761433,
peak: 1.8881824429738323,
hit_window: 35.0,
stars: 2.9778030386845606,
max_combo: 289,
};
Catch: 2118524, CatchDifficultyAttributes {
stars: 3.2502669316166624,
ar: 8.0,
n_fruits: 728,
n_droplets: 2,
n_tiny_droplets: 291,
};
Mania: 1974394, ManiaDifficultyAttributes {
stars: 4.824631127426499,
hit_window: 40.0,
max_combo: 5064,
};
}
+40
View File
@@ -0,0 +1,40 @@
#![cfg(not(any(feature = "async_tokio", feature = "async_std")))]
use common::{test_map, Osu};
use rosu_pp::{CatchStars, ManiaStars, OsuStars, TaikoStars};
use crate::common::{Catch, Mania, Mode, Taiko};
mod common;
#[test]
fn difficulty_osu() {
let map = test_map::<Osu>();
let attrs = OsuStars::new(&map).calculate();
assert_eq!(Osu::TEST_DIFF_ATTRS, attrs);
}
#[test]
fn difficulty_taiko() {
let map = test_map::<Taiko>();
let attrs = TaikoStars::new(&map).calculate();
assert_eq!(Taiko::TEST_DIFF_ATTRS, attrs);
}
#[test]
fn difficulty_catch() {
let map = test_map::<Catch>();
let attrs = CatchStars::new(&map).calculate();
assert_eq!(Catch::TEST_DIFF_ATTRS, attrs);
}
#[test]
fn difficulty_mania() {
let map = test_map::<Mania>();
let attrs = ManiaStars::new(&map).calculate();
assert_eq!(Mania::TEST_DIFF_ATTRS, attrs);
}
+119
View File
@@ -0,0 +1,119 @@
#![cfg(not(any(feature = "async_tokio", feature = "async_std")))]
use rosu_pp::{
catch::{CatchGradualDifficultyAttributes, CatchGradualPerformanceAttributes, CatchScoreState},
Beatmap, CatchPP, CatchStars,
};
use crate::common::Catch;
mod common;
#[test]
fn empty_map() {
let map = Beatmap::default();
let mut attributes = CatchGradualDifficultyAttributes::new(&map, 0);
assert!(attributes.next().is_none());
}
#[test]
fn iter_end_eq_regular() {
let map = test_map!(Catch);
let regular = CatchStars::new(&map).calculate();
let iter_end = CatchGradualDifficultyAttributes::new(&map, 0)
.last()
.expect("empty iter");
assert_eq!(regular, iter_end);
}
#[test]
fn correct_empty() {
let map = test_map!(Catch);
let mut gradual = CatchGradualPerformanceAttributes::new(&map, 0);
let state = CatchScoreState::default();
let first_attrs = gradual.process_next_n_objects(state.clone(), usize::MAX);
assert!(first_attrs.is_some());
assert!(gradual.process_next_object(state).is_none());
}
#[test]
fn next_and_next_n() {
let map = test_map!(Catch);
let state = CatchScoreState::default();
let mut gradual1 = CatchGradualPerformanceAttributes::new(&map, 0);
let mut gradual2 = CatchGradualPerformanceAttributes::new(&map, 0);
for _ in 0..20 {
let _ = gradual1.process_next_object(state.clone());
let _ = gradual2.process_next_object(state.clone());
}
let n = 80;
for _ in 1..n {
let _ = gradual1.process_next_object(state.clone());
}
let state = CatchScoreState {
max_combo: 101,
n_fruits: 99,
n_droplets: 2,
n_tiny_droplets: 68,
n_tiny_droplet_misses: 0,
n_misses: 0,
};
let next = gradual1.process_next_object(state.clone());
let next_n = gradual2.process_next_n_objects(state, n);
assert_eq!(next_n, next);
}
#[test]
fn gradual_end_eq_regular() {
let map = test_map!(Catch);
let regular = CatchPP::new(&map).calculate();
let mut gradual = CatchGradualPerformanceAttributes::new(&map, 0);
let state = CatchScoreState {
max_combo: 730,
n_fruits: 728,
n_droplets: 2,
n_tiny_droplets: 291,
n_tiny_droplet_misses: 0,
n_misses: 0,
};
let gradual_end = gradual.process_next_n_objects(state, usize::MAX).unwrap();
assert_eq!(regular, gradual_end);
}
#[test]
fn gradual_eq_regular_passed() {
let map = test_map!(Catch);
let n = 100;
let regular = CatchPP::new(&map).passed_objects(n).calculate();
let mut gradual = CatchGradualPerformanceAttributes::new(&map, 0);
let state = CatchScoreState {
max_combo: 101,
n_fruits: 99,
n_droplets: 2,
n_tiny_droplets: 68,
n_tiny_droplet_misses: 0,
n_misses: 0,
};
let gradual = gradual.process_next_n_objects(state, n).unwrap();
assert_eq!(regular, gradual);
}
+131
View File
@@ -0,0 +1,131 @@
#![cfg(not(any(feature = "async_tokio", feature = "async_std")))]
use rosu_pp::{
mania::{ManiaGradualDifficultyAttributes, ManiaGradualPerformanceAttributes, ManiaScoreState},
Beatmap, ManiaPP, ManiaStars,
};
use crate::common::Mania;
mod common;
#[test]
fn empty_map() {
let map = Beatmap::default();
let mut attributes = ManiaGradualDifficultyAttributes::new(&map, 0);
assert!(attributes.next().is_none());
}
#[test]
fn iter_end_eq_regular() {
let map = test_map!(Mania);
let regular = ManiaStars::new(&map).calculate();
let iter_end = ManiaGradualDifficultyAttributes::new(&map, 0)
.last()
.expect("empty iter");
assert_eq!(regular, iter_end);
}
#[test]
fn correct_empty() {
let map = test_map!(Mania);
let mut gradual = ManiaGradualPerformanceAttributes::new(&map, 0);
let state = ManiaScoreState {
n320: 0,
n300: 0,
n200: 0,
n100: 0,
n50: 0,
n_misses: 0,
};
let first_attrs = gradual.process_next_n_objects(state.clone(), usize::MAX);
assert!(first_attrs.is_some());
assert!(gradual.process_next_object(state).is_none());
}
#[test]
fn next_and_next_n() {
let map = test_map!(Mania);
let mut state = ManiaScoreState {
n320: 0,
n300: 0,
n200: 0,
n100: 0,
n50: 0,
n_misses: 0,
};
let mut gradual1 = ManiaGradualPerformanceAttributes::new(&map, 0);
let mut gradual2 = ManiaGradualPerformanceAttributes::new(&map, 0);
for _ in 0..20 {
let _ = gradual1.process_next_object(state.clone());
let _ = gradual2.process_next_object(state.clone());
state.n320 += 1;
}
let n = 80;
for _ in 1..n {
let _ = gradual1.process_next_object(state.clone());
state.n320 += 1;
}
let next = gradual1.process_next_object(state.clone());
let next_n = gradual2.process_next_n_objects(state, n);
assert_eq!(next_n, next);
}
#[test]
fn gradual_end_eq_regular() {
let map = test_map!(Mania);
let regular = ManiaPP::new(&map).calculate();
let mut gradual = ManiaGradualPerformanceAttributes::new(&map, 0);
let state = ManiaScoreState {
n320: 3238,
n300: 0,
n200: 0,
n100: 0,
n50: 0,
n_misses: 0,
};
let gradual_end = gradual.process_next_n_objects(state, usize::MAX).unwrap();
assert_eq!(regular, gradual_end);
}
#[test]
fn gradual_eq_regular_passed() {
let map = test_map!(Mania);
let n = 100;
let state = ManiaScoreState {
n320: 100,
n300: 0,
n200: 0,
n100: 0,
n50: 0,
n_misses: 0,
};
let regular = ManiaPP::new(&map)
.passed_objects(n)
.state(state.clone())
.calculate();
let mut gradual = ManiaGradualPerformanceAttributes::new(&map, 0);
let gradual = gradual.process_next_n_objects(state, n).unwrap();
assert_eq!(regular, gradual);
}
+115
View File
@@ -0,0 +1,115 @@
#![cfg(not(any(feature = "async_tokio", feature = "async_std")))]
use rosu_pp::{
osu::{OsuGradualDifficultyAttributes, OsuGradualPerformanceAttributes, OsuScoreState},
Beatmap, OsuPP, OsuStars,
};
use crate::common::Osu;
mod common;
#[test]
fn empty_map() {
let map = Beatmap::default();
let mut attributes = OsuGradualDifficultyAttributes::new(&map, 0);
assert!(attributes.next().is_none());
}
#[test]
fn iter_end_eq_regular() {
let map = test_map!(Osu);
let regular = OsuStars::new(&map).calculate();
let iter_end = OsuGradualDifficultyAttributes::new(&map, 0)
.last()
.expect("empty iter");
assert_eq!(regular, iter_end);
}
#[test]
fn correct_empty() {
let map = test_map!(Osu);
let mut gradual = OsuGradualPerformanceAttributes::new(&map, 0);
let state = OsuScoreState::default();
let first_attrs = gradual.process_next_n_objects(state.clone(), usize::MAX);
assert!(first_attrs.is_some());
assert!(gradual.process_next_object(state).is_none());
}
#[test]
fn next_and_next_n() {
let map = test_map!(Osu);
let state = OsuScoreState::default();
let mut gradual1 = OsuGradualPerformanceAttributes::new(&map, 0);
let mut gradual2 = OsuGradualPerformanceAttributes::new(&map, 0);
for _ in 0..20 {
let _ = gradual1.process_next_object(state.clone());
let _ = gradual2.process_next_object(state.clone());
}
let n = 80;
for _ in 1..n {
let _ = gradual1.process_next_object(state.clone());
}
let state = OsuScoreState {
max_combo: 122,
n300: 88,
n100: 8,
n50: 2,
n_misses: 2,
};
let next = gradual1.process_next_object(state.clone());
let next_n = gradual2.process_next_n_objects(state, n);
assert_eq!(next_n, next);
}
#[test]
fn gradual_end_eq_regular() {
let map = test_map!(Osu);
let regular = OsuPP::new(&map).calculate();
let mut gradual = OsuGradualPerformanceAttributes::new(&map, 0);
let state = OsuScoreState {
max_combo: 909,
n300: 601,
n100: 0,
n50: 0,
n_misses: 0,
};
let gradual_end = gradual.process_next_n_objects(state, usize::MAX).unwrap();
assert_eq!(regular, gradual_end);
}
#[test]
fn gradual_eq_regular_passed() {
let map = test_map!(Osu);
let n = 100;
let regular = OsuPP::new(&map).passed_objects(n).calculate();
let mut gradual = OsuGradualPerformanceAttributes::new(&map, 0);
let state = OsuScoreState {
max_combo: 122,
n300: 100,
n100: 0,
n50: 0,
n_misses: 0,
};
let gradual = gradual.process_next_n_objects(state, n).unwrap();
assert_eq!(regular, gradual);
}
+113
View File
@@ -0,0 +1,113 @@
#![cfg(not(any(feature = "async_tokio", feature = "async_std")))]
use rosu_pp::{
taiko::{TaikoGradualDifficultyAttributes, TaikoGradualPerformanceAttributes, TaikoScoreState},
Beatmap, TaikoPP, TaikoStars,
};
use crate::common::Taiko;
mod common;
#[test]
fn empty_map() {
let map = Beatmap::default();
let mut attrs = TaikoGradualDifficultyAttributes::new(&map, 0);
assert!(attrs.next().is_none());
}
#[test]
fn iter_end_eq_regular() {
let map = test_map!(Taiko);
let regular = TaikoStars::new(&map).calculate();
let iter_end = TaikoGradualDifficultyAttributes::new(&map, 0)
.last()
.expect("empty iter");
assert_eq!(regular, iter_end);
}
#[test]
fn correct_empty() {
let map = test_map!(Taiko);
let mut gradual = TaikoGradualPerformanceAttributes::new(&map, 0);
let state = TaikoScoreState::default();
let first_attrs = gradual.process_next_n_objects(state.clone(), usize::MAX);
assert!(first_attrs.is_some());
assert!(gradual.process_next_object(state).is_none());
}
#[test]
fn next_and_next_n() {
let map = test_map!(Taiko);
let state = TaikoScoreState::default();
let mut gradual1 = TaikoGradualPerformanceAttributes::new(&map, 0);
let mut gradual2 = TaikoGradualPerformanceAttributes::new(&map, 0);
for _ in 0..50 {
let _ = gradual1.process_next_object(state.clone());
let _ = gradual2.process_next_object(state.clone());
}
let n = 200;
for _ in 1..n {
let _ = gradual1.process_next_object(state.clone());
}
let state = TaikoScoreState {
max_combo: 246,
n300: 200,
n100: 40,
n_misses: 6,
};
let next = gradual1.process_next_object(state.clone());
let next_n = gradual2.process_next_n_objects(state, n);
assert_eq!(next_n, next);
}
#[test]
fn gradual_end_eq_regular() {
let map = test_map!(Taiko);
let regular = TaikoPP::new(&map).calculate();
let mut gradual = TaikoGradualPerformanceAttributes::new(&map, 0);
let state = TaikoScoreState {
max_combo: 289,
n300: 289,
n100: 0,
n_misses: 0,
};
let gradual_end = gradual.process_next_n_objects(state, usize::MAX).unwrap();
assert_eq!(regular, gradual_end);
}
#[test]
fn gradual_eq_regular_passed() {
let map = test_map!(Taiko);
let n = 250;
let regular = TaikoPP::new(&map).passed_objects(n).calculate();
let mut gradual = TaikoGradualPerformanceAttributes::new(&map, 0);
let state = TaikoScoreState {
max_combo: 246,
n300: 246,
n100: 0,
n_misses: 0,
};
let gradual = gradual.process_next_n_objects(state, n).unwrap();
// Cyclic types in difficulty calculation prohibit values to coincide completely
assert_eq!(regular.difficulty.max_combo, gradual.difficulty.max_combo);
}