refactor!: overhauled gradual calc for catch
This commit is contained in:
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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};
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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)
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user