fix: improve catch hitresult generation
This commit is contained in:
@@ -0,0 +1,14 @@
|
|||||||
|
# Seeds for failure cases proptest has generated in the past. It is
|
||||||
|
# automatically read and these particular cases re-run before any
|
||||||
|
# novel cases are generated.
|
||||||
|
#
|
||||||
|
# It is recommended to check this file in to source control so that
|
||||||
|
# everyone who runs the test benefits from these saved cases.
|
||||||
|
cc c75ef0e1156e1fb7c3eec623d7bb3ff00373825b76fe5a699be004eabfab596f # shrinks to acc = 0.0, n_fruits = None, n_droplets = None, n_tiny_droplets = None, n_tiny_droplet_misses = None, n_misses = None
|
||||||
|
cc 7bf5d798492945aadd68cf23a1d25c530171b4e396be8c7c8b7b5273e3fb5a75 # shrinks to acc = 0.0, n_fruits = Some(0), n_droplets = None, n_tiny_droplets = None, n_tiny_droplet_misses = None, n_misses = None
|
||||||
|
cc 3b895b7f5cf43539ab1536bb51a0212bc354b0de7959cd0f548c8eb19085f99c # shrinks to acc = 0.9572076425636069, n_fruits = None, n_droplets = None, n_tiny_droplets = None, n_tiny_droplet_misses = None, n_misses = Some(2)
|
||||||
|
cc fffc517d7d7aeadfd6e7e9514cdfe04239233e7fb9c35dca2a6fe47b6bb3c215 # shrinks to acc = 0.0, n_fruits = None, n_droplets = Some(0), n_tiny_droplets = None, n_tiny_droplet_misses = None, n_misses = None
|
||||||
|
cc fdeec58e1c568f52970c0a74a824ab6cf0300892af19f4bd3826f2af19a38d08 # shrinks to acc = 0.0, n_fruits = Some(10), n_droplets = None, n_tiny_droplets = None, n_tiny_droplet_misses = None, n_misses = Some(721)
|
||||||
|
cc dd03c40dec8e5efd5031fca53c2a1804e9537a3e864b0c21136169b8d411ec23 # shrinks to acc = 0.0, n_fruits = None, n_droplets = None, n_tiny_droplets = Some(0), n_tiny_droplet_misses = Some(0), n_misses = None
|
||||||
|
cc dd7bf3c50e5165f9e2f265336af23a38c78a0bfafd9b1dc54d32f50dfeb84c79 # shrinks to acc = 0.9407007157860147, n_fruits = Some(671), n_droplets = Some(0), n_tiny_droplets = None, n_tiny_droplet_misses = None, n_misses = Some(61)
|
||||||
|
cc 6e78f69d0a44ad58d5a2408cbadf7913cc3b77934b6c16eb1f41587fc151545d # shrinks to acc = 0.0, n_fruits = None, n_droplets = None, n_tiny_droplets = None, n_tiny_droplet_misses = Some(292), n_misses = None
|
||||||
+421
-289
@@ -1,5 +1,6 @@
|
|||||||
use super::{CatchDifficultyAttributes, CatchPerformanceAttributes, CatchScoreState, CatchStars};
|
use super::{CatchDifficultyAttributes, CatchPerformanceAttributes, CatchScoreState, CatchStars};
|
||||||
use crate::{Beatmap, DifficultyAttributes, Mods, OsuPP, PerformanceAttributes};
|
use crate::{Beatmap, DifficultyAttributes, Mods, OsuPP, PerformanceAttributes};
|
||||||
|
use std::cmp::Ordering;
|
||||||
|
|
||||||
/// Performance calculator on osu!catch maps.
|
/// Performance calculator on osu!catch maps.
|
||||||
///
|
///
|
||||||
@@ -33,18 +34,19 @@ use crate::{Beatmap, DifficultyAttributes, Mods, OsuPP, PerformanceAttributes};
|
|||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
#[allow(clippy::upper_case_acronyms)]
|
#[allow(clippy::upper_case_acronyms)]
|
||||||
pub struct CatchPP<'map> {
|
pub struct CatchPP<'map> {
|
||||||
map: &'map Beatmap,
|
pub(crate) map: &'map Beatmap,
|
||||||
attributes: Option<CatchDifficultyAttributes>,
|
pub(crate) attributes: Option<CatchDifficultyAttributes>,
|
||||||
mods: u32,
|
pub(crate) mods: u32,
|
||||||
combo: Option<usize>,
|
pub(crate) acc: Option<f64>,
|
||||||
|
pub(crate) combo: Option<usize>,
|
||||||
|
|
||||||
pub(crate) n_fruits: Option<usize>,
|
pub(crate) n_fruits: Option<usize>,
|
||||||
pub(crate) n_droplets: Option<usize>,
|
pub(crate) n_droplets: Option<usize>,
|
||||||
pub(crate) n_tiny_droplets: Option<usize>,
|
pub(crate) n_tiny_droplets: Option<usize>,
|
||||||
pub(crate) n_tiny_droplet_misses: Option<usize>,
|
pub(crate) n_tiny_droplet_misses: Option<usize>,
|
||||||
pub(crate) n_misses: Option<usize>,
|
pub(crate) n_misses: Option<usize>,
|
||||||
passed_objects: Option<usize>,
|
pub(crate) passed_objects: Option<usize>,
|
||||||
clock_rate: Option<f64>,
|
pub(crate) clock_rate: Option<f64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'map> CatchPP<'map> {
|
impl<'map> CatchPP<'map> {
|
||||||
@@ -55,6 +57,7 @@ impl<'map> CatchPP<'map> {
|
|||||||
map,
|
map,
|
||||||
attributes: None,
|
attributes: None,
|
||||||
mods: 0,
|
mods: 0,
|
||||||
|
acc: None,
|
||||||
combo: None,
|
combo: None,
|
||||||
|
|
||||||
n_fruits: None,
|
n_fruits: None,
|
||||||
@@ -73,7 +76,7 @@ impl<'map> CatchPP<'map> {
|
|||||||
#[inline]
|
#[inline]
|
||||||
pub fn attributes(mut self, attributes: impl CatchAttributeProvider) -> Self {
|
pub fn attributes(mut self, attributes: impl CatchAttributeProvider) -> Self {
|
||||||
if let Some(attributes) = attributes.attributes() {
|
if let Some(attributes) = attributes.attributes() {
|
||||||
self.attributes.replace(attributes);
|
self.attributes = Some(attributes);
|
||||||
}
|
}
|
||||||
|
|
||||||
self
|
self
|
||||||
@@ -92,7 +95,7 @@ impl<'map> CatchPP<'map> {
|
|||||||
/// Specify the max combo of the play.
|
/// Specify the max combo of the play.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn combo(mut self, combo: usize) -> Self {
|
pub fn combo(mut self, combo: usize) -> Self {
|
||||||
self.combo.replace(combo);
|
self.combo = Some(combo);
|
||||||
|
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
@@ -100,7 +103,7 @@ impl<'map> CatchPP<'map> {
|
|||||||
/// Specify the amount of fruits of a play i.e. n300.
|
/// Specify the amount of fruits of a play i.e. n300.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn fruits(mut self, n_fruits: usize) -> Self {
|
pub fn fruits(mut self, n_fruits: usize) -> Self {
|
||||||
self.n_fruits.replace(n_fruits);
|
self.n_fruits = Some(n_fruits);
|
||||||
|
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
@@ -108,7 +111,7 @@ impl<'map> CatchPP<'map> {
|
|||||||
/// Specify the amount of droplets of a play i.e. n100.
|
/// Specify the amount of droplets of a play i.e. n100.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn droplets(mut self, n_droplets: usize) -> Self {
|
pub fn droplets(mut self, n_droplets: usize) -> Self {
|
||||||
self.n_droplets.replace(n_droplets);
|
self.n_droplets = Some(n_droplets);
|
||||||
|
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
@@ -116,7 +119,7 @@ impl<'map> CatchPP<'map> {
|
|||||||
/// Specify the amount of tiny droplets of a play i.e. n50.
|
/// Specify the amount of tiny droplets of a play i.e. n50.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn tiny_droplets(mut self, n_tiny_droplets: usize) -> Self {
|
pub fn tiny_droplets(mut self, n_tiny_droplets: usize) -> Self {
|
||||||
self.n_tiny_droplets.replace(n_tiny_droplets);
|
self.n_tiny_droplets = Some(n_tiny_droplets);
|
||||||
|
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
@@ -124,7 +127,7 @@ impl<'map> CatchPP<'map> {
|
|||||||
/// Specify the amount of tiny droplet misses of a play i.e. n_katu.
|
/// Specify the amount of tiny droplet misses of a play i.e. n_katu.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn tiny_droplet_misses(mut self, n_tiny_droplet_misses: usize) -> Self {
|
pub fn tiny_droplet_misses(mut self, n_tiny_droplet_misses: usize) -> Self {
|
||||||
self.n_tiny_droplet_misses.replace(n_tiny_droplet_misses);
|
self.n_tiny_droplet_misses = Some(n_tiny_droplet_misses);
|
||||||
|
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
@@ -144,7 +147,7 @@ impl<'map> CatchPP<'map> {
|
|||||||
/// [`CatchGradualPerformanceAttributes`](crate::catch::CatchGradualPerformance).
|
/// [`CatchGradualPerformanceAttributes`](crate::catch::CatchGradualPerformance).
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn passed_objects(mut self, passed_objects: usize) -> Self {
|
pub fn passed_objects(mut self, passed_objects: usize) -> Self {
|
||||||
self.passed_objects.replace(passed_objects);
|
self.passed_objects = Some(passed_objects);
|
||||||
|
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
@@ -181,169 +184,192 @@ impl<'map> CatchPP<'map> {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: adjust this on the next rework
|
/// Specify the accuracy of a play between `0.0` and `100.0`.
|
||||||
/// Generate the hit results with respect to the given accuracy between `0.0` and `100.0`.
|
/// This will be used to generate matching hitresults.
|
||||||
///
|
#[inline]
|
||||||
/// Be sure to set `misses` beforehand! Also, if available, set `attributes` beforehand.
|
pub fn accuracy(mut self, acc: f64) -> Self {
|
||||||
pub fn accuracy(mut self, mut acc: f64) -> Self {
|
self.acc = Some(acc / 100.0);
|
||||||
if self.attributes.is_none() {
|
|
||||||
let mut calculator = CatchStars::new(self.map).mods(self.mods);
|
|
||||||
|
|
||||||
if let Some(passed_objects) = self.passed_objects {
|
|
||||||
calculator = calculator.passed_objects(passed_objects);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(clock_rate) = self.clock_rate {
|
|
||||||
calculator = calculator.clock_rate(clock_rate);
|
|
||||||
}
|
|
||||||
|
|
||||||
self.attributes = Some(calculator.calculate());
|
|
||||||
}
|
|
||||||
|
|
||||||
let attributes = self.attributes.as_ref().unwrap();
|
|
||||||
|
|
||||||
let n_droplets = self.n_droplets.unwrap_or_else(|| {
|
|
||||||
attributes
|
|
||||||
.n_droplets
|
|
||||||
.saturating_sub(self.n_misses.unwrap_or(0))
|
|
||||||
});
|
|
||||||
|
|
||||||
let max_combo = attributes.max_combo();
|
|
||||||
|
|
||||||
let n_fruits = self.n_fruits.unwrap_or_else(|| {
|
|
||||||
max_combo
|
|
||||||
.saturating_sub(self.n_misses.unwrap_or(0))
|
|
||||||
.saturating_sub(n_droplets)
|
|
||||||
});
|
|
||||||
|
|
||||||
let max_tiny_droplets = attributes.n_tiny_droplets;
|
|
||||||
acc /= 100.0;
|
|
||||||
|
|
||||||
let n_tiny_droplets = self.n_tiny_droplets.unwrap_or_else(|| {
|
|
||||||
((acc * (max_combo + max_tiny_droplets) as f64).round() as usize)
|
|
||||||
.saturating_sub(n_fruits)
|
|
||||||
.saturating_sub(n_droplets)
|
|
||||||
});
|
|
||||||
|
|
||||||
let n_tiny_droplet_misses = max_tiny_droplets.saturating_sub(n_tiny_droplets);
|
|
||||||
|
|
||||||
self.n_fruits.replace(n_fruits);
|
|
||||||
self.n_droplets.replace(n_droplets);
|
|
||||||
self.n_tiny_droplets.replace(n_tiny_droplets);
|
|
||||||
self.n_tiny_droplet_misses.replace(n_tiny_droplet_misses);
|
|
||||||
|
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
fn assert_hitresults(self, attributes: CatchDifficultyAttributes) -> CatchPPInner {
|
/// Create the hitresults that will be used for performance calculation.
|
||||||
let max_combo = attributes.max_combo();
|
pub fn generate_state(&mut self) -> CatchScoreState {
|
||||||
|
let attrs = match self.attributes {
|
||||||
|
Some(ref attrs) => attrs,
|
||||||
|
None => self.attributes.insert(self.generate_attributes()),
|
||||||
|
};
|
||||||
|
|
||||||
let correct_combo_hits = self
|
let n_misses = self
|
||||||
.n_fruits
|
.n_misses
|
||||||
.and_then(|f| self.n_droplets.map(|d| f + d + self.n_misses.unwrap_or(0)))
|
.map_or(0, |n| n.min(attrs.n_fruits + attrs.n_droplets));
|
||||||
.filter(|h| *h == max_combo);
|
|
||||||
|
|
||||||
let correct_fruits = self.n_fruits.filter(|f| {
|
let max_combo = self.combo.unwrap_or_else(|| attrs.max_combo() - n_misses);
|
||||||
*f >= attributes
|
|
||||||
.n_fruits
|
|
||||||
.saturating_sub(self.n_misses.unwrap_or(0))
|
|
||||||
});
|
|
||||||
|
|
||||||
let correct_droplets = self.n_droplets.filter(|d| {
|
let mut best_state = CatchScoreState {
|
||||||
*d >= attributes
|
max_combo,
|
||||||
.n_droplets
|
n_misses,
|
||||||
.saturating_sub(self.n_misses.unwrap_or(0))
|
..Default::default()
|
||||||
});
|
};
|
||||||
|
|
||||||
let correct_tinies = self
|
let mut best_dist = f64::INFINITY;
|
||||||
.n_tiny_droplets
|
|
||||||
.and_then(|t| self.n_tiny_droplet_misses.map(|m| t + m))
|
|
||||||
.filter(|h| *h == attributes.n_tiny_droplets);
|
|
||||||
|
|
||||||
if correct_combo_hits
|
let (n_fruits, n_droplets) = match (self.n_fruits, self.n_droplets) {
|
||||||
.and(correct_fruits)
|
(Some(mut n_fruits), Some(mut n_droplets)) => {
|
||||||
.and(correct_droplets)
|
let n_remaining = (attrs.n_fruits + attrs.n_droplets)
|
||||||
.and(correct_tinies)
|
.saturating_sub(n_fruits + n_droplets + n_misses);
|
||||||
.is_none()
|
|
||||||
{
|
|
||||||
let mut n_fruits = self.n_fruits.unwrap_or(0);
|
|
||||||
let mut n_droplets = self.n_droplets.unwrap_or(0);
|
|
||||||
let mut n_tiny_droplets = self.n_tiny_droplets.unwrap_or(0);
|
|
||||||
let n_tiny_droplet_misses = self.n_tiny_droplet_misses.unwrap_or(0);
|
|
||||||
|
|
||||||
let missing = max_combo
|
let new_droplets = n_remaining.min(attrs.n_droplets.saturating_sub(n_droplets));
|
||||||
.saturating_sub(n_fruits)
|
n_droplets += new_droplets;
|
||||||
.saturating_sub(n_droplets)
|
n_fruits += n_remaining - new_droplets;
|
||||||
.saturating_sub(self.n_misses.unwrap_or(0));
|
|
||||||
|
|
||||||
let missing_fruits =
|
n_fruits = n_fruits
|
||||||
missing.saturating_sub(attributes.n_droplets.saturating_sub(n_droplets));
|
.min((attrs.n_fruits + attrs.n_droplets).saturating_sub(n_droplets + n_misses));
|
||||||
|
n_droplets =
|
||||||
|
n_droplets.min(attrs.n_fruits + attrs.n_droplets - n_fruits - n_misses);
|
||||||
|
|
||||||
n_fruits += missing_fruits;
|
(n_fruits, n_droplets)
|
||||||
n_droplets += missing.saturating_sub(missing_fruits);
|
}
|
||||||
n_tiny_droplets += attributes
|
(Some(mut n_fruits), None) => {
|
||||||
|
let n_droplets = attrs.n_droplets.saturating_sub(
|
||||||
|
n_misses.saturating_sub(attrs.n_fruits.saturating_sub(n_fruits)),
|
||||||
|
);
|
||||||
|
|
||||||
|
n_fruits = attrs.n_fruits + attrs.n_droplets - n_misses - n_droplets;
|
||||||
|
|
||||||
|
(n_fruits, n_droplets)
|
||||||
|
}
|
||||||
|
(None, Some(mut n_droplets)) => {
|
||||||
|
let n_fruits = attrs.n_fruits.saturating_sub(
|
||||||
|
n_misses.saturating_sub(attrs.n_droplets.saturating_sub(n_droplets)),
|
||||||
|
);
|
||||||
|
|
||||||
|
n_droplets = attrs.n_fruits + attrs.n_droplets - n_misses - n_fruits;
|
||||||
|
|
||||||
|
(n_fruits, n_droplets)
|
||||||
|
}
|
||||||
|
(None, None) => {
|
||||||
|
let n_droplets = attrs.n_droplets.saturating_sub(n_misses);
|
||||||
|
let n_fruits =
|
||||||
|
attrs.n_fruits - (n_misses - (attrs.n_droplets.saturating_sub(n_droplets)));
|
||||||
|
|
||||||
|
(n_fruits, n_droplets)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
best_state.n_fruits = n_fruits;
|
||||||
|
best_state.n_droplets = n_droplets;
|
||||||
|
|
||||||
|
let mut find_best_tiny_droplets = |acc: f64| {
|
||||||
|
let raw_tiny_droplets = acc
|
||||||
|
* (attrs.n_fruits + attrs.n_droplets + attrs.n_tiny_droplets) as f64
|
||||||
|
- (n_fruits + n_droplets) as f64;
|
||||||
|
let min_tiny_droplets = attrs
|
||||||
.n_tiny_droplets
|
.n_tiny_droplets
|
||||||
.saturating_sub(n_tiny_droplets)
|
.min(raw_tiny_droplets.floor() as usize);
|
||||||
.saturating_sub(n_tiny_droplet_misses);
|
let max_tiny_droplets = attrs.n_tiny_droplets.min(raw_tiny_droplets.ceil() as usize);
|
||||||
|
|
||||||
return CatchPPInner {
|
for n_tiny_droplets in min_tiny_droplets..=max_tiny_droplets {
|
||||||
attributes,
|
let n_tiny_droplet_misses = attrs.n_tiny_droplets - n_tiny_droplets;
|
||||||
mods: self.mods,
|
|
||||||
combo: self.combo,
|
let curr_acc = accuracy(
|
||||||
n_fruits,
|
n_fruits,
|
||||||
n_droplets,
|
n_droplets,
|
||||||
n_tiny_droplets,
|
n_tiny_droplets,
|
||||||
n_tiny_droplet_misses,
|
n_tiny_droplet_misses,
|
||||||
n_misses: self.n_misses.unwrap_or(0),
|
n_misses,
|
||||||
};
|
);
|
||||||
|
let curr_dist = (acc - curr_acc).abs();
|
||||||
|
|
||||||
|
if curr_dist < best_dist {
|
||||||
|
best_dist = curr_dist;
|
||||||
|
best_state.n_tiny_droplets = n_tiny_droplets;
|
||||||
|
best_state.n_tiny_droplet_misses = n_tiny_droplet_misses;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match (self.n_tiny_droplets, self.n_tiny_droplet_misses) {
|
||||||
|
(Some(n_tiny_droplets), Some(n_tiny_droplet_misses)) => match self.acc {
|
||||||
|
Some(acc) => {
|
||||||
|
match (n_tiny_droplets + n_tiny_droplet_misses).cmp(&attrs.n_tiny_droplets) {
|
||||||
|
Ordering::Equal => {
|
||||||
|
best_state.n_tiny_droplets = n_tiny_droplets;
|
||||||
|
best_state.n_tiny_droplet_misses = n_tiny_droplet_misses;
|
||||||
|
}
|
||||||
|
Ordering::Less | Ordering::Greater => find_best_tiny_droplets(acc),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
let n_remaining = attrs
|
||||||
|
.n_tiny_droplets
|
||||||
|
.saturating_sub(n_tiny_droplets + n_tiny_droplet_misses);
|
||||||
|
|
||||||
|
best_state.n_tiny_droplets = n_tiny_droplets + n_remaining;
|
||||||
|
best_state.n_tiny_droplet_misses = n_tiny_droplet_misses;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
(Some(n_tiny_droplets), None) => {
|
||||||
|
best_state.n_tiny_droplets = attrs.n_tiny_droplets.min(n_tiny_droplets);
|
||||||
|
best_state.n_tiny_droplet_misses =
|
||||||
|
attrs.n_tiny_droplets.saturating_sub(n_tiny_droplets);
|
||||||
|
}
|
||||||
|
(None, Some(n_tiny_droplet_misses)) => {
|
||||||
|
best_state.n_tiny_droplets =
|
||||||
|
attrs.n_tiny_droplets.saturating_sub(n_tiny_droplet_misses);
|
||||||
|
best_state.n_tiny_droplet_misses = attrs.n_tiny_droplets.min(n_tiny_droplet_misses);
|
||||||
|
}
|
||||||
|
(None, None) => match self.acc {
|
||||||
|
Some(acc) => find_best_tiny_droplets(acc),
|
||||||
|
None => best_state.n_tiny_droplets = attrs.n_tiny_droplets,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
CatchPPInner {
|
best_state
|
||||||
attributes,
|
|
||||||
mods: self.mods,
|
|
||||||
combo: self.combo,
|
|
||||||
n_fruits: self.n_fruits.unwrap_or(0),
|
|
||||||
n_droplets: self.n_droplets.unwrap_or(0),
|
|
||||||
n_tiny_droplets: self.n_tiny_droplets.unwrap_or(0),
|
|
||||||
n_tiny_droplet_misses: self.n_tiny_droplet_misses.unwrap_or(0),
|
|
||||||
n_misses: self.n_misses.unwrap_or(0),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Calculate all performance related values, including pp and stars.
|
/// Calculate all performance related values, including pp and stars.
|
||||||
pub fn calculate(mut self) -> CatchPerformanceAttributes {
|
pub fn calculate(mut self) -> CatchPerformanceAttributes {
|
||||||
let attributes = self.attributes.take().unwrap_or_else(|| {
|
let state = self.generate_state();
|
||||||
let mut calculator = CatchStars::new(self.map).mods(self.mods);
|
|
||||||
|
|
||||||
if let Some(passed_objects) = self.passed_objects {
|
let attrs = self
|
||||||
calculator = calculator.passed_objects(passed_objects);
|
.attributes
|
||||||
}
|
.take()
|
||||||
|
.unwrap_or_else(|| self.generate_attributes());
|
||||||
|
|
||||||
if let Some(clock_rate) = self.clock_rate {
|
let inner = CatchPPInner {
|
||||||
calculator = calculator.clock_rate(clock_rate);
|
attrs,
|
||||||
}
|
mods: self.mods,
|
||||||
|
state,
|
||||||
|
};
|
||||||
|
|
||||||
calculator.calculate()
|
inner.calculate()
|
||||||
});
|
}
|
||||||
|
|
||||||
self.assert_hitresults(attributes).calculate()
|
fn generate_attributes(&self) -> CatchDifficultyAttributes {
|
||||||
|
let mut calculator = CatchStars::new(self.map).mods(self.mods);
|
||||||
|
|
||||||
|
if let Some(passed_objects) = self.passed_objects {
|
||||||
|
calculator = calculator.passed_objects(passed_objects);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(clock_rate) = self.clock_rate {
|
||||||
|
calculator = calculator.clock_rate(clock_rate);
|
||||||
|
}
|
||||||
|
|
||||||
|
calculator.calculate()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct CatchPPInner {
|
struct CatchPPInner {
|
||||||
attributes: CatchDifficultyAttributes,
|
attrs: CatchDifficultyAttributes,
|
||||||
mods: u32,
|
mods: u32,
|
||||||
combo: Option<usize>,
|
state: CatchScoreState,
|
||||||
n_fruits: usize,
|
|
||||||
n_droplets: usize,
|
|
||||||
n_tiny_droplets: usize,
|
|
||||||
n_tiny_droplet_misses: usize,
|
|
||||||
n_misses: usize,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CatchPPInner {
|
impl CatchPPInner {
|
||||||
fn calculate(self) -> CatchPerformanceAttributes {
|
fn calculate(self) -> CatchPerformanceAttributes {
|
||||||
let attributes = &self.attributes;
|
let attributes = &self.attrs;
|
||||||
let stars = attributes.stars;
|
let stars = attributes.stars;
|
||||||
let max_combo = attributes.max_combo();
|
let max_combo = attributes.max_combo();
|
||||||
|
|
||||||
@@ -364,11 +390,13 @@ impl CatchPPInner {
|
|||||||
pp *= len_bonus;
|
pp *= len_bonus;
|
||||||
|
|
||||||
// Penalize misses exponentially
|
// Penalize misses exponentially
|
||||||
pp *= 0.97_f64.powi(self.n_misses as i32);
|
pp *= 0.97_f64.powi(self.state.n_misses as i32);
|
||||||
|
|
||||||
// Combo scaling
|
// Combo scaling
|
||||||
if let Some(combo) = self.combo.filter(|_| max_combo > 0) {
|
if self.state.max_combo > 0 {
|
||||||
pp *= (combo as f64 / max_combo as f64).powf(0.8).min(1.0);
|
pp *= (self.state.max_combo as f64 / max_combo as f64)
|
||||||
|
.powf(0.8)
|
||||||
|
.min(1.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// AR scaling
|
// AR scaling
|
||||||
@@ -396,7 +424,7 @@ impl CatchPPInner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Accuracy scaling
|
// Accuracy scaling
|
||||||
pp *= self.acc().powf(5.5);
|
pp *= self.state.accuracy().powf(5.5);
|
||||||
|
|
||||||
// NF penalty
|
// NF penalty
|
||||||
if self.mods.nf() {
|
if self.mods.nf() {
|
||||||
@@ -404,35 +432,13 @@ impl CatchPPInner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
CatchPerformanceAttributes {
|
CatchPerformanceAttributes {
|
||||||
difficulty: self.attributes,
|
difficulty: self.attrs,
|
||||||
pp,
|
pp,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
|
||||||
fn combo_hits(&self) -> usize {
|
fn combo_hits(&self) -> usize {
|
||||||
self.n_fruits + self.n_droplets + self.n_misses
|
self.state.n_fruits + self.state.n_droplets + self.state.n_misses
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
fn successful_hits(&self) -> usize {
|
|
||||||
self.n_fruits + self.n_droplets + self.n_tiny_droplets
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
fn total_hits(&self) -> usize {
|
|
||||||
self.successful_hits() + self.n_tiny_droplet_misses + self.n_misses
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
fn acc(&self) -> f64 {
|
|
||||||
let total_hits = self.total_hits();
|
|
||||||
|
|
||||||
if total_hits == 0 {
|
|
||||||
1.0
|
|
||||||
} else {
|
|
||||||
(self.successful_hits() as f64 / total_hits as f64).clamp(0.0, 1.0)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -441,6 +447,7 @@ impl<'map> From<OsuPP<'map>> for CatchPP<'map> {
|
|||||||
fn from(osu: OsuPP<'map>) -> Self {
|
fn from(osu: OsuPP<'map>) -> Self {
|
||||||
let OsuPP {
|
let OsuPP {
|
||||||
map,
|
map,
|
||||||
|
attributes: _,
|
||||||
mods,
|
mods,
|
||||||
acc,
|
acc,
|
||||||
combo,
|
combo,
|
||||||
@@ -450,13 +457,14 @@ impl<'map> From<OsuPP<'map>> for CatchPP<'map> {
|
|||||||
n_misses,
|
n_misses,
|
||||||
passed_objects,
|
passed_objects,
|
||||||
clock_rate,
|
clock_rate,
|
||||||
..
|
hitresult_priority: _,
|
||||||
} = osu;
|
} = osu;
|
||||||
|
|
||||||
let res = Self {
|
Self {
|
||||||
map,
|
map,
|
||||||
attributes: None,
|
attributes: None,
|
||||||
mods,
|
mods,
|
||||||
|
acc,
|
||||||
combo,
|
combo,
|
||||||
n_fruits: n300,
|
n_fruits: n300,
|
||||||
n_droplets: n100,
|
n_droplets: n100,
|
||||||
@@ -465,15 +473,23 @@ impl<'map> From<OsuPP<'map>> for CatchPP<'map> {
|
|||||||
n_misses,
|
n_misses,
|
||||||
passed_objects,
|
passed_objects,
|
||||||
clock_rate,
|
clock_rate,
|
||||||
};
|
|
||||||
|
|
||||||
match acc {
|
|
||||||
Some(acc) => res.accuracy(acc),
|
|
||||||
None => res,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn accuracy(
|
||||||
|
n_fruits: usize,
|
||||||
|
n_droplets: usize,
|
||||||
|
n_tiny_droplets: usize,
|
||||||
|
n_tiny_droplet_misses: usize,
|
||||||
|
n_misses: usize,
|
||||||
|
) -> f64 {
|
||||||
|
let numerator = n_fruits + n_droplets + n_tiny_droplets;
|
||||||
|
let denominator = numerator + n_tiny_droplet_misses + n_misses;
|
||||||
|
|
||||||
|
numerator as f64 / denominator as f64
|
||||||
|
}
|
||||||
|
|
||||||
/// Abstract type to provide flexibility when passing difficulty attributes to a performance calculation.
|
/// Abstract type to provide flexibility when passing difficulty attributes to a performance calculation.
|
||||||
pub trait CatchAttributeProvider {
|
pub trait CatchAttributeProvider {
|
||||||
/// Provide the actual difficulty attributes.
|
/// Provide the actual difficulty attributes.
|
||||||
@@ -518,130 +534,246 @@ impl CatchAttributeProvider for PerformanceAttributes {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(any(feature = "async_tokio", feature = "async_std")))]
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::Beatmap;
|
use crate::Beatmap;
|
||||||
|
use proptest::{option, prelude::*};
|
||||||
|
use std::sync::OnceLock;
|
||||||
|
|
||||||
fn attributes() -> CatchDifficultyAttributes {
|
static DATA: OnceLock<(Beatmap, CatchDifficultyAttributes)> = OnceLock::new();
|
||||||
CatchDifficultyAttributes {
|
|
||||||
n_fruits: 1234,
|
const N_FRUITS: usize = 728;
|
||||||
n_droplets: 567,
|
const N_DROPLETS: usize = 2;
|
||||||
n_tiny_droplets: 2345,
|
const N_TINY_DROPLETS: usize = 291;
|
||||||
|
|
||||||
|
fn test_data() -> (&'static Beatmap, CatchDifficultyAttributes) {
|
||||||
|
let (map, attrs) = DATA.get_or_init(|| {
|
||||||
|
let path = "./maps/2118524.osu";
|
||||||
|
let map = Beatmap::from_path(path).unwrap();
|
||||||
|
|
||||||
|
let attrs = CatchStars::new(&map).calculate();
|
||||||
|
|
||||||
|
let expected = CatchDifficultyAttributes {
|
||||||
|
stars: 3.2502669316166624,
|
||||||
|
ar: 8.0,
|
||||||
|
n_fruits: 728,
|
||||||
|
n_droplets: 2,
|
||||||
|
n_tiny_droplets: 291,
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(attrs, expected);
|
||||||
|
assert_eq!(N_FRUITS, attrs.n_fruits);
|
||||||
|
assert_eq!(N_DROPLETS, attrs.n_droplets);
|
||||||
|
assert_eq!(N_TINY_DROPLETS, attrs.n_tiny_droplets);
|
||||||
|
|
||||||
|
(map, attrs)
|
||||||
|
});
|
||||||
|
|
||||||
|
(map, attrs.to_owned())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn catch_custom() {
|
||||||
|
const ACC: f64 = 0.9407007157860147;
|
||||||
|
|
||||||
|
let (map, attrs) = test_data();
|
||||||
|
|
||||||
|
let calc = CatchPP::new(&map)
|
||||||
|
.attributes(attrs)
|
||||||
|
.accuracy(ACC * 100.0)
|
||||||
|
.fruits(671)
|
||||||
|
.droplets(0)
|
||||||
|
.misses(61)
|
||||||
|
.generate_state();
|
||||||
|
|
||||||
|
let bf = brute_force_best(ACC, Some(671), Some(0), None, None, 61);
|
||||||
|
|
||||||
|
assert_eq!(calc, bf);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks all remaining hitresult combinations w.r.t. the given parameters
|
||||||
|
/// and returns the [`OsuScoreState`] that matches `acc` the best.
|
||||||
|
///
|
||||||
|
/// Very slow but accurate.
|
||||||
|
fn brute_force_best(
|
||||||
|
acc: f64,
|
||||||
|
n_fruits: Option<usize>,
|
||||||
|
n_droplets: Option<usize>,
|
||||||
|
n_tiny_droplets: Option<usize>,
|
||||||
|
n_tiny_droplet_misses: Option<usize>,
|
||||||
|
n_misses: usize,
|
||||||
|
) -> CatchScoreState {
|
||||||
|
let n_misses = n_misses.min(N_FRUITS + N_DROPLETS);
|
||||||
|
|
||||||
|
let mut best_state = CatchScoreState {
|
||||||
|
max_combo: N_FRUITS + N_DROPLETS - n_misses,
|
||||||
|
n_misses,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut best_dist = f64::INFINITY;
|
||||||
|
|
||||||
|
let (new_fruits, new_droplets) = match (n_fruits, n_droplets) {
|
||||||
|
(Some(mut n_fruits), Some(mut n_droplets)) => {
|
||||||
|
let n_remaining =
|
||||||
|
(N_FRUITS + N_DROPLETS).saturating_sub(n_fruits + n_droplets + n_misses);
|
||||||
|
|
||||||
|
let new_droplets = n_remaining.min(N_DROPLETS.saturating_sub(n_droplets));
|
||||||
|
n_droplets += new_droplets;
|
||||||
|
n_fruits += n_remaining - new_droplets;
|
||||||
|
|
||||||
|
n_fruits =
|
||||||
|
n_fruits.min((N_FRUITS + N_DROPLETS).saturating_sub(n_droplets + n_misses));
|
||||||
|
n_droplets = n_droplets.min(N_FRUITS + N_DROPLETS - n_fruits - n_misses);
|
||||||
|
|
||||||
|
(n_fruits, n_droplets)
|
||||||
|
}
|
||||||
|
(Some(mut n_fruits), None) => {
|
||||||
|
let n_droplets = N_DROPLETS
|
||||||
|
.saturating_sub(n_misses.saturating_sub(N_FRUITS.saturating_sub(n_fruits)));
|
||||||
|
n_fruits = N_FRUITS + N_DROPLETS - n_misses - n_droplets;
|
||||||
|
|
||||||
|
(n_fruits, n_droplets)
|
||||||
|
}
|
||||||
|
(None, Some(mut n_droplets)) => {
|
||||||
|
let n_fruits = N_FRUITS
|
||||||
|
.saturating_sub(n_misses.saturating_sub(N_DROPLETS.saturating_sub(n_droplets)));
|
||||||
|
n_droplets = N_FRUITS + N_DROPLETS - n_misses - n_fruits;
|
||||||
|
|
||||||
|
(n_fruits, n_droplets)
|
||||||
|
}
|
||||||
|
(None, None) => {
|
||||||
|
let n_droplets = N_DROPLETS.saturating_sub(n_misses);
|
||||||
|
let n_fruits = N_FRUITS - (n_misses - (N_DROPLETS.saturating_sub(n_droplets)));
|
||||||
|
|
||||||
|
(n_fruits, n_droplets)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
best_state.n_fruits = new_fruits;
|
||||||
|
best_state.n_droplets = new_droplets;
|
||||||
|
|
||||||
|
let (min_tiny_droplets, max_tiny_droplets) = match (n_tiny_droplets, n_tiny_droplet_misses)
|
||||||
|
{
|
||||||
|
(Some(n_tiny_droplets), Some(n_tiny_droplet_misses)) => {
|
||||||
|
match (n_tiny_droplets + n_tiny_droplet_misses).cmp(&N_TINY_DROPLETS) {
|
||||||
|
Ordering::Equal => (
|
||||||
|
N_TINY_DROPLETS.min(n_tiny_droplets),
|
||||||
|
N_TINY_DROPLETS.min(n_tiny_droplets),
|
||||||
|
),
|
||||||
|
Ordering::Less | Ordering::Greater => (0, N_TINY_DROPLETS),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(Some(n_tiny_droplets), None) => (
|
||||||
|
N_TINY_DROPLETS.min(n_tiny_droplets),
|
||||||
|
N_TINY_DROPLETS.min(n_tiny_droplets),
|
||||||
|
),
|
||||||
|
(None, Some(n_tiny_droplet_misses)) => (
|
||||||
|
N_TINY_DROPLETS.saturating_sub(n_tiny_droplet_misses),
|
||||||
|
N_TINY_DROPLETS.saturating_sub(n_tiny_droplet_misses),
|
||||||
|
),
|
||||||
|
(None, None) => (0, N_TINY_DROPLETS),
|
||||||
|
};
|
||||||
|
|
||||||
|
for new_tiny_droplets in min_tiny_droplets..=max_tiny_droplets {
|
||||||
|
let new_tiny_droplet_misses = N_TINY_DROPLETS - new_tiny_droplets;
|
||||||
|
|
||||||
|
let curr_acc = accuracy(
|
||||||
|
new_fruits,
|
||||||
|
new_droplets,
|
||||||
|
new_tiny_droplets,
|
||||||
|
new_tiny_droplet_misses,
|
||||||
|
n_misses,
|
||||||
|
);
|
||||||
|
|
||||||
|
let curr_dist = (acc - curr_acc).abs();
|
||||||
|
|
||||||
|
if curr_dist < best_dist {
|
||||||
|
best_dist = curr_dist;
|
||||||
|
best_state.n_tiny_droplets = new_tiny_droplets;
|
||||||
|
best_state.n_tiny_droplet_misses = new_tiny_droplet_misses;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
best_state
|
||||||
|
}
|
||||||
|
|
||||||
|
proptest! {
|
||||||
|
#![proptest_config(ProptestConfig::with_cases(20_000))]
|
||||||
|
#[test]
|
||||||
|
fn catch_hitresults(
|
||||||
|
acc in 0.0..=1.0,
|
||||||
|
n_fruits in option::weighted(0.10, 0_usize..=N_FRUITS + 10),
|
||||||
|
n_droplets in option::weighted(0.10, 0_usize..=N_DROPLETS + 10),
|
||||||
|
n_tiny_droplets in option::weighted(0.10, 0_usize..=N_TINY_DROPLETS + 10),
|
||||||
|
n_tiny_droplet_misses in option::weighted(0.10, 0_usize..=N_TINY_DROPLETS + 10),
|
||||||
|
n_misses in option::weighted(0.15, 0_usize..=N_FRUITS + N_DROPLETS + 10),
|
||||||
|
) {
|
||||||
|
let (map, attrs) = test_data();
|
||||||
|
|
||||||
|
let mut state = CatchPP::new(map)
|
||||||
|
.attributes(attrs)
|
||||||
|
.accuracy(acc * 100.0);
|
||||||
|
|
||||||
|
if let Some(n_fruits) = n_fruits {
|
||||||
|
state = state.fruits(n_fruits);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(n_droplets) = n_droplets {
|
||||||
|
state = state.droplets(n_droplets);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(n_tiny_droplets) = n_tiny_droplets {
|
||||||
|
state = state.tiny_droplets(n_tiny_droplets);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(n_tiny_droplet_misses) = n_tiny_droplet_misses {
|
||||||
|
state = state.tiny_droplet_misses(n_tiny_droplet_misses);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(n_misses) = n_misses {
|
||||||
|
state = state.misses(n_misses);
|
||||||
|
}
|
||||||
|
|
||||||
|
let hitresults = state.generate_hitresults();
|
||||||
|
|
||||||
|
let expected = brute_force_best(
|
||||||
|
acc,
|
||||||
|
n_fruits,
|
||||||
|
n_droplets,
|
||||||
|
n_tiny_droplets,
|
||||||
|
n_tiny_droplet_misses,
|
||||||
|
n_misses.unwrap_or(0),
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(hitresults, expected);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn fruits_only_accuracy() {
|
|
||||||
let map = Beatmap::default();
|
|
||||||
let attributes = attributes();
|
|
||||||
|
|
||||||
let total_objects = attributes.n_fruits + attributes.n_droplets;
|
|
||||||
let target_acc = 97.5;
|
|
||||||
|
|
||||||
let calculator = CatchPP::new(&map)
|
|
||||||
.attributes(attributes)
|
|
||||||
.passed_objects(total_objects)
|
|
||||||
.accuracy(target_acc);
|
|
||||||
|
|
||||||
let numerator = calculator.n_fruits.unwrap_or(0)
|
|
||||||
+ calculator.n_droplets.unwrap_or(0)
|
|
||||||
+ calculator.n_tiny_droplets.unwrap_or(0);
|
|
||||||
let denominator = numerator
|
|
||||||
+ calculator.n_tiny_droplet_misses.unwrap_or(0)
|
|
||||||
+ calculator.n_misses.unwrap_or(0);
|
|
||||||
let acc = 100.0 * numerator as f64 / denominator as f64;
|
|
||||||
|
|
||||||
assert!(
|
|
||||||
(target_acc - acc).abs() < 1.0,
|
|
||||||
"Expected: {target_acc} | Actual: {acc}",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn fruits_accuracy_droplets_and_tiny_droplets() {
|
|
||||||
let map = Beatmap::default();
|
|
||||||
let attributes = attributes();
|
|
||||||
|
|
||||||
let total_objects = attributes.n_fruits + attributes.n_droplets;
|
|
||||||
let target_acc = 97.5;
|
|
||||||
let n_droplets = 550;
|
|
||||||
let n_tiny_droplets = 2222;
|
|
||||||
|
|
||||||
let calculator = CatchPP::new(&map)
|
|
||||||
.attributes(attributes)
|
|
||||||
.passed_objects(total_objects)
|
|
||||||
.droplets(n_droplets)
|
|
||||||
.tiny_droplets(n_tiny_droplets)
|
|
||||||
.accuracy(target_acc);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
n_droplets,
|
|
||||||
calculator.n_droplets.unwrap(),
|
|
||||||
"Expected: {} | Actual: {}",
|
|
||||||
n_droplets,
|
|
||||||
calculator.n_droplets.unwrap()
|
|
||||||
);
|
|
||||||
|
|
||||||
let numerator = calculator.n_fruits.unwrap_or(0)
|
|
||||||
+ calculator.n_droplets.unwrap_or(0)
|
|
||||||
+ calculator.n_tiny_droplets.unwrap_or(0);
|
|
||||||
let denominator = numerator
|
|
||||||
+ calculator.n_tiny_droplet_misses.unwrap_or(0)
|
|
||||||
+ calculator.n_misses.unwrap_or(0);
|
|
||||||
let acc = 100.0 * numerator as f64 / denominator as f64;
|
|
||||||
|
|
||||||
assert!(
|
|
||||||
(target_acc - acc).abs() < 1.0,
|
|
||||||
"Expected: {target_acc} | Actual: {acc}",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn fruits_missing_objects() {
|
fn fruits_missing_objects() {
|
||||||
let map = Beatmap::default();
|
let (map, attrs) = test_data();
|
||||||
let attributes = attributes();
|
|
||||||
|
|
||||||
let total_objects = attributes.n_fruits + attributes.n_droplets;
|
let state = CatchPP::new(&map)
|
||||||
let n_fruits = attributes.n_fruits - 10;
|
.attributes(attrs)
|
||||||
let n_droplets = attributes.n_droplets - 5;
|
.fruits(N_FRUITS - 10)
|
||||||
let n_tiny_droplets = attributes.n_tiny_droplets - 50;
|
.droplets(N_DROPLETS - 1)
|
||||||
let n_tiny_droplet_misses = 20;
|
.tiny_droplets(N_TINY_DROPLETS - 50)
|
||||||
let n_misses = 2;
|
.tiny_droplet_misses(20)
|
||||||
|
.misses(2)
|
||||||
|
.generate_state();
|
||||||
|
|
||||||
let calculator = CatchPP::new(&map)
|
let expected = CatchScoreState {
|
||||||
.attributes(attributes.clone())
|
max_combo: N_FRUITS + N_DROPLETS - 2,
|
||||||
.passed_objects(total_objects)
|
n_fruits: N_FRUITS - 2,
|
||||||
.fruits(n_fruits)
|
n_droplets: N_DROPLETS,
|
||||||
.droplets(n_droplets)
|
n_tiny_droplets: N_TINY_DROPLETS - 20,
|
||||||
.tiny_droplets(n_tiny_droplets)
|
n_tiny_droplet_misses: 20,
|
||||||
.tiny_droplet_misses(n_tiny_droplet_misses)
|
n_misses: 2,
|
||||||
.misses(n_misses)
|
};
|
||||||
.assert_hitresults(attributes.clone());
|
|
||||||
|
|
||||||
assert!(
|
assert_eq!(state, expected);
|
||||||
(attributes.n_fruits as i32 - calculator.n_fruits as i32).abs() <= n_misses as i32,
|
|
||||||
"Expected: {} | Actual: {} [+/- {} misses]",
|
|
||||||
attributes.n_fruits,
|
|
||||||
calculator.n_fruits,
|
|
||||||
n_misses
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
attributes.n_droplets,
|
|
||||||
calculator.n_droplets - (n_misses - (attributes.n_fruits - calculator.n_fruits)),
|
|
||||||
"Expected: {} | Actual: {}",
|
|
||||||
attributes.n_droplets,
|
|
||||||
calculator.n_droplets - (n_misses - (attributes.n_fruits - calculator.n_fruits)),
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
attributes.n_tiny_droplets,
|
|
||||||
calculator.n_tiny_droplets + calculator.n_tiny_droplet_misses,
|
|
||||||
"Expected: {} | Actual: {}",
|
|
||||||
attributes.n_tiny_droplets,
|
|
||||||
calculator.n_tiny_droplets + calculator.n_tiny_droplet_misses,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,4 +24,29 @@ impl CatchScoreState {
|
|||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self::default()
|
Self::default()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Return the total amount of hits by adding everything up.
|
||||||
|
#[inline]
|
||||||
|
pub fn total_hits(&self) -> usize {
|
||||||
|
self.n_fruits
|
||||||
|
+ self.n_droplets
|
||||||
|
+ self.n_tiny_droplets
|
||||||
|
+ self.n_tiny_droplet_misses
|
||||||
|
+ 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 = self.n_fruits + self.n_droplets + self.n_tiny_droplets;
|
||||||
|
let denominator = total_hits;
|
||||||
|
|
||||||
|
numerator as f64 / denominator as f64
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -206,7 +206,7 @@ impl<'map> AnyPP<'map> {
|
|||||||
match self {
|
match self {
|
||||||
Self::Osu(o) => Self::Osu(o.hitresult_priority(priority)),
|
Self::Osu(o) => Self::Osu(o.hitresult_priority(priority)),
|
||||||
Self::Taiko(t) => Self::Taiko(t.hitresult_priority(priority)),
|
Self::Taiko(t) => Self::Taiko(t.hitresult_priority(priority)),
|
||||||
Self::Catch(_) => self, // FIXME: update when ctb hitresult generation is updated
|
Self::Catch(_) => self,
|
||||||
Self::Mania(m) => Self::Mania(m.hitresult_priority(priority)),
|
Self::Mania(m) => Self::Mania(m.hitresult_priority(priority)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user