refactor!: overhauled gradual calc for taiko

This commit is contained in:
MaxOhn
2023-11-07 22:20:15 +01:00
parent 79e19d5a9b
commit 74083e5ecb
11 changed files with 188 additions and 182 deletions
+1 -1
View File
@@ -228,7 +228,7 @@ impl<'map> GradualPerformanceAttributes<'map> {
o.nth(state.into(), n).map(PerformanceAttributes::Osu)
}
GradualPerformanceAttributes::Taiko(t) => t
.process_next_n_objects(state.into(), n)
.nth(state.into(), n)
.map(PerformanceAttributes::Taiko),
GradualPerformanceAttributes::Catch(f) => f
.process_next_n_objects(state.into(), n)
+1 -1
View File
@@ -151,7 +151,7 @@
//! };
//!
//! // Process the next 10 objects in one go
//! let curr_performance = match gradual_performance.process_next_n_objects(state, 10) {
//! let curr_performance = match gradual_performance.nth(state, 10) {
//! Some(perf) => perf,
//! None => panic!("the last `process_next_object` already processed the last object"),
//! };
View File
+1 -1
View File
@@ -154,7 +154,7 @@ impl ColourDifficultyPreprocessor {
mut data: VecDeque<Rc<RefCell<AlternatingMonoPattern>>>,
) -> Vec<Rc<RefCell<RepeatingHitPatterns>>> {
let mut hit_patterns = Vec::new();
let mut curr_hit_pattern: Option<Rc<std::cell::RefCell<_>>> = None;
let mut curr_hit_pattern: Option<Rc<RefCell<_>>> = None;
while !data.is_empty() {
let old = curr_hit_pattern.as_ref().map(Rc::downgrade);
+7 -7
View File
@@ -62,21 +62,21 @@ impl RepeatingHitPatterns {
}
pub(crate) fn find_repetition_interval(&mut self) {
let mut other = match self.prev.as_ref().and_then(Weak::upgrade) {
Some(prev) => prev,
None => return self.repetition_interval = Self::MAX_REPETITION_INTERVAL + 1,
let Some(mut other) = self.prev.as_ref().and_then(Weak::upgrade) else {
return self.repetition_interval = Self::MAX_REPETITION_INTERVAL + 1;
};
let mut interval = 1;
while interval < Self::MAX_REPETITION_INTERVAL {
if self.is_repetition_of(&other.borrow()) {
return self.repetition_interval = interval.min(Self::MAX_REPETITION_INTERVAL);
self.repetition_interval = interval.min(Self::MAX_REPETITION_INTERVAL);
return;
}
let next = match other.borrow().prev.as_ref().and_then(Weak::upgrade) {
Some(prev) => prev,
None => break,
let Some(next) = other.borrow().prev.as_ref().and_then(Weak::upgrade) else {
break;
};
// gotta love NLL...
+1
View File
@@ -111,6 +111,7 @@ fn closest_rhythm(
.unwrap()
}
// TODO: Remove Default impl and replace with `with_capacity` method for efficiency
#[derive(Clone, Debug, Default)]
pub(crate) struct ObjectLists {
pub(crate) all: Vec<Rc<RefCell<TaikoDifficultyObject>>>,
+61 -50
View File
@@ -1,3 +1,5 @@
#![cfg(feature = "gradual")]
use std::{borrow::Cow, cell::RefCell, rc::Rc, vec::IntoIter};
use crate::{beatmap::BeatmapHitWindows, taiko::rescale, Beatmap, GameMode, Mods};
@@ -40,15 +42,15 @@ use super::{
/// // ...
/// }
/// ```
#[derive(Clone, Debug)]
#[derive(Debug)]
pub struct TaikoGradualDifficultyAttributes {
pub(crate) idx: usize,
attrs: TaikoDifficultyAttributes,
hit_objects: IntoIter<Rc<RefCell<TaikoDifficultyObject>>>,
diff_objects: IntoIter<Rc<RefCell<TaikoDifficultyObject>>>,
lists: ObjectLists,
peaks: Peaks,
total_hits: usize,
is_convert: bool,
pub(crate) started: bool,
}
impl TaikoGradualDifficultyAttributes {
@@ -77,67 +79,62 @@ impl TaikoGradualDifficultyAttributes {
if map.hit_objects.len() < 2 {
return Self {
hit_objects: Vec::new().into_iter(),
idx: 0,
diff_objects: Vec::new().into_iter(),
lists: ObjectLists::default(),
peaks,
attrs,
total_hits: 0,
is_convert,
started: false,
};
}
attrs.max_combo += map.hit_objects[0].is_circle() as usize;
attrs.max_combo += map.hit_objects[1].is_circle() as usize;
let mut total_hits = attrs.max_combo;
let mut diff_objects = ObjectLists::default();
let mut diff_objects = map
.taiko_objects()
map.taiko_objects()
.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))| {
total_hits += base.is_hit as usize;
.for_each(|(idx, (((base, base_start_time), last), last_last))| {
total_hits += base.is_hit as usize;
let diff_obj = TaikoDifficultyObject::new(
base,
base_start_time,
last.start_time,
last_last.start_time,
clock_rate,
&lists,
idx,
);
let diff_obj = TaikoDifficultyObject::new(
base,
base_start_time,
last.start_time,
last_last.start_time,
clock_rate,
&diff_objects,
idx,
);
match &diff_obj.mono_idx {
MonoIndex::Centre(_) => lists.centres.push(idx),
MonoIndex::Rim(_) => lists.rims.push(idx),
MonoIndex::None => {}
}
match &diff_obj.mono_idx {
MonoIndex::Centre(_) => diff_objects.centres.push(idx),
MonoIndex::Rim(_) => diff_objects.rims.push(idx),
MonoIndex::None => {}
}
if diff_obj.note_idx.is_some() {
lists.notes.push(idx);
}
if diff_obj.note_idx.is_some() {
diff_objects.notes.push(idx);
}
lists.all.push(Rc::new(RefCell::new(diff_obj)));
lists
},
);
diff_objects.all.push(Rc::new(RefCell::new(diff_obj)));
});
ColourDifficultyPreprocessor::process_and_assign(&mut diff_objects);
Self {
hit_objects: diff_objects.all.clone().into_iter(),
idx: 0,
diff_objects: diff_objects.all.clone().into_iter(),
lists: diff_objects,
peaks,
attrs,
total_hits,
is_convert,
started: false,
}
}
}
@@ -146,20 +143,28 @@ impl Iterator for TaikoGradualDifficultyAttributes {
type Item = TaikoDifficultyAttributes;
fn next(&mut self) -> Option<Self::Item> {
self.started = true;
// The first difficulty object belongs to the third note since each difficulty
// object requires the current the last, and the second to last note. Hence, if we're still
// on the first or second object, we don't have a difficulty object yet and just skip
// processing.
if self.idx >= 2 {
loop {
let curr = self.diff_objects.next()?;
let borrowed = curr.borrow();
self.peaks.process(&borrowed, &self.lists);
loop {
let curr = self.hit_objects.next()?;
let borrowed = curr.borrow();
self.peaks.process(&borrowed, &self.lists);
if borrowed.base.is_hit {
self.attrs.max_combo += 1;
if borrowed.base.is_hit {
self.attrs.max_combo += 1;
break;
break;
}
}
} else if self.lists.all.is_empty() {
return None;
}
self.idx += 1;
let PeaksDifficultyValues {
mut colour_rating,
mut rhythm_rating,
@@ -203,18 +208,24 @@ impl Iterator for TaikoGradualDifficultyAttributes {
}
fn nth(&mut self, n: usize) -> Option<Self::Item> {
let skip = n
.min(self.total_hits - self.attrs.max_combo)
.saturating_sub(1);
let mut take = n.min(self.len().saturating_sub(1));
for _ in 0..skip {
// The first two notes have no difficulty object
if self.idx < 2 && take > 0 {
let skipped = take.min(2);
take -= skipped;
self.idx += skipped;
}
for _ in 0..take {
loop {
let curr = self.hit_objects.next()?;
let curr = self.diff_objects.next()?;
let borrowed = curr.borrow();
self.peaks.process(&borrowed, &self.lists);
if borrowed.base.is_hit {
self.attrs.max_combo += 1;
self.idx += 1;
break;
}
@@ -228,6 +239,6 @@ impl Iterator for TaikoGradualDifficultyAttributes {
impl ExactSizeIterator for TaikoGradualDifficultyAttributes {
#[inline]
fn len(&self) -> usize {
self.hit_objects.len()
self.total_hits - self.idx
}
}
+28 -82
View File
@@ -1,66 +1,21 @@
use crate::{Beatmap, TaikoPP};
#![cfg(feature = "gradual")]
use crate::{taiko::TaikoScoreState, Beatmap, TaikoPP};
use super::{TaikoGradualDifficultyAttributes, TaikoPerformanceAttributes};
/// 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 [`TaikoGradualPerformanceAttributes`].
#[derive(Clone, Debug, Default, Eq, PartialEq)]
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 n_misses: usize,
}
impl TaikoScoreState {
/// 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.n300 + self.n100 + 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 = 2 * self.n300 + self.n100;
let denominator = 2 * total_hits;
numerator as f64 / denominator as f64
}
}
/// Gradually calculate the performance attributes of an osu!taiko map.
///
/// After each hit object you can call
/// [`process_next_object`](`TaikoGradualPerformanceAttributes::process_next_object`)
/// After each hit object you can call [`next`](`TaikoGradualPerformanceAttributes::next`)
/// 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.
/// [`nth`](`TaikoGradualPerformanceAttributes::nth`) instead.
///
/// Both methods require a [`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.
/// [`TaikoGradualDifficultyAttributes`] instead.
///
/// # Example
///
@@ -82,10 +37,10 @@ impl TaikoScoreState {
/// 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);
/// # */
/// # let _ = gradual_perf.process_next_object(state.clone());
/// # let _ = gradual_perf.next(state.clone());
/// }
///
/// // Then comes a miss.
@@ -93,29 +48,30 @@ impl TaikoScoreState {
/// // the next few objects because the combo is reset.
/// 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);
/// # */
/// # let _ = gradual_perf.process_next_object(state.clone());
/// # let _ = gradual_perf.next(state.clone());
///
/// // 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;
/// // 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);
/// # */
/// # let _ = gradual_perf.process_next_n_objects(state.clone(), 10);
/// # let _ = gradual_perf.nth(state.clone(), 9);
///
/// // 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.clone()).unwrap();
/// let performance = gradual_perf.next(state.clone()).unwrap();
/// println!("PP: {}", performance.pp);
/// # */
/// # let _ = gradual_perf.process_next_object(state.clone());
/// # let _ = gradual_perf.next(state.clone());
///
/// // Skip to the end
/// # /*
@@ -123,16 +79,16 @@ impl TaikoScoreState {
/// state.n300 = ...
/// state.n100 = ...
/// 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);
/// # */
/// # 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,
/// // 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(Debug)]
pub struct TaikoGradualPerformanceAttributes<'map> {
difficulty: TaikoGradualDifficultyAttributes,
performance: TaikoPP<'map>,
@@ -152,34 +108,24 @@ impl<'map> TaikoGradualPerformanceAttributes<'map> {
/// 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)
pub fn next(&mut self, state: TaikoScoreState) -> Option<TaikoPerformanceAttributes> {
self.nth(state, 0)
}
/// Same as [`process_next_object`](`TaikoGradualPerformanceAttributes::process_next_object`)
/// but instead of processing only one object it process `n` many.
/// Process everything up the the next `n`th hit object and calculate the performance
/// attributes for the resulting score state.
///
/// 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 sub = 2 * !self.difficulty.started as usize;
let difficulty = self.difficulty.nth(n.saturating_sub(sub))?;
let passed_objects = difficulty.max_combo;
/// Note that the count is zero-indexed, so `n=0` will process 1 object, `n=1` will process 2,
/// and so on.
pub fn nth(&mut self, state: TaikoScoreState, n: usize) -> Option<TaikoPerformanceAttributes> {
let difficulty = self.difficulty.nth(n)?;
let performance = self
.performance
.clone()
.attributes(difficulty)
.state(state)
.passed_objects(passed_objects)
.passed_objects(self.difficulty.idx)
.calculate();
Some(performance)
+34 -31
View File
@@ -1,17 +1,24 @@
mod colours;
mod difficulty_object;
mod gradual_difficulty;
mod gradual_performance;
mod pp;
mod rim;
mod score_state;
mod skills;
mod taiko_object;
#[cfg(feature = "gradual")]
mod gradual_difficulty;
#[cfg(feature = "gradual")]
mod gradual_performance;
use std::{borrow::Cow, cell::RefCell, rc::Rc};
pub use self::{pp::*, score_state::TaikoScoreState, taiko_object::TaikoObjectPub as TaikoObject};
#[cfg(feature = "gradual")]
pub use self::{
gradual_difficulty::*, gradual_performance::*, pp::*,
taiko_object::TaikoObjectPub as TaikoObject,
gradual_difficulty::TaikoGradualDifficultyAttributes,
gradual_performance::TaikoGradualPerformanceAttributes,
};
pub(crate) use self::taiko_object::IntoTaikoObjectIter;
@@ -226,8 +233,9 @@ fn calculate_skills(params: TaikoStars<'_>) -> (Peaks, usize) {
let mut peaks = Peaks::new();
let mut max_combo = 0;
let mut diff_objects = map
.taiko_objects()
let mut diff_objects = ObjectLists::default();
map.taiko_objects()
.take_while(|(h, _)| {
if h.is_hit {
if take == 0 {
@@ -244,34 +252,29 @@ fn calculate_skills(params: TaikoStars<'_>) -> (Peaks, usize) {
.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))| {
let diff_obj = TaikoDifficultyObject::new(
base,
base_start_time,
last.start_time,
last_last.start_time,
clock_rate,
&lists,
idx,
);
.for_each(|(idx, (((base, base_start_time), last), last_last))| {
let diff_obj = TaikoDifficultyObject::new(
base,
base_start_time,
last.start_time,
last_last.start_time,
clock_rate,
&diff_objects,
idx,
);
match &diff_obj.mono_idx {
MonoIndex::Centre(_) => lists.centres.push(idx),
MonoIndex::Rim(_) => lists.rims.push(idx),
MonoIndex::None => {}
}
match &diff_obj.mono_idx {
MonoIndex::Centre(_) => diff_objects.centres.push(idx),
MonoIndex::Rim(_) => diff_objects.rims.push(idx),
MonoIndex::None => {}
}
if diff_obj.note_idx.is_some() {
lists.notes.push(idx);
}
if diff_obj.note_idx.is_some() {
diff_objects.notes.push(idx);
}
lists.all.push(Rc::new(RefCell::new(diff_obj)));
lists
},
);
diff_objects.all.push(Rc::new(RefCell::new(diff_obj)));
});
ColourDifficultyPreprocessor::process_and_assign(&mut diff_objects);
+45
View File
@@ -0,0 +1,45 @@
/// 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 [`TaikoGradualPerformanceAttributes`](crate::taiko::TaikoGradualPerformanceAttributes).
#[derive(Clone, Debug, Default, Eq, PartialEq)]
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 n_misses: usize,
}
impl TaikoScoreState {
/// 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.n300 + self.n100 + 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 = 2 * self.n300 + self.n100;
let denominator = 2 * total_hits;
numerator as f64 / denominator as f64
}
}
+9 -9
View File
@@ -35,10 +35,10 @@ fn correct_empty() {
let mut gradual = TaikoGradualPerformanceAttributes::new(&map, 0);
let state = TaikoScoreState::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!(gradual.process_next_object(state).is_none());
assert!(gradual.next(state).is_none());
}
#[test]
@@ -50,14 +50,14 @@ fn next_and_next_n() {
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 _ = gradual1.next(state.clone());
let _ = gradual2.next(state.clone());
}
let n = 200;
for _ in 1..n {
let _ = gradual1.process_next_object(state.clone());
let _ = gradual1.next(state.clone());
}
let state = TaikoScoreState {
@@ -67,8 +67,8 @@ fn next_and_next_n() {
n_misses: 6,
};
let next = gradual1.process_next_object(state.clone());
let next_n = gradual2.process_next_n_objects(state, n);
let next = gradual1.next(state.clone());
let next_n = gradual2.nth(state, n - 1);
assert_eq!(next_n, next);
}
@@ -86,7 +86,7 @@ fn gradual_end_eq_regular() {
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);
}
@@ -106,7 +106,7 @@ fn gradual_eq_regular_passed() {
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);
}