fix: handle taiko passed objects properly & improve testing

This commit is contained in:
MaxOhn
2023-11-13 13:38:14 +01:00
parent 037654c771
commit 1ff200dbda
4 changed files with 142 additions and 106 deletions
+10 -2
View File
@@ -111,8 +111,7 @@ fn closest_rhythm(
.unwrap() .unwrap()
} }
// TODO: Remove Default impl and replace with `with_capacity` method for efficiency #[derive(Clone, Debug)]
#[derive(Clone, Debug, Default)]
pub(crate) struct ObjectLists { pub(crate) struct ObjectLists {
pub(crate) all: Vec<Rc<RefCell<TaikoDifficultyObject>>>, pub(crate) all: Vec<Rc<RefCell<TaikoDifficultyObject>>>,
pub(crate) centres: Vec<usize>, pub(crate) centres: Vec<usize>,
@@ -121,6 +120,15 @@ pub(crate) struct ObjectLists {
} }
impl ObjectLists { impl ObjectLists {
pub(crate) fn with_capacity(capacity: usize) -> Self {
Self {
all: Vec::with_capacity(capacity),
centres: Vec::new(),
rims: Vec::new(),
notes: Vec::new(),
}
}
pub(crate) fn prev_mono( pub(crate) fn prev_mono(
&self, &self,
curr: usize, curr: usize,
+72 -19
View File
@@ -51,9 +51,18 @@ pub struct TaikoGradualDifficulty {
lists: ObjectLists, lists: ObjectLists,
peaks: Peaks, peaks: Peaks,
total_hits: usize, total_hits: usize,
first_combos: FirstTwoCombos,
is_convert: bool, is_convert: bool,
} }
#[derive(Copy, Clone, Debug)]
enum FirstTwoCombos {
None,
OnlyFirst,
OnlySecond,
Both,
}
impl TaikoGradualDifficulty { impl TaikoGradualDifficulty {
/// Create a new difficulty attributes iterator for osu!taiko maps. /// Create a new difficulty attributes iterator for osu!taiko maps.
pub fn new(map: &Beatmap, mods: u32) -> Self { pub fn new(map: &Beatmap, mods: u32) -> Self {
@@ -68,41 +77,44 @@ impl TaikoGradualDifficulty {
.clock_rate(clock_rate) .clock_rate(clock_rate)
.hit_windows(); .hit_windows();
let mut attrs = TaikoDifficultyAttributes { let attrs = TaikoDifficultyAttributes {
stamina: 0.0,
rhythm: 0.0,
colour: 0.0,
peak: 0.0,
hit_window, hit_window,
stars: 0.0, ..Default::default()
max_combo: 0,
}; };
if map.hit_objects.len() < 2 { if map.hit_objects.len() < 2 {
return Self { return Self {
idx: 0, idx: 0,
diff_objects: Vec::new().into_iter(), diff_objects: Vec::new().into_iter(),
lists: ObjectLists::default(), lists: ObjectLists::with_capacity(0),
peaks, peaks,
attrs, attrs,
total_hits: 0, total_hits: 0,
first_combos: FirstTwoCombos::None,
is_convert, is_convert,
}; };
} }
attrs.max_combo += map.hit_objects[0].is_circle() as usize; let first_combos = match (
attrs.max_combo += map.hit_objects[1].is_circle() as usize; map.hit_objects[0].is_circle(),
let mut total_hits = attrs.max_combo; map.hit_objects[1].is_circle(),
let mut diff_objects = ObjectLists::default(); ) {
(false, false) => FirstTwoCombos::None,
(true, false) => FirstTwoCombos::OnlyFirst,
(false, true) => FirstTwoCombos::OnlySecond,
(true, true) => FirstTwoCombos::Both,
};
let mut total_hits = 0;
let mut diff_objects = ObjectLists::with_capacity(map.hit_objects.len().saturating_sub(2));
map.taiko_objects() map.taiko_objects()
.inspect(|(h, _)| total_hits += h.is_hit as usize)
.skip(2) .skip(2)
.zip(map.hit_objects.iter().skip(1)) .zip(map.hit_objects.iter().skip(1))
.zip(map.hit_objects.iter()) .zip(map.hit_objects.iter())
.enumerate() .enumerate()
.for_each(|(idx, (((base, base_start_time), last), last_last))| { .for_each(|(idx, (((base, base_start_time), last), last_last))| {
total_hits += base.is_hit as usize;
let diff_obj = TaikoDifficultyObject::new( let diff_obj = TaikoDifficultyObject::new(
base, base,
base_start_time, base_start_time,
@@ -135,6 +147,7 @@ impl TaikoGradualDifficulty {
peaks, peaks,
attrs, attrs,
total_hits, total_hits,
first_combos,
is_convert, is_convert,
} }
} }
@@ -162,6 +175,14 @@ impl Iterator for TaikoGradualDifficulty {
} }
} else if self.lists.all.is_empty() { } else if self.lists.all.is_empty() {
return None; return None;
} else {
match self.first_combos {
FirstTwoCombos::OnlyFirst => self.attrs.max_combo = 1,
FirstTwoCombos::OnlySecond if self.idx == 1 => self.attrs.max_combo = 1,
FirstTwoCombos::Both if self.idx == 0 => self.attrs.max_combo = 1,
FirstTwoCombos::Both if self.idx == 1 => self.attrs.max_combo = 2,
_ => {}
}
} }
self.idx += 1; self.idx += 1;
@@ -211,11 +232,43 @@ impl Iterator for TaikoGradualDifficulty {
fn nth(&mut self, n: usize) -> Option<Self::Item> { fn nth(&mut self, n: usize) -> Option<Self::Item> {
let mut take = n.min(self.len().saturating_sub(1)); let mut take = n.min(self.len().saturating_sub(1));
// The first two notes have no difficulty object // The first two notes have no difficulty object but might add to combo
if self.idx < 2 && take > 0 { match (take, self.idx) {
let skipped = take.min(2); (_, 2..) | (0, _) => {}
take -= skipped; (1, 0) => {
self.idx += skipped; take -= 1;
self.idx += 1;
match self.first_combos {
FirstTwoCombos::None => {}
FirstTwoCombos::OnlyFirst => self.attrs.max_combo = 1,
FirstTwoCombos::OnlySecond => {}
FirstTwoCombos::Both => self.attrs.max_combo = 1,
}
}
(_, 0) => {
take -= 2;
self.idx += 2;
match self.first_combos {
FirstTwoCombos::None => {}
FirstTwoCombos::OnlyFirst => self.attrs.max_combo = 1,
FirstTwoCombos::OnlySecond => self.attrs.max_combo = 1,
FirstTwoCombos::Both => self.attrs.max_combo = 2,
}
}
(_, 1) => {
take -= 1;
self.idx += 1;
match self.first_combos {
FirstTwoCombos::None => {}
FirstTwoCombos::OnlyFirst => self.attrs.max_combo = 1,
FirstTwoCombos::OnlySecond => self.attrs.max_combo = 1,
FirstTwoCombos::Both => self.attrs.max_combo = 2,
}
}
_ => unreachable!(),
} }
for _ in 0..take { for _ in 0..take {
+12 -14
View File
@@ -227,26 +227,19 @@ fn calculate_skills(params: TaikoStars<'_>) -> (Peaks, usize) {
is_convert: _, is_convert: _,
} = params; } = params;
let mut take = passed_objects.unwrap_or(map.hit_objects.len()); let take = passed_objects.unwrap_or(map.hit_objects.len());
let clock_rate = clock_rate.unwrap_or_else(|| mods.clock_rate()); let clock_rate = clock_rate.unwrap_or_else(|| mods.clock_rate());
let mut peaks = Peaks::new(); let mut peaks = Peaks::new();
let mut max_combo = 0; let mut max_combo = 0;
let mut n_diff_objects = 0;
let mut diff_objects = ObjectLists::default(); let mut diff_objects = ObjectLists::with_capacity(map.hit_objects.len().saturating_sub(2));
map.taiko_objects() map.taiko_objects()
.take_while(|(h, _)| { .inspect(|(h, _)| {
if h.is_hit { n_diff_objects += (max_combo < take) as usize;
if take == 0 { max_combo += (max_combo < take && h.is_hit) as usize;
return false;
}
max_combo += 1;
take -= 1;
}
true
}) })
.skip(2) .skip(2)
.zip(map.hit_objects.iter().skip(1)) .zip(map.hit_objects.iter().skip(1))
@@ -276,9 +269,14 @@ fn calculate_skills(params: TaikoStars<'_>) -> (Peaks, usize) {
diff_objects.all.push(Rc::new(RefCell::new(diff_obj))); diff_objects.all.push(Rc::new(RefCell::new(diff_obj)));
}); });
map.hit_objects
.iter()
.take(2)
.for_each(|h| n_diff_objects = n_diff_objects.saturating_sub(h.is_circle() as usize));
ColourDifficultyPreprocessor::process_and_assign(&mut diff_objects); ColourDifficultyPreprocessor::process_and_assign(&mut diff_objects);
for hit_object in diff_objects.all.iter() { for hit_object in diff_objects.all.iter().take(n_diff_objects) {
peaks.process(&hit_object.borrow(), &diff_objects); peaks.process(&hit_object.borrow(), &diff_objects);
} }
+48 -71
View File
@@ -4,8 +4,11 @@
))] ))]
use rosu_pp::{ use rosu_pp::{
taiko::{TaikoGradualDifficulty, TaikoGradualPerformance, TaikoScoreState}, taiko::{
Beatmap, TaikoPP, TaikoStars, TaikoGradualDifficulty, TaikoGradualPerformance, TaikoOwnedGradualPerformance,
TaikoScoreState,
},
Beatmap, TaikoPP,
}; };
mod common; mod common;
@@ -18,62 +21,6 @@ fn empty_map() {
assert!(attrs.next().is_none()); assert!(attrs.next().is_none());
} }
#[test]
fn iter_end_eq_regular() {
let map = test_map!(Taiko);
let regular = TaikoStars::new(&map).calculate();
let iter_end = TaikoGradualDifficulty::new(&map, 0)
.last()
.expect("empty iter");
assert_eq!(regular, iter_end);
}
#[test]
fn correct_empty() {
let map = test_map!(Taiko);
let mut gradual = TaikoGradualPerformance::new(&map, 0);
let state = TaikoScoreState::default();
let first_attrs = gradual.nth(state.clone(), usize::MAX);
assert!(first_attrs.is_some());
assert!(gradual.next(state).is_none());
}
#[test]
fn next_and_next_n() {
let map = test_map!(Taiko);
let state = TaikoScoreState::default();
let mut gradual1 = TaikoGradualPerformance::new(&map, 0);
let mut gradual2 = TaikoGradualPerformance::new(&map, 0);
for _ in 0..50 {
let _ = gradual1.next(state.clone());
let _ = gradual2.next(state.clone());
}
let n = 200;
for _ in 1..n {
let _ = gradual1.next(state.clone());
}
let state = TaikoScoreState {
max_combo: 246,
n300: 200,
n100: 40,
n_misses: 6,
};
let next = gradual1.next(state.clone());
let next_n = gradual2.nth(state, n - 1);
assert_eq!(next_n, next);
}
#[test] #[test]
fn gradual_end_eq_regular() { fn gradual_end_eq_regular() {
let map = test_map!(Taiko); let map = test_map!(Taiko);
@@ -87,27 +34,57 @@ fn gradual_end_eq_regular() {
n_misses: 0, n_misses: 0,
}; };
let gradual_end = gradual.nth(state, usize::MAX).unwrap(); let gradual_end = gradual.last(state).unwrap();
assert_eq!(regular, gradual_end); assert_eq!(regular, gradual_end);
} }
#[test] #[test]
fn gradual_eq_regular_passed() { fn gradual_complete_next() {
let map = test_map!(Taiko); let map = test_map!(Taiko);
let n = 250; let mods = 88; // HDHRDT
let regular = TaikoPP::new(&map).passed_objects(n).calculate(); let mut gradual = TaikoGradualPerformance::new(map, mods);
let mut gradual = TaikoGradualPerformance::new(&map, 0); let mut gradual_2nd = TaikoGradualPerformance::new(map, mods);
let mut gradual_3rd = TaikoGradualPerformance::new(map, mods);
let mut gradual_owned = TaikoOwnedGradualPerformance::new(map.to_owned(), mods);
let state = TaikoScoreState { let mut state = TaikoScoreState::default();
max_combo: 250,
n300: 250,
n100: 0,
n_misses: 0,
};
let gradual = gradual.nth(state, n - 1).unwrap(); for i in 1.. {
state.n_misses += 1;
assert_eq!(regular, gradual); let Some(next_gradual) = gradual.next(state.clone()) else {
assert_eq!(i, map.n_circles as usize + 1);
assert!(gradual_2nd.last(state.clone()).is_some() || map.hit_objects.len() % 2 == 0);
assert!(gradual_3rd.last(state.clone()).is_some() || map.hit_objects.len() % 3 == 0);
assert!(gradual_owned.next(state.clone()).is_none());
break;
};
if i % 2 == 0 {
let next_gradual_2nd = gradual_2nd.nth(state.clone(), 1).unwrap();
assert_eq!(next_gradual, next_gradual_2nd, "i={i}");
}
if i % 3 == 0 {
let next_gradual_3rd = gradual_3rd.nth(state.clone(), 2).unwrap();
assert_eq!(next_gradual, next_gradual_3rd, "i={i}");
}
let next_gradual_owned = gradual_owned.next(state.clone()).unwrap();
let mut regular_calc = TaikoPP::new(&map)
.mods(mods)
.passed_objects(i)
.state(state.clone());
let regular_state = regular_calc.generate_state();
assert_eq!(state, regular_state);
let regular = regular_calc.calculate();
assert_eq!(next_gradual, next_gradual_owned, "i={i}");
assert_eq!(next_gradual, regular, "i={i}");
}
} }