gradual calc for catch

This commit is contained in:
MaxOhn
2024-02-24 17:38:33 +01:00
parent 2dafb18a88
commit 77642e83c7
10 changed files with 597 additions and 76 deletions
+94 -23
View File
@@ -1,3 +1,5 @@
use std::mem;
use crate::catch::performance::CatchPerformance;
/// The result of a difficulty calculation on an osu!catch map.
@@ -36,6 +38,22 @@ impl CatchDifficultyAttributes {
pub fn performance<'a>(self) -> CatchPerformance<'a> {
self.into()
}
pub(crate) fn set_object_count(&mut self, count: &ObjectCount) {
self.n_fruits = count.fruits;
self.n_droplets = count.droplets;
self.n_tiny_droplets = count.tiny_droplets;
}
pub(crate) fn add_object_count(&mut self, count: GradualObjectCount) {
if count.fruit {
self.n_fruits += 1;
} else {
self.n_droplets += 1;
}
self.n_tiny_droplets += count.tiny_droplets;
}
}
/// The result of a performance calculation on an osu!catch map.
@@ -71,43 +89,96 @@ impl CatchPerformanceAttributes {
}
}
pub struct CatchDifficultyAttributesBuilder {
inner: CatchDifficultyAttributes,
take: usize,
#[derive(Clone, Default)]
pub struct ObjectCount {
fruits: u32,
droplets: u32,
tiny_droplets: u32,
}
impl CatchDifficultyAttributesBuilder {
pub const fn new(attrs: CatchDifficultyAttributes, take: usize) -> Self {
Self { inner: attrs, take }
#[derive(Copy, Clone, Default)]
pub struct GradualObjectCount {
fruit: bool,
tiny_droplets: u32,
}
pub enum ObjectCountBuilder {
Regular {
count: ObjectCount,
take: usize,
},
Gradual {
count: GradualObjectCount,
all: Vec<GradualObjectCount>,
},
}
impl ObjectCountBuilder {
pub fn new_regular(take: usize) -> Self {
Self::Regular {
count: ObjectCount::default(),
take,
}
}
pub const fn into_inner(self) -> CatchDifficultyAttributes {
self.inner
pub fn new_gradual() -> Self {
Self::Gradual {
count: GradualObjectCount::default(),
all: Vec::with_capacity(512),
}
}
pub const fn take_more(&self) -> bool {
self.take > 0
pub fn into_regular(self) -> ObjectCount {
if let Self::Regular { count, .. } = self {
count
} else {
unreachable!()
}
}
pub fn inc_fruits(&mut self) {
Self::inc_value(&mut self.inner.n_fruits, &mut self.take);
pub fn into_gradual(self) -> Vec<GradualObjectCount> {
if let Self::Gradual { all, .. } = self {
all
} else {
unreachable!()
}
}
pub fn inc_droplets(&mut self) {
Self::inc_value(&mut self.inner.n_droplets, &mut self.take);
pub fn record_fruit(&mut self) {
match self {
Self::Regular { count, take } => {
if *take > 0 {
*take -= 1;
count.fruits += 1;
}
}
Self::Gradual { count, all } => {
count.fruit = true;
all.push(mem::take(count));
}
}
}
/// Should only be used if [`take_more`] returns `true`.
///
/// [`take_more`]: Self::take_more
pub fn inc_tiny_droplets(&mut self) {
self.inner.n_tiny_droplets += 1;
pub fn record_droplet(&mut self) {
match self {
Self::Regular { count, take } => {
if *take > 0 {
*take -= 1;
count.droplets += 1;
}
}
Self::Gradual { count, all } => all.push(mem::take(count)),
}
}
fn inc_value(value: &mut u32, take: &mut usize) {
if *take > 0 {
*value += 1;
*take -= 1;
pub fn record_tiny_droplets(&mut self, n: u32) {
match self {
Self::Regular { count, take } => {
if *take > 0 {
count.tiny_droplets += n;
}
}
Self::Gradual { count, .. } => count.tiny_droplets += n,
}
}
}
+6 -6
View File
@@ -12,7 +12,7 @@ use crate::{
};
use super::{
attributes::CatchDifficultyAttributesBuilder,
attributes::ObjectCountBuilder,
catcher::Catcher,
object::{
banana_shower::BananaShower,
@@ -42,7 +42,7 @@ pub fn try_convert(map: &mut Cow<'_, Beatmap>) -> ConvertStatus {
pub fn convert_objects(
converted: &CatchBeatmap<'_>,
attrs: &mut CatchDifficultyAttributesBuilder,
count: &mut ObjectCountBuilder,
hr: bool,
cs: f32,
) -> Vec<PalpableObject> {
@@ -62,7 +62,7 @@ pub fn convert_objects(
let mut last_start_time = 0.0;
for h in converted.map.hit_objects.iter() {
let mut new_objects = convert_object(h, converted, attrs, &mut bufs);
let mut new_objects = convert_object(h, converted, count, &mut bufs);
apply_pos_offset(
&mut new_objects,
@@ -96,14 +96,14 @@ pub fn convert_objects(
fn convert_object<'a>(
h: &'a HitObject,
converted: &CatchBeatmap<'_>,
attrs: &mut CatchDifficultyAttributesBuilder,
count: &mut ObjectCountBuilder,
bufs: &'a mut JuiceStreamBufs,
) -> ObjectIter<'a> {
let state = match h.kind {
HitObjectKind::Circle => ObjectIterState::Fruit(Some(Fruit::new(attrs))),
HitObjectKind::Circle => ObjectIterState::Fruit(Some(Fruit::new(count))),
HitObjectKind::Slider(ref slider) => {
let x = JuiceStream::clamp_to_playfield(h.pos.x);
let stream = JuiceStream::new(x, h.start_time, slider, converted, attrs, bufs);
let stream = JuiceStream::new(x, h.start_time, slider, converted, count, bufs);
ObjectIterState::JuiceStream(stream)
}
+215
View File
@@ -0,0 +1,215 @@
use crate::{
any::difficulty::skills::Skill,
catch::{
attributes::{GradualObjectCount, ObjectCountBuilder},
convert::convert_objects,
CatchBeatmap, CatchDifficultyAttributes,
},
util::mods::Mods,
ModeDifficulty,
};
use super::{
object::CatchDifficultyObject, skills::movement::Movement, CatchDifficultySetup,
DifficultyValues,
};
/// Gradually calculate the difficulty attributes of an osu!catch map.
///
/// Note that this struct implements [`Iterator`].
/// On every call of [`Iterator::next`], the map's next fruit or droplet
/// will be processed and the [`CatchDifficultyAttributes`] will be updated and
/// returned.
///
/// Note that it does not return attributes after a tiny droplet. Only for
/// fruits and droplets.
///
/// If you want to calculate performance attributes, use
/// [`CatchGradualPerformance`] instead.
///
/// # Example
///
/// ```
/// use rosu_pp::{Beatmap, ModeDifficulty};
/// use rosu_pp::catch::{Catch, CatchGradualDifficulty};
///
/// let map = Beatmap::from_path();
/// .unwrap()
/// .unchecked_into_converted::<Catch>();
///
/// let difficulty = ModeDifficulty::new().mods(64); // DT
/// let mut iter = CatchGradualDifficulty::new(&difficulty, &converted);
///
/// // the difficulty of the map after the first hit object
/// let attrs1 = iter.next();
/// // ... after the second hit object
/// let attrs2 = iter.next();
///
/// // Remaining hit objects
/// for difficulty in iter {
/// // ...
/// }
/// ```
///
/// [`CatchGradualPerformance`]: crate::catch::CatchGradualPerformance
pub struct CatchGradualDifficulty {
pub(crate) idx: usize,
pub(crate) mods: u32,
pub(crate) clock_rate: f64,
attrs: CatchDifficultyAttributes,
/// The delta of object counts after each palpable object
count: Vec<GradualObjectCount>,
diff_objects: Box<[CatchDifficultyObject]>,
movement: Movement,
}
impl CatchGradualDifficulty {
pub fn new(difficulty: &ModeDifficulty, converted: &CatchBeatmap<'_>) -> Self {
let mods = difficulty.get_mods();
let clock_rate = difficulty.get_clock_rate();
let CatchDifficultySetup { map_attrs, attrs } =
CatchDifficultySetup::new(difficulty, converted);
let hr = mods.hr();
let mut count = ObjectCountBuilder::new_gradual();
let palpable_objects = convert_objects(converted, &mut count, hr, map_attrs.cs as f32);
let diff_objects = DifficultyValues::create_difficulty_objects(
&map_attrs,
clock_rate,
palpable_objects.iter(),
);
let count = count.into_gradual();
let movement = Movement::new(clock_rate);
Self {
idx: 0,
mods,
clock_rate,
attrs,
count,
diff_objects,
movement,
}
}
}
impl Iterator for CatchGradualDifficulty {
type Item = CatchDifficultyAttributes;
fn next(&mut self) -> Option<Self::Item> {
// The first difficulty object belongs to the second palpable object
// since each difficulty 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)?;
Skill::new(&mut self.movement, &self.diff_objects).process(curr);
} else if self.count.is_empty() {
return None;
}
self.attrs.add_object_count(self.count[self.idx]);
self.idx += 1;
let mut attrs = self.attrs.clone();
let movement = self.movement.as_difficulty_value();
DifficultyValues::eval(&mut attrs, movement);
Some(attrs)
}
fn size_hint(&self) -> (usize, Option<usize>) {
let len = self.len();
(len, Some(len))
}
fn nth(&mut self, n: usize) -> Option<Self::Item> {
let skip_iter = self.diff_objects.iter().skip(self.idx.saturating_sub(1));
let mut take = n.min(self.len().saturating_sub(1));
// The first palpable object has no difficulty object
if self.idx == 0 && take > 0 {
take -= 1;
self.attrs.add_object_count(self.count[self.idx]);
self.idx += 1;
}
let mut movement = Skill::new(&mut self.movement, &self.diff_objects);
for curr in skip_iter.take(take) {
movement.process(curr);
self.attrs.add_object_count(self.count[self.idx]);
self.idx += 1;
}
self.next()
}
}
impl ExactSizeIterator for CatchGradualDifficulty {
fn len(&self) -> usize {
self.diff_objects.len() + 1 - self.idx
}
}
#[cfg(test)]
mod tests {
use crate::Beatmap;
use super::*;
#[test]
fn empty() {
let converted = Beatmap::from_bytes(&[]).unwrap().unchecked_into_converted();
let difficulty = ModeDifficulty::new();
let mut gradual = CatchGradualDifficulty::new(&difficulty, &converted);
assert!(gradual.next().is_none());
}
#[test]
fn next_and_nth() {
let converted = Beatmap::from_path("./resources/2118524.osu")
.unwrap()
.unchecked_into_converted();
let difficulty = ModeDifficulty::new();
let mut gradual = CatchGradualDifficulty::new(&difficulty, &converted);
let mut gradual_2nd = CatchGradualDifficulty::new(&difficulty, &converted);
let mut gradual_3rd = CatchGradualDifficulty::new(&difficulty, &converted);
for i in 1.. {
let Some(next_gradual) = gradual.next() else {
assert_eq!(i, 731);
assert!(gradual_2nd.last().is_none()); // 730 % 2 == 0
assert!(gradual_3rd.last().is_some()); // 730 % 3 == 1
break;
};
if i % 2 == 0 {
let next_gradual_2nd = gradual_2nd.nth(1).unwrap();
assert_eq!(next_gradual, next_gradual_2nd);
}
if i % 3 == 0 {
let next_gradual_3rd = gradual_3rd.nth(2).unwrap();
assert_eq!(next_gradual, next_gradual_3rd);
}
let expected = ModeDifficulty::new()
.passed_objects(i as u32)
.calculate(&converted);
assert_eq!(next_gradual, expected);
}
}
}
+70 -36
View File
@@ -3,16 +3,19 @@ use crate::{
catch::{
catcher::Catcher, convert::convert_objects, difficulty::object::CatchDifficultyObject,
},
model::beatmap::BeatmapAttributes,
util::mods::Mods,
};
use self::skills::movement::Movement;
use super::{
attributes::{CatchDifficultyAttributes, CatchDifficultyAttributesBuilder},
attributes::{CatchDifficultyAttributes, ObjectCountBuilder},
convert::CatchBeatmap,
object::palpable::PalpableObject,
};
pub mod gradual;
mod object;
mod skills;
@@ -27,11 +30,37 @@ pub fn difficulty(
mut attrs,
} = DifficultyValues::calculate(difficulty, converted);
attrs.stars = movement.difficulty_value().sqrt() * STAR_SCALING_FACTOR;
DifficultyValues::eval(&mut attrs, movement.difficulty_value());
attrs
}
pub struct CatchDifficultySetup {
map_attrs: BeatmapAttributes,
attrs: CatchDifficultyAttributes,
}
impl CatchDifficultySetup {
pub fn new(difficulty: &ModeDifficulty, converted: &CatchBeatmap<'_>) -> Self {
let mods = difficulty.get_mods();
let clock_rate = difficulty.get_clock_rate();
let map_attrs = converted
.attributes()
.mods(mods)
.clock_rate(clock_rate)
.build();
let attrs = CatchDifficultyAttributes {
ar: map_attrs.ar,
is_convert: converted.is_convert,
..Default::default()
};
Self { map_attrs, attrs }
}
}
pub struct DifficultyValues {
pub movement: Movement,
pub attrs: CatchDifficultyAttributes,
@@ -40,33 +69,51 @@ pub struct DifficultyValues {
impl DifficultyValues {
pub fn calculate(difficulty: &ModeDifficulty, converted: &CatchBeatmap<'_>) -> Self {
let take = difficulty.get_passed_objects();
let mods = difficulty.get_mods();
let clock_rate = difficulty.get_clock_rate();
let map_attrs = converted
.attributes()
.mods(difficulty.get_mods())
.clock_rate(clock_rate)
.build();
let CatchDifficultySetup {
map_attrs,
mut attrs,
} = CatchDifficultySetup::new(difficulty, converted);
let attrs = CatchDifficultyAttributes {
ar: map_attrs.ar,
is_convert: converted.is_convert,
..Default::default()
};
let hr = mods.hr();
let mut count = ObjectCountBuilder::new_regular(take);
let mut attrs = CatchDifficultyAttributesBuilder::new(attrs, take);
let palpable_objects = convert_objects(converted, &mut count, hr, map_attrs.cs as f32);
let diff_objects = Self::create_difficulty_objects(
&map_attrs,
clock_rate,
palpable_objects.iter().take(take),
);
let hr = difficulty.get_mods().hr();
let mut movement = Movement::new(clock_rate);
let palpable_objects = convert_objects(converted, &mut attrs, hr, map_attrs.cs as f32);
let mut palpable_objects_iter = palpable_objects.iter().take(take);
{
let mut movement = Skill::new(&mut movement, &diff_objects);
let Some(mut last_object) = palpable_objects_iter.next() else {
return Self {
movement,
attrs: attrs.into_inner(),
};
for curr in diff_objects.iter() {
movement.process(curr);
}
}
attrs.set_object_count(&count.into_regular());
Self { movement, attrs }
}
pub fn eval(attrs: &mut CatchDifficultyAttributes, movement_difficulty_value: f64) {
attrs.stars = movement_difficulty_value.sqrt() * STAR_SCALING_FACTOR;
}
pub fn create_difficulty_objects<'a>(
map_attrs: &BeatmapAttributes,
clock_rate: f64,
mut palpable_objects: impl ExactSizeIterator<Item = &'a PalpableObject>,
) -> Box<[CatchDifficultyObject]> {
let Some(mut last_object) = palpable_objects.next() else {
return Box::default();
};
let mut half_catcher_width = Catcher::calculate_catch_width(map_attrs.cs as f32) * 0.5;
@@ -74,7 +121,7 @@ impl DifficultyValues {
let scaling_factor =
CatchDifficultyObject::NORMALIZED_HITOBJECT_RADIUS / half_catcher_width;
let diff_objects: Vec<_> = palpable_objects_iter
palpable_objects
.enumerate()
.map(|(i, hit_object)| {
let diff_object = CatchDifficultyObject::new(
@@ -88,19 +135,6 @@ impl DifficultyValues {
diff_object
})
.collect();
{
let mut movement = Skill::new(&mut movement, &diff_objects);
for curr in diff_objects.iter() {
movement.process(curr);
}
}
Self {
movement,
attrs: attrs.into_inner(),
}
.collect()
}
}
+11 -1
View File
@@ -112,7 +112,17 @@ impl Movement {
}
pub fn difficulty_value(self) -> f64 {
self.inner.difficulty_value(DECAY_WEIGHT)
Self::static_difficulty_value(self.inner)
}
/// Use [`difficulty_value`] instead whenever possible because
/// [`as_difficulty_value`] clones internally.
pub fn as_difficulty_value(&self) -> f64 {
Self::static_difficulty_value(self.inner.clone())
}
fn static_difficulty_value(skill: StrainDecaySkill) -> f64 {
skill.difficulty_value(DECAY_WEIGHT)
}
}
+2 -1
View File
@@ -11,7 +11,8 @@ use crate::{
pub use self::{
attributes::{CatchDifficultyAttributes, CatchPerformanceAttributes},
convert::CatchBeatmap,
performance::CatchPerformance,
difficulty::gradual::CatchGradualDifficulty,
performance::{gradual::CatchGradualPerformance, CatchPerformance},
score_state::CatchScoreState,
strains::CatchStrains,
};
+3 -3
View File
@@ -1,12 +1,12 @@
use crate::catch::attributes::CatchDifficultyAttributesBuilder;
use crate::catch::attributes::ObjectCountBuilder;
pub struct Fruit {
pub x_offset: f32,
}
impl Fruit {
pub fn new(attrs: &mut CatchDifficultyAttributesBuilder) -> Self {
attrs.inc_fruits();
pub fn new(count: &mut ObjectCountBuilder) -> Self {
count.record_fruit();
Self { x_offset: 0.0 }
}
+9 -6
View File
@@ -5,7 +5,7 @@ use rosu_map::section::hit_objects::{
};
use crate::{
catch::{attributes::CatchDifficultyAttributesBuilder, convert::CatchBeatmap, PLAYFIELD_WIDTH},
catch::{attributes::ObjectCountBuilder, convert::CatchBeatmap, PLAYFIELD_WIDTH},
model::{
control_point::{DifficultyPoint, TimingPoint},
hit_object::Slider,
@@ -25,7 +25,7 @@ impl<'a> JuiceStream<'a> {
start_time: f64,
slider: &'a Slider,
converted: &CatchBeatmap<'_>,
attrs: &mut CatchDifficultyAttributesBuilder,
count: &mut ObjectCountBuilder,
bufs: &'a mut JuiceStreamBufs,
) -> Self {
let slider_multiplier = converted.map.slider_multiplier;
@@ -69,7 +69,8 @@ impl<'a> JuiceStream<'a> {
let mut last_event_time = None;
for e in events {
if let Some(last_event_time) = last_event_time.filter(|_| attrs.take_more()) {
if let Some(last_event_time) = last_event_time {
let mut tiny_droplets = 0;
let since_last_tick = e.time - last_event_time;
if since_last_tick > 80.0 {
@@ -82,7 +83,7 @@ impl<'a> JuiceStream<'a> {
let mut t = time_between_tiny;
while t < since_last_tick {
attrs.inc_tiny_droplets();
tiny_droplets += 1;
let nested = NestedJuiceStreamObject {
pos: 0.0, // not important
@@ -95,18 +96,20 @@ impl<'a> JuiceStream<'a> {
t += time_between_tiny;
}
}
count.record_tiny_droplets(tiny_droplets);
}
last_event_time = Some(e.time);
let kind = match e.kind {
SliderEventType::Tick => {
attrs.inc_droplets();
count.record_droplet();
NestedJuiceStreamObjectKind::Droplet
}
SliderEventType::Head | SliderEventType::Repeat | SliderEventType::Tail => {
attrs.inc_fruits();
count.record_fruit();
NestedJuiceStreamObjectKind::Fruit
}
+185
View File
@@ -0,0 +1,185 @@
use crate::{
catch::{CatchBeatmap, CatchGradualDifficulty, CatchPerformanceAttributes, CatchScoreState},
ModeDifficulty,
};
/// Gradually calculate the performance attributes of an osu!catch map.
///
/// After each hit object you can call [`next`] and it will return the resulting
/// current [`CatchPerformanceAttributes`]. To process multiple objects at once,
/// use [`nth`] instead.
///
/// Both methods require a [`CatchScoreState`] that contains the current
/// hitresults as well as the maximum combo so far.
///
/// Note that neither hits nor misses of tiny droplets require to be processed.
/// Only fruits and droplets do.
///
/// If you only want to calculate difficulty attributes use
/// [`CatchGradualDifficulty`] instead.
///
/// # Example
///
/// ```
/// use rosu_pp::{Beatmap, ModeDifficulty};
/// use rosu_pp::catch::{Catch, CatchGradualPerformance, CatchScoreState};
///
/// let converted = Beatmap::from_path()
/// .unwrap()
/// .unchecked_into_converted::<Catch>();
///
/// let difficulty = ModeDifficulty::new().mods(64); // DT
/// let mut gradual_perf = CatchGradualPerformance::new(&difficulty, &converted);
/// let mut state = CatchScoreState::new(); // empty state, everything is on 0.
///
/// // The first 10 hitresults are only fruits
/// for _ in 0..10 {
/// state.n_fruits += 1;
/// state.max_combo += 1;
///
/// let performance = gradual_perf.next(state.clone()).unwrap();
/// println!("PP: {}", performance.pp);
/// }
///
/// // Then comes a miss.
/// // Note that state's max combo won't be incremented for
/// // the next few objects because the combo is reset.
/// state.n_misses += 1;
/// let performance = gradual_perf.next(state.clone()).unwrap();
/// println!("PP: {}", performance.pp);
///
/// // The next 10 objects will be a mixture of fruits and droplets.
/// // Notice how tiny droplets from sliders do not count as hit objects
/// // that require processing. Only fruits and droplets do.
/// // Also notice how all 10 objects will be processed in one go.
/// state.n_fruits += 4;
/// state.n_droplets += 6;
/// state.n_tiny_droplets += 12;
/// // The `nth` method takes a zero-based value.
/// let performance = gradual_perf.nth(state.clone(), 9).unwrap();
/// println!("PP: {}", performance.pp);
///
/// // Now comes another fruit. Note that the max combo gets incremented again.
/// state.n_fruits += 1;
/// state.max_combo += 1;
/// let performance = gradual_perf.next(state.clone()).unwrap();
/// println!("PP: {}", performance.pp);
///
/// // Skip to the end
/// # /*
/// state.max_combo = ...
/// state.n_fruits = ...
/// state.n_droplets = ...
/// state.n_tiny_droplets = ...
/// state.n_tiny_droplet_misses = ...
/// state.n_misses = ...
/// # */
/// let final_performance = gradual_perf.last(state.clone()).unwrap();
/// println!("PP: {}", performance.pp);
///
/// // Once the final performance has been calculated,
/// // attempting to process further objects will return `None`.
/// assert!(gradual_perf.next(state).is_none());
/// ```
///
/// [`next`]: CatchGradualPerformance::next
/// [`nth`]: CatchGradualPerformance::nth
pub struct CatchGradualPerformance {
difficulty: CatchGradualDifficulty,
}
impl CatchGradualPerformance {
/// Create a new gradual performance calculator for osu!catch maps.
pub fn new(difficulty: &ModeDifficulty, converted: &CatchBeatmap<'_>) -> Self {
let difficulty = CatchGradualDifficulty::new(difficulty, converted);
Self { difficulty }
}
/// Process the next hit object and calculate the performance attributes
/// for the resulting score state.
///
/// Note that neither hits nor misses of tiny droplets require to be
/// processed. Only fruits and droplets do.
pub fn next(&mut self, state: CatchScoreState) -> Option<CatchPerformanceAttributes> {
self.nth(state, 0)
}
/// Process all remaining hit objects and calculate the final performance
/// attributes.
pub fn last(&mut self, state: CatchScoreState) -> Option<CatchPerformanceAttributes> {
self.nth(state, usize::MAX)
}
/// Process everything up the the next `n`th hit object and calculate the
/// performance attributes for the resulting score state.
///
/// 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: CatchScoreState, n: usize) -> Option<CatchPerformanceAttributes> {
let performance = self
.difficulty
.nth(n)?
.performance()
.state(state)
.mods(self.difficulty.mods)
.clock_rate(self.difficulty.clock_rate)
.passed_objects(self.difficulty.idx as u32)
.calculate();
Some(performance)
}
}
#[cfg(test)]
mod tests {
use crate::{catch::CatchPerformance, Beatmap};
use super::*;
#[test]
fn next_and_nth() {
let converted = Beatmap::from_path("./resources/2118524.osu")
.unwrap()
.unchecked_into_converted();
let mods = 88; // HDHRDT
let difficulty = ModeDifficulty::new().mods(88);
let mut gradual = CatchGradualPerformance::new(&difficulty, &converted);
let mut gradual_2nd = CatchGradualPerformance::new(&difficulty, &converted);
let mut gradual_3rd = CatchGradualPerformance::new(&difficulty, &converted);
let mut state = CatchScoreState::default();
for i in 1.. {
state.n_misses += 1;
let Some(next_gradual) = gradual.next(state.clone()) else {
assert_eq!(i, 731);
assert!(gradual_2nd.last(state.clone()).is_none()); // 730 % 2 == 0
assert!(gradual_3rd.last(state.clone()).is_some()); // 730 % 3 == 1
break;
};
if i % 2 == 0 {
let next_gradual_2nd = gradual_2nd.nth(state.clone(), 1).unwrap();
assert_eq!(next_gradual, next_gradual_2nd);
}
if i % 3 == 0 {
let next_gradual_3rd = gradual_3rd.nth(state.clone(), 2).unwrap();
assert_eq!(next_gradual, next_gradual_3rd);
}
let regular_calc = CatchPerformance::new(converted.as_owned())
.mods(mods)
.passed_objects(i as u32)
.state(state.clone());
let expected = regular_calc.calculate();
assert_eq!(next_gradual, expected);
}
}
}
@@ -14,6 +14,8 @@ use super::{
Catch,
};
pub mod gradual;
/// Performance calculator on osu!catch maps.
#[derive(Clone, Debug, PartialEq)]
#[must_use]