refactor!: overhauled gradual calc for mania

This commit is contained in:
MaxOhn
2023-11-08 11:37:02 +01:00
parent 74083e5ecb
commit 4d583f5469
6 changed files with 164 additions and 151 deletions
+1 -1
View File
@@ -234,7 +234,7 @@ impl<'map> GradualPerformanceAttributes<'map> {
.process_next_n_objects(state.into(), n) .process_next_n_objects(state.into(), n)
.map(PerformanceAttributes::Catch), .map(PerformanceAttributes::Catch),
GradualPerformanceAttributes::Mania(m) => m GradualPerformanceAttributes::Mania(m) => m
.process_next_n_objects(state.into(), n) .nth(state.into(), n)
.map(PerformanceAttributes::Mania), .map(PerformanceAttributes::Mania),
} }
} }
+59 -29
View File
@@ -1,3 +1,5 @@
#![cfg(feature = "gradual")]
use std::borrow::Cow; use std::borrow::Cow;
use crate::{ use crate::{
@@ -16,8 +18,8 @@ use super::{
/// Gradually calculate the difficulty attributes of an osu!mania map. /// Gradually calculate the difficulty attributes of an osu!mania map.
/// ///
/// Note that this struct implements [`Iterator`](std::iter::Iterator). /// Note that this struct implements [`Iterator`].
/// On every call of [`Iterator::next`](std::iter::Iterator::next), the map's next hit object will /// On every call of [`Iterator::next`](Iterator::next), the map's next hit object will
/// be processed and the [`ManiaDifficultyAttributes`] will be updated and returned. /// be processed and the [`ManiaDifficultyAttributes`] will be updated and returned.
/// ///
/// If you want to calculate performance attributes, use /// If you want to calculate performance attributes, use
@@ -50,7 +52,7 @@ pub struct ManiaGradualDifficultyAttributes<'map> {
map: Cow<'map, Beatmap>, map: Cow<'map, Beatmap>,
hit_window: f64, hit_window: f64,
strain: Strain, strain: Strain,
diff_objects: Vec<ManiaDifficultyObject>, diff_objects: Box<[ManiaDifficultyObject]>,
curr_combo: usize, curr_combo: usize,
clock_rate: f64, clock_rate: f64,
} }
@@ -71,25 +73,35 @@ impl<'map> ManiaGradualDifficultyAttributes<'map> {
.hit_windows(); .hit_windows();
let mut params = ObjectParameters::new(map.as_ref()); let mut params = ObjectParameters::new(map.as_ref());
let mut curr_combo = 0;
let mut hit_objects = map.hit_objects.iter(); let mut hit_objects = map.hit_objects.iter();
let first = match hit_objects.next() { let first = match hit_objects.next() {
Some(h) => ManiaObject::new(h, total_columns, &mut params), Some(h) => {
let hit_object = ManiaObject::new(h, total_columns, &mut params);
Self::increment_combo_raw(
h,
hit_object.start_time,
hit_object.end_time,
&mut curr_combo,
);
hit_object
}
None => { None => {
return Self { return Self {
idx: 0, idx: 0,
map, map,
hit_window, hit_window,
strain, strain,
diff_objects: Vec::new(), diff_objects: Box::from([]),
curr_combo: 0, curr_combo: 0,
clock_rate, clock_rate,
} }
} }
}; };
let curr_combo = params.max_combo;
let diff_objects_iter = hit_objects.enumerate().scan(first, |last, (i, h)| { let diff_objects_iter = hit_objects.enumerate().scan(first, |last, (i, h)| {
let base = ManiaObject::new(h, total_columns, &mut params); let base = ManiaObject::new(h, total_columns, &mut params);
let diff_object = ManiaDifficultyObject::new(&base, &*last, clock_rate, i); let diff_object = ManiaDifficultyObject::new(&base, &*last, clock_rate, i);
@@ -98,15 +110,17 @@ impl<'map> ManiaGradualDifficultyAttributes<'map> {
Some(diff_object) Some(diff_object)
}); });
let mut diff_objects = Vec::with_capacity(map.hit_objects.len().saturating_sub(1)); let mut diff_objects = Vec::with_capacity(map.hit_objects.len() - 1);
diff_objects.extend(diff_objects_iter); diff_objects.extend(diff_objects_iter);
debug_assert_eq!(diff_objects.len(), diff_objects.capacity());
Self { Self {
idx: 0, idx: 0,
map, map,
hit_window, hit_window,
strain, strain,
diff_objects, diff_objects: diff_objects.into_boxed_slice(),
curr_combo, curr_combo,
clock_rate, clock_rate,
} }
@@ -118,15 +132,18 @@ impl<'map> ManiaGradualDifficultyAttributes<'map> {
curr_combo: &mut usize, curr_combo: &mut usize,
clock_rate: f64, clock_rate: f64,
) { ) {
match &h.kind { Self::increment_combo_raw(
HitObjectKind::Circle => *curr_combo += 1, h,
_ => { diff_obj.start_time * clock_rate,
let start_time = diff_obj.start_time * clock_rate; diff_obj.end_time * clock_rate,
let end_time = diff_obj.end_time * clock_rate; curr_combo,
let duration = end_time - start_time; );
}
*curr_combo += 1 + (duration / 100.0) as usize; fn increment_combo_raw(h: &HitObject, start_time: f64, end_time: f64, curr_combo: &mut usize) {
} match h.kind {
HitObjectKind::Circle => *curr_combo += 1,
_ => *curr_combo += 1 + ((end_time - start_time) / 100.0) as usize,
} }
} }
} }
@@ -135,14 +152,20 @@ impl Iterator for ManiaGradualDifficultyAttributes<'_> {
type Item = ManiaDifficultyAttributes; type Item = ManiaDifficultyAttributes;
fn next(&mut self) -> Option<Self::Item> { fn next(&mut self) -> Option<Self::Item> {
let curr = self.diff_objects.get(self.idx)?; // The first difficulty object belongs to the second note since each difficulty
self.idx += 1; // object requires the current and the last note. Hence, if we're still on the first
// object, we don't have a difficulty object yet and just skip processing.
if self.idx > 0 {
let curr = self.diff_objects.get(self.idx - 1)?;
self.strain.process(curr, &self.diff_objects);
if let Some(h) = self.map.hit_objects.get(self.idx) { let h = &self.map.hit_objects[self.idx];
Self::increment_combo(h, curr, &mut self.curr_combo, self.clock_rate); Self::increment_combo(h, curr, &mut self.curr_combo, self.clock_rate);
} else if self.map.hit_objects.is_empty() {
return None;
} }
self.strain.process(curr, &self.diff_objects); self.idx += 1;
Some(ManiaDifficultyAttributes { Some(ManiaDifficultyAttributes {
stars: self.strain.clone().difficulty_value() * STAR_SCALING_FACTOR, stars: self.strain.clone().difficulty_value() * STAR_SCALING_FACTOR,
@@ -159,17 +182,24 @@ impl Iterator for ManiaGradualDifficultyAttributes<'_> {
} }
fn nth(&mut self, n: usize) -> Option<Self::Item> { fn nth(&mut self, n: usize) -> Option<Self::Item> {
let skip = n.min(self.len()).saturating_sub(1); let skip_iter = self
.diff_objects
.iter()
.zip(self.map.hit_objects.iter().skip(1))
.skip(self.idx.saturating_sub(1));
for _ in 0..skip { let mut take = n.min(self.len().saturating_sub(1));
let curr = self.diff_objects.get(self.idx)?;
// The first note has no difficulty object
if self.idx == 0 && take > 0 {
take -= 1;
self.idx += 1; self.idx += 1;
}
if let Some(h) = self.map.hit_objects.get(self.idx) { for (curr, h) in skip_iter.take(take) {
Self::increment_combo(h, curr, &mut self.curr_combo, self.clock_rate); Self::increment_combo(h, curr, &mut self.curr_combo, self.clock_rate);
}
self.strain.process(curr, &self.diff_objects); self.strain.process(curr, &self.diff_objects);
self.idx += 1;
} }
self.next() self.next()
@@ -179,6 +209,6 @@ impl Iterator for ManiaGradualDifficultyAttributes<'_> {
impl ExactSizeIterator for ManiaGradualDifficultyAttributes<'_> { impl ExactSizeIterator for ManiaGradualDifficultyAttributes<'_> {
#[inline] #[inline]
fn len(&self) -> usize { fn len(&self) -> usize {
self.diff_objects.len() - self.idx self.diff_objects.len() + 1 - self.idx
} }
} }
+24 -80
View File
@@ -1,69 +1,21 @@
#![cfg(feature = "gradual")]
use crate::{Beatmap, ManiaPP}; use crate::{Beatmap, ManiaPP};
use super::{ManiaGradualDifficultyAttributes, ManiaPerformanceAttributes}; use super::{ManiaGradualDifficultyAttributes, ManiaPerformanceAttributes, ManiaScoreState};
/// 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.
pub n320: usize,
/// Amount of current 300s.
pub n300: usize,
/// Amount of current 200s.
pub n200: usize,
/// Amount of current 100s.
pub n100: usize,
/// Amount of current 50s.
pub n50: usize,
/// Amount of current misses.
pub n_misses: usize,
}
impl ManiaScoreState {
/// Create a new empty score state.
#[inline]
pub fn new() -> Self {
Self::default()
}
/// Return the total amount of hits by adding everything up.
#[inline]
pub fn total_hits(&self) -> usize {
self.n320 + self.n300 + self.n200 + self.n100 + self.n50 + self.n_misses
}
/// Calculate the accuracy between `0.0` and `1.0` for this state.
#[inline]
pub fn accuracy(&self) -> f64 {
let total_hits = self.total_hits();
if total_hits == 0 {
return 0.0;
}
let numerator = 6 * (self.n320 + self.n300) + 4 * self.n200 + 2 * self.n100 + self.n50;
let denominator = 6 * total_hits;
numerator as f64 / denominator as f64
}
}
/// Gradually calculate the performance attributes of an osu!mania map. /// Gradually calculate the performance attributes of an osu!mania map.
/// ///
/// After each hit object you can call /// After each hit object you can call [`next`](`ManiaGradualPerformanceAttributes::next`)
/// [`process_next_object`](`ManiaGradualPerformanceAttributes::process_next_object`)
/// and it will return the resulting current [`ManiaPerformanceAttributes`]. /// and it will return the resulting current [`ManiaPerformanceAttributes`].
/// To process multiple objects at once, use /// To process multiple objects at once, use
/// [`process_next_n_objects`](`ManiaGradualPerformanceAttributes::process_next_n_objects`) instead. /// [`nth`](`ManiaGradualPerformanceAttributes::nth`) instead.
/// ///
/// Both methods require a play's current score so far. /// Both methods require a play's current score so far.
/// Be sure the given score is adjusted with respect to mods. /// Be sure the given score is adjusted with respect to mods.
/// ///
/// If you only want to calculate difficulty attributes use /// If you only want to calculate difficulty attributes use
/// [`ManiaGradualDifficultyAttributes`](crate::mania::ManiaGradualDifficultyAttributes) instead. /// [`ManiaGradualDifficultyAttributes`] instead.
/// ///
/// # Example /// # Example
/// ///
@@ -84,29 +36,30 @@ impl ManiaScoreState {
/// state.n320 += 1; /// state.n320 += 1;
/// ///
/// # /* /// # /*
/// let performance = gradual_perf.process_next_object(score).unwrap(); /// let performance = gradual_perf.next(score).unwrap();
/// println!("PP: {}", performance.pp); /// println!("PP: {}", performance.pp);
/// # */ /// # */
/// # let _ = gradual_perf.process_next_object(state.clone()); /// # let _ = gradual_perf.next(state.clone());
/// } /// }
/// ///
/// // Then comes a miss. /// // Then comes a miss.
/// state.n_misses += 1; /// state.n_misses += 1;
/// # /* /// # /*
/// let performance = gradual_perf.process_next_object(score).unwrap(); /// let performance = gradual_perf.next(score).unwrap();
/// println!("PP: {}", performance.pp); /// println!("PP: {}", performance.pp);
/// # */ /// # */
/// # let _ = gradual_perf.process_next_object(state.clone()); /// # let _ = gradual_perf.next(state.clone());
/// ///
/// // The next 10 objects will be a mixture of 320s and 100s. /// // The next 10 objects will be a mixture of 320s and 100s.
/// // Notice how all 10 objects will be processed in one go. /// // Notice how all 10 objects will be processed in one go.
/// state.n320 += 3; /// state.n320 += 3;
/// state.n100 += 7; /// state.n100 += 7;
/// // The `nth` method takes a zero-based value.
/// # /* /// # /*
/// let performance = gradual_perf.process_next_n_objects(score, 10).unwrap(); /// let performance = gradual_perf.nth(score, 9).unwrap();
/// println!("PP: {}", performance.pp); /// println!("PP: {}", performance.pp);
/// # */ /// # */
/// # let _ = gradual_perf.process_next_n_objects(state.clone(), 10); /// # let _ = gradual_perf.nth(state.clone(), 9);
/// ///
/// // Skip to the end /// // Skip to the end
/// # /* /// # /*
@@ -114,14 +67,14 @@ impl ManiaScoreState {
/// state.n300 = ... /// state.n300 = ...
/// state.n100 = ... /// state.n100 = ...
/// state.n_misses = ... /// state.n_misses = ...
/// let final_performance = gradual_perf.process_next_n_objects(state.clone(), usize::MAX).unwrap(); /// let final_performance = gradual_perf.nth(state.clone(), usize::MAX).unwrap();
/// println!("PP: {}", performance.pp); /// println!("PP: {}", performance.pp);
/// # */ /// # */
/// # let _ = gradual_perf.process_next_n_objects(state.clone(), usize::MAX); /// # let _ = gradual_perf.nth(state.clone(), usize::MAX);
/// ///
/// // Once the final performance was calculated, /// // Once the final performance was calculated,
/// // attempting to process further objects will return `None`. /// // attempting to process further objects will return `None`.
/// assert!(gradual_perf.process_next_object(state).is_none()); /// assert!(gradual_perf.next(state).is_none());
/// ``` /// ```
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct ManiaGradualPerformanceAttributes<'map> { pub struct ManiaGradualPerformanceAttributes<'map> {
@@ -143,26 +96,17 @@ impl<'map> ManiaGradualPerformanceAttributes<'map> {
/// Process the next hit object and calculate the /// Process the next hit object and calculate the
/// performance attributes for the resulting score. /// performance attributes for the resulting score.
pub fn process_next_object( pub fn next(&mut self, state: ManiaScoreState) -> Option<ManiaPerformanceAttributes> {
&mut self, self.nth(state, 0)
state: ManiaScoreState,
) -> Option<ManiaPerformanceAttributes> {
self.process_next_n_objects(state, 1)
} }
/// Same as [`process_next_object`](`ManiaGradualPerformanceAttributes::process_next_object`) /// Process everything up the the next `n`th hit object and calculate the performance
/// but instead of processing only one object it process `n` many. /// attributes for the resulting score state.
/// ///
/// If `n` is 0 it will be considered as 1. /// Note that the count is zero-indexed, so `n=0` will process 1 object, `n=1` will process 2,
/// If there are still objects to be processed but `n` is larger than the amount /// and so on.
/// of remaining objects, `n` will be considered as the amount of remaining objects. pub fn nth(&mut self, state: ManiaScoreState, n: usize) -> Option<ManiaPerformanceAttributes> {
pub fn process_next_n_objects( let difficulty = self.difficulty.nth(n)?;
&mut self,
state: ManiaScoreState,
n: usize,
) -> Option<ManiaPerformanceAttributes> {
let sub = (self.difficulty.idx == 0) as usize;
let difficulty = self.difficulty.nth(n.saturating_sub(sub))?;
let performance = self let performance = self
.performance .performance
+13 -3
View File
@@ -1,15 +1,25 @@
mod difficulty_object; mod difficulty_object;
mod gradual_difficulty;
mod gradual_performance;
mod mania_object; mod mania_object;
mod pp; mod pp;
mod score_state;
mod skills; mod skills;
#[cfg(feature = "gradual")]
mod gradual_difficulty;
#[cfg(feature = "gradual")]
mod gradual_performance;
use std::borrow::Cow; use std::borrow::Cow;
use crate::{beatmap::BeatmapHitWindows, util::FloatExt, Beatmap, GameMode, Mods, OsuStars}; use crate::{beatmap::BeatmapHitWindows, util::FloatExt, Beatmap, GameMode, Mods, OsuStars};
pub use self::{gradual_difficulty::*, gradual_performance::*, mania_object::ManiaObject, pp::*}; pub use self::{mania_object::ManiaObject, pp::*, score_state::ManiaScoreState};
#[cfg(feature = "gradual")]
pub use self::{
gradual_difficulty::ManiaGradualDifficultyAttributes,
gradual_performance::ManiaGradualPerformanceAttributes,
};
pub(crate) use self::mania_object::ObjectParameters; pub(crate) use self::mania_object::ObjectParameters;
+47
View File
@@ -0,0 +1,47 @@
/// Aggregation for a score's current state i.e. what are the current hitresults.
///
/// This struct is used for [`ManiaGradualPerformanceAttributes`](crate::mania::ManiaGradualPerformanceAttributes).
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct ManiaScoreState {
/// Amount of current 320s.
pub n320: usize,
/// Amount of current 300s.
pub n300: usize,
/// Amount of current 200s.
pub n200: usize,
/// Amount of current 100s.
pub n100: usize,
/// Amount of current 50s.
pub n50: usize,
/// Amount of current misses.
pub n_misses: usize,
}
impl ManiaScoreState {
/// Create a new empty score state.
#[inline]
pub fn new() -> Self {
Self::default()
}
/// Return the total amount of hits by adding everything up.
#[inline]
pub fn total_hits(&self) -> usize {
self.n320 + self.n300 + self.n200 + self.n100 + self.n50 + self.n_misses
}
/// Calculate the accuracy between `0.0` and `1.0` for this state.
#[inline]
pub fn accuracy(&self) -> f64 {
let total_hits = self.total_hits();
if total_hits == 0 {
return 0.0;
}
let numerator = 6 * (self.n320 + self.n300) + 4 * self.n200 + 2 * self.n100 + self.n50;
let denominator = 6 * total_hits;
numerator as f64 / denominator as f64
}
}
+20 -38
View File
@@ -1,4 +1,7 @@
#![cfg(all(not(any(feature = "async_tokio", feature = "async_std")), feature = "gradual"))] #![cfg(all(
not(any(feature = "async_tokio", feature = "async_std")),
feature = "gradual"
))]
use rosu_pp::{ use rosu_pp::{
mania::{ManiaGradualDifficultyAttributes, ManiaGradualPerformanceAttributes, ManiaScoreState}, mania::{ManiaGradualDifficultyAttributes, ManiaGradualPerformanceAttributes, ManiaScoreState},
@@ -34,52 +37,38 @@ fn correct_empty() {
let map = test_map!(Mania); let map = test_map!(Mania);
let mut gradual = ManiaGradualPerformanceAttributes::new(&map, 0); let mut gradual = ManiaGradualPerformanceAttributes::new(&map, 0);
let state = ManiaScoreState { let state = ManiaScoreState::default();
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); let first_attrs = gradual.nth(state.clone(), usize::MAX);
assert!(first_attrs.is_some()); assert!(first_attrs.is_some());
assert!(gradual.process_next_object(state).is_none()); assert!(gradual.next(state).is_none());
} }
#[test] #[test]
fn next_and_next_n() { fn next_and_next_n() {
let map = test_map!(Mania); let map = test_map!(Mania);
let mut state = ManiaScoreState { let mut state = ManiaScoreState::default();
n320: 0,
n300: 0,
n200: 0,
n100: 0,
n50: 0,
n_misses: 0,
};
let mut gradual1 = ManiaGradualPerformanceAttributes::new(&map, 0); let mut gradual1 = ManiaGradualPerformanceAttributes::new(&map, 0);
let mut gradual2 = ManiaGradualPerformanceAttributes::new(&map, 0); let mut gradual2 = ManiaGradualPerformanceAttributes::new(&map, 0);
for _ in 0..20 { for _ in 0..20 {
let _ = gradual1.process_next_object(state.clone()); let _ = gradual1.next(state.clone());
let _ = gradual2.process_next_object(state.clone()); let _ = gradual2.next(state.clone());
state.n320 += 1; state.n320 += 1;
} }
let n = 80; let n = 80;
for _ in 1..n { for _ in 1..n {
let _ = gradual1.process_next_object(state.clone()); let _ = gradual1.next(state.clone());
state.n320 += 1; state.n320 += 1;
} }
let next = gradual1.process_next_object(state.clone()); let next = gradual1.next(state.clone());
let next_n = gradual2.process_next_n_objects(state, n); let next_n = gradual2.nth(state, n - 1);
assert_eq!(next_n, next); assert_eq!(next_n, next);
} }
@@ -92,15 +81,11 @@ fn gradual_end_eq_regular() {
let mut gradual = ManiaGradualPerformanceAttributes::new(&map, 0); let mut gradual = ManiaGradualPerformanceAttributes::new(&map, 0);
let state = ManiaScoreState { let state = ManiaScoreState {
n320: 3238, n320: map.hit_objects.len(),
n300: 0, ..Default::default()
n200: 0,
n100: 0,
n50: 0,
n_misses: 0,
}; };
let gradual_end = gradual.process_next_n_objects(state, usize::MAX).unwrap(); let gradual_end = gradual.nth(state, usize::MAX).unwrap();
assert_eq!(regular, gradual_end); assert_eq!(regular, gradual_end);
} }
@@ -112,11 +97,7 @@ fn gradual_eq_regular_passed() {
let state = ManiaScoreState { let state = ManiaScoreState {
n320: 100, n320: 100,
n300: 0, ..Default::default()
n200: 0,
n100: 0,
n50: 0,
n_misses: 0,
}; };
let regular = ManiaPP::new(&map) let regular = ManiaPP::new(&map)
@@ -124,8 +105,9 @@ fn gradual_eq_regular_passed() {
.state(state.clone()) .state(state.clone())
.calculate(); .calculate();
let mut gradual = ManiaGradualPerformanceAttributes::new(&map, 0); let gradual = ManiaGradualPerformanceAttributes::new(&map, 0)
let gradual = gradual.process_next_n_objects(state, n).unwrap(); .nth(state, n - 1)
.unwrap();
assert_eq!(regular, gradual); assert_eq!(regular, gradual);
} }