refactor!: overhauled gradual calc for catch

This commit is contained in:
MaxOhn
2023-11-08 21:08:07 +01:00
parent 4d583f5469
commit 2be5bdc365
6 changed files with 96 additions and 93 deletions
+21 -11
View File
@@ -153,11 +153,15 @@ impl Iterator for CatchGradualDifficultyAttributes<'_> {
*last = self.movement.curr_section_peak; *last = self.movement.curr_section_peak;
} }
let mut attributes = self.hit_objects.attributes(); let stars =
attributes.stars =
Movement::difficulty_value(&mut self.strain_peak_buf).sqrt() * STAR_SCALING_FACTOR; Movement::difficulty_value(&mut self.strain_peak_buf).sqrt() * STAR_SCALING_FACTOR;
Some(attributes) let attrs = CatchDifficultyAttributes {
stars,
..self.hit_objects.attributes()
};
Some(attrs)
} }
} }
@@ -196,16 +200,22 @@ impl Iterator for CatchObjectIter<'_> {
type Item = CatchObject; type Item = CatchObject;
fn next(&mut self) -> Option<Self::Item> { fn next(&mut self) -> Option<Self::Item> {
if let Some(h) = self.last_object.as_mut().and_then(Iterator::next) { if let opt @ Some(_) = self.last_object.as_mut().and_then(Iterator::next) {
return Some(h); return opt;
} }
for h in &mut self.hit_objects { self.hit_objects
if let Some(h) = FruitOrJuice::new(h, &mut self.params) { .find_map(|h| FruitOrJuice::new(h, &mut self.params))
return self.last_object.insert(h).next(); .and_then(|h| self.last_object.insert(h).next())
} }
}
None fn size_hint(&self) -> (usize, Option<usize>) {
let min = self
.last_object
.as_ref()
.map(ExactSizeIterator::len)
.unwrap_or(0);
(min, None)
} }
} }
+23 -69
View File
@@ -1,44 +1,14 @@
use crate::{Beatmap, CatchPP}; use crate::{Beatmap, CatchPP};
use super::{CatchGradualDifficultyAttributes, CatchPerformanceAttributes}; use super::{CatchGradualDifficultyAttributes, CatchPerformanceAttributes, CatchScoreState};
/// Aggregation for a score's current state i.e. what was the
/// maximum combo so far and what are the current hitresults.
///
/// This struct is used for [`CatchGradualPerformanceAttributes`].
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct CatchScoreState {
/// Maximum combo that the score has had so far.
/// **Not** the maximum possible combo of the map so far.
///
/// Note that only fruits and droplets are considered for osu!catch combo.
pub max_combo: usize,
/// Amount of current fruits (300s).
pub n_fruits: usize,
/// Amount of current droplets (100s).
pub n_droplets: usize,
/// Amount of current tiny droplets (50s).
pub n_tiny_droplets: usize,
/// Amount of current tiny droplet misses (katus).
pub n_tiny_droplet_misses: usize,
/// Amount of current misses (fruits and droplets).
pub n_misses: usize,
}
impl CatchScoreState {
/// Create a new empty score state.
pub fn new() -> Self {
Self::default()
}
}
/// Gradually calculate the performance attributes of an osu!catch map. /// Gradually calculate the performance attributes of an osu!catch map.
/// ///
/// After each hit object you can call /// After each hit object you can call
/// [`process_next_object`](`CatchGradualPerformanceAttributes::process_next_object`) /// [`next`](`CatchGradualPerformanceAttributes::next`)
/// and it will return the resulting current [`CatchPerformanceAttributes`]. /// and it will return the resulting current [`CatchPerformanceAttributes`].
/// To process multiple objects at once, use /// To process multiple objects at once, use
/// [`process_next_n_objects`](`CatchGradualPerformanceAttributes::process_next_n_objects`) instead. /// [`nth`](`CatchGradualPerformanceAttributes::nth`) instead.
/// ///
/// Both methods require a [`CatchScoreState`] that contains the current /// Both methods require a [`CatchScoreState`] that contains the current
/// hitresults as well as the maximum combo so far. /// hitresults as well as the maximum combo so far.
@@ -69,10 +39,10 @@ impl CatchScoreState {
/// state.max_combo += 1; /// state.max_combo += 1;
/// ///
/// # /* /// # /*
/// let performance = gradual_perf.process_next_object(state.clone()).unwrap(); /// let performance = gradual_perf.next(state.clone()).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.
@@ -80,10 +50,10 @@ impl CatchScoreState {
/// // the next few objects because the combo is reset. /// // the next few objects because the combo is reset.
/// state.n_misses += 1; /// state.n_misses += 1;
/// # /* /// # /*
/// let performance = gradual_perf.process_next_object(state.clone()).unwrap(); /// let performance = gradual_perf.next(state.clone()).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 fruits and droplets. /// // The next 10 objects will be a mixture of fruits and droplets.
/// // Notice how tiny droplets from sliders do not count as hit objects /// // Notice how tiny droplets from sliders do not count as hit objects
@@ -92,20 +62,21 @@ impl CatchScoreState {
/// state.n_fruits += 4; /// state.n_fruits += 4;
/// state.n_droplets += 6; /// state.n_droplets += 6;
/// state.n_tiny_droplets += 12; /// state.n_tiny_droplets += 12;
/// // The `nth` method takes a zero-based value.
/// # /* /// # /*
/// let performance = gradual_perf.process_next_n_objects(state.clone(), 10).unwrap(); /// let performance = gradual_perf.nth(state.clone(), 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);
/// ///
/// // Now comes another fruit. Note that the max combo gets incremented again. /// // Now comes another fruit. Note that the max combo gets incremented again.
/// state.n_fruits += 1; /// state.n_fruits += 1;
/// state.max_combo += 1; /// state.max_combo += 1;
/// # /* /// # /*
/// let performance = gradual_perf.process_next_object(state.clone()).unwrap(); /// let performance = gradual_perf.next(state.clone()).unwrap();
/// println!("PP: {}", performance.pp); /// println!("PP: {}", performance.pp);
/// # */ /// # */
/// # let _ = gradual_perf.process_next_object(state.clone()); /// # let _ = gradual_perf.next(state.clone());
/// ///
/// // Skip to the end /// // Skip to the end
/// # /* /// # /*
@@ -115,14 +86,14 @@ impl CatchScoreState {
/// state.n_tiny_droplets = ... /// state.n_tiny_droplets = ...
/// state.n_tiny_droplet_misses = ... /// state.n_tiny_droplet_misses = ...
/// 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 CatchGradualPerformanceAttributes<'map> { pub struct CatchGradualPerformanceAttributes<'map> {
@@ -147,34 +118,17 @@ impl<'map> CatchGradualPerformanceAttributes<'map> {
/// ///
/// Note that neither hits nor misses of tiny droplets require /// Note that neither hits nor misses of tiny droplets require
/// to be processed. Only fruits and droplets do. /// to be processed. Only fruits and droplets do.
pub fn process_next_object( pub fn next(&mut self, state: CatchScoreState) -> Option<CatchPerformanceAttributes> {
&mut self, self.nth(state, 0)
state: CatchScoreState,
) -> Option<CatchPerformanceAttributes> {
self.process_next_n_objects(state, 1)
} }
/// Same as [`process_next_object`](`CatchGradualPerformanceAttributes::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: CatchScoreState, n: usize) -> Option<CatchPerformanceAttributes> {
pub fn process_next_n_objects( let difficulty = self.difficulty.by_ref().take(n.saturating_add(1)).last()?;
&mut self,
state: CatchScoreState,
n: usize,
) -> Option<CatchPerformanceAttributes> {
let mut difficulty = None;
for _ in 0..n.max(1) {
match self.difficulty.next() {
Some(attrs) => difficulty = Some(attrs),
None => break,
}
}
let difficulty = difficulty?;
let performance = self let performance = self
.performance .performance
+13 -3
View File
@@ -1,15 +1,25 @@
mod catch_object; mod catch_object;
mod difficulty_object; mod difficulty_object;
mod fruit_or_juice; mod fruit_or_juice;
mod gradual_difficulty;
mod gradual_performance;
mod movement; mod movement;
mod pp; mod pp;
mod score_state;
#[cfg(feature = "gradual")]
mod gradual_difficulty;
#[cfg(feature = "gradual")]
mod gradual_performance;
use difficulty_object::DifficultyObject; use difficulty_object::DifficultyObject;
use movement::Movement; use movement::Movement;
pub use self::{catch_object::CatchObject, gradual_difficulty::*, gradual_performance::*, pp::*}; pub use self::{catch_object::CatchObject, pp::*, score_state::CatchScoreState};
#[cfg(feature = "gradual")]
pub use self::{
gradual_difficulty::CatchGradualDifficultyAttributes,
gradual_performance::CatchGradualPerformanceAttributes,
};
pub(crate) use self::fruit_or_juice::{FruitOrJuice, FruitParams}; pub(crate) use self::fruit_or_juice::{FruitOrJuice, FruitParams};
+29
View File
@@ -0,0 +1,29 @@
/// Aggregation for a score's current state i.e. what was the
/// maximum combo so far and what are the current hitresults.
///
/// This struct is used for [`CatchGradualPerformanceAttributes`].
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct CatchScoreState {
/// Maximum combo that the score has had so far.
/// **Not** the maximum possible combo of the map so far.
///
/// Note that only fruits and droplets are considered for osu!catch combo.
pub max_combo: usize,
/// Amount of current fruits (300s).
pub n_fruits: usize,
/// Amount of current droplets (100s).
pub n_droplets: usize,
/// Amount of current tiny droplets (50s).
pub n_tiny_droplets: usize,
/// Amount of current tiny droplet misses (katus).
pub n_tiny_droplet_misses: usize,
/// Amount of current misses (fruits and droplets).
pub n_misses: usize,
}
impl CatchScoreState {
/// Create a new empty score state.
pub fn new() -> Self {
Self::default()
}
}
+1 -1
View File
@@ -231,7 +231,7 @@ impl<'map> GradualPerformanceAttributes<'map> {
.nth(state.into(), n) .nth(state.into(), n)
.map(PerformanceAttributes::Taiko), .map(PerformanceAttributes::Taiko),
GradualPerformanceAttributes::Catch(f) => f GradualPerformanceAttributes::Catch(f) => f
.process_next_n_objects(state.into(), n) .nth(state.into(), n)
.map(PerformanceAttributes::Catch), .map(PerformanceAttributes::Catch),
GradualPerformanceAttributes::Mania(m) => m GradualPerformanceAttributes::Mania(m) => m
.nth(state.into(), n) .nth(state.into(), n)
+9 -9
View File
@@ -35,10 +35,10 @@ fn correct_empty() {
let mut gradual = CatchGradualPerformanceAttributes::new(&map, 0); let mut gradual = CatchGradualPerformanceAttributes::new(&map, 0);
let state = CatchScoreState::default(); let state = CatchScoreState::default();
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]
@@ -50,14 +50,14 @@ fn next_and_next_n() {
let mut gradual2 = CatchGradualPerformanceAttributes::new(&map, 0); let mut gradual2 = CatchGradualPerformanceAttributes::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());
} }
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());
} }
let state = CatchScoreState { let state = CatchScoreState {
@@ -69,8 +69,8 @@ fn next_and_next_n() {
n_misses: 0, n_misses: 0,
}; };
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);
} }
@@ -91,7 +91,7 @@ fn gradual_end_eq_regular() {
n_misses: 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);
} }
@@ -113,7 +113,7 @@ fn gradual_eq_regular_passed() {
n_misses: 0, n_misses: 0,
}; };
let gradual = gradual.process_next_n_objects(state, n).unwrap(); let gradual = gradual.nth(state, n - 1).unwrap();
assert_eq!(regular, gradual); assert_eq!(regular, gradual);
} }