added TaikoGradualPerformanceAttributes
This commit is contained in:
@@ -379,7 +379,7 @@ mod tests {
|
||||
#[cfg(not(any(feature = "async_tokio", feature = "async_std")))]
|
||||
#[test]
|
||||
fn iter_end_eq_regular() {
|
||||
let map = Beatmap::from_path("./maps/222766.osu").expect("failed to parse map");
|
||||
let map = Beatmap::from_path("./maps/1028484.osu").expect("failed to parse map");
|
||||
let mods = 64;
|
||||
let regular = crate::taiko::stars(&map, mods, None);
|
||||
|
||||
|
||||
@@ -0,0 +1,262 @@
|
||||
use crate::{Beatmap, TaikoPP};
|
||||
|
||||
use super::{TaikoGradualDifficultyAttributes, TaikoPerformanceAttributes};
|
||||
|
||||
// TODO: Benchmark if Copy is faster than Clone
|
||||
/// Aggregation for a score's current state i.e. what was the
|
||||
/// maximum combo so far and what are the current hitresults.
|
||||
#[derive(Copy, Clone, Debug, Default)]
|
||||
pub struct TaikoScoreState {
|
||||
/// Maximum combo that the score has had so far.
|
||||
/// **Not** the maximum possible combo of the map so far.
|
||||
pub max_combo: usize,
|
||||
/// Amount of current 300s.
|
||||
pub n300: usize,
|
||||
/// Amount of current 100s.
|
||||
pub n100: usize,
|
||||
/// Amount of current misses.
|
||||
pub misses: usize,
|
||||
}
|
||||
|
||||
impl TaikoScoreState {
|
||||
/// Create a new empty score state.
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Gradually calculate the performance attributes of an osu!taiko map.
|
||||
///
|
||||
/// After each hit object you can call
|
||||
/// [`process_next_object`](`TaikoGradualPerformanceAttributes::process_next_object`)
|
||||
/// and it will return the resulting current [`TaikoPerformanceAttributes`].
|
||||
/// To process multiple objects at once, use
|
||||
/// [`process_next_n_objects`](`TaikoGradualPerformanceAttributes::process_next_n_objects`) instead.
|
||||
///
|
||||
/// Both methods require an [`TaikoScoreState`] that contains the current
|
||||
/// hitresults as well as the maximum combo so far.
|
||||
///
|
||||
/// If you only want to calculate difficulty attributes use
|
||||
/// [`TaikoGradualDifficultyAttributes`](crate::taiko::TaikoGradualDifficultyAttributes) instead.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use rosu_pp::{Beatmap, taiko::{TaikoGradualPerformanceAttributes, TaikoScoreState}};
|
||||
///
|
||||
/// # /*
|
||||
/// let map: Beatmap = ...
|
||||
/// # */
|
||||
/// # let map = Beatmap::default();
|
||||
///
|
||||
/// let mods = 64; // DT
|
||||
/// let mut gradual_perf = TaikoGradualPerformanceAttributes::new(&map, mods);
|
||||
/// let mut state = TaikoScoreState::new(); // empty state, everything is on 0.
|
||||
///
|
||||
/// // The first 10 hitresults are 300s
|
||||
/// for _ in 0..10 {
|
||||
/// state.n300 += 1;
|
||||
/// state.max_combo += 1;
|
||||
///
|
||||
/// # /*
|
||||
/// let performance = gradual_perf.process_next_object(state).unwrap();
|
||||
/// println!("PP: {}", performance.pp);
|
||||
/// # */
|
||||
/// # let _ = gradual_perf.process_next_object(state);
|
||||
/// }
|
||||
///
|
||||
/// // 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;
|
||||
/// # /*
|
||||
/// let performance = gradual_perf.process_next_object(state).unwrap();
|
||||
/// println!("PP: {}", performance.pp);
|
||||
/// # */
|
||||
/// # let _ = gradual_perf.process_next_object(state);
|
||||
///
|
||||
/// // The next 10 objects will be a mixture of 300s and 100s.
|
||||
/// // Notice how all 10 objects will be processed in one go.
|
||||
/// state.n300 += 3;
|
||||
/// state.n100 += 7;
|
||||
/// # /*
|
||||
/// let performance = gradual_perf.process_next_n_objects(state, 10).unwrap();
|
||||
/// println!("PP: {}", performance.pp);
|
||||
/// # */
|
||||
/// # let _ = gradual_perf.process_next_n_objects(state, 10);
|
||||
///
|
||||
/// // Now comes another 300. Note that the max combo gets incremented again.
|
||||
/// state.n300 += 1;
|
||||
/// state.max_combo += 1;
|
||||
/// # /*
|
||||
/// let performance = gradual_perf.process_next_object(state).unwrap();
|
||||
/// println!("PP: {}", performance.pp);
|
||||
/// # */
|
||||
/// # let _ = gradual_perf.process_next_object(state);
|
||||
///
|
||||
/// // Skip to the end
|
||||
/// # /*
|
||||
/// state.max_combo = ...
|
||||
/// state.n300 = ...
|
||||
/// state.n100 = ...
|
||||
/// state.misses = ...
|
||||
/// let final_performance = gradual_perf.process_next_n_objects(state, usize::MAX).unwrap();
|
||||
/// println!("PP: {}", performance.pp);
|
||||
/// # */
|
||||
/// # let _ = gradual_perf.process_next_n_objects(state, usize::MAX);
|
||||
///
|
||||
/// // Once the final performance was calculated,
|
||||
/// // attempting to process further objects will return `None`.
|
||||
/// assert!(gradual_perf.process_next_object(state).is_none());
|
||||
/// ```
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct TaikoGradualPerformanceAttributes<'map> {
|
||||
difficulty: TaikoGradualDifficultyAttributes<'map>,
|
||||
performance: TaikoPP<'map>,
|
||||
}
|
||||
|
||||
impl<'map> TaikoGradualPerformanceAttributes<'map> {
|
||||
/// Create a new gradual performance calculator for osu!taiko maps.
|
||||
pub fn new(map: &'map Beatmap, mods: u32) -> Self {
|
||||
let difficulty = TaikoGradualDifficultyAttributes::new(map, mods);
|
||||
let performance = TaikoPP::new(map).mods(mods).passed_objects(0);
|
||||
|
||||
Self {
|
||||
difficulty,
|
||||
performance,
|
||||
}
|
||||
}
|
||||
|
||||
/// Process the next hit object and calculate the
|
||||
/// performance attributes for the resulting score.
|
||||
pub fn process_next_object(
|
||||
&mut self,
|
||||
state: TaikoScoreState,
|
||||
) -> Option<TaikoPerformanceAttributes> {
|
||||
self.process_next_n_objects(state, 1)
|
||||
}
|
||||
|
||||
/// Same as [`process_next_object`](`TaikoGradualPerformanceAttributes::process_next_object`)
|
||||
/// but instead of processing only one object it process `n` many.
|
||||
///
|
||||
/// If `n` is 0 it will be considered as 1.
|
||||
/// If there are still objects to be processed but `n` is larger than the amount
|
||||
/// of remaining objects, `n` will be considered as the amount of remaining objects.
|
||||
pub fn process_next_n_objects(
|
||||
&mut self,
|
||||
state: TaikoScoreState,
|
||||
n: usize,
|
||||
) -> Option<TaikoPerformanceAttributes> {
|
||||
let n = n.min(self.difficulty.len()).saturating_sub(1);
|
||||
let difficulty = self.difficulty.nth(n)?;
|
||||
|
||||
let _ = self.performance.n300.insert(state.n300);
|
||||
let _ = self.performance.n100.insert(state.n100);
|
||||
self.performance.n_misses = state.misses;
|
||||
|
||||
let performance = self
|
||||
.performance
|
||||
.clone()
|
||||
.attributes(difficulty)
|
||||
.combo(state.max_combo)
|
||||
.passed_objects(self.difficulty.idx)
|
||||
.calculate();
|
||||
|
||||
Some(performance)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
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, 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);
|
||||
let _ = gradual2.process_next_object(state);
|
||||
}
|
||||
|
||||
let n = 200;
|
||||
|
||||
for _ in 1..n {
|
||||
let _ = gradual1.process_next_object(state);
|
||||
}
|
||||
|
||||
let state = TaikoScoreState {
|
||||
max_combo: 246,
|
||||
n300: 200,
|
||||
n100: 40,
|
||||
misses: 6,
|
||||
};
|
||||
|
||||
let next = gradual1.process_next_object(state);
|
||||
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,
|
||||
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,
|
||||
misses: 0,
|
||||
};
|
||||
|
||||
let gradual = gradual.process_next_n_objects(state, n).unwrap();
|
||||
|
||||
assert_eq!(regular, gradual);
|
||||
}
|
||||
}
|
||||
+3
-1
@@ -2,6 +2,7 @@
|
||||
|
||||
mod difficulty_object;
|
||||
mod gradual_difficulty;
|
||||
mod gradual_performance;
|
||||
mod hitobject_rhythm;
|
||||
mod limited_queue;
|
||||
mod pp;
|
||||
@@ -12,6 +13,7 @@ mod stamina_cheese;
|
||||
|
||||
use difficulty_object::DifficultyObject;
|
||||
pub use gradual_difficulty::*;
|
||||
pub use gradual_performance::*;
|
||||
use hitobject_rhythm::{closest_rhythm, HitObjectRhythm};
|
||||
use limited_queue::LimitedQueue;
|
||||
pub use pp::*;
|
||||
@@ -227,7 +229,7 @@ impl TaikoDifficultyAttributes {
|
||||
}
|
||||
|
||||
/// The result of a performance calculation on an osu!taiko map.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct TaikoPerformanceAttributes {
|
||||
/// The difficulty attributes that were used for the performance calculation
|
||||
pub difficulty: TaikoDifficultyAttributes,
|
||||
|
||||
+3
-3
@@ -38,11 +38,11 @@ pub struct TaikoPP<'map> {
|
||||
mods: u32,
|
||||
combo: Option<usize>,
|
||||
acc: f64,
|
||||
n_misses: usize,
|
||||
passed_objects: Option<usize>,
|
||||
|
||||
n300: Option<usize>,
|
||||
n100: Option<usize>,
|
||||
pub(crate) n300: Option<usize>,
|
||||
pub(crate) n100: Option<usize>,
|
||||
pub(crate) n_misses: usize,
|
||||
}
|
||||
|
||||
impl<'map> TaikoPP<'map> {
|
||||
|
||||
Reference in New Issue
Block a user