fixed & extended test cases
This commit is contained in:
@@ -1,4 +1,9 @@
|
||||
use crate::{beatmap::BeatmapHitWindows, parse::HitObjectKind, util::FloatExt, Beatmap, Mods};
|
||||
use crate::{
|
||||
beatmap::BeatmapHitWindows,
|
||||
parse::{HitObject, HitObjectKind},
|
||||
util::FloatExt,
|
||||
Beatmap, Mods,
|
||||
};
|
||||
|
||||
use super::{
|
||||
difficulty_object::ManiaDifficultyObject,
|
||||
@@ -52,7 +57,6 @@ impl<'map> ManiaGradualDifficultyAttributes<'map> {
|
||||
/// Create a new difficulty attributes iterator for osu!mania maps.
|
||||
pub fn new(map: &'map Beatmap, mods: u32) -> Self {
|
||||
let total_columns = map.cs.round_even().max(1.0);
|
||||
|
||||
let clock_rate = mods.clock_rate();
|
||||
let strain = Strain::new(total_columns as usize);
|
||||
|
||||
@@ -82,6 +86,8 @@ impl<'map> ManiaGradualDifficultyAttributes<'map> {
|
||||
}
|
||||
};
|
||||
|
||||
let curr_combo = params.max_combo;
|
||||
|
||||
let diff_objects_iter = hit_objects.enumerate().scan(first, |last, (i, h)| {
|
||||
let base = ManiaObject::new(h, total_columns, &mut params);
|
||||
let diff_object = ManiaDifficultyObject::new(&base, &*last, clock_rate, i);
|
||||
@@ -90,17 +96,6 @@ impl<'map> ManiaGradualDifficultyAttributes<'map> {
|
||||
Some(diff_object)
|
||||
});
|
||||
|
||||
let curr_combo = if let Some(h) = map.hit_objects.first() {
|
||||
match &h.kind {
|
||||
HitObjectKind::Hold { end_time } => {
|
||||
1 + ((*end_time - h.start_time) / 100.0) as usize
|
||||
}
|
||||
_ => 1,
|
||||
}
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
let mut diff_objects = Vec::with_capacity(map.hit_objects.len().saturating_sub(1));
|
||||
diff_objects.extend(diff_objects_iter);
|
||||
|
||||
@@ -114,6 +109,24 @@ impl<'map> ManiaGradualDifficultyAttributes<'map> {
|
||||
clock_rate,
|
||||
}
|
||||
}
|
||||
|
||||
fn increment_combo(
|
||||
h: &HitObject,
|
||||
diff_obj: &ManiaDifficultyObject,
|
||||
curr_combo: &mut usize,
|
||||
clock_rate: f64,
|
||||
) {
|
||||
match &h.kind {
|
||||
HitObjectKind::Circle => *curr_combo += 1,
|
||||
_ => {
|
||||
let start_time = diff_obj.start_time * clock_rate;
|
||||
let end_time = diff_obj.end_time * clock_rate;
|
||||
let duration = end_time - start_time;
|
||||
|
||||
*curr_combo += 1 + (duration / 100.0) as usize;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for ManiaGradualDifficultyAttributes<'_> {
|
||||
@@ -124,16 +137,7 @@ impl Iterator for ManiaGradualDifficultyAttributes<'_> {
|
||||
self.idx += 1;
|
||||
|
||||
if let Some(h) = self.map.hit_objects.get(self.idx) {
|
||||
match &h.kind {
|
||||
HitObjectKind::Circle => self.curr_combo += 1,
|
||||
_ => {
|
||||
let start_time = curr.start_time * self.clock_rate;
|
||||
let end_time = curr.end_time * self.clock_rate;
|
||||
let duration = end_time - start_time;
|
||||
|
||||
self.curr_combo += 1 + (duration / 100.0) as usize;
|
||||
}
|
||||
}
|
||||
Self::increment_combo(h, curr, &mut self.curr_combo, self.clock_rate);
|
||||
}
|
||||
|
||||
self.strain.process(curr, &self.diff_objects);
|
||||
@@ -151,6 +155,23 @@ impl Iterator for ManiaGradualDifficultyAttributes<'_> {
|
||||
|
||||
(len, Some(len))
|
||||
}
|
||||
|
||||
fn nth(&mut self, n: usize) -> Option<Self::Item> {
|
||||
let skip = n.min(self.len()).saturating_sub(1);
|
||||
|
||||
for _ in 0..skip {
|
||||
let curr = self.diff_objects.get(self.idx)?;
|
||||
self.idx += 1;
|
||||
|
||||
if let Some(h) = self.map.hit_objects.get(self.idx) {
|
||||
Self::increment_combo(h, curr, &mut self.curr_combo, self.clock_rate);
|
||||
}
|
||||
|
||||
self.strain.process(curr, &self.diff_objects);
|
||||
}
|
||||
|
||||
self.next()
|
||||
}
|
||||
}
|
||||
|
||||
impl ExactSizeIterator for ManiaGradualDifficultyAttributes<'_> {
|
||||
@@ -159,29 +180,3 @@ impl ExactSizeIterator for ManiaGradualDifficultyAttributes<'_> {
|
||||
self.diff_objects.len() - self.idx
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn empty_map() {
|
||||
let map = Beatmap::default();
|
||||
let mut attributes = ManiaGradualDifficultyAttributes::new(&map, 0);
|
||||
assert!(attributes.next().is_none());
|
||||
}
|
||||
|
||||
#[cfg(not(any(feature = "async_tokio", feature = "async_std")))]
|
||||
#[test]
|
||||
fn iter_end_eq_regular() {
|
||||
let map = Beatmap::from_path("./maps/1974394.osu").expect("failed to parse map");
|
||||
let mods = 64;
|
||||
let regular = crate::ManiaStars::new(&map).mods(mods).calculate();
|
||||
|
||||
let iter_end = ManiaGradualDifficultyAttributes::new(&map, mods)
|
||||
.last()
|
||||
.expect("empty iter");
|
||||
|
||||
assert_eq!(regular, iter_end);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,10 @@ use crate::{Beatmap, ManiaPP};
|
||||
|
||||
use super::{ManiaGradualDifficultyAttributes, ManiaPerformanceAttributes};
|
||||
|
||||
/// TODO: docs
|
||||
/// Aggregation for a score's current state
|
||||
/// i.e. what are the current hitresults.
|
||||
///
|
||||
/// This struct is used for [`ManiaGradualPerformanceAttributes`].
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq)]
|
||||
pub struct ManiaScoreState {
|
||||
/// Amount of current 320s.
|
||||
@@ -65,7 +68,7 @@ impl ManiaScoreState {
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use rosu_pp::{Beatmap, mania::ManiaGradualPerformanceAttributes};
|
||||
/// use rosu_pp::{Beatmap, mania::{ManiaGradualPerformanceAttributes, ManiaScoreState}};
|
||||
///
|
||||
/// # /*
|
||||
/// let map: Beatmap = ...
|
||||
@@ -74,45 +77,51 @@ impl ManiaScoreState {
|
||||
///
|
||||
/// let mods = 64; // DT
|
||||
/// let mut gradual_perf = ManiaGradualPerformanceAttributes::new(&map, mods);
|
||||
/// let mut score = 0;
|
||||
/// let mut state = ManiaScoreState::new(); // empty state, everything is on 0.
|
||||
///
|
||||
/// // The first 10 objects each increase the score by 123.
|
||||
/// // The first 10 hitresults are 320s
|
||||
/// for _ in 0..10 {
|
||||
/// score += 123;
|
||||
/// state.n320 += 1;
|
||||
///
|
||||
/// # /*
|
||||
/// let performance = gradual_perf.process_next_object(score).unwrap();
|
||||
/// println!("PP: {}", performance.pp);
|
||||
/// # */
|
||||
/// # let _ = gradual_perf.process_next_object(score);
|
||||
/// # let _ = gradual_perf.process_next_object(state.clone());
|
||||
/// }
|
||||
///
|
||||
/// // Then comes a miss so no additional score is added.
|
||||
/// // Then comes a miss.
|
||||
/// state.n_misses += 1;
|
||||
/// # /*
|
||||
/// let performance = gradual_perf.process_next_object(score).unwrap();
|
||||
/// println!("PP: {}", performance.pp);
|
||||
/// # */
|
||||
/// # let _ = gradual_perf.process_next_object(score);
|
||||
/// # let _ = gradual_perf.process_next_object(state.clone());
|
||||
///
|
||||
/// // The next 10 objects give a total of 987 score and will be processed in one go.
|
||||
/// score += 987;
|
||||
/// // The next 10 objects will be a mixture of 320s and 100s.
|
||||
/// // Notice how all 10 objects will be processed in one go.
|
||||
/// state.n320 += 3;
|
||||
/// state.n100 += 7;
|
||||
/// # /*
|
||||
/// let performance = gradual_perf.process_next_n_objects(score, 10).unwrap();
|
||||
/// println!("PP: {}", performance.pp);
|
||||
/// # */
|
||||
/// # let _ = gradual_perf.process_next_n_objects(score, 10);
|
||||
/// # let _ = gradual_perf.process_next_n_objects(state.clone(), 10);
|
||||
///
|
||||
/// // Skip to the end
|
||||
/// # /*
|
||||
/// score = ...
|
||||
/// let final_performance = gradual_perf.process_next_n_objects(score, usize::MAX).unwrap();
|
||||
/// state.max_combo = ...
|
||||
/// state.n300 = ...
|
||||
/// state.n100 = ...
|
||||
/// state.n_misses = ...
|
||||
/// let final_performance = gradual_perf.process_next_n_objects(state.clone(), usize::MAX).unwrap();
|
||||
/// println!("PP: {}", performance.pp);
|
||||
/// # */
|
||||
/// # let _ = gradual_perf.process_next_n_objects(score, usize::MAX);
|
||||
/// # let _ = gradual_perf.process_next_n_objects(state.clone(), usize::MAX);
|
||||
///
|
||||
/// // Once the final performance was calculated,
|
||||
/// // attempting to process further objects will return `None`.
|
||||
/// assert!(gradual_perf.process_next_object(score).is_none());
|
||||
/// assert!(gradual_perf.process_next_object(state).is_none());
|
||||
/// ```
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ManiaGradualPerformanceAttributes<'map> {
|
||||
@@ -152,140 +161,17 @@ impl<'map> ManiaGradualPerformanceAttributes<'map> {
|
||||
state: ManiaScoreState,
|
||||
n: usize,
|
||||
) -> Option<ManiaPerformanceAttributes> {
|
||||
let n = n.min(self.difficulty.len()).saturating_sub(1);
|
||||
let difficulty = self.difficulty.nth(n)?;
|
||||
|
||||
self.performance.n320 = Some(state.n320);
|
||||
self.performance.n300 = Some(state.n300);
|
||||
self.performance.n200 = Some(state.n200);
|
||||
self.performance.n100 = Some(state.n100);
|
||||
self.performance.n50 = Some(state.n50);
|
||||
self.performance.n_misses = Some(state.n_misses);
|
||||
let sub = (self.difficulty.idx == 0) as usize;
|
||||
let difficulty = self.difficulty.nth(n.saturating_sub(sub))?;
|
||||
|
||||
let performance = self
|
||||
.performance
|
||||
.clone()
|
||||
.attributes(difficulty)
|
||||
.state(state)
|
||||
.passed_objects(self.difficulty.idx)
|
||||
.calculate();
|
||||
|
||||
Some(performance)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[allow(unused_imports)]
|
||||
use super::*;
|
||||
|
||||
#[cfg(not(any(feature = "async_tokio", feature = "async_std")))]
|
||||
#[test]
|
||||
fn correct_empty() {
|
||||
let map = Beatmap::from_path("./maps/1974394.osu").expect("failed to parse map");
|
||||
let mods = 64;
|
||||
|
||||
let mut gradual = ManiaGradualPerformanceAttributes::new(&map, mods);
|
||||
|
||||
let state = ManiaScoreState {
|
||||
n320: 0,
|
||||
n300: 0,
|
||||
n200: 0,
|
||||
n100: 0,
|
||||
n50: 0,
|
||||
n_misses: 0,
|
||||
};
|
||||
|
||||
assert!(gradual
|
||||
.process_next_n_objects(state.clone(), usize::MAX)
|
||||
.is_some());
|
||||
assert!(gradual.process_next_object(state).is_none());
|
||||
}
|
||||
|
||||
#[cfg(not(any(feature = "async_tokio", feature = "async_std")))]
|
||||
#[test]
|
||||
fn next_and_next_n() {
|
||||
let map = Beatmap::from_path("./maps/1974394.osu").expect("failed to parse map");
|
||||
let mods = 64;
|
||||
|
||||
let mut state = ManiaScoreState {
|
||||
n320: 0,
|
||||
n300: 0,
|
||||
n200: 0,
|
||||
n100: 0,
|
||||
n50: 0,
|
||||
n_misses: 0,
|
||||
};
|
||||
|
||||
let mut gradual1 = ManiaGradualPerformanceAttributes::new(&map, mods);
|
||||
let mut gradual2 = ManiaGradualPerformanceAttributes::new(&map, mods);
|
||||
|
||||
for _ in 0..20 {
|
||||
let _ = gradual1.process_next_object(state.clone());
|
||||
let _ = gradual2.process_next_object(state.clone());
|
||||
state.n320 += 1;
|
||||
}
|
||||
|
||||
let n = 80;
|
||||
|
||||
for _ in 1..n {
|
||||
let _ = gradual1.process_next_object(state.clone());
|
||||
state.n320 += 1;
|
||||
}
|
||||
|
||||
let next = gradual1.process_next_object(state.clone());
|
||||
let next_n = gradual2.process_next_n_objects(state, n);
|
||||
|
||||
assert_eq!(next_n, next);
|
||||
}
|
||||
|
||||
#[cfg(not(any(feature = "async_tokio", feature = "async_std")))]
|
||||
#[test]
|
||||
fn gradual_end_eq_regular() {
|
||||
let map = Beatmap::from_path("./maps/1974394.osu").expect("failed to parse map");
|
||||
let mods = 64;
|
||||
let regular = ManiaPP::new(&map).mods(mods).calculate();
|
||||
|
||||
let mut gradual = ManiaGradualPerformanceAttributes::new(&map, mods);
|
||||
|
||||
let state = ManiaScoreState {
|
||||
n320: 3238,
|
||||
n300: 0,
|
||||
n200: 0,
|
||||
n100: 0,
|
||||
n50: 0,
|
||||
n_misses: 0,
|
||||
};
|
||||
|
||||
let gradual_end = gradual.process_next_n_objects(state, usize::MAX).unwrap();
|
||||
|
||||
assert_eq!(regular, gradual_end);
|
||||
}
|
||||
|
||||
#[cfg(not(any(feature = "async_tokio", feature = "async_std")))]
|
||||
#[test]
|
||||
fn gradual_eq_regular_passed() {
|
||||
let map = Beatmap::from_path("./maps/1974394.osu").expect("failed to parse map");
|
||||
let mods = 64;
|
||||
let n = 100;
|
||||
|
||||
let state = ManiaScoreState {
|
||||
n320: 100,
|
||||
n300: 0,
|
||||
n200: 0,
|
||||
n100: 0,
|
||||
n50: 0,
|
||||
n_misses: 0,
|
||||
};
|
||||
|
||||
let regular = ManiaPP::new(&map)
|
||||
.mods(mods)
|
||||
.passed_objects(n)
|
||||
.state(state.clone())
|
||||
.calculate();
|
||||
|
||||
let mut gradual = ManiaGradualPerformanceAttributes::new(&map, mods);
|
||||
let gradual = gradual.process_next_n_objects(state, n).unwrap();
|
||||
|
||||
assert_eq!(regular, gradual);
|
||||
}
|
||||
}
|
||||
|
||||
+7
-1
@@ -175,7 +175,7 @@ fn calculate_result(params: ManiaStars<'_>) -> ManiaResult {
|
||||
let clock_rate = clock_rate.unwrap_or_else(|| mods.clock_rate());
|
||||
let mut strain = Strain::new(total_columns as usize);
|
||||
let mut params = ObjectParameters::new(map.as_ref());
|
||||
let mut hit_objects = map.hit_objects.iter();
|
||||
let mut hit_objects = map.hit_objects.iter().take(take);
|
||||
|
||||
let first = match hit_objects.next() {
|
||||
Some(h) => ManiaObject::new(h, total_columns, &mut params),
|
||||
@@ -247,6 +247,12 @@ impl ManiaPerformanceAttributes {
|
||||
pub fn pp(&self) -> f64 {
|
||||
self.pp
|
||||
}
|
||||
|
||||
/// Return the maximum combo of the map.
|
||||
#[inline]
|
||||
pub fn max_combo(&self) -> usize {
|
||||
self.difficulty.max_combo
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ManiaPerformanceAttributes> for ManiaDifficultyAttributes {
|
||||
|
||||
+647
-72
@@ -20,15 +20,21 @@ use crate::{
|
||||
///
|
||||
/// let pp_result = ManiaPP::new(&map)
|
||||
/// .mods(64) // DT
|
||||
/// .score(765_432)
|
||||
/// .n_misses(1)
|
||||
/// .accuracy(98.5)
|
||||
/// .calculate();
|
||||
///
|
||||
/// println!("PP: {} | Stars: {}", pp_result.pp(), pp_result.stars());
|
||||
///
|
||||
/// let next_result = ManiaPP::new(&map)
|
||||
/// .attributes(pp_result) // reusing previous results for performance
|
||||
/// .mods(8 + 64) // has to be the same to reuse attributes
|
||||
/// .score(950_000)
|
||||
/// .attributes(pp_result) // reusing previous results for performance
|
||||
/// .mods(8 + 64) // has to be the same to reuse attributes
|
||||
/// .n320(2000)
|
||||
/// .n300(500)
|
||||
/// .n200(200)
|
||||
/// .n100(100)
|
||||
/// .n50(10)
|
||||
/// .n_misses(1)
|
||||
/// .calculate();
|
||||
///
|
||||
/// println!("PP: {} | Stars: {}", next_result.pp(), next_result.stars());
|
||||
@@ -122,7 +128,7 @@ impl<'map> ManiaPP<'map> {
|
||||
/// This will be used to generate matching hitresults.
|
||||
#[inline]
|
||||
pub fn accuracy(mut self, acc: f64) -> Self {
|
||||
self.acc = Some(acc);
|
||||
self.acc = Some(acc / 100.0);
|
||||
|
||||
self
|
||||
}
|
||||
@@ -238,110 +244,258 @@ impl<'map> ManiaPP<'map> {
|
||||
let n_objects = self.passed_objects.unwrap_or(self.map.hit_objects.len());
|
||||
let priority = self.hitresult_priority.unwrap_or_default();
|
||||
|
||||
let mut state = ManiaScoreState {
|
||||
n320: self.n320.unwrap_or(0),
|
||||
n300: self.n300.unwrap_or(0),
|
||||
n200: self.n200.unwrap_or(0),
|
||||
n100: self.n100.unwrap_or(0),
|
||||
n50: self.n50.unwrap_or(0),
|
||||
n_misses: self.n_misses.unwrap_or(0),
|
||||
};
|
||||
let mut n320 = self.n320.unwrap_or(0);
|
||||
let mut n300 = self.n300.unwrap_or(0);
|
||||
let mut n200 = self.n200.unwrap_or(0);
|
||||
let mut n100 = self.n100.unwrap_or(0);
|
||||
let mut n50 = self.n50.unwrap_or(0);
|
||||
let n_misses = self.n_misses.unwrap_or(0);
|
||||
|
||||
if let Some(acc) = self.acc {
|
||||
// TODO: test
|
||||
let target_total = (acc * (n_objects * 6) as f64).round() as usize;
|
||||
|
||||
let mut delta = target_total.saturating_sub(n_objects.saturating_sub(state.n_misses));
|
||||
match (self.n320, self.n300, self.n200, self.n100, self.n50) {
|
||||
(Some(_), Some(_), Some(_), Some(_), Some(_)) => {
|
||||
let remaining =
|
||||
n_objects.saturating_sub(n320 + n300 + n200 + n100 + n50 + n_misses);
|
||||
|
||||
if self.n50.is_some() {
|
||||
delta /= 2;
|
||||
}
|
||||
|
||||
if self.n100.is_some() {
|
||||
delta /= 2;
|
||||
}
|
||||
|
||||
if let Some(n320) = self.n320 {
|
||||
delta = delta.saturating_sub(n320 * 6);
|
||||
} else {
|
||||
state.n320 = delta / 5;
|
||||
}
|
||||
|
||||
if self.n100.is_none() {
|
||||
state.n100 = delta % 5;
|
||||
}
|
||||
|
||||
state.n50 += n_objects.saturating_sub(state.total_hits() - state.n50);
|
||||
|
||||
if let HitResultPriority::BestCase = priority {
|
||||
// Shift n50 to n200
|
||||
if self.n320.or(self.n300).or(self.n200).or(self.n50).is_none() {
|
||||
let n = (state.n320 + state.n300).min(state.n50 / 2);
|
||||
|
||||
if n <= state.n300 {
|
||||
state.n300 -= n;
|
||||
} else {
|
||||
state.n320 -= n - state.n300;
|
||||
state.n300 = 0;
|
||||
};
|
||||
|
||||
state.n200 += 2 * n;
|
||||
state.n50 -= n;
|
||||
match priority {
|
||||
HitResultPriority::BestCase => n320 += remaining,
|
||||
HitResultPriority::WorstCase => n50 += remaining,
|
||||
}
|
||||
}
|
||||
(Some(_), None, Some(_), Some(_), Some(_)) => {
|
||||
n300 = n_objects.saturating_sub(n320 + n200 + n100 + n50 + n_misses)
|
||||
}
|
||||
(None, Some(_), Some(_), Some(_), Some(_)) => {
|
||||
n320 = n_objects.saturating_sub(n300 + n200 + n100 + n50 + n_misses)
|
||||
}
|
||||
(Some(_), _, Some(_), Some(_), None) | (_, Some(_), Some(_), Some(_), None) => {
|
||||
n50 = n_objects.saturating_sub(n320 + n300 + n200 + n100 + n_misses);
|
||||
}
|
||||
(Some(_), _, _, None, None) | (_, Some(_), _, None, None) => {
|
||||
let n3x0 = n320 + n300;
|
||||
let delta = (target_total - n_objects.saturating_sub(n_misses))
|
||||
.saturating_sub(n3x0 * 5 + n200 * 3);
|
||||
|
||||
// Shift n50 to n100
|
||||
if self.n320.or(self.n300).or(self.n100).or(self.n50).is_none() {
|
||||
let n = (state.n320 + state.n300).min(state.n50 / 4);
|
||||
n100 = delta % 5;
|
||||
n50 = n_objects.saturating_sub(n3x0 + n200 + n100 + n_misses);
|
||||
|
||||
if n <= state.n300 {
|
||||
state.n300 -= n;
|
||||
let curr_total = 6 * n3x0 + 4 * n200 + 2 * n100 + n50;
|
||||
|
||||
if curr_total < target_total {
|
||||
let n = (target_total - curr_total).min(n50);
|
||||
n50 -= n;
|
||||
n100 += n;
|
||||
} else {
|
||||
state.n320 -= n - state.n300;
|
||||
state.n300 = 0;
|
||||
};
|
||||
let n = (curr_total - target_total).min(n100);
|
||||
n100 -= n;
|
||||
n50 += n;
|
||||
}
|
||||
}
|
||||
(Some(_), _, None, Some(_), None) | (_, Some(_), None, Some(_), None) => {
|
||||
let n3x0 = n320 + n300;
|
||||
let delta = (target_total - n_objects.saturating_sub(n_misses))
|
||||
.saturating_sub(n3x0 * 5 + n100);
|
||||
|
||||
state.n100 += 5 * n;
|
||||
state.n50 -= 4 * n;
|
||||
n200 = delta / 3;
|
||||
n50 = n_objects.saturating_sub(n3x0 + n200 + n100 + n_misses);
|
||||
}
|
||||
(Some(_), _, None, None, Some(_)) | (_, Some(_), None, None, Some(_)) => {
|
||||
let remaining = n_objects.saturating_sub(n320 + n300 + n50 + n_misses);
|
||||
|
||||
match priority {
|
||||
HitResultPriority::BestCase => n100 = remaining,
|
||||
HitResultPriority::WorstCase => n200 = remaining,
|
||||
}
|
||||
}
|
||||
(Some(_), _, None, Some(_), Some(_)) | (_, Some(_), None, Some(_), Some(_)) => {
|
||||
n200 = n_objects.saturating_sub(n320 + n300 + n100 + n50 + n_misses);
|
||||
}
|
||||
(Some(_), _, Some(_), None, Some(_)) | (_, Some(_), Some(_), None, Some(_)) => {
|
||||
n100 = n_objects.saturating_sub(n320 + n300 + n200 + n50 + n_misses);
|
||||
}
|
||||
(None, None, Some(_), Some(_), Some(_)) => {
|
||||
let remaining = n_objects.saturating_sub(n200 + n100 + n50 + n_misses);
|
||||
|
||||
match priority {
|
||||
HitResultPriority::BestCase => n320 = remaining,
|
||||
HitResultPriority::WorstCase => n300 = remaining,
|
||||
}
|
||||
}
|
||||
(None, None, None, Some(_), Some(_)) => {
|
||||
let delta =
|
||||
(target_total - n_objects.saturating_sub(n_misses)).saturating_sub(n100);
|
||||
|
||||
match priority {
|
||||
HitResultPriority::BestCase => n320 = delta / 5,
|
||||
HitResultPriority::WorstCase => n300 = delta / 5,
|
||||
}
|
||||
|
||||
n200 = n_objects.saturating_sub(n320 + n100 + n50 + n_misses);
|
||||
|
||||
let curr_total = 6 * (n320 + n300) + 4 * n200 + 2 * n100 + n50;
|
||||
|
||||
if curr_total < target_total {
|
||||
let n = n200.min((target_total - curr_total) / 2);
|
||||
n200 -= n;
|
||||
|
||||
match priority {
|
||||
HitResultPriority::BestCase => n320 += n,
|
||||
HitResultPriority::WorstCase => n300 += n,
|
||||
}
|
||||
} else {
|
||||
let n = (n320 + n300).min((curr_total - target_total) / 2);
|
||||
n200 += n;
|
||||
|
||||
match priority {
|
||||
HitResultPriority::BestCase => n320 -= n,
|
||||
HitResultPriority::WorstCase => n300 -= n,
|
||||
}
|
||||
}
|
||||
}
|
||||
(None, None, Some(_), None, None) => {
|
||||
let delta = (target_total - n_objects.saturating_sub(n_misses))
|
||||
.saturating_sub(n200 * 3);
|
||||
|
||||
match priority {
|
||||
HitResultPriority::BestCase => n320 = delta / 5,
|
||||
HitResultPriority::WorstCase => n300 = delta / 5,
|
||||
}
|
||||
|
||||
n100 = delta % 5;
|
||||
n50 = n_objects.saturating_sub(n320 + n200 + n100 + n_misses);
|
||||
|
||||
let curr_total = 6 * (n320 + n300) + 4 * n200 * 2 * n100 + n50;
|
||||
|
||||
if curr_total < target_total {
|
||||
let n = (target_total - curr_total).min(n50);
|
||||
n50 -= n;
|
||||
n100 += n;
|
||||
} else {
|
||||
let n = (curr_total - target_total).min(n100);
|
||||
n100 -= n;
|
||||
n50 += n;
|
||||
}
|
||||
|
||||
if let HitResultPriority::BestCase = priority {
|
||||
// Shift n50 to n100
|
||||
let n = n320.min(n50 / 4);
|
||||
|
||||
n320 -= n;
|
||||
n100 += 5 * n;
|
||||
n50 -= 4 * n;
|
||||
}
|
||||
}
|
||||
(None, None, _, Some(_), None) => {
|
||||
let delta = (target_total - n_objects.saturating_sub(n_misses))
|
||||
.saturating_sub(n200 * 3 + n100);
|
||||
|
||||
match priority {
|
||||
HitResultPriority::BestCase => n320 = delta / 5,
|
||||
HitResultPriority::WorstCase => n300 = delta / 5,
|
||||
}
|
||||
|
||||
n50 = n_objects.saturating_sub(n320 + n300 + n200 + n100 + n_misses);
|
||||
}
|
||||
(None, None, _, None, Some(_)) => {
|
||||
let delta =
|
||||
target_total - n_objects.saturating_sub(n_misses).saturating_sub(n200 * 3);
|
||||
|
||||
match priority {
|
||||
HitResultPriority::BestCase => n320 = delta / 5,
|
||||
HitResultPriority::WorstCase => n300 = delta / 5,
|
||||
}
|
||||
|
||||
n100 = delta % 5;
|
||||
n100 += n_objects.saturating_sub(n320 + n300 + n200 + n100 + n50 + n_misses);
|
||||
|
||||
let curr_total = 6 * (n320 + n300) + 4 * n200 + 2 * n100 + n50;
|
||||
|
||||
if curr_total < target_total {
|
||||
let n = n100.min((target_total - curr_total) / 4);
|
||||
n100 -= n;
|
||||
|
||||
match priority {
|
||||
HitResultPriority::BestCase => n320 += n,
|
||||
HitResultPriority::WorstCase => n300 += n,
|
||||
}
|
||||
} else {
|
||||
let n = (n320 + n300).min((curr_total - target_total) / 4);
|
||||
n100 += n;
|
||||
|
||||
match priority {
|
||||
HitResultPriority::BestCase => n320 -= n,
|
||||
HitResultPriority::WorstCase => n300 -= n,
|
||||
}
|
||||
}
|
||||
}
|
||||
(None, None, None, None, None) => {
|
||||
let delta = target_total - n_objects.saturating_sub(n_misses);
|
||||
|
||||
match priority {
|
||||
HitResultPriority::BestCase => n320 = delta / 5,
|
||||
HitResultPriority::WorstCase => n300 = delta / 5,
|
||||
}
|
||||
|
||||
n100 = delta % 5;
|
||||
n50 = n_objects.saturating_sub(n320 + n300 + n100 + n_misses);
|
||||
|
||||
if let HitResultPriority::BestCase = priority {
|
||||
// Shift n50 to n100
|
||||
let n = n320.min(n50 / 4);
|
||||
n320 -= n;
|
||||
n100 += 5 * n;
|
||||
n50 -= 4 * n;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let remaining = n_objects.saturating_sub(state.total_hits());
|
||||
let remaining = n_objects.saturating_sub(n320 + n300 + n200 + n100 + n50 + n_misses);
|
||||
|
||||
match priority {
|
||||
HitResultPriority::BestCase => {
|
||||
if self.n320.is_none() {
|
||||
state.n320 = remaining;
|
||||
n320 = remaining;
|
||||
} else if self.n300.is_none() {
|
||||
state.n300 = remaining;
|
||||
n300 = remaining;
|
||||
} else if self.n200.is_none() {
|
||||
state.n200 = remaining;
|
||||
n200 = remaining;
|
||||
} else if self.n100.is_none() {
|
||||
state.n100 = remaining;
|
||||
n100 = remaining;
|
||||
} else if self.n50.is_none() {
|
||||
state.n50 = remaining;
|
||||
n50 = remaining;
|
||||
} else {
|
||||
state.n320 = remaining;
|
||||
n320 += remaining;
|
||||
}
|
||||
}
|
||||
HitResultPriority::WorstCase => {
|
||||
if self.n50.is_none() {
|
||||
state.n50 = remaining;
|
||||
n50 = remaining;
|
||||
} else if self.n100.is_none() {
|
||||
state.n100 = remaining;
|
||||
n100 = remaining;
|
||||
} else if self.n200.is_none() {
|
||||
state.n200 = remaining;
|
||||
n200 = remaining;
|
||||
} else if self.n300.is_none() {
|
||||
state.n300 = remaining;
|
||||
n300 = remaining;
|
||||
} else if self.n320.is_none() {
|
||||
state.n320 = remaining;
|
||||
n320 = remaining;
|
||||
} else {
|
||||
state.n50 = remaining;
|
||||
n50 += remaining;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
state
|
||||
ManiaScoreState {
|
||||
n320,
|
||||
n300,
|
||||
n200,
|
||||
n100,
|
||||
n50,
|
||||
n_misses,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -488,3 +642,424 @@ impl ManiaAttributeProvider for PerformanceAttributes {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(any(feature = "async_tokio", feature = "async_str")))]
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::Beatmap;
|
||||
|
||||
fn test_data() -> (Beatmap, ManiaDifficultyAttributes) {
|
||||
let path = "./maps/1974394.osu";
|
||||
let map = Beatmap::from_path(path).unwrap();
|
||||
|
||||
let attrs = ManiaDifficultyAttributes {
|
||||
stars: 4.824631127426499,
|
||||
hit_window: 40.0,
|
||||
max_combo: 5064,
|
||||
};
|
||||
|
||||
(map, attrs)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hitresults_acc_n320_n200_n_misses_best() {
|
||||
let (map, attrs) = test_data();
|
||||
|
||||
let state = ManiaPP::new(&map)
|
||||
.attributes(attrs)
|
||||
.accuracy(90.0)
|
||||
.n320(2600)
|
||||
.n200(400)
|
||||
.n_misses(2)
|
||||
.hitresult_priority(HitResultPriority::BestCase)
|
||||
.generate_hitresults();
|
||||
|
||||
let expected = ManiaScoreState {
|
||||
n320: 2600,
|
||||
n300: 0,
|
||||
n200: 400,
|
||||
n100: 49,
|
||||
n50: 187,
|
||||
n_misses: 2,
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
state,
|
||||
expected,
|
||||
"{}% vs {}%",
|
||||
state.accuracy(),
|
||||
expected.accuracy()
|
||||
);
|
||||
assert_eq!(state.total_hits(), 3238);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hitresults_acc_n320_n300_n200_n_misses_best() {
|
||||
let (map, attrs) = test_data();
|
||||
|
||||
let state = ManiaPP::new(&map)
|
||||
.attributes(attrs)
|
||||
.accuracy(90.0)
|
||||
.n320(2250)
|
||||
.n300(500)
|
||||
.n200(100)
|
||||
.n_misses(2)
|
||||
.hitresult_priority(HitResultPriority::BestCase)
|
||||
.generate_hitresults();
|
||||
|
||||
let expected = ManiaScoreState {
|
||||
n320: 2250,
|
||||
n300: 500,
|
||||
n200: 100,
|
||||
n100: 199,
|
||||
n50: 187,
|
||||
n_misses: 2,
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
state,
|
||||
expected,
|
||||
"{}% vs {}%",
|
||||
state.accuracy(),
|
||||
expected.accuracy()
|
||||
);
|
||||
assert_eq!(state.total_hits(), 3238);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hitresults_acc_n320_n300_n100_n_misses_best() {
|
||||
let (map, attrs) = test_data();
|
||||
|
||||
let state = ManiaPP::new(&map)
|
||||
.attributes(attrs)
|
||||
.accuracy(90.0)
|
||||
.n320(2000)
|
||||
.n300(500)
|
||||
.n100(100)
|
||||
.n_misses(2)
|
||||
.hitresult_priority(HitResultPriority::BestCase)
|
||||
.generate_hitresults();
|
||||
|
||||
let expected = ManiaScoreState {
|
||||
n320: 2000,
|
||||
n300: 500,
|
||||
n200: 549,
|
||||
n100: 100,
|
||||
n50: 87,
|
||||
n_misses: 2,
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
state,
|
||||
expected,
|
||||
"{}% vs {}%",
|
||||
state.accuracy(),
|
||||
expected.accuracy()
|
||||
);
|
||||
assert_eq!(state.total_hits(), 3238);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hitresults_acc_n320_n100_n50_n_misses_best() {
|
||||
let (map, attrs) = test_data();
|
||||
|
||||
let state = ManiaPP::new(&map)
|
||||
.attributes(attrs)
|
||||
.accuracy(90.0)
|
||||
.n320(2700)
|
||||
.n100(200)
|
||||
.n50(10)
|
||||
.n_misses(2)
|
||||
.hitresult_priority(HitResultPriority::BestCase)
|
||||
.generate_hitresults();
|
||||
|
||||
let expected = ManiaScoreState {
|
||||
n320: 2700,
|
||||
n300: 0,
|
||||
n200: 326,
|
||||
n100: 200,
|
||||
n50: 10,
|
||||
n_misses: 2,
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
state,
|
||||
expected,
|
||||
"{}% vs {}%",
|
||||
state.accuracy(),
|
||||
expected.accuracy()
|
||||
);
|
||||
assert_eq!(state.total_hits(), 3238);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hitresults_acc_n320_n50_n_misses_worst() {
|
||||
let (map, attrs) = test_data();
|
||||
|
||||
let state = ManiaPP::new(&map)
|
||||
.attributes(attrs)
|
||||
.accuracy(90.0)
|
||||
.n320(2000)
|
||||
.n50(50)
|
||||
.n_misses(2)
|
||||
.hitresult_priority(HitResultPriority::WorstCase)
|
||||
.generate_hitresults();
|
||||
|
||||
let expected = ManiaScoreState {
|
||||
n320: 2000,
|
||||
n300: 0,
|
||||
n200: 1186,
|
||||
n100: 0,
|
||||
n50: 50,
|
||||
n_misses: 2,
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
state,
|
||||
expected,
|
||||
"{}% vs {}%",
|
||||
state.accuracy(),
|
||||
expected.accuracy()
|
||||
);
|
||||
assert_eq!(state.total_hits(), 3238);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hitresults_acc_n100_n50_n_misses_best() {
|
||||
let (map, attrs) = test_data();
|
||||
|
||||
let state = ManiaPP::new(&map)
|
||||
.attributes(attrs)
|
||||
.accuracy(90.0)
|
||||
.n100(200)
|
||||
.n50(50)
|
||||
.n_misses(2)
|
||||
.hitresult_priority(HitResultPriority::BestCase)
|
||||
.generate_hitresults();
|
||||
|
||||
let expected = ManiaScoreState {
|
||||
n320: 2546,
|
||||
n300: 0,
|
||||
n200: 440,
|
||||
n100: 200,
|
||||
n50: 50,
|
||||
n_misses: 2,
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
state,
|
||||
expected,
|
||||
"{}% vs {}%",
|
||||
state.accuracy(),
|
||||
expected.accuracy()
|
||||
);
|
||||
assert_eq!(state.total_hits(), 3238);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hitresults_acc_n200_n_misses_best() {
|
||||
let (map, attrs) = test_data();
|
||||
|
||||
let state = ManiaPP::new(&map)
|
||||
.attributes(attrs)
|
||||
.accuracy(90.0)
|
||||
.n200(500)
|
||||
.n_misses(2)
|
||||
.hitresult_priority(HitResultPriority::BestCase)
|
||||
.generate_hitresults();
|
||||
|
||||
let expected = ManiaScoreState {
|
||||
n320: 2503,
|
||||
n300: 0,
|
||||
n200: 500,
|
||||
n100: 230,
|
||||
n50: 3,
|
||||
n_misses: 2,
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
state,
|
||||
expected,
|
||||
"{}% vs {}%",
|
||||
state.accuracy(),
|
||||
expected.accuracy()
|
||||
);
|
||||
assert_eq!(state.total_hits(), 3238);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hitresults_acc_n200_n100_n_misses_best() {
|
||||
let (map, attrs) = test_data();
|
||||
|
||||
let state = ManiaPP::new(&map)
|
||||
.attributes(attrs)
|
||||
.accuracy(90.0)
|
||||
.n200(500)
|
||||
.n100(200)
|
||||
.n_misses(2)
|
||||
.hitresult_priority(HitResultPriority::BestCase)
|
||||
.generate_hitresults();
|
||||
|
||||
let expected = ManiaScoreState {
|
||||
n320: 2509,
|
||||
n300: 0,
|
||||
n200: 500,
|
||||
n100: 200,
|
||||
n50: 27,
|
||||
n_misses: 2,
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
state,
|
||||
expected,
|
||||
"{}% vs {}%",
|
||||
state.accuracy(),
|
||||
expected.accuracy()
|
||||
);
|
||||
assert_eq!(state.total_hits(), 3238);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hitresults_acc_n50_n_misses_best() {
|
||||
let (map, attrs) = test_data();
|
||||
|
||||
let state = ManiaPP::new(&map)
|
||||
.attributes(attrs)
|
||||
.accuracy(90.0)
|
||||
.n50(200)
|
||||
.n_misses(2)
|
||||
.hitresult_priority(HitResultPriority::BestCase)
|
||||
.generate_hitresults();
|
||||
|
||||
let expected = ManiaScoreState {
|
||||
n320: 2804,
|
||||
n300: 0,
|
||||
n200: 0,
|
||||
n100: 232,
|
||||
n50: 200,
|
||||
n_misses: 2,
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
state,
|
||||
expected,
|
||||
"{}% vs {}%",
|
||||
state.accuracy(),
|
||||
expected.accuracy()
|
||||
);
|
||||
assert_eq!(state.total_hits(), 3238);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hitresults_acc_n200_n100_n50_n_misses_best() {
|
||||
let (map, attrs) = test_data();
|
||||
|
||||
let state = ManiaPP::new(&map)
|
||||
.attributes(attrs)
|
||||
.accuracy(90.0)
|
||||
.n200(500)
|
||||
.n100(300)
|
||||
.n50(100)
|
||||
.n_misses(2)
|
||||
.hitresult_priority(HitResultPriority::BestCase)
|
||||
.generate_hitresults();
|
||||
|
||||
let expected = ManiaScoreState {
|
||||
n320: 2336,
|
||||
n300: 0,
|
||||
n200: 500,
|
||||
n100: 300,
|
||||
n50: 100,
|
||||
n_misses: 2,
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
state,
|
||||
expected,
|
||||
"{}% vs {}%",
|
||||
state.accuracy(),
|
||||
expected.accuracy()
|
||||
);
|
||||
assert_eq!(state.total_hits(), 3238);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hitresults_acc_n_misses_worst() {
|
||||
let (map, attrs) = test_data();
|
||||
|
||||
let state = ManiaPP::new(&map)
|
||||
.attributes(attrs)
|
||||
.accuracy(90.0)
|
||||
.n_misses(2)
|
||||
.hitresult_priority(HitResultPriority::WorstCase)
|
||||
.generate_hitresults();
|
||||
|
||||
let expected = ManiaScoreState {
|
||||
n320: 0,
|
||||
n300: 2849,
|
||||
n200: 0,
|
||||
n100: 4,
|
||||
n50: 383,
|
||||
n_misses: 2,
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
state,
|
||||
expected,
|
||||
"{}% vs {}%",
|
||||
state.accuracy(),
|
||||
expected.accuracy()
|
||||
);
|
||||
assert_eq!(state.total_hits(), 3238);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hitresults_n320_n_misses_best() {
|
||||
let (map, attrs) = test_data();
|
||||
|
||||
let state = ManiaPP::new(&map)
|
||||
.attributes(attrs)
|
||||
.n320(2000)
|
||||
.n_misses(2)
|
||||
.hitresult_priority(HitResultPriority::BestCase)
|
||||
.generate_hitresults();
|
||||
|
||||
let expected = ManiaScoreState {
|
||||
n320: 2000,
|
||||
n300: 1236,
|
||||
n200: 0,
|
||||
n100: 0,
|
||||
n50: 0,
|
||||
n_misses: 2,
|
||||
};
|
||||
|
||||
assert_eq!(state, expected);
|
||||
assert_eq!(state.total_hits(), 3238);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hitresults_n100_n50_n_misses_worst() {
|
||||
let (map, attrs) = test_data();
|
||||
|
||||
let state = ManiaPP::new(&map)
|
||||
.attributes(attrs)
|
||||
.n100(500)
|
||||
.n50(100)
|
||||
.n_misses(2)
|
||||
.hitresult_priority(HitResultPriority::WorstCase)
|
||||
.generate_hitresults();
|
||||
|
||||
let expected = ManiaScoreState {
|
||||
n320: 0,
|
||||
n300: 0,
|
||||
n200: 2636,
|
||||
n100: 500,
|
||||
n50: 100,
|
||||
n_misses: 2,
|
||||
};
|
||||
|
||||
assert_eq!(state, expected);
|
||||
assert_eq!(state.total_hits(), 3238);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user